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}