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}