diff --git a/Sources/BuildServerIntegration/BuildServerManager.swift b/Sources/BuildServerIntegration/BuildServerManager.swift index f85cabd82..4cb60da0d 100644 --- a/Sources/BuildServerIntegration/BuildServerManager.swift +++ b/Sources/BuildServerIntegration/BuildServerManager.swift @@ -706,7 +706,7 @@ package actor BuildServerManager: QueueBasedMessageHandler { package func handle( request: Request, id: RequestID, - reply: @Sendable @escaping (LSPResult) -> Void + reply: @Sendable @escaping (Result) -> Void ) async { let request = RequestAndReply(request, reply: reply) switch request { diff --git a/Sources/BuildServerIntegration/BuiltInBuildServerAdapter.swift b/Sources/BuildServerIntegration/BuiltInBuildServerAdapter.swift index 11023100b..c60f5e65e 100644 --- a/Sources/BuildServerIntegration/BuiltInBuildServerAdapter.swift +++ b/Sources/BuildServerIntegration/BuiltInBuildServerAdapter.swift @@ -120,7 +120,7 @@ actor BuiltInBuildServerAdapter: QueueBasedMessageHandler { func handle( request: Request, id: RequestID, - reply: @Sendable @escaping (LSPResult) -> Void + reply: @Sendable @escaping (Result) -> Void ) async { let request = RequestAndReply(request, reply: reply) await buildServerHooks.preHandleRequest?(request.params) diff --git a/Sources/SKOptions/SourceKitLSPOptions.swift b/Sources/SKOptions/SourceKitLSPOptions.swift index aae11a091..e5eb502a7 100644 --- a/Sources/SKOptions/SourceKitLSPOptions.swift +++ b/Sources/SKOptions/SourceKitLSPOptions.swift @@ -382,7 +382,7 @@ public struct SourceKitLSPOptions: Sendable, Codable, Equatable { public var cancelTextDocumentRequestsOnEditAndClose: Bool? = nil public var cancelTextDocumentRequestsOnEditAndCloseOrDefault: Bool { - return cancelTextDocumentRequestsOnEditAndClose ?? true + return cancelTextDocumentRequestsOnEditAndClose ?? false } /// Experimental features that are enabled. diff --git a/Sources/SourceKitLSP/SourceKitLSPServer.swift b/Sources/SourceKitLSP/SourceKitLSPServer.swift index 2883b0f3e..028b9abfa 100644 --- a/Sources/SourceKitLSP/SourceKitLSPServer.swift +++ b/Sources/SourceKitLSP/SourceKitLSPServer.swift @@ -656,10 +656,25 @@ extension SourceKitLSPServer: QueueBasedMessageHandler { /// - Important: Should be invoked on `textDocumentTrackingQueue` to ensure that new text document requests are /// registered before a notification that triggers cancellation might come in. private func cancelTextDocumentRequests(for uri: DocumentURI, reason: ImplicitTextDocumentRequestCancellationReason) { - guard self.options.cancelTextDocumentRequestsOnEditAndCloseOrDefault else { - return - } + let staleRequestSupport = self.capabilityRegistry?.clientCapabilities.general?.staleRequestSupport for (requestID, requestMethod) in self.inProgressTextDocumentRequests[uri, default: []] { + // Implicitly cancel text document requests if: + // - We have enabled implicit text document request cancellation in the SourceKit options + // - The client has indicated that it does not cancel stale requests. Defaults to `true` because this is an + // option introduced in LSP 3.17 and most clients cancel requests diligently without setting + // `staleRequestSupport.cancel = true` + // - `staleRequestSupport.retryOnContentModified` contains this request method. Documentation for this behavior + // is very limited but it appears that if a request is included in that array, the client (VS Code in + // particular) expects to receive a `ContentModified` response when the LSP server detects an edit, in which + // case it will re-run the request with the new file contents. + guard + self.options.cancelTextDocumentRequestsOnEditAndCloseOrDefault + || !(staleRequestSupport?.cancel ?? true) + || staleRequestSupport?.retryOnContentModified.contains(requestMethod) ?? false + else { + continue + } + if reason == .documentChanged && requestMethod == CompletionRequest.method { // As the user types, we filter the code completion results. Cancelling the completion request on every // keystroke means that we will never build the initial list of completion results for this code @@ -668,7 +683,7 @@ extension SourceKitLSPServer: QueueBasedMessageHandler { continue } logger.info("Implicitly cancelling request \(requestID)") - self.messageHandlingHelper.cancelRequest(id: requestID) + self.messageHandlingHelper.cancelRequest(id: requestID, error: .contentModified) } } @@ -722,7 +737,7 @@ extension SourceKitLSPServer: QueueBasedMessageHandler { package func handle( request params: Request, id: RequestID, - reply: @Sendable @escaping (LSPResult) -> Void + reply: @Sendable @escaping (Result) -> Void ) async { defer { if let request = params as? any TextDocumentRequest { diff --git a/Tests/SourceKitLSPTests/SemanticTokensTests.swift b/Tests/SourceKitLSPTests/SemanticTokensTests.swift index c5221fe58..89394243b 100644 --- a/Tests/SourceKitLSPTests/SemanticTokensTests.swift +++ b/Tests/SourceKitLSPTests/SemanticTokensTests.swift @@ -986,14 +986,22 @@ final class SemanticTokensTests: SourceKitLSPTestCase { try? await Task.sleep(for: .seconds(1)) } } - }) + }), + capabilities: ClientCapabilities( + general: .init( + staleRequestSupport: .init( + cancel: true, + retryOnContentModified: [DocumentSemanticTokensRequest.method] + ) + ) + ) ) let uri = DocumentURI(for: .swift) let positions = testClient.openDocument("1️⃣", uri: uri) let receivedSemanticTokensResponse = self.expectation(description: "Received semantic tokens response") testClient.send(DocumentSemanticTokensRequest(textDocument: TextDocumentIdentifier(uri))) { result in - XCTAssertEqual(result, .failure(ResponseError.cancelled)) + XCTAssertEqual(result, .failure(ResponseError(code: .contentModified, message: "content modified"))) receivedSemanticTokensResponse.fulfill() } testClient.send(