Skip to main content

lattice_completion/
traits.rs

1//! The four pluggable traits the pipeline composes.
2//!
3//! Each is its own extension point. Plugins (post-WASM) register
4//! impls against the [`crate::CompletionRegistry`] just like they
5//! register commands or keymaps. Default impls ship as built-ins
6//! (`crate::builtins::*`).
7
8use std::ops::Range;
9
10use crate::candidate::{CacheKey, MatchScore, RawCandidate, RenderedCandidate, ScoredCandidate};
11use lattice_core::Buffer;
12use lattice_grammar::CommandRegistry;
13
14/// Snapshot of editor state a generator may need to consult. Held
15/// by reference so the pipeline borrows from the surrounding
16/// frame for the duration of the call. Generators that don't need
17/// buffer or registry state simply ignore those fields.
18///
19/// `buffer` is the raw rope-backed text the cmdline cursor sits
20/// over. We pass `&Buffer` rather than the full `Document` so
21/// completion stays decoupled from the actor crate
22/// (`lattice-runtime`); generators that need richer document
23/// state will get a `&DocumentSnapshot` once a consumer requires
24/// one.
25pub struct GenerateContext<'a> {
26    /// Partial text in the current slot, before the cursor. The
27    /// matcher gets the same string as `query` -- generators may
28    /// also use it (e.g. `gen:files` resolves the directory by
29    /// splitting prefix at the last `/`).
30    pub prefix: &'a str,
31    pub buffer: &'a Buffer,
32    pub registry: &'a CommandRegistry,
33    /// Whether matching should be case-sensitive. Generators that
34    /// do their own filtering before returning candidates honor
35    /// this; pure "produce everything" generators ignore it.
36    pub case_sensitive: bool,
37}
38
39/// Produces raw candidates for a slot. Implementations vary from
40/// pure (`gen:commands` walks the registry) to side-effecting
41/// (`gen:files` reads the filesystem). Caching is opt-in via
42/// [`Self::cache_key`] -- the default is "no cache".
43pub trait CandidateGenerator: Send + Sync {
44    fn generate(&self, ctx: &GenerateContext<'_>) -> Vec<RawCandidate>;
45
46    /// Optional cache key. When two contexts produce the same
47    /// `Some(key)`, the pipeline serves the second from cache. The
48    /// query (prefix) is implicitly part of the key only if the
49    /// generator includes it -- many generators key on a registry
50    /// version (cache covers all queries until the registry
51    /// changes) and let the matcher do the per-query filtering.
52    fn cache_key(&self, _ctx: &GenerateContext<'_>) -> Option<CacheKey> {
53        None
54    }
55
56    /// Soft TTL for cache entries. After elapsing, the entry is
57    /// regenerated on next access. Defaults to no expiry.
58    fn cache_ttl(&self) -> std::time::Duration {
59        std::time::Duration::MAX
60    }
61}
62
63/// Matches and scores candidates against a query string. The
64/// default v1 matcher (`match:prefix`) returns `Some` only when the
65/// query is a prefix of `candidate.text`. The `fuzzy` matcher
66/// (orderless-equivalent) returns sub-character match ranges and a
67/// score that decays with skipped chars.
68pub trait CandidateMatcher: Send + Sync {
69    /// Score the candidate against `query`. `None` means "no match"
70    /// (filter out). The byte ranges record which parts of
71    /// `candidate.text` the matcher consumed -- the renderer paints
72    /// these with the match-face style.
73    fn matches(
74        &self,
75        query: &str,
76        candidate: &RawCandidate,
77    ) -> Option<(MatchScore, Vec<Range<usize>>)>;
78}
79
80/// Reorders the matched + scored candidate set. The default
81/// (`rank:score`) sorts by descending score; `rank:alphabetical`
82/// is an alternative. Plugin rankers can implement frecency,
83/// recency, smart-case ordering, etc.
84pub trait CandidateRanker: Send + Sync {
85    fn rank(&self, scored: &mut Vec<ScoredCandidate>);
86}
87
88/// Decorates a candidate with display metadata (right-side text in
89/// the popup -- marginalia's role). Multiple annotators are active
90/// simultaneously; they run in registration order, each appending
91/// a typed [`crate::Annotation`] to `candidate.annotations`. The
92/// renderer paints each annotation with the style its category
93/// resolves to and joins consecutive annotations with two spaces.
94///
95/// MARG.1 (2026-06-03): annotation payload is now typed
96/// (`Vec<Annotation>`, not `Vec<String>`). Annotators choose the
97/// variant that matches the semantic — `Annotation::Kind` for
98/// category labels, `Annotation::DocSnippet` for inline docs,
99/// `Annotation::Keybinding` for bound chords (MARG.2), etc. The
100/// `Custom { text, slot }` escape hatch covers extensions that
101/// don't fit a built-in variant.
102///
103/// Run order is registration order in v1. A future `priority: i32`
104/// field will allow plugins to slot themselves between built-in
105/// annotators when the registration-order constraint becomes
106/// limiting.
107pub trait CandidateAnnotator: Send + Sync {
108    fn annotate(&self, candidate: &mut RenderedCandidate);
109}