lattice_host/host_generators.rs
1//! App-state-dependent completion generators.
2//!
3//! `lattice-completion`'s built-in generators (`gen:commands`,
4//! `gen:files`) and `lattice-config`'s `gen:options` cover the
5//! state-free / config-tier sources. `gen:help-topics` lives in
6//! `lattice-help` next to the topic registry. Everything else --
7//! sources that need App-level handles (mode registry, event
8//! descriptors, LSP supervisor) -- lives here so the cmdline can
9//! offer `<Tab>` candidates for every described command in
10//! `lattice_grammar::ex_commands`.
11//!
12//! Each generator captures the minimal `Arc<...>` slice of state
13//! it needs and is registered in `App::new` next to the existing
14//! `gen:options` registration. Names are stable -- they appear as
15//! string literals in `ArgSpec::completion` -- so adding a new
16//! generator means registering it here and pointing the relevant
17//! arg schemas at it.
18
19use std::sync::Weak;
20
21use lattice_completion::candidate::{CandidateData, CandidateKind, RawCandidate};
22use lattice_completion::traits::{CandidateGenerator, GenerateContext};
23use lattice_picker::{
24 PickerAcceptOutcome, PickerContext, PickerInitResult, PickerSourceGenerator, PickerSourceSpec,
25 RoutingPayload, SourceResult,
26};
27use lattice_theme::ThemeRegistryHandle;
28
29/// `gen:modes` -- one candidate per registered mode. Walks
30/// `ModeRegistry::iter_meta` so the candidate set follows the
31/// runtime registry (built-in foundation modes plus any feature-
32/// crate `register_*_modes` additions).
33///
34/// Holds a [`Weak`] to the registry so the generator doesn't keep
35/// the Arc alive past the App's lifetime, and -- the operational
36/// reason -- so `Arc::get_mut(&mut app.mode_registry)` still
37/// succeeds in tests that need to register a test-only mode
38/// post-boot. Upgrade-on-demand: a dropped registry yields an
39/// empty candidate set (no panic).
40pub struct ModesGenerator {
41 pub registry: Weak<arc_swap::ArcSwap<lattice_mode::ModeRegistry>>,
42}
43
44impl CandidateGenerator for ModesGenerator {
45 fn generate(&self, _ctx: &GenerateContext<'_>) -> Vec<RawCandidate> {
46 let Some(registry) = self.registry.upgrade() else {
47 return Vec::new();
48 };
49 let registry = registry.load();
50 let mut out: Vec<RawCandidate> = registry
51 .iter_meta()
52 .map(|(id, _kind)| RawCandidate {
53 insert_text: None,
54 text: id.as_str().to_string(),
55 display: id.as_str().to_string(),
56 kind: CandidateKind::Plain,
57 data: CandidateData::Plain,
58 source: None,
59 accept_action: None,
60 annotations: Vec::new(),
61 display_spans: Vec::new(),
62 })
63 .collect();
64 out.sort_by(|a, b| a.text.cmp(&b.text));
65 out
66 }
67}
68
69/// `gen:events` -- one candidate per registered event, from the unified view
70/// (`event_registry::all_events`): the compile-time `EVENT_DESCRIPTORS` linkme
71/// slice (built-ins) PLUS the runtime registry (plugin-defined events, PH7.8b),
72/// so a plugin's custom events complete here the moment they register.
73pub struct EventsGenerator;
74
75impl CandidateGenerator for EventsGenerator {
76 fn generate(&self, _ctx: &GenerateContext<'_>) -> Vec<RawCandidate> {
77 let mut out: Vec<RawCandidate> = lattice_protocol::event_registry::all_events()
78 .into_iter()
79 .map(|d| RawCandidate {
80 insert_text: None,
81 text: d.name.clone(),
82 display: d.name.clone(),
83 kind: CandidateKind::Plain,
84 data: CandidateData::Plain,
85 source: None,
86 accept_action: None,
87 annotations: Vec::new(),
88 display_spans: Vec::new(),
89 })
90 .collect();
91 out.sort_by(|a, b| a.text.cmp(&b.text));
92 out
93 }
94}
95
96/// `gen:elements` -- one candidate per registered theme element / face.
97/// Walks [`ThemeRegistry::element_names`] so the candidate set follows the
98/// live registry (core elements plus any mode/plugin-contributed ones).
99/// Drives `:describe-element <Tab>` / `:describe-face <Tab>`.
100///
101/// Holds a strong [`ThemeRegistryHandle`] clone, mirroring
102/// [`ThemePickerSource`] (the theme registry is a host ServiceRegistry
103/// service that renderers read; nothing takes `Arc::get_mut` of it
104/// post-boot, so the [`Weak`] discipline the mode-registry generators use
105/// doesn't apply here).
106pub struct ElementsGenerator {
107 pub registry: ThemeRegistryHandle,
108}
109
110impl CandidateGenerator for ElementsGenerator {
111 fn generate(&self, _ctx: &GenerateContext<'_>) -> Vec<RawCandidate> {
112 // `element_names()` is already sorted.
113 self.registry
114 .element_names()
115 .into_iter()
116 .map(|name| RawCandidate {
117 insert_text: None,
118 text: name.clone(),
119 display: name,
120 kind: CandidateKind::Plain,
121 data: CandidateData::Plain,
122 source: None,
123 accept_action: None,
124 annotations: Vec::new(),
125 display_spans: Vec::new(),
126 })
127 .collect()
128 }
129}
130
131/// `gen:log-levels` -- the five canonical log levels accepted by
132/// `:lsp-log-level`. Returned in severity order so the popup reads
133/// the same way the level enum does.
134pub struct LogLevelsGenerator;
135
136impl CandidateGenerator for LogLevelsGenerator {
137 fn generate(&self, _ctx: &GenerateContext<'_>) -> Vec<RawCandidate> {
138 ["error", "warn", "info", "debug", "trace"]
139 .iter()
140 .map(|name| RawCandidate {
141 insert_text: None,
142 text: (*name).to_string(),
143 display: (*name).to_string(),
144 kind: CandidateKind::Plain,
145 data: CandidateData::Plain,
146 source: None,
147 accept_action: None,
148 annotations: Vec::new(),
149 display_spans: Vec::new(),
150 })
151 .collect()
152 }
153}
154
155/// `gen:picker-sources` -- one candidate per source id registered
156/// with the host's [`PickerRegistry`](lattice_picker::PickerRegistry).
157/// Drives `:picker <Tab>` completion; the registry's contents
158/// dictate the candidate set, so feature crates that register new
159/// picker sources automatically surface in the popup.
160///
161/// Holds a [`Weak`] (mirror of [`ModesGenerator`]) so the App can
162/// still take ownership of the registry on shutdown / replacement;
163/// dropped-registry yields an empty candidate set rather than a
164/// panic.
165pub struct PickerSourcesGenerator {
166 pub registry: Weak<arc_swap::ArcSwap<lattice_picker::PickerRegistry>>,
167}
168
169impl CandidateGenerator for PickerSourcesGenerator {
170 fn generate(&self, _ctx: &GenerateContext<'_>) -> Vec<RawCandidate> {
171 let Some(registry) = self.registry.upgrade() else {
172 return Vec::new();
173 };
174 // Wait-free snapshot of the current registry (a plugin load may have
175 // RCU-swapped a fresh one in). `PickerRegistry::iter` is id-sorted
176 // already; mirror its order so popup ordering stays stable across runs.
177 let registry = registry.load();
178 registry
179 .iter()
180 .map(|(id, _spec)| RawCandidate {
181 insert_text: None,
182 text: id.to_string(),
183 display: id.to_string(),
184 kind: CandidateKind::Plain,
185 data: CandidateData::Plain,
186 source: None,
187 accept_action: None,
188 annotations: Vec::new(),
189 display_spans: Vec::new(),
190 })
191 .collect()
192 }
193}
194
195/// `gen:lsp-servers` -- one candidate per currently running LSP
196/// server id. Reads through the supervisor's wait-free snapshot
197/// (`ArcSwap`-backed) so completion never blocks on the supervisor
198/// task. Dedup-by-id across multi-workspace runs: the same server
199/// id attached to two different roots collapses to one candidate.
200pub struct LspServersGenerator {
201 pub lsp: lattice_lsp::LspSupervisorHandle,
202}
203
204impl CandidateGenerator for LspServersGenerator {
205 fn generate(&self, _ctx: &GenerateContext<'_>) -> Vec<RawCandidate> {
206 let mut ids: Vec<String> = self
207 .lsp
208 .running_actors()
209 .into_iter()
210 .map(|((_root, id), _handle)| id)
211 .collect();
212 ids.sort();
213 ids.dedup();
214 ids.into_iter()
215 .map(|id| RawCandidate {
216 insert_text: None,
217 text: id.clone(),
218 display: id,
219 kind: CandidateKind::Plain,
220 data: CandidateData::Plain,
221 source: None,
222 accept_action: None,
223 annotations: Vec::new(),
224 display_spans: Vec::new(),
225 })
226 .collect()
227 }
228}
229
230/// `gen:customize` -- candidates for `:customize <name>`. Merges
231/// two sources:
232///
233/// - Every group declared via the `groups!` macro
234/// (`GROUP_DECLS` distributed slice) -- bare names like
235/// `editor`, `display`, `lsp`.
236/// - Every registered mode -- names ending in `-mode` like
237/// `lsp-completion-mode`, `text-mode`.
238/// - Every plugin option namespace in the live [`ConfigRegistry`]
239/// -- `treesitter-context`, and whatever else is loaded.
240///
241/// The `:customize` parser routes on the `-mode` suffix, so a
242/// single candidate set covers both forms. Same [`Weak`] discipline
243/// as [`ModesGenerator`].
244///
245/// The third source exists because the first two are COMPILE-time
246/// slices. A plugin registers its options at runtime, so a
247/// decl-only candidate set silently omits every plugin group --
248/// `:customize <plugin>` works but is never offered, which reads as
249/// "the feature isn't there". The config handle is a strong `Arc`
250/// (matching `gen:options`, which holds the same registry) because
251/// the registry outlives the completion registry rather than the
252/// other way round.
253pub struct CustomizeNamesGenerator {
254 pub registry: Weak<arc_swap::ArcSwap<lattice_mode::ModeRegistry>>,
255 pub config: std::sync::Arc<lattice_config::ConfigRegistry>,
256}
257
258impl CandidateGenerator for CustomizeNamesGenerator {
259 fn generate(&self, _ctx: &GenerateContext<'_>) -> Vec<RawCandidate> {
260 let mut out: Vec<RawCandidate> = Vec::new();
261 for meta in lattice_config::group::GROUP_DECLS.iter() {
262 out.push(RawCandidate {
263 insert_text: None,
264 text: meta.name.to_string(),
265 display: meta.name.to_string(),
266 kind: CandidateKind::Plain,
267 data: CandidateData::Plain,
268 source: None,
269 accept_action: None,
270 annotations: Vec::new(),
271 display_spans: Vec::new(),
272 });
273 }
274 for ns in lattice_config::plugin_option_groups(&self.config).keys() {
275 out.push(RawCandidate {
276 insert_text: None,
277 text: ns.clone(),
278 display: ns.clone(),
279 kind: CandidateKind::Plain,
280 data: CandidateData::Plain,
281 source: None,
282 accept_action: None,
283 annotations: Vec::new(),
284 display_spans: Vec::new(),
285 });
286 }
287 if let Some(registry) = self.registry.upgrade() {
288 let registry = registry.load();
289 for (id, _kind) in registry.iter_meta() {
290 out.push(RawCandidate {
291 insert_text: None,
292 text: id.as_str().to_string(),
293 display: id.as_str().to_string(),
294 kind: CandidateKind::Plain,
295 data: CandidateData::Plain,
296 source: None,
297 accept_action: None,
298 annotations: Vec::new(),
299 display_spans: Vec::new(),
300 });
301 }
302 }
303 out.sort_by(|a, b| a.text.cmp(&b.text));
304 out.dedup_by(|a, b| a.text == b.text);
305 out
306 }
307}
308
309/// T.12a: `:colorscheme` (no arg) — the live-preview theme picker.
310/// A trait-driven [`PickerSourceGenerator`] holding a clone of the
311/// [`ThemeRegistryHandle`] so it can enumerate registered theme names
312/// at `init` time. Arrowing through candidates LIVE-PREVIEWS each
313/// theme (the host applies the
314/// [`PickerPreviewOutcome::Colorscheme`](lattice_picker::PickerPreviewOutcome::Colorscheme)
315/// the [`Self::preview`] hook returns); `<Esc>` restores the theme
316/// active when the picker opened; `<CR>` keeps the highlighted theme.
317///
318/// Mode-owned-surface note: the host owns the theme subsystem (the
319/// `ThemeRegistry` is a host ServiceRegistry service the renderers
320/// read), so this source + its handler body both live host-side — no
321/// half-migration.
322pub struct ThemePickerSource {
323 spec: PickerSourceSpec,
324 registry: ThemeRegistryHandle,
325}
326
327impl ThemePickerSource {
328 pub fn new(registry: ThemeRegistryHandle) -> Self {
329 Self {
330 spec: PickerSourceSpec::no_args("colorscheme", "Pick a colour theme (live preview)."),
331 registry,
332 }
333 }
334}
335
336impl PickerSourceGenerator for ThemePickerSource {
337 fn spec(&self) -> &PickerSourceSpec {
338 &self.spec
339 }
340
341 fn init(&self, _ctx: &PickerContext<'_>, _args: &[String]) -> SourceResult<PickerInitResult> {
342 let pairs = self
343 .registry
344 .theme_names()
345 .into_iter()
346 .map(|name| {
347 let cand = RawCandidate::plain(name.clone(), CandidateKind::Plain);
348 (cand, RoutingPayload::Colorscheme { name })
349 })
350 .collect();
351 Ok(PickerInitResult::Inline(pairs))
352 }
353
354 fn accept(
355 &self,
356 _ctx: &PickerContext<'_>,
357 routing: &RoutingPayload,
358 ) -> SourceResult<PickerAcceptOutcome> {
359 match routing {
360 RoutingPayload::Colorscheme { name } => {
361 Ok(PickerAcceptOutcome::ApplyColorscheme { name: name.clone() })
362 }
363 other => Err(format!("colorscheme: unexpected routing payload {other:?}")),
364 }
365 }
366
367 fn preview(
368 &self,
369 _ctx: &PickerContext<'_>,
370 routing: &RoutingPayload,
371 ) -> Option<lattice_picker::PickerPreviewOutcome> {
372 match routing {
373 RoutingPayload::Colorscheme { name } => {
374 Some(lattice_picker::PickerPreviewOutcome::Colorscheme { name: name.clone() })
375 }
376 _ => None,
377 }
378 }
379}
380
381/// `gen:themes` — one candidate per registered colour theme. Drives
382/// `:colorscheme <Tab>`.
383///
384/// The same `theme_names()` the `colorscheme` picker enumerates, so the two
385/// surfaces cannot disagree about which themes exist. `:colorscheme` keeps its
386/// picker as well: `ArgSpec` carries `completion` and `picker` as separate
387/// fields precisely so an argument can offer inline `<Tab>` AND the richer
388/// surface — here the picker adds live preview, which completion cannot.
389pub struct ThemesGenerator {
390 pub registry: ThemeRegistryHandle,
391}
392
393impl CandidateGenerator for ThemesGenerator {
394 fn generate(&self, _ctx: &GenerateContext<'_>) -> Vec<RawCandidate> {
395 self.registry
396 .theme_names()
397 .into_iter()
398 .map(|name| RawCandidate {
399 insert_text: None,
400 text: name.clone(),
401 display: name,
402 kind: CandidateKind::Plain,
403 data: CandidateData::Plain,
404 source: None,
405 accept_action: None,
406 annotations: Vec::new(),
407 display_spans: Vec::new(),
408 })
409 .collect()
410 }
411}
412
413/// `gen:plugin-api-seams` — one candidate per WIT interface in the plugin-API
414/// catalog. Drives `:describe-plugin-api <Tab>`.
415///
416/// The catalog is generated from the WIT at build time and is static, so this
417/// needs no handle — and a seam renamed in the WIT changes the candidates with
418/// no second place to update.
419pub struct PluginApiSeamsGenerator;
420
421impl CandidateGenerator for PluginApiSeamsGenerator {
422 fn generate(&self, _ctx: &GenerateContext<'_>) -> Vec<RawCandidate> {
423 lattice_plugin_api::catalog()
424 .interfaces
425 .iter()
426 .map(|i| RawCandidate {
427 insert_text: None,
428 text: i.name.clone(),
429 display: i.name.clone(),
430 kind: CandidateKind::Plain,
431 data: CandidateData::Plain,
432 source: None,
433 accept_action: None,
434 annotations: Vec::new(),
435 display_spans: Vec::new(),
436 })
437 .collect()
438 }
439}
440
441/// `gen:plugin-api-formats` — the two export formats `:export-plugin-api`
442/// accepts. A two-element set is still worth completing: the alternative is
443/// reading the command's doc to learn that `md` is spelled `markdown`.
444pub struct PluginApiFormatsGenerator;
445
446impl CandidateGenerator for PluginApiFormatsGenerator {
447 fn generate(&self, _ctx: &GenerateContext<'_>) -> Vec<RawCandidate> {
448 ["markdown", "json"]
449 .into_iter()
450 .map(|s| RawCandidate {
451 insert_text: None,
452 text: s.to_string(),
453 display: s.to_string(),
454 kind: CandidateKind::Plain,
455 data: CandidateData::Plain,
456 source: None,
457 accept_action: None,
458 annotations: Vec::new(),
459 display_spans: Vec::new(),
460 })
461 .collect()
462 }
463}
464
465/// `gen:plugins` — one candidate per loaded plugin. Drives `:describe-plugin <Tab>` and the plugin manager's
466/// `:plugin-unload` / `:plugin-reload` / `:plugin-update`.
467///
468/// Both call sites carried this as a deferred comment — "`gen:plugins`
469/// completion is a follow-up … empty until Phase-8" in `ex_commands.rs`, and
470/// the same note on `lattice-plugin-loader`'s `string_arg`. Phase 8 landed and
471/// the comments did not get grepped, which is why the rule is now a test
472/// (`ex_string_args_have_completion.rs`) rather than a convention.
473///
474/// Holds a strong `Arc<PluginMetaRegistry>`, the same instance the host's
475/// `register_plugin` writes through and `:list-plugins` reads. It is registered
476/// once at boot and never swapped, so there is no stale-Arc hazard; the
477/// interior `RwLock` is what a mid-session load mutates, and reading it per
478/// `generate` is what makes a plugin loaded after boot appear under `<Tab>`.
479pub struct PluginsGenerator {
480 pub meta: std::sync::Arc<crate::dispatch::PluginMetaRegistry>,
481}
482
483impl CandidateGenerator for PluginsGenerator {
484 fn generate(&self, _ctx: &GenerateContext<'_>) -> Vec<RawCandidate> {
485 let Ok(map) = self.meta.0.read() else {
486 return Vec::new();
487 };
488 let mut names: Vec<String> = map.values().map(|m| m.name.clone()).collect();
489 names.sort();
490 names
491 .into_iter()
492 .map(|name| RawCandidate {
493 insert_text: None,
494 text: name.clone(),
495 display: name,
496 kind: CandidateKind::Plain,
497 data: CandidateData::Plain,
498 source: None,
499 accept_action: None,
500 annotations: Vec::new(),
501 display_spans: Vec::new(),
502 })
503 .collect()
504 }
505}
506
507/// MB.5: `gen:history-kinds` — completion source for `:history <Tab>`.
508/// Returns the two valid kind arguments: `commands` and `searches`.
509pub struct HistoryKindsGenerator;
510
511impl CandidateGenerator for HistoryKindsGenerator {
512 fn generate(&self, _ctx: &GenerateContext<'_>) -> Vec<RawCandidate> {
513 vec!["commands", "searches", "pane-buffers"]
514 .into_iter()
515 .map(|s| RawCandidate {
516 insert_text: None,
517 text: s.to_string(),
518 display: s.to_string(),
519 kind: CandidateKind::Plain,
520 data: CandidateData::Plain,
521 source: None,
522 accept_action: None,
523 annotations: Vec::new(),
524 display_spans: Vec::new(),
525 })
526 .collect()
527 }
528}
529
530#[cfg(test)]
531mod tests {
532 use super::*;
533 use lattice_core::Document;
534 use lattice_grammar::CommandRegistry;
535 use std::sync::Arc;
536
537 /// T.9.d follow-up: `gen:elements` (`:describe-element <Tab>`) enumerates
538 /// the live theme-element registry. Confirms the generator surfaces builtin
539 /// element names in sorted order, mirroring `ThemeRegistry::element_names`.
540 #[test]
541 fn elements_generator_produces_sorted_theme_element_names() {
542 let registry: ThemeRegistryHandle =
543 Arc::new(lattice_theme::InMemoryThemeRegistry::with_defaults());
544 let g = ElementsGenerator {
545 registry: registry.clone(),
546 };
547 let doc = Document::from_text("");
548 let buf = doc.buffer();
549 let cmd_reg = CommandRegistry::new();
550 let ctx = GenerateContext {
551 prefix: "",
552 buffer: buf,
553 registry: &cmd_reg,
554 case_sensitive: false,
555 };
556
557 let out = g.generate(&ctx);
558 let names: Vec<String> = out.iter().map(|c| c.text.clone()).collect();
559
560 assert!(
561 names.contains(&"syntax.keyword".to_string()),
562 "a builtin theme element must complete for `:describe-element`"
563 );
564 // The generator returns exactly what the registry enumerates, in the
565 // same (sorted) order — no drift, so popup ordering is stable.
566 assert_eq!(names, registry.element_names());
567 let mut sorted = names.clone();
568 sorted.sort();
569 assert_eq!(names, sorted, "candidates must be sorted");
570 }
571
572 /// `gen:plugins` enumerates the LOADED set, read per `generate` rather than
573 /// snapshotted, so a plugin loaded after boot completes without a restart.
574 #[test]
575 fn plugins_generator_enumerates_loaded_plugins_sorted() {
576 let meta = Arc::new(crate::dispatch::PluginMetaRegistry::default());
577 let g = PluginsGenerator { meta: meta.clone() };
578 let doc = Document::from_text("");
579 let buf = doc.buffer();
580 let cmd_reg = CommandRegistry::new();
581 let ctx = GenerateContext {
582 prefix: "",
583 buffer: buf,
584 registry: &cmd_reg,
585 case_sensitive: false,
586 };
587
588 assert!(
589 g.generate(&ctx).is_empty(),
590 "nothing loaded yet ⇒ no candidates"
591 );
592
593 // Registered the way the loader does it, AFTER the generator was built.
594 // A boot-time snapshot would list nothing here forever.
595 {
596 let mut map = meta.0.write().expect("meta registry lock");
597 for (id, name) in [(7u32, "zebra"), (8, "auto-pair")] {
598 map.insert(
599 id,
600 crate::dispatch::PluginMeta {
601 name: name.to_string(),
602 doc: String::new(),
603 },
604 );
605 }
606 }
607
608 let names: Vec<String> = g.generate(&ctx).iter().map(|c| c.text.clone()).collect();
609 assert_eq!(
610 names,
611 vec!["auto-pair".to_string(), "zebra".to_string()],
612 "every loaded plugin completes, sorted — popup order must be stable \
613 even though the backing map is a HashMap"
614 );
615 }
616
617 /// A plugin's option namespace must be offered by `:customize <Tab>`.
618 ///
619 /// The regression this pins: the generator was built from
620 /// `GROUP_DECLS` + modes, both of which are populated at COMPILE time. A
621 /// plugin registers its options at RUNTIME, so `:customize
622 /// treesitter-context` worked while `<Tab>` never once mentioned it —
623 /// making a working group indistinguishable from an absent one, which is
624 /// the form the report arrived in.
625 #[test]
626 fn customize_completion_offers_runtime_plugin_namespaces() {
627 use lattice_config::ConfigRegistry;
628 use lattice_core::Document;
629 use lattice_grammar::CommandRegistry;
630
631 // Registered exactly the way the plugin config seam does it
632 // (`register_plugin_option` -> `try_register(ConfigOption::new(..))`),
633 // so the test cannot pass against a shape production never produces.
634 let config = std::sync::Arc::new(ConfigRegistry::default());
635 for (name, doc) in [
636 ("treesitter-context.enabled", "Enable it."),
637 ("treesitter-context.max-lines", "Cap the strip."),
638 ] {
639 config
640 .try_register(lattice_config::option::Option::<bool>::new(
641 name.to_owned(),
642 true,
643 doc.to_owned(),
644 ))
645 .expect("fresh registry");
646 }
647
648 let modes = std::sync::Arc::new(arc_swap::ArcSwap::from_pointee(
649 lattice_mode::ModeRegistry::new(),
650 ));
651 let g = CustomizeNamesGenerator {
652 registry: std::sync::Arc::downgrade(&modes),
653 config: config.clone(),
654 };
655
656 let doc = Document::from_text("");
657 let buf = doc.buffer();
658 let cmd_reg = CommandRegistry::new();
659 let ctx = GenerateContext {
660 prefix: "",
661 buffer: buf,
662 registry: &cmd_reg,
663 case_sensitive: false,
664 };
665
666 let names: Vec<String> = g.generate(&ctx).into_iter().map(|c| c.text).collect();
667
668 assert!(
669 names.contains(&"treesitter-context".to_string()),
670 "the plugin namespace must be offered; got {names:?}"
671 );
672 // The namespace is offered ONCE, not once per option in it.
673 assert_eq!(
674 names.iter().filter(|n| *n == "treesitter-context").count(),
675 1
676 );
677 // And the compile-time groups are still there — this is an addition.
678 assert!(names.contains(&"editor".to_string()));
679 }
680}