File: CapabilityRegistry.swift

package info (click to toggle)
swiftlang 6.0.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,519,992 kB
  • sloc: cpp: 9,107,863; ansic: 2,040,022; asm: 1,135,751; python: 296,500; objc: 82,456; f90: 60,502; lisp: 34,951; pascal: 19,946; sh: 18,133; perl: 7,482; ml: 4,937; javascript: 4,117; makefile: 3,840; awk: 3,535; xml: 914; fortran: 619; cs: 573; ruby: 573
file content (351 lines) | stat: -rw-r--r-- 12,874 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import LSPLogging
import LanguageServerProtocol
import SKSupport

/// A class which tracks the client's capabilities as well as our dynamic
/// capability registrations in order to avoid registering conflicting
/// capabilities.
public final actor CapabilityRegistry {
  /// The client's capabilities as they were reported when sourcekit-lsp was launched.
  public let clientCapabilities: ClientCapabilities

  // MARK: Tracking capabilities dynamically registered in the client

  /// Dynamically registered completion options.
  private var completion: [CapabilityRegistration: CompletionRegistrationOptions] = [:]

  /// Dynamically registered folding range options.
  private var foldingRange: [CapabilityRegistration: FoldingRangeRegistrationOptions] = [:]

  /// Dynamically registered semantic tokens options.
  private var semanticTokens: [CapabilityRegistration: SemanticTokensRegistrationOptions] = [:]

  /// Dynamically registered inlay hint options.
  private var inlayHint: [CapabilityRegistration: InlayHintRegistrationOptions] = [:]

  /// Dynamically registered pull diagnostics options.
  private var pullDiagnostics: [CapabilityRegistration: DiagnosticRegistrationOptions] = [:]

  /// Dynamically registered file watchers.
  private var didChangeWatchedFiles: DidChangeWatchedFilesRegistrationOptions?

  /// Dynamically registered command IDs.
  private var commandIds: Set<String> = []

  // MARK: Query if client has dynamic registration

  public var clientHasDynamicCompletionRegistration: Bool {
    clientCapabilities.textDocument?.completion?.dynamicRegistration == true
  }

  public var clientHasDynamicFoldingRangeRegistration: Bool {
    clientCapabilities.textDocument?.foldingRange?.dynamicRegistration == true
  }

  public var clientHasDynamicSemanticTokensRegistration: Bool {
    clientCapabilities.textDocument?.semanticTokens?.dynamicRegistration == true
  }

  public var clientHasDynamicInlayHintRegistration: Bool {
    clientCapabilities.textDocument?.inlayHint?.dynamicRegistration == true
  }

  public var clientHasDynamicDocumentDiagnosticsRegistration: Bool {
    clientCapabilities.textDocument?.diagnostic?.dynamicRegistration == true
  }

  public var clientHasDynamicExecuteCommandRegistration: Bool {
    clientCapabilities.workspace?.executeCommand?.dynamicRegistration == true
  }

  public var clientHasDynamicDidChangeWatchedFilesRegistration: Bool {
    clientCapabilities.workspace?.didChangeWatchedFiles?.dynamicRegistration == true
  }

  // MARK: Other capability queries

  public var clientHasDiagnosticsCodeDescriptionSupport: Bool {
    clientCapabilities.textDocument?.publishDiagnostics?.codeDescriptionSupport == true
  }

  /// Since LSP 3.17.0, diagnostics can be reported through pull-based requests in addition to the existing push-based
  /// publish notifications.
  ///
  /// The `DiagnosticOptions` were added at the same time as the pull diagnostics request and allow specification of
  /// options for the pull diagnostics request. If the client doesn't reject this dynamic capability registration,
  /// it supports the pull diagnostics request.
  public func clientSupportsPullDiagnostics(for language: Language) -> Bool {
    registration(for: [language], in: pullDiagnostics) != nil
  }

  // MARK: Initializer

  public init(clientCapabilities: ClientCapabilities) {
    self.clientCapabilities = clientCapabilities
  }

  // MARK: Query registered capabilities

  /// Return a registration in `registrations` for one or more of the given
  /// `languages`.
  private func registration<T: TextDocumentRegistrationOptionsProtocol>(
    for languages: [Language],
    in registrations: [CapabilityRegistration: T]
  ) -> T? {
    var languageIds: Set<String> = []
    for language in languages {
      languageIds.insert(language.rawValue)
    }

    for registration in registrations {
      let options = registration.value.textDocumentRegistrationOptions
      guard let filters = options.documentSelector else { continue }
      for filter in filters {
        guard let filterLanguage = filter.language else { continue }
        if languageIds.contains(filterLanguage) {
          return registration.value
        }
      }
    }
    return nil
  }

  // MARK: Dynamic registration of server capabilities

  /// Register a dynamic server capability with the client.
  ///
  /// If the registration of `options` for the given `method` and `languages` was successful, the capability will be
  /// added to `registrationDict` by calling `setRegistrationDict`.
  /// If registration failed, the capability won't be added to `registrationDict`.
  private func registerLanguageSpecificCapability<
    Options: RegistrationOptions & TextDocumentRegistrationOptionsProtocol & Equatable
  >(
    options: Options,
    forMethod method: String,
    languages: [Language],
    in server: SourceKitLSPServer,
    registrationDict: [CapabilityRegistration: Options],
    setRegistrationDict: (CapabilityRegistration, Options?) -> Void
  ) async {
    if let registration = registration(for: languages, in: registrationDict) {
      if options != registration {
        logger.fault(
          """
          Failed to dynamically register for \(method, privacy: .public) for \(languages, privacy: .public) \
          due to pre-existing options:
          Existing options: \(String(reflecting: registration), privacy: .public)
          New options: \(String(reflecting: options), privacy: .public)
          """
        )
      }
      return
    }

    let registration = CapabilityRegistration(
      method: method,
      registerOptions: options.encodeToLSPAny()
    )

    // Add the capability to the registration dictionary.
    // This ensures that concurrent calls for the same capability don't register it as well.
    // If the capability is rejected by the client, we remove it again.
    setRegistrationDict(registration, options)

    do {
      _ = try await server.client.send(RegisterCapabilityRequest(registrations: [registration]))
    } catch {
      setRegistrationDict(registration, nil)
    }
  }

  /// Dynamically register completion capabilities if the client supports it and
  /// we haven't yet registered any completion capabilities for the given
  /// languages.
  public func registerCompletionIfNeeded(
    options: CompletionOptions,
    for languages: [Language],
    server: SourceKitLSPServer
  ) async {
    guard clientHasDynamicCompletionRegistration else { return }

    await registerLanguageSpecificCapability(
      options: CompletionRegistrationOptions(
        documentSelector: DocumentSelector(for: languages),
        completionOptions: options
      ),
      forMethod: CompletionRequest.method,
      languages: languages,
      in: server,
      registrationDict: completion,
      setRegistrationDict: { completion[$0] = $1 }
    )
  }

  public func registerDidChangeWatchedFiles(
    watchers: [FileSystemWatcher],
    server: SourceKitLSPServer
  ) async {
    guard clientHasDynamicDidChangeWatchedFilesRegistration else { return }
    if let registration = didChangeWatchedFiles {
      if watchers != registration.watchers {
        logger.fault(
          "Unable to register new file system watchers \(watchers) due to pre-existing options \(registration.watchers)"
        )
      }
      return
    }
    let registrationOptions = DidChangeWatchedFilesRegistrationOptions(
      watchers: watchers
    )
    let registration = CapabilityRegistration(
      method: DidChangeWatchedFilesNotification.method,
      registerOptions: registrationOptions.encodeToLSPAny()
    )

    self.didChangeWatchedFiles = registrationOptions

    do {
      _ = try await server.client.send(RegisterCapabilityRequest(registrations: [registration]))
    } catch {
      logger.error("Failed to dynamically register for watched files: \(error.forLogging)")
      self.didChangeWatchedFiles = nil
    }
  }

  /// Dynamically register folding range capabilities if the client supports it and
  /// we haven't yet registered any folding range capabilities for the given
  /// languages.
  public func registerFoldingRangeIfNeeded(
    options: FoldingRangeOptions,
    for languages: [Language],
    server: SourceKitLSPServer
  ) async {
    guard clientHasDynamicFoldingRangeRegistration else { return }

    await registerLanguageSpecificCapability(
      options: FoldingRangeRegistrationOptions(
        documentSelector: DocumentSelector(for: languages),
        foldingRangeOptions: options
      ),
      forMethod: FoldingRangeRequest.method,
      languages: languages,
      in: server,
      registrationDict: foldingRange,
      setRegistrationDict: { foldingRange[$0] = $1 }
    )
  }

  /// Dynamically register semantic tokens capabilities if the client supports
  /// it and we haven't yet registered any semantic tokens capabilities for the
  /// given languages.
  public func registerSemanticTokensIfNeeded(
    options: SemanticTokensOptions,
    for languages: [Language],
    server: SourceKitLSPServer
  ) async {
    guard clientHasDynamicSemanticTokensRegistration else { return }

    await registerLanguageSpecificCapability(
      options: SemanticTokensRegistrationOptions(
        documentSelector: DocumentSelector(for: languages),
        semanticTokenOptions: options
      ),
      forMethod: SemanticTokensRegistrationOptions.method,
      languages: languages,
      in: server,
      registrationDict: semanticTokens,
      setRegistrationDict: { semanticTokens[$0] = $1 }
    )
  }

  /// Dynamically register inlay hint capabilities if the client supports
  /// it and we haven't yet registered any inlay hint capabilities for the
  /// given languages.
  public func registerInlayHintIfNeeded(
    options: InlayHintOptions,
    for languages: [Language],
    server: SourceKitLSPServer
  ) async {
    guard clientHasDynamicInlayHintRegistration else { return }
    await registerLanguageSpecificCapability(
      options: InlayHintRegistrationOptions(
        documentSelector: DocumentSelector(for: languages),
        inlayHintOptions: options
      ),
      forMethod: InlayHintRequest.method,
      languages: languages,
      in: server,
      registrationDict: inlayHint,
      setRegistrationDict: { inlayHint[$0] = $1 }
    )
  }

  /// Dynamically register (pull model) diagnostic capabilities,
  /// if the client supports it.
  public func registerDiagnosticIfNeeded(
    options: DiagnosticOptions,
    for languages: [Language],
    server: SourceKitLSPServer
  ) async {
    guard clientHasDynamicDocumentDiagnosticsRegistration else { return }

    await registerLanguageSpecificCapability(
      options: DiagnosticRegistrationOptions(
        documentSelector: DocumentSelector(for: languages),
        diagnosticOptions: options
      ),
      forMethod: DocumentDiagnosticsRequest.method,
      languages: languages,
      in: server,
      registrationDict: pullDiagnostics,
      setRegistrationDict: { pullDiagnostics[$0] = $1 }
    )
  }

  /// Dynamically register executeCommand with the given IDs if the client supports
  /// it and we haven't yet registered the given command IDs yet.
  public func registerExecuteCommandIfNeeded(
    commands: [String],
    server: SourceKitLSPServer
  ) {
    guard clientHasDynamicExecuteCommandRegistration else { return }

    var newCommands = Set(commands)
    newCommands.subtract(self.commandIds)

    // We only want to send the registration with unregistered command IDs since
    // clients such as VS Code only allow a command to be registered once. We could
    // unregister all our commandIds first but this is simpler.
    guard !newCommands.isEmpty else { return }
    self.commandIds.formUnion(newCommands)

    let registration = CapabilityRegistration(
      method: ExecuteCommandRequest.method,
      registerOptions: ExecuteCommandRegistrationOptions(commands: Array(newCommands)).encodeToLSPAny()
    )

    let _ = server.client.send(RegisterCapabilityRequest(registrations: [registration])) { result in
      if let error = result.failure {
        logger.error("Failed to dynamically register commands: \(error.forLogging)")
      }
    }
  }
}

fileprivate extension DocumentSelector {
  init(for languages: [Language]) {
    self.init(languages.map { DocumentFilter(language: $0.rawValue) })
  }
}