Skip to content

Commit d66705c

Browse files
authored
Merge pull request #746 from tristanlabelle/implement-pull-document-diagnostics
Implement pull-model documentDiagnostics
2 parents b7d4270 + ee043b5 commit d66705c

File tree

11 files changed

+222
-38
lines changed

11 files changed

+222
-38
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 & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public final class CapabilityRegistry {
6363
clientCapabilities.textDocument?.inlayHint?.dynamicRegistration == true
6464
}
6565

66-
public var clientHasDocumentDiagnosticsRegistration: Bool {
66+
public var clientHasDynamicDocumentDiagnosticsRegistration: Bool {
6767
clientCapabilities.textDocument?.diagnostic?.dynamicRegistration == true
6868
}
6969

@@ -75,6 +75,14 @@ public final class CapabilityRegistry {
7575
clientCapabilities.workspace?.didChangeWatchedFiles?.dynamicRegistration == true
7676
}
7777

78+
public var clientHasSemanticTokenRefreshSupport: Bool {
79+
clientCapabilities.workspace?.semanticTokens?.refreshSupport == true
80+
}
81+
82+
public var clientHasDiagnosticsCodeDescriptionSupport: Bool {
83+
clientCapabilities.textDocument?.publishDiagnostics?.codeDescriptionSupport == true
84+
}
85+
7886
/// Dynamically register completion capabilities if the client supports it and
7987
/// we haven't yet registered any completion capabilities for the given
8088
/// languages.
@@ -216,7 +224,7 @@ public final class CapabilityRegistry {
216224
for languages: [Language],
217225
registerOnClient: ClientRegistrationHandler
218226
) {
219-
guard clientHasDocumentDiagnosticsRegistration else { return }
227+
guard clientHasDynamicDocumentDiagnosticsRegistration else { return }
220228
if let registration = registration(for: languages, in: pullDiagnostics) {
221229
if options != registration.diagnosticOptions {
222230
log("Unable to register new pull diagnostics options \(options) for " +
@@ -266,13 +274,26 @@ public final class CapabilityRegistry {
266274
if registration.method == CompletionRequest.method {
267275
completion.removeValue(forKey: registration)
268276
}
277+
if registration.method == FoldingRangeRequest.method {
278+
foldingRange.removeValue(forKey: registration)
279+
}
269280
if registration.method == SemanticTokensRegistrationOptions.method {
270281
semanticTokens.removeValue(forKey: registration)
271282
}
283+
if registration.method == InlayHintRequest.method {
284+
inlayHint.removeValue(forKey: registration)
285+
}
286+
if registration.method == DocumentDiagnosticsRequest.method {
287+
pullDiagnostics.removeValue(forKey: registration)
288+
}
289+
}
290+
291+
public func pullDiagnosticsRegistration(for language: Language) -> DiagnosticRegistrationOptions? {
292+
registration(for: [language], in: pullDiagnostics)
272293
}
273294

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

278299
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: 93 additions & 16 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))
@@ -1326,10 +1338,75 @@ extension SwiftLanguageServer {
13261338
}
13271339
}
13281340
}
1341+
1342+
// Must be called on self.queue
1343+
public func _documentDiagnostic(
1344+
_ uri: DocumentURI,
1345+
_ completion: @escaping (Result<[Diagnostic], ResponseError>) -> Void
1346+
) {
1347+
dispatchPrecondition(condition: .onQueue(queue))
1348+
1349+
guard let snapshot = documentManager.latestSnapshot(uri) else {
1350+
let msg = "failed to find snapshot for url \(uri)"
1351+
log(msg)
1352+
return completion(.failure(.unknown(msg)))
1353+
}
1354+
1355+
let keys = self.keys
1356+
1357+
let skreq = SKDRequestDictionary(sourcekitd: self.sourcekitd)
1358+
skreq[keys.request] = requests.diagnostics
1359+
skreq[keys.sourcefile] = snapshot.document.uri.pseudoPath
1360+
1361+
// FIXME: SourceKit should probably cache this for us.
1362+
if let compileCommand = self.commandsByFile[uri] {
1363+
skreq[keys.compilerargs] = compileCommand.compilerArgs
1364+
}
1365+
1366+
let supportsCodeDescription = capabilityRegistry.clientHasDiagnosticsCodeDescriptionSupport
1367+
1368+
let handle = self.sourcekitd.send(skreq, self.queue) { response in
1369+
guard let dict = response.success else {
1370+
return completion(.failure(ResponseError(response.failure!)))
1371+
}
1372+
1373+
var diagnostics: [Diagnostic] = []
1374+
dict[keys.diagnostics]?.forEach { _, diag in
1375+
if let diagnostic = Diagnostic(diag, in: snapshot, useEducationalNoteAsCode: supportsCodeDescription) {
1376+
diagnostics.append(diagnostic)
1377+
}
1378+
return true
1379+
}
1380+
1381+
completion(.success(diagnostics))
1382+
}
1383+
1384+
// FIXME: cancellation
1385+
_ = handle
1386+
}
13291387

1388+
public func documentDiagnostic(
1389+
_ uri: DocumentURI,
1390+
_ completion: @escaping (Result<[Diagnostic], ResponseError>) -> Void
1391+
) {
1392+
self.queue.async {
1393+
self._documentDiagnostic(uri, completion)
1394+
}
1395+
}
1396+
13301397
public func documentDiagnostic(_ req: Request<DocumentDiagnosticsRequest>) {
1331-
// TODO: Return provider object in initializeSync and implement pull-model document diagnostics here.
1332-
req.reply(.failure(.unknown("Pull-model diagnostics not implemented yet.")))
1398+
let uri = req.params.textDocument.uri
1399+
documentDiagnostic(req.params.textDocument.uri) { result in
1400+
switch result {
1401+
case .success(let diagnostics):
1402+
req.reply(.full(.init(items: diagnostics)))
1403+
1404+
case .failure(let error):
1405+
let message = "document diagnostic failed \(uri): \(error)"
1406+
log(message, level: .warning)
1407+
return req.reply(.failure(.unknown(message)))
1408+
}
1409+
}
13331410
}
13341411

13351412
public func executeCommand(_ req: Request<ExecuteCommandRequest>) {

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)