lattice_mode/mode.rs
1//! The `Mode` trait, plus `ModeId`, `ModeKind`, and the
2//! [`LifecycleFuture`] type alias.
3
4use std::any::Any;
5use std::future::Future;
6use std::pin::Pin;
7
8pub use lattice_keymap::ModeId;
9
10use crate::action_handler_registry::ActionHandlerContribution;
11use crate::capability::CapabilitySet;
12use crate::context::ModeContext;
13use crate::contributions::{DecorationCtx, DecorationProvider, GutterDecoration, Keymap};
14use crate::error::ModeActivationError;
15use lattice_config::OptionOverrideSet;
16use lattice_core::BufferKind;
17
18/// Major / minor distinction. A buffer has exactly one major and
19/// any number of minors active simultaneously
20/// (mode-architecture.md §3).
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum ModeKind {
23 /// Content-type identity (`rust-mode`, `help-mode`). Exactly one per
24 /// buffer; activating another replaces it. Chosen by the host's major
25 /// resolver ([`Mode::target_buffer_kind`], [`Mode::target_language`],
26 /// [`Mode::presents_extensions`]), never by an [`ActivationPolicy`].
27 Major,
28 /// Additive behaviour layered over the major (`line-numbers-mode`,
29 /// `table-mode`). Any number per buffer, kept in activation order;
30 /// auto-activated per [`Mode::activation_policy`] or pulled in by
31 /// another mode's [`Mode::implies`].
32 Minor,
33}
34
35/// A minor mode's *default* auto-activation policy — the allowlist of
36/// major modes it activates inside, as the mode itself ships it
37/// (mode-architecture.md §7.4). The host's minor-activation resolver
38/// subscribes once to [`lattice_protocol::Event::MajorEntered`] and,
39/// for each registered minor whose policy [`admits`](Self::admits)
40/// the entered major, activates it.
41///
42/// This is the mode's *declared default*. Config
43/// (`<mode>.activation = global | <allowlist> | off`) folds over it;
44/// that fold is the host's job (SN.3), not the mode's. The default on
45/// the `Mode` trait is [`Manual`](Self::Manual): a mode auto-activates
46/// nowhere until it opts in or the user does. Leaving the onus on the
47/// user is a legitimate choice — some modes won't ship a sensible
48/// default and shouldn't guess.
49///
50/// Only *enabled* minors are auto-activated
51/// ([`ModeRegistry::is_minor_enabled`](crate::ModeRegistry::is_minor_enabled)):
52/// native modes are enabled at registration, plugin modes are not.
53///
54/// # Examples
55///
56/// ```
57/// use lattice_core::BufferKind;
58/// use lattice_mode::{ActivationPolicy, ModeId};
59///
60/// // A content minor: every real document, never a synthetic buffer.
61/// assert!(ActivationPolicy::Global.admits("rust-mode", BufferKind::Document));
62/// assert!(!ActivationPolicy::Global.admits("help-mode", BufferKind::Help));
63///
64/// // A universal leader: everywhere the user can focus.
65/// assert!(ActivationPolicy::Universal.admits("help-mode", BufferKind::Help));
66///
67/// // An allowlist is matched on the major's id, independent of kind.
68/// let tables = ActivationPolicy::Majors(vec![
69/// ModeId::new("markdown-mode"),
70/// ModeId::new("org-mode"),
71/// ]);
72/// assert!(tables.admits("org-mode", BufferKind::Document));
73/// assert!(!tables.admits("rust-mode", BufferKind::Document));
74///
75/// // The trait default auto-activates nowhere.
76/// assert!(!ActivationPolicy::default().admits("rust-mode", BufferKind::Document));
77/// ```
78#[derive(Debug, Clone, PartialEq, Eq, Default)]
79pub enum ActivationPolicy {
80 /// Never auto-activate; only explicit (user / host / `:<mode>`)
81 /// activation turns the mode on. The trait default.
82 #[default]
83 Manual,
84 /// Auto-activate on every **document** buffer that enters a major
85 /// mode (scoped to [`BufferKind::Document`]). The right policy for
86 /// content modes (snippets, LSP, …) that only make sense over
87 /// user-edited text, not synthetic UI buffers.
88 Global,
89 /// Auto-activate on **every** buffer kind that enters a major mode —
90 /// documents *and* synthetic UI buffers (`*messages*`, help, file
91 /// tree, oil, terminal). For *universal* contributions like the
92 /// `emacs-keys` `<C-x>` leader, where navigation chords (switch
93 /// buffer, switch pane, quit) should work everywhere the user can
94 /// focus — mirroring emacs, whose `C-x` map is live in `*Messages*`
95 /// and every other buffer. NOT for content modes (use [`Global`]).
96 /// Mode-local keymaps are gated by binding mode, so Terminal-Insert
97 /// keystroke passthrough is unaffected by a Normal-only leader.
98 ///
99 /// [`Global`]: Self::Global
100 Universal,
101 /// Auto-activate only when the entered major's id is in this
102 /// allowlist. An empty list behaves like [`Manual`](Self::Manual)
103 /// (matches no major).
104 Majors(Vec<ModeId>),
105}
106
107impl ActivationPolicy {
108 /// Does this policy auto-activate when a buffer of kind
109 /// `buffer_kind` enters the major mode named `major`?
110 ///
111 /// `Global` is scoped to **real document buffers**
112 /// ([`BufferKind::Document`]) — every code/text buffer, not the
113 /// synthetic UI buffers (file tree, help, `*messages*`, terminal,
114 /// …). `Universal` admits every kind (documents *and* synthetic
115 /// buffers) for universal-leader modes. A mode that wants a narrow
116 /// synthetic opt-in instead names that buffer's major explicitly
117 /// via `Majors([..])`, which is kind-independent.
118 pub fn admits(&self, major: &str, buffer_kind: BufferKind) -> bool {
119 match self {
120 Self::Manual => false,
121 Self::Global => buffer_kind == BufferKind::Document,
122 Self::Universal => true,
123 Self::Majors(allow) => allow.iter().any(|m| m.as_str() == major),
124 }
125 }
126}
127
128/// An editable region at the **tail** of an otherwise read-only,
129/// owner-written buffer — the comint pattern (AU‑3) (the agent-conversation prompt,
130/// future `*scratch*` / REPL input lines). A mode declares it via
131/// [`Mode::editable_tail`]; the host's read-only edit gate consults it so
132/// user keystrokes may edit only the tail while the owner's projection writes
133/// (which bypass the gate by going through the runtime document handle
134/// directly) keep the rest owner-controlled.
135///
136/// The region is expressed **structurally, relative to the buffer end**, not
137/// as an absolute position — so it stays valid as the owner appends content
138/// above the tail without any per-edit bookkeeping:
139///
140/// - `trailing_lines` — the number of trailing lines that form the region
141/// (`1` for a single-line prompt). The first editable line is
142/// `line_count - trailing_lines`.
143/// - `first_line_min_byte` — the minimum byte column on that first editable
144/// line, protecting a prompt marker rendered as buffer text (e.g. the
145/// `"> "` prefix ⇒ `2`). Lines strictly after the first are editable from
146/// column 0.
147///
148/// `Default` is the empty tail (`trailing_lines = 0`), i.e. nothing editable.
149///
150/// ## Bottom-relative vs. anchored
151///
152/// The bottom-relative `trailing_lines` encoding is correct for a *fixed-height*
153/// tail: it stays valid as the owner appends content ABOVE the tail, but breaks
154/// the moment the user grows the tail itself (a multi-line prompt), because the
155/// added lines push the marker line out of the region. For a prompt whose height
156/// changes with user newlines AND whose top drifts as a transcript streams above
157/// it, set [`first_editable_line`](Self::first_editable_line) to the ABSOLUTE
158/// line where the editable region begins (the transcript-end line); the owning
159/// mode updates it as the transcript grows. When set it overrides
160/// `trailing_lines`.
161///
162/// # Examples
163///
164/// ```
165/// use lattice_mode::EditableTail;
166///
167/// // A one-line `> ` prompt at the bottom of a 5-line transcript.
168/// let prompt = EditableTail { trailing_lines: 1, first_line_min_byte: 2, first_editable_line: None };
169/// assert!(prompt.permits(4, 2, 5)); // after the marker, on the prompt line
170/// assert!(!prompt.permits(4, 0, 5)); // inside the `> ` marker
171/// assert!(!prompt.permits(3, 0, 5)); // history above the prompt
172/// assert!(prompt.permits(9, 2, 10)); // still the last line after the owner appends
173///
174/// // The default tail permits nothing.
175/// assert!(!EditableTail::default().permits(0, 0, 1));
176/// ```
177#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
178pub struct EditableTail {
179 /// Number of trailing lines forming the editable region. Ignored when
180 /// [`first_editable_line`](Self::first_editable_line) is `Some`.
181 pub trailing_lines: u32,
182 /// Minimum editable byte column on the first editable line (guards a
183 /// text-rendered prompt marker). Ignored for lines after the first.
184 pub first_line_min_byte: u32,
185 /// When `Some(anchor)`, the editable region is `anchor..EOF` (absolute),
186 /// overriding the bottom-relative `trailing_lines`. Lets a mode with a
187 /// multi-line, growing prompt anchor the region to the transcript end and
188 /// keep it correct as the user adds newlines. Clamped to the last line so a
189 /// stale-high anchor never freezes the whole buffer.
190 pub first_editable_line: Option<u32>,
191}
192
193impl EditableTail {
194 /// Is a keystroke edit whose earliest affected position is
195 /// `(start_line, start_byte)` permitted, given the buffer currently has
196 /// `line_count` lines? Pure + unit-testable: the host gate computes the
197 /// live `line_count` from the document snapshot and delegates here.
198 pub fn permits(&self, start_line: u32, start_byte: u32, line_count: u32) -> bool {
199 let first_editable = match self.first_editable_line {
200 // Absolute anchor: `anchor..EOF`, clamped so a stale-high anchor
201 // still leaves the last line editable rather than freezing the tail.
202 Some(anchor) => anchor.min(line_count.saturating_sub(1)),
203 None => {
204 if self.trailing_lines == 0 {
205 return false;
206 }
207 line_count.saturating_sub(self.trailing_lines)
208 }
209 };
210 if start_line < first_editable {
211 return false;
212 }
213 if start_line == first_editable && start_byte < self.first_line_min_byte {
214 return false;
215 }
216 true
217 }
218}
219
220/// Pinned, boxed, send-able future for `Mode::on_activate`.
221///
222/// The explicit `Pin<Box<dyn Future + Send>>` desugaring (rather
223/// than `async fn` in trait) is needed because:
224///
225/// 1. **Object safety.** [`Mode`] has an associated type
226/// ([`Mode::Guard`]) and is not directly object-safe. The
227/// dispatcher stores modes as `Arc<dyn DynMode>` via the
228/// [`DynMode`](crate::DynMode) adapter; the adapter's
229/// `on_activate_dyn` returns a future whose output is
230/// type-erased to `Box<dyn Any + Send>`.
231/// 2. **`Send` bound.** Lifecycle futures may be scheduled across
232/// threads (M-async.2 swaps `poll_now` for runtime-spawned
233/// `.await`); the future itself must be `Send` so the executor
234/// can move it between worker threads.
235/// 3. **Explicit lifetime.** Modes capture their `&self` and the
236/// [`ModeContext`] (owned, `Send + 'static`); the future's
237/// lifetime is tied to `&self` via `'a`.
238///
239/// The default type parameter `T = ()` lets marker modes write
240/// `LifecycleFuture<'_>` without naming the unit type.
241pub type LifecycleFuture<'a, T = ()> =
242 Pin<Box<dyn Future<Output = Result<T, ModeActivationError>> + Send + 'a>>;
243
244/// Declarative mode contract.
245///
246/// Per mode-architecture.md §5.2 + §7.1, this trait splits into
247/// three concerns:
248///
249/// 1. **Declarative methods** (`options`, `keymap`,
250/// `subscriptions`, `decorations`, `required_capabilities`,
251/// `conflicts_with`, `implies`, `completion_sources`,
252/// `mirrors_option`) return read-only data. The registry
253/// applies these to the layer stack on activation and removes
254/// them on deactivation. The mode can never leak contributions
255/// past its lifetime by construction.
256/// 2. **Lifecycle hook** ([`Mode::on_activate`]) returns an
257/// owned [`Guard`](Mode::Guard) value carrying every resource
258/// the mode allocated (subscription IDs, prior option values
259/// to restore, supervisor handles, etc.). The dispatcher
260/// stashes the Guard in a [`GuardStore`](crate::GuardStore)
261/// keyed by `(BufferId, ModeId)`.
262/// 3. **Deactivation cleanup.** There is **no `on_deactivate`**.
263/// On deactivation the dispatcher drops the stashed Guard;
264/// the Guard's `Drop` impl performs every cleanup action.
265/// This makes cleanup mandatory (compiler-enforced via
266/// Rust ownership), bug-resistant (a forgotten cleanup step
267/// becomes a compile-time leak rather than a runtime resource
268/// leak), and uniform (marker modes use `()` as Guard).
269///
270/// Validated against Zed's `Subscription` / `Task<T>` cancel-on-
271/// drop pattern and helix's Rust-ownership-based cleanup; see
272/// mode-architecture.md §7.1.
273///
274/// `Send + Sync + 'static` so a single trait object can be shared
275/// across threads (the registry runs on whatever task drives
276/// activation; subscribers can be on any task).
277///
278/// ## Lifecycle, in order
279///
280/// 1. **Registration** — [`ModeRegistry::register`](crate::ModeRegistry::register)
281/// (native, enabled) or `register_available` (plugin, disabled until
282/// the user enables it). The registry reads [`id`](Self::id),
283/// [`kind`](Self::kind), [`target_buffer_kind`](Self::target_buffer_kind),
284/// [`target_language`](Self::target_language) and
285/// [`presents_extensions`](Self::presents_extensions) once, to build its
286/// indexes. The host separately walks every registered mode once at boot
287/// to translate [`keymap`](Self::keymap) into a
288/// `KeymapLayer::MinorMode(id)` / `MajorMode(id)` layer and to register
289/// [`action_handlers`](Self::action_handlers).
290/// 2. **Activation** — per buffer, on the editor actor.
291/// `activate_major` / `activate_minor` validates (registered, right
292/// kind, [`required_capabilities`](Self::required_capabilities),
293/// [`conflicts_with`](Self::conflicts_with), [`implies`](Self::implies)
294/// registered), records the mode *and its implied cascade* in
295/// [`ActiveModes`](crate::ActiveModes) synchronously, then runs each
296/// step's [`on_activate`](Self::on_activate) in DFS order. The cascade
297/// future is polled once inline: a hook that never awaits completes
298/// before the activate call returns; the first `Pending` moves the rest
299/// onto the runtime. Success publishes `MajorEntered` /
300/// `MinorActivated`; failure publishes a
301/// [`ModeEvent::ModeActivationFailed`](crate::ModeEvent::ModeActivationFailed)
302/// and the host rolls the cascade back.
303/// 3. **While active** — the host reads the declarative methods
304/// ([`options`](Self::options), [`completion_sources`](Self::completion_sources),
305/// [`gutter_decorations`](Self::gutter_decorations) per frame,
306/// [`editable_tail`](Self::editable_tail) per edit, …). The keymap layer
307/// is scoped to buffers where the mode is active by a per-keystroke
308/// filter.
309/// 4. **Deactivation** — synchronous: the lifecycle event publishes, then
310/// the stashed Guard is dropped. Implied minors cascade-deactivate.
311///
312/// ## What an implementor must not do
313///
314/// - Keep per-buffer state on `self`. One instance serves every buffer;
315/// per-activation state goes in the Guard.
316/// - Block in `on_activate`: it may run inline on the editor actor. Do I/O
317/// with `.await` or hand it to a spawned task.
318/// - Make the declarative methods impure. Most are read once; returning a
319/// different answer later is not observed consistently.
320/// - Bind feature chords at `KeymapLayer::Builtin` or put handler bodies in
321/// the host. The mode owns its chords ([`keymap`](Self::keymap)) *and*
322/// the bodies ([`action_handlers`](Self::action_handlers) or handlers
323/// registered in `on_activate`).
324///
325/// # Examples
326///
327/// A minimal minor mode with an owned Guard, driven through registration,
328/// activation and deactivation exactly as the host drives it:
329///
330/// ```
331/// use std::sync::Arc;
332/// use std::sync::atomic::{AtomicUsize, Ordering};
333///
334/// use lattice_config::ConfigRegistry;
335/// use lattice_mode::{
336/// ActivationPolicy, ActiveModes, GuardStoreHandle, LifecycleFuture, Mode, ModeContext,
337/// ModeId, ModeKind, ModeRegistry, ServiceRegistry,
338/// };
339/// use lattice_protocol::BufferId;
340/// use lattice_runtime::EventBus;
341///
342/// /// Counts live activations; a real Guard would hold a `Subscription`,
343/// /// a restored option value, a supervisor handle, …
344/// struct CountGuard(Arc<AtomicUsize>);
345/// impl Drop for CountGuard {
346/// fn drop(&mut self) {
347/// self.0.fetch_sub(1, Ordering::SeqCst);
348/// }
349/// }
350///
351/// struct TrailingSpaceMode {
352/// live: Arc<AtomicUsize>,
353/// }
354///
355/// impl Mode for TrailingSpaceMode {
356/// type Guard = CountGuard;
357/// fn id(&self) -> ModeId {
358/// ModeId::new("trailing-space-mode") // must end in `-mode`
359/// }
360/// fn kind(&self) -> ModeKind {
361/// ModeKind::Minor
362/// }
363/// fn activation_policy(&self) -> ActivationPolicy {
364/// ActivationPolicy::Global // every document buffer
365/// }
366/// fn on_activate(&self, ctx: ModeContext) -> LifecycleFuture<'_, CountGuard> {
367/// let live = self.live.clone();
368/// Box::pin(async move {
369/// let _ = ctx.buffer_id(); // per-buffer state goes in the Guard
370/// live.fetch_add(1, Ordering::SeqCst);
371/// Ok(CountGuard(live))
372/// })
373/// }
374/// }
375///
376/// let live = Arc::new(AtomicUsize::new(0));
377/// let mut registry = ModeRegistry::new();
378/// let id = registry.register(TrailingSpaceMode { live: live.clone() }).unwrap();
379///
380/// // What the host holds per editor, and per buffer.
381/// let (guards, events) = (GuardStoreHandle::new(), Arc::new(EventBus::new()));
382/// let (config, services) = (Arc::new(ConfigRegistry::new()), Arc::new(ServiceRegistry::new()));
383/// let buffer = BufferId::new(1);
384/// let mut active = ActiveModes::new();
385///
386/// registry
387/// .activate_minor(&mut active, &guards, &config, &events, &services, buffer, id, Default::default())
388/// .unwrap();
389/// // The hook never awaited, so it completed inline and its Guard is stashed.
390/// assert!(active.has_minor(id));
391/// assert!(guards.contains(buffer, id));
392/// assert_eq!(live.load(Ordering::SeqCst), 1);
393///
394/// // Deactivation drops the Guard: cleanup is the Guard's `Drop`.
395/// registry.deactivate_minor(&mut active, &guards, &events, buffer, id).unwrap();
396/// assert!(!active.has_minor(id));
397/// assert_eq!(live.load(Ordering::SeqCst), 0);
398/// ```
399pub trait Mode: Send + Sync + 'static {
400 /// Owned cleanup token returned by [`Self::on_activate`].
401 ///
402 /// The mode allocates whatever resources it needs (event
403 /// subscriptions, supervisor handles, prior option values
404 /// to restore) and packages them in a Guard struct with a
405 /// `Drop` impl that performs cleanup. Marker modes that
406 /// have no cleanup work use `()`.
407 ///
408 /// `Send + 'static` so the dispatcher can stash the Guard
409 /// in a typed-erased `Box<dyn Any + Send>` and move it
410 /// across threads if needed.
411 type Guard: Send + 'static;
412
413 /// Canonical identity. Same value every call.
414 ///
415 /// Must end in `-mode`: [`ModeRegistry::register`](crate::ModeRegistry::register)
416 /// refuses anything else with
417 /// [`RegistrationError::MissingModeSuffix`](crate::RegistrationError::MissingModeSuffix).
418 /// It is also the name users and plugins refer to the mode by
419 /// (`:describe-mode <id>`, a plugin's `enable-mode(<id>)`, the
420 /// `<id>.activation` config key) and the key of the mode's keymap layer, so
421 /// changing it is a breaking change. Convention: expose it as an
422 /// associated `fn mode_id() -> ModeId` so other code can name the mode
423 /// without an instance.
424 fn id(&self) -> ModeId;
425
426 /// Major or minor. Read at registration and on every activation
427 /// call (`activate_major` on a minor, or vice versa, is
428 /// [`ModeActivationError::WrongKind`]).
429 fn kind(&self) -> ModeKind;
430
431 /// For major modes, the [`BufferKind`] this mode is the default
432 /// major for (H.2, 2026-05-31). `ModeRegistry::register`
433 /// indexes this so
434 /// [`ModeRegistry::find_major_for_kind`](crate::ModeRegistry::find_major_for_kind) can
435 /// dispatch buffer-creation events to the right major without
436 /// host-side `match BufferKind { ... }` blocks.
437 ///
438 /// Returns `None` for:
439 /// - All minor modes.
440 /// - Major modes that don't bind to a [`BufferKind`] directly
441 /// (e.g. language majors like `rust-mode` / `markdown-mode`
442 /// on plain Documents — they activate via `Lang` detection
443 /// on [`BufferKind::Document`], not via kind dispatch).
444 ///
445 /// One [`BufferKind`] is owned by at most one major; the
446 /// registry treats the first registration as authoritative
447 /// and warns on subsequent claims (clobbering is a
448 /// developer bug, not an extensibility seam).
449 ///
450 /// Note: a single major may be referenced by *both* a kind and
451 /// a `Lang` (e.g. `markdown-mode` is the major for
452 /// [`BufferKind::Help`] and also the language major for
453 /// [`BufferKind::Document`] + `Lang::Markdown`). Declaring
454 /// `target_buffer_kind = Some(Help)` does not exclude the
455 /// `Lang`-detected dispatch path — they cohabit.
456 fn target_buffer_kind(&self) -> Option<BufferKind> {
457 None
458 }
459
460 /// For major modes, the **language** this mode is the
461 /// default major for, by canonical name (`Lang::name()` —
462 /// `"rust"`, `"org"`) (OM.1). The peer of
463 /// [`target_buffer_kind`](Self::target_buffer_kind) for the
464 /// dispatch path [`BufferKind::Document`] takes: language
465 /// detection rather than kind dispatch.
466 ///
467 /// `ModeRegistry::register` indexes this so
468 /// [`ModeRegistry::find_major_for_lang`](crate::ModeRegistry::find_major_for_lang) can resolve a
469 /// document's major without a host-side `match Lang { ... }`,
470 /// which is what makes a **plugin-contributed** language's
471 /// major possible at all: `Lang::Plugin(_)` has no arm in the
472 /// host's hand-written table and never will, because the host
473 /// does not know the language exists until a plugin says so.
474 ///
475 /// Returns `None` for:
476 /// - All minor modes. A minor declaring one is ignored at
477 /// register-time rather than indexed — resolving a minor as
478 /// a buffer's major would corrupt activation.
479 /// - Major modes not bound to a language (kind-bound majors
480 /// like `file-tree-mode`, or manual-only majors).
481 ///
482 /// One language is owned by at most one major; first
483 /// registration wins and later claims warn, matching
484 /// `target_buffer_kind`.
485 ///
486 /// The built-in language majors (`rust-mode`, `markdown-mode`,
487 /// …) do **not** declare this yet — they resolve through
488 /// `lattice_syntax::major_mode_id_for_lang`'s table, which is
489 /// consulted first. Migrating them onto this index would
490 /// collapse that table, and is deliberately left as separate
491 /// work: this slice makes plugin languages reachable, it does
492 /// not rewrite how the built-ins resolve.
493 fn target_language(&self) -> Option<&str> {
494 None
495 }
496
497 /// Option overrides this mode contributes. Pure declarative
498 /// (same return value every call); the host merges these
499 /// into the buffer's option-resolution layer stack while the mode
500 /// is active, at the mode's layer (later-activated minors win ties),
501 /// and drops them on deactivation. Build with
502 /// `lattice_config::overrides! { lattice_config::Wrap = true, … }`.
503 fn options(&self) -> OptionOverrideSet {
504 OptionOverrideSet::default()
505 }
506
507 /// Keymap chord → command additions / overrides.
508 ///
509 /// Read once when the mode is registered; the host translates every
510 /// binding and entry into the layer `KeymapLayer::MinorMode(id)` (or
511 /// `MajorMode(id)`), and a per-keystroke filter makes that layer live
512 /// only in buffers where this mode is active. A mode cannot place a
513 /// binding in any other layer. Table-form entries name commands by
514 /// string and are resolved against the command registry at that point
515 /// (an unknown name is logged and skipped).
516 ///
517 /// # Examples
518 ///
519 /// ```
520 /// use std::sync::LazyLock;
521 /// use lattice_mode::{
522 /// keymap_entry, Keymap, KeymapEntry, LifecycleFuture, Mode, ModeContext, ModeId,
523 /// ModeKind,
524 /// };
525 ///
526 /// static PREVIEW_KEYS: LazyLock<Vec<KeymapEntry>> = LazyLock::new(|| {
527 /// vec![keymap_entry! { mode: Normal, chord: "q", doc: "Close the preview", cmd: "preview:close" }]
528 /// });
529 ///
530 /// struct PreviewMode;
531 /// impl Mode for PreviewMode {
532 /// type Guard = ();
533 /// fn id(&self) -> ModeId { ModeId::new("preview-mode") }
534 /// fn kind(&self) -> ModeKind { ModeKind::Minor }
535 /// fn keymap(&self) -> Keymap { Keymap::from_entries(PREVIEW_KEYS.as_slice()) }
536 /// fn on_activate(&self, _ctx: ModeContext) -> LifecycleFuture<'_, ()> {
537 /// Box::pin(async { Ok(()) })
538 /// }
539 /// }
540 ///
541 /// let km = PreviewMode.keymap();
542 /// assert_eq!(km.entries.len(), 1);
543 /// assert_eq!(km.entries[0].doc, "Close the preview");
544 /// ```
545 fn keymap(&self) -> Keymap {
546 Keymap::default()
547 }
548
549 /// Decoration providers (gutter / inline / overlay /
550 /// statusline). Stub — reserved for the WIT plugin path (M.10).
551 fn decorations(&self) -> Vec<DecorationProvider> {
552 Vec::new()
553 }
554
555 /// Gutter sign decorations this mode contributes while active.
556 /// Called once per pane per frame with a [`DecorationCtx`]
557 /// carrying relevant render-state snapshots (diff sign map, LSP
558 /// diagnostics arc). Returns per-line `GutterDecoration` values;
559 /// the renderer partitions them by variant into the appropriate
560 /// gutter column. Default: empty (no contribution).
561 fn gutter_decorations(&self, _ctx: &DecorationCtx<'_>) -> Vec<GutterDecoration> {
562 Vec::new()
563 }
564
565 // ML.3: `status_line_items` retired. Modes contribute modeline
566 // content as registered elements pushed over the event bus
567 // (`lattice_mode::ModelineElementUpdate`, see modeline.rs §6), not via
568 // a render-path trait pull — a Rust trait can't cross the WASM plugin
569 // boundary, which is exactly the limitation the element model removes.
570
571 /// Insert-mode completion sources this mode contributes while
572 /// active on a buffer. Empty by default; minors that own a
573 /// completion source (`lsp-completion-mode`,
574 /// `snippet-completion-mode`, `buffer-words-mode`,
575 /// `tree-sitter-completion-mode`, `path-completion-mode`,
576 /// plugin sources) override.
577 fn completion_sources(&self) -> Vec<lattice_completion::CompletionSourceContribution> {
578 Vec::new()
579 }
580
581 /// *Global* (buffer-agnostic) action handlers this
582 /// mode contributes (SN.3c.0). The host walks every registered mode's
583 /// `action_handlers()` once at boot, resolves each
584 /// `action_name` → `CommandId`, registers the handler in the
585 /// `ActionHandlerRegistry`, and holds the tokens for the app's
586 /// lifetime. Use this for handlers that read the active
587 /// buffer / cursor / services from the `ActionContext` at call
588 /// time and close over no per-buffer state (e.g. snippet
589 /// expand). Per-buffer, session-scoped handlers register in
590 /// [`on_activate`](Self::on_activate) instead, so their tokens
591 /// drop with the Guard. Default: none. See
592 /// `feedback_effect_vocabulary_is_host_boundary`.
593 fn action_handlers(&self) -> Vec<ActionHandlerContribution> {
594 Vec::new()
595 }
596
597 /// Capabilities the mode requires. Validated at activation;
598 /// missing capability ⇒
599 /// [`ModeActivationError::MissingCapability`], never silent
600 /// skip.
601 fn required_capabilities(&self) -> CapabilitySet {
602 CapabilitySet::empty()
603 }
604
605 /// Modes this one cannot be active alongside.
606 ///
607 /// Checked symmetrically when a **minor** is activated (directly or
608 /// through an `implies` cascade): activation fails with
609 /// [`ModeActivationError::Conflict`] if any mode listed here is active,
610 /// or if the active major or any active minor lists this mode. Nothing
611 /// is auto-deactivated — the caller decides whether to deactivate the
612 /// other mode and retry. `activate_major` does not consult this list.
613 fn conflicts_with(&self) -> &[ModeId] {
614 &[]
615 }
616
617 /// Minor modes activating this mode also activates.
618 /// Used by `relative-line-numbers-mode` ⇒ `line-numbers-mode`, and by
619 /// read-only majors ⇒ `read-only-mode`.
620 ///
621 /// Every id must be registered, or activation fails with
622 /// [`ModeActivationError::UnregisteredDependency`]. The whole tree is
623 /// validated and recorded before any hook runs; hooks then run parent
624 /// first, depth-first. Deactivating a minor cascade-deactivates the
625 /// minors it implied (deactivating a major does not).
626 fn implies(&self) -> &[ModeId] {
627 &[]
628 }
629
630 /// File extensions (lowercase, no dot) this major PRESENTS rather than
631 /// edits — the file is never loaded as text.
632 ///
633 /// The open path consults this before reading: a match builds a
634 /// placeholder buffer with the real path and no content, and this major
635 /// renders the file some other way. `image-mode` shows it as a media
636 /// block; a future PDF or archive viewer is the same shape.
637 ///
638 /// Default empty, which is every ordinary major: its buffer IS the file's
639 /// text.
640 ///
641 /// A mode declaring this **must** also be read-only in both of the ways
642 /// that matter — `ReadOnly = true` in [`options`](Self::options) AND
643 /// `read-only-mode` in [`implies`](Self::implies) — because the buffer's
644 /// text is a placeholder and saving it would overwrite the real file with
645 /// nothing. The option alone gates typing; the implied mode is what
646 /// refuses the operators.
647 fn presents_extensions(&self) -> &[&'static str] {
648 &[]
649 }
650
651 /// Declarative mirror hint for "this mode is the on/off
652 /// switch for a typed option of the same observable state".
653 /// `Some(canonical_name)` ⇒ a host-driven cascade keeps the
654 /// mode's active state and the option's value in sync.
655 fn mirrors_option(&self) -> Option<&'static str> {
656 None
657 }
658
659 /// Invocation-runner discovery (2026-05-26). Modes that own
660 /// command-invocation dispatch for their buffer kind
661 /// (terminal-mode, oil-mode, file-tree-mode, help-mode, …)
662 /// return their canonical [`ModeId`]; the host registers a
663 /// runner function under that id at boot, and
664 /// `Editor::run_invocation` looks it up by walking the
665 /// active modes on the active pane's buffer (minors first,
666 /// then major) before falling back to the central grammar
667 /// Action gate.
668 ///
669 /// Returning `None` (the default) means the mode doesn't
670 /// claim invocation dispatch — the keymap / decorations /
671 /// completion-source contributions still apply.
672 ///
673 /// Replaces the hardcoded `match BufferKind` block that
674 /// previously lived in `Editor::run_invocation`. Plugin-
675 /// installed modes for plugin-installed buffer kinds now
676 /// extend the dispatcher without touching host code.
677 fn invocation_runner(&self) -> Option<ModeId> {
678 None
679 }
680
681 /// Which of *this mode's own actions* refreshes
682 /// its view, or `None` (the default) when the mode backs nothing
683 /// refreshable (RV.1, 2026-08-10).
684 ///
685 /// `gr` means "refresh this view" in every synthetic buffer. That
686 /// is a property of synthetic views as a class, so the chord lives
687 /// once on [`RefreshableViewMode`](crate::RefreshableViewMode) —
688 /// **not** re-declared per mode. Before RV.1 it was re-declared per
689 /// mode, and the two views that landed most recently (`*problems*`,
690 /// narrow) had no `gr` at all: a gap in a copied set does not
691 /// announce itself.
692 ///
693 /// This declares a **target, not a body**. The handler stays exactly
694 /// where [`action_handlers`](Self::action_handlers) puts it — a mode
695 /// returning `Some("action:magit-refresh")` keeps the closure it
696 /// already registered under that name. Declaring a target rather
697 /// than doing the work is the same shape
698 /// [`invocation_runner`](Self::invocation_runner) and
699 /// [`mirrors_option`](Self::mirrors_option) already have; a
700 /// `refresh(&self, ctx)` doing the work would give modes two ways to
701 /// express one body.
702 ///
703 /// Returning `Some` also **auto-activates** `refreshable-view-mode`
704 /// through the implies cascade, so a mode author writes one line and
705 /// gets the chord.
706 ///
707 /// The host resolves this by walking the buffer's active modes
708 /// (minors most-recently-activated first, then major) — see
709 /// `Editor::resolve_refresh_action`. When no active mode declares
710 /// one, the chord echoes `nothing to refresh here` rather than being
711 /// swallowed, so the absence is spoken.
712 ///
713 /// See `docs/dev/architecture/mode-architecture.md` §5.5.
714 fn refresh_action(&self) -> Option<&'static str> {
715 None
716 }
717
718 /// Which of this mode's actions `<Tab>` fires, and the declaration that
719 /// pulls in
720 /// [`foldable-view-mode`](crate::foldable_view_mode::FoldableViewMode) —
721 /// the shared minor owning the chord.
722 ///
723 /// `Some(FOLD_TOGGLE_DEFAULT_ACTION)` for the ordinary "cycle the fold at
724 /// the cursor"; `Some(own_action)` for a view with a real specialisation
725 /// (magit expands a diff on the first press over a status file line).
726 ///
727 /// `None` — the default — means this mode's buffers keep `<Tab>` as
728 /// jump-list-forward, which is what every ordinary document wants.
729 fn fold_toggle_action(&self) -> Option<&'static str> {
730 None
731 }
732
733 /// Should re-opening this view **re-run its refresh**?
734 ///
735 /// A synthetic buffer is created once and reused: the host's
736 /// `ensure_named_synthetic_document` returns the existing buffer by
737 /// name, so a mode's `on_activate` — which is what fills the buffer
738 /// — runs on the FIRST open only. For a view whose content is a
739 /// snapshot of external state, that makes every later open a time
740 /// capsule: `C-x g` on an already-open `*magit:status*` showed the
741 /// repository as it was when the buffer was first created, with
742 /// nothing on screen saying so.
743 ///
744 /// Returning `true` makes the host dispatch this mode's declared
745 /// [`refresh_action`](Self::refresh_action) after an open that
746 /// **reused** an existing buffer. First opens are untouched:
747 /// `on_activate` has just built the content, and refreshing again
748 /// would be a second scan for the same answer.
749 ///
750 /// **Opt-in, and only for content derived from outside the editor.**
751 /// A view whose content is authored in the editor (a help page, a
752 /// transcript, `*messages*`) has nothing to re-derive, and refreshing
753 /// it would discard scroll position for no gain.
754 ///
755 /// **The contract on the body:** a refresh reached this way must be
756 /// self-contained — spawn its own work and return no `Effect`. This
757 /// path has no dispatch outcome to route renderer-coupled effects
758 /// through (`OpenBuffer`, `OpenPicker`, …), so a returned effect is
759 /// logged as a wiring error rather than half-applied. Magit's
760 /// refresh satisfies this: it spawns the git work and returns
761 /// `None`.
762 ///
763 /// Declaring `true` without a `refresh_action` does nothing; the two
764 /// are read together.
765 fn refresh_on_open(&self) -> bool {
766 false
767 }
768
769 /// A *minor* mode's default auto-activation policy
770 /// (MA.1; mode-architecture.md §7.4). The host's minor-activation
771 /// resolver reads this for every registered minor when a buffer
772 /// enters a major mode, and activates those whose policy
773 /// [`admits`](ActivationPolicy::admits) the entered major. The
774 /// default is [`ActivationPolicy::Manual`] — auto-activate
775 /// nowhere until the mode or the user opts in. Ignored for major
776 /// modes (a buffer's major is chosen by the major resolver, not
777 /// this allowlist).
778 fn activation_policy(&self) -> ActivationPolicy {
779 ActivationPolicy::Manual
780 }
781
782 /// The mode's editable tail on an otherwise read-only buffer, or
783 /// `None` (the default) for a fully read-only / fully writable buffer
784 /// (AU‑3).
785 ///
786 /// A mode backing an owner-written buffer (the agent conversation,
787 /// future REPL / scratch buffers) declares a tail so the host's
788 /// read-only edit gate lets user keystrokes edit only the trailing
789 /// prompt region — the comint pattern. Consulted directly by the gate
790 /// (no per-buffer seeding): the tail is expressed relative to the buffer
791 /// end (see [`EditableTail`]), so it stays valid as the owner appends
792 /// content above it. Returning `None` leaves the read-only gate's
793 /// behaviour unchanged (edits rejected iff `ReadOnly` is resolved true).
794 fn editable_tail(&self) -> Option<EditableTail> {
795 None
796 }
797
798 /// Lifecycle. Called once per (buffer, activation) cycle
799 /// after the registry has applied the declarative
800 /// contributions. Returns an owned [`Guard`](Self::Guard)
801 /// carrying every resource the mode allocated. The
802 /// dispatcher stashes the Guard until deactivation, at which
803 /// point dropping it performs cleanup via the Guard's `Drop`
804 /// impl.
805 ///
806 /// Marker modes whose `Guard = ()` typically write:
807 ///
808 /// ```
809 /// # use lattice_mode::{LifecycleFuture, Mode, ModeContext, ModeId, ModeKind};
810 /// # struct MarkerMode;
811 /// # impl Mode for MarkerMode {
812 /// # fn id(&self) -> ModeId { ModeId::new("marker-mode") }
813 /// # fn kind(&self) -> ModeKind { ModeKind::Minor }
814 /// type Guard = ();
815 /// fn on_activate(&self, _ctx: ModeContext) -> LifecycleFuture<'_, ()> {
816 /// Box::pin(async { Ok(()) })
817 /// }
818 /// # }
819 /// ```
820 ///
821 /// **Where it runs.** On the editor actor, polled once inline; if it
822 /// returns `Pending` the remainder continues as a runtime task. Awaiting
823 /// real I/O is therefore fine; blocking is not. Within one cascade,
824 /// steps run strictly in order, so an implied child's hook never
825 /// observes its parent's half-built state.
826 ///
827 /// **Late results.** If the mode is deactivated (or re-activated)
828 /// while this future is still pending, the Guard it eventually returns
829 /// is dropped immediately instead of stashed — so the Guard's `Drop`
830 /// must be correct even for an activation nobody observed.
831 ///
832 /// **What `ctx` gives you:** the buffer id, typed services
833 /// ([`ModeContext::service`]), the config registry and the event bus —
834 /// not the buffer's text. A mode that needs to create or fill a
835 /// synthetic buffer reaches the host through a service
836 /// ([`BufferStoreHandle`](crate::BufferStoreHandle),
837 /// [`ModeActivator`](crate::ModeActivator)); asynchronous results must
838 /// reach the screen through an inbound channel
839 /// ([`inbound`](crate::inbound)), which wakes the editor, not a bare
840 /// tick callback.
841 ///
842 /// Stateful modes return a Guard struct whose `Drop` impl
843 /// performs cleanup (unsubscribe, restore prior option,
844 /// drop supervisor handle, etc.).
845 ///
846 /// Errors propagate as [`ModeActivationError`]; do not panic.
847 ///
848 /// Idempotent setup contract: `on_activate` may run more
849 /// than once in a buffer's lifetime (each preceded by a
850 /// Guard-drop if previously active). Implementations must
851 /// produce a fresh Guard every time.
852 fn on_activate(&self, ctx: ModeContext) -> LifecycleFuture<'_, Self::Guard>;
853}
854
855/// Object-safe adapter for `Mode`. The registry stores modes
856/// as `Arc<dyn DynMode>`; the blanket impl below box-erases
857/// each `Mode`'s typed `Guard` into `Box<dyn Any + Send>` so
858/// the dispatcher can stash heterogeneous Guards in a single
859/// [`GuardStore`](crate::GuardStore) and drop them on
860/// deactivation.
861///
862/// Public (not sealed): the trait is implemented automatically
863/// for every `Mode`; consumers never implement `DynMode`
864/// directly. Exposed in `pub` form because the registry's
865/// public API (`Arc<dyn DynMode>`) leaks it.
866pub trait DynMode: Send + Sync + 'static {
867 /// Forwards to [`Mode::id`].
868 fn id(&self) -> ModeId;
869 /// Forwards to [`Mode::kind`].
870 fn kind(&self) -> ModeKind;
871 /// Forwards to [`Mode::target_buffer_kind`].
872 fn target_buffer_kind(&self) -> Option<BufferKind>;
873 /// Forwards to [`Mode::target_language`].
874 fn target_language(&self) -> Option<&str>;
875 /// Forwards to [`Mode::options`].
876 fn options(&self) -> OptionOverrideSet;
877 /// Forwards to [`Mode::keymap`].
878 fn keymap(&self) -> Keymap;
879 /// Forwards to [`Mode::decorations`].
880 fn decorations(&self) -> Vec<DecorationProvider>;
881 /// Forwards to [`Mode::gutter_decorations`].
882 fn gutter_decorations(&self, ctx: &DecorationCtx<'_>) -> Vec<GutterDecoration>;
883 /// Forwards to [`Mode::completion_sources`].
884 fn completion_sources(&self) -> Vec<lattice_completion::CompletionSourceContribution>;
885 /// Forwards to [`Mode::action_handlers`].
886 fn action_handlers(&self) -> Vec<ActionHandlerContribution>;
887 /// Forwards to [`Mode::required_capabilities`].
888 fn required_capabilities(&self) -> CapabilitySet;
889 /// Forwards to [`Mode::conflicts_with`].
890 fn conflicts_with(&self) -> &[ModeId];
891 /// Forwards to [`Mode::implies`].
892 fn implies(&self) -> &[ModeId];
893 /// Forwards to [`Mode::presents_extensions`].
894 fn presents_extensions(&self) -> &[&'static str];
895 /// Forwards to [`Mode::mirrors_option`].
896 fn mirrors_option(&self) -> Option<&'static str>;
897 /// Forwards to [`Mode::invocation_runner`].
898 fn invocation_runner(&self) -> Option<ModeId>;
899 /// Forwards to [`Mode::refresh_action`].
900 fn refresh_action(&self) -> Option<&'static str>;
901 /// Forwards to [`Mode::fold_toggle_action`].
902 fn fold_toggle_action(&self) -> Option<&'static str>;
903 /// Forwards to [`Mode::refresh_on_open`].
904 fn refresh_on_open(&self) -> bool;
905 /// Forwards to [`Mode::activation_policy`].
906 fn activation_policy(&self) -> ActivationPolicy;
907 /// Forwards to [`Mode::editable_tail`].
908 fn editable_tail(&self) -> Option<EditableTail>;
909
910 /// Type-erased lifecycle entry. Returns a future whose
911 /// output is the typed Guard erased to `Box<dyn Any + Send>`.
912 /// The dispatcher stashes this box keyed by
913 /// `(BufferId, ModeId)`; deactivation drops it.
914 fn on_activate_dyn<'a>(
915 &'a self,
916 ctx: ModeContext,
917 ) -> Pin<Box<dyn Future<Output = Result<Box<dyn Any + Send>, ModeActivationError>> + Send + 'a>>;
918}
919
920impl<M: Mode> DynMode for M {
921 fn id(&self) -> ModeId {
922 <M as Mode>::id(self)
923 }
924 fn kind(&self) -> ModeKind {
925 <M as Mode>::kind(self)
926 }
927 fn target_buffer_kind(&self) -> Option<BufferKind> {
928 <M as Mode>::target_buffer_kind(self)
929 }
930 fn target_language(&self) -> Option<&str> {
931 <M as Mode>::target_language(self)
932 }
933 fn options(&self) -> OptionOverrideSet {
934 <M as Mode>::options(self)
935 }
936 fn keymap(&self) -> Keymap {
937 <M as Mode>::keymap(self)
938 }
939 fn decorations(&self) -> Vec<DecorationProvider> {
940 <M as Mode>::decorations(self)
941 }
942 fn gutter_decorations(&self, ctx: &DecorationCtx<'_>) -> Vec<GutterDecoration> {
943 <M as Mode>::gutter_decorations(self, ctx)
944 }
945 fn completion_sources(&self) -> Vec<lattice_completion::CompletionSourceContribution> {
946 <M as Mode>::completion_sources(self)
947 }
948 fn action_handlers(&self) -> Vec<ActionHandlerContribution> {
949 <M as Mode>::action_handlers(self)
950 }
951 fn required_capabilities(&self) -> CapabilitySet {
952 <M as Mode>::required_capabilities(self)
953 }
954 fn conflicts_with(&self) -> &[ModeId] {
955 <M as Mode>::conflicts_with(self)
956 }
957 fn implies(&self) -> &[ModeId] {
958 <M as Mode>::implies(self)
959 }
960 fn presents_extensions(&self) -> &[&'static str] {
961 <M as Mode>::presents_extensions(self)
962 }
963 fn mirrors_option(&self) -> Option<&'static str> {
964 <M as Mode>::mirrors_option(self)
965 }
966 fn invocation_runner(&self) -> Option<ModeId> {
967 <M as Mode>::invocation_runner(self)
968 }
969 fn refresh_action(&self) -> Option<&'static str> {
970 <M as Mode>::refresh_action(self)
971 }
972 fn fold_toggle_action(&self) -> Option<&'static str> {
973 <M as Mode>::fold_toggle_action(self)
974 }
975 fn refresh_on_open(&self) -> bool {
976 <M as Mode>::refresh_on_open(self)
977 }
978 fn activation_policy(&self) -> ActivationPolicy {
979 <M as Mode>::activation_policy(self)
980 }
981 fn editable_tail(&self) -> Option<EditableTail> {
982 <M as Mode>::editable_tail(self)
983 }
984
985 fn on_activate_dyn<'a>(
986 &'a self,
987 ctx: ModeContext,
988 ) -> Pin<Box<dyn Future<Output = Result<Box<dyn Any + Send>, ModeActivationError>> + Send + 'a>>
989 {
990 let fut = <M as Mode>::on_activate(self, ctx);
991 Box::pin(async move {
992 let guard = fut.await?;
993 Ok(Box::new(guard) as Box<dyn Any + Send>)
994 })
995 }
996}
997
998#[cfg(test)]
999mod tests {
1000 #![allow(clippy::unwrap_used)]
1001 use super::*;
1002
1003 /// AU‑3: the default `editable_tail()` is `None` (unchanged
1004 /// read-only semantics for every existing mode).
1005 #[test]
1006 fn editable_tail_defaults_to_none() {
1007 struct BareMode;
1008 impl Mode for BareMode {
1009 type Guard = ();
1010 fn id(&self) -> ModeId {
1011 ModeId::new("bare-mode")
1012 }
1013 fn kind(&self) -> ModeKind {
1014 ModeKind::Minor
1015 }
1016 fn on_activate(&self, _ctx: ModeContext) -> LifecycleFuture<'_, ()> {
1017 Box::pin(async { Ok(()) })
1018 }
1019 }
1020 assert_eq!(<BareMode as Mode>::editable_tail(&BareMode), None);
1021 }
1022
1023 /// AU‑3: a single-line prompt tail (`> ` marker ⇒ min byte 2) permits
1024 /// edits on the last line at/after column 2 and rejects everything above
1025 /// it or before the marker — computed against the live line count, so it
1026 /// tracks the prompt as the transcript grows.
1027 #[test]
1028 fn editable_tail_permits_prompt_and_rejects_history() {
1029 let tail = EditableTail {
1030 trailing_lines: 1,
1031 first_line_min_byte: 2,
1032 first_editable_line: None,
1033 };
1034 // 5-line buffer: prompt is line 4 (`line_count - 1`).
1035 // In the prompt, at/after the marker → allowed.
1036 assert!(tail.permits(4, 2, 5));
1037 assert!(tail.permits(4, 7, 5));
1038 // In the prompt but inside the `> ` marker → rejected.
1039 assert!(!tail.permits(4, 0, 5));
1040 assert!(!tail.permits(4, 1, 5));
1041 // Any history line → rejected.
1042 assert!(!tail.permits(0, 0, 5));
1043 assert!(!tail.permits(3, 9, 5));
1044 // Grow the transcript: prompt is now line 9; the same rule tracks it.
1045 assert!(tail.permits(9, 2, 10));
1046 assert!(!tail.permits(4, 2, 10));
1047 }
1048
1049 /// AU‑3+ (`<C-j>` multi-line prompt): an absolute anchor makes the region
1050 /// `anchor..EOF` regardless of the tail's height, so a growing multi-line
1051 /// prompt stays fully editable while everything above the anchor is frozen.
1052 #[test]
1053 fn anchored_editable_tail_covers_a_multiline_prompt() {
1054 // Transcript ends at line 3; the prompt is lines 3.. (marker on line 3).
1055 let tail = EditableTail {
1056 trailing_lines: 1,
1057 first_line_min_byte: 2,
1058 first_editable_line: Some(3),
1059 };
1060 // Marker line: at/after column 2 allowed, inside the marker rejected.
1061 assert!(tail.permits(3, 2, 6));
1062 assert!(!tail.permits(3, 0, 6));
1063 // Continuation prompt lines (added via `<C-j>`) are fully editable,
1064 // including column 0 (no marker there) — this is what a 1-line tail
1065 // could not express.
1066 assert!(tail.permits(4, 0, 6));
1067 assert!(tail.permits(5, 0, 6));
1068 // Transcript lines above the anchor stay frozen.
1069 assert!(!tail.permits(2, 0, 6));
1070 assert!(!tail.permits(0, 0, 6));
1071 // A stale-high anchor clamps to the last line rather than freezing all.
1072 let stale = EditableTail {
1073 trailing_lines: 1,
1074 first_line_min_byte: 2,
1075 first_editable_line: Some(99),
1076 };
1077 assert!(stale.permits(5, 2, 6));
1078 }
1079
1080 /// AU‑3: an empty tail (`trailing_lines = 0`, the `Default`) permits
1081 /// nothing — a fully read-only buffer.
1082 #[test]
1083 fn empty_editable_tail_permits_nothing() {
1084 let tail = EditableTail::default();
1085 assert!(!tail.permits(0, 0, 3));
1086 assert!(!tail.permits(2, 5, 3));
1087 }
1088
1089 /// A bare `Mode` impl with `Guard = ()` and a trivial
1090 /// `on_activate`. Confirms `completion_sources()` defaults
1091 /// to empty.
1092 #[test]
1093 fn completion_sources_defaults_to_empty() {
1094 struct BareMode;
1095 impl Mode for BareMode {
1096 type Guard = ();
1097 fn id(&self) -> ModeId {
1098 ModeId::new("bare-mode")
1099 }
1100 fn kind(&self) -> ModeKind {
1101 ModeKind::Minor
1102 }
1103 fn on_activate(&self, _ctx: ModeContext) -> LifecycleFuture<'_, ()> {
1104 Box::pin(async { Ok(()) })
1105 }
1106 }
1107 assert!(<BareMode as Mode>::completion_sources(&BareMode).is_empty());
1108 }
1109
1110 /// A mode that DOES contribute a source returns it through
1111 /// the new trait method.
1112 #[test]
1113 fn mode_can_contribute_a_completion_source() {
1114 use lattice_completion::{
1115 CompletionSourceContribution, CompletionSourceKind, RawCandidate, SyncCompletionSource,
1116 candidate::CandidateKind,
1117 };
1118 use std::sync::Arc;
1119
1120 #[derive(Debug)]
1121 struct StubSource;
1122 impl SyncCompletionSource for StubSource {
1123 fn produce(&self, _ctx: &lattice_completion::InsertContext<'_>) -> Vec<RawCandidate> {
1124 vec![RawCandidate::plain("stub", CandidateKind::Plain)]
1125 }
1126 }
1127 struct StubMode;
1128 impl Mode for StubMode {
1129 type Guard = ();
1130 fn id(&self) -> ModeId {
1131 ModeId::new("stub-mode")
1132 }
1133 fn kind(&self) -> ModeKind {
1134 ModeKind::Minor
1135 }
1136 fn completion_sources(&self) -> Vec<CompletionSourceContribution> {
1137 vec![CompletionSourceContribution {
1138 accepts_non_word_query: false,
1139 id: lattice_completion::SourceId::new("gen:stub"),
1140 default_priority: 100,
1141 auto_trigger: true,
1142 trigger_chars: Vec::new(),
1143 popup_filter_chord: None,
1144 kind: CompletionSourceKind::Sync(Arc::new(StubSource)),
1145 }]
1146 }
1147 fn on_activate(&self, _ctx: ModeContext) -> LifecycleFuture<'_, ()> {
1148 Box::pin(async { Ok(()) })
1149 }
1150 }
1151 let sources = <StubMode as Mode>::completion_sources(&StubMode);
1152 assert_eq!(sources.len(), 1);
1153 assert_eq!(sources[0].id.as_str(), "gen:stub");
1154 assert_eq!(sources[0].kind.kind_label(), "sync");
1155 }
1156}