Skip to content

Commit 889f0f2

Browse files
authored
Merge pull request #2624 from rintaro/copied-file-map
Factor out CopiedFileMap as a Sendable value type
2 parents ec0080e + 3d7ebf7 commit 889f0f2

3 files changed

Lines changed: 223 additions & 183 deletions

File tree

Sources/BuildServerIntegration/BuildServerManager.swift

Lines changed: 168 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,165 @@ private extension BuildServerSpec {
346346
}
347347
}
348348

349+
/// Maps build-directory copies of source files back to their original source URIs.
350+
///
351+
/// During target preparation the build system may copy source files into the build directory
352+
/// (recorded as `copyDestinations` in `SourceFileInfo`). Index records and diagnostics can
353+
/// therefore reference the copy rather than the original. `CopiedFileMap` holds the reverse
354+
/// mapping — copy URI → original source URI — so that the server can translate those locations
355+
/// back to the files the user is actually editing before returning them to the client.
356+
package struct CopiedFileMap: Sendable {
357+
private var map: [DocumentURI: DocumentURI]
358+
359+
init(map: [DocumentURI: DocumentURI]) {
360+
self.map = map
361+
}
362+
363+
/// Returns `true` if `uri` is a copy of a source file created during target preparation.
364+
package func isCopiedFile(_ uri: DocumentURI) -> Bool {
365+
return map[uri] != nil
366+
}
367+
368+
/// Returns the original source URI for the given copied file URI, or `nil` if the URI is not a known copy.
369+
package func originalURI(for uri: DocumentURI) -> DocumentURI? {
370+
return map[uri]
371+
}
372+
373+
/// Check if the URI referenced by `location` has been copied during the preparation phase. If so, adjust the URI to
374+
/// the original source file.
375+
package func locationAdjustedForCopiedFiles(_ location: Location) -> Location {
376+
guard let originalUri = map[location.uri] else {
377+
return location
378+
}
379+
380+
if let fileUrl = originalUri.fileURL, !FileManager.default.fileExists(at: fileUrl) {
381+
return location
382+
}
383+
// If we regularly get issues that the copied file is out-of-sync with its original, we can check that the contents
384+
// of the lines touched by the location match and only return the original URI if they do. For now, we avoid this
385+
// check due to its performance cost of reading files from disk.
386+
return Location(uri: originalUri, range: location.range)
387+
}
388+
389+
/// Check if the URI referenced by `location` has been copied during the preparation phase. If so, adjust the URI to
390+
/// the original source file.
391+
package func locationsAdjustedForCopiedFiles(_ locations: [Location]) -> [Location] {
392+
return locations.map { locationAdjustedForCopiedFiles($0) }
393+
}
394+
395+
private func uriAdjustedForCopiedFiles(_ uri: DocumentURI) -> DocumentURI {
396+
guard let originalUri = map[uri] else {
397+
return uri
398+
}
399+
return originalUri
400+
}
401+
402+
package func workspaceEditAdjustedForCopiedFiles(_ workspaceEdit: WorkspaceEdit?) -> WorkspaceEdit? {
403+
guard var edit = workspaceEdit else {
404+
return nil
405+
}
406+
if let changes = edit.changes {
407+
var newChanges: [DocumentURI: [TextEdit]] = [:]
408+
for (uri, edits) in changes {
409+
let newUri = self.uriAdjustedForCopiedFiles(uri)
410+
newChanges[newUri, default: []] += edits
411+
}
412+
edit.changes = newChanges
413+
}
414+
if let documentChanges = edit.documentChanges {
415+
edit.documentChanges = documentChanges.map { change in
416+
switch change {
417+
case .textDocumentEdit(var textEdit):
418+
textEdit.textDocument.uri = self.uriAdjustedForCopiedFiles(textEdit.textDocument.uri)
419+
return .textDocumentEdit(textEdit)
420+
case .createFile(var create):
421+
create.uri = self.uriAdjustedForCopiedFiles(create.uri)
422+
return .createFile(create)
423+
case .renameFile(var rename):
424+
rename.oldUri = self.uriAdjustedForCopiedFiles(rename.oldUri)
425+
rename.newUri = self.uriAdjustedForCopiedFiles(rename.newUri)
426+
return .renameFile(rename)
427+
case .deleteFile(var delete):
428+
delete.uri = self.uriAdjustedForCopiedFiles(delete.uri)
429+
return .deleteFile(delete)
430+
}
431+
}
432+
}
433+
return edit
434+
}
435+
436+
package func locationsOrLocationLinksAdjustedForCopiedFiles(
437+
_ response: LocationsOrLocationLinksResponse?
438+
) -> LocationsOrLocationLinksResponse? {
439+
guard let response = response else {
440+
return nil
441+
}
442+
switch response {
443+
case .locations(let locations):
444+
let remappedLocations = self.locationsAdjustedForCopiedFiles(locations)
445+
return .locations(remappedLocations)
446+
case .locationLinks(let locationLinks):
447+
let remappedLinks = locationLinks.map { link -> LocationLink in
448+
let adjustedTargetLocation = self.locationAdjustedForCopiedFiles(
449+
Location(uri: link.targetUri, range: link.targetRange)
450+
)
451+
let adjustedTargetSelectionLocation = self.locationAdjustedForCopiedFiles(
452+
Location(uri: link.targetUri, range: link.targetSelectionRange)
453+
)
454+
return LocationLink(
455+
originSelectionRange: link.originSelectionRange,
456+
targetUri: adjustedTargetLocation.uri,
457+
targetRange: adjustedTargetLocation.range,
458+
targetSelectionRange: adjustedTargetSelectionLocation.range
459+
)
460+
}
461+
return .locationLinks(remappedLinks)
462+
}
463+
}
464+
465+
package func typeHierarchyItemAdjustedForCopiedFiles(_ item: TypeHierarchyItem) -> TypeHierarchyItem {
466+
let adjustedLocation = self.locationAdjustedForCopiedFiles(Location(uri: item.uri, range: item.range))
467+
let adjustedSelectionLocation = self.locationAdjustedForCopiedFiles(
468+
Location(uri: item.uri, range: item.selectionRange)
469+
)
470+
return TypeHierarchyItem(
471+
name: item.name,
472+
kind: item.kind,
473+
tags: item.tags,
474+
detail: item.detail,
475+
uri: adjustedLocation.uri,
476+
range: adjustedLocation.range,
477+
selectionRange: adjustedSelectionLocation.range,
478+
data: item.data
479+
)
480+
}
481+
482+
package func callHierarchyItemAdjustedForCopiedFiles(_ item: CallHierarchyItem) -> CallHierarchyItem {
483+
let adjustedLocation = self.locationAdjustedForCopiedFiles(Location(uri: item.uri, range: item.range))
484+
let adjustedSelectionLocation = self.locationAdjustedForCopiedFiles(
485+
Location(uri: item.uri, range: item.selectionRange)
486+
)
487+
return CallHierarchyItem(
488+
name: item.name,
489+
kind: item.kind,
490+
tags: item.tags,
491+
detail: item.detail,
492+
uri: adjustedLocation.uri,
493+
range: adjustedLocation.range,
494+
selectionRange: adjustedSelectionLocation.range,
495+
data: .dictionary([
496+
"usr": item.data.flatMap { data in
497+
if case let .dictionary(dict) = data {
498+
return dict["usr"]
499+
}
500+
return nil
501+
} ?? .null,
502+
"uri": .string(adjustedLocation.uri.stringValue),
503+
])
504+
)
505+
}
506+
}
507+
349508
/// Entry point for all build server queries.
350509
package actor BuildServerManager: QueueBasedMessageHandler {
351510
package let messageHandlingHelper = QueueBasedMessageHandlerHelper(
@@ -504,14 +663,14 @@ package actor BuildServerManager: QueueBasedMessageHandler {
504663
private let cachedSourceFilesAndDirectories = Cache<SourceFilesAndDirectoriesKey, SourceFilesAndDirectories>()
505664

506665
/// Task that computes the latest map of copied file URIs to their original source locations.
507-
private var copiedFileMap: Task<[DocumentURI: DocumentURI], Never>?
666+
private var copiedFileMap: Task<CopiedFileMap, Never>?
508667

509668
/// The last computed copied file map, which may be out-of-date.
510669
///
511670
/// Even with out-of-date information for the copied file map, we can provide reasonable functionality - in the worst
512671
/// case we jump to a file in the build directory instead of the source directory. We don't want to block requests
513672
/// like definition on receiving up-to-date build target information from the build server.
514-
private var cachedCopiedFileMap: [DocumentURI: DocumentURI] = [:]
673+
package private(set) var cachedCopiedFileMap: CopiedFileMap = CopiedFileMap(map: [:])
515674

516675
/// The `SourceKitInitializeBuildResponseData` received from the `build/initialize` request, if any.
517676
package var initializationData: SourceKitInitializeBuildResponseData? {
@@ -980,142 +1139,9 @@ package actor BuildServerManager: QueueBasedMessageHandler {
9801139
}
9811140
}
9821141

983-
/// Check if the URI referenced by `location` has been copied during the preparation phase. If so, adjust the URI to
984-
/// the original source file.
985-
package func locationAdjustedForCopiedFiles(_ location: Location) -> Location {
986-
guard let originalUri = cachedCopiedFileMap[location.uri] else {
987-
return location
988-
}
989-
if let fileUrl = originalUri.fileURL, !FileManager.default.fileExists(at: fileUrl) {
990-
return location
991-
}
992-
// If we regularly get issues that the copied file is out-of-sync with its original, we can check that the contents
993-
// of the lines touched by the location match and only return the original URI if they do. For now, we avoid this
994-
// check due to its performance cost of reading files from disk.
995-
return Location(uri: originalUri, range: location.range)
996-
}
997-
998-
/// Check if the URI referenced by `location` has been copied during the preparation phase. If so, adjust the URI to
999-
/// the original source file.
1000-
package func locationsAdjustedForCopiedFiles(_ locations: [Location]) -> [Location] {
1001-
return locations.map { locationAdjustedForCopiedFiles($0) }
1002-
}
1003-
1004-
private func uriAdjustedForCopiedFiles(_ uri: DocumentURI) -> DocumentURI {
1005-
guard let originalUri = cachedCopiedFileMap[uri] else {
1006-
return uri
1007-
}
1008-
return originalUri
1009-
}
1010-
1011-
package func workspaceEditAdjustedForCopiedFiles(_ workspaceEdit: WorkspaceEdit?) -> WorkspaceEdit? {
1012-
guard var edit = workspaceEdit else {
1013-
return nil
1014-
}
1015-
if let changes = edit.changes {
1016-
var newChanges: [DocumentURI: [TextEdit]] = [:]
1017-
for (uri, edits) in changes {
1018-
let newUri = self.uriAdjustedForCopiedFiles(uri)
1019-
newChanges[newUri, default: []] += edits
1020-
}
1021-
edit.changes = newChanges
1022-
}
1023-
if let documentChanges = edit.documentChanges {
1024-
edit.documentChanges = documentChanges.map { change in
1025-
switch change {
1026-
case .textDocumentEdit(var textEdit):
1027-
textEdit.textDocument.uri = self.uriAdjustedForCopiedFiles(textEdit.textDocument.uri)
1028-
return .textDocumentEdit(textEdit)
1029-
case .createFile(var create):
1030-
create.uri = self.uriAdjustedForCopiedFiles(create.uri)
1031-
return .createFile(create)
1032-
case .renameFile(var rename):
1033-
rename.oldUri = self.uriAdjustedForCopiedFiles(rename.oldUri)
1034-
rename.newUri = self.uriAdjustedForCopiedFiles(rename.newUri)
1035-
return .renameFile(rename)
1036-
case .deleteFile(var delete):
1037-
delete.uri = self.uriAdjustedForCopiedFiles(delete.uri)
1038-
return .deleteFile(delete)
1039-
}
1040-
}
1041-
}
1042-
return edit
1043-
}
1044-
1045-
package func locationsOrLocationLinksAdjustedForCopiedFiles(
1046-
_ response: LocationsOrLocationLinksResponse?
1047-
) -> LocationsOrLocationLinksResponse? {
1048-
guard let response = response else {
1049-
return nil
1050-
}
1051-
switch response {
1052-
case .locations(let locations):
1053-
let remappedLocations = self.locationsAdjustedForCopiedFiles(locations)
1054-
return .locations(remappedLocations)
1055-
case .locationLinks(let locationLinks):
1056-
let remappedLinks = locationLinks.map { link -> LocationLink in
1057-
let adjustedTargetLocation = self.locationAdjustedForCopiedFiles(
1058-
Location(uri: link.targetUri, range: link.targetRange)
1059-
)
1060-
let adjustedTargetSelectionLocation = self.locationAdjustedForCopiedFiles(
1061-
Location(uri: link.targetUri, range: link.targetSelectionRange)
1062-
)
1063-
return LocationLink(
1064-
originSelectionRange: link.originSelectionRange,
1065-
targetUri: adjustedTargetLocation.uri,
1066-
targetRange: adjustedTargetLocation.range,
1067-
targetSelectionRange: adjustedTargetSelectionLocation.range
1068-
)
1069-
}
1070-
return .locationLinks(remappedLinks)
1071-
}
1072-
}
1073-
1074-
package func typeHierarchyItemAdjustedForCopiedFiles(_ item: TypeHierarchyItem) -> TypeHierarchyItem {
1075-
let adjustedLocation = self.locationAdjustedForCopiedFiles(Location(uri: item.uri, range: item.range))
1076-
let adjustedSelectionLocation = self.locationAdjustedForCopiedFiles(
1077-
Location(uri: item.uri, range: item.selectionRange)
1078-
)
1079-
return TypeHierarchyItem(
1080-
name: item.name,
1081-
kind: item.kind,
1082-
tags: item.tags,
1083-
detail: item.detail,
1084-
uri: adjustedLocation.uri,
1085-
range: adjustedLocation.range,
1086-
selectionRange: adjustedSelectionLocation.range,
1087-
data: item.data
1088-
)
1089-
}
1090-
1091-
package func callHierarchyItemAdjustedForCopiedFiles(_ item: CallHierarchyItem) -> CallHierarchyItem {
1092-
let adjustedLocation = self.locationAdjustedForCopiedFiles(Location(uri: item.uri, range: item.range))
1093-
let adjustedSelectionLocation = self.locationAdjustedForCopiedFiles(
1094-
Location(uri: item.uri, range: item.selectionRange)
1095-
)
1096-
return CallHierarchyItem(
1097-
name: item.name,
1098-
kind: item.kind,
1099-
tags: item.tags,
1100-
detail: item.detail,
1101-
uri: adjustedLocation.uri,
1102-
range: adjustedLocation.range,
1103-
selectionRange: adjustedSelectionLocation.range,
1104-
data: .dictionary([
1105-
"usr": item.data.flatMap { data in
1106-
if case let .dictionary(dict) = data {
1107-
return dict["usr"]
1108-
}
1109-
return nil
1110-
} ?? .null,
1111-
"uri": .string(adjustedLocation.uri.stringValue),
1112-
])
1113-
)
1114-
}
1115-
11161142
@discardableResult
1117-
package func scheduleRecomputeCopyFileMap() -> Task<[DocumentURI: DocumentURI], Never> {
1118-
let task = Task<[DocumentURI: DocumentURI], Never> { [previousUpdateTask = copiedFileMap] in
1143+
package func scheduleRecomputeCopyFileMap() -> Task<CopiedFileMap, Never> {
1144+
let task = Task<CopiedFileMap, Never> { [previousUpdateTask = copiedFileMap] in
11191145
previousUpdateTask?.cancel()
11201146
return await orLog("Re-computing copy file map") {
11211147
let sourceFilesAndDirectories = try await self.sourceFilesAndDirectories()
@@ -1126,9 +1152,9 @@ package actor BuildServerManager: QueueBasedMessageHandler {
11261152
copiedFileMap[copyDestination] = file
11271153
}
11281154
}
1129-
self.cachedCopiedFileMap = copiedFileMap
1130-
return copiedFileMap
1131-
} ?? [:]
1155+
self.cachedCopiedFileMap = CopiedFileMap(map: copiedFileMap)
1156+
return self.cachedCopiedFileMap
1157+
} ?? CopiedFileMap(map: [:])
11321158
}
11331159
copiedFileMap = task
11341160
return task
@@ -1140,7 +1166,7 @@ package actor BuildServerManager: QueueBasedMessageHandler {
11401166
if await !targets(for: document).isEmpty {
11411167
return true
11421168
}
1143-
if await self.copiedFileMap?.value[document] != nil {
1169+
if await self.copiedFileMap?.value.isCopiedFile(document) == true {
11441170
return true
11451171
}
11461172
return false
@@ -1281,7 +1307,7 @@ package actor BuildServerManager: QueueBasedMessageHandler {
12811307
language: Language?,
12821308
fallbackAfterTimeout: Bool
12831309
) async throws -> FileBuildSettings? {
1284-
guard let copySource = cachedCopiedFileMap[document] else {
1310+
guard let copySource = cachedCopiedFileMap.originalURI(for: document) else {
12851311
return nil
12861312
}
12871313
let copySourceSettings = await self.buildSettingsInferredFromMainFile(

Sources/ClangLanguageService/ClangLanguageService.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,7 @@ extension ClangLanguageService {
487487
guard let workspace = self.workspace.value else {
488488
return result
489489
}
490-
return await workspace.buildServerManager.locationsOrLocationLinksAdjustedForCopiedFiles(result)
490+
return await workspace.buildServerManager.cachedCopiedFileMap.locationsOrLocationLinksAdjustedForCopiedFiles(result)
491491
}
492492

493493
package func typeDefinition(_ req: TypeDefinitionRequest) async throws -> LocationsOrLocationLinksResponse? {
@@ -636,7 +636,7 @@ extension ClangLanguageService {
636636
guard let workspace = self.workspace.value else {
637637
return workspaceEdit
638638
}
639-
return await workspace.buildServerManager.workspaceEditAdjustedForCopiedFiles(workspaceEdit)
639+
return await workspace.buildServerManager.cachedCopiedFileMap.workspaceEditAdjustedForCopiedFiles(workspaceEdit)
640640
}
641641

642642
// MARK: - Other
@@ -656,7 +656,8 @@ extension ClangLanguageService {
656656
guard let workspace = self.workspace.value else {
657657
return (workspaceEdit, symbolDetail?.usr)
658658
}
659-
let remappedEdit = await workspace.buildServerManager.workspaceEditAdjustedForCopiedFiles(workspaceEdit)
659+
let remappedEdit = await workspace.buildServerManager.cachedCopiedFileMap
660+
.workspaceEditAdjustedForCopiedFiles(workspaceEdit)
660661
return (remappedEdit ?? WorkspaceEdit(), symbolDetail?.usr)
661662
}
662663

0 commit comments

Comments
 (0)