Skip to main content

lattice_mode/modes/
completion.rs

1//! Completion-mode pair (insert-completion.md §12).
2//!
3//! The completion machinery surfaces through two minors with
4//! different lifecycles:
5//!
6//! - **`completion-mode`** -- "this buffer participates in
7//!   insert-mode completion." Auto-activates on writable buffer
8//!   kinds (Document) at buffer creation; stays active for the
9//!   buffer's lifetime. Acts as the gate `do_completion_trigger`
10//!   checks before opening the popup -- read-only kinds (Help,
11//!   FileTree, Oil) never activate it, so `<C-Space>` in those
12//!   buffers is a silent no-op.
13//!
14//! - **`completion-popup-mode`** -- transient. Active iff the
15//!   candidate popup is live. Owns the popup-internal keymap
16//!   (`<C-n>` / `<C-p>` / `<C-y>` / `<Tab>` / `<CR>` / `<Esc>` /
17//!   `<C-e>` / `<C-d>` / `<C-Space>` / `<C-f>` / `<C-b>` plus
18//!   the per-source filter chords from CSM.K2). Replaces the
19//!   imperative `App.insert_completion.is_some()` flag the v1
20//!   wiring gated on (CSM.2's original `completion-mode`).
21//!
22//! Both modes own no contributed options today. The pairing
23//! mirrors `lsp-mode` (umbrella, persistent) + the LSP sub-modes
24//! (per-feature, persistent) -- here the lifecycle distinction
25//! is "persistent on writable buffers" vs "transient with the
26//! popup."
27//!
28//! Placement: in `lattice-mode::modes` (not `lattice-completion`)
29//! to avoid a dep cycle -- `lattice-mode` already depends on
30//! `lattice-completion` for the `CompletionSourceContribution`
31//! return type on `Mode::completion_sources()` (CSM.1), so
32//! reversing direction would require `Mode` itself to live in
33//! completion.
34
35use std::sync::Arc;
36
37use lattice_completion::{
38    BufferWordsSource, CompletionSourceContribution, CompletionSourceKind, PathCompletionSource,
39    SourceId,
40};
41use lattice_config::OptionOverrideSet;
42
43use crate::{BufferLocal, CapabilitySet, LifecycleFuture, Mode, ModeContext, ModeId, ModeKind};
44
45/// The "buffer participates in insert-mode completion" marker.
46/// Auto-activates on writable kinds at buffer creation. See
47/// module docs.
48pub struct CompletionMode;
49
50impl CompletionMode {
51    /// The canonical id, `"completion-mode"` — what [`Mode::id`](crate::Mode::id)
52    /// returns. Use it to name this mode without an instance (activation,
53    /// `implies`, keymap layers, tests).
54    pub fn mode_id() -> ModeId {
55        ModeId::new("completion-mode")
56    }
57}
58
59impl Mode for CompletionMode {
60    type Guard = ();
61    fn id(&self) -> ModeId {
62        Self::mode_id()
63    }
64    fn kind(&self) -> ModeKind {
65        ModeKind::Minor
66    }
67    fn options(&self) -> OptionOverrideSet {
68        OptionOverrideSet::default()
69    }
70    fn required_capabilities(&self) -> CapabilitySet {
71        CapabilitySet::empty()
72    }
73    fn on_activate(&self, _ctx: ModeContext) -> LifecycleFuture<'_, ()> {
74        Box::pin(async { Ok(()) })
75    }
76}
77
78/// CSM.4 (insert-completion.md §12 first source migration):
79/// `buffer-words-mode` -- contributes the buffer-words
80/// completion source. Auto-activates on writable buffer kinds
81/// (Document) so the popup's all-sources view shows words
82/// scraped from the active buffer's text. `popup_filter_chord =
83/// Some('b')` ⇒ `<C-b>` inside `completion-popup-mode` narrows
84/// the popup to buffer-words only.
85///
86/// Placement: lives in `lattice-mode::modes::completion`
87/// alongside `CompletionMode`. Ideal location would be
88/// `lattice-completion::modes` (the "feature crate owns its
89/// mode" rule), but `Mode` is defined in `lattice-mode` and the
90/// dep direction is `lattice-mode` → `lattice-completion`
91/// (CSM.1); the `Mode` impl can't live in `lattice-completion`
92/// without a cycle. The source struct (`BufferWordsSource`)
93/// stays in `lattice-completion::insert` where it belongs;
94/// only the thin `Mode` adapter sits here.
95pub struct BufferWordsMode;
96
97impl BufferWordsMode {
98    /// The canonical id, `"buffer-words-mode"` — what [`Mode::id`](crate::Mode::id)
99    /// returns. Use it to name this mode without an instance (activation,
100    /// `implies`, keymap layers, tests).
101    pub fn mode_id() -> ModeId {
102        ModeId::new("buffer-words-mode")
103    }
104}
105
106impl Mode for BufferWordsMode {
107    type Guard = ();
108    fn id(&self) -> ModeId {
109        Self::mode_id()
110    }
111    fn kind(&self) -> ModeKind {
112        ModeKind::Minor
113    }
114    fn options(&self) -> OptionOverrideSet {
115        OptionOverrideSet::default()
116    }
117    fn required_capabilities(&self) -> CapabilitySet {
118        CapabilitySet::empty()
119    }
120    fn completion_sources(&self) -> Vec<CompletionSourceContribution> {
121        vec![CompletionSourceContribution {
122            accepts_non_word_query: false,
123            id: SourceId::new(BufferWordsSource::ID),
124            default_priority: 100,
125            auto_trigger: true,
126            trigger_chars: Vec::new(),
127            popup_filter_chord: Some('b'),
128            kind: CompletionSourceKind::Sync(Arc::new(BufferWordsSource::new())),
129        }]
130    }
131    fn on_activate(&self, _ctx: ModeContext) -> LifecycleFuture<'_, ()> {
132        Box::pin(async { Ok(()) })
133    }
134}
135
136/// CSM.7 (insert-completion.md §12 fourth source migration):
137/// `path-completion-mode` -- contributes the path-completion
138/// source. Auto-activates on writable buffer kinds; the
139/// source self-suppresses outside string scopes (gated by
140/// `ctx.path_context` which the host sets from its tree-sitter
141/// scope detection). `popup_filter_chord = Some('f')` ⇒
142/// `<C-f>` inside `completion-popup-mode` narrows the popup
143/// to filesystem entries only.
144///
145/// Same placement note as `BufferWordsMode`: ideal location
146/// would be `lattice-completion::modes` per the "feature
147/// crate owns its mode" rule, but the
148/// `lattice-mode -> lattice-completion` dep direction forces
149/// the thin `Mode` adapter to live in `lattice-mode`; the
150/// underlying `PathCompletionSource` stays in
151/// `lattice-completion::path`.
152pub struct PathCompletionMode;
153
154impl PathCompletionMode {
155    /// The canonical id, `"path-completion-mode"` — what [`Mode::id`](crate::Mode::id)
156    /// returns. Use it to name this mode without an instance (activation,
157    /// `implies`, keymap layers, tests).
158    pub fn mode_id() -> ModeId {
159        ModeId::new("path-completion-mode")
160    }
161}
162
163impl Mode for PathCompletionMode {
164    type Guard = ();
165    fn id(&self) -> ModeId {
166        Self::mode_id()
167    }
168    fn kind(&self) -> ModeKind {
169        ModeKind::Minor
170    }
171    fn options(&self) -> OptionOverrideSet {
172        OptionOverrideSet::default()
173    }
174    fn required_capabilities(&self) -> CapabilitySet {
175        CapabilitySet::empty()
176    }
177    fn completion_sources(&self) -> Vec<CompletionSourceContribution> {
178        vec![CompletionSourceContribution {
179            accepts_non_word_query: false,
180            id: SourceId::new(lattice_completion::PATH_SOURCE_ID),
181            // 90 per insert-completion.md §3.4. The source self-
182            // suppresses outside string scopes; when active,
183            // paths sort below buffer-words (100) and snippet
184            // (150) but above tree-sitter symbols (80).
185            default_priority: 90,
186            auto_trigger: true,
187            trigger_chars: vec!['/'],
188            popup_filter_chord: Some('f'),
189            kind: CompletionSourceKind::Sync(Arc::new(PathCompletionSource)),
190        }]
191    }
192    fn on_activate(&self, _ctx: ModeContext) -> LifecycleFuture<'_, ()> {
193        Box::pin(async { Ok(()) })
194    }
195}
196
197/// The popup-is-live transient minor. See module docs.
198pub struct CompletionPopupMode;
199
200impl CompletionPopupMode {
201    /// The canonical id, `"completion-popup-mode"` — what [`Mode::id`](crate::Mode::id)
202    /// returns. Use it to name this mode without an instance (activation,
203    /// `implies`, keymap layers, tests).
204    pub fn mode_id() -> ModeId {
205        ModeId::new("completion-popup-mode")
206    }
207}
208
209impl Mode for CompletionPopupMode {
210    type Guard = ();
211    fn id(&self) -> ModeId {
212        Self::mode_id()
213    }
214    fn kind(&self) -> ModeKind {
215        ModeKind::Minor
216    }
217    fn options(&self) -> OptionOverrideSet {
218        OptionOverrideSet::default()
219    }
220    fn required_capabilities(&self) -> CapabilitySet {
221        CapabilitySet::empty()
222    }
223    fn on_activate(&self, _ctx: ModeContext) -> LifecycleFuture<'_, ()> {
224        Box::pin(async { Ok(()) })
225    }
226}
227
228/// Cached active completion-
229/// source set for a buffer. The host
230/// (`App::recompute_active_completion_sources_for`) recomputes
231/// this on every mode-activation / -deactivation transition by
232/// walking `active_modes` and calling
233/// `mode.completion_sources()` on each. The aggregator reads
234/// the cache on the popup-open / refilter path -- O(1) buffer-
235/// local lookup, never a walk over every active mode per
236/// keystroke (CSM.3, insert-completion.md §12.4).
237///
238/// `OWNER_MODE` is `"completion-mode"` because that's the
239/// persistent gate; the cache is meaningless when
240/// `completion-mode` is inactive (read-only buffers never
241/// trigger the popup, never need the cache).
242#[derive(Debug, Clone, Default)]
243pub struct ActiveCompletionSources(pub Vec<CompletionSourceContribution>);
244
245impl BufferLocal for ActiveCompletionSources {
246    const NAME: &'static str = "completion-mode.active-sources";
247    const DOC: &'static str = "Cached active insert-completion source set for this \
248         buffer. Recomputed on every mode-activation / \
249         -deactivation transition; read by the aggregator on \
250         the popup-open / refilter path.";
251    const OWNER_MODE: &'static str = "completion-mode";
252    fn describe(&self) -> String {
253        format!("{} source(s)", self.0.len())
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260    use crate::{ActiveModes, BufferLocals, GuardStoreHandle, ModeRegistry};
261    use lattice_protocol::ids::BufferId;
262
263    #[test]
264    fn completion_mode_is_a_minor() {
265        assert_eq!(CompletionMode.kind(), ModeKind::Minor);
266        assert_eq!(CompletionMode.id(), CompletionMode::mode_id());
267        assert_eq!(CompletionMode::mode_id().as_str(), "completion-mode");
268    }
269
270    #[test]
271    fn completion_popup_mode_is_a_minor() {
272        assert_eq!(CompletionPopupMode.kind(), ModeKind::Minor);
273        assert_eq!(CompletionPopupMode.id(), CompletionPopupMode::mode_id());
274        assert_eq!(
275            CompletionPopupMode::mode_id().as_str(),
276            "completion-popup-mode",
277        );
278    }
279
280    #[test]
281    fn both_modes_register_and_activate() {
282        let mut registry = ModeRegistry::new();
283        registry
284            .register(CompletionMode)
285            .expect("register completion-mode");
286        registry
287            .register(CompletionPopupMode)
288            .expect("register completion-popup-mode");
289        let mut active = ActiveModes::new();
290        let guards = GuardStoreHandle::new();
291        let cfg = Arc::new(lattice_config::ConfigRegistry::new());
292        let evt = Arc::new(lattice_runtime::EventBus::new());
293        let svc = Arc::new(crate::services::ServiceRegistry::new());
294        registry
295            .activate_minor(
296                &mut active,
297                &guards,
298                &cfg,
299                &evt,
300                &svc,
301                BufferId::new(0),
302                CompletionMode::mode_id(),
303                CapabilitySet::empty(),
304            )
305            .expect("activate completion-mode");
306        registry
307            .activate_minor(
308                &mut active,
309                &guards,
310                &cfg,
311                &evt,
312                &svc,
313                BufferId::new(0),
314                CompletionPopupMode::mode_id(),
315                CapabilitySet::empty(),
316            )
317            .expect("activate completion-popup-mode");
318        assert!(active.has_minor(CompletionMode::mode_id()));
319        assert!(active.has_minor(CompletionPopupMode::mode_id()));
320    }
321
322    #[test]
323    fn both_modes_default_completion_sources_is_empty() {
324        // Neither mode is a source-contributor.
325        assert!(<CompletionMode as Mode>::completion_sources(&CompletionMode).is_empty());
326        assert!(<CompletionPopupMode as Mode>::completion_sources(&CompletionPopupMode).is_empty());
327    }
328
329    #[test]
330    fn active_completion_sources_describes_count() {
331        let empty = ActiveCompletionSources::default();
332        assert_eq!(empty.describe(), "0 source(s)");
333    }
334
335    #[test]
336    fn active_completion_sources_is_a_buffer_local() {
337        let mut locals = BufferLocals::new();
338        locals.insert(ActiveCompletionSources(Vec::new()));
339        assert!(locals.get::<ActiveCompletionSources>().is_some());
340        let d = locals.iter_descriptors().next().expect("descriptor");
341        assert_eq!(d.name, "completion-mode.active-sources");
342        assert_eq!(d.owner_mode, "completion-mode");
343    }
344}