Skip to content

Commit d04bf84

Browse files
committed
Don’t cancel in-progress diagnostic generation when calling DiagnosticReportManager.removeItemsFromCache
This was causing a non-deterministic test failure: When target preparation finishes while a diagnostic request is in progress, it will re-open the document, which calls `DiagnosticReportManager.removeItemsFromCache` for that document’s URI. With the old implementation, we would thus cancel the diagnostics sourcekitd request and return a cancelled error to the diagnostics LSP request. While doing this, I also realized that there was a race condition: Document re-opening would happen outside of the SourceKit-LSP message handling queue and could thus run concurrently to any other request. This means that a sourcekitd request could run after `reopenDocument` had closed the document but before it was opened again. Introduce an internal reopen request that can be handled on the main message handling queue and thus doesn’t have this problem
1 parent f203b3a commit d04bf84

File tree

8 files changed

+73
-16
lines changed

8 files changed

+73
-16
lines changed

Sources/LanguageServerProtocol/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ add_library(LanguageServerProtocol STATIC
1919
Notifications/LogMessageNotification.swift
2020
Notifications/LogTraceNotification.swift
2121
Notifications/PublishDiagnosticsNotification.swift
22+
Notifications/ReopenTextDocumentNotifications.swift
2223
Notifications/SetTraceNotification.swift
2324
Notifications/ShowMessageNotification.swift
2425
Notifications/TextSynchronizationNotifications.swift
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
/// Re-open the given document, discarding any in-memory state.
14+
///
15+
/// This notification is designed to be used internally in SourceKit-LSP: When build setting have changed, we re-open
16+
/// the document in sourcekitd to re-build its AST. This needs to be handled via a notification to ensure that no other
17+
/// request for this document is executing at the same time.
18+
///
19+
/// **(LSP Extension)**
20+
public struct ReopenTextDocumentNotification: NotificationType, Hashable {
21+
public static let method: String = "textDocument/reopen"
22+
23+
/// The document identifier and initial contents.
24+
public var textDocument: TextDocumentIdentifier
25+
26+
public init(textDocument: TextDocumentIdentifier) {
27+
self.textDocument = textDocument
28+
}
29+
}

Sources/SourceKitLSP/Clang/ClangLanguageService.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,8 @@ extension ClangLanguageService {
478478
clangd.send(notification)
479479
}
480480

481+
func reopenDocument(_ notification: ReopenTextDocumentNotification) {}
482+
481483
public func changeDocument(_ notification: DidChangeTextDocumentNotification) {
482484
clangd.send(notification)
483485
}

Sources/SourceKitLSP/LanguageService.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,14 @@ public protocol LanguageService: AnyObject, Sendable {
118118

119119
/// Sent to close a document on the Language Server.
120120
func closeDocument(_ notification: DidCloseTextDocumentNotification) async
121+
122+
/// Re-open the given document, discarding any in-memory state and forcing an AST to be re-built after build settings
123+
/// have been changed. This needs to be handled via a notification to ensure that no other request for this document
124+
/// is executing at the same time.
125+
///
126+
/// Only intended for `SwiftLanguageService`.
127+
func reopenDocument(_ notification: ReopenTextDocumentNotification) async
128+
121129
func changeDocument(_ notification: DidChangeTextDocumentNotification) async
122130
func willSaveDocument(_ notification: WillSaveTextDocumentNotification) async
123131
func didSaveDocument(_ notification: DidSaveTextDocumentNotification) async

Sources/SourceKitLSP/MessageHandlingDependencyTracker.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ enum MessageHandlingDependencyTracker: DependencyTracker {
126126
self = .freestanding
127127
case is PublishDiagnosticsNotification:
128128
self = .freestanding
129+
case let notification as ReopenTextDocumentNotification:
130+
self = .documentUpdate(notification.textDocument.uri)
129131
case is SetTraceNotification:
130132
self = .globalConfigurationChange
131133
case is ShowMessageNotification:

Sources/SourceKitLSP/SourceKitLSPServer.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,8 @@ extension SourceKitLSPServer: MessageHandler {
569569
await self.openDocument(notification)
570570
case let notification as DidCloseTextDocumentNotification:
571571
await self.closeDocument(notification)
572+
case let notification as ReopenTextDocumentNotification:
573+
await self.reopenDocument(notification)
572574
case let notification as DidChangeTextDocumentNotification:
573575
await self.changeDocument(notification)
574576
case let notification as DidChangeWorkspaceFoldersNotification:
@@ -1300,6 +1302,17 @@ extension SourceKitLSPServer {
13001302
await self.closeDocument(notification, workspace: workspace)
13011303
}
13021304

1305+
func reopenDocument(_ notification: ReopenTextDocumentNotification) async {
1306+
let uri = notification.textDocument.uri
1307+
guard let workspace = await workspaceForDocument(uri: uri) else {
1308+
logger.error(
1309+
"received reopen notification for file '\(uri.forLogging)' without a corresponding workspace, ignoring..."
1310+
)
1311+
return
1312+
}
1313+
await workspace.documentService.value[uri]?.reopenDocument(notification)
1314+
}
1315+
13031316
func closeDocument(_ notification: DidCloseTextDocumentNotification, workspace: Workspace) async {
13041317
// Immediately close the document. We need to be sure to clear our pending work queue in case
13051318
// the build system still isn't ready.

Sources/SourceKitLSP/Swift/DiagnosticReportManager.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,7 @@ actor DiagnosticReportManager {
8686
}
8787

8888
func removeItemsFromCache(with uri: DocumentURI) async {
89-
let tasksToCancel = reportTaskCache.filter { $0.snapshotID.uri == uri }
9089
reportTaskCache.removeAll(where: { $0.snapshotID.uri == uri })
91-
for task in tasksToCancel {
92-
await task.reportTask.cancel()
93-
}
9490
}
9591

9692
private func requestReport(

Sources/SourceKitLSP/Swift/SwiftLanguageService.swift

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -327,15 +327,24 @@ extension SwiftLanguageService {
327327

328328
// MARK: - Build System Integration
329329

330-
private func reopenDocument(_ snapshot: DocumentSnapshot, _ compileCmd: SwiftCompileCommand?) async {
330+
public func reopenDocument(_ notification: ReopenTextDocumentNotification) async {
331+
let snapshot = orLog("Getting snapshot to re-open document") {
332+
try documentManager.latestSnapshot(notification.textDocument.uri)
333+
}
334+
guard let snapshot else {
335+
return
336+
}
331337
cancelInFlightPublishDiagnosticsTask(for: snapshot.uri)
332338
await diagnosticReportManager.removeItemsFromCache(with: snapshot.uri)
333339

334340
let closeReq = closeDocumentSourcekitdRequest(uri: snapshot.uri)
335-
_ = try? await self.sourcekitd.send(closeReq, fileContents: nil)
341+
_ = await orLog("Closing document to re-open it") { try await self.sourcekitd.send(closeReq, fileContents: nil) }
336342

337-
let openReq = openDocumentSourcekitdRequest(snapshot: snapshot, compileCommand: compileCmd)
338-
_ = try? await self.sourcekitd.send(openReq, fileContents: snapshot.text)
343+
let openReq = openDocumentSourcekitdRequest(
344+
snapshot: snapshot,
345+
compileCommand: await buildSettings(for: snapshot.uri)
346+
)
347+
_ = await orLog("Re-opening document") { try await self.sourcekitd.send(openReq, fileContents: snapshot.text) }
339348

340349
if await capabilityRegistry.clientSupportsPullDiagnostics(for: .swift) {
341350
await self.refreshDiagnosticsDebouncer.scheduleCall()
@@ -345,14 +354,11 @@ extension SwiftLanguageService {
345354
}
346355

347356
public func documentUpdatedBuildSettings(_ uri: DocumentURI) async {
348-
// We may not have a snapshot if this is called just before `openDocument`.
349-
guard let snapshot = try? self.documentManager.latestSnapshot(uri) else {
350-
return
351-
}
352-
353-
// Close and re-open the document internally to inform sourcekitd to update the compile
354-
// command. At the moment there's no better way to do this.
355-
await self.reopenDocument(snapshot, await self.buildSettings(for: uri))
357+
// Close and re-open the document internally to inform sourcekitd to update the compile command. At the moment
358+
// there's no better way to do this.
359+
// Schedule the document re-open in the SourceKit-LSP server. This ensures that the re-open happens exclusively with
360+
// no other request running at the same time.
361+
sourceKitLSPServer?.handle(ReopenTextDocumentNotification(textDocument: TextDocumentIdentifier(uri)))
356362
}
357363

358364
public func documentDependenciesUpdated(_ uri: DocumentURI) async {

0 commit comments

Comments
 (0)