Skip to content

Commit c076bd5

Browse files
Implement pull-model documentDiagnostics
1 parent ea9f173 commit c076bd5

File tree

11 files changed

+225
-37
lines changed

11 files changed

+225
-37
lines changed

Sources/LanguageServerProtocol/SupportTypes/RegistrationOptions.swift

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -171,12 +171,7 @@ public struct DiagnosticRegistrationOptions: RegistrationOptions, TextDocumentRe
171171

172172
public func encodeIntoLSPAny(dict: inout [String: LSPAny]) {
173173
textDocumentRegistrationOptions.encodeIntoLSPAny(dict: &dict)
174-
175-
dict["interFileDependencies"] = .bool(diagnosticOptions.interFileDependencies)
176-
dict["workspaceDiagnostics"] = .bool(diagnosticOptions.workspaceDiagnostics)
177-
if let workDoneProgress = diagnosticOptions.workDoneProgress {
178-
dict["workDoneProgress"] = .bool(workDoneProgress)
179-
}
174+
diagnosticOptions.encodeIntoLSPAny(dict: &dict)
180175
}
181176
}
182177

Sources/LanguageServerProtocol/SupportTypes/ServerCapabilities.swift

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -868,9 +868,6 @@ public struct DiagnosticOptions: WorkDoneProgressOptions, Codable, Hashable {
868868
/// The server provides support for workspace diagnostics as well.
869869
public var workspaceDiagnostics: Bool
870870

871-
/// 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.
872-
public var documentSelector: DocumentSelector?
873-
874871
/// The id used to register the request. The id can be used to deregister the request again. See also Registration#id
875872
public var id: String?
876873

@@ -880,17 +877,29 @@ public struct DiagnosticOptions: WorkDoneProgressOptions, Codable, Hashable {
880877
identifier: String? = nil,
881878
interFileDependencies: Bool,
882879
workspaceDiagnostics: Bool,
883-
documentSelector: DocumentSelector? = nil,
884880
id: String? = nil,
885881
workDoneProgress: Bool? = nil
886882
) {
887883
self.identifier = identifier
888884
self.interFileDependencies = interFileDependencies
889885
self.workspaceDiagnostics = workspaceDiagnostics
890-
self.documentSelector = documentSelector
891886
self.id = id
892887
self.workDoneProgress = workDoneProgress
893888
}
889+
890+
public func encodeIntoLSPAny(dict: inout [String: LSPAny]) {
891+
if let identifier = identifier {
892+
dict["identifier"] = .string(identifier)
893+
}
894+
dict["interFileDependencies"] = .bool(interFileDependencies)
895+
dict["workspaceDiagnostics"] = .bool(workspaceDiagnostics)
896+
if let id = id {
897+
dict["id"] = .string(id)
898+
}
899+
if let workDoneProgress = workDoneProgress {
900+
dict["workDoneProgress"] = .bool(workDoneProgress)
901+
}
902+
}
894903
}
895904

896905
public struct WorkspaceServerCapabilities: Codable, Hashable {

Sources/SourceKitD/sourcekitd_uids.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ public struct sourcekitd_requests {
177177
public let codecomplete_update: sourcekitd_uid_t
178178
public let codecomplete_close: sourcekitd_uid_t
179179
public let cursorinfo: sourcekitd_uid_t
180+
public let diagnostics: sourcekitd_uid_t
180181
public let expression_type: sourcekitd_uid_t
181182
public let find_usr: sourcekitd_uid_t
182183
public let variable_type: sourcekitd_uid_t
@@ -194,6 +195,7 @@ public struct sourcekitd_requests {
194195
codecomplete_update = api.uid_get_from_cstr("source.request.codecomplete.update")!
195196
codecomplete_close = api.uid_get_from_cstr("source.request.codecomplete.close")!
196197
cursorinfo = api.uid_get_from_cstr("source.request.cursorinfo")!
198+
diagnostics = api.uid_get_from_cstr("source.request.diagnostics")!
197199
expression_type = api.uid_get_from_cstr("source.request.expression.type")!
198200
find_usr = api.uid_get_from_cstr("source.request.editor.find_usr")!
199201
variable_type = api.uid_get_from_cstr("source.request.variable.type")!

Sources/SourceKitLSP/CapabilityRegistry.swift

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,7 @@ public final class CapabilityRegistry {
6262
public var clientHasDynamicInlayHintRegistration: Bool {
6363
clientCapabilities.textDocument?.inlayHint?.dynamicRegistration == true
6464
}
65-
66-
public var clientHasDocumentDiagnosticsRegistration: Bool {
65+
public var clientHasDynamicDocumentDiagnosticsRegistration: Bool {
6766
clientCapabilities.textDocument?.diagnostic?.dynamicRegistration == true
6867
}
6968

@@ -75,6 +74,14 @@ public final class CapabilityRegistry {
7574
clientCapabilities.workspace?.didChangeWatchedFiles?.dynamicRegistration == true
7675
}
7776

77+
public var clientHasSemanticTokenRefreshSupport: Bool {
78+
clientCapabilities.workspace?.semanticTokens?.refreshSupport == true
79+
}
80+
81+
public var clientHasDiagnosticsCodeDescriptionSupport: Bool {
82+
clientCapabilities.textDocument?.publishDiagnostics?.codeDescriptionSupport == true
83+
}
84+
7885
/// Dynamically register completion capabilities if the client supports it and
7986
/// we haven't yet registered any completion capabilities for the given
8087
/// languages.
@@ -216,7 +223,7 @@ public final class CapabilityRegistry {
216223
for languages: [Language],
217224
registerOnClient: ClientRegistrationHandler
218225
) {
219-
guard clientHasDocumentDiagnosticsRegistration else { return }
226+
guard clientHasDynamicDocumentDiagnosticsRegistration else { return }
220227
if let registration = registration(for: languages, in: pullDiagnostics) {
221228
if options != registration.diagnosticOptions {
222229
log("Unable to register new pull diagnostics options \(options) for " +
@@ -266,13 +273,26 @@ public final class CapabilityRegistry {
266273
if registration.method == CompletionRequest.method {
267274
completion.removeValue(forKey: registration)
268275
}
276+
if registration.method == FoldingRangeRequest.method {
277+
foldingRange.removeValue(forKey: registration)
278+
}
269279
if registration.method == SemanticTokensRegistrationOptions.method {
270280
semanticTokens.removeValue(forKey: registration)
271281
}
282+
if registration.method == InlayHintRequest.method {
283+
inlayHint.removeValue(forKey: registration)
284+
}
285+
if registration.method == DocumentDiagnosticsRequest.method {
286+
pullDiagnostics.removeValue(forKey: registration)
287+
}
288+
}
289+
290+
public func pullDiagnosticsRegistration(for language: Language) -> DiagnosticRegistrationOptions? {
291+
registration(for: [language], in: pullDiagnostics)
272292
}
273293

274-
private func documentSelector(for langauges: [Language]) -> DocumentSelector {
275-
return DocumentSelector(langauges.map { DocumentFilter(language: $0.rawValue) })
294+
private func documentSelector(for languages: [Language]) -> DocumentSelector {
295+
return DocumentSelector(languages.map { DocumentFilter(language: $0.rawValue) })
276296
}
277297

278298
private func encode<T: RegistrationOptions>(_ options: T) -> LSPAny {

Sources/SourceKitLSP/Clang/ClangLanguageServer.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,6 @@ final class ClangLanguageServerShim: LanguageServer, ToolchainLanguageServer {
103103
public init?(
104104
client: LocalConnection,
105105
toolchain: Toolchain,
106-
clientCapabilities: ClientCapabilities?,
107106
options: SourceKitServer.Options,
108107
workspace: Workspace,
109108
reopenDocuments: @escaping (ToolchainLanguageServer) -> Void

Sources/SourceKitLSP/SourceKitServer.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1766,7 +1766,6 @@ func languageService(
17661766
let server = try languageServerType.serverType.init(
17671767
client: connectionToClient,
17681768
toolchain: toolchain,
1769-
clientCapabilities: workspace.capabilityRegistry.clientCapabilities,
17701769
options: options,
17711770
workspace: workspace,
17721771
reopenDocuments: reopenDocuments

Sources/SourceKitLSP/Swift/CodeCompletion.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,8 @@ extension SwiftLanguageServer {
139139
let typeName: String? = value[self.keys.typename]
140140
let docBrief: String? = value[self.keys.doc_brief]
141141

142-
let clientCompletionCapabilities = self.clientCapabilities.textDocument?.completion
143-
let clientSupportsSnippets = clientCompletionCapabilities?.completionItem?.snippetSupport == true
142+
let completionCapabilities = self.capabilityRegistry.clientCapabilities.textDocument?.completion
143+
let clientSupportsSnippets = completionCapabilities?.completionItem?.snippetSupport == true
144144
let text = insertText.map {
145145
rewriteSourceKitPlaceholders(inString: $0, clientSupportsSnippets: clientSupportsSnippets)
146146
}

Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift

Lines changed: 96 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ public final class SwiftLanguageServer: ToolchainLanguageServer {
103103

104104
let sourcekitd: SourceKitD
105105

106-
let clientCapabilities: ClientCapabilities
106+
let capabilityRegistry: CapabilityRegistry
107107

108108
let serverOptions: SourceKitServer.Options
109109

@@ -122,6 +122,14 @@ public final class SwiftLanguageServer: ToolchainLanguageServer {
122122
var keys: sourcekitd_keys { return sourcekitd.keys }
123123
var requests: sourcekitd_requests { return sourcekitd.requests }
124124
var values: sourcekitd_values { return sourcekitd.values }
125+
126+
var enablePublishDiagnostics: Bool {
127+
// Since LSP 3.17.0, diagnostics can be reported through pull-based requests,
128+
// in addition to the existing push-based publish notifications.
129+
// If the client supports pull diagnostics, we report the capability
130+
// and we should disable the publish notifications to avoid double-reporting.
131+
return capabilityRegistry.pullDiagnosticsRegistration(for: .swift) == nil
132+
}
125133

126134
private var state: LanguageServerState {
127135
didSet {
@@ -144,15 +152,14 @@ public final class SwiftLanguageServer: ToolchainLanguageServer {
144152
public init?(
145153
client: LocalConnection,
146154
toolchain: Toolchain,
147-
clientCapabilities: ClientCapabilities?,
148155
options: SourceKitServer.Options,
149156
workspace: Workspace,
150157
reopenDocuments: @escaping (ToolchainLanguageServer) -> Void
151158
) throws {
152159
guard let sourcekitd = toolchain.sourcekitd else { return nil }
153160
self.client = client
154161
self.sourcekitd = try SourceKitDImpl.getOrCreate(dylibPath: sourcekitd)
155-
self.clientCapabilities = clientCapabilities ?? ClientCapabilities(workspace: nil, textDocument: nil)
162+
self.capabilityRegistry = workspace.capabilityRegistry
156163
self.serverOptions = options
157164
self.documentManager = DocumentManager()
158165
self.state = .connected
@@ -242,7 +249,7 @@ public final class SwiftLanguageServer: ToolchainLanguageServer {
242249

243250
/// Inform the client about changes to the syntax highlighting tokens.
244251
private func requestTokensRefresh() {
245-
if clientCapabilities.workspace?.semanticTokens?.refreshSupport ?? false {
252+
if capabilityRegistry.clientHasSemanticTokenRefreshSupport {
246253
_ = client.send(WorkspaceSemanticTokensRefreshRequest(), queue: queue) { result in
247254
if let error = result.failure {
248255
log("refreshing tokens failed: \(error)", level: .warning)
@@ -285,8 +292,7 @@ public final class SwiftLanguageServer: ToolchainLanguageServer {
285292
let stageUID: sourcekitd_uid_t? = response[sourcekitd.keys.diagnostic_stage]
286293
let stage = stageUID.flatMap { DiagnosticStage($0, sourcekitd: sourcekitd) } ?? .sema
287294

288-
let supportsCodeDescription =
289-
(clientCapabilities.textDocument?.publishDiagnostics?.codeDescriptionSupport == true)
295+
let supportsCodeDescription = capabilityRegistry.clientHasDiagnosticsCodeDescriptionSupport
290296

291297
// Note: we make the notification even if there are no diagnostics to clear the current state.
292298
var newDiags: [CachedDiagnostic] = []
@@ -326,7 +332,10 @@ public final class SwiftLanguageServer: ToolchainLanguageServer {
326332
req[keys.sourcetext] = ""
327333

328334
if let dict = try? self.sourcekitd.sendSync(req) {
329-
publishDiagnostics(response: dict, for: snapshot, compileCommand: compileCommand)
335+
if (enablePublishDiagnostics) {
336+
publishDiagnostics(response: dict, for: snapshot, compileCommand: compileCommand)
337+
}
338+
330339
if dict[keys.diagnostic_stage] as sourcekitd_uid_t? == sourcekitd.values.diag_stage_sema {
331340
// Only update semantic tokens if the 0,0 replacetext request returned semantic information.
332341
updateSemanticTokens(response: dict, for: snapshot)
@@ -370,7 +379,10 @@ extension SwiftLanguageServer {
370379
range: .bool(true),
371380
full: .bool(true)),
372381
inlayHintProvider: .value(InlayHintOptions(
373-
resolveProvider: false))
382+
resolveProvider: false)),
383+
diagnosticProvider: DiagnosticOptions(
384+
interFileDependencies: true,
385+
workspaceDiagnostics: false)
374386
))
375387
}
376388

@@ -964,6 +976,7 @@ extension SwiftLanguageServer {
964976
}
965977

966978
public func foldingRange(_ req: Request<FoldingRangeRequest>) {
979+
let foldingRangeCapabilities = capabilityRegistry.clientCapabilities.textDocument?.foldingRange
967980
queue.async {
968981
guard let snapshot = self.documentManager.latestSnapshot(req.params.textDocument.uri) else {
969982
log("failed to find snapshot for url \(req.params.textDocument.uri)")
@@ -1142,17 +1155,16 @@ extension SwiftLanguageServer {
11421155
}
11431156
}
11441157

1145-
let capabilities = self.clientCapabilities.textDocument?.foldingRange
11461158
// If the limit is less than one, do nothing.
1147-
if let limit = capabilities?.rangeLimit, limit <= 0 {
1159+
if let limit = foldingRangeCapabilities?.rangeLimit, limit <= 0 {
11481160
req.reply([])
11491161
return
11501162
}
11511163

11521164
let rangeFinder = FoldingRangeFinder(
11531165
snapshot: snapshot,
1154-
rangeLimit: capabilities?.rangeLimit,
1155-
lineFoldingOnly: capabilities?.lineFoldingOnly ?? false)
1166+
rangeLimit: foldingRangeCapabilities?.rangeLimit,
1167+
lineFoldingOnly: foldingRangeCapabilities?.lineFoldingOnly ?? false)
11561168
rangeFinder.walk(sourceFile)
11571169
let ranges = rangeFinder.finalize()
11581170

@@ -1167,12 +1179,12 @@ extension SwiftLanguageServer {
11671179
]
11681180
let wantedActionKinds = req.params.context.only
11691181
let providers = providersAndKinds.filter { wantedActionKinds?.contains($0.1) != false }
1182+
let codeActionCapabilities = capabilityRegistry.clientCapabilities.textDocument?.codeAction
11701183
retrieveCodeActions(req, providers: providers.map { $0.provider }) { result in
11711184
switch result {
11721185
case .success(let codeActions):
1173-
let capabilities = self.clientCapabilities.textDocument?.codeAction
11741186
let response = CodeActionRequestResponse(codeActions: codeActions,
1175-
clientCapabilities: capabilities)
1187+
clientCapabilities: codeActionCapabilities)
11761188
req.reply(response)
11771189
case .failure(let error):
11781190
req.reply(.failure(error))
@@ -1332,6 +1344,76 @@ extension SwiftLanguageServer {
13321344
req.reply(.failure(.unknown("Pull-model diagnostics not implemented yet.")))
13331345
}
13341346

1347+
// Must be called on self.queue
1348+
public func _documentDiagnostic(
1349+
_ uri: DocumentURI,
1350+
_ completion: @escaping (Result<[Diagnostic], ResponseError>) -> Void
1351+
) {
1352+
dispatchPrecondition(condition: .onQueue(queue))
1353+
1354+
guard let snapshot = documentManager.latestSnapshot(uri) else {
1355+
let msg = "failed to find snapshot for url \(uri)"
1356+
log(msg)
1357+
return completion(.failure(.unknown(msg)))
1358+
}
1359+
1360+
let keys = self.keys
1361+
1362+
let skreq = SKDRequestDictionary(sourcekitd: self.sourcekitd)
1363+
skreq[keys.request] = requests.diagnostics
1364+
skreq[keys.sourcefile] = snapshot.document.uri.pseudoPath
1365+
1366+
// FIXME: SourceKit should probably cache this for us.
1367+
if let compileCommand = self.commandsByFile[uri] {
1368+
skreq[keys.compilerargs] = compileCommand.compilerArgs
1369+
}
1370+
1371+
let supportsCodeDescription = capabilityRegistry.clientHasDiagnosticsCodeDescriptionSupport
1372+
1373+
let handle = self.sourcekitd.send(skreq, self.queue) { response in
1374+
guard let dict = response.success else {
1375+
return completion(.failure(ResponseError(response.failure!)))
1376+
}
1377+
1378+
var diagnostics: [Diagnostic] = []
1379+
dict[keys.diagnostics]?.forEach { _, diag in
1380+
if let diagnostic = Diagnostic(diag, in: snapshot, useEducationalNoteAsCode: supportsCodeDescription) {
1381+
diagnostics.append(diagnostic)
1382+
}
1383+
return true
1384+
}
1385+
1386+
completion(.success(diagnostics))
1387+
}
1388+
1389+
// FIXME: cancellation
1390+
_ = handle
1391+
}
1392+
1393+
public func documentDiagnostic(
1394+
_ uri: DocumentURI,
1395+
_ completion: @escaping (Result<[Diagnostic], ResponseError>) -> Void
1396+
) {
1397+
self.queue.async {
1398+
self._documentDiagnostic(uri, completion)
1399+
}
1400+
}
1401+
1402+
public func documentDiagnostic(_ req: Request<DocumentDiagnosticsRequest>) {
1403+
let uri = req.params.textDocument.uri
1404+
documentDiagnostic(req.params.textDocument.uri) { result in
1405+
switch result {
1406+
case .success(let diagnostics):
1407+
req.reply(.full(.init(items: diagnostics)))
1408+
1409+
case .failure(let error):
1410+
let message = "document diagnostic failed \(uri): \(error)"
1411+
log(message, level: .warning)
1412+
return req.reply(.failure(.unknown(message)))
1413+
}
1414+
}
1415+
}
1416+
13351417
public func executeCommand(_ req: Request<ExecuteCommandRequest>) {
13361418
let params = req.params
13371419
//TODO: If there's support for several types of commands, we might need to structure this similarly to the code actions request.

Sources/SourceKitLSP/ToolchainLanguageServer.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ public protocol ToolchainLanguageServer: AnyObject {
3232
init?(
3333
client: LocalConnection,
3434
toolchain: Toolchain,
35-
clientCapabilities: ClientCapabilities?,
3635
options: SourceKitServer.Options,
3736
workspace: Workspace,
3837
reopenDocuments: @escaping (ToolchainLanguageServer) -> Void

Tests/SourceKitLSPTests/DiagnosticsTests.swift renamed to Tests/SourceKitLSPTests/PublishDiagnosticsTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import LSPTestSupport
1515
import SKTestSupport
1616
import XCTest
1717

18-
final class DiagnosticsTests: XCTestCase {
18+
final class PublishDiagnosticsTests: XCTestCase {
1919
/// Connection and lifetime management for the service.
2020
var connection: TestSourceKitServer! = nil
2121

@@ -28,7 +28,7 @@ final class DiagnosticsTests: XCTestCase {
2828

2929
override func setUp() {
3030
version = 0
31-
uri = DocumentURI(URL(fileURLWithPath: "/DiagnosticsTests/\(UUID()).swift"))
31+
uri = DocumentURI(URL(fileURLWithPath: "/PublishDiagnosticsTests/\(UUID()).swift"))
3232
connection = TestSourceKitServer()
3333
sk = connection.client
3434
let documentCapabilities = TextDocumentClientCapabilities()

0 commit comments

Comments
 (0)