Skip to content
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

Implement pull-model documentDiagnostics #746

Merged
merged 12 commits into from
Jun 1, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?

Expand All @@ -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 {
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
29 changes: 25 additions & 4 deletions Sources/SourceKitLSP/CapabilityRegistry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public final class CapabilityRegistry {
clientCapabilities.textDocument?.inlayHint?.dynamicRegistration == true
}

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

Expand All @@ -75,6 +75,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 @@ -216,7 +224,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 " +
Expand Down Expand Up @@ -266,13 +274,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
1 change: 0 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
1 change: 0 additions & 1 deletion Sources/SourceKitLSP/SourceKitServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
109 changes: 93 additions & 16 deletions Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ public final class SwiftLanguageServer: ToolchainLanguageServer {

let sourcekitd: SourceKitD

let clientCapabilities: ClientCapabilities
let capabilityRegistry: CapabilityRegistry

let serverOptions: SourceKitServer.Options

Expand All @@ -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.
Comment on lines +127 to +130
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it documented somewhere that LSP servers shouldn’t publish diagnostics if the client supports pullDiagnostics? Or, if not, do you know if other LSP servers are also doing this as well or if VSCode is relying on it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the two can be mix-and-matched if they use different provider names, but it wouldn't be desirable here because we would be reporting the same diagnostics twice, and the push-model ones would still have the same staleness issue being addressed by pull-model.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m just worried that some LSP client says that it supports pull diagnostics but for some reason still expects that diagnostics get published. That’s why I’d like to know what other LSP servers do…

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return capabilityRegistry.pullDiagnosticsRegistration(for: .swift) == nil
}

private var state: LanguageServerState {
didSet {
Expand All @@ -144,15 +152,14 @@ public final class SwiftLanguageServer: ToolchainLanguageServer {
public init?(
client: LocalConnection,
toolchain: Toolchain,
clientCapabilities: ClientCapabilities?,
options: SourceKitServer.Options,
workspace: Workspace,
reopenDocuments: @escaping (ToolchainLanguageServer) -> Void
) throws {
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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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] = []
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
))
}

Expand Down Expand Up @@ -964,6 +976,7 @@ extension SwiftLanguageServer {
}

public func foldingRange(_ req: Request<FoldingRangeRequest>) {
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)")
Expand Down Expand Up @@ -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()

Expand All @@ -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))
Expand Down Expand Up @@ -1326,10 +1338,75 @@ extension SwiftLanguageServer {
}
}
}

// 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
Comment on lines +1384 to +1385
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we leave this to a PR that fixes all cancellation at once? This was copied from many places

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me as well

}

public func documentDiagnostic(
_ uri: DocumentURI,
_ completion: @escaping (Result<[Diagnostic], ResponseError>) -> Void
) {
self.queue.async {
self._documentDiagnostic(uri, completion)
}
}

public func documentDiagnostic(_ req: Request<DocumentDiagnosticsRequest>) {
// TODO: Return provider object in initializeSync and implement pull-model document diagnostics here.
req.reply(.failure(.unknown("Pull-model diagnostics not implemented yet.")))
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<ExecuteCommandRequest>) {
Expand Down
1 change: 0 additions & 1 deletion Sources/SourceKitLSP/ToolchainLanguageServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ public protocol ToolchainLanguageServer: AnyObject {
init?(
client: LocalConnection,
toolchain: Toolchain,
clientCapabilities: ClientCapabilities?,
options: SourceKitServer.Options,
workspace: Workspace,
reopenDocuments: @escaping (ToolchainLanguageServer) -> Void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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()
Expand Down
Loading