Skip to main content

lattice_lsp/
cache.rs

1//! Per-buffer LSP response caches + outcome enums.
2//!
3//! Phase 5.2: extracted from `lattice-ui-tui::app` to their proper
4//! home in `lattice-lsp`. Every type here is renderer-agnostic
5//! pure data describing the *result* of an LSP request -- the
6//! shape the App's per-tick drain expects after the spawned task
7//! delivers its response.
8//!
9//! Cache types are keyed on `(BufferId, document_version)` so the
10//! pump can invalidate-by-version when the buffer mutates.
11//! Outcome enums carry the same key plus the actual payload (or
12//! a sentinel variant for `Empty` / `NoProvider` / etc.) so the
13//! drain can route to the appropriate echo / apply path.
14//!
15//! `lattice-ui-tui::app` re-exports every type from this module
16//! so existing `crate::app::HoverOutcome` etc. references in the
17//! host crate's source continue to resolve unchanged.
18
19use std::sync::Arc;
20
21use lattice_core::{BufferId, Fold};
22use lattice_protocol::position::Position;
23
24use crate::Uri;
25
26// ---- Selection range ---------------------------------------------------
27
28/// 4.4.e: cached `textDocument/selectionRange` chain anchored
29/// at a specific buffer + cursor position. Flat `Vec<Range>`
30/// (innermost first) instead of the LSP linked-list shape so
31/// the operator can index into it in O(1). Captured once on
32/// the first `:lsp-expand-region` after a fresh cursor; reused
33/// across subsequent expand/shrink steps until the cursor
34/// moves outside `ranges[0]` (the innermost) or the buffer
35/// changes.
36#[derive(Debug, Clone)]
37pub struct LspSelectionChain {
38    pub buffer_id: BufferId,
39    pub anchor_cursor: Position,
40    pub ranges: Vec<lsp_types::Range>,
41}
42
43/// 4.4.e: outcome of an in-flight `textDocument/selectionRange`
44/// request. The drain consumes one of these per response and
45/// either seats the chain into `App::lsp_selection_chain` or
46/// surfaces an error echo. `pending_step` carries whether the
47/// triggering invocation was `:lsp-expand-region` or the first
48/// step of `:lsp-shrink-region` (the latter is rare -- shrink
49/// without an existing chain is a user error -- but the drain
50/// handles it uniformly).
51#[derive(Debug, Clone)]
52pub enum SelectionRangeOutcome {
53    /// Server returned a non-empty chain; `ranges[0]` is the
54    /// innermost. The drain stores this and applies the first
55    /// step at `index = 0` (then bumps for expand).
56    Items {
57        anchor_cursor: Position,
58        anchor_buffer: BufferId,
59        ranges: Vec<lsp_types::Range>,
60        pending_step: SelectionRangeStep,
61    },
62    NoProvider,
63    Empty,
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum SelectionRangeStep {
68    Expand,
69    Shrink,
70}
71
72// ---- Document highlight ------------------------------------------------
73
74/// 4.4.e: cached `textDocument/documentHighlight` result
75/// anchored at a specific buffer + cursor position. The
76/// renderer reads `highlights` to paint a soft overlay; the
77/// pump compares `cursor` to the live cursor to decide whether
78/// to invalidate and re-request.
79#[derive(Debug, Clone)]
80pub struct DocumentHighlightCache {
81    pub buffer_id: BufferId,
82    pub cursor: Position,
83    pub highlights: Vec<lsp_types::DocumentHighlight>,
84}
85
86/// 4.4.e: in-flight `documentHighlight` request outcome.
87#[derive(Debug, Clone)]
88pub enum DocumentHighlightOutcome {
89    Items {
90        buffer_id: BufferId,
91        cursor: Position,
92        highlights: Vec<lsp_types::DocumentHighlight>,
93    },
94    /// Server returned null (cursor not on a known symbol);
95    /// the pump clears the cache so we don't paint stale
96    /// highlights.
97    Empty { buffer_id: BufferId },
98}
99
100// ---- Folding range -----------------------------------------------------
101
102/// 4.4.f: cached `textDocument/foldingRange` response for one
103/// buffer. `document_version` is the buffer's
104/// [`lattice_core::Document`] version at the time the request
105/// was issued; the pump compares it against the live version
106/// to decide whether to refresh.
107#[derive(Debug, Clone)]
108pub struct LspFoldsCache {
109    pub document_version: u64,
110    pub folds: Vec<Fold>,
111}
112
113/// 4.4.f: in-flight `foldingRange` request outcome.
114#[derive(Debug, Clone)]
115pub enum FoldingRangeOutcome {
116    Items {
117        buffer_id: BufferId,
118        document_version: u64,
119        folds: Vec<Fold>,
120    },
121    Empty {
122        buffer_id: BufferId,
123        document_version: u64,
124    },
125}
126
127// ---- Inlay hint --------------------------------------------------------
128
129/// 4.4.g: cached `textDocument/inlayHint` response for one
130/// buffer. Keyed on `(BufferId, document_version)`; pump
131/// invalidates when the version changes. `hints` are sorted
132/// by position so the renderer can stop scanning once it
133/// walks past the current line.
134///
135/// 4.4.g viewport polish: `requested_first_line` /
136/// `requested_last_line` (inclusive, 0-based LSP line indices)
137/// record the line range that produced this cache entry. The
138/// pump refetches when the visible viewport scrolls outside
139/// this window plus an overscan margin -- so small scrolls
140/// stay cached, larger ones trigger a new request bounded to
141/// the new viewport. Small files where viewport + overscan
142/// covers the whole buffer cache exactly once and never
143/// refetch on scroll.
144#[derive(Debug, Clone)]
145pub struct LspInlayHintCache {
146    pub document_version: u64,
147    pub hints: Vec<lsp_types::InlayHint>,
148    pub requested_first_line: u32,
149    pub requested_last_line: u32,
150}
151
152/// 4.4.g: in-flight `inlayHint` request outcome. The
153/// `requested_*_line` pair rides through so the drain can seat
154/// the cache with the range that actually produced these
155/// hints (matters for the viewport pump -- subsequent scrolls
156/// reuse the cache only while the viewport sits inside that
157/// range).
158#[derive(Debug, Clone)]
159pub enum InlayHintOutcome {
160    Items {
161        buffer_id: BufferId,
162        document_version: u64,
163        hints: Vec<lsp_types::InlayHint>,
164        requested_first_line: u32,
165        requested_last_line: u32,
166    },
167    Empty {
168        buffer_id: BufferId,
169        document_version: u64,
170        requested_first_line: u32,
171        requested_last_line: u32,
172    },
173}
174
175// ---- Document link -----------------------------------------------------
176
177/// 4.5.c: per-buffer cache of `textDocument/documentLink`
178/// responses. Filled by the pump on document-version change;
179/// `gx` walks the entries looking for the first link whose
180/// range covers the cursor and follows it. Cache invalidates
181/// when the version changes.
182#[derive(Debug, Clone)]
183pub struct LspDocumentLinksCache {
184    pub document_version: u64,
185    pub links: Vec<lsp_types::DocumentLink>,
186}
187
188/// 4.5.c: outcome of an in-flight `textDocument/documentLink`
189/// request. `Empty` for server responses that returned no
190/// links (still updates the cache so we don't keep
191/// re-issuing for the same version).
192#[derive(Debug, Clone)]
193pub enum DocumentLinksOutcome {
194    Items {
195        buffer_id: BufferId,
196        document_version: u64,
197        links: Vec<lsp_types::DocumentLink>,
198    },
199    Empty {
200        buffer_id: BufferId,
201        document_version: u64,
202    },
203}
204
205// ---- Code lens ---------------------------------------------------------
206
207/// 4.5.d: per-buffer cache of `textDocument/codeLens` results.
208/// Filled by the pump on doc-version change or after
209/// `workspace/codeLens/refresh` evicts the entry. The
210/// `:lsp-code-lens` picker reads the cache; accept routes the
211/// chosen lens's `command` through `workspace/executeCommand`.
212#[derive(Debug, Clone)]
213pub struct LspCodeLensCache {
214    pub document_version: u64,
215    pub lenses: Vec<lsp_types::CodeLens>,
216    /// Which server produced the cached batch. Code-lens
217    /// `command` payloads are server-specific; routing the
218    /// `executeCommand` to the originating server keeps the
219    /// dispatch unambiguous when multiple servers attach.
220    pub server_id: Arc<str>,
221}
222
223/// 4.5.d: outcome of an in-flight `textDocument/codeLens`
224/// request. Carries the server id so the eviction-on-refresh
225/// path can match by server.
226#[derive(Debug, Clone)]
227pub enum CodeLensOutcome {
228    Items {
229        buffer_id: BufferId,
230        document_version: u64,
231        server_id: Arc<str>,
232        lenses: Vec<lsp_types::CodeLens>,
233    },
234    Empty {
235        buffer_id: BufferId,
236        document_version: u64,
237    },
238}
239
240// ---- Document color ----------------------------------------------------
241
242/// 4.5.e: per-buffer cache of color literals + their resolved
243/// values. Filled by the per-tick pump on document-version
244/// change; consumed by `:lsp-color-presentation`. Renderer
245/// swatch overlay queued -- today the cache only feeds the
246/// picker.
247#[derive(Debug, Clone)]
248pub struct LspDocumentColorCache {
249    pub document_version: u64,
250    pub colors: Vec<lsp_types::ColorInformation>,
251    /// Server that produced the cache. Routing `colorPresentation`
252    /// back to the same server keeps the alternatives consistent
253    /// (a CSS server's `rgb()` formats aren't useful for a Rust
254    /// `rgb!` macro).
255    pub server_id: Arc<str>,
256}
257
258/// 4.5.e: outcome of an in-flight `documentColor` request.
259#[derive(Debug, Clone)]
260pub enum DocumentColorOutcome {
261    Items {
262        buffer_id: BufferId,
263        document_version: u64,
264        server_id: Arc<str>,
265        colors: Vec<lsp_types::ColorInformation>,
266    },
267    Empty {
268        buffer_id: BufferId,
269        document_version: u64,
270    },
271}
272
273// ---- Semantic tokens ---------------------------------------------------
274
275/// 4.4.h: one decoded LSP semantic token, expanded from the
276/// server's relative-position varint encoding into absolute
277/// positions. `token_type` is the canonical name from the
278/// server's legend (e.g. `"keyword"`, `"function"`) so the
279/// renderer can pick a style without looking up the index.
280/// `length` is in utf-16 code units per the LSP spec.
281#[derive(Debug, Clone)]
282pub struct DecodedSemanticToken {
283    pub line: u32,
284    pub start_char: u32,
285    pub length: u32,
286    pub token_type: String,
287    /// Names of every modifier bit set on this token (legend
288    /// resolved). Empty when the server set no modifier bits.
289    pub modifiers: Vec<String>,
290}
291
292/// 4.4.h: cached decoded `textDocument/semanticTokens/full`
293/// response for one buffer. Same shape as the other LSP
294/// per-buffer caches: keyed on `(BufferId, document_version)`,
295/// invalidated by the pump when the version changes.
296///
297/// `result_id` is the server-issued tag the host sends back
298/// on the next `full/delta` request so the server knows which
299/// baseline to diff against (4.4.i).
300///
301/// `raw_data` (4.4.i) keeps the un-decoded `Vec<SemanticToken>`
302/// alongside the decoded view so delta edits can splice into
303/// it before the host re-decodes for the renderer.
304#[derive(Debug, Clone)]
305pub struct LspSemanticTokensCache {
306    pub document_version: u64,
307    pub result_id: Option<String>,
308    pub raw_data: Vec<lsp_types::SemanticToken>,
309    pub tokens: Vec<DecodedSemanticToken>,
310}
311
312/// 4.4.h: in-flight `semanticTokens/full` request outcome.
313/// 4.4.i extends with the `Delta` variant for the
314/// `full/delta` path.
315#[derive(Debug, Clone)]
316pub enum SemanticTokensOutcome {
317    Items {
318        buffer_id: BufferId,
319        document_version: u64,
320        result_id: Option<String>,
321        raw_data: Vec<lsp_types::SemanticToken>,
322        tokens: Vec<DecodedSemanticToken>,
323    },
324    /// 4.4.i: server returned a delta against `previous_result_id`.
325    /// The drain looks up the previous cache, splices the edits
326    /// into `raw_data`, and re-decodes using the legend captured
327    /// at request time.
328    Delta {
329        buffer_id: BufferId,
330        document_version: u64,
331        previous_result_id: String,
332        new_result_id: Option<String>,
333        edits: Vec<lsp_types::SemanticTokensEdit>,
334        token_types: Vec<lsp_types::SemanticTokenType>,
335        token_modifiers: Vec<lsp_types::SemanticTokenModifier>,
336    },
337    Empty {
338        buffer_id: BufferId,
339        document_version: u64,
340    },
341}
342
343/// 4.4.i: apply a server-issued `SemanticTokensEdit` script
344/// to `raw_data` in place. Each edit specifies a start index
345/// (into the previous token vec), a count to delete, and a
346/// replacement slice. Edits are applied in order; the server
347/// constructs them against the index space of the input vec.
348///
349/// Returns `Err(())` and leaves `raw_data` untouched when an
350/// edit references a range outside the current vec (defensive
351/// guard against server bugs; the host falls back to a fresh
352/// full request when this fires).
353pub fn apply_semantic_token_edits(
354    raw_data: &mut Vec<lsp_types::SemanticToken>,
355    edits: &[lsp_types::SemanticTokensEdit],
356) -> Result<(), ()> {
357    for edit in edits {
358        let start = edit.start as usize;
359        let delete_count = edit.delete_count as usize;
360        let end = start.checked_add(delete_count).ok_or(())?;
361        if end > raw_data.len() {
362            return Err(());
363        }
364        let replacement = edit.data.clone().unwrap_or_default();
365        raw_data.splice(start..end, replacement);
366    }
367    Ok(())
368}
369
370/// 4.4.h: decode the LSP semantic-tokens stream into
371/// absolute-position tokens. Format per LSP §3.17.6: each
372/// token's `delta_line` is relative to the previous token's
373/// line; `delta_start` is relative to the previous token's
374/// start when on the same line, otherwise relative to column
375/// 0. `token_types` / `token_modifiers` are the server's
376/// legend slices -- indexes outside the legend are dropped
377/// (defense-in-depth; real servers don't emit out-of-range).
378pub fn decode_semantic_tokens(
379    data: &[lsp_types::SemanticToken],
380    token_types: &[lsp_types::SemanticTokenType],
381    token_modifiers: &[lsp_types::SemanticTokenModifier],
382) -> Vec<DecodedSemanticToken> {
383    let mut out = Vec::with_capacity(data.len());
384    let mut cur_line: u32 = 0;
385    let mut cur_start: u32 = 0;
386    for tok in data {
387        if tok.delta_line == 0 {
388            cur_start += tok.delta_start;
389        } else {
390            cur_line += tok.delta_line;
391            cur_start = tok.delta_start;
392        }
393        let Some(token_type) = token_types.get(tok.token_type as usize) else {
394            continue;
395        };
396        let mut modifiers: Vec<String> = Vec::new();
397        for (i, m) in token_modifiers.iter().enumerate() {
398            if (tok.token_modifiers_bitset >> i) & 1 == 1 {
399                modifiers.push(m.as_str().to_string());
400            }
401        }
402        out.push(DecodedSemanticToken {
403            line: cur_line,
404            start_char: cur_start,
405            length: tok.length,
406            token_type: token_type.as_str().to_string(),
407            modifiers,
408        });
409    }
410    out
411}
412
413// ---- Pull diagnostics --------------------------------------------------
414
415/// 4.4.j: cached `textDocument/diagnostic` state per buffer.
416/// `result_id` is what the server issued on the previous
417/// response; threading it back in `previous_result_id` lets
418/// the server answer `Unchanged` when nothing moved.
419#[derive(Debug, Clone, Default)]
420pub struct LspPullDiagnosticsCache {
421    pub document_version: u64,
422    pub result_id: Option<String>,
423}
424
425/// 4.4.j: in-flight `textDocument/diagnostic` request outcome.
426/// `Full` means "here are the diagnostics" (apply to the
427/// layer); `Unchanged` means "nothing moved since the
428/// previous `result_id`" (no-op on the layer, just refresh
429/// the cache's version). `Empty` is the "no server / cancelled
430/// / error" path -- still seats a cache entry with the
431/// current version so the pump doesn't re-fire on the next
432/// tick without an actual edit.
433#[derive(Debug, Clone)]
434pub enum PullDiagnosticsOutcome {
435    Full {
436        buffer_id: BufferId,
437        server_id: Arc<str>,
438        uri: Uri,
439        document_version: u64,
440        result_id: Option<String>,
441        diagnostics: Vec<lsp_types::Diagnostic>,
442    },
443    Unchanged {
444        buffer_id: BufferId,
445        document_version: u64,
446        result_id: String,
447    },
448    Empty {
449        buffer_id: BufferId,
450        document_version: u64,
451    },
452}
453
454// ---- Hover -------------------------------------------------------------
455
456/// Result of a `K` (LSP hover) request, sent from the spawned
457/// task to the App's main thread. Carrying the no-result
458/// variants explicitly (instead of just dropping the channel
459/// send) lets the drain echo a clear message so the user
460/// always gets feedback on `K`.
461#[derive(Debug, Clone)]
462pub enum HoverOutcome {
463    /// Markdown body to feed into the popup. First non-empty
464    /// wins across attached servers.
465    Body(String),
466    /// Walked every attached server; each returned an empty /
467    /// missing hover.
468    NoBody { servers_tried: usize },
469    /// The buffer's URI maps to no attached servers.
470    NoServers,
471}
472
473// ---- References --------------------------------------------------------
474
475/// Result of a `gr` (LSP references) request. Carries the
476/// symbol-under-cursor verbatim so the rendered help buffer's
477/// title reads `References for "foo"` and the user has
478/// confirmation of what they searched for.
479#[derive(Debug, Clone)]
480pub enum ReferencesOutcome {
481    /// Merged + deduped reference list across attached
482    /// servers. May be empty (Found(symbol, [])) when servers
483    /// know about the symbol but it has no other call sites.
484    Found {
485        symbol: String,
486        locations: Vec<lsp_types::Location>,
487    },
488    /// The buffer's URI maps to no attached servers.
489    NoServers,
490}
491
492// ---- Completion (LSP-specific) -----------------------------------------
493
494/// Drain payload for `completionItem/resolve` (Phase
495/// 4.2.g.3). CSM.8b.5: `meta_index` is now the index of the
496/// fired candidate within state.raw's LSP-row sequence; the
497/// drain decodes that row's payload, applies the resolved
498/// fields, re-encodes in place. Multiple resolves in flight
499/// (selection change → cancel prior → fire new) cancel via
500/// the supplied token.
501#[derive(Debug, Clone)]
502pub struct CompletionResolveOutcome {
503    pub meta_index: usize,
504    pub resolved: lsp_types::CompletionItem,
505}
506
507// OR.7: `AsyncCompletionOutcome` moved to
508// `lattice_completion::AsyncCompletionOutcome`. It never was LSP's — it
509// is the drain payload for the whole async fan-out, and living here
510// under an LSP name is part of why the WASM completion seam sat
511// undriven. See `lattice-completion/src/source.rs`.
512
513/// One row of an LSP completion picker. Carries the item
514/// label, kind glyph, optional detail blurb, and the insert
515/// text. `replace_range` is the byte range in the active line
516/// to splice the insert text over.
517#[derive(Debug, Clone)]
518pub struct CompletionItemRow {
519    pub label: String,
520    pub kind_glyph: &'static str,
521    pub detail: Option<String>,
522    /// Text to insert (raw -- no snippet expansion yet).
523    pub insert_text: String,
524    /// Replace range as `(start_byte, end_byte)` on the active
525    /// line.
526    pub replace_range: (u32, u32),
527    /// Line the replace range is on (LSP 0-based). Always the
528    /// cursor's line; carried explicitly to keep the accept
529    /// path independent of cursor mutations.
530    pub line: u32,
531}
532
533/// Outcome of a `textDocument/completion` request.
534#[derive(Debug, Clone)]
535pub enum CompletionOutcome {
536    Items(Vec<CompletionItemRow>),
537    NoServers,
538}
539
540// ---- Code action -------------------------------------------------------
541
542/// One row of a code-action picker. Carries the action title,
543/// kind glyph, and the original `CodeAction` payload (or its
544/// raw `Command`-only form). Action items survive on the App
545/// across the request → picker accept gap so the resolve /
546/// apply path can read them by index.
547#[derive(Debug, Clone)]
548pub struct CodeActionRow {
549    pub title: String,
550    pub kind_glyph: &'static str,
551    pub action: lsp_types::CodeActionOrCommand,
552}
553
554/// Outcome of a `:code-actions` request. Drained per frame.
555#[derive(Debug, Clone)]
556pub enum CodeActionOutcome {
557    /// Fresh code-action result list. Drain opens a picker.
558    Items(Vec<CodeActionRow>),
559    /// Resolved action (post-`codeAction/resolve`). Drain
560    /// applies directly -- the picker is already gone.
561    Resolved(lsp_types::CodeAction),
562    NoProvider,
563}
564
565// ---- Rename ------------------------------------------------------------
566
567/// Outcome of a `:rename` request. The success arm
568/// pre-flattens the WorkspaceEdit into a per-file
569/// `Vec<TextEdit>` map so the App-side apply path doesn't have
570/// to walk lsp-types' enum shapes. `NoProvider` echoes when no
571/// attached server advertises `renameProvider`; `NotRenameable`
572/// when prepareRename refused; `Empty` when the rename
573/// succeeded but the server returned no edits.
574#[derive(Debug, Clone)]
575pub enum RenameOutcome {
576    Edits {
577        /// Per-file edits keyed by URI string. Each Vec is
578        /// already in the order the server returned (the apply
579        /// path reverse-sorts before applying, same as
580        /// formatting).
581        per_file: Vec<(lsp_types::Uri, Vec<lsp_types::TextEdit>)>,
582        new_name: String,
583    },
584    NoProvider,
585    NotRenameable {
586        reason: String,
587    },
588    Empty,
589}
590
591// ---- Signature help ----------------------------------------------------
592
593/// Outcome of a `textDocument/signatureHelp` request. The
594/// response carries multiple signatures (one per overload)
595/// plus the active signature/parameter indices. We collapse
596/// to the active overload + parameter highlight for the popup
597/// body.
598#[derive(Debug, Clone)]
599pub enum SignatureHelpOutcome {
600    /// Pre-rendered markdown body for the popup. Empty body
601    /// means "no signature info" (server returned None or an
602    /// empty `signatures` array).
603    Body(String),
604    /// No server attached / no provider advertised.
605    NoServers,
606}
607
608// ---- Format ------------------------------------------------------------
609
610/// Outcome of a `:format` / `:format-range` request. Drained
611/// per frame; the App applies the edits as one undo unit or
612/// echoes the appropriate failure / no-op state.
613#[derive(Debug, Clone)]
614pub enum FormatOutcome {
615    /// Server returned a (possibly empty) edit list. Empty ==
616    /// no changes needed; non-empty == apply.
617    Edits(Vec<lsp_types::TextEdit>),
618    /// No attached server advertises the relevant formatting
619    /// provider (`is_range` distinguishes whole-buffer from
620    /// range-format providers since they're separate caps).
621    NoProvider { is_range: bool },
622}
623
624// ---- Symbols -----------------------------------------------------------
625
626/// One row of a document-symbol / workspace-symbol picker.
627/// Carries the symbol's name, kind, depth (for in-document
628/// hierarchy indent), and the location to jump to. Built
629/// host-side from the LSP `DocumentSymbolResponse` /
630/// `Vec<SymbolInformation>` so the picker doesn't depend on
631/// lsp-types.
632#[derive(Debug, Clone)]
633pub struct SymbolRow {
634    pub name: String,
635    pub kind_glyph: &'static str,
636    pub container: Option<String>,
637    /// Indent depth (0 = top-level). Document-symbol responses
638    /// nest; workspace-symbol responses are flat (depth = 0).
639    pub depth: u32,
640    pub path: std::path::PathBuf,
641    /// LSP 0-based line.
642    pub line: u32,
643    /// utf-8 byte column.
644    pub col: u32,
645}
646
647/// Outcome of a document-symbol / workspace-symbol request --
648/// drained per frame and either opens a picker or echoes.
649#[derive(Debug, Clone)]
650pub enum SymbolsOutcome {
651    Found { title: String, rows: Vec<SymbolRow> },
652    NoServers,
653}
654
655// ---- Nav kind ----------------------------------------------------------
656
657/// Which navigation request flavour produced an in-flight nav
658/// response (Phase 4.2.c). All four share the same dispatch
659/// shape (per-server `Vec<Location>` merge + dedup + jump-or-
660/// list) -- the kind only changes the LSP method called and
661/// the user-facing "no X found" echo.
662#[derive(Debug, Clone, Copy, PartialEq, Eq)]
663pub enum LspNavKind {
664    Definition,
665    Declaration,
666    TypeDefinition,
667    Implementation,
668}
669
670impl LspNavKind {
671    /// Verb used in echoes ("no definitions found",
672    /// "3 implementations; jumping to first", etc.).
673    pub fn noun_plural(self) -> &'static str {
674        match self {
675            Self::Definition => "definitions",
676            Self::Declaration => "declarations",
677            Self::TypeDefinition => "type definitions",
678            Self::Implementation => "implementations",
679        }
680    }
681
682    /// Single-word noun used in error contexts ("definition
683    /// target uri is not a file", etc.).
684    pub fn noun_singular(self) -> &'static str {
685        match self {
686            Self::Definition => "definition",
687            Self::Declaration => "declaration",
688            Self::TypeDefinition => "type definition",
689            Self::Implementation => "implementation",
690        }
691    }
692}