@@ -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}
@@ -436,6 +441,18 @@ package actor BuildServerManager: QueueBasedMessageHandler {
436441
437442 private let cachedSourceFilesAndDirectories = Cache < SourceFilesAndDirectoriesKey , SourceFilesAndDirectories > ( )
438443
444+ /// The latest map of copied file URIs to their original source locations.
445+ ///
446+ /// We don't use a `Cache` for this because we can provide reasonable functionality even without or with an
447+ /// out-of-date copied file map - in the worst case we jump to a file in the build directory instead of the source
448+ /// directory.
449+ /// We don't want to block requests like definition on receiving up-to-date index information from the build server.
450+ private var cachedCopiedFileMap : [ DocumentURI : DocumentURI ] = [ : ]
451+
452+ /// The latest task to update the `cachedCopiedFileMap`. This allows us to cancel previous tasks to update the copied
453+ /// file map when a new update is requested.
454+ private var copiedFileMapUpdateTask : Task < Void , Never > ?
455+
439456 /// The `SourceKitInitializeBuildResponseData` received from the `build/initialize` request, if any.
440457 package var initializationData : SourceKitInitializeBuildResponseData ? {
441458 get async {
@@ -675,6 +692,7 @@ package actor BuildServerManager: QueueBasedMessageHandler {
675692 return !updatedTargets. intersection ( cacheKey. targets) . isEmpty
676693 }
677694 self . cachedSourceFilesAndDirectories. clearAll ( isolation: self )
695+ self . scheduleRecomputeCopyFileMap ( )
678696
679697 await delegate? . buildTargetsChanged ( notification. changes)
680698 await filesBuildSettingsChangedDebouncer. scheduleCall ( Set ( watchedFiles. keys) )
@@ -854,6 +872,43 @@ package actor BuildServerManager: QueueBasedMessageHandler {
854872 }
855873 }
856874
875+ /// Check if the URI referenced by `location` has been copied during the preparation phase. If so, adjust the URI to
876+ /// the original source file.
877+ package func locationAdjustedForCopiedFiles( _ location: Location ) -> Location {
878+ guard let originalUri = cachedCopiedFileMap [ location. uri] else {
879+ return location
880+ }
881+ // If we regularly get issues that the copied file is out-of-sync with its original, we can check that the contents
882+ // of the lines touched by the location match and only return the original URI if they do. For now, we avoid this
883+ // check due to its performance cost of reading files from disk.
884+ return Location ( uri: originalUri, range: location. range)
885+ }
886+
887+ /// Check if the URI referenced by `location` has been copied during the preparation phase. If so, adjust the URI to
888+ /// the original source file.
889+ package func locationsAdjustedForCopiedFiles( _ locations: [ Location ] ) -> [ Location ] {
890+ return locations. map { locationAdjustedForCopiedFiles ( $0) }
891+ }
892+
893+ @discardableResult
894+ package func scheduleRecomputeCopyFileMap( ) -> Task < Void , Never > {
895+ let task = Task { [ previousUpdateTask = copiedFileMapUpdateTask] in
896+ previousUpdateTask? . cancel ( )
897+ await orLog ( " Re-computing copy file map " ) {
898+ let sourceFilesAndDirectories = try await self . sourceFilesAndDirectories ( )
899+ var copiedFileMap : [ DocumentURI : DocumentURI ] = [ : ]
900+ for (file, fileInfo) in sourceFilesAndDirectories. files {
901+ for copyDestination in fileInfo. copyDestinations {
902+ copiedFileMap [ copyDestination] = file
903+ }
904+ }
905+ self . cachedCopiedFileMap = copiedFileMap
906+ }
907+ }
908+ copiedFileMapUpdateTask = task
909+ return task
910+ }
911+
857912 /// Returns all the targets that the document is part of.
858913 package func targets( for document: DocumentURI ) async -> [ BuildTargetIdentifier ] {
859914 guard let targets = await sourceFileInfo ( for: document) ? . targets else {
@@ -1307,7 +1362,8 @@ package actor BuildServerManager: QueueBasedMessageHandler {
13071362 isPartOfRootProject: isPartOfRootProject,
13081363 mayContainTests: mayContainTests,
13091364 isBuildable: !( target? . tags. contains ( . notBuildable) ?? false )
1310- && ( sourceKitData? . kind ?? . source) == . source
1365+ && ( sourceKitData? . kind ?? . source) == . source,
1366+ copyDestinations: Set ( sourceKitData? . copyDestinations ?? [ ] )
13111367 )
13121368 switch sourceItem. kind {
13131369 case . file:
0 commit comments