Skip to content

Commit d645f44

Browse files
authored
Merge pull request #2054 from ahoppen/file-change-freestanding
Handle file did change notifications as freestanding messages
2 parents 209fb1b + ce75053 commit d645f44

14 files changed

+152
-188
lines changed

Sources/SKTestSupport/MultiFileTestProject.swift

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
package import Foundation
1414
package import LanguageServerProtocol
15+
import SKLogging
1516
package import SKOptions
1617
package import SourceKitLSP
1718
import SwiftExtensions
@@ -66,7 +67,10 @@ package class MultiFileTestProject {
6667

6768
enum Error: Swift.Error {
6869
/// No file with the given filename is known to the `MultiFileTestProject`.
69-
case fileNotFound
70+
case fileNotFound(name: String)
71+
72+
/// Trying to delete a file in `changeDocument` that doesn't exist.
73+
case deleteNonExistentFile
7074
}
7175

7276
/// The directory in which the temporary files are being placed.
@@ -168,7 +172,7 @@ package class MultiFileTestProject {
168172
language: Language? = nil
169173
) throws -> (uri: DocumentURI, positions: DocumentPositions) {
170174
guard let fileData = self.fileData[fileName] else {
171-
throw Error.fileNotFound
175+
throw Error.fileNotFound(name: fileName)
172176
}
173177
let positions = testClient.openDocument(fileData.markedText, uri: fileData.uri, language: language)
174178
return (fileData.uri, positions)
@@ -177,15 +181,15 @@ package class MultiFileTestProject {
177181
/// Returns the URI of the file with the given name.
178182
package func uri(for fileName: String) throws -> DocumentURI {
179183
guard let fileData = self.fileData[fileName] else {
180-
throw Error.fileNotFound
184+
throw Error.fileNotFound(name: fileName)
181185
}
182186
return fileData.uri
183187
}
184188

185189
/// Returns the position of the given marker in the given file.
186190
package func position(of marker: String, in fileName: String) throws -> Position {
187191
guard let fileData = self.fileData[fileName] else {
188-
throw Error.fileNotFound
192+
throw Error.fileNotFound(name: fileName)
189193
}
190194
return DocumentPositions(markedText: fileData.markedText)[marker]
191195
}
@@ -198,4 +202,50 @@ package class MultiFileTestProject {
198202
let range = try self.range(from: fromMarker, to: toMarker, in: fileName)
199203
return Location(uri: try self.uri(for: fileName), range: range)
200204
}
205+
206+
/// Modify the given file on disk, send a watched files did change notification to SourceKit-LSP and wait for that
207+
/// notification to get handled.
208+
///
209+
/// When `newMarkedContents` is `nil`, the file is deleted.
210+
///
211+
/// Returns the URI and the document positions of the new document.
212+
@discardableResult
213+
package func changeFileOnDisk(
214+
_ fileName: String,
215+
newMarkedContents: String?
216+
) async throws -> (uri: DocumentURI, positions: DocumentPositions) {
217+
let uri = try self.uri(for: fileName)
218+
guard let url = uri.fileURL else {
219+
throw Error.fileNotFound(name: fileName)
220+
}
221+
let positions: DocumentPositions
222+
let newContents: String?
223+
if let newMarkedContents {
224+
(positions, newContents) = DocumentPositions.extract(from: newMarkedContents)
225+
} else {
226+
positions = DocumentPositions(markedText: "")
227+
newContents = nil
228+
}
229+
230+
let fileExists = FileManager.default.fileExists(at: url)
231+
let changeType: FileChangeType
232+
switch (fileExists, newContents) {
233+
case (false, let newContents?):
234+
try await newContents.writeWithRetry(to: url)
235+
changeType = .created
236+
case (false, nil):
237+
throw Error.deleteNonExistentFile
238+
case (true, let newContents?):
239+
try await newContents.writeWithRetry(to: url)
240+
changeType = .changed
241+
case (true, nil):
242+
try FileManager.default.removeItem(at: url)
243+
changeType = .deleted
244+
}
245+
testClient.send(DidChangeWatchedFilesNotification(changes: [FileEvent(uri: uri, type: changeType)]))
246+
// Ensure that we handle the `DidChangeWatchedFilesNotification`.
247+
try await testClient.send(BarrierRequest())
248+
249+
return (uri, positions)
250+
}
201251
}

Sources/SourceKitLSP/MessageHandlingDependencyTracker.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,12 @@ package enum MessageHandlingDependencyTracker: QueueBasedMessageHandlerDependenc
103103
case let notification as DidChangeTextDocumentNotification:
104104
self = .documentUpdate(notification.textDocument.uri)
105105
case is DidChangeWatchedFilesNotification:
106-
self = .globalConfigurationChange
106+
// Technically, the watched files notification can change the response of any other request (eg. because a target
107+
// needs to be re-prepared). But treating it as a `globalConfiguration` inserts a lot of barriers in request
108+
// handling and significantly prevents parallelism. Since many editors batch file change notifications already,
109+
// they might have delayed the file change notification even more, which is equivalent to handling the
110+
// notification a little later inside SourceKit-LSP. Thus, treating it as `freestanding` should be acceptable.
111+
self = .freestanding
107112
case is DidChangeWorkspaceFoldersNotification:
108113
self = .globalConfigurationChange
109114
case let notification as DidCloseNotebookDocumentNotification:

Sources/SourceKitLSP/SourceKitLSPServer.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1482,9 +1482,7 @@ extension SourceKitLSPServer {
14821482
// a file doesn't mean a file can't affect the build system's build settings
14831483
// (e.g. Package.swift doesn't have build settings but affects build
14841484
// settings). Inform the build system about all file changes.
1485-
for workspace in workspaces {
1486-
await workspace.filesDidChange(notification.changes)
1487-
}
1485+
await workspaces.concurrentForEach { await $0.filesDidChange(notification.changes) }
14881486
}
14891487

14901488
func setBackgroundIndexingPaused(_ request: SetOptionsRequest) async throws -> VoidResponse {

Sources/SourceKitLSP/Workspace.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -382,8 +382,10 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate {
382382

383383
// Notify all clients about the reported and inferred edits.
384384
await buildSystemManager.filesDidChange(events)
385-
await syntacticTestIndex.filesDidChange(events)
386-
await semanticIndexManager?.filesDidChange(events)
385+
386+
async let updateSyntacticIndex: Void = await syntacticTestIndex.filesDidChange(events)
387+
async let updateSemanticIndex: Void? = await semanticIndexManager?.filesDidChange(events)
388+
_ = await (updateSyntacticIndex, updateSemanticIndex)
387389
}
388390

389391
func documentService(for uri: DocumentURI) -> LanguageService? {

Tests/SourceKitLSPTests/BackgroundIndexingTests.swift

Lines changed: 34 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -430,24 +430,14 @@ final class BackgroundIndexingTests: XCTestCase {
430430
)
431431
XCTAssertEqual(callsBeforeEdit, [])
432432

433-
let (otherFilePositions, otherFileMarkedContents) = DocumentPositions.extract(
434-
from: """
433+
let (otherFileUri, otherFilePositions) = try await project.changeFileOnDisk(
434+
"MyOtherFile.swift",
435+
newMarkedContents: """
435436
func 2️⃣bar() {
436437
3️⃣foo()
437438
}
438439
"""
439440
)
440-
441-
let otherFileUri = try project.uri(for: "MyOtherFile.swift")
442-
let otherFileUrl = try XCTUnwrap(otherFileUri.fileURL)
443-
444-
try otherFileMarkedContents.write(
445-
to: otherFileUrl,
446-
atomically: true,
447-
encoding: .utf8
448-
)
449-
450-
project.testClient.send(DidChangeWatchedFilesNotification(changes: [FileEvent(uri: otherFileUri, type: .changed)]))
451441
try await project.testClient.send(PollIndexRequest())
452442

453443
let callsAfterEdit = try await project.testClient.send(
@@ -498,21 +488,16 @@ final class BackgroundIndexingTests: XCTestCase {
498488
)
499489
XCTAssertEqual(callsBeforeEdit, [])
500490

501-
let (newPositions, headerNewMarkedContents) = DocumentPositions.extract(
502-
from: """
491+
let (_, newPositions) = try await project.changeFileOnDisk(
492+
"Header.h",
493+
newMarkedContents: """
503494
void someFunc();
504495
505496
void 2️⃣test() {
506497
3️⃣someFunc();
507498
};
508499
"""
509500
)
510-
511-
// clangd might have Header.h open, which prevents us from updating it. Keep retrying until we get a successful
512-
// write. This matches what a user would do.
513-
try await headerNewMarkedContents.writeWithRetry(to: try XCTUnwrap(uri.fileURL))
514-
515-
project.testClient.send(DidChangeWatchedFilesNotification(changes: [FileEvent(uri: uri, type: .changed)]))
516501
try await project.testClient.send(PollIndexRequest())
517502

518503
let callsAfterEdit = try await project.testClient.send(
@@ -587,15 +572,7 @@ final class BackgroundIndexingTests: XCTestCase {
587572
)
588573
XCTAssertNotEqual(initialDiagnostics.fullReport?.items, [])
589574

590-
try "public func foo() {}".write(
591-
to: try XCTUnwrap(project.uri(for: "MyFile.swift").fileURL),
592-
atomically: true,
593-
encoding: .utf8
594-
)
595-
596-
project.testClient.send(
597-
DidChangeWatchedFilesNotification(changes: [FileEvent(uri: try project.uri(for: "MyFile.swift"), type: .changed)])
598-
)
575+
try await project.changeFileOnDisk("MyFile.swift", newMarkedContents: "public func foo() {}")
599576

600577
let receivedEmptyDiagnostics = self.expectation(description: "Received diagnostic refresh request")
601578
receivedEmptyDiagnostics.assertForOverFulfill = false
@@ -703,6 +680,8 @@ final class BackgroundIndexingTests: XCTestCase {
703680
project.testClient.send(
704681
DidChangeWatchedFilesNotification(changes: [FileEvent(uri: try project.uri(for: "LibA.swift"), type: .changed)])
705682
)
683+
// Ensure that we handle the `DidChangeWatchedFilesNotification`.
684+
try await project.testClient.send(BarrierRequest())
706685

707686
// Quickly flip through all files. The way the test is designed to work is as follows:
708687
// - LibB.swift gets opened and prepared. Preparation is simulated to take a long time until both LibC.swift and
@@ -1657,19 +1636,14 @@ final class BackgroundIndexingTests: XCTestCase {
16571636
)
16581637
XCTAssertNil(definitionBeforeEdit)
16591638

1660-
let libAUri = try project.uri(for: "LibA.swift")
1661-
let (newAMarkers, newAContents) = DocumentPositions.extract(
1662-
from: """
1639+
let (libAUri, newAMarkers) = try await project.changeFileOnDisk(
1640+
"LibA.swift",
1641+
newMarkedContents: """
16631642
public struct LibA {
16641643
public func 2️⃣test() {}
16651644
}
16661645
"""
16671646
)
1668-
try await newAContents.writeWithRetry(to: XCTUnwrap(libAUri.fileURL))
1669-
1670-
project.testClient.send(
1671-
DidChangeWatchedFilesNotification(changes: [FileEvent(uri: libAUri, type: .changed)])
1672-
)
16731647

16741648
// Triggering a definition request causes `LibC` to be re-prepared. Repeat the request until LibC has been prepared
16751649
// and we get the expected result.
@@ -1718,16 +1692,15 @@ final class BackgroundIndexingTests: XCTestCase {
17181692
)
17191693
XCTAssertEqual(completionBeforeEdit.items.map(\.label), ["self"])
17201694

1721-
let libAUri = try project.uri(for: "LibA.swift")
1722-
try await """
1723-
public struct LibA {
1724-
public func test() {}
1725-
}
1726-
""".writeWithRetry(to: XCTUnwrap(libAUri.fileURL))
1727-
1728-
project.testClient.send(
1729-
DidChangeWatchedFilesNotification(changes: [FileEvent(uri: libAUri, type: .changed)])
1695+
try await project.changeFileOnDisk(
1696+
"LibA.swift",
1697+
newMarkedContents: """
1698+
public struct LibA {
1699+
public func test() {}
1700+
}
1701+
"""
17301702
)
1703+
17311704
try await repeatUntilExpectedResult {
17321705
let completionAfterEdit = try await project.testClient.send(
17331706
CompletionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
@@ -1781,9 +1754,7 @@ final class BackgroundIndexingTests: XCTestCase {
17811754
"Pre edit hover content '\(preEditHoverContent)' does not contain 'String'"
17821755
)
17831756

1784-
let libAUri = try project.uri(for: "LibA.swift")
1785-
try await "public let myVar: Int".writeWithRetry(to: try XCTUnwrap(libAUri.fileURL))
1786-
project.testClient.send(DidChangeWatchedFilesNotification(changes: [FileEvent(uri: libAUri, type: .changed)]))
1757+
try await project.changeFileOnDisk("LibA.swift", newMarkedContents: "public let myVar: Int")
17871758

17881759
try await repeatUntilExpectedResult {
17891760
let postEditHover = try await project.testClient.send(
@@ -2080,22 +2051,22 @@ final class BackgroundIndexingTests: XCTestCase {
20802051
)
20812052
XCTAssertNil(hoverWithMissingDependencyDeclaration)
20822053

2083-
let manifestUri = try project.uri(for: "Package.swift")
2084-
try """
2085-
// swift-tools-version: 5.7
2054+
try await project.changeFileOnDisk(
2055+
"Package.swift",
2056+
newMarkedContents: """
2057+
// swift-tools-version: 5.7
20862058
2087-
import PackageDescription
2059+
import PackageDescription
20882060
2089-
let package = Package(
2090-
name: "MyLibrary",
2091-
targets: [
2092-
.target(name: "LibA", swiftSettings: [.define("ENABLE_FOO")]),
2093-
.target(name: "LibB", dependencies: ["LibA"]),
2094-
]
2061+
let package = Package(
2062+
name: "MyLibrary",
2063+
targets: [
2064+
.target(name: "LibA", swiftSettings: [.define("ENABLE_FOO")]),
2065+
.target(name: "LibB", dependencies: ["LibA"]),
2066+
]
2067+
)
2068+
"""
20952069
)
2096-
""".write(to: XCTUnwrap(project.uri(for: "Package.swift").fileURL), atomically: true, encoding: .utf8)
2097-
2098-
project.testClient.send(DidChangeWatchedFilesNotification(changes: [FileEvent(uri: manifestUri, type: .changed)]))
20992070

21002071
try await repeatUntilExpectedResult {
21012072
let hoverAfterAddingDependencyDeclaration = try await project.testClient.send(

Tests/SourceKitLSPTests/CompilationDatabaseTests.swift

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -56,17 +56,7 @@ final class CompilationDatabaseTests: XCTestCase {
5656

5757
// Remove -DFOO from the compile commands.
5858

59-
let compileFlagsUri = try project.uri(for: FixedCompilationDatabaseBuildSystem.dbName)
60-
try await "".writeWithRetry(to: XCTUnwrap(compileFlagsUri.fileURL))
61-
62-
project.testClient.send(
63-
DidChangeWatchedFilesNotification(changes: [
64-
FileEvent(uri: compileFlagsUri, type: .changed)
65-
])
66-
)
67-
68-
// Ensure that the DidChangeWatchedFilesNotification is handled before we continue.
69-
try await project.testClient.send(PollIndexRequest())
59+
try await project.changeFileOnDisk(FixedCompilationDatabaseBuildSystem.dbName, newMarkedContents: "")
7060

7161
// DocumentHighlight should now point to the definition in the `#else` block.
7262

Tests/SourceKitLSPTests/DefinitionTests.swift

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -354,12 +354,9 @@ class DefinitionTests: XCTestCase {
354354
)
355355
XCTAssertNil(beforeChangingFileA)
356356

357-
let (updatedAPositions, updatedACode) = DocumentPositions.extract(from: "func 2️⃣sayHello() {}")
358-
359-
let aUri = try project.uri(for: "FileA.swift")
360-
try await updatedACode.writeWithRetry(to: XCTUnwrap(aUri.fileURL))
361-
project.testClient.send(
362-
DidChangeWatchedFilesNotification(changes: [FileEvent(uri: aUri, type: .changed)])
357+
let (aUri, updatedAPositions) = try await project.changeFileOnDisk(
358+
"FileA.swift",
359+
newMarkedContents: "func 2️⃣sayHello() {}"
363360
)
364361

365362
// Wait until SourceKit-LSP has handled the `DidChangeWatchedFilesNotification` (which it only does after a delay

Tests/SourceKitLSPTests/MainFilesProviderTests.swift

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -188,13 +188,11 @@ final class MainFilesProviderTests: XCTestCase {
188188
let preEditDiag = try XCTUnwrap(preEditDiags.diagnostics.first)
189189
XCTAssertEqual(preEditDiag.message, "Unused variable 'fromMyLibrary'")
190190

191-
let newFancyLibraryContents = """
192-
#include "\(try project.scratchDirectory.filePath)/Sources/shared.h"
193-
"""
194-
let fancyLibraryUri = try project.uri(for: "MyFancyLibrary.c")
195-
try await newFancyLibraryContents.writeWithRetry(to: XCTUnwrap(fancyLibraryUri.fileURL))
196-
project.testClient.send(
197-
DidChangeWatchedFilesNotification(changes: [FileEvent(uri: fancyLibraryUri, type: .changed)])
191+
try await project.changeFileOnDisk(
192+
"MyFancyLibrary.c",
193+
newMarkedContents: """
194+
#include "\(try project.scratchDirectory.filePath)/Sources/shared.h"
195+
"""
198196
)
199197

200198
// 'MyFancyLibrary.c' now also includes 'shared.h'. Since it lexicographically precedes MyLibrary, we should use its

Tests/SourceKitLSPTests/PublishDiagnosticsTests.swift

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -146,12 +146,7 @@ final class PublishDiagnosticsTests: XCTestCase {
146146
return false
147147
}
148148

149-
let updatedACode = "func sayHello() {}"
150-
let aUri = try project.uri(for: "FileA.swift")
151-
try await updatedACode.writeWithRetry(to: XCTUnwrap(aUri.fileURL))
152-
project.testClient.send(
153-
DidChangeWatchedFilesNotification(changes: [FileEvent(uri: aUri, type: .changed)])
154-
)
149+
try await project.changeFileOnDisk("FileA.swift", newMarkedContents: "func sayHello() {}")
155150

156151
let diagnosticsAfterChangingFileA = try await project.testClient.nextDiagnosticsNotification()
157152
XCTAssertEqual(diagnosticsAfterChangingFileA.diagnostics, [])

Tests/SourceKitLSPTests/PullDiagnosticsTests.swift

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -199,13 +199,7 @@ final class PullDiagnosticsTests: XCTestCase {
199199
return VoidResponse()
200200
}
201201

202-
let updatedACode = "func sayHello() {}"
203-
let aUri = try project.uri(for: "FileA.swift")
204-
try await updatedACode.writeWithRetry(to: XCTUnwrap(aUri.fileURL))
205-
project.testClient.send(
206-
DidChangeWatchedFilesNotification(changes: [FileEvent(uri: aUri, type: .changed)])
207-
)
208-
202+
try await project.changeFileOnDisk("FileA.swift", newMarkedContents: "func sayHello() {}")
209203
try await fulfillmentOfOrThrow([diagnosticsRefreshRequestReceived])
210204

211205
let afterChangingFileA = try await project.testClient.send(

0 commit comments

Comments
 (0)