@@ -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.
350509package 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 (
0 commit comments