a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
170 lines
5.9 KiB
Go
170 lines
5.9 KiB
Go
package lsp
|
|
|
|
import (
|
|
"encoding/json"
|
|
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// Capability tracking — provider-side support for the LSP
|
|
// client/registerCapability and client/unregisterCapability flow.
|
|
//
|
|
// Background. The initialize handshake gives the client a snapshot of
|
|
// the server's capabilities, but sophisticated servers (jdtls,
|
|
// tsserver, rust-analyzer) lean on dynamic registration: they keep the
|
|
// initial handshake cheap and announce extra capabilities lazily, once
|
|
// they finish workspace warmup. Without dynamic-registration support,
|
|
// any feature the server publishes late is silently unused.
|
|
//
|
|
// The flow:
|
|
//
|
|
// server → client/registerCapability {registrations:[…]} → reply null
|
|
// server → client/unregisterCapability {unregisterations:[…]} → reply null
|
|
//
|
|
// We accept both the request form (with an id, expecting an ack) and
|
|
// the notification form (no id, no reply). Some servers send one,
|
|
// some the other — handle both shapes via the same applyXxx helper.
|
|
//
|
|
// Supports(method) is the single read API. It consults the static
|
|
// initialize-time snapshot AND the dynamic table, returning true if
|
|
// either advertises the method. The dynamic table is keyed by
|
|
// Registration.ID (the wire handle the server uses for unregister) —
|
|
// Supports iterates values; for the small N we expect (~tens of
|
|
// dynamic registrations per server, ever) iteration is cheaper than
|
|
// maintaining a method index that has to handle multi-registration.
|
|
|
|
// applyRegistrations records every registration in params on the
|
|
// provider's dynamic capability table. Safe to call from both the
|
|
// request-form handler (where the LSP id is meaningful) and the
|
|
// notification-form handler (no reply expected).
|
|
func (p *Provider) applyRegistrations(params json.RawMessage) {
|
|
if len(params) == 0 {
|
|
return
|
|
}
|
|
var rp RegistrationParams
|
|
if err := json.Unmarshal(params, &rp); err != nil {
|
|
if p.logger != nil {
|
|
p.logger.Debug("LSP: malformed registerCapability params", zap.Error(err))
|
|
}
|
|
return
|
|
}
|
|
if len(rp.Registrations) == 0 {
|
|
return
|
|
}
|
|
p.capsMu.Lock()
|
|
for _, r := range rp.Registrations {
|
|
if r.ID == "" || r.Method == "" {
|
|
// Malformed registration — record nothing rather than
|
|
// poison the table with an unkeyable entry.
|
|
continue
|
|
}
|
|
p.dynamicCaps[r.ID] = r
|
|
}
|
|
p.capsMu.Unlock()
|
|
}
|
|
|
|
// applyUnregistrations removes every entry in params from the dynamic
|
|
// table. Per the LSP wire format the JSON field is the misspelled
|
|
// "unregisterations" — UnregistrationParams keeps that spelling.
|
|
// Servers may send an unregister for an id we never recorded
|
|
// (e.g., one issued before our last respawn); ignore those silently.
|
|
func (p *Provider) applyUnregistrations(params json.RawMessage) {
|
|
if len(params) == 0 {
|
|
return
|
|
}
|
|
var up UnregistrationParams
|
|
if err := json.Unmarshal(params, &up); err != nil {
|
|
if p.logger != nil {
|
|
p.logger.Debug("LSP: malformed unregisterCapability params", zap.Error(err))
|
|
}
|
|
return
|
|
}
|
|
if len(up.Unregisterations) == 0 {
|
|
return
|
|
}
|
|
p.capsMu.Lock()
|
|
for _, u := range up.Unregisterations {
|
|
if u.ID == "" {
|
|
continue
|
|
}
|
|
delete(p.dynamicCaps, u.ID)
|
|
}
|
|
p.capsMu.Unlock()
|
|
}
|
|
|
|
// Supports reports whether the server advertises the given LSP method,
|
|
// consulting both the initialize-time snapshot and any dynamic
|
|
// registrations the server has issued since.
|
|
//
|
|
// Convention. The static-side check uses a small switch over the
|
|
// methods Provider actually dispatches today (hover, references,
|
|
// definition, implementation, codeAction, executeCommand, the call /
|
|
// type hierarchy preparators, plus the document-sync notifications).
|
|
// Methods outside that set fall through to the dynamic check — if a
|
|
// server registers a capability we don't have a static mapping for
|
|
// (e.g. textDocument/foldingRange, textDocument/semanticTokens) we
|
|
// still record it and report it as supported, so a future feature
|
|
// site can flip on once the registration arrives.
|
|
//
|
|
// Callers may use Supports as a precondition before issuing an LSP
|
|
// call, but it is not required: the existing dispatch sites tolerate
|
|
// "method not found" errors from the server. Supports is most useful
|
|
// for new feature sites that want to short-circuit cleanly when the
|
|
// capability was never announced.
|
|
func (p *Provider) Supports(method string) bool {
|
|
if method == "" {
|
|
return false
|
|
}
|
|
p.capsMu.RLock()
|
|
defer p.capsMu.RUnlock()
|
|
if staticServerSupports(&p.caps, method) {
|
|
return true
|
|
}
|
|
for _, r := range p.dynamicCaps {
|
|
if r.Method == method {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// staticServerSupports inspects the initialize-time ServerCapabilities
|
|
// snapshot for the methods this provider's dispatch sites use today.
|
|
// A nil / zero field means "the server did not advertise this at
|
|
// initialize time" — the dynamic table may still flip it on later.
|
|
func staticServerSupports(caps *ServerCapabilities, method string) bool {
|
|
if caps == nil {
|
|
return false
|
|
}
|
|
switch method {
|
|
case "textDocument/hover":
|
|
return caps.HoverProvider != nil
|
|
case "textDocument/definition":
|
|
return caps.DefinitionProvider != nil
|
|
case "textDocument/references":
|
|
return caps.ReferencesProvider != nil
|
|
case "textDocument/implementation":
|
|
return caps.ImplementationProvider != nil
|
|
case "textDocument/codeAction":
|
|
return caps.CodeActionProvider != nil
|
|
case "workspace/executeCommand":
|
|
return caps.ExecuteCommandProvider != nil
|
|
case "textDocument/prepareCallHierarchy",
|
|
"callHierarchy/incomingCalls",
|
|
"callHierarchy/outgoingCalls":
|
|
return caps.CallHierarchyProvider != nil
|
|
case "textDocument/prepareTypeHierarchy",
|
|
"typeHierarchy/supertypes",
|
|
"typeHierarchy/subtypes":
|
|
return caps.TypeHierarchyProvider != nil
|
|
case "textDocument/didOpen",
|
|
"textDocument/didChange",
|
|
"textDocument/didClose":
|
|
return caps.TextDocumentSync != nil
|
|
}
|
|
// Method is outside the static mapping. Let the dynamic table
|
|
// have the final say — Supports() iterates dynamicCaps after this
|
|
// returns false.
|
|
return false
|
|
}
|