@@ -75,6 +75,10 @@ package struct SourceFileInfo: Sendable {
75
75
/// compiler arguments for these files to provide semantic editor functionality but we can't build them.
76
76
package var isBuildable : Bool
77
77
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
+
78
82
fileprivate func merging( _ other: SourceFileInfo ? ) -> SourceFileInfo {
79
83
guard let other else {
80
84
return self
@@ -99,7 +103,8 @@ package struct SourceFileInfo: Sendable {
99
103
targetsToOutputPath: mergedTargetsToOutputPaths,
100
104
isPartOfRootProject: other. isPartOfRootProject || isPartOfRootProject,
101
105
mayContainTests: other. mayContainTests || mayContainTests,
102
- isBuildable: other. isBuildable || isBuildable
106
+ isBuildable: other. isBuildable || isBuildable,
107
+ copyDestinations: copyDestinations. union ( other. copyDestinations)
103
108
)
104
109
}
105
110
}
@@ -436,6 +441,18 @@ package actor BuildServerManager: QueueBasedMessageHandler {
436
441
437
442
private let cachedSourceFilesAndDirectories = Cache < SourceFilesAndDirectoriesKey , SourceFilesAndDirectories > ( )
438
443
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
+
439
456
/// The `SourceKitInitializeBuildResponseData` received from the `build/initialize` request, if any.
440
457
package var initializationData : SourceKitInitializeBuildResponseData ? {
441
458
get async {
@@ -675,6 +692,7 @@ package actor BuildServerManager: QueueBasedMessageHandler {
675
692
return !updatedTargets. intersection ( cacheKey. targets) . isEmpty
676
693
}
677
694
self . cachedSourceFilesAndDirectories. clearAll ( isolation: self )
695
+ self . scheduleRecomputeCopyFileMap ( )
678
696
679
697
await delegate? . buildTargetsChanged ( notification. changes)
680
698
await filesBuildSettingsChangedDebouncer. scheduleCall ( Set ( watchedFiles. keys) )
@@ -854,6 +872,43 @@ package actor BuildServerManager: QueueBasedMessageHandler {
854
872
}
855
873
}
856
874
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
+
857
912
/// Returns all the targets that the document is part of.
858
913
package func targets( for document: DocumentURI ) async -> [ BuildTargetIdentifier ] {
859
914
guard let targets = await sourceFileInfo ( for: document) ? . targets else {
@@ -1307,7 +1362,8 @@ package actor BuildServerManager: QueueBasedMessageHandler {
1307
1362
isPartOfRootProject: isPartOfRootProject,
1308
1363
mayContainTests: mayContainTests,
1309
1364
isBuildable: !( target? . tags. contains ( . notBuildable) ?? false )
1310
- && ( sourceKitData? . kind ?? . source) == . source
1365
+ && ( sourceKitData? . kind ?? . source) == . source,
1366
+ copyDestinations: Set ( sourceKitData? . copyDestinations ?? [ ] )
1311
1367
)
1312
1368
switch sourceItem. kind {
1313
1369
case . file:
0 commit comments