Skip to content

Commit 3635d25

Browse files
authored
Dynamically register completion options for supporting clients (#380)
Using dynamic registration (when supported by the client) allows us to provide different completion options for ObjC and Swift files. We should be able to expand this to other capabilities in the future (e.g. semantic highlighting, execute command support).
1 parent f1d0316 commit 3635d25

File tree

11 files changed

+247
-74
lines changed

11 files changed

+247
-74
lines changed

Sources/LanguageServerProtocol/Requests/RegisterCapabilityRequest.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,15 @@ public struct RegisterCapabilityRequest: RequestType, Hashable {
2727
public typealias Response = VoidResponse
2828

2929
/// Capability registrations.
30-
public var registrations: [Registration]
30+
public var registrations: [CapabilityRegistration]
3131

32-
public init(registrations: [Registration]) {
32+
public init(registrations: [CapabilityRegistration]) {
3333
self.registrations = registrations
3434
}
3535
}
3636

3737
/// General parameters to register a capability.
38-
public struct Registration: Codable, Hashable {
38+
public struct CapabilityRegistration: Codable, Hashable {
3939
/// The id used to register the capability which may be used to unregister support.
4040
public var id: String
4141

@@ -51,8 +51,8 @@ public struct Registration: Codable, Hashable {
5151
self.registerOptions = registerOptions
5252
}
5353

54-
/// Create a new `Registration` with a randomly generated id. Save the generated
55-
/// id if you wish to unregister the given registration.
54+
/// Create a new `CapabilityRegistration` with a randomly generated id. Save
55+
/// the generated id if you wish to unregister the given registration.
5656
public init(method: String, registerOptions: LSPAny?) {
5757
self.id = UUID().uuidString
5858
self.method = method

Sources/LanguageServerProtocol/SupportTypes/RegistrationOptions.swift

Lines changed: 50 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,23 @@
1212

1313
import Foundation
1414

15-
/// An event describing a file change.
16-
public protocol RegistrationOptions: Codable, LSPAnyCodable, Hashable {
15+
/// Protocol for capability registration options, which must be encodable to
16+
/// `LSPAny` so they can be included in a `Registration`.
17+
public protocol RegistrationOptions: Hashable {
18+
func encodeIntoLSPAny(dict: inout [String: LSPAny])
19+
}
20+
21+
fileprivate func encode(strings: [String]) -> LSPAny {
22+
var values = [LSPAny]()
23+
values.reserveCapacity(strings.count)
24+
for str in strings {
25+
values.append(.string(str))
26+
}
27+
return .array(values)
1728
}
1829

1930
/// General text document registration options.
20-
public struct TextDocumentRegistrationOptions: RegistrationOptions {
31+
public struct TextDocumentRegistrationOptions: RegistrationOptions, Hashable {
2132
/// A document selector to identify the scope of the registration. If not set,
2233
/// the document selector provided on the client side will be used.
2334
public var documentSelector: DocumentSelector?
@@ -26,21 +37,40 @@ public struct TextDocumentRegistrationOptions: RegistrationOptions {
2637
self.documentSelector = documentSelector
2738
}
2839

29-
public init?(fromLSPDictionary dictionary: [String : LSPAny]) {
30-
guard let selectorValue = dictionary[CodingKeys.documentSelector.stringValue] else {
31-
self.documentSelector = nil
32-
return
33-
}
34-
guard case .dictionary(let selectorDictionary) = selectorValue else { return nil }
35-
guard let documentSelector = DocumentSelector(fromLSPDictionary: selectorDictionary) else {
36-
return nil
37-
}
38-
self.documentSelector = documentSelector
40+
public func encodeIntoLSPAny(dict: inout [String: LSPAny]) {
41+
guard let documentSelector = documentSelector else { return }
42+
dict["documentSelector"] = documentSelector.encodeToLSPAny()
43+
}
44+
}
45+
46+
/// Protocol for a type which structurally represents`TextDocumentRegistrationOptions`.
47+
public protocol TextDocumentRegistrationOptionsProtocol {
48+
var textDocumentRegistrationOptions: TextDocumentRegistrationOptions {get}
49+
}
50+
51+
/// Code completiion registration options.
52+
public struct CompletionRegistrationOptions: RegistrationOptions, TextDocumentRegistrationOptionsProtocol, Hashable {
53+
public var textDocumentRegistrationOptions: TextDocumentRegistrationOptions
54+
public var completionOptions: CompletionOptions
55+
56+
public init(documentSelector: DocumentSelector? = nil, completionOptions: CompletionOptions) {
57+
self.textDocumentRegistrationOptions =
58+
TextDocumentRegistrationOptions(documentSelector: documentSelector)
59+
self.completionOptions = completionOptions
3960
}
4061

41-
public func encodeToLSPAny() -> LSPAny {
42-
guard let documentSelector = documentSelector else { return .dictionary([:]) }
43-
return .dictionary([CodingKeys.documentSelector.stringValue: documentSelector.encodeToLSPAny()])
62+
public func encodeIntoLSPAny(dict: inout [String: LSPAny]) {
63+
textDocumentRegistrationOptions.encodeIntoLSPAny(dict: &dict)
64+
65+
if let resolveProvider = completionOptions.resolveProvider {
66+
dict["resolveProvider"] = .bool(resolveProvider)
67+
}
68+
if let triggerCharacters = completionOptions.triggerCharacters {
69+
dict["triggerCharacters"] = encode(strings: triggerCharacters)
70+
}
71+
if let allCommitCharacters = completionOptions.allCommitCharacters {
72+
dict["allCommitCharacters"] = encode(strings: allCommitCharacters)
73+
}
4474
}
4575
}
4676

@@ -53,14 +83,8 @@ public struct DidChangeWatchedFilesRegistrationOptions: RegistrationOptions {
5383
self.watchers = watchers
5484
}
5585

56-
public init?(fromLSPDictionary dictionary: [String : LSPAny]) {
57-
guard let watchersLSPAny = dictionary[CodingKeys.watchers.stringValue] else { return nil }
58-
guard let watchers = [FileSystemWatcher].init(fromLSPArray: watchersLSPAny) else { return nil }
59-
self.watchers = watchers
60-
}
61-
62-
public func encodeToLSPAny() -> LSPAny {
63-
return .dictionary([CodingKeys.watchers.stringValue: watchers.encodeToLSPAny()])
86+
public func encodeIntoLSPAny(dict: inout [String: LSPAny]) {
87+
dict["watchers"] = watchers.encodeToLSPAny()
6488
}
6589
}
6690

@@ -73,25 +97,7 @@ public struct ExecuteCommandRegistrationOptions: RegistrationOptions {
7397
self.commands = commands
7498
}
7599

76-
public init?(fromLSPDictionary dictionary: [String : LSPAny]) {
77-
guard case .array(let commandsArray) = dictionary[CodingKeys.commands.stringValue] else {
78-
return nil
79-
}
80-
var values = [String]()
81-
values.reserveCapacity(commandsArray.count)
82-
for lspAny in commandsArray {
83-
guard case .string(let value) = lspAny else { return nil }
84-
values.append(value)
85-
}
86-
self.commands = values
87-
}
88-
89-
public func encodeToLSPAny() -> LSPAny {
90-
var values = [LSPAny]()
91-
values.reserveCapacity(commands.count)
92-
for command in commands {
93-
values.append(.string(command))
94-
}
95-
return .dictionary([CodingKeys.commands.stringValue: .array(values)])
100+
public func encodeIntoLSPAny(dict: inout [String: LSPAny]) {
101+
dict["commands"] = encode(strings: commands)
96102
}
97103
}

Sources/LanguageServerProtocol/SupportTypes/ServerCapabilities.swift

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -249,16 +249,23 @@ public enum TextDocumentSyncKind: Int, Codable, Hashable {
249249
}
250250

251251
public struct CompletionOptions: Codable, Hashable {
252-
253252
/// Whether to use `textDocument/resolveCompletion`
254253
public var resolveProvider: Bool?
255254

256255
/// The characters that should trigger automatic completion.
257-
public var triggerCharacters: [String]
256+
public var triggerCharacters: [String]?
258257

259-
public init(resolveProvider: Bool? = false, triggerCharacters: [String]) {
258+
/// The list of all possible characters that commit a completion.
259+
public var allCommitCharacters: [String]?
260+
261+
public init(
262+
resolveProvider: Bool? = false,
263+
triggerCharacters: [String]? = nil,
264+
allCommitCharacters: [String]? = nil
265+
) {
260266
self.resolveProvider = resolveProvider
261267
self.triggerCharacters = triggerCharacters
268+
self.allCommitCharacters = allCommitCharacters
262269
}
263270
}
264271

Sources/SKTestSupport/SKSwiftPMTestWorkspace.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public final class SKSwiftPMTestWorkspace {
9090
let server = testServer.server!
9191
server.workspace = Workspace(
9292
rootUri: DocumentURI(sources.rootDirectory),
93-
clientCapabilities: ClientCapabilities(),
93+
capabilityRegistry: CapabilityRegistry(clientCapabilities: ClientCapabilities()),
9494
toolchainRegistry: ToolchainRegistry.shared,
9595
buildSetup: buildSetup,
9696
underlyingBuildSystem: swiftpm,

Sources/SKTestSupport/SKTibsTestWorkspace.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public final class SKTibsTestWorkspace {
6969

7070
testServer.server!.workspace = Workspace(
7171
rootUri: DocumentURI(sources.rootDirectory),
72-
clientCapabilities: clientCapabilities,
72+
capabilityRegistry: CapabilityRegistry(clientCapabilities: clientCapabilities),
7373
toolchainRegistry: ToolchainRegistry.shared,
7474
buildSetup: BuildSetup(configuration: .debug, path: buildPath, flags: BuildFlags()),
7575
underlyingBuildSystem: buildSystem,

Sources/SourceKitLSP/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ if(CMAKE_VERSION VERSION_LESS 3.16)
66
endif()
77

88
add_library(SourceKitLSP
9+
CapabilityRegistry.swift
910
DocumentManager.swift
1011
IndexStoreDB+MainFilesProvider.swift
1112
SourceKitIndexDelegate.swift
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2021 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+
15+
/// Handler responsible for registering a capability with the client.
16+
public typealias ClientRegistrationHandler = (CapabilityRegistration) -> Void
17+
18+
/// A class which tracks the client's capabilities as well as our dynamic
19+
/// capability registrations in order to avoid registering conflicting
20+
/// capabilities.
21+
public final class CapabilityRegistry {
22+
/// Registered completion options.
23+
private var completion: [CapabilityRegistration: CompletionRegistrationOptions] = [:]
24+
25+
public let clientCapabilities: ClientCapabilities
26+
27+
public init(clientCapabilities: ClientCapabilities) {
28+
self.clientCapabilities = clientCapabilities
29+
}
30+
31+
public var clientHasDynamicCompletionRegistration: Bool {
32+
clientCapabilities.textDocument?.completion?.dynamicRegistration == true
33+
}
34+
35+
/// Dynamically register completion capabilities if the client supports it and
36+
/// we haven't yet registered any completion capabilities for the given
37+
/// languages.
38+
public func registerCompletionIfNeeded(
39+
options: CompletionOptions,
40+
for languages: [Language],
41+
registerOnClient: ClientRegistrationHandler
42+
) {
43+
guard clientHasDynamicCompletionRegistration && !hasCompletionRegistrations(for: languages) else {
44+
return
45+
}
46+
let registrationOptions = CompletionRegistrationOptions(
47+
documentSelector: self.documentSelector(for: languages),
48+
completionOptions: options)
49+
let registration = CapabilityRegistration(
50+
method: CompletionRequest.method,
51+
registerOptions: self.encode(registrationOptions))
52+
53+
self.completion[registration] = registrationOptions
54+
55+
registerOnClient(registration)
56+
}
57+
58+
/// Unregister a previously registered registration, e.g. if no longer needed
59+
/// or if registration fails.
60+
public func remove(registration: CapabilityRegistration) {
61+
if registration.method == CompletionRequest.method {
62+
completion.removeValue(forKey: registration)
63+
}
64+
}
65+
66+
private func hasCompletionRegistrations(for languages: [Language]) -> Bool {
67+
return self.hasAnyRegistrations(for: languages, in: self.completion)
68+
}
69+
70+
private func documentSelector(for langauges: [Language]) -> DocumentSelector {
71+
return DocumentSelector(langauges.map { DocumentFilter(language: $0.rawValue) })
72+
}
73+
74+
private func encode<T: RegistrationOptions>(_ options: T) -> LSPAny {
75+
var dict = [String: LSPAny]()
76+
options.encodeIntoLSPAny(dict: &dict)
77+
return .dictionary(dict)
78+
}
79+
80+
/// Check if we have any text document registration in `registrations` scoped to
81+
/// one or more of the given `languages`.
82+
private func hasAnyRegistrations(
83+
for languages: [Language],
84+
in registrations: [CapabilityRegistration: TextDocumentRegistrationOptionsProtocol]
85+
) -> Bool {
86+
var languageIds: Set<String> = []
87+
for language in languages {
88+
languageIds.insert(language.rawValue)
89+
}
90+
91+
for registration in registrations {
92+
let options = registration.value.textDocumentRegistrationOptions
93+
guard let filters = options.documentSelector else { continue }
94+
for filter in filters {
95+
guard let filterLanguage = filter.language else { continue }
96+
if languageIds.contains(filterLanguage) {
97+
return true
98+
}
99+
}
100+
}
101+
return false
102+
}
103+
}

0 commit comments

Comments
 (0)