Skip to main content

Mode

Trait Mode 

Source
pub trait Mode:
    Send
    + Sync
    + 'static {
    type Guard: Send + 'static;

Show 22 methods // Required methods fn id(&self) -> ModeId; fn kind(&self) -> ModeKind; fn on_activate(&self, ctx: ModeContext) -> LifecycleFuture<'_, Self::Guard>; // Provided methods fn target_buffer_kind(&self) -> Option<BufferKind> { ... } fn target_language(&self) -> Option<&str> { ... } fn options(&self) -> OptionOverrideSet { ... } fn keymap(&self) -> Keymap { ... } fn decorations(&self) -> Vec<DecorationProvider> { ... } fn gutter_decorations( &self, _ctx: &DecorationCtx<'_>, ) -> Vec<GutterDecoration> { ... } fn completion_sources(&self) -> Vec<CompletionSourceContribution> { ... } fn action_handlers(&self) -> Vec<ActionHandlerContribution> { ... } fn required_capabilities(&self) -> CapabilitySet { ... } fn conflicts_with(&self) -> &[ModeId] { ... } fn implies(&self) -> &[ModeId] { ... } fn presents_extensions(&self) -> &[&'static str] { ... } fn mirrors_option(&self) -> Option<&'static str> { ... } fn invocation_runner(&self) -> Option<ModeId> { ... } fn refresh_action(&self) -> Option<&'static str> { ... } fn fold_toggle_action(&self) -> Option<&'static str> { ... } fn refresh_on_open(&self) -> bool { ... } fn activation_policy(&self) -> ActivationPolicy { ... } fn editable_tail(&self) -> Option<EditableTail> { ... }
}
Expand description

Declarative mode contract.

Per mode-architecture.md §5.2 + §7.1, this trait splits into three concerns:

  1. Declarative methods (options, keymap, subscriptions, decorations, required_capabilities, conflicts_with, implies, completion_sources, mirrors_option) return read-only data. The registry applies these to the layer stack on activation and removes them on deactivation. The mode can never leak contributions past its lifetime by construction.
  2. Lifecycle hook (Mode::on_activate) returns an owned Guard value carrying every resource the mode allocated (subscription IDs, prior option values to restore, supervisor handles, etc.). The dispatcher stashes the Guard in a GuardStore keyed by (BufferId, ModeId).
  3. Deactivation cleanup. There is no on_deactivate. On deactivation the dispatcher drops the stashed Guard; the Guard’s Drop impl performs every cleanup action. This makes cleanup mandatory (compiler-enforced via Rust ownership), bug-resistant (a forgotten cleanup step becomes a compile-time leak rather than a runtime resource leak), and uniform (marker modes use () as Guard).

Validated against Zed’s Subscription / Task<T> cancel-on- drop pattern and helix’s Rust-ownership-based cleanup; see mode-architecture.md §7.1.

Send + Sync + 'static so a single trait object can be shared across threads (the registry runs on whatever task drives activation; subscribers can be on any task).

§Lifecycle, in order

  1. Registration — ModeRegistry::register (native, enabled) or register_available (plugin, disabled until the user enables it). The registry reads id, kind, target_buffer_kind, target_language and presents_extensions once, to build its indexes. The host separately walks every registered mode once at boot to translate keymap into a KeymapLayer::MinorMode(id) / MajorMode(id) layer and to register action_handlers.
  2. Activation — per buffer, on the editor actor. activate_major / activate_minor validates (registered, right kind, required_capabilities, conflicts_with, implies registered), records the mode and its implied cascade in ActiveModes synchronously, then runs each step’s on_activate in DFS order. The cascade future is polled once inline: a hook that never awaits completes before the activate call returns; the first Pending moves the rest onto the runtime. Success publishes MajorEntered / MinorActivated; failure publishes a ModeEvent::ModeActivationFailed and the host rolls the cascade back.
  3. While active — the host reads the declarative methods (options, completion_sources, gutter_decorations per frame, editable_tail per edit, …). The keymap layer is scoped to buffers where the mode is active by a per-keystroke filter.
  4. Deactivation — synchronous: the lifecycle event publishes, then the stashed Guard is dropped. Implied minors cascade-deactivate.

§What an implementor must not do

  • Keep per-buffer state on self. One instance serves every buffer; per-activation state goes in the Guard.
  • Block in on_activate: it may run inline on the editor actor. Do I/O with .await or hand it to a spawned task.
  • Make the declarative methods impure. Most are read once; returning a different answer later is not observed consistently.
  • Bind feature chords at KeymapLayer::Builtin or put handler bodies in the host. The mode owns its chords (keymap) and the bodies (action_handlers or handlers registered in on_activate).

§Examples

A minimal minor mode with an owned Guard, driven through registration, activation and deactivation exactly as the host drives it:

use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};

use lattice_config::ConfigRegistry;
use lattice_mode::{
    ActivationPolicy, ActiveModes, GuardStoreHandle, LifecycleFuture, Mode, ModeContext,
    ModeId, ModeKind, ModeRegistry, ServiceRegistry,
};
use lattice_protocol::BufferId;
use lattice_runtime::EventBus;

/// Counts live activations; a real Guard would hold a `Subscription`,
/// a restored option value, a supervisor handle, …
struct CountGuard(Arc<AtomicUsize>);
impl Drop for CountGuard {
    fn drop(&mut self) {
        self.0.fetch_sub(1, Ordering::SeqCst);
    }
}

struct TrailingSpaceMode {
    live: Arc<AtomicUsize>,
}

impl Mode for TrailingSpaceMode {
    type Guard = CountGuard;
    fn id(&self) -> ModeId {
        ModeId::new("trailing-space-mode") // must end in `-mode`
    }
    fn kind(&self) -> ModeKind {
        ModeKind::Minor
    }
    fn activation_policy(&self) -> ActivationPolicy {
        ActivationPolicy::Global // every document buffer
    }
    fn on_activate(&self, ctx: ModeContext) -> LifecycleFuture<'_, CountGuard> {
        let live = self.live.clone();
        Box::pin(async move {
            let _ = ctx.buffer_id(); // per-buffer state goes in the Guard
            live.fetch_add(1, Ordering::SeqCst);
            Ok(CountGuard(live))
        })
    }
}

let live = Arc::new(AtomicUsize::new(0));
let mut registry = ModeRegistry::new();
let id = registry.register(TrailingSpaceMode { live: live.clone() }).unwrap();

// What the host holds per editor, and per buffer.
let (guards, events) = (GuardStoreHandle::new(), Arc::new(EventBus::new()));
let (config, services) = (Arc::new(ConfigRegistry::new()), Arc::new(ServiceRegistry::new()));
let buffer = BufferId::new(1);
let mut active = ActiveModes::new();

registry
    .activate_minor(&mut active, &guards, &config, &events, &services, buffer, id, Default::default())
    .unwrap();
// The hook never awaited, so it completed inline and its Guard is stashed.
assert!(active.has_minor(id));
assert!(guards.contains(buffer, id));
assert_eq!(live.load(Ordering::SeqCst), 1);

// Deactivation drops the Guard: cleanup is the Guard's `Drop`.
registry.deactivate_minor(&mut active, &guards, &events, buffer, id).unwrap();
assert!(!active.has_minor(id));
assert_eq!(live.load(Ordering::SeqCst), 0);

Required Associated Types§

Source

type Guard: Send + 'static

Owned cleanup token returned by Self::on_activate.

The mode allocates whatever resources it needs (event subscriptions, supervisor handles, prior option values to restore) and packages them in a Guard struct with a Drop impl that performs cleanup. Marker modes that have no cleanup work use ().

Send + 'static so the dispatcher can stash the Guard in a typed-erased Box<dyn Any + Send> and move it across threads if needed.

Required Methods§

Source

fn id(&self) -> ModeId

Canonical identity. Same value every call.

Must end in -mode: ModeRegistry::register refuses anything else with RegistrationError::MissingModeSuffix. It is also the name users and plugins refer to the mode by (:describe-mode <id>, a plugin’s enable-mode(<id>), the <id>.activation config key) and the key of the mode’s keymap layer, so changing it is a breaking change. Convention: expose it as an associated fn mode_id() -> ModeId so other code can name the mode without an instance.

Source

fn kind(&self) -> ModeKind

Major or minor. Read at registration and on every activation call (activate_major on a minor, or vice versa, is ModeActivationError::WrongKind).

Source

fn on_activate(&self, ctx: ModeContext) -> LifecycleFuture<'_, Self::Guard>

Lifecycle. Called once per (buffer, activation) cycle after the registry has applied the declarative contributions. Returns an owned Guard carrying every resource the mode allocated. The dispatcher stashes the Guard until deactivation, at which point dropping it performs cleanup via the Guard’s Drop impl.

Marker modes whose Guard = () typically write:

type Guard = ();
fn on_activate(&self, _ctx: ModeContext) -> LifecycleFuture<'_, ()> {
    Box::pin(async { Ok(()) })
}

Where it runs. On the editor actor, polled once inline; if it returns Pending the remainder continues as a runtime task. Awaiting real I/O is therefore fine; blocking is not. Within one cascade, steps run strictly in order, so an implied child’s hook never observes its parent’s half-built state.

Late results. If the mode is deactivated (or re-activated) while this future is still pending, the Guard it eventually returns is dropped immediately instead of stashed — so the Guard’s Drop must be correct even for an activation nobody observed.

What ctx gives you: the buffer id, typed services (ModeContext::service), the config registry and the event bus — not the buffer’s text. A mode that needs to create or fill a synthetic buffer reaches the host through a service (BufferStoreHandle, ModeActivator); asynchronous results must reach the screen through an inbound channel (inbound), which wakes the editor, not a bare tick callback.

Stateful modes return a Guard struct whose Drop impl performs cleanup (unsubscribe, restore prior option, drop supervisor handle, etc.).

Errors propagate as ModeActivationError; do not panic.

Idempotent setup contract: on_activate may run more than once in a buffer’s lifetime (each preceded by a Guard-drop if previously active). Implementations must produce a fresh Guard every time.

Provided Methods§

Source

fn target_buffer_kind(&self) -> Option<BufferKind>

For major modes, the [BufferKind] this mode is the default major for (H.2, 2026-05-31). ModeRegistry::register indexes this so ModeRegistry::find_major_for_kind can dispatch buffer-creation events to the right major without host-side match BufferKind { ... } blocks.

Returns None for:

  • All minor modes.
  • Major modes that don’t bind to a [BufferKind] directly (e.g. language majors like rust-mode / markdown-mode on plain Documents — they activate via Lang detection on [BufferKind::Document], not via kind dispatch).

One [BufferKind] is owned by at most one major; the registry treats the first registration as authoritative and warns on subsequent claims (clobbering is a developer bug, not an extensibility seam).

Note: a single major may be referenced by both a kind and a Lang (e.g. markdown-mode is the major for [BufferKind::Help] and also the language major for [BufferKind::Document] + Lang::Markdown). Declaring target_buffer_kind = Some(Help) does not exclude the Lang-detected dispatch path — they cohabit.

Source

fn target_language(&self) -> Option<&str>

For major modes, the language this mode is the default major for, by canonical name (Lang::name() — "rust", "org") (OM.1). The peer of target_buffer_kind for the dispatch path [BufferKind::Document] takes: language detection rather than kind dispatch.

ModeRegistry::register indexes this so ModeRegistry::find_major_for_lang can resolve a document’s major without a host-side match Lang { ... }, which is what makes a plugin-contributed language’s major possible at all: Lang::Plugin(_) has no arm in the host’s hand-written table and never will, because the host does not know the language exists until a plugin says so.

Returns None for:

  • All minor modes. A minor declaring one is ignored at register-time rather than indexed — resolving a minor as a buffer’s major would corrupt activation.
  • Major modes not bound to a language (kind-bound majors like file-tree-mode, or manual-only majors).

One language is owned by at most one major; first registration wins and later claims warn, matching target_buffer_kind.

The built-in language majors (rust-mode, markdown-mode, …) do not declare this yet — they resolve through lattice_syntax::major_mode_id_for_lang’s table, which is consulted first. Migrating them onto this index would collapse that table, and is deliberately left as separate work: this slice makes plugin languages reachable, it does not rewrite how the built-ins resolve.

Source

fn options(&self) -> OptionOverrideSet

Option overrides this mode contributes. Pure declarative (same return value every call); the host merges these into the buffer’s option-resolution layer stack while the mode is active, at the mode’s layer (later-activated minors win ties), and drops them on deactivation. Build with lattice_config::overrides! { lattice_config::Wrap = true, … }.

Source

fn keymap(&self) -> Keymap

Keymap chord → command additions / overrides.

Read once when the mode is registered; the host translates every binding and entry into the layer KeymapLayer::MinorMode(id) (or MajorMode(id)), and a per-keystroke filter makes that layer live only in buffers where this mode is active. A mode cannot place a binding in any other layer. Table-form entries name commands by string and are resolved against the command registry at that point (an unknown name is logged and skipped).

§Examples
use std::sync::LazyLock;
use lattice_mode::{
    keymap_entry, Keymap, KeymapEntry, LifecycleFuture, Mode, ModeContext, ModeId,
    ModeKind,
};

static PREVIEW_KEYS: LazyLock<Vec<KeymapEntry>> = LazyLock::new(|| {
    vec![keymap_entry! { mode: Normal, chord: "q", doc: "Close the preview", cmd: "preview:close" }]
});

struct PreviewMode;
impl Mode for PreviewMode {
    type Guard = ();
    fn id(&self) -> ModeId { ModeId::new("preview-mode") }
    fn kind(&self) -> ModeKind { ModeKind::Minor }
    fn keymap(&self) -> Keymap { Keymap::from_entries(PREVIEW_KEYS.as_slice()) }
    fn on_activate(&self, _ctx: ModeContext) -> LifecycleFuture<'_, ()> {
        Box::pin(async { Ok(()) })
    }
}

let km = PreviewMode.keymap();
assert_eq!(km.entries.len(), 1);
assert_eq!(km.entries[0].doc, "Close the preview");
Source

fn decorations(&self) -> Vec<DecorationProvider>

Decoration providers (gutter / inline / overlay / statusline). Stub — reserved for the WIT plugin path (M.10).

Source

fn gutter_decorations(&self, _ctx: &DecorationCtx<'_>) -> Vec<GutterDecoration>

Gutter sign decorations this mode contributes while active. Called once per pane per frame with a DecorationCtx carrying relevant render-state snapshots (diff sign map, LSP diagnostics arc). Returns per-line GutterDecoration values; the renderer partitions them by variant into the appropriate gutter column. Default: empty (no contribution).

Source

fn completion_sources(&self) -> Vec<CompletionSourceContribution>

Insert-mode completion sources this mode contributes while active on a buffer. Empty by default; minors that own a completion source (lsp-completion-mode, snippet-completion-mode, buffer-words-mode, tree-sitter-completion-mode, path-completion-mode, plugin sources) override.

Source

fn action_handlers(&self) -> Vec<ActionHandlerContribution>

Global (buffer-agnostic) action handlers this mode contributes (SN.3c.0). The host walks every registered mode’s action_handlers() once at boot, resolves each action_name → CommandId, registers the handler in the ActionHandlerRegistry, and holds the tokens for the app’s lifetime. Use this for handlers that read the active buffer / cursor / services from the ActionContext at call time and close over no per-buffer state (e.g. snippet expand). Per-buffer, session-scoped handlers register in on_activate instead, so their tokens drop with the Guard. Default: none. See feedback_effect_vocabulary_is_host_boundary.

Source

fn required_capabilities(&self) -> CapabilitySet

Capabilities the mode requires. Validated at activation; missing capability ⇒ ModeActivationError::MissingCapability, never silent skip.

Source

fn conflicts_with(&self) -> &[ModeId]

Modes this one cannot be active alongside.

Checked symmetrically when a minor is activated (directly or through an implies cascade): activation fails with ModeActivationError::Conflict if any mode listed here is active, or if the active major or any active minor lists this mode. Nothing is auto-deactivated — the caller decides whether to deactivate the other mode and retry. activate_major does not consult this list.

Source

fn implies(&self) -> &[ModeId]

Minor modes activating this mode also activates. Used by relative-line-numbers-mode ⇒ line-numbers-mode, and by read-only majors ⇒ read-only-mode.

Every id must be registered, or activation fails with ModeActivationError::UnregisteredDependency. The whole tree is validated and recorded before any hook runs; hooks then run parent first, depth-first. Deactivating a minor cascade-deactivates the minors it implied (deactivating a major does not).

Source

fn presents_extensions(&self) -> &[&'static str]

File extensions (lowercase, no dot) this major PRESENTS rather than edits — the file is never loaded as text.

The open path consults this before reading: a match builds a placeholder buffer with the real path and no content, and this major renders the file some other way. image-mode shows it as a media block; a future PDF or archive viewer is the same shape.

Default empty, which is every ordinary major: its buffer IS the file’s text.

A mode declaring this must also be read-only in both of the ways that matter — ReadOnly = true in options AND read-only-mode in implies — because the buffer’s text is a placeholder and saving it would overwrite the real file with nothing. The option alone gates typing; the implied mode is what refuses the operators.

Source

fn mirrors_option(&self) -> Option<&'static str>

Declarative mirror hint for “this mode is the on/off switch for a typed option of the same observable state”. Some(canonical_name) ⇒ a host-driven cascade keeps the mode’s active state and the option’s value in sync.

Source

fn invocation_runner(&self) -> Option<ModeId>

Invocation-runner discovery (2026-05-26). Modes that own command-invocation dispatch for their buffer kind (terminal-mode, oil-mode, file-tree-mode, help-mode, …) return their canonical ModeId; the host registers a runner function under that id at boot, and Editor::run_invocation looks it up by walking the active modes on the active pane’s buffer (minors first, then major) before falling back to the central grammar Action gate.

Returning None (the default) means the mode doesn’t claim invocation dispatch — the keymap / decorations / completion-source contributions still apply.

Replaces the hardcoded match BufferKind block that previously lived in Editor::run_invocation. Plugin- installed modes for plugin-installed buffer kinds now extend the dispatcher without touching host code.

Source

fn refresh_action(&self) -> Option<&'static str>

Which of this mode’s own actions refreshes its view, or None (the default) when the mode backs nothing refreshable (RV.1, 2026-08-10).

gr means “refresh this view” in every synthetic buffer. That is a property of synthetic views as a class, so the chord lives once on RefreshableViewMode — not re-declared per mode. Before RV.1 it was re-declared per mode, and the two views that landed most recently (*problems*, narrow) had no gr at all: a gap in a copied set does not announce itself.

This declares a target, not a body. The handler stays exactly where action_handlers puts it — a mode returning Some("action:magit-refresh") keeps the closure it already registered under that name. Declaring a target rather than doing the work is the same shape invocation_runner and mirrors_option already have; a refresh(&self, ctx) doing the work would give modes two ways to express one body.

Returning Some also auto-activates refreshable-view-mode through the implies cascade, so a mode author writes one line and gets the chord.

The host resolves this by walking the buffer’s active modes (minors most-recently-activated first, then major) — see Editor::resolve_refresh_action. When no active mode declares one, the chord echoes nothing to refresh here rather than being swallowed, so the absence is spoken.

See docs/dev/architecture/mode-architecture.md §5.5.

Source

fn fold_toggle_action(&self) -> Option<&'static str>

Which of this mode’s actions <Tab> fires, and the declaration that pulls in foldable-view-mode — the shared minor owning the chord.

Some(FOLD_TOGGLE_DEFAULT_ACTION) for the ordinary “cycle the fold at the cursor”; Some(own_action) for a view with a real specialisation (magit expands a diff on the first press over a status file line).

None — the default — means this mode’s buffers keep <Tab> as jump-list-forward, which is what every ordinary document wants.

Source

fn refresh_on_open(&self) -> bool

Should re-opening this view re-run its refresh?

A synthetic buffer is created once and reused: the host’s ensure_named_synthetic_document returns the existing buffer by name, so a mode’s on_activate — which is what fills the buffer — runs on the FIRST open only. For a view whose content is a snapshot of external state, that makes every later open a time capsule: C-x g on an already-open *magit:status* showed the repository as it was when the buffer was first created, with nothing on screen saying so.

Returning true makes the host dispatch this mode’s declared refresh_action after an open that reused an existing buffer. First opens are untouched: on_activate has just built the content, and refreshing again would be a second scan for the same answer.

Opt-in, and only for content derived from outside the editor. A view whose content is authored in the editor (a help page, a transcript, *messages*) has nothing to re-derive, and refreshing it would discard scroll position for no gain.

The contract on the body: a refresh reached this way must be self-contained — spawn its own work and return no Effect. This path has no dispatch outcome to route renderer-coupled effects through (OpenBuffer, OpenPicker, …), so a returned effect is logged as a wiring error rather than half-applied. Magit’s refresh satisfies this: it spawns the git work and returns None.

Declaring true without a refresh_action does nothing; the two are read together.

Source

fn activation_policy(&self) -> ActivationPolicy

A minor mode’s default auto-activation policy (MA.1; mode-architecture.md §7.4). The host’s minor-activation resolver reads this for every registered minor when a buffer enters a major mode, and activates those whose policy admits the entered major. The default is ActivationPolicy::Manual — auto-activate nowhere until the mode or the user opts in. Ignored for major modes (a buffer’s major is chosen by the major resolver, not this allowlist).

Source

fn editable_tail(&self) -> Option<EditableTail>

The mode’s editable tail on an otherwise read-only buffer, or None (the default) for a fully read-only / fully writable buffer (AU‑3).

A mode backing an owner-written buffer (the agent conversation, future REPL / scratch buffers) declares a tail so the host’s read-only edit gate lets user keystrokes edit only the trailing prompt region — the comint pattern. Consulted directly by the gate (no per-buffer seeding): the tail is expressed relative to the buffer end (see EditableTail), so it stays valid as the owner appends content above it. Returning None leaves the read-only gate’s behaviour unchanged (edits rejected iff ReadOnly is resolved true).

Dyn Compatibility§

This trait is dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety".

Implementors§