@@ -62,6 +62,51 @@ fileprivate func firstNonNil<T>(
62
62
return try await defaultValue ( )
63
63
}
64
64
65
+ /// Actor that caches realpaths for `sourceFilesWithSameRealpath`.
66
+ fileprivate actor SourceFilesWithSameRealpathInferrer {
67
+ private let buildSystemManager : BuildSystemManager
68
+ private var realpathCache : [ DocumentURI : DocumentURI ] = [ : ]
69
+
70
+ init ( buildSystemManager: BuildSystemManager ) {
71
+ self . buildSystemManager = buildSystemManager
72
+ }
73
+
74
+ private func realpath( of uri: DocumentURI ) -> DocumentURI {
75
+ if let cached = realpathCache [ uri] {
76
+ return cached
77
+ }
78
+ let value = uri. symlinkTarget ?? uri
79
+ realpathCache [ uri] = value
80
+ return value
81
+ }
82
+
83
+ /// Returns the URIs of all source files in the project that have the same realpath as a document in `documents` but
84
+ /// are not in `documents`.
85
+ ///
86
+ /// This is useful in the following scenario: A project has target A containing A.swift an target B containing B.swift
87
+ /// B.swift is a symlink to A.swift. When A.swift is modified, both the dependencies of A and B need to be marked as
88
+ /// having an out-of-date preparation status, not just A.
89
+ package func sourceFilesWithSameRealpath( as documents: [ DocumentURI ] ) async -> [ DocumentURI ] {
90
+ let realPaths = Set ( documents. map { realpath ( of: $0) } )
91
+ return await orLog ( " Determining source files with same realpath " ) {
92
+ var result : [ DocumentURI ] = [ ]
93
+ let filesAndDirectories = try await buildSystemManager. sourceFiles ( includeNonBuildableFiles: true )
94
+ for file in filesAndDirectories. keys {
95
+ if realPaths. contains ( realpath ( of: file) ) && !documents. contains ( file) {
96
+ result. append ( file)
97
+ }
98
+ }
99
+ return result
100
+ } ?? [ ]
101
+ }
102
+
103
+ func filesDidChange( _ events: [ FileEvent ] ) {
104
+ for event in events {
105
+ realpathCache [ event. uri] = nil
106
+ }
107
+ }
108
+ }
109
+
65
110
/// Represents the configuration and state of a project or combination of projects being worked on
66
111
/// together.
67
112
///
@@ -86,6 +131,8 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate {
86
131
/// The build system manager to use for documents in this workspace.
87
132
package let buildSystemManager : BuildSystemManager
88
133
134
+ private let sourceFilesWithSameRealpathInferrer : SourceFilesWithSameRealpathInferrer
135
+
89
136
let options : SourceKitLSPOptions
90
137
91
138
/// The source code index, if available.
@@ -126,6 +173,9 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate {
126
173
self . options = options
127
174
self . _uncheckedIndex = ThreadSafeBox ( initialValue: uncheckedIndex)
128
175
self . buildSystemManager = buildSystemManager
176
+ self . sourceFilesWithSameRealpathInferrer = SourceFilesWithSameRealpathInferrer (
177
+ buildSystemManager: buildSystemManager
178
+ )
129
179
if options. backgroundIndexingOrDefault, let uncheckedIndex,
130
180
await buildSystemManager. initializationData? . prepareProvider ?? false
131
181
{
@@ -316,6 +366,17 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate {
316
366
}
317
367
318
368
package func filesDidChange( _ events: [ FileEvent] ) async {
369
+ // First clear any cached realpaths in `sourceFilesWithSameRealpathInferrer`.
370
+ await sourceFilesWithSameRealpathInferrer. filesDidChange ( events)
371
+
372
+ // Now infer any edits for source files that share the same realpath as one of the modified files.
373
+ var events = events
374
+ events +=
375
+ await sourceFilesWithSameRealpathInferrer
376
+ . sourceFilesWithSameRealpath ( as: events. filter { $0. type == . changed } . map ( \. uri) )
377
+ . map { FileEvent ( uri: $0, type: . changed) }
378
+
379
+ // Notify all clients about the reported and inferred edits.
319
380
await buildSystemManager. filesDidChange ( events)
320
381
await syntacticTestIndex. filesDidChange ( events)
321
382
await semanticIndexManager? . filesDidChange ( events)
0 commit comments