Skip to main content

lattice_lsp/
capabilities.rs

1//! Client capability advertisement (sent during `initialize`)
2//! and server capability storage (returned by `initialize`).
3//!
4//! ## Advertise discipline
5//!
6//! Per LSP spec, advertising a capability obliges the client to
7//! handle the corresponding server behaviour. We grow the
8//! advertised set in lockstep with feature implementations:
9//!
10//! | Phase | Capability bucket |
11//! |---|---|
12//! | 4.1 (this commit) | `general` (encoding + stale-request), `workspace` (applyEdit, configuration, workspaceFolders), `textDocument.synchronization`, `textDocument.publishDiagnostics` |
13//! | 4.2 | `textDocument`: hover, definition/declaration/typeDefinition/implementation, references, documentSymbol, workspace.symbol, completion |
14//! | 4.3 | `textDocument`: codeAction, rename, formatting/rangeFormatting/onTypeFormatting, signatureHelp |
15//! | 4.4 | `textDocument`: semanticTokens, inlayHint, foldingRange, documentHighlight, selectionRange, callHierarchy, etc. |
16//!
17//! Each phase ADDs to [`client_capabilities`]; the server side
18//! gating just reads from [`Capabilities`] and skips features the
19//! server didn't advertise back.
20//!
21//! ## Position encoding
22//!
23//! We prefer `utf-8` (LSP 3.17 introduced `general.positionEncodings`).
24//! Most modern servers honour the negotiation; older ones default
25//! to utf-16, which we handle via the column converter (queued for
26//! 4.1.c). Keeping both in the advertised list lets us survive
27//! servers that ignore `positionEncodings` entirely.
28
29use std::sync::Arc;
30
31use lsp_types::{
32    ClientCapabilities, GeneralClientCapabilities, HoverClientCapabilities, MarkupKind,
33    PositionEncodingKind, PublishDiagnosticsClientCapabilities, ServerCapabilities,
34    SignatureHelpClientCapabilities, SignatureInformationSettings,
35    StaleRequestSupportClientCapabilities, TagSupport, TextDocumentClientCapabilities,
36    TextDocumentSyncClientCapabilities, WorkspaceClientCapabilities,
37    WorkspaceEditClientCapabilities,
38};
39
40use crate::dynamic_registration::DynamicRegistry;
41
42/// 4.4.m: discriminator for the six file-operation hooks. Used
43/// by [`Capabilities::file_operations_options`] so probes don't
44/// repeat the
45/// `server.workspace.as_ref().and_then(|w| w.file_operations.as_ref())`
46/// walk; the enum names the field instead.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
48pub enum FileOpKind {
49    WillCreate,
50    DidCreate,
51    WillRename,
52    DidRename,
53    WillDelete,
54    DidDelete,
55}
56
57/// Build the full set of capabilities the client advertises in
58/// `initialize`. Pure -- safe to call from any task / thread.
59///
60/// The structure is verbose by design: each `Some(...)` here is
61/// a deliberate "we handle this" promise. Anything left as
62/// `None` (or absent from the struct) is something we don't yet
63/// implement; servers MAY still send it, but we ignore the
64/// response.
65pub fn client_capabilities() -> ClientCapabilities {
66    ClientCapabilities {
67        general: Some(general_capabilities()),
68        workspace: Some(workspace_capabilities()),
69        text_document: Some(text_document_capabilities()),
70        // L1c/L3: advertise window.workDoneProgress so servers
71        // (rust-analyzer et al.) are allowed to create server-initiated
72        // work-done progress (`window/workDoneProgress/create` +
73        // `$/progress`). Without it rust-analyzer never reports indexing
74        // progress, so the modeline can't show the scan or readiness.
75        // notebook_document / experimental: not used yet; added per
76        // phase as features land.
77        window: Some(window_capabilities()),
78        // L2: advertise rust-analyzer's serverStatus extension so it
79        // sends `experimental/serverStatus { quiescent, health }` -- the
80        // exact readiness signal the modeline turns into ✓/⟳/✗.
81        experimental: Some(serde_json::json!({ "serverStatusNotification": true })),
82        ..Default::default()
83    }
84}
85
86/// Advertise `window.workDoneProgress = true`. rust-analyzer (and most
87/// servers) gate their indexing / cache-priming progress on this; with
88/// it they stream the `$/progress` the modeline surfaces
89/// (lsp-architecture.md §12/§14). showMessage / showDocument request
90/// sub-caps are left default — we handle those requests regardless of
91/// advertisement.
92fn window_capabilities() -> lsp_types::WindowClientCapabilities {
93    lsp_types::WindowClientCapabilities {
94        work_done_progress: Some(true),
95        ..Default::default()
96    }
97}
98
99fn general_capabilities() -> GeneralClientCapabilities {
100    GeneralClientCapabilities {
101        // Prefer utf-8 (one byte == one code unit; matches our
102        // internal Position::byte). Accept utf-16 as a fallback
103        // for servers that don't honour positionEncodings.
104        // utf-32 is allowed by the spec but practically unused;
105        // we'd add it if a server demanded it.
106        position_encodings: Some(vec![
107            PositionEncodingKind::UTF8,
108            PositionEncodingKind::UTF16,
109        ]),
110        // Tells the server we honour `$/cancelRequest` -- it can
111        // free its scheduling slot when we cancel. Without this,
112        // some servers run the cancelled request to completion.
113        stale_request_support: Some(StaleRequestSupportClientCapabilities {
114            cancel: true,
115            // No specific retry-on-content-modified routing yet;
116            // when 4.4 lands semanticTokens delta we'll add the
117            // affected method names here.
118            retry_on_content_modified: Vec::new(),
119        }),
120        // markdown / regular-expressions advertisement is
121        // server-specific; rust-analyzer / pyright / gopls don't
122        // strictly require them. Leave None until a feature
123        // depends on the negotiation.
124        ..Default::default()
125    }
126}
127
128fn workspace_capabilities() -> WorkspaceClientCapabilities {
129    WorkspaceClientCapabilities {
130        // We honour `workspace/applyEdit` -- the server-initiated
131        // request that drives rename / code-action edits in 4.3.
132        // Advertising in 4.1 is harmless: a server that doesn't
133        // use it still gets a server-error if it tries before
134        // we ship the handler.
135        apply_edit: Some(true),
136        // workspaceEdit shape: documentChanges + resourceOperations
137        // are needed by 4.3's rename + code-action pipeline. We
138        // advertise them now so servers that begin sending them
139        // pre-4.3 get the promise honoured by then; the receiver
140        // is implemented when the feature lands.
141        workspace_edit: Some(WorkspaceEditClientCapabilities {
142            document_changes: Some(true),
143            resource_operations: Some(vec![
144                lsp_types::ResourceOperationKind::Create,
145                lsp_types::ResourceOperationKind::Rename,
146                lsp_types::ResourceOperationKind::Delete,
147            ]),
148            failure_handling: Some(lsp_types::FailureHandlingKind::TextOnlyTransactional),
149            normalizes_line_endings: Some(true),
150            change_annotation_support: None,
151        }),
152        // workspace/configuration: lets the server pull config
153        // from us at runtime. Useful even before §5.12 lands --
154        // we can return the empty object and most servers cope.
155        configuration: Some(true),
156        // workspace/symbol with lazy-resolve support (Phase
157        // 4.2 follow-up). When the server returns a
158        // `WorkspaceSymbol` whose `location` is the
159        // `WorkspaceLocation` variant (URI only, no range), the
160        // client fires `workspaceSymbol/resolve` to fill in the
161        // range on accept. We list `location.range` as the
162        // resolvable property -- matches what rust-analyzer /
163        // gopls populate today.
164        symbol: Some(lsp_types::WorkspaceSymbolClientCapabilities {
165            dynamic_registration: Some(false),
166            symbol_kind: None,
167            tag_support: None,
168            resolve_support: Some(lsp_types::WorkspaceSymbolResolveSupportCapability {
169                properties: vec!["location.range".into()],
170            }),
171        }),
172        // Single-root workspace for v1; multi-root WorkspaceFolder
173        // arrives later. Advertising true here means we send
174        // `workspaceFolders` in initialize and emit the
175        // `workspace/didChangeWorkspaceFolders` notification.
176        workspace_folders: Some(true),
177        // 4.4.g: tell servers we honour
178        // `workspace/inlayHint/refresh`. Without this the server
179        // either never sends the request or sends it and we
180        // reject -- both make stale inlay hints stick around.
181        inlay_hint: Some(lsp_types::InlayHintWorkspaceClientCapabilities {
182            refresh_support: Some(true),
183        }),
184        // 4.4.i: tell servers we honour
185        // `workspace/semanticTokens/refresh`. Required for the
186        // delta path -- when the server can't compute a delta
187        // for a previously-issued result_id (e.g. after an
188        // internal index rebuild), it pushes a refresh request
189        // and we fall back to a fresh full baseline.
190        semantic_tokens: Some(lsp_types::SemanticTokensWorkspaceClientCapabilities {
191            refresh_support: Some(true),
192        }),
193        // 4.4.j: tell servers we honour
194        // `workspace/diagnostic/refresh`. The per-document
195        // pull pump caches result_ids per buffer; on refresh
196        // we evict the cache for every attached buffer so
197        // the next tick re-pulls and the server can answer
198        // either `Full` (forced refresh) or `Unchanged`.
199        diagnostic: Some(lsp_types::DiagnosticWorkspaceClientCapabilities {
200            refresh_support: Some(true),
201        }),
202        // 4.5.d: tell servers we honour
203        // `workspace/codeLens/refresh`. On refresh we evict
204        // the cache for every attached buffer so the next
205        // tick's pump re-issues `textDocument/codeLens`.
206        code_lens: Some(lsp_types::CodeLensWorkspaceClientCapabilities {
207            refresh_support: Some(true),
208        }),
209        // 4.5.h: tell servers we honour
210        // `workspace/inlineValue/refresh`. Same shape as the
211        // other workspace refresh advertisements; the actor
212        // accepts the inbound request inline and publishes
213        // `LspInlineValueRefresh`. The host-side renderer
214        // trigger is itself deferred (no debug-adapter
215        // integration), so the refresh just logs for now.
216        inline_value: Some(lsp_types::InlineValueWorkspaceClientCapabilities {
217            refresh_support: Some(true),
218        }),
219        // 4.4.k: tell servers we send
220        // `workspace/didChangeConfiguration` when typed
221        // options under `lsp.*` change. `dynamic_registration
222        // = false` because our config keyspace is static at
223        // build time; runtime registration of new keys is a
224        // post-1.0 feature (it would need WASM plugins to
225        // declare option schemas).
226        did_change_configuration: Some(lsp_types::DynamicRegistrationClientCapabilities {
227            dynamic_registration: Some(false),
228        }),
229        // 4.4.n: we honour dynamic registration of
230        // `workspace/didChangeWatchedFiles`. Servers (notably
231        // rust-analyzer + tsserver) register file-watcher
232        // patterns lazily after handshake -- the glob set
233        // depends on the active workspace, not on anything
234        // visible at `initialize` time. Without this advert
235        // they fall back to "watch nothing" and miss out-of-
236        // band edits (e.g. a `git checkout` that rewrites
237        // source files without a `didChange`).
238        //
239        // `relative_pattern_support = true` lets the server
240        // anchor patterns to a `WorkspaceFolder` rather than
241        // emitting absolute globs -- matters once 4.4.l's
242        // file-watcher pump consumes the registrations.
243        did_change_watched_files: Some(lsp_types::DidChangeWatchedFilesClientCapabilities {
244            dynamic_registration: Some(true),
245            relative_pattern_support: Some(true),
246        }),
247        // 4.4.m: workspace file-operation hooks. Advertising
248        // each as `Some(true)` tells the server we honour the
249        // corresponding will/did request or notification. The
250        // host's trigger discipline lives in the save / saveas
251        // / bd paths; today the wire wrappers are callable but
252        // not every trigger is fully wired (see the per-row
253        // strong-reason defers in lsp-features.md). Servers
254        // only emit the hooks for URIs matching their
255        // registered `FileOperationFilter`s, so a server that
256        // doesn't care about a particular path stays quiet
257        // even when we advertise interest.
258        file_operations: Some(lsp_types::WorkspaceFileOperationsClientCapabilities {
259            dynamic_registration: Some(false),
260            did_create: Some(true),
261            will_create: Some(true),
262            did_rename: Some(true),
263            will_rename: Some(true),
264            did_delete: Some(true),
265            will_delete: Some(true),
266        }),
267        // Other 4.4 workspace-side advertisements
268        // (codeLens.refresh, diagnostics.refresh) are added when
269        // those phases land.
270        ..Default::default()
271    }
272}
273
274fn text_document_capabilities() -> TextDocumentClientCapabilities {
275    TextDocumentClientCapabilities {
276        synchronization: Some(synchronization_capabilities()),
277        publish_diagnostics: Some(publish_diagnostics_capabilities()),
278        // Advertise that we render hover / signatureHelp content as
279        // markdown (preferred) or plaintext. Without this the
280        // server defaults to plaintext per the LSP spec, which
281        // strips fenced code blocks and inline markup -- the
282        // markdown highlighter then has no patterns to colour and
283        // the popup renders as flat grey text.
284        hover: Some(hover_capabilities()),
285        signature_help: Some(signature_help_capabilities()),
286        // 4.4.j: pull-based diagnostics client capabilities.
287        // `related_document_support = true` tells the server
288        // it can include related-document reports in the
289        // response; the host applies them via the same
290        // DiagnosticEvent path used by push diagnostics.
291        diagnostic: Some(lsp_types::DiagnosticClientCapabilities {
292            dynamic_registration: Some(false),
293            related_document_support: Some(true),
294        }),
295        // 4.5.a: tell servers we honour the call-hierarchy
296        // pipeline (`textDocument/prepareCallHierarchy` →
297        // `callHierarchy/{incoming,outgoing}Calls`).
298        // `dynamic_registration: false` keeps the gate purely
299        // server-cap driven; switching to dynamic doesn't buy
300        // us anything because the host can't suppress the
301        // commands at register time -- they probe `supports_*`
302        // on each invocation.
303        call_hierarchy: Some(lsp_types::DynamicRegistrationClientCapabilities {
304            dynamic_registration: Some(false),
305        }),
306        // 4.5.b: tell servers we honour the type-hierarchy
307        // pipeline (`textDocument/prepareTypeHierarchy` →
308        // `typeHierarchy/{super,sub}types`). Same shape
309        // rationale as `call_hierarchy` above.
310        type_hierarchy: Some(lsp_types::DynamicRegistrationClientCapabilities {
311            dynamic_registration: Some(false),
312        }),
313        // 4.5.g: tell servers we honour
314        // `textDocument/moniker`. Useful for cross-project
315        // navigation (SCIP/LSIF-style indexes); the host
316        // surfaces results via `:lsp-moniker` as a plain echo.
317        moniker: Some(lsp_types::DynamicRegistrationClientCapabilities {
318            dynamic_registration: Some(false),
319        }),
320        // 4.5.c: tell servers we honour
321        // `textDocument/documentLink`. `tooltip_support = true`
322        // lets the server emit the `tooltip` field; future
323        // hover-on-link UX consumes it (the renderer overlay
324        // / `gx` path doesn't need it today, but advertising
325        // is harmless).
326        document_link: Some(lsp_types::DocumentLinkClientCapabilities {
327            dynamic_registration: Some(false),
328            tooltip_support: Some(true),
329        }),
330        // 4.5.d: tell servers we honour `textDocument/codeLens`.
331        // No `tooltipSupport` / `commandSupport` fields here
332        // (LSP doesn't model them); the wire wrapper consumes
333        // every lens shape the server emits.
334        code_lens: Some(lsp_types::DynamicRegistrationClientCapabilities {
335            dynamic_registration: Some(false),
336        }),
337        // 4.5.e: tell servers we honour `textDocument/documentColor`
338        // (+ `colorPresentation`). The host caches color
339        // ranges per buffer; `:lsp-color-presentation` opens
340        // the alternative-format picker at the cursor.
341        color_provider: Some(lsp_types::DynamicRegistrationClientCapabilities {
342            dynamic_registration: Some(false),
343        }),
344        ..Default::default()
345    }
346}
347
348fn hover_capabilities() -> HoverClientCapabilities {
349    HoverClientCapabilities {
350        dynamic_registration: Some(false),
351        content_format: Some(vec![MarkupKind::Markdown, MarkupKind::PlainText]),
352    }
353}
354
355fn signature_help_capabilities() -> SignatureHelpClientCapabilities {
356    SignatureHelpClientCapabilities {
357        dynamic_registration: Some(false),
358        signature_information: Some(SignatureInformationSettings {
359            documentation_format: Some(vec![MarkupKind::Markdown, MarkupKind::PlainText]),
360            parameter_information: None,
361            active_parameter_support: None,
362        }),
363        context_support: None,
364    }
365}
366
367fn synchronization_capabilities() -> TextDocumentSyncClientCapabilities {
368    TextDocumentSyncClientCapabilities {
369        dynamic_registration: Some(false),
370        // We honour will-save and will-save-wait-until in 4.3
371        // (when format-on-save lands). Advertising now is
372        // harmless; the methods are gated by `text_document_sync`
373        // on the *server* side.
374        will_save: Some(true),
375        will_save_wait_until: Some(true),
376        did_save: Some(true),
377    }
378}
379
380fn publish_diagnostics_capabilities() -> PublishDiagnosticsClientCapabilities {
381    PublishDiagnosticsClientCapabilities {
382        // We surface `relatedInformation` in the diagnostics
383        // buffer (a diagnostic with linked secondary locations
384        // shows them as sub-rows).
385        related_information: Some(true),
386        // tagSupport lets the renderer render `Unnecessary` /
387        // `Deprecated` differently (strikethrough / dim).
388        tag_support: Some(TagSupport {
389            value_set: vec![
390                lsp_types::DiagnosticTag::UNNECESSARY,
391                lsp_types::DiagnosticTag::DEPRECATED,
392            ],
393        }),
394        // version_support: server tags each diagnostic with the
395        // doc version it computed against; we drop diagnostics
396        // older than the current version so a stale
397        // publishDiagnostics from before our last edit doesn't
398        // overwrite fresher state.
399        version_support: Some(true),
400        // codeDescriptionSupport: adds `codeDescription.href` to
401        // each diagnostic -- a deep link to the rule's docs (e.g.
402        // rustc explain output). Used by the diagnostics buffer's
403        // `gx` handler.
404        code_description_support: Some(true),
405        // dataSupport: lets the server attach an opaque blob to a
406        // diagnostic that we round-trip on `codeAction.diagnostics`.
407        // No-op until 4.3.
408        data_support: Some(true),
409    }
410}
411
412/// Snapshot of the negotiated capabilities. The actor publishes
413/// one of these through an `arc_swap::ArcSwap` cell on the
414/// [`ServerHandle`](crate::ServerHandle) (4.4.n); per-feature
415/// dispatch loads a fresh snapshot before issuing a request.
416///
417/// Before 4.4.n this lived in a plain `Arc<Capabilities>`
418/// captured at handshake; the dynamic registry was tracked
419/// nowhere. The ArcSwap layer makes register / unregister
420/// observable without changing the read-side ergonomics:
421/// `caps.supports_completion()` still works the same way, it
422/// just now consults the dynamic side too.
423#[derive(Debug, Clone)]
424pub struct Capabilities {
425    /// What we advertised. Stable for the actor's lifetime.
426    pub client: ClientCapabilities,
427    /// What the server advertised in its `initialize` response.
428    /// Stable for the actor's lifetime; dynamic registrations
429    /// ride in [`Self::dynamic`].
430    pub server: ServerCapabilities,
431    /// Final negotiated position encoding. utf-8 if the server
432    /// honoured our preference; utf-16 otherwise (older servers
433    /// that ignore `general.positionEncodings`).
434    pub position_encoding: PositionEncodingKind,
435    /// 4.4.n: registrations the server added via
436    /// `client/registerCapability` after handshake. Empty on a
437    /// freshly-attached server; each register / unregister
438    /// notification produces a new snapshot the actor publishes
439    /// through the ArcSwap cell. Probes (`supports_*` and
440    /// friends) that care about dynamic-only registrations OR
441    /// in `dynamic.has(method)`; consumers that need the
442    /// `register_options` blob (file watchers, etc.) walk
443    /// [`crate::DynamicRegistry::registrations_for`].
444    pub dynamic: DynamicRegistry,
445}
446
447impl Capabilities {
448    /// Construct from server's `initialize` response. Picks the
449    /// position encoding the server advertised (if any) or
450    /// defaults to utf-16 -- the LSP 3.17 fallback when the
451    /// server doesn't honour negotiation. Starts with an empty
452    /// dynamic registry; the actor adds entries as
453    /// `client/registerCapability` notifications arrive.
454    pub fn from_initialize(client: ClientCapabilities, server: ServerCapabilities) -> Arc<Self> {
455        // Server's position encoding preference wins. If absent,
456        // the spec says utf-16 is the default (3.16 and earlier
457        // had no negotiation at all -- everything was utf-16).
458        let position_encoding = server
459            .position_encoding
460            .clone()
461            .unwrap_or(PositionEncodingKind::UTF16);
462        Arc::new(Self {
463            client,
464            server,
465            position_encoding,
466            dynamic: DynamicRegistry::new(),
467        })
468    }
469
470    /// 4.4.n: clone this capabilities snapshot with the
471    /// `dynamic` registry mutated by `f`. Returns the new
472    /// `Arc<Capabilities>` the caller stores in the ArcSwap
473    /// cell. Pure -- the old Arc stays valid for any reader
474    /// that already loaded it; the new Arc carries the
475    /// updated registry. Used by the actor on register /
476    /// unregister to publish without locking.
477    pub fn with_dynamic_mut(self: &Arc<Self>, f: impl FnOnce(&mut DynamicRegistry)) -> Arc<Self> {
478        let mut next = (**self).clone();
479        f(&mut next.dynamic);
480        Arc::new(next)
481    }
482
483    /// True iff the negotiated encoding is utf-8. Used by the
484    /// position-conversion shim to bypass utf-16 conversion when
485    /// the server agrees.
486    pub fn is_utf8(&self) -> bool {
487        self.position_encoding == PositionEncodingKind::UTF8
488    }
489
490    /// Server's `hoverProvider` presence -- gates 4.2's hover
491    /// dispatch. Returns false until 4.2 supplements the helper.
492    /// Consults the dynamic registry (4.4.n) so servers that
493    /// register hover lazily via `client/registerCapability`
494    /// still gate-in.
495    pub fn supports_hover(&self) -> bool {
496        self.server.hover_provider.is_some() || self.dynamic.has("textDocument/hover")
497    }
498
499    /// Server's `definitionProvider` presence. Consults the
500    /// dynamic registry (4.4.n).
501    pub fn supports_definition(&self) -> bool {
502        self.server.definition_provider.is_some() || self.dynamic.has("textDocument/definition")
503    }
504
505    /// Server's `referencesProvider` presence -- gates 4.2.d's
506    /// `gr` dispatch. Consults the dynamic registry (4.4.n).
507    pub fn supports_references(&self) -> bool {
508        self.server.references_provider.is_some() || self.dynamic.has("textDocument/references")
509    }
510
511    /// Server's `documentSymbolProvider` presence -- gates the
512    /// `:document-symbols` outline view (Phase 4.2.e). Consults
513    /// the dynamic registry (4.4.n).
514    pub fn supports_document_symbol(&self) -> bool {
515        self.server.document_symbol_provider.is_some()
516            || self.dynamic.has("textDocument/documentSymbol")
517    }
518
519    /// Server's `workspaceSymbolProvider` presence -- gates the
520    /// `:workspace-symbols` picker (Phase 4.2.f). Consults the
521    /// dynamic registry (4.4.n).
522    pub fn supports_workspace_symbol(&self) -> bool {
523        self.server.workspace_symbol_provider.is_some() || self.dynamic.has("workspace/symbol")
524    }
525
526    /// Whether the server advertises `resolveProvider` on its
527    /// workspaceSymbolProvider options. When true, the server
528    /// MAY return `WorkspaceSymbol` entries whose `location` is
529    /// the `WorkspaceLocation` (URI only) variant; the client
530    /// fires `workspaceSymbol/resolve` on accept to fill in the
531    /// `range` before jumping. Phase 4.2 follow-up.
532    pub fn workspace_symbol_resolve_provider(&self) -> bool {
533        match &self.server.workspace_symbol_provider {
534            Some(lsp_types::OneOf::Right(opts)) => opts.resolve_provider.unwrap_or(false),
535            // The `OneOf::Left(bool)` form doesn't carry options
536            // -- the boolean only signals presence; resolve is
537            // implicitly false there.
538            _ => false,
539        }
540    }
541
542    /// Server's `completionProvider` presence -- gates 4.2.g's
543    /// `gen:lsp-completion` source. Consults the dynamic
544    /// registry (4.4.n).
545    pub fn supports_completion(&self) -> bool {
546        self.server.completion_provider.is_some() || self.dynamic.has("textDocument/completion")
547    }
548
549    /// Whether the server advertises `resolveProvider` on its
550    /// completionProvider options. When true, completion items
551    /// may arrive with `documentation` / `additionalTextEdits`
552    /// missing and need `completionItem/resolve` before the
553    /// docs popup or accept paths read those fields.
554    pub fn completion_resolve_provider(&self) -> bool {
555        self.server
556            .completion_provider
557            .as_ref()
558            .and_then(|p| p.resolve_provider)
559            .unwrap_or(false)
560    }
561
562    /// Trigger characters the server wants completion to fire
563    /// on (auto-trigger mode). Empty when none advertised or
564    /// the provider is absent.
565    pub fn completion_trigger_chars(&self) -> Vec<char> {
566        self.server
567            .completion_provider
568            .as_ref()
569            .and_then(|p| p.trigger_characters.as_ref())
570            .map(|v| v.iter().filter_map(|s| s.chars().next()).collect())
571            .unwrap_or_default()
572    }
573
574    /// Server's `documentFormattingProvider` presence -- gates
575    /// `:format` (Phase 4.3). Consults the dynamic registry
576    /// (4.4.n).
577    pub fn supports_formatting(&self) -> bool {
578        self.server.document_formatting_provider.is_some()
579            || self.dynamic.has("textDocument/formatting")
580    }
581
582    /// Server's `documentRangeFormattingProvider` presence --
583    /// gates `:format-range` / `=` operator on motions / objects.
584    /// Consults the dynamic registry (4.4.n).
585    pub fn supports_range_formatting(&self) -> bool {
586        self.server.document_range_formatting_provider.is_some()
587            || self.dynamic.has("textDocument/rangeFormatting")
588    }
589
590    /// Server's `signatureHelpProvider` presence -- gates the
591    /// trigger-character signature popup (Phase 4.3). Consults
592    /// the dynamic registry (4.4.n).
593    pub fn supports_signature_help(&self) -> bool {
594        self.server.signature_help_provider.is_some()
595            || self.dynamic.has("textDocument/signatureHelp")
596    }
597
598    /// Server's `renameProvider` presence -- gates `:rename`
599    /// (Phase 4.3). Returns true for both bool and options
600    /// shapes the LSP spec allows. Consults the dynamic
601    /// registry (4.4.n).
602    pub fn supports_rename(&self) -> bool {
603        let static_ok = match self.server.rename_provider.as_ref() {
604            Some(lsp_types::OneOf::Left(b)) => *b,
605            Some(lsp_types::OneOf::Right(_)) => true,
606            None => false,
607        };
608        static_ok || self.dynamic.has("textDocument/rename")
609    }
610
611    /// Server's `codeActionProvider` presence -- gates
612    /// `:code-actions` (Phase 4.3). Returns true for both
613    /// bool and options shapes the LSP spec allows. Consults
614    /// the dynamic registry (4.4.n).
615    pub fn supports_code_action(&self) -> bool {
616        let static_ok = match self.server.code_action_provider.as_ref() {
617            Some(lsp_types::CodeActionProviderCapability::Simple(b)) => *b,
618            Some(lsp_types::CodeActionProviderCapability::Options(_)) => true,
619            None => false,
620        };
621        static_ok || self.dynamic.has("textDocument/codeAction")
622    }
623
624    /// Whether the server advertises `resolveProvider` on its
625    /// codeActionProvider options. When true, codeAction
626    /// items may arrive without `edit` and need
627    /// `codeAction/resolve` before apply.
628    pub fn code_action_resolve_provider(&self) -> bool {
629        match self.server.code_action_provider.as_ref() {
630            Some(lsp_types::CodeActionProviderCapability::Options(o)) => {
631                o.resolve_provider.unwrap_or(false)
632            }
633            _ => false,
634        }
635    }
636
637    /// Server's `executeCommandProvider` presence -- gates
638    /// `workspace/executeCommand` for codeAction items that
639    /// carry a `Command` payload.
640    pub fn supports_execute_command(&self) -> bool {
641        self.server.execute_command_provider.is_some()
642    }
643
644    /// 4.4.e: `documentHighlightProvider` -- references in the
645    /// current document at the cursor; used to paint same-symbol
646    /// occurrences as a soft overlay. Consults the dynamic
647    /// registry (4.4.n).
648    pub fn supports_document_highlight(&self) -> bool {
649        self.server.document_highlight_provider.is_some()
650            || self.dynamic.has("textDocument/documentHighlight")
651    }
652
653    /// 4.4.e: `selectionRangeProvider` -- structural smart-
654    /// expansion ranges around a position (token → expression →
655    /// statement → block → function ...). Consults the dynamic
656    /// registry (4.4.n).
657    pub fn supports_selection_range(&self) -> bool {
658        self.server.selection_range_provider.is_some()
659            || self.dynamic.has("textDocument/selectionRange")
660    }
661
662    /// 4.4.f: `foldingRangeProvider` -- feeds the LSP
663    /// foldmethod. The host's per-tick pump fires
664    /// `textDocument/foldingRange` when the buffer's document
665    /// version changes; the response seats into a per-buffer
666    /// cache the `recompute_folds` dispatcher reads. Consults
667    /// the dynamic registry (4.4.n).
668    pub fn supports_folding_range(&self) -> bool {
669        self.server.folding_range_provider.is_some()
670            || self.dynamic.has("textDocument/foldingRange")
671    }
672
673    /// 4.4.g: `inlayHintProvider` -- type / parameter
674    /// annotations rendered as virtual text inline with the
675    /// buffer's actual characters. The host's per-tick pump
676    /// fires `textDocument/inlayHint` over the visible range
677    /// when the buffer's document version changes; the
678    /// renderer overlay splices each hint's label at its
679    /// position. Consults the dynamic registry (4.4.n).
680    pub fn supports_inlay_hint(&self) -> bool {
681        self.server.inlay_hint_provider.is_some() || self.dynamic.has("textDocument/inlayHint")
682    }
683
684    /// 4.4.g follow-up: `InlayHintOptions.resolve_provider`.
685    /// When set, the server populates `tooltip` and
686    /// `text_edits` lazily -- callers issue `inlayHint/resolve`
687    /// for individual hints rather than getting the full
688    /// payload in the batched response. The host doesn't wire
689    /// a trigger today (interaction UX is strong-reason
690    /// deferred -- see lsp-features.md); the probe ships
691    /// alongside the wrapper so the future trigger has a
692    /// stable check to gate on.
693    pub fn supports_inlay_hint_resolve(&self) -> bool {
694        let Some(p) = self.server.inlay_hint_provider.as_ref() else {
695            return false;
696        };
697        match p {
698            lsp_types::OneOf::Left(_b) => false,
699            lsp_types::OneOf::Right(opts) => match opts {
700                lsp_types::InlayHintServerCapabilities::Options(options) => {
701                    options.resolve_provider == Some(true)
702                }
703                lsp_types::InlayHintServerCapabilities::RegistrationOptions(reg) => {
704                    reg.inlay_hint_options.resolve_provider == Some(true)
705                }
706            },
707        }
708    }
709
710    /// 4.4.h: `semanticTokensProvider` -- LSP-side highlight
711    /// layer that augments tree-sitter. The provider's legend
712    /// (declared at handshake) names the token types
713    /// (e.g. `"keyword"`, `"function"`) and modifiers
714    /// (e.g. `"static"`, `"readonly"`) the server's
715    /// `textDocument/semanticTokens/full` response references
716    /// by index. The host caches the legend at attach time so
717    /// decoding doesn't re-read capability state per request.
718    pub fn supports_semantic_tokens(&self) -> bool {
719        self.server.semantic_tokens_provider.is_some()
720    }
721
722    /// 4.4.h: token-type legend declared by the server. Used
723    /// by the host's decoder to map the integer token-type
724    /// index in `SemanticTokens.data` back to a semantic name
725    /// (`"keyword"`, `"function"`, etc.) for the renderer's
726    /// per-kind styling. Returns an empty vec when the server
727    /// doesn't advertise semantic tokens (the decoder then
728    /// skips every token as unrecognized).
729    pub fn semantic_token_types(&self) -> Vec<lsp_types::SemanticTokenType> {
730        use lsp_types::SemanticTokensServerCapabilities;
731        let Some(p) = self.server.semantic_tokens_provider.as_ref() else {
732            return Vec::new();
733        };
734        match p {
735            SemanticTokensServerCapabilities::SemanticTokensOptions(opts) => {
736                opts.legend.token_types.clone()
737            }
738            SemanticTokensServerCapabilities::SemanticTokensRegistrationOptions(opts) => {
739                opts.semantic_tokens_options.legend.token_types.clone()
740            }
741        }
742    }
743
744    /// 4.4.h: token-modifier legend declared by the server.
745    /// Same shape as [`Self::semantic_token_types`] but for
746    /// the bit-flag modifiers each token can carry.
747    pub fn semantic_token_modifiers(&self) -> Vec<lsp_types::SemanticTokenModifier> {
748        use lsp_types::SemanticTokensServerCapabilities;
749        let Some(p) = self.server.semantic_tokens_provider.as_ref() else {
750            return Vec::new();
751        };
752        match p {
753            SemanticTokensServerCapabilities::SemanticTokensOptions(opts) => {
754                opts.legend.token_modifiers.clone()
755            }
756            SemanticTokensServerCapabilities::SemanticTokensRegistrationOptions(opts) => {
757                opts.semantic_tokens_options.legend.token_modifiers.clone()
758            }
759        }
760    }
761
762    /// 4.4.i: server advertises `semanticTokens/full/delta`?
763    /// When `true`, the host issues delta requests with the
764    /// previous `result_id` after the first full response;
765    /// the server returns a small edit script the host
766    /// splices into the cached token vec. Falls back to full
767    /// when `false`.
768    pub fn supports_semantic_tokens_delta(&self) -> bool {
769        use lsp_types::{SemanticTokensFullOptions, SemanticTokensServerCapabilities};
770        let Some(p) = self.server.semantic_tokens_provider.as_ref() else {
771            return false;
772        };
773        let full = match p {
774            SemanticTokensServerCapabilities::SemanticTokensOptions(opts) => opts.full.as_ref(),
775            SemanticTokensServerCapabilities::SemanticTokensRegistrationOptions(opts) => {
776                opts.semantic_tokens_options.full.as_ref()
777            }
778        };
779        match full {
780            Some(SemanticTokensFullOptions::Delta { delta }) => delta.unwrap_or(false),
781            _ => false,
782        }
783    }
784
785    /// 4.4.i: server advertises `semanticTokens/range`?
786    /// Viewport-bounded request -- the host can issue this
787    /// for very large files to skip decoding tokens outside
788    /// the visible window. Not used by the v1 whole-buffer
789    /// pump but exposed for future viewport-aware fetching.
790    pub fn supports_semantic_tokens_range(&self) -> bool {
791        use lsp_types::SemanticTokensServerCapabilities;
792        let Some(p) = self.server.semantic_tokens_provider.as_ref() else {
793            return false;
794        };
795        let range_opt = match p {
796            SemanticTokensServerCapabilities::SemanticTokensOptions(opts) => opts.range.as_ref(),
797            SemanticTokensServerCapabilities::SemanticTokensRegistrationOptions(opts) => {
798                opts.semantic_tokens_options.range.as_ref()
799            }
800        };
801        // `Some(true)` advertises range support;
802        // `Some(false)` and `None` mean no.
803        matches!(range_opt, Some(true))
804    }
805
806    /// Server's `documentOnTypeFormattingProvider` presence
807    /// (Phase 4.3). Trigger-character driven formatting in
808    /// Insert mode -- returns the first character that fires
809    /// the request, plus more in `more_trigger_character`.
810    pub fn supports_on_type_formatting(&self) -> bool {
811        self.server.document_on_type_formatting_provider.is_some()
812    }
813
814    /// Trigger characters for onTypeFormatting. Empty when
815    /// the provider isn't advertised.
816    pub fn on_type_formatting_trigger_chars(&self) -> Vec<char> {
817        let Some(p) = self.server.document_on_type_formatting_provider.as_ref() else {
818            return Vec::new();
819        };
820        let mut out: Vec<char> = Vec::new();
821        if let Some(c) = p.first_trigger_character.chars().next() {
822            out.push(c);
823        }
824        if let Some(more) = p.more_trigger_character.as_ref() {
825            for s in more {
826                if let Some(c) = s.chars().next()
827                    && !out.contains(&c)
828                {
829                    out.push(c);
830                }
831            }
832        }
833        out
834    }
835
836    /// Whether the server advertises `prepareProvider` on its
837    /// rename options -- if so, `prepareRename` should run
838    /// before `rename` to validate the cursor and pick up the
839    /// placeholder. Most modern servers (rust-analyzer,
840    /// pyright, gopls) advertise this.
841    pub fn supports_prepare_rename(&self) -> bool {
842        match self.server.rename_provider.as_ref() {
843            Some(lsp_types::OneOf::Right(opts)) => opts.prepare_provider.unwrap_or(false),
844            _ => false,
845        }
846    }
847
848    /// 4.4.j: whether the server advertises pull-based
849    /// diagnostics (`textDocument/diagnostic`). Servers can
850    /// support pull alongside push or pull-only; in both
851    /// cases the host's pull pump fires on document-version
852    /// change and threads the cached `result_id` so the
853    /// server can answer `Unchanged` when nothing moved.
854    pub fn supports_pull_diagnostics(&self) -> bool {
855        self.server.diagnostic_provider.is_some()
856    }
857
858    /// 4.4.j: whether the server's diagnostic provider also
859    /// supports the workspace-wide `workspace/diagnostic`
860    /// pull. Reads `DiagnosticOptions.workspace_diagnostics`
861    /// from either flavour of `DiagnosticServerCapabilities`.
862    /// Returned `true` even when the host doesn't issue
863    /// workspace pulls today (strong-reason deferred -- see
864    /// lsp-features.md); future workspace-view rework
865    /// branches off this probe.
866    pub fn supports_workspace_diagnostic_pull(&self) -> bool {
867        let Some(p) = self.server.diagnostic_provider.as_ref() else {
868            return false;
869        };
870        match p {
871            lsp_types::DiagnosticServerCapabilities::Options(o) => o.workspace_diagnostics,
872            lsp_types::DiagnosticServerCapabilities::RegistrationOptions(r) => {
873                r.diagnostic_options.workspace_diagnostics
874            }
875        }
876    }
877
878    /// 4.5.a: server's `callHierarchyProvider` presence --
879    /// gates `:lsp-incoming-calls` / `:lsp-outgoing-calls`.
880    /// Returns true for either the bool-only or options-shape
881    /// the LSP spec allows. Consults the dynamic registry
882    /// (4.4.n).
883    pub fn supports_call_hierarchy(&self) -> bool {
884        let static_ok = matches!(
885            self.server.call_hierarchy_provider,
886            Some(lsp_types::CallHierarchyServerCapability::Simple(true))
887                | Some(lsp_types::CallHierarchyServerCapability::Options(_))
888        );
889        static_ok || self.dynamic.has("textDocument/prepareCallHierarchy")
890    }
891
892    /// 4.5.b: server's type-hierarchy support. lsp-types 0.97
893    /// doesn't model a static `type_hierarchy_provider` field on
894    /// `ServerCapabilities` (newer LSP versions add one); the
895    /// probe consults the dynamic registry only. Most servers
896    /// that support type hierarchy register it dynamically anyway
897    /// (rust-analyzer, pyright). If a server advertises static
898    /// type-hierarchy support via a `serverInfo`-embedded blob
899    /// we don't parse it; the dynamic path is the standard way
900    /// to negotiate.
901    pub fn supports_type_hierarchy(&self) -> bool {
902        self.dynamic.has("textDocument/prepareTypeHierarchy")
903    }
904
905    /// 4.5.g: server's `monikerProvider` presence. The probe
906    /// returns true for either the bool-only or options-shape;
907    /// also consults the dynamic registry (4.4.n).
908    pub fn supports_moniker(&self) -> bool {
909        let static_ok = matches!(
910            self.server.moniker_provider,
911            Some(lsp_types::OneOf::Left(true)) | Some(lsp_types::OneOf::Right(_))
912        );
913        static_ok || self.dynamic.has("textDocument/moniker")
914    }
915
916    /// 4.5.c: server's `documentLinkProvider` presence --
917    /// gates the per-tick pump that caches link ranges + the
918    /// `gx` keystroke that consults the cache. Consults the
919    /// dynamic registry (4.4.n).
920    pub fn supports_document_link(&self) -> bool {
921        self.server.document_link_provider.is_some()
922            || self.dynamic.has("textDocument/documentLink")
923    }
924
925    /// 4.5.c: whether `documentLinkProvider.resolveProvider`
926    /// is advertised. When true, links may arrive without a
927    /// `target` and need `documentLink/resolve` before they
928    /// can be followed. `gx` fires the resolve only when the
929    /// matched link lacks a target.
930    pub fn document_link_resolve_provider(&self) -> bool {
931        self.server
932            .document_link_provider
933            .as_ref()
934            .and_then(|opts| opts.resolve_provider)
935            .unwrap_or(false)
936    }
937
938    /// 4.5.d: server's `codeLensProvider` presence -- gates
939    /// the per-tick `textDocument/codeLens` pump + the
940    /// `:lsp-code-lens` picker. Consults the dynamic
941    /// registry (4.4.n).
942    pub fn supports_code_lens(&self) -> bool {
943        self.server.code_lens_provider.is_some() || self.dynamic.has("textDocument/codeLens")
944    }
945
946    /// 4.5.d: whether `codeLensProvider.resolveProvider` is
947    /// advertised. When true, lenses may arrive without a
948    /// `command` and need `codeLens/resolve` before the
949    /// accept-action can route through `executeCommand`.
950    pub fn code_lens_resolve_provider(&self) -> bool {
951        self.server
952            .code_lens_provider
953            .as_ref()
954            .and_then(|opts| opts.resolve_provider)
955            .unwrap_or(false)
956    }
957
958    /// 4.5.e: server's `colorProvider` presence -- gates the
959    /// per-tick `documentColor` pump + the
960    /// `:lsp-color-presentation` picker. Consults the dynamic
961    /// registry (4.4.n). lsp-types models the cap as
962    /// `Option<ColorProviderCapability>` which is a `OneOf<bool,
963    /// ...Options>`; both shapes count.
964    pub fn supports_color(&self) -> bool {
965        let static_ok = matches!(
966            self.server.color_provider,
967            Some(lsp_types::ColorProviderCapability::Simple(true))
968                | Some(lsp_types::ColorProviderCapability::ColorProvider(_))
969                | Some(lsp_types::ColorProviderCapability::Options(_))
970        );
971        static_ok || self.dynamic.has("textDocument/documentColor")
972    }
973
974    /// 4.5.f: server's `linkedEditingRangeProvider` presence.
975    /// Returns true for the bool-only, options, or
976    /// registration-options shapes; also consults the
977    /// dynamic registry.
978    pub fn supports_linked_editing_range(&self) -> bool {
979        let static_ok = self.server.linked_editing_range_provider.is_some();
980        static_ok || self.dynamic.has("textDocument/linkedEditingRange")
981    }
982
983    /// 4.5.h: server's `inlineValueProvider` presence.
984    /// Returns true for either the bool-only or options
985    /// shape; consults the dynamic registry.
986    pub fn supports_inline_value(&self) -> bool {
987        let static_ok = matches!(
988            self.server.inline_value_provider,
989            Some(lsp_types::OneOf::Left(true)) | Some(lsp_types::OneOf::Right(_))
990        );
991        static_ok || self.dynamic.has("textDocument/inlineValue")
992    }
993
994    // 4.5.i: `inlineCompletionProvider` (LSP 3.18 / pre-
995    // spec). lsp-types 0.97 doesn't export the param /
996    // response types -- see the wire-layer note in
997    // `features.rs`. Probe deferred to the same revisit.
998
999    /// 4.4.m: server advertises interest in
1000    /// `workspace/willCreateFiles` requests (server returns
1001    /// `WorkspaceEdit` BEFORE the file is created on disk; the
1002    /// host applies the edits in the same save txn).
1003    pub fn supports_will_create_files(&self) -> bool {
1004        self.file_operations_options(FileOpKind::WillCreate)
1005            .is_some()
1006    }
1007
1008    /// 4.4.m: server advertises interest in
1009    /// `workspace/didCreateFiles` notifications.
1010    pub fn supports_did_create_files(&self) -> bool {
1011        self.file_operations_options(FileOpKind::DidCreate)
1012            .is_some()
1013    }
1014
1015    /// 4.4.m: server advertises interest in
1016    /// `workspace/willRenameFiles` requests.
1017    pub fn supports_will_rename_files(&self) -> bool {
1018        self.file_operations_options(FileOpKind::WillRename)
1019            .is_some()
1020    }
1021
1022    /// 4.4.m: server advertises interest in
1023    /// `workspace/didRenameFiles` notifications.
1024    pub fn supports_did_rename_files(&self) -> bool {
1025        self.file_operations_options(FileOpKind::DidRename)
1026            .is_some()
1027    }
1028
1029    /// 4.4.m: server advertises interest in
1030    /// `workspace/willDeleteFiles` requests.
1031    pub fn supports_will_delete_files(&self) -> bool {
1032        self.file_operations_options(FileOpKind::WillDelete)
1033            .is_some()
1034    }
1035
1036    /// 4.4.m: server advertises interest in
1037    /// `workspace/didDeleteFiles` notifications.
1038    pub fn supports_did_delete_files(&self) -> bool {
1039        self.file_operations_options(FileOpKind::DidDelete)
1040            .is_some()
1041    }
1042
1043    /// 4.4.m: borrow the server-supplied
1044    /// `FileOperationRegistrationOptions` for the requested
1045    /// file-operation kind. The filters drive the host's
1046    /// will/did fan-out -- a server only receives events whose
1047    /// URIs match at least one filter's
1048    /// `FileOperationPattern.glob`. `None` when the server
1049    /// didn't advertise the kind.
1050    pub fn file_operations_options(
1051        &self,
1052        kind: FileOpKind,
1053    ) -> Option<&lsp_types::FileOperationRegistrationOptions> {
1054        let ops = self
1055            .server
1056            .workspace
1057            .as_ref()
1058            .and_then(|w| w.file_operations.as_ref())?;
1059        match kind {
1060            FileOpKind::WillCreate => ops.will_create.as_ref(),
1061            FileOpKind::DidCreate => ops.did_create.as_ref(),
1062            FileOpKind::WillRename => ops.will_rename.as_ref(),
1063            FileOpKind::DidRename => ops.did_rename.as_ref(),
1064            FileOpKind::WillDelete => ops.will_delete.as_ref(),
1065            FileOpKind::DidDelete => ops.did_delete.as_ref(),
1066        }
1067    }
1068
1069    /// 4.4.j: diagnostic provider's optional `identifier` --
1070    /// the namespace under which the server groups its
1071    /// diagnostics. Passed back in `DocumentDiagnosticParams`
1072    /// so a server that hosts multiple diagnostic streams
1073    /// (e.g. lint + typecheck on the same wire) can route the
1074    /// request to the right computation.
1075    pub fn diagnostic_identifier(&self) -> Option<String> {
1076        let p = self.server.diagnostic_provider.as_ref()?;
1077        match p {
1078            lsp_types::DiagnosticServerCapabilities::Options(o) => o.identifier.clone(),
1079            lsp_types::DiagnosticServerCapabilities::RegistrationOptions(r) => {
1080                r.diagnostic_options.identifier.clone()
1081            }
1082        }
1083    }
1084
1085    /// Whether the server wants `textDocument/willSave`
1086    /// notifications. Reads `text_document_sync.will_save`
1087    /// (Phase 4.3).
1088    pub fn wants_will_save(&self) -> bool {
1089        self.sync_options()
1090            .and_then(|o| o.will_save)
1091            .unwrap_or(false)
1092    }
1093
1094    /// Whether the server wants `textDocument/willSaveWaitUntil`
1095    /// requests. Used for format-on-save when true.
1096    pub fn wants_will_save_wait_until(&self) -> bool {
1097        self.sync_options()
1098            .and_then(|o| o.will_save_wait_until)
1099            .unwrap_or(false)
1100    }
1101
1102    /// Whether the server wants `textDocument/didSave`
1103    /// notifications. True when the server advertises any
1104    /// save options (LSP spec: "if save is set, send didSave").
1105    pub fn wants_did_save(&self) -> bool {
1106        self.sync_options()
1107            .map(|o| o.save.is_some())
1108            .unwrap_or(false)
1109    }
1110
1111    /// Whether didSave should include the post-save text.
1112    /// Reads `text_document_sync.save.include_text`.
1113    pub fn did_save_include_text(&self) -> bool {
1114        self.text_document_save_options()
1115            .and_then(|s| s.include_text)
1116            .unwrap_or(false)
1117    }
1118
1119    /// Pull the negotiated `TextDocumentSyncOptions` if the
1120    /// server advertised the options shape. Returns `None` for
1121    /// the legacy `Kind(bool)` shape.
1122    fn sync_options(&self) -> Option<&lsp_types::TextDocumentSyncOptions> {
1123        match self.server.text_document_sync.as_ref()? {
1124            lsp_types::TextDocumentSyncCapability::Options(o) => Some(o),
1125            _ => None,
1126        }
1127    }
1128
1129    /// Pull the negotiated save-options shape (the modern
1130    /// `SaveOptions` struct, not the legacy bool). Some servers
1131    /// emit `Save(bool)` -- treat that as "default options" by
1132    /// returning None here; callers fall back via
1133    /// `wants_did_save`.
1134    fn text_document_save_options(&self) -> Option<&lsp_types::SaveOptions> {
1135        let opts = self.sync_options()?;
1136        match opts.save.as_ref()? {
1137            lsp_types::TextDocumentSyncSaveOptions::SaveOptions(s) => Some(s),
1138            lsp_types::TextDocumentSyncSaveOptions::Supported(_) => None,
1139        }
1140    }
1141
1142    /// Trigger characters that should fire `textDocument/signatureHelp`
1143    /// in Insert mode. Empty when the server doesn't advertise the
1144    /// provider or doesn't list any.
1145    pub fn signature_help_trigger_chars(&self) -> Vec<char> {
1146        self.server
1147            .signature_help_provider
1148            .as_ref()
1149            .and_then(|p| p.trigger_characters.as_ref())
1150            .map(|v| v.iter().filter_map(|s| s.chars().next()).collect())
1151            .unwrap_or_default()
1152    }
1153
1154    /// Server's text-document sync mode -- determines whether we
1155    /// send incremental or full content on `didChange`. None is
1156    /// the LSP signal that the server doesn't want sync at all.
1157    pub fn text_document_sync_kind(&self) -> Option<lsp_types::TextDocumentSyncKind> {
1158        match self.server.text_document_sync.as_ref()? {
1159            lsp_types::TextDocumentSyncCapability::Kind(k) => Some(*k),
1160            lsp_types::TextDocumentSyncCapability::Options(o) => o.change,
1161        }
1162    }
1163}
1164
1165#[cfg(test)]
1166mod tests {
1167    use super::*;
1168
1169    #[test]
1170    fn client_capabilities_advertise_minimum_4_1_set() {
1171        let caps = client_capabilities();
1172        // general
1173        let g = caps.general.unwrap();
1174        let encs = g.position_encodings.unwrap();
1175        assert!(encs.contains(&PositionEncodingKind::UTF8));
1176        assert!(encs.contains(&PositionEncodingKind::UTF16));
1177        assert!(g.stale_request_support.unwrap().cancel);
1178        // workspace
1179        let w = caps.workspace.unwrap();
1180        assert_eq!(w.apply_edit, Some(true));
1181        assert_eq!(w.configuration, Some(true));
1182        assert_eq!(w.workspace_folders, Some(true));
1183        // textDocument
1184        let td = caps.text_document.unwrap();
1185        assert!(td.synchronization.is_some());
1186        assert!(td.publish_diagnostics.is_some());
1187    }
1188
1189    #[test]
1190    fn client_advertises_markdown_for_hover() {
1191        // rust-analyzer (and most servers) downgrade to plaintext
1192        // unless the client opts into Markdown via
1193        // `textDocument.hover.contentFormat`. Without this advert
1194        // hover popups arrive as flat plaintext and the markdown
1195        // grammar has no patterns to colour.
1196        let caps = client_capabilities();
1197        let td = caps.text_document.unwrap();
1198        let hover = td.hover.expect("hover capability advertised");
1199        let formats = hover.content_format.expect("content_format advertised");
1200        assert!(
1201            formats.contains(&MarkupKind::Markdown),
1202            "expected Markdown in hover content_format, got {:?}",
1203            formats
1204        );
1205    }
1206
1207    #[test]
1208    fn client_advertises_markdown_for_signature_help() {
1209        let caps = client_capabilities();
1210        let td = caps.text_document.unwrap();
1211        let sig = td.signature_help.expect("signature_help advertised");
1212        let info = sig
1213            .signature_information
1214            .expect("signature info advertised");
1215        let formats = info
1216            .documentation_format
1217            .expect("documentation_format advertised");
1218        assert!(
1219            formats.contains(&MarkupKind::Markdown),
1220            "expected Markdown in signatureHelp documentation_format, got {:?}",
1221            formats
1222        );
1223    }
1224
1225    #[test]
1226    fn capabilities_picks_utf8_when_server_advertises_it() {
1227        let server = ServerCapabilities {
1228            position_encoding: Some(PositionEncodingKind::UTF8),
1229            ..Default::default()
1230        };
1231        let caps = Capabilities::from_initialize(client_capabilities(), server);
1232        assert!(caps.is_utf8());
1233    }
1234
1235    #[test]
1236    fn capabilities_falls_back_to_utf16_when_server_silent() {
1237        // Older servers don't advertise position_encoding at all;
1238        // LSP 3.16 said utf-16 by default.
1239        let server = ServerCapabilities::default();
1240        let caps = Capabilities::from_initialize(client_capabilities(), server);
1241        assert!(!caps.is_utf8());
1242        assert_eq!(caps.position_encoding, PositionEncodingKind::UTF16);
1243    }
1244
1245    #[test]
1246    fn capabilities_text_document_sync_kind_extraction() {
1247        // Kind variant.
1248        let server = ServerCapabilities {
1249            text_document_sync: Some(lsp_types::TextDocumentSyncCapability::Kind(
1250                lsp_types::TextDocumentSyncKind::INCREMENTAL,
1251            )),
1252            ..Default::default()
1253        };
1254        let caps = Capabilities::from_initialize(client_capabilities(), server);
1255        assert_eq!(
1256            caps.text_document_sync_kind(),
1257            Some(lsp_types::TextDocumentSyncKind::INCREMENTAL)
1258        );
1259
1260        // Options variant.
1261        let server = ServerCapabilities {
1262            text_document_sync: Some(lsp_types::TextDocumentSyncCapability::Options(
1263                lsp_types::TextDocumentSyncOptions {
1264                    open_close: Some(true),
1265                    change: Some(lsp_types::TextDocumentSyncKind::FULL),
1266                    ..Default::default()
1267                },
1268            )),
1269            ..Default::default()
1270        };
1271        let caps = Capabilities::from_initialize(client_capabilities(), server);
1272        assert_eq!(
1273            caps.text_document_sync_kind(),
1274            Some(lsp_types::TextDocumentSyncKind::FULL)
1275        );
1276    }
1277
1278    /// 4.4.j: when the server doesn't advertise a
1279    /// `diagnostic_provider`, `supports_pull_diagnostics` is
1280    /// false.
1281    #[test]
1282    fn pull_diagnostics_off_when_provider_absent() {
1283        let server = ServerCapabilities::default();
1284        let caps = Capabilities::from_initialize(client_capabilities(), server);
1285        assert!(!caps.supports_pull_diagnostics());
1286        assert!(!caps.supports_workspace_diagnostic_pull());
1287        assert!(caps.diagnostic_identifier().is_none());
1288    }
1289
1290    /// 4.4.j: with `DiagnosticOptions { workspace_diagnostics
1291    /// = true, identifier = Some(..) }` the per-document
1292    /// probe AND the workspace probe both light up, and the
1293    /// identifier round-trips.
1294    #[test]
1295    fn pull_diagnostics_options_variant_reads_workspace_and_identifier() {
1296        let server = ServerCapabilities {
1297            diagnostic_provider: Some(lsp_types::DiagnosticServerCapabilities::Options(
1298                lsp_types::DiagnosticOptions {
1299                    identifier: Some("rust-analyzer".into()),
1300                    inter_file_dependencies: false,
1301                    workspace_diagnostics: true,
1302                    work_done_progress_options: Default::default(),
1303                },
1304            )),
1305            ..Default::default()
1306        };
1307        let caps = Capabilities::from_initialize(client_capabilities(), server);
1308        assert!(caps.supports_pull_diagnostics());
1309        assert!(caps.supports_workspace_diagnostic_pull());
1310        assert_eq!(
1311            caps.diagnostic_identifier().as_deref(),
1312            Some("rust-analyzer"),
1313        );
1314    }
1315
1316    /// 4.4.j: client advertises pull-diagnostic capabilities
1317    /// on both the text-document and workspace sides so
1318    /// servers know we honour pull + refresh.
1319    #[test]
1320    fn client_advertises_pull_diagnostic_capabilities() {
1321        let caps = client_capabilities();
1322        let td = caps.text_document.expect("text_document advertised");
1323        let diag = td.diagnostic.expect("td.diagnostic advertised");
1324        assert_eq!(diag.related_document_support, Some(true));
1325        let ws = caps.workspace.expect("workspace advertised");
1326        let ws_diag = ws.diagnostic.expect("ws.diagnostic advertised");
1327        assert_eq!(ws_diag.refresh_support, Some(true));
1328    }
1329
1330    /// 4.4.m: client advertises all six workspace file-
1331    /// operation hooks so servers know they can fire any of
1332    /// them. Without these, servers default to "no file-op
1333    /// hooks" and won't even attempt to register filters.
1334    #[test]
1335    fn client_advertises_all_file_operation_hooks() {
1336        let caps = client_capabilities();
1337        let ws = caps.workspace.expect("workspace advertised");
1338        let ops = ws.file_operations.expect("file_operations advertised");
1339        assert_eq!(ops.did_create, Some(true));
1340        assert_eq!(ops.will_create, Some(true));
1341        assert_eq!(ops.did_rename, Some(true));
1342        assert_eq!(ops.will_rename, Some(true));
1343        assert_eq!(ops.did_delete, Some(true));
1344        assert_eq!(ops.will_delete, Some(true));
1345    }
1346
1347    /// 4.4.m: probes reflect the server's per-kind advertisement.
1348    /// A server registering `did_create` lights up that probe
1349    /// only; other kinds stay false.
1350    #[test]
1351    fn file_operation_probes_consult_per_kind_server_field() {
1352        let server = ServerCapabilities {
1353            workspace: Some(lsp_types::WorkspaceServerCapabilities {
1354                workspace_folders: None,
1355                file_operations: Some(lsp_types::WorkspaceFileOperationsServerCapabilities {
1356                    did_create: Some(lsp_types::FileOperationRegistrationOptions {
1357                        filters: vec![lsp_types::FileOperationFilter {
1358                            scheme: Some("file".into()),
1359                            pattern: lsp_types::FileOperationPattern {
1360                                glob: "**/*.rs".into(),
1361                                matches: None,
1362                                options: None,
1363                            },
1364                        }],
1365                    }),
1366                    ..Default::default()
1367                }),
1368            }),
1369            ..Default::default()
1370        };
1371        let caps = Capabilities::from_initialize(client_capabilities(), server);
1372        assert!(caps.supports_did_create_files());
1373        assert!(!caps.supports_will_create_files());
1374        assert!(!caps.supports_did_rename_files());
1375        assert!(!caps.supports_will_rename_files());
1376        assert!(!caps.supports_did_delete_files());
1377        assert!(!caps.supports_will_delete_files());
1378        // The options are reachable so the host pump can read
1379        // the filter list when it lands.
1380        let opts = caps
1381            .file_operations_options(FileOpKind::DidCreate)
1382            .expect("server advertised did_create options");
1383        assert_eq!(opts.filters[0].pattern.glob, "**/*.rs");
1384    }
1385}