modes
Direction: guest calls into the host through it · Capability: none (pure data / dispatch) · Worlds: auto-pair-plugin (imports), comment-plugin (imports), modes-plugin (imports), project-plugin (imports), treesitter-context-plugin (imports)
Mirrors the Mode trait declaration surface + ModeRegistry (lattice-mode). The guest declares a minor mode as DATA (id + kind + activation policy + capability requirements); the host builds a marker Mode impl (PluginMode, the EmacsKeysMode template) and registers it into the SAME ModeRegistry builtins use, so :describe-mode / mode introspection treat it uniformly.
PH7.11a lands the declaration + registration path (this file); keymap bindings (chord→command-name at the mode's OWN layer, the KeymapCapability write-gate) are PH7.11b. OM.2 lands major modes — a plugin that contributes a language contributes its major too, which is the only way a plugin language can have one (Lang::Plugin(_) has no arm in the host's hand-written table). MO.1 lands typed option-overrides — the last part of its surface a plugin mode could not own. Lifecycle callbacks / decorations / bundled modes-as-components remain Phase 8.
The CANONICAL, language-agnostic surface — any component-model language calls register-mode directly (see the WIT-canonical principle).
Functions (3)
disable-mode
disable-mode: func(id: string)
Disable a registered minor mode globally (CI.4) — the inverse of enable-mode; the Editor deactivates it on open buffers.
enable-mode
enable-mode: func(id: string)
Enable a registered minor mode globally (CI.4) — the user-enablement path (config-and-init.md §6). A plugin minor mode is registered available-but-off; an init.rs on-plugin-loaded handler calls this to turn it on. The host publishes a mode-enablement-requested signal and the Editor flips the enablement + re-activates open buffers (this call is the request, not the apply — the guest can't reach the activator). A no-bus / unknown-id case is a warn + drop (graceful), never a trap.
Example — Enable a plugin's mode and set its option once that plugin has loaded · crates/lattice-plugin-host/tests/fixtures/init-guest/src/lib.rs
if let Event::PluginLoaded(p) = ev {
// Deferred config: enable auto-pair-mode the moment auto-pair loads.
if p.name == "auto-pair" {
modes::enable_mode("auto-pair-mode");
// …and SET one of its options, which is the other half of the
// documented deferred shape and the half that was never driven.
// `enable-mode` reaches the bus; `set-option` reaches the config
// registry, and the events store did not carry one — so this
// call warned and no-oped while the test above still passed.
// Full name, not the short one: `set-option` prefixes with the
// CALLING plugin's id, so `style` would resolve as `init.style`.
config::set_option("auto-pair.style", "manual");
}
}
register-mode
register-mode: func(decl: mode-declaration)
Declare a mode. Records the declaration; the host builds a PluginMode and registers it into the ModeRegistry after register-modes returns (the register-grammar drain precedent — registration needs &mut ModeRegistry, not a live handle). Registration failures (bad -mode suffix, id collision, major kind) are logged + skipped at drain.
Example — Declare a global minor mode that owns the plugin's surface · plugins/comment/src/lib.rs
modes::register_mode(&ModeDeclaration {
id: "comment-mode".to_string(),
kind: ModeKind::Minor,
// `global`, not `universal`: every DOCUMENT buffer. `gc` over
// user-edited text is the point; `gc` in `*messages*`, the file
// tree or a help popup is noise.
activation_policy: ActivationPolicy::Global,
capabilities: ModeCapabilities::empty(),
// Not language-scoped: `gc` works in every document buffer, and
// which leader to use is decided per-buffer from the path.
target_language: None,
// No option overrides — the mode changes how keys behave, not how
// its buffers behave.
options: Vec::new(),
// No keymap here. The operator's chord is bound by the host into
// the operator-pending layer (CM.2) — a plain binding could not
// give `gc` a motion, and would kill `gcc` besides.
keymap: Vec::new(),
});
Types (8)
enum mode-kind
enum mode-kind {
major,
minor,
}
Major (content-type identity) vs minor (orthogonal behavior). Both register (OM.2); a buffer has exactly one major and any number of minors. A major's keymap lands at KeymapLayer::MajorMode(id), below active minors and above the built-in vim grammar — so a minor can refine what its major bound, and neither can shadow the grammar globally.
variant activation-policy
variant activation-policy {
manual,
global,
universal,
majors(list<string>),
}
Which buffers a mode auto-activates on — mirrors ActivationPolicy. manual = only on explicit activation; global = document buffers only; universal = every buffer; majors = an allowlist of major-mode ids.
flags mode-capabilities
flags mode-capabilities {
buffer-uri,
lsp,
tree-sitter,
folds,
writable,
diagnostics,
}
Capability requirements a mode declares — mirrors the CapabilitySet bitflags (lattice-mode). Enforcement stays the native mode-activation path; the declaration sizes the requirement honestly (fragment §6).
enum binding-mode
enum binding-mode {
normal,
insert,
visual,
select,
replace,
command,
search,
}
The vim binding mode a keymap entry lives in — the plugin-facing subset of the native BindingMode (the transient operator-pending / after-key states are internal grammar states, not plugin-bindable).
record mode-keymap-binding
record mode-keymap-binding {
binding-mode: binding-mode,
chord: string,
command: string,
}
One keymap binding a mode contributes (PH7.11b): bind chord in binding-mode to an EXISTING command named command, resolved against the CommandRegistry at registration. command is any registered command name — a built-in (ex:write), a host action (action:split-pane-horizontal), or the plugin's OWN grammar contribution (PH7.7 register-action). An unparseable chord or unknown command skips that one binding (logged).
Fields
binding-mode:binding-modechord:stringcommand:string
enum override-priority
enum override-priority {
low,
normal,
high,
}
Where a mode's option override sits in the resolver — mirrors the native OverridePriority. normal is what a mode should almost always use; within a layer the last-activated normal wins and the host fires a ModeEvent::OptionConflict. high and low exist for a mode that genuinely must out-rank or yield to its peers, and reaching for either to win an argument with another mode is how a conflict becomes invisible instead of reported.
record mode-option-override
record mode-option-override {
name: string,
value: string,
priority: override-priority,
}
MO.1: one option a mode sets for its own buffers — the plugin-facing form of a native mode's options() set.
name is the option's registered name (foldmethod), resolved host-side against the SAME ConfigRegistry :set uses. value is its value in exactly the spelling :set name=value accepts, coerced host-side through the same parser and validator — so a mode declares an override in the vocabulary the user already knows, and does not get a private settings channel that could disagree with :set about what a value means.
This is a LAYER, not a write. It changes what the option resolves to in this mode's buffers; it does not alter the user's global setting, and it does not outrank a buffer-local :setlocal.
An override the host cannot resolve is skipped, warned, and named — the rest of the set still applies, because one bad entry must not cost a mode its other options. Two things do not resolve: an option name nothing registered, and a value the option's own validator rejects. A third is worth stating because it will surprise: an option the PLUGIN ITSELF registered through the config seam has no native type identity, so it cannot be overridden here yet. That is a known hole with a known fix (plugin-mode-options.md §3c) and no consumer yet.
Fields
name:stringvalue:stringpriority:override-priority
record mode-declaration
record mode-declaration {
id: string,
kind: mode-kind,
activation-policy: activation-policy,
capabilities: mode-capabilities,
keymap: list<mode-keymap-binding>,
target-language: option<string>,
options: list<mode-option-override>,
}
A mode declaration. id must carry the conventional -mode suffix (the ModeRegistry::register gate, e.g. git-blame-mode); a bare or mis-suffixed id is rejected at registration. keymap bindings land at KeymapLayer::MinorMode(id) gated by KeymapCapability::OwnedLayer{id} — a plugin mode can write ONLY its own layer (PH7.11b write-gate).
Fields
id:stringkind:mode-kindactivation-policy:activation-policycapabilities:mode-capabilitieskeymap:list<mode-keymap-binding>target-language:option<string>— OM.2: for a MAJOR, the language this mode is the default major for, by canonical name ("org") — the name the plugin'slanguageseam registered. The host indexes it (ModeRegistry::find_major_for_lang) and a document of that language activates this mode, the same path a built-in language major takes.Ignored (with a warning) on a
minor: a buffer has exactly one major, and indexing a minor here would install it AS the major. A minor that wants to ride a major names it inactivation-policy.majorsinstead.noneon a major means manual activation only — it is not the default for any language.A language the host resolves through its own table (
rust,markdown, …) is NOT claimable: the built-in table is consulted first, so a claim on one is inert rather than a hijack.options:list<mode-option-override>— MO.1: options this mode sets for its own buffers.Until this existed a plugin mode owned its keymap, its handlers and its lifecycle but NOT the options deciding how its buffers actually behave — whether they are writable, whether they wrap, how they fold. That was a hole in the mode-ownership rule, and org was standing in it: org folding worked only because the user happened to set
foldmethodglobally, making it correct by coincidence on one machine and wrong everywhere else.Empty is the normal case and costs nothing.