@@ -75,6 +75,10 @@ package struct SourceFileInfo: Sendable {
7575 /// compiler arguments for these files to provide semantic editor functionality but we can't build them.
7676 package var isBuildable : Bool
7777
78+ /// If this source item gets copied to a different destination during preparation, the destinations it will be copied
79+ /// to.
80+ package var copyDestinations : Set < DocumentURI >
81+
7882 fileprivate func merging( _ other: SourceFileInfo ? ) -> SourceFileInfo {
7983 guard let other else {
8084 return self
@@ -99,7 +103,8 @@ package struct SourceFileInfo: Sendable {
99103 targetsToOutputPath: mergedTargetsToOutputPaths,
100104 isPartOfRootProject: other. isPartOfRootProject || isPartOfRootProject,
101105 mayContainTests: other. mayContainTests || mayContainTests,
102- isBuildable: other. isBuildable || isBuildable
106+ isBuildable: other. isBuildable || isBuildable,
107+ copyDestinations: copyDestinations. union ( other. copyDestinations)
103108 )
104109 }
105110}
@@ -434,6 +439,18 @@ package actor BuildServerManager: QueueBasedMessageHandler {
434439
435440 private let cachedSourceFilesAndDirectories = Cache < SourceFilesAndDirectoriesKey , SourceFilesAndDirectories > ( )
436441
442+ /// The latest map of copied file URIs to their original source locations.
443+ ///
444+ /// We don't use a `Cache` for this because we can provide reasonable functionality even without or with an
445+ /// out-of-date copied file map - in the worst case we jump to a file in the build directory instead of the source
446+ /// directory.
447+ /// We don't want to block requests like definition on receiving up-to-date index information from the build server.
448+ private var cachedCopiedFileMap : [ DocumentURI : DocumentURI ] = [ : ]
449+
450+ /// The latest task to update the `cachedCopiedFileMap`. This allows us to cancel previous tasks to update the copied
451+ /// file map when a new update is requested.
452+ private var copiedFileMapUpdateTask : Task < Void , Never > ?
453+
437454 /// The `SourceKitInitializeBuildResponseData` received from the `build/initialize` request, if any.
438455 package var initializationData : SourceKitInitializeBuildResponseData ? {
439456 get async {
@@ -660,6 +677,7 @@ package actor BuildServerManager: QueueBasedMessageHandler {
660677 return !updatedTargets. intersection ( cacheKey. targets) . isEmpty
661678 }
662679 self . cachedSourceFilesAndDirectories. clearAll ( isolation: self )
680+ self . scheduleRecomputeCopyFileMap ( )
663681
664682 await delegate? . buildTargetsChanged ( notification. changes)
665683 await filesBuildSettingsChangedDebouncer. scheduleCall ( Set ( watchedFiles. keys) )
@@ -839,6 +857,43 @@ package actor BuildServerManager: QueueBasedMessageHandler {
839857 }
840858 }
841859
860+ /// Check if the URI referenced by `location` has been copied during the preparation phase. If so, adjust the URI to
861+ /// the original source file.
862+ package func locationAdjustedForCopiedFiles( _ location: Location ) -> Location {
863+ guard let originalUri = cachedCopiedFileMap [ location. uri] else {
864+ return location
865+ }
866+ // If we regularly get issues that the copied file is out-of-sync with its original, we can check that the contents
867+ // of the lines touched by the location match and only return the original URI if they do. For now, we avoid this
868+ // check due to its performance cost of reading files from disk.
869+ return Location ( uri: originalUri, range: location. range)
870+ }
871+
872+ /// Check if the URI referenced by `location` has been copied during the preparation phase. If so, adjust the URI to
873+ /// the original source file.
874+ package func locationsAdjustedForCopiedFiles( _ locations: [ Location ] ) -> [ Location ] {
875+ return locations. map { locationAdjustedForCopiedFiles ( $0) }
876+ }
877+
878+ @discardableResult
879+ package func scheduleRecomputeCopyFileMap( ) -> Task < Void , Never > {
880+ let task = Task { [ previousUpdateTask = copiedFileMapUpdateTask] in
881+ previousUpdateTask? . cancel ( )
882+ await orLog ( " Re-computing copy file map " ) {
883+ let sourceFilesAndDirectories = try await self . sourceFilesAndDirectories ( )
884+ var copiedFileMap : [ DocumentURI : DocumentURI ] = [ : ]
885+ for (file, fileInfo) in sourceFilesAndDirectories. files {
886+ for copyDestination in fileInfo. copyDestinations {
887+ copiedFileMap [ copyDestination] = file
888+ }
889+ }
890+ self . cachedCopiedFileMap = copiedFileMap
891+ }
892+ }
893+ copiedFileMapUpdateTask = task
894+ return task
895+ }
896+
842897 /// Returns all the targets that the document is part of.
843898 package func targets( for document: DocumentURI ) async -> [ BuildTargetIdentifier ] {
844899 guard let targets = await sourceFileInfo ( for: document) ? . targets else {
@@ -1292,7 +1347,8 @@ package actor BuildServerManager: QueueBasedMessageHandler {
12921347 isPartOfRootProject: isPartOfRootProject,
12931348 mayContainTests: mayContainTests,
12941349 isBuildable: !( target? . tags. contains ( . notBuildable) ?? false )
1295- && ( sourceKitData? . kind ?? . source) == . source
1350+ && ( sourceKitData? . kind ?? . source) == . source,
1351+ copyDestinations: Set ( sourceKitData? . copyDestinations ?? [ ] )
12961352 )
12971353 switch sourceItem. kind {
12981354 case . file:
0 commit comments