From ea9f173339e6d6e49918e943ac6c247cd5569b41 Mon Sep 17 00:00:00 2001 From: Tristan Labelle Date: Wed, 3 May 2023 11:25:28 -0400 Subject: [PATCH 1/3] Add boilerplate for pull-model diagnostics --- .../SupportTypes/ClientCapabilities.swift | 16 ++++++++- .../SupportTypes/RegistrationOptions.swift | 24 +++++++++++++ .../SupportTypes/ServerCapabilities.swift | 5 +++ Sources/SourceKitLSP/CapabilityRegistry.swift | 34 +++++++++++++++++++ .../Clang/ClangLanguageServer.swift | 4 +++ Sources/SourceKitLSP/SourceKitServer.swift | 15 ++++++++ .../Swift/SwiftLanguageServer.swift | 5 +++ .../ToolchainLanguageServer.swift | 1 + 8 files changed, 103 insertions(+), 1 deletion(-) diff --git a/Sources/LanguageServerProtocol/SupportTypes/ClientCapabilities.swift b/Sources/LanguageServerProtocol/SupportTypes/ClientCapabilities.swift index 648c11653..a3a8cc7a4 100644 --- a/Sources/LanguageServerProtocol/SupportTypes/ClientCapabilities.swift +++ b/Sources/LanguageServerProtocol/SupportTypes/ClientCapabilities.swift @@ -526,6 +526,16 @@ public struct TextDocumentClientCapabilities: Hashable, Codable { } } + /// Capabilities specific to 'textDocument/diagnostic'. Since LSP 3.17.0. + public struct Diagnostic: Equatable, Hashable, Codable { + + /// Whether implementation supports dynamic registration. + public var dynamicRegistration: Bool? + + /// Whether the clients supports related documents for document diagnostic pulls. + public var relatedDocumentSupport: Bool? + } + // MARK: Properties public var synchronization: Synchronization? = nil @@ -575,6 +585,8 @@ public struct TextDocumentClientCapabilities: Hashable, Codable { public var semanticTokens: SemanticTokens? = nil public var inlayHint: InlayHint? = nil + + public var diagnostic: Diagnostic? = nil public init(synchronization: Synchronization? = nil, completion: Completion? = nil, @@ -599,7 +611,8 @@ public struct TextDocumentClientCapabilities: Hashable, Codable { foldingRange: FoldingRange? = nil, callHierarchy: DynamicRegistrationCapability? = nil, semanticTokens: SemanticTokens? = nil, - inlayHint: InlayHint? = nil) { + inlayHint: InlayHint? = nil, + diagnostic: Diagnostic? = nil) { self.synchronization = synchronization self.completion = completion self.hover = hover @@ -624,5 +637,6 @@ public struct TextDocumentClientCapabilities: Hashable, Codable { self.callHierarchy = callHierarchy self.semanticTokens = semanticTokens self.inlayHint = inlayHint + self.diagnostic = diagnostic } } diff --git a/Sources/LanguageServerProtocol/SupportTypes/RegistrationOptions.swift b/Sources/LanguageServerProtocol/SupportTypes/RegistrationOptions.swift index cb1b19fd4..cfec220bd 100644 --- a/Sources/LanguageServerProtocol/SupportTypes/RegistrationOptions.swift +++ b/Sources/LanguageServerProtocol/SupportTypes/RegistrationOptions.swift @@ -156,6 +156,30 @@ public struct InlayHintRegistrationOptions: RegistrationOptions, TextDocumentReg } } +/// Describe options to be used when registering for pull diagnostics. Since LSP 3.17.0 +public struct DiagnosticRegistrationOptions: RegistrationOptions, TextDocumentRegistrationOptionsProtocol { + public var textDocumentRegistrationOptions: TextDocumentRegistrationOptions + public var diagnosticOptions: DiagnosticOptions + + public init( + documentSelector: DocumentSelector? = nil, + diagnosticOptions: DiagnosticOptions + ) { + textDocumentRegistrationOptions = TextDocumentRegistrationOptions(documentSelector: documentSelector) + self.diagnosticOptions = diagnosticOptions + } + + public func encodeIntoLSPAny(dict: inout [String: LSPAny]) { + textDocumentRegistrationOptions.encodeIntoLSPAny(dict: &dict) + + dict["interFileDependencies"] = .bool(diagnosticOptions.interFileDependencies) + dict["workspaceDiagnostics"] = .bool(diagnosticOptions.workspaceDiagnostics) + if let workDoneProgress = diagnosticOptions.workDoneProgress { + dict["workDoneProgress"] = .bool(workDoneProgress) + } + } +} + /// Describe options to be used when registering for file system change events. public struct DidChangeWatchedFilesRegistrationOptions: RegistrationOptions { /// The watchers to register. diff --git a/Sources/LanguageServerProtocol/SupportTypes/ServerCapabilities.swift b/Sources/LanguageServerProtocol/SupportTypes/ServerCapabilities.swift index 0c5278cc7..4db49892c 100644 --- a/Sources/LanguageServerProtocol/SupportTypes/ServerCapabilities.swift +++ b/Sources/LanguageServerProtocol/SupportTypes/ServerCapabilities.swift @@ -108,6 +108,9 @@ public struct ServerCapabilities: Codable, Hashable { /// Whether the server supports the `textDocument/inlayHint` family of requests. public var inlayHintProvider: ValueOrBool? + /// Whether the server supports the `textDocument/diagnostic` request. + public var diagnosticProvider: DiagnosticOptions? + /// Whether the server provides selection range support. public var selectionRangeProvider: ValueOrBool? @@ -152,6 +155,7 @@ public struct ServerCapabilities: Codable, Hashable { typeHierarchyProvider: ValueOrBool? = nil, semanticTokensProvider: SemanticTokensOptions? = nil, inlayHintProvider: ValueOrBool? = nil, + diagnosticProvider: DiagnosticOptions? = nil, selectionRangeProvider: ValueOrBool? = nil, linkedEditingRangeProvider: ValueOrBool? = nil, monikerProvider: ValueOrBool? = nil, @@ -188,6 +192,7 @@ public struct ServerCapabilities: Codable, Hashable { self.typeHierarchyProvider = typeHierarchyProvider self.semanticTokensProvider = semanticTokensProvider self.inlayHintProvider = inlayHintProvider + self.diagnosticProvider = diagnosticProvider self.selectionRangeProvider = selectionRangeProvider self.linkedEditingRangeProvider = linkedEditingRangeProvider self.experimental = experimental diff --git a/Sources/SourceKitLSP/CapabilityRegistry.swift b/Sources/SourceKitLSP/CapabilityRegistry.swift index fa684b364..268b57954 100644 --- a/Sources/SourceKitLSP/CapabilityRegistry.swift +++ b/Sources/SourceKitLSP/CapabilityRegistry.swift @@ -31,6 +31,9 @@ public final class CapabilityRegistry { /// Dynamically registered inlay hint options. private var inlayHint: [CapabilityRegistration: InlayHintRegistrationOptions] = [:] + + /// Dynamically registered pull diagnostics options. + private var pullDiagnostics: [CapabilityRegistration: DiagnosticRegistrationOptions] = [:] /// Dynamically registered file watchers. private var didChangeWatchedFiles: DidChangeWatchedFilesRegistrationOptions? @@ -60,6 +63,10 @@ public final class CapabilityRegistry { clientCapabilities.textDocument?.inlayHint?.dynamicRegistration == true } + public var clientHasDocumentDiagnosticsRegistration: Bool { + clientCapabilities.textDocument?.diagnostic?.dynamicRegistration == true + } + public var clientHasDynamicExecuteCommandRegistration: Bool { clientCapabilities.workspace?.executeCommand?.dynamicRegistration == true } @@ -202,6 +209,33 @@ public final class CapabilityRegistry { registerOnClient(registration) } + /// Dynamically register (pull model) diagnostic capabilities, + /// if the client supports it. + public func registerDiagnosticIfNeeded( + options: DiagnosticOptions, + for languages: [Language], + registerOnClient: ClientRegistrationHandler + ) { + guard clientHasDocumentDiagnosticsRegistration else { return } + if let registration = registration(for: languages, in: pullDiagnostics) { + if options != registration.diagnosticOptions { + log("Unable to register new pull diagnostics options \(options) for " + + "\(languages) due to pre-existing options \(registration.diagnosticOptions)", level: .warning) + } + return + } + let registrationOptions = DiagnosticRegistrationOptions( + documentSelector: self.documentSelector(for: languages), + diagnosticOptions: options) + let registration = CapabilityRegistration( + method: DocumentDiagnosticsRequest.method, + registerOptions: self.encode(registrationOptions)) + + self.pullDiagnostics[registration] = registrationOptions + + registerOnClient(registration) + } + /// Dynamically register executeCommand with the given IDs if the client supports /// it and we haven't yet registered the given command IDs yet. public func registerExecuteCommandIfNeeded( diff --git a/Sources/SourceKitLSP/Clang/ClangLanguageServer.swift b/Sources/SourceKitLSP/Clang/ClangLanguageServer.swift index e271c2001..9141f38af 100644 --- a/Sources/SourceKitLSP/Clang/ClangLanguageServer.swift +++ b/Sources/SourceKitLSP/Clang/ClangLanguageServer.swift @@ -515,6 +515,10 @@ extension ClangLanguageServerShim { forwardRequestToClangdOnQueue(req) } + func documentDiagnostic(_ req: Request) { + forwardRequestToClangdOnQueue(req) + } + func foldingRange(_ req: Request) { queue.async { if self.capabilities?.foldingRangeProvider?.isSupported == true { diff --git a/Sources/SourceKitLSP/SourceKitServer.swift b/Sources/SourceKitLSP/SourceKitServer.swift index b9f51efde..7a0f69c36 100644 --- a/Sources/SourceKitLSP/SourceKitServer.swift +++ b/Sources/SourceKitLSP/SourceKitServer.swift @@ -202,6 +202,8 @@ public final class SourceKitServer: LanguageServer { registerToolchainTextDocumentRequest(SourceKitServer.colorPresentation, []) registerToolchainTextDocumentRequest(SourceKitServer.codeAction, nil) registerToolchainTextDocumentRequest(SourceKitServer.inlayHint, []) + registerToolchainTextDocumentRequest(SourceKitServer.documentDiagnostic, + .full(.init(items: []))) } /// Register a `TextDocumentRequest` that requires a valid `Workspace`, `ToolchainLanguageServer`, @@ -709,6 +711,11 @@ extension SourceKitServer { self.dynamicallyRegisterCapability($0, registry) } } + if let diagnosticOptions = server.diagnosticProvider { + registry.registerDiagnosticIfNeeded(options: diagnosticOptions, for: languages) { + self.dynamicallyRegisterCapability($0, registry) + } + } if let commandOptions = server.executeCommandProvider { registry.registerExecuteCommandIfNeeded(commands: commandOptions.commands) { self.dynamicallyRegisterCapability($0, registry) @@ -1209,6 +1216,14 @@ extension SourceKitServer { languageService.inlayHint(req) } + func documentDiagnostic( + _ req: Request, + workspace: Workspace, + languageService: ToolchainLanguageServer + ) { + languageService.documentDiagnostic(req) + } + /// Converts a location from the symbol index to an LSP location. /// /// - Parameter location: The symbol index location diff --git a/Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift b/Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift index d3848437c..e9ca27268 100644 --- a/Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift +++ b/Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift @@ -1326,6 +1326,11 @@ extension SwiftLanguageServer { } } } + + public func documentDiagnostic(_ req: Request) { + // TODO: Return provider object in initializeSync and implement pull-model document diagnostics here. + req.reply(.failure(.unknown("Pull-model diagnostics not implemented yet."))) + } public func executeCommand(_ req: Request) { let params = req.params diff --git a/Sources/SourceKitLSP/ToolchainLanguageServer.swift b/Sources/SourceKitLSP/ToolchainLanguageServer.swift index c98ed715f..13d01706e 100644 --- a/Sources/SourceKitLSP/ToolchainLanguageServer.swift +++ b/Sources/SourceKitLSP/ToolchainLanguageServer.swift @@ -97,6 +97,7 @@ public protocol ToolchainLanguageServer: AnyObject { func colorPresentation(_ req: Request) func codeAction(_ req: Request) func inlayHint(_ req: Request) + func documentDiagnostic(_ req: Request) // MARK: - Other From c076bd528259433bc703c58c70357009fcef833e Mon Sep 17 00:00:00 2001 From: Tristan Labelle Date: Thu, 11 May 2023 17:07:53 -0400 Subject: [PATCH 2/3] Implement pull-model documentDiagnostics --- .../SupportTypes/RegistrationOptions.swift | 7 +- .../SupportTypes/ServerCapabilities.swift | 19 ++- Sources/SourceKitD/sourcekitd_uids.swift | 2 + Sources/SourceKitLSP/CapabilityRegistry.swift | 30 ++++- .../Clang/ClangLanguageServer.swift | 1 - Sources/SourceKitLSP/SourceKitServer.swift | 1 - .../SourceKitLSP/Swift/CodeCompletion.swift | 4 +- .../Swift/SwiftLanguageServer.swift | 110 +++++++++++++++--- .../ToolchainLanguageServer.swift | 1 - ...ts.swift => PublishDiagnosticsTests.swift} | 4 +- .../PullDiagnosticsTests.swift | 83 +++++++++++++ 11 files changed, 225 insertions(+), 37 deletions(-) rename Tests/SourceKitLSPTests/{DiagnosticsTests.swift => PublishDiagnosticsTests.swift} (98%) create mode 100644 Tests/SourceKitLSPTests/PullDiagnosticsTests.swift diff --git a/Sources/LanguageServerProtocol/SupportTypes/RegistrationOptions.swift b/Sources/LanguageServerProtocol/SupportTypes/RegistrationOptions.swift index cfec220bd..8e1f2bf1b 100644 --- a/Sources/LanguageServerProtocol/SupportTypes/RegistrationOptions.swift +++ b/Sources/LanguageServerProtocol/SupportTypes/RegistrationOptions.swift @@ -171,12 +171,7 @@ public struct DiagnosticRegistrationOptions: RegistrationOptions, TextDocumentRe public func encodeIntoLSPAny(dict: inout [String: LSPAny]) { textDocumentRegistrationOptions.encodeIntoLSPAny(dict: &dict) - - dict["interFileDependencies"] = .bool(diagnosticOptions.interFileDependencies) - dict["workspaceDiagnostics"] = .bool(diagnosticOptions.workspaceDiagnostics) - if let workDoneProgress = diagnosticOptions.workDoneProgress { - dict["workDoneProgress"] = .bool(workDoneProgress) - } + diagnosticOptions.encodeIntoLSPAny(dict: &dict) } } diff --git a/Sources/LanguageServerProtocol/SupportTypes/ServerCapabilities.swift b/Sources/LanguageServerProtocol/SupportTypes/ServerCapabilities.swift index 4db49892c..80adbb612 100644 --- a/Sources/LanguageServerProtocol/SupportTypes/ServerCapabilities.swift +++ b/Sources/LanguageServerProtocol/SupportTypes/ServerCapabilities.swift @@ -868,9 +868,6 @@ public struct DiagnosticOptions: WorkDoneProgressOptions, Codable, Hashable { /// The server provides support for workspace diagnostics as well. public var workspaceDiagnostics: Bool - /// A document selector to identify the scope of the registration. If set to null the document selector provided on the client side will be used. - public var documentSelector: DocumentSelector? - /// The id used to register the request. The id can be used to deregister the request again. See also Registration#id public var id: String? @@ -880,17 +877,29 @@ public struct DiagnosticOptions: WorkDoneProgressOptions, Codable, Hashable { identifier: String? = nil, interFileDependencies: Bool, workspaceDiagnostics: Bool, - documentSelector: DocumentSelector? = nil, id: String? = nil, workDoneProgress: Bool? = nil ) { self.identifier = identifier self.interFileDependencies = interFileDependencies self.workspaceDiagnostics = workspaceDiagnostics - self.documentSelector = documentSelector self.id = id self.workDoneProgress = workDoneProgress } + + public func encodeIntoLSPAny(dict: inout [String: LSPAny]) { + if let identifier = identifier { + dict["identifier"] = .string(identifier) + } + dict["interFileDependencies"] = .bool(interFileDependencies) + dict["workspaceDiagnostics"] = .bool(workspaceDiagnostics) + if let id = id { + dict["id"] = .string(id) + } + if let workDoneProgress = workDoneProgress { + dict["workDoneProgress"] = .bool(workDoneProgress) + } + } } public struct WorkspaceServerCapabilities: Codable, Hashable { diff --git a/Sources/SourceKitD/sourcekitd_uids.swift b/Sources/SourceKitD/sourcekitd_uids.swift index b88dd9390..052bf0557 100644 --- a/Sources/SourceKitD/sourcekitd_uids.swift +++ b/Sources/SourceKitD/sourcekitd_uids.swift @@ -177,6 +177,7 @@ public struct sourcekitd_requests { public let codecomplete_update: sourcekitd_uid_t public let codecomplete_close: sourcekitd_uid_t public let cursorinfo: sourcekitd_uid_t + public let diagnostics: sourcekitd_uid_t public let expression_type: sourcekitd_uid_t public let find_usr: sourcekitd_uid_t public let variable_type: sourcekitd_uid_t @@ -194,6 +195,7 @@ public struct sourcekitd_requests { codecomplete_update = api.uid_get_from_cstr("source.request.codecomplete.update")! codecomplete_close = api.uid_get_from_cstr("source.request.codecomplete.close")! cursorinfo = api.uid_get_from_cstr("source.request.cursorinfo")! + diagnostics = api.uid_get_from_cstr("source.request.diagnostics")! expression_type = api.uid_get_from_cstr("source.request.expression.type")! find_usr = api.uid_get_from_cstr("source.request.editor.find_usr")! variable_type = api.uid_get_from_cstr("source.request.variable.type")! diff --git a/Sources/SourceKitLSP/CapabilityRegistry.swift b/Sources/SourceKitLSP/CapabilityRegistry.swift index 268b57954..16aff6185 100644 --- a/Sources/SourceKitLSP/CapabilityRegistry.swift +++ b/Sources/SourceKitLSP/CapabilityRegistry.swift @@ -62,8 +62,7 @@ public final class CapabilityRegistry { public var clientHasDynamicInlayHintRegistration: Bool { clientCapabilities.textDocument?.inlayHint?.dynamicRegistration == true } - - public var clientHasDocumentDiagnosticsRegistration: Bool { + public var clientHasDynamicDocumentDiagnosticsRegistration: Bool { clientCapabilities.textDocument?.diagnostic?.dynamicRegistration == true } @@ -75,6 +74,14 @@ public final class CapabilityRegistry { clientCapabilities.workspace?.didChangeWatchedFiles?.dynamicRegistration == true } + public var clientHasSemanticTokenRefreshSupport: Bool { + clientCapabilities.workspace?.semanticTokens?.refreshSupport == true + } + + public var clientHasDiagnosticsCodeDescriptionSupport: Bool { + clientCapabilities.textDocument?.publishDiagnostics?.codeDescriptionSupport == true + } + /// Dynamically register completion capabilities if the client supports it and /// we haven't yet registered any completion capabilities for the given /// languages. @@ -216,7 +223,7 @@ public final class CapabilityRegistry { for languages: [Language], registerOnClient: ClientRegistrationHandler ) { - guard clientHasDocumentDiagnosticsRegistration else { return } + guard clientHasDynamicDocumentDiagnosticsRegistration else { return } if let registration = registration(for: languages, in: pullDiagnostics) { if options != registration.diagnosticOptions { log("Unable to register new pull diagnostics options \(options) for " + @@ -266,13 +273,26 @@ public final class CapabilityRegistry { if registration.method == CompletionRequest.method { completion.removeValue(forKey: registration) } + if registration.method == FoldingRangeRequest.method { + foldingRange.removeValue(forKey: registration) + } if registration.method == SemanticTokensRegistrationOptions.method { semanticTokens.removeValue(forKey: registration) } + if registration.method == InlayHintRequest.method { + inlayHint.removeValue(forKey: registration) + } + if registration.method == DocumentDiagnosticsRequest.method { + pullDiagnostics.removeValue(forKey: registration) + } + } + + public func pullDiagnosticsRegistration(for language: Language) -> DiagnosticRegistrationOptions? { + registration(for: [language], in: pullDiagnostics) } - private func documentSelector(for langauges: [Language]) -> DocumentSelector { - return DocumentSelector(langauges.map { DocumentFilter(language: $0.rawValue) }) + private func documentSelector(for languages: [Language]) -> DocumentSelector { + return DocumentSelector(languages.map { DocumentFilter(language: $0.rawValue) }) } private func encode(_ options: T) -> LSPAny { diff --git a/Sources/SourceKitLSP/Clang/ClangLanguageServer.swift b/Sources/SourceKitLSP/Clang/ClangLanguageServer.swift index 9141f38af..f6acc61bf 100644 --- a/Sources/SourceKitLSP/Clang/ClangLanguageServer.swift +++ b/Sources/SourceKitLSP/Clang/ClangLanguageServer.swift @@ -103,7 +103,6 @@ final class ClangLanguageServerShim: LanguageServer, ToolchainLanguageServer { public init?( client: LocalConnection, toolchain: Toolchain, - clientCapabilities: ClientCapabilities?, options: SourceKitServer.Options, workspace: Workspace, reopenDocuments: @escaping (ToolchainLanguageServer) -> Void diff --git a/Sources/SourceKitLSP/SourceKitServer.swift b/Sources/SourceKitLSP/SourceKitServer.swift index 7a0f69c36..3ebe5ec26 100644 --- a/Sources/SourceKitLSP/SourceKitServer.swift +++ b/Sources/SourceKitLSP/SourceKitServer.swift @@ -1766,7 +1766,6 @@ func languageService( let server = try languageServerType.serverType.init( client: connectionToClient, toolchain: toolchain, - clientCapabilities: workspace.capabilityRegistry.clientCapabilities, options: options, workspace: workspace, reopenDocuments: reopenDocuments diff --git a/Sources/SourceKitLSP/Swift/CodeCompletion.swift b/Sources/SourceKitLSP/Swift/CodeCompletion.swift index 241cffddf..89ba7b2ca 100644 --- a/Sources/SourceKitLSP/Swift/CodeCompletion.swift +++ b/Sources/SourceKitLSP/Swift/CodeCompletion.swift @@ -139,8 +139,8 @@ extension SwiftLanguageServer { let typeName: String? = value[self.keys.typename] let docBrief: String? = value[self.keys.doc_brief] - let clientCompletionCapabilities = self.clientCapabilities.textDocument?.completion - let clientSupportsSnippets = clientCompletionCapabilities?.completionItem?.snippetSupport == true + let completionCapabilities = self.capabilityRegistry.clientCapabilities.textDocument?.completion + let clientSupportsSnippets = completionCapabilities?.completionItem?.snippetSupport == true let text = insertText.map { rewriteSourceKitPlaceholders(inString: $0, clientSupportsSnippets: clientSupportsSnippets) } diff --git a/Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift b/Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift index e9ca27268..a2acb5474 100644 --- a/Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift +++ b/Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift @@ -103,7 +103,7 @@ public final class SwiftLanguageServer: ToolchainLanguageServer { let sourcekitd: SourceKitD - let clientCapabilities: ClientCapabilities + let capabilityRegistry: CapabilityRegistry let serverOptions: SourceKitServer.Options @@ -122,6 +122,14 @@ public final class SwiftLanguageServer: ToolchainLanguageServer { var keys: sourcekitd_keys { return sourcekitd.keys } var requests: sourcekitd_requests { return sourcekitd.requests } var values: sourcekitd_values { return sourcekitd.values } + + var enablePublishDiagnostics: Bool { + // Since LSP 3.17.0, diagnostics can be reported through pull-based requests, + // in addition to the existing push-based publish notifications. + // If the client supports pull diagnostics, we report the capability + // and we should disable the publish notifications to avoid double-reporting. + return capabilityRegistry.pullDiagnosticsRegistration(for: .swift) == nil + } private var state: LanguageServerState { didSet { @@ -144,7 +152,6 @@ public final class SwiftLanguageServer: ToolchainLanguageServer { public init?( client: LocalConnection, toolchain: Toolchain, - clientCapabilities: ClientCapabilities?, options: SourceKitServer.Options, workspace: Workspace, reopenDocuments: @escaping (ToolchainLanguageServer) -> Void @@ -152,7 +159,7 @@ public final class SwiftLanguageServer: ToolchainLanguageServer { guard let sourcekitd = toolchain.sourcekitd else { return nil } self.client = client self.sourcekitd = try SourceKitDImpl.getOrCreate(dylibPath: sourcekitd) - self.clientCapabilities = clientCapabilities ?? ClientCapabilities(workspace: nil, textDocument: nil) + self.capabilityRegistry = workspace.capabilityRegistry self.serverOptions = options self.documentManager = DocumentManager() self.state = .connected @@ -242,7 +249,7 @@ public final class SwiftLanguageServer: ToolchainLanguageServer { /// Inform the client about changes to the syntax highlighting tokens. private func requestTokensRefresh() { - if clientCapabilities.workspace?.semanticTokens?.refreshSupport ?? false { + if capabilityRegistry.clientHasSemanticTokenRefreshSupport { _ = client.send(WorkspaceSemanticTokensRefreshRequest(), queue: queue) { result in if let error = result.failure { log("refreshing tokens failed: \(error)", level: .warning) @@ -285,8 +292,7 @@ public final class SwiftLanguageServer: ToolchainLanguageServer { let stageUID: sourcekitd_uid_t? = response[sourcekitd.keys.diagnostic_stage] let stage = stageUID.flatMap { DiagnosticStage($0, sourcekitd: sourcekitd) } ?? .sema - let supportsCodeDescription = - (clientCapabilities.textDocument?.publishDiagnostics?.codeDescriptionSupport == true) + let supportsCodeDescription = capabilityRegistry.clientHasDiagnosticsCodeDescriptionSupport // Note: we make the notification even if there are no diagnostics to clear the current state. var newDiags: [CachedDiagnostic] = [] @@ -326,7 +332,10 @@ public final class SwiftLanguageServer: ToolchainLanguageServer { req[keys.sourcetext] = "" if let dict = try? self.sourcekitd.sendSync(req) { - publishDiagnostics(response: dict, for: snapshot, compileCommand: compileCommand) + if (enablePublishDiagnostics) { + publishDiagnostics(response: dict, for: snapshot, compileCommand: compileCommand) + } + if dict[keys.diagnostic_stage] as sourcekitd_uid_t? == sourcekitd.values.diag_stage_sema { // Only update semantic tokens if the 0,0 replacetext request returned semantic information. updateSemanticTokens(response: dict, for: snapshot) @@ -370,7 +379,10 @@ extension SwiftLanguageServer { range: .bool(true), full: .bool(true)), inlayHintProvider: .value(InlayHintOptions( - resolveProvider: false)) + resolveProvider: false)), + diagnosticProvider: DiagnosticOptions( + interFileDependencies: true, + workspaceDiagnostics: false) )) } @@ -964,6 +976,7 @@ extension SwiftLanguageServer { } public func foldingRange(_ req: Request) { + let foldingRangeCapabilities = capabilityRegistry.clientCapabilities.textDocument?.foldingRange queue.async { guard let snapshot = self.documentManager.latestSnapshot(req.params.textDocument.uri) else { log("failed to find snapshot for url \(req.params.textDocument.uri)") @@ -1142,17 +1155,16 @@ extension SwiftLanguageServer { } } - let capabilities = self.clientCapabilities.textDocument?.foldingRange // If the limit is less than one, do nothing. - if let limit = capabilities?.rangeLimit, limit <= 0 { + if let limit = foldingRangeCapabilities?.rangeLimit, limit <= 0 { req.reply([]) return } let rangeFinder = FoldingRangeFinder( snapshot: snapshot, - rangeLimit: capabilities?.rangeLimit, - lineFoldingOnly: capabilities?.lineFoldingOnly ?? false) + rangeLimit: foldingRangeCapabilities?.rangeLimit, + lineFoldingOnly: foldingRangeCapabilities?.lineFoldingOnly ?? false) rangeFinder.walk(sourceFile) let ranges = rangeFinder.finalize() @@ -1167,12 +1179,12 @@ extension SwiftLanguageServer { ] let wantedActionKinds = req.params.context.only let providers = providersAndKinds.filter { wantedActionKinds?.contains($0.1) != false } + let codeActionCapabilities = capabilityRegistry.clientCapabilities.textDocument?.codeAction retrieveCodeActions(req, providers: providers.map { $0.provider }) { result in switch result { case .success(let codeActions): - let capabilities = self.clientCapabilities.textDocument?.codeAction let response = CodeActionRequestResponse(codeActions: codeActions, - clientCapabilities: capabilities) + clientCapabilities: codeActionCapabilities) req.reply(response) case .failure(let error): req.reply(.failure(error)) @@ -1332,6 +1344,76 @@ extension SwiftLanguageServer { req.reply(.failure(.unknown("Pull-model diagnostics not implemented yet."))) } + // Must be called on self.queue + public func _documentDiagnostic( + _ uri: DocumentURI, + _ completion: @escaping (Result<[Diagnostic], ResponseError>) -> Void + ) { + dispatchPrecondition(condition: .onQueue(queue)) + + guard let snapshot = documentManager.latestSnapshot(uri) else { + let msg = "failed to find snapshot for url \(uri)" + log(msg) + return completion(.failure(.unknown(msg))) + } + + let keys = self.keys + + let skreq = SKDRequestDictionary(sourcekitd: self.sourcekitd) + skreq[keys.request] = requests.diagnostics + skreq[keys.sourcefile] = snapshot.document.uri.pseudoPath + + // FIXME: SourceKit should probably cache this for us. + if let compileCommand = self.commandsByFile[uri] { + skreq[keys.compilerargs] = compileCommand.compilerArgs + } + + let supportsCodeDescription = capabilityRegistry.clientHasDiagnosticsCodeDescriptionSupport + + let handle = self.sourcekitd.send(skreq, self.queue) { response in + guard let dict = response.success else { + return completion(.failure(ResponseError(response.failure!))) + } + + var diagnostics: [Diagnostic] = [] + dict[keys.diagnostics]?.forEach { _, diag in + if let diagnostic = Diagnostic(diag, in: snapshot, useEducationalNoteAsCode: supportsCodeDescription) { + diagnostics.append(diagnostic) + } + return true + } + + completion(.success(diagnostics)) + } + + // FIXME: cancellation + _ = handle + } + + public func documentDiagnostic( + _ uri: DocumentURI, + _ completion: @escaping (Result<[Diagnostic], ResponseError>) -> Void + ) { + self.queue.async { + self._documentDiagnostic(uri, completion) + } + } + + public func documentDiagnostic(_ req: Request) { + let uri = req.params.textDocument.uri + documentDiagnostic(req.params.textDocument.uri) { result in + switch result { + case .success(let diagnostics): + req.reply(.full(.init(items: diagnostics))) + + case .failure(let error): + let message = "document diagnostic failed \(uri): \(error)" + log(message, level: .warning) + return req.reply(.failure(.unknown(message))) + } + } + } + public func executeCommand(_ req: Request) { let params = req.params //TODO: If there's support for several types of commands, we might need to structure this similarly to the code actions request. diff --git a/Sources/SourceKitLSP/ToolchainLanguageServer.swift b/Sources/SourceKitLSP/ToolchainLanguageServer.swift index 13d01706e..23482a62b 100644 --- a/Sources/SourceKitLSP/ToolchainLanguageServer.swift +++ b/Sources/SourceKitLSP/ToolchainLanguageServer.swift @@ -32,7 +32,6 @@ public protocol ToolchainLanguageServer: AnyObject { init?( client: LocalConnection, toolchain: Toolchain, - clientCapabilities: ClientCapabilities?, options: SourceKitServer.Options, workspace: Workspace, reopenDocuments: @escaping (ToolchainLanguageServer) -> Void diff --git a/Tests/SourceKitLSPTests/DiagnosticsTests.swift b/Tests/SourceKitLSPTests/PublishDiagnosticsTests.swift similarity index 98% rename from Tests/SourceKitLSPTests/DiagnosticsTests.swift rename to Tests/SourceKitLSPTests/PublishDiagnosticsTests.swift index 6cf7433b4..cc5286338 100644 --- a/Tests/SourceKitLSPTests/DiagnosticsTests.swift +++ b/Tests/SourceKitLSPTests/PublishDiagnosticsTests.swift @@ -15,7 +15,7 @@ import LSPTestSupport import SKTestSupport import XCTest -final class DiagnosticsTests: XCTestCase { +final class PublishDiagnosticsTests: XCTestCase { /// Connection and lifetime management for the service. var connection: TestSourceKitServer! = nil @@ -28,7 +28,7 @@ final class DiagnosticsTests: XCTestCase { override func setUp() { version = 0 - uri = DocumentURI(URL(fileURLWithPath: "/DiagnosticsTests/\(UUID()).swift")) + uri = DocumentURI(URL(fileURLWithPath: "/PublishDiagnosticsTests/\(UUID()).swift")) connection = TestSourceKitServer() sk = connection.client let documentCapabilities = TextDocumentClientCapabilities() diff --git a/Tests/SourceKitLSPTests/PullDiagnosticsTests.swift b/Tests/SourceKitLSPTests/PullDiagnosticsTests.swift new file mode 100644 index 000000000..36e7c59a8 --- /dev/null +++ b/Tests/SourceKitLSPTests/PullDiagnosticsTests.swift @@ -0,0 +1,83 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import LanguageServerProtocol +import LSPTestSupport +import SKTestSupport +import XCTest + +final class PullDiagnosticsTests: XCTestCase { + enum Error: Swift.Error { + case unexpectedDiagnosticReport + } + + /// Connection and lifetime management for the service. + var connection: TestSourceKitServer! = nil + + /// The primary interface to make requests to the SourceKitServer. + var sk: TestClient! = nil + + override func setUp() { + connection = TestSourceKitServer() + sk = connection.client + _ = try! sk.sendSync(InitializeRequest( + processId: nil, + rootPath: nil, + rootURI: nil, + initializationOptions: nil, + capabilities: ClientCapabilities(workspace: nil, textDocument: nil), + trace: .off, + workspaceFolders: nil + )) + } + + override func tearDown() { + sk = nil + connection = nil + } + + func performDiagnosticRequest(text: String) throws -> [Diagnostic] { + let url = URL(fileURLWithPath: "/PullDiagnostics/\(UUID()).swift") + + sk.send(DidOpenTextDocumentNotification(textDocument: TextDocumentItem( + uri: DocumentURI(url), + language: .swift, + version: 17, + text: text + ))) + + let request = DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(url)) + + let report: DocumentDiagnosticReport + do { + report = try sk.sendSync(request) + } catch let error as ResponseError where error.message.contains("unknown request: source.request.diagnostics") { + throw XCTSkip("toolchain does not support source.request.diagnostics request") + } + + guard case .full(let fullReport) = report else { + throw Error.unexpectedDiagnosticReport + } + + return fullReport.items + } + + func testUnknownIdentifierDiagnostic() throws { + let diagnostics = try performDiagnosticRequest(text: """ + func foo() { + invalid + } + """) + XCTAssertEqual(diagnostics.count, 1) + XCTAssertEqual(diagnostics[0].range, Position(line: 1, utf16index: 2).. Date: Mon, 12 Jun 2023 10:49:10 -0400 Subject: [PATCH 3/3] Fix bad merge resulting in duplicated method --- Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift b/Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift index a2acb5474..f416f692e 100644 --- a/Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift +++ b/Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift @@ -1338,11 +1338,6 @@ extension SwiftLanguageServer { } } } - - public func documentDiagnostic(_ req: Request) { - // TODO: Return provider object in initializeSync and implement pull-model document diagnostics here. - req.reply(.failure(.unknown("Pull-model diagnostics not implemented yet."))) - } // Must be called on self.queue public func _documentDiagnostic(