@@ -62,6 +62,51 @@ fileprivate func firstNonNil<T>(
6262 return try await defaultValue ( )
6363}
6464
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+
65110/// Represents the configuration and state of a project or combination of projects being worked on
66111/// together.
67112///
@@ -86,6 +131,8 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate {
86131 /// The build system manager to use for documents in this workspace.
87132 package let buildSystemManager : BuildSystemManager
88133
134+ private let sourceFilesWithSameRealpathInferrer : SourceFilesWithSameRealpathInferrer
135+
89136 let options : SourceKitLSPOptions
90137
91138 /// The source code index, if available.
@@ -126,6 +173,9 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate {
126173 self . options = options
127174 self . _uncheckedIndex = ThreadSafeBox ( initialValue: uncheckedIndex)
128175 self . buildSystemManager = buildSystemManager
176+ self . sourceFilesWithSameRealpathInferrer = SourceFilesWithSameRealpathInferrer (
177+ buildSystemManager: buildSystemManager
178+ )
129179 if options. backgroundIndexingOrDefault, let uncheckedIndex,
130180 await buildSystemManager. initializationData? . prepareProvider ?? false
131181 {
@@ -316,6 +366,17 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate {
316366 }
317367
318368 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.
319380 await buildSystemManager. filesDidChange ( events)
320381 await syntacticTestIndex. filesDidChange ( events)
321382 await semanticIndexManager? . filesDidChange ( events)
0 commit comments