Skip to content

Commit d29d125

Browse files
authored
Add support for clangd commands + code actions (#429)
This adds support for clangd commands for clients which support dynamic registration (including VS Code), as well as fixes an issue which prevented clangd's code actions from working. Also added a test to ensure the clangd code actions work, as well as regenerated the Linux test main (which was missing some other newly added tests).
1 parent 1cb9e07 commit d29d125

File tree

9 files changed

+150
-37
lines changed

9 files changed

+150
-37
lines changed

Sources/LanguageServerProtocol/Messages.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ public let builtinRequests: [_RequestType.Type] = [
4242
ColorPresentationRequest.self,
4343
CodeActionRequest.self,
4444
ExecuteCommandRequest.self,
45+
ApplyEditRequest.self,
46+
RegisterCapabilityRequest.self,
47+
UnregisterCapabilityRequest.self,
4548

4649
// MARK: LSP Extension Requests
4750

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
enum Color { RED, GREEN, BLUE };
2+
3+
void someFunction(Color color) {
4+
switch /*SwitchColor*/(color)/*SwitchColor:end*/ {}
5+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{ "sources": ["main.cpp"] }

Sources/SourceKitLSP/CapabilityRegistry.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ public final class CapabilityRegistry {
2929
/// Dynamically registered semantic tokens options.
3030
private var semanticTokens: [CapabilityRegistration: SemanticTokensRegistrationOptions] = [:]
3131

32+
/// Dynamically registered command IDs.
33+
private var commandIds: Set<String> = []
34+
3235
public let clientCapabilities: ClientCapabilities
3336

3437
public init(clientCapabilities: ClientCapabilities) {
@@ -47,6 +50,10 @@ public final class CapabilityRegistry {
4750
clientCapabilities.textDocument?.semanticTokens?.dynamicRegistration == true
4851
}
4952

53+
public var clientHasDynamicExecuteCommandRegistration: Bool {
54+
clientCapabilities.workspace?.executeCommand?.dynamicRegistration == true
55+
}
56+
5057
/// Dynamically register completion capabilities if the client supports it and
5158
/// we haven't yet registered any completion capabilities for the given
5259
/// languages.
@@ -131,6 +138,30 @@ public final class CapabilityRegistry {
131138
registerOnClient(registration)
132139
}
133140

141+
/// Dynamically register executeCommand with the given IDs if the client supports
142+
/// it and we haven't yet registered the given command IDs yet.
143+
public func registerExecuteCommandIfNeeded(
144+
commands: [String],
145+
registerOnClient: ClientRegistrationHandler
146+
) {
147+
guard clientHasDynamicExecuteCommandRegistration else { return }
148+
var newCommands = Set(commands)
149+
newCommands.subtract(self.commandIds)
150+
151+
// We only want to send the registration with unregistered command IDs since
152+
// clients such as VS Code only allow a command to be registered once. We could
153+
// unregister all our commandIds first but this is simpler.
154+
guard !newCommands.isEmpty else { return }
155+
self.commandIds.formUnion(newCommands)
156+
157+
let registrationOptions = ExecuteCommandRegistrationOptions(commands: Array(newCommands))
158+
let registration = CapabilityRegistration(
159+
method: ExecuteCommandRequest.method,
160+
registerOptions: self.encode(registrationOptions))
161+
162+
registerOnClient(registration)
163+
}
164+
134165
/// Unregister a previously registered registration, e.g. if no longer needed
135166
/// or if registration fails.
136167
public func remove(registration: CapabilityRegistration) {

Sources/SourceKitLSP/Clang/ClangLanguageServer.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -510,8 +510,7 @@ extension ClangLanguageServerShim {
510510
// MARK: - Other
511511

512512
func executeCommand(_ req: Request<ExecuteCommandRequest>) {
513-
//TODO: Implement commands.
514-
return req.reply(nil)
513+
forwardRequestToClangdOnQueue(req)
515514
}
516515
}
517516

Sources/SourceKitLSP/SourceKitServer.swift

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,12 @@ extension SourceKitServer {
524524
triggerCharacters: ["."]
525525
)
526526
}
527+
let executeCommandOptions: ExecuteCommandOptions?
528+
if registry.clientHasDynamicExecuteCommandRegistration {
529+
executeCommandOptions = nil
530+
} else {
531+
executeCommandOptions = ExecuteCommandOptions(commands: builtinSwiftCommands)
532+
}
527533
return ServerCapabilities(
528534
textDocumentSync: TextDocumentSyncOptions(
529535
openClose: true,
@@ -547,9 +553,7 @@ extension SourceKitServer {
547553
)),
548554
colorProvider: .bool(true),
549555
foldingRangeProvider: .bool(!registry.clientHasDynamicFoldingRangeRegistration),
550-
executeCommandProvider: ExecuteCommandOptions(
551-
commands: builtinSwiftCommands // FIXME: Clangd commands?
552-
)
556+
executeCommandProvider: executeCommandOptions
553557
)
554558
}
555559

@@ -573,6 +577,11 @@ extension SourceKitServer {
573577
self.dynamicallyRegisterCapability($0, registry)
574578
}
575579
}
580+
if let commandOptions = server.executeCommandProvider {
581+
registry.registerExecuteCommandIfNeeded(commands: commandOptions.commands) {
582+
self.dynamicallyRegisterCapability($0, registry)
583+
}
584+
}
576585
}
577586

578587
private func dynamicallyRegisterCapability(

Tests/SKSwiftPMWorkspaceTests/XCTestManifests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ extension SwiftPMWorkspaceTests {
1515
("testMultiTargetSwift", testMultiTargetSwift),
1616
("testNoPackage", testNoPackage),
1717
("testNoToolchain", testNoToolchain),
18+
("testSwiftDerivedSources", testSwiftDerivedSources),
1819
("testSymlinkInWorkspaceCXX", testSymlinkInWorkspaceCXX),
1920
("testSymlinkInWorkspaceSwift", testSymlinkInWorkspaceSwift),
2021
("testUnknownFile", testUnknownFile),

Tests/SourceKitLSPTests/LocalClangTests.swift

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,68 @@ final class LocalClangTests: XCTestCase {
174174
XCTAssertEqual(syms.first?.children?.first?.name, "foo")
175175
}
176176

177+
func testCodeAction() throws {
178+
guard let ws = try staticSourceKitTibsWorkspace(name: "CodeActionCxx") else { return }
179+
if ToolchainRegistry.shared.default?.clangd == nil { return }
180+
181+
let loc = ws.testLoc("SwitchColor")
182+
let endLoc = ws.testLoc("SwitchColor:end")
183+
184+
let expectation = XCTestExpectation(description: "diagnostics")
185+
186+
ws.sk.handleNextNotification { (note: Notification<PublishDiagnosticsNotification>) in
187+
let diagnostics = note.params.diagnostics
188+
// It seems we either get no diagnostics or a `-Wswitch` warning. Either is fine
189+
// as long as our code action works properly.
190+
XCTAssert(diagnostics.isEmpty ||
191+
(diagnostics.count == 1 && diagnostics.first?.code == .string("-Wswitch")),
192+
"Unexpected diagnostics \(diagnostics)")
193+
expectation.fulfill()
194+
}
195+
196+
try ws.openDocument(loc.url, language: .cpp)
197+
198+
let result = XCTWaiter.wait(for: [expectation], timeout: 15)
199+
if result != .completed {
200+
fatalError("error \(result) waiting for diagnostics notification")
201+
}
202+
203+
let codeAction = CodeActionRequest(
204+
range: Position(loc)..<Position(endLoc),
205+
context: CodeActionContext(),
206+
textDocument: loc.docIdentifier
207+
)
208+
guard let reply = try ws.sk.sendSync(codeAction) else {
209+
XCTFail("CodeActionRequest had nil reply")
210+
return
211+
}
212+
guard case let .commands(commands) = reply else {
213+
XCTFail("Expected [Command] but got \(reply)")
214+
return
215+
}
216+
guard let command = commands.first else {
217+
XCTFail("Expected a non-empty [Command]")
218+
return
219+
}
220+
XCTAssertEqual(command.command, "clangd.applyTweak")
221+
222+
let applyEdit = XCTestExpectation(description: "applyEdit")
223+
ws.sk.handleNextRequest { (request: Request<ApplyEditRequest>) in
224+
XCTAssertNotNil(request.params.edit.changes)
225+
request.reply(ApplyEditResponse(applied: true, failureReason: nil))
226+
applyEdit.fulfill()
227+
}
228+
229+
let executeCommand = ExecuteCommandRequest(
230+
command: command.command, arguments: command.arguments)
231+
_ = try ws.sk.sendSync(executeCommand)
232+
233+
let editResult = XCTWaiter.wait(for: [applyEdit], timeout: 15)
234+
if editResult != .completed {
235+
fatalError("error \(editResult) waiting for applyEdit request")
236+
}
237+
}
238+
177239
func testClangStdHeaderCanary() throws {
178240
guard let ws = try staticSourceKitTibsWorkspace(name: "ClangStdHeaderCanary") else { return }
179241
if ToolchainRegistry.shared.default?.clangd == nil { return }

Tests/SourceKitLSPTests/XCTestManifests.swift

Lines changed: 34 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ extension LocalClangTests {
110110
static let __allTests__LocalClangTests = [
111111
("testClangModules", testClangModules),
112112
("testClangStdHeaderCanary", testClangStdHeaderCanary),
113+
("testCodeAction", testCodeAction),
114+
("testDocumentSymbols", testDocumentSymbols),
113115
("testFoldingRange", testFoldingRange),
114116
("testSemanticHighlighting", testSemanticHighlighting),
115117
("testSymbolInfo", testSymbolInfo),
@@ -154,37 +156,6 @@ extension MainFilesProviderTests {
154156
]
155157
}
156158

157-
extension SemanticTokensTests {
158-
// DO NOT MODIFY: This is autogenerated, use:
159-
// `swift test --generate-linuxmain`
160-
// to regenerate.
161-
static let __allTests__SemanticTokensTests = [
162-
("testEmpty", testEmpty),
163-
("testEmptyEdit", testEmptyEdit),
164-
("testInsertNewline", testInsertNewline),
165-
("testInsertSpaceAfterToken", testInsertSpaceAfterToken),
166-
("testInsertSpaceBeforeToken", testInsertSpaceBeforeToken),
167-
("testInsertTokens", testInsertTokens),
168-
("testIntArrayCoding", testIntArrayCoding),
169-
("testRanged", testRanged),
170-
("testRangeSplitting", testRangeSplitting),
171-
("testRemoveNewline", testRemoveNewline),
172-
("testReplaceUntilEndOfToken", testReplaceUntilEndOfToken),
173-
("testReplaceUntilMiddleOfToken", testReplaceUntilMiddleOfToken),
174-
("testSemanticMultiEdit", testSemanticMultiEdit),
175-
("testSemanticTokens", testSemanticTokens),
176-
("testSemanticTokensForEnumMembers", testSemanticTokensForEnumMembers),
177-
("testSemanticTokensForFunctionSignatures", testSemanticTokensForFunctionSignatures),
178-
("testSemanticTokensForFunctionSignaturesWithEmoji", testSemanticTokensForFunctionSignaturesWithEmoji),
179-
("testSemanticTokensForProtocols", testSemanticTokensForProtocols),
180-
("testSemanticTokensForStaticMethods", testSemanticTokensForStaticMethods),
181-
("testLexicalTokens", testLexicalTokens),
182-
("testLexicalTokensForBackticks", testLexicalTokensForBackticks),
183-
("testLexicalTokensForDocComments", testLexicalTokensForDocComments),
184-
("testLexicalTokensForMultiLineComments", testLexicalTokensForMultiLineComments),
185-
]
186-
}
187-
188159
extension SKTests {
189160
// DO NOT MODIFY: This is autogenerated, use:
190161
// `swift test --generate-linuxmain`
@@ -202,6 +173,37 @@ extension SKTests {
202173
]
203174
}
204175

176+
extension SemanticTokensTests {
177+
// DO NOT MODIFY: This is autogenerated, use:
178+
// `swift test --generate-linuxmain`
179+
// to regenerate.
180+
static let __allTests__SemanticTokensTests = [
181+
("testEmpty", testEmpty),
182+
("testEmptyEdit", testEmptyEdit),
183+
("testInsertNewline", testInsertNewline),
184+
("testInsertSpaceAfterToken", testInsertSpaceAfterToken),
185+
("testInsertSpaceBeforeToken", testInsertSpaceBeforeToken),
186+
("testInsertTokens", testInsertTokens),
187+
("testIntArrayCoding", testIntArrayCoding),
188+
("testLexicalTokens", testLexicalTokens),
189+
("testLexicalTokensForBackticks", testLexicalTokensForBackticks),
190+
("testLexicalTokensForDocComments", testLexicalTokensForDocComments),
191+
("testLexicalTokensForMultiLineComments", testLexicalTokensForMultiLineComments),
192+
("testRanged", testRanged),
193+
("testRangeSplitting", testRangeSplitting),
194+
("testRemoveNewline", testRemoveNewline),
195+
("testReplaceUntilEndOfToken", testReplaceUntilEndOfToken),
196+
("testReplaceUntilMiddleOfToken", testReplaceUntilMiddleOfToken),
197+
("testSemanticMultiEdit", testSemanticMultiEdit),
198+
("testSemanticTokens", testSemanticTokens),
199+
("testSemanticTokensForEnumMembers", testSemanticTokensForEnumMembers),
200+
("testSemanticTokensForFunctionSignatures", testSemanticTokensForFunctionSignatures),
201+
("testSemanticTokensForFunctionSignaturesWithEmoji", testSemanticTokensForFunctionSignaturesWithEmoji),
202+
("testSemanticTokensForProtocols", testSemanticTokensForProtocols),
203+
("testSemanticTokensForStaticMethods", testSemanticTokensForStaticMethods),
204+
]
205+
}
206+
205207
extension SwiftCompileCommandsTest {
206208
// DO NOT MODIFY: This is autogenerated, use:
207209
// `swift test --generate-linuxmain`
@@ -256,8 +258,8 @@ public func __allTests() -> [XCTestCaseEntry] {
256258
testCase(LocalClangTests.__allTests__LocalClangTests),
257259
testCase(LocalSwiftTests.__allTests__LocalSwiftTests),
258260
testCase(MainFilesProviderTests.__allTests__MainFilesProviderTests),
259-
testCase(SemanticTokensTests.__allTests__SemanticTokensTests),
260261
testCase(SKTests.__allTests__SKTests),
262+
testCase(SemanticTokensTests.__allTests__SemanticTokensTests),
261263
testCase(SwiftCompileCommandsTest.__allTests__SwiftCompileCommandsTest),
262264
testCase(SwiftCompletionTests.__allTests__SwiftCompletionTests),
263265
testCase(SwiftPMIntegrationTests.__allTests__SwiftPMIntegrationTests),

0 commit comments

Comments
 (0)