Skip to content

Commit a5854f4

Browse files
authored
Add signature help LSP request support (#2250)
Depends on swiftlang/swift#83378 --- Adds support for the LSP signature help request. > [!NOTE] > As of swiftlang/swift#83378, SourceKitD still doesn't separate parameter documentation from the signature documentation and thus parameters don't have their own separate documentation. This should just work once SourceKitD implements this functionality and we'll only need to modify the tests.
1 parent 41ddebb commit a5854f4

File tree

12 files changed

+1041
-0
lines changed

12 files changed

+1041
-0
lines changed

Sources/ClangLanguageService/ClangLanguageService.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,10 @@ extension ClangLanguageService {
491491
return try await forwardRequestToClangd(req)
492492
}
493493

494+
package func signatureHelp(_ req: SignatureHelpRequest) async throws -> SignatureHelp? {
495+
return try await forwardRequestToClangd(req)
496+
}
497+
494498
package func hover(_ req: HoverRequest) async throws -> HoverResponse? {
495499
return try await forwardRequestToClangd(req)
496500
}

Sources/LanguageServerProtocol/SupportTypes/RegistrationOptions.swift

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,46 @@ public struct CompletionRegistrationOptions: RegistrationOptions, TextDocumentRe
9090
}
9191
}
9292

93+
/// Signature help registration options.
94+
public struct SignatureHelpRegistrationOptions: RegistrationOptions, TextDocumentRegistrationOptionsProtocol, Hashable {
95+
public var textDocumentRegistrationOptions: TextDocumentRegistrationOptions
96+
public var signatureHelpOptions: SignatureHelpOptions
97+
98+
public init(documentSelector: DocumentSelector? = nil, signatureHelpOptions: SignatureHelpOptions) {
99+
self.textDocumentRegistrationOptions =
100+
TextDocumentRegistrationOptions(documentSelector: documentSelector)
101+
self.signatureHelpOptions = signatureHelpOptions
102+
}
103+
104+
public init?(fromLSPDictionary dictionary: [String: LSPAny]) {
105+
guard let signatureHelpOptions = SignatureHelpOptions(fromLSPDictionary: dictionary) else {
106+
return nil
107+
}
108+
109+
self.signatureHelpOptions = signatureHelpOptions
110+
111+
guard let textDocumentRegistrationOptions = TextDocumentRegistrationOptions(fromLSPDictionary: dictionary) else {
112+
return nil
113+
}
114+
115+
self.textDocumentRegistrationOptions = textDocumentRegistrationOptions
116+
}
117+
118+
public func encodeToLSPAny() -> LSPAny {
119+
var dict: [String: LSPAny] = [:]
120+
121+
if case .dictionary(let dictionary) = signatureHelpOptions.encodeToLSPAny() {
122+
dict.merge(dictionary) { (current, _) in current }
123+
}
124+
125+
if case .dictionary(let dictionary) = textDocumentRegistrationOptions.encodeToLSPAny() {
126+
dict.merge(dictionary) { (current, _) in current }
127+
}
128+
129+
return .dictionary(dict)
130+
}
131+
}
132+
93133
/// Folding range registration options.
94134
public struct FoldingRangeRegistrationOptions: RegistrationOptions, TextDocumentRegistrationOptionsProtocol, Hashable {
95135
public var textDocumentRegistrationOptions: TextDocumentRegistrationOptions

Sources/LanguageServerProtocol/SupportTypes/ServerCapabilities.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,38 @@ public struct SignatureHelpOptions: WorkDoneProgressOptions, Codable, Hashable,
612612
self.triggerCharacters = triggerCharacters
613613
self.retriggerCharacters = retriggerCharacters
614614
}
615+
616+
public init?(fromLSPDictionary dictionary: [String: LSPAny]) {
617+
if let arrayAny = dictionary["triggerCharacters"] {
618+
triggerCharacters = [String](fromLSPArray: arrayAny)
619+
}
620+
621+
if let arrayAny = dictionary["retriggerCharacters"] {
622+
retriggerCharacters = [String](fromLSPArray: arrayAny)
623+
}
624+
625+
if case .bool(let value) = dictionary["workDoneProgress"] {
626+
workDoneProgress = value
627+
}
628+
}
629+
630+
public func encodeToLSPAny() -> LSPAny {
631+
var dict: [String: LSPAny] = [:]
632+
633+
if let triggerCharacters {
634+
dict["triggerCharacters"] = triggerCharacters.encodeToLSPAny()
635+
}
636+
637+
if let retriggerCharacters {
638+
dict["retriggerCharacters"] = retriggerCharacters.encodeToLSPAny()
639+
}
640+
641+
if let workDoneProgress {
642+
dict["workDoneProgress"] = .bool(workDoneProgress)
643+
}
644+
645+
return .dictionary(dict)
646+
}
615647
}
616648

617649
public struct DocumentFilter: Codable, Hashable, Sendable {

Sources/SourceKitD/sourcekitd_uids.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,14 @@ package struct sourcekitd_api_keys {
103103
package let fullyAnnotatedDecl: sourcekitd_api_uid_t
104104
/// `key.fully_annotated_generic_signature`
105105
package let fullyAnnotatedGenericSignature: sourcekitd_api_uid_t
106+
/// `key.signatures`
107+
package let signatures: sourcekitd_api_uid_t
108+
/// `key.active_signature`
109+
package let activeSignature: sourcekitd_api_uid_t
110+
/// `key.parameters`
111+
package let parameters: sourcekitd_api_uid_t
112+
/// `key.active_parameter`
113+
package let activeParameter: sourcekitd_api_uid_t
106114
/// `key.doc.brief`
107115
package let docBrief: sourcekitd_api_uid_t
108116
/// `key.context`
@@ -411,6 +419,8 @@ package struct sourcekitd_api_keys {
411419
package let indexUnitOutputPath: sourcekitd_api_uid_t
412420
/// `key.include_locals`
413421
package let includeLocals: sourcekitd_api_uid_t
422+
/// `key.compress`
423+
package let compress: sourcekitd_api_uid_t
414424
/// `key.ignore_clang_modules`
415425
package let ignoreClangModules: sourcekitd_api_uid_t
416426
/// `key.include_system_modules`
@@ -566,6 +576,10 @@ package struct sourcekitd_api_keys {
566576
annotatedDecl = api.uid_get_from_cstr("key.annotated_decl")!
567577
fullyAnnotatedDecl = api.uid_get_from_cstr("key.fully_annotated_decl")!
568578
fullyAnnotatedGenericSignature = api.uid_get_from_cstr("key.fully_annotated_generic_signature")!
579+
signatures = api.uid_get_from_cstr("key.signatures")!
580+
activeSignature = api.uid_get_from_cstr("key.active_signature")!
581+
parameters = api.uid_get_from_cstr("key.parameters")!
582+
activeParameter = api.uid_get_from_cstr("key.active_parameter")!
569583
docBrief = api.uid_get_from_cstr("key.doc.brief")!
570584
context = api.uid_get_from_cstr("key.context")!
571585
typeRelation = api.uid_get_from_cstr("key.typerelation")!
@@ -720,6 +734,7 @@ package struct sourcekitd_api_keys {
720734
indexStorePath = api.uid_get_from_cstr("key.index_store_path")!
721735
indexUnitOutputPath = api.uid_get_from_cstr("key.index_unit_output_path")!
722736
includeLocals = api.uid_get_from_cstr("key.include_locals")!
737+
compress = api.uid_get_from_cstr("key.compress")!
723738
ignoreClangModules = api.uid_get_from_cstr("key.ignore_clang_modules")!
724739
includeSystemModules = api.uid_get_from_cstr("key.include_system_modules")!
725740
ignoreStdlib = api.uid_get_from_cstr("key.ignore_stdlib")!
@@ -809,6 +824,8 @@ package struct sourcekitd_api_requests {
809824
package let codeCompleteSetPopularAPI: sourcekitd_api_uid_t
810825
/// `source.request.codecomplete.setcustom`
811826
package let codeCompleteSetCustom: sourcekitd_api_uid_t
827+
/// `source.request.signaturehelp`
828+
package let signatureHelp: sourcekitd_api_uid_t
812829
/// `source.request.typecontextinfo`
813830
package let typeContextInfo: sourcekitd_api_uid_t
814831
/// `source.request.conformingmethods`
@@ -907,6 +924,7 @@ package struct sourcekitd_api_requests {
907924
codeCompleteCacheOnDisk = api.uid_get_from_cstr("source.request.codecomplete.cache.ondisk")!
908925
codeCompleteSetPopularAPI = api.uid_get_from_cstr("source.request.codecomplete.setpopularapi")!
909926
codeCompleteSetCustom = api.uid_get_from_cstr("source.request.codecomplete.setcustom")!
927+
signatureHelp = api.uid_get_from_cstr("source.request.signaturehelp")!
910928
typeContextInfo = api.uid_get_from_cstr("source.request.typecontextinfo")!
911929
conformingMethodList = api.uid_get_from_cstr("source.request.conformingmethods")!
912930
activeRegions = api.uid_get_from_cstr("source.request.activeregions")!

Sources/SourceKitLSP/CapabilityRegistry.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ package final actor CapabilityRegistry {
2727
/// Dynamically registered completion options.
2828
private var completion: [CapabilityRegistration: CompletionRegistrationOptions] = [:]
2929

30+
/// Dynamically registered signature help options.
31+
private var signatureHelp: [CapabilityRegistration: SignatureHelpRegistrationOptions] = [:]
32+
3033
/// Dynamically registered folding range options.
3134
private var foldingRange: [CapabilityRegistration: FoldingRangeRegistrationOptions] = [:]
3235

@@ -51,6 +54,10 @@ package final actor CapabilityRegistry {
5154
clientCapabilities.textDocument?.completion?.dynamicRegistration == true
5255
}
5356

57+
package var clientHasDynamicSignatureHelpRegistration: Bool {
58+
clientCapabilities.textDocument?.signatureHelp?.dynamicRegistration == true
59+
}
60+
5461
package var clientHasDynamicFoldingRangeRegistration: Bool {
5562
clientCapabilities.textDocument?.foldingRange?.dynamicRegistration == true
5663
}
@@ -220,6 +227,26 @@ package final actor CapabilityRegistry {
220227
)
221228
}
222229

230+
package func registerSignatureHelpIfNeeded(
231+
options: SignatureHelpOptions,
232+
for languages: [Language],
233+
server: SourceKitLSPServer
234+
) async {
235+
guard clientHasDynamicCompletionRegistration else { return }
236+
237+
await registerLanguageSpecificCapability(
238+
options: SignatureHelpRegistrationOptions(
239+
documentSelector: DocumentSelector(for: languages),
240+
signatureHelpOptions: options
241+
),
242+
forMethod: SignatureHelpRequest.method,
243+
languages: languages,
244+
in: server,
245+
registrationDict: signatureHelp,
246+
setRegistrationDict: { signatureHelp[$0] = $1 }
247+
)
248+
}
249+
223250
package func registerDidChangeWatchedFiles(
224251
watchers: [FileSystemWatcher],
225252
server: SourceKitLSPServer

Sources/SourceKitLSP/LanguageService.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ package protocol LanguageService: AnyObject, Sendable {
196196

197197
func completion(_ req: CompletionRequest) async throws -> CompletionList
198198
func completionItemResolve(_ req: CompletionItemResolveRequest) async throws -> CompletionItem
199+
func signatureHelp(_ req: SignatureHelpRequest) async throws -> SignatureHelp?
199200
func hover(_ req: HoverRequest) async throws -> HoverResponse?
200201
func doccDocumentation(_ req: DoccDocumentationRequest) async throws -> DoccDocumentationResponse
201202
func symbolInfo(_ request: SymbolInfoRequest) async throws -> [SymbolDetails]
@@ -370,6 +371,10 @@ package extension LanguageService {
370371
throw ResponseError.requestNotImplemented(CompletionItemResolveRequest.self)
371372
}
372373

374+
func signatureHelp(_ req: SignatureHelpRequest) async throws -> SignatureHelp? {
375+
throw ResponseError.requestNotImplemented(SignatureHelpRequest.self)
376+
}
377+
373378
func hover(_ req: HoverRequest) async throws -> HoverResponse? {
374379
throw ResponseError.requestNotImplemented(HoverRequest.self)
375380
}

Sources/SourceKitLSP/SourceKitLSPServer.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,8 @@ extension SourceKitLSPServer: QueueBasedMessageHandler {
780780
await self.handleRequest(for: request, requestHandler: self.completion)
781781
case let request as RequestAndReply<CompletionItemResolveRequest>:
782782
await request.reply { try await completionItemResolve(request: request.params) }
783+
case let request as RequestAndReply<SignatureHelpRequest>:
784+
await self.handleRequest(for: request, requestHandler: self.signatureHelp)
783785
case let request as RequestAndReply<DeclarationRequest>:
784786
await self.handleRequest(for: request, requestHandler: self.declaration)
785787
case let request as RequestAndReply<DefinitionRequest>:
@@ -1075,6 +1077,15 @@ extension SourceKitLSPServer {
10751077
triggerCharacters: [".", "("]
10761078
)
10771079

1080+
let signatureHelpOptions =
1081+
await registry.clientHasDynamicSignatureHelpRegistration
1082+
? nil
1083+
: LanguageServerProtocol.SignatureHelpOptions(
1084+
triggerCharacters: ["(", "["],
1085+
// We retrigger on `:` as it's potentially after an argument label which can change the active parameter or signature.
1086+
retriggerCharacters: [",", ":"]
1087+
)
1088+
10781089
let onTypeFormattingOptions =
10791090
options.hasExperimentalFeature(.onTypeFormatting)
10801091
? DocumentOnTypeFormattingOptions(triggerCharacters: ["\n", "\r\n", "\r", "{", "}", ";", ".", ":", "#"])
@@ -1129,6 +1140,7 @@ extension SourceKitLSPServer {
11291140
),
11301141
hoverProvider: .bool(true),
11311142
completionProvider: completionOptions,
1143+
signatureHelpProvider: signatureHelpOptions,
11321144
definitionProvider: .bool(true),
11331145
implementationProvider: .bool(true),
11341146
referencesProvider: .bool(true),
@@ -1177,6 +1189,9 @@ extension SourceKitLSPServer {
11771189
if let completionOptions = server.completionProvider {
11781190
await registry.registerCompletionIfNeeded(options: completionOptions, for: languages, server: self)
11791191
}
1192+
if let signatureHelpOptions = server.signatureHelpProvider {
1193+
await registry.registerSignatureHelpIfNeeded(options: signatureHelpOptions, for: languages, server: self)
1194+
}
11801195
if server.foldingRangeProvider?.isSupported == true {
11811196
await registry.registerFoldingRangeIfNeeded(options: FoldingRangeOptions(), for: languages, server: self)
11821197
}
@@ -1638,6 +1653,14 @@ extension SourceKitLSPServer {
16381653
return try await languageService.hover(req)
16391654
}
16401655

1656+
func signatureHelp(
1657+
_ req: SignatureHelpRequest,
1658+
workspace: Workspace,
1659+
languageService: LanguageService
1660+
) async throws -> SignatureHelp? {
1661+
return try await languageService.signatureHelp(req)
1662+
}
1663+
16411664
/// Handle a workspace/symbol request, returning the SymbolInformation.
16421665
/// - returns: An array with SymbolInformation for each matching symbol in the workspace.
16431666
func workspaceSymbols(_ req: WorkspaceSymbolsRequest) async throws -> [WorkspaceSymbolItem]? {
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import LanguageServerProtocol
14+
import SKLogging
15+
import SourceKitLSP
16+
import SwiftSyntax
17+
18+
private class StartOfArgumentFinder: SyntaxAnyVisitor {
19+
let requestedPosition: AbsolutePosition
20+
var resolvedPosition: AbsolutePosition?
21+
22+
init(position: AbsolutePosition) {
23+
self.requestedPosition = position
24+
super.init(viewMode: .sourceAccurate)
25+
}
26+
27+
override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind {
28+
if node.range.contains(requestedPosition) {
29+
return .visitChildren
30+
}
31+
return .skipChildren
32+
}
33+
34+
override func visit(_ node: FunctionCallExprSyntax) -> SyntaxVisitorContinueKind {
35+
return visit(arguments: node.arguments)
36+
}
37+
38+
override func visit(_ node: SubscriptCallExprSyntax) -> SyntaxVisitorContinueKind {
39+
return visit(arguments: node.arguments)
40+
}
41+
42+
private func visit(arguments: LabeledExprListSyntax) -> SyntaxVisitorContinueKind {
43+
guard (arguments.position...arguments.endPosition).contains(requestedPosition) else {
44+
return .skipChildren
45+
}
46+
47+
guard !arguments.isEmpty else {
48+
self.resolvedPosition = arguments.position
49+
return .skipChildren
50+
}
51+
52+
for argument in arguments {
53+
if (argument.position...argument.endPosition).contains(requestedPosition) {
54+
if let trailingComma = argument.trailingComma,
55+
requestedPosition >= trailingComma.endPositionBeforeTrailingTrivia
56+
{
57+
self.resolvedPosition = trailingComma.endPositionBeforeTrailingTrivia
58+
} else {
59+
self.resolvedPosition = argument.expression.positionAfterSkippingLeadingTrivia
60+
}
61+
return .visitChildren
62+
}
63+
}
64+
65+
return .skipChildren
66+
}
67+
}
68+
69+
extension SwiftLanguageService {
70+
func adjustPositionToStartOfArgument(
71+
_ position: Position,
72+
in snapshot: DocumentSnapshot
73+
) async -> Position {
74+
let tree = await self.syntaxTreeManager.syntaxTree(for: snapshot)
75+
let visitor = StartOfArgumentFinder(position: snapshot.absolutePosition(of: position))
76+
visitor.walk(tree)
77+
if let resolvedPosition = visitor.resolvedPosition {
78+
return snapshot.position(of: resolvedPosition)
79+
}
80+
return position
81+
}
82+
}

Sources/SwiftLanguageService/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ add_library(SwiftLanguageService STATIC
4444
DocumentSymbols.swift
4545
SyntaxHighlightingTokens.swift
4646
GeneratedInterfaceManager.swift
47+
SignatureHelp.swift
48+
AdjustPositionToStartOfArgument.swift
4749
)
4850
set_target_properties(SwiftLanguageService PROPERTIES
4951
INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY})

0 commit comments

Comments
 (0)