Skip to main content

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}