@@ -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
}
@@ -434,6 +439,18 @@ package actor BuildServerManager: QueueBasedMessageHandler {
434
439
435
440
private let cachedSourceFilesAndDirectories = Cache < SourceFilesAndDirectoriesKey , SourceFilesAndDirectories > ( )
436
441
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
+
437
454
/// The `SourceKitInitializeBuildResponseData` received from the `build/initialize` request, if any.
438
455
package var initializationData : SourceKitInitializeBuildResponseData ? {
439
456
get async {
@@ -660,6 +677,7 @@ package actor BuildServerManager: QueueBasedMessageHandler {
660
677
return !updatedTargets. intersection ( cacheKey. targets) . isEmpty
661
678
}
662
679
self . cachedSourceFilesAndDirectories. clearAll ( isolation: self )
680
+ self . scheduleRecomputeCopyFileMap ( )
663
681
664
682
await delegate? . buildTargetsChanged ( notification. changes)
665
683
await filesBuildSettingsChangedDebouncer. scheduleCall ( Set ( watchedFiles. keys) )
@@ -839,6 +857,43 @@ package actor BuildServerManager: QueueBasedMessageHandler {
839
857
}
840
858
}
841
859
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
+
842
897
/// Returns all the targets that the document is part of.
843
898
package func targets( for document: DocumentURI ) async -> [ BuildTargetIdentifier ] {
844
899
guard let targets = await sourceFileInfo ( for: document) ? . targets else {
@@ -1292,7 +1347,8 @@ package actor BuildServerManager: QueueBasedMessageHandler {
1292
1347
isPartOfRootProject: isPartOfRootProject,
1293
1348
mayContainTests: mayContainTests,
1294
1349
isBuildable: !( target? . tags. contains ( . notBuildable) ?? false )
1295
- && ( sourceKitData? . kind ?? . source) == . source
1350
+ && ( sourceKitData? . kind ?? . source) == . source,
1351
+ copyDestinations: Set ( sourceKitData? . copyDestinations ?? [ ] )
1296
1352
)
1297
1353
switch sourceItem. kind {
1298
1354
case . file:
0 commit comments