Skip to content

Implement pull-model documentDiagnostics (cherry-pick from main) #751

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -624,5 +637,6 @@ public struct TextDocumentClientCapabilities: Hashable, Codable {
self.callHierarchy = callHierarchy
self.semanticTokens = semanticTokens
self.inlayHint = inlayHint
self.diagnostic = diagnostic
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,25 @@ 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)
diagnosticOptions.encodeIntoLSPAny(dict: &dict)
}
}

/// Describe options to be used when registering for file system change events.
public struct DidChangeWatchedFilesRegistrationOptions: RegistrationOptions {
/// The watchers to register.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ public struct ServerCapabilities: Codable, Hashable {
/// Whether the server supports the `textDocument/inlayHint` family of requests.
public var inlayHintProvider: ValueOrBool<InlayHintOptions>?

/// Whether the server supports the `textDocument/diagnostic` request.
public var diagnosticProvider: DiagnosticOptions?

/// Whether the server provides selection range support.
public var selectionRangeProvider: ValueOrBool<TextDocumentAndStaticRegistrationOptions>?

Expand Down Expand Up @@ -152,6 +155,7 @@ public struct ServerCapabilities: Codable, Hashable {
typeHierarchyProvider: ValueOrBool<TextDocumentAndStaticRegistrationOptions>? = nil,
semanticTokensProvider: SemanticTokensOptions? = nil,
inlayHintProvider: ValueOrBool<InlayHintOptions>? = nil,
diagnosticProvider: DiagnosticOptions? = nil,
selectionRangeProvider: ValueOrBool<TextDocumentAndStaticRegistrationOptions>? = nil,
linkedEditingRangeProvider: ValueOrBool<TextDocumentAndStaticRegistrationOptions>? = nil,
monikerProvider: ValueOrBool<MonikerOptions>? = nil,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -863,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?

Expand All @@ -875,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 {
Expand Down
2 changes: 2 additions & 0 deletions Sources/SourceKitD/sourcekitd_uids.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")!
Expand Down
60 changes: 57 additions & 3 deletions Sources/SourceKitLSP/CapabilityRegistry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -59,7 +62,10 @@ public final class CapabilityRegistry {
public var clientHasDynamicInlayHintRegistration: Bool {
clientCapabilities.textDocument?.inlayHint?.dynamicRegistration == true
}

public var clientHasDynamicDocumentDiagnosticsRegistration: Bool {
clientCapabilities.textDocument?.diagnostic?.dynamicRegistration == true
}

public var clientHasDynamicExecuteCommandRegistration: Bool {
clientCapabilities.workspace?.executeCommand?.dynamicRegistration == true
}
Expand All @@ -68,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.
Expand Down Expand Up @@ -202,6 +216,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 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 " +
"\(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(
Expand Down Expand Up @@ -232,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<T: RegistrationOptions>(_ options: T) -> LSPAny {
Expand Down
5 changes: 4 additions & 1 deletion Sources/SourceKitLSP/Clang/ClangLanguageServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -515,6 +514,10 @@ extension ClangLanguageServerShim {
forwardRequestToClangdOnQueue(req)
}

func documentDiagnostic(_ req: Request<DocumentDiagnosticsRequest>) {
forwardRequestToClangdOnQueue(req)
}

func foldingRange(_ req: Request<FoldingRangeRequest>) {
queue.async {
if self.capabilities?.foldingRangeProvider?.isSupported == true {
Expand Down
16 changes: 15 additions & 1 deletion Sources/SourceKitLSP/SourceKitServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -1209,6 +1216,14 @@ extension SourceKitServer {
languageService.inlayHint(req)
}

func documentDiagnostic(
_ req: Request<DocumentDiagnosticsRequest>,
workspace: Workspace,
languageService: ToolchainLanguageServer
) {
languageService.documentDiagnostic(req)
}

/// Converts a location from the symbol index to an LSP location.
///
/// - Parameter location: The symbol index location
Expand Down Expand Up @@ -1751,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
Expand Down
4 changes: 2 additions & 2 deletions Sources/SourceKitLSP/Swift/CodeCompletion.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Loading