Skip to main content

Effect

Enum Effect 

Source
pub enum Effect {
Show 139 variants None, Declined, Edits(Vec<AppliedEdit>), ApplyEdit { target: BufferId, edit: Edit, cursor: Option<Position>, }, WriteToFile { path: PathBuf, anchor: FileAnchor, text: String, cut: Option<Range>, create_parents: bool, save: bool, }, SelectionChange(SelectionSet), CursorMove(Position), CursorMoveIn { target: BufferId, position: Position, }, Yank { register: Register, content: String, kind: YankKind, explicit_yank: bool, }, EnterMode(ModalState), SaveBuffer { path: Option<PathBuf>, }, QuitEditor { force: bool, scope: QuitScope, }, OpenBuffer { path: Option<PathBuf>, force: bool, }, OpenBufferAt { path: Option<PathBuf>, position: Position, force: bool, content: Option<String>, activate_minor: Option<String>, }, OpenInTarget { path: Option<PathBuf>, position: Position, target: OpenTarget, }, OilNavigate { view: BufferId, dir: PathBuf, focus: Option<String>, }, FileTreeToggle { view: BufferId, entry_index: u32, }, OpenExternalUri { uri: String, }, OpenBufferAtColumn { path: Option<PathBuf>, column: Option<Utf16Pos>, force: bool, }, SpawnTerminal { cmd_line: Option<String>, cwd: Option<PathBuf>, env: Vec<(String, String)>, activate_minor: Option<String>, }, TerminalInput(Vec<u8>), SetOption { spec: String, }, SetLocalOption { spec: String, }, SetGlobalOption { spec: String, }, ClearSearchHighlight, SetColorscheme(String), Echo { level: EchoLevel, text: String, }, ShowDiagnosticsPopup { lines: Vec<(String, u8)>, }, Lsp(LspRequest), EchoRegisters, EchoMarks, Substitute { scope: SubstituteScope, pattern: String, replacement: String, global: bool, }, Global { pattern: String, inverted: bool, body: Box<CommandInvocation>, }, DeleteCurrentLine, DescribeCommand { name: String, anchor: Option<String>, }, DescribeBuffer, Apropos { pattern: String, }, DescribeKey { chord: String, }, ListKeymap, BufferNext, BufferPrev, FocusBuffer(u32), InvokeCommand { id: String, args: Args, }, ListBuffers, ChangeDir(Option<String>), PrintWorkingDir, PrintProjectRoot, OpenBufferPicker, OpenPicker { source: String, args: Vec<String>, root: Option<PathBuf>, fill_action: Option<String>, query: Option<String>, }, BufferDelete { force: bool, }, OpenFileTree { root: Option<PathBuf>, }, CloseFileTree, OpenOil { dir: Option<PathBuf>, }, DescribeOption { name: String, }, DescribeElement { name: String, }, ListOptions, DescribePluginApi { seam: Option<String>, }, ListPluginApis, ExportPluginApi { format: Option<String>, }, ListCommands, DescribePlugin { name: String, }, ListPlugins, OpenHover { markdown: String, }, DismissPopup, DismissPopupNamed { name: String, }, OpenPopup { name: String, mode_id: String, placement: PopupPlacement, focus: PopupFocus, }, OpenHelpTopic { topic: Option<String>, }, ListDiagnostics, ListErrors, NextDiagnostic, PrevDiagnostic, OpenLspLog { server_id: Option<String>, }, OpenAiLog { session: Option<String>, }, OpenSyntheticBuffer { name: String, mode_id: String, content: Option<String>, cursor: Option<Position>, activate_minor: Option<String>, }, OpenSyntheticBufferAt { name: String, mode_id: String, position: Position, }, OpenMessages, OpenDashboard, ToggleLspTrace { server_id: String, }, OpenLspTraceLog { server_id: Option<String>, }, LspStatus, LspDiagnosticsToErrorList, LspServerLogListing, LspRestart { server_id: String, }, LspProgressCancel { server_id: Option<String>, }, LspExpandRegion, LspShrinkRegion, SetLspLogLevel { server_id: Option<String>, level: String, }, LspLogClear { server_id: Option<String>, }, LspDocumentSymbol, LspWorkspaceSymbol { query: String, }, LspIncomingCalls, LspOutgoingCalls, LspSupertypes, LspSubtypes, LspMoniker, LspCodeLens, LspColorPresentation, LspFormat, Format, LspFormatRange, LspSignatureHelp, LspComplete, LspRename { new_name: String, }, LspCodeAction, ExpandSnippet { replace_range: Range, }, ReloadSnippets, DescribeEvents, DescribeDiff, DiffOpen, DiffOff { force: bool, }, Diffthis, Diffsplit { path: PathBuf, remote: Option<PathBuf>, }, DiffGetCmd { target: Option<u32>, }, DiffPutCmd { target: Option<u32>, }, DiffAccept, DiffReject, DiffAcceptAll, DiffRejectAll, CloseSessionDiffs { origin_session: u64, tab_name: String, }, CloseAllSessionDiffs { origin_session: u64, }, NextHunk, PrevHunk, DescribeEvent { name: String, }, ListModes, DescribeMode { name: String, }, DescribeActiveModes, DescribeActiveBindings, DescribeOptionResolution { name: String, }, Customize { name: Option<String>, }, Tutor { lesson: Option<u32>, }, ToggleMode { mode_name: String, }, AppAction(AppEffect), RecordJump, Confirm { prompt: String, yes_action: String, args: Args, }, BuryBuffer, KillBuffer, OpenTransient { source: String, args: Args, }, OpenPrompt { prompt: String, initial: String, on_submit_action: String, buffer_name: Option<String>, }, Many(Vec<Effect>),
}
Expand description

What a command asks the host to do. See the module docs for the coordinate convention, which buffer an un-addressed variant acts on, and which variants are host- vs. peer-applied.

§Contract for producers

  • An Effect is a request; it cannot report failure back (apply_effect_host returns nothing to the producer). Put anything that must happen only after a write lands after it in an Effect::Many – a Effect::WriteToFile that fails stops the rest of its batch.
  • An evaluator that fails returns Err instead of an effect; nothing is committed (see crate::CommandError).
  • Async producers address their buffer explicitly (Effect::CursorMoveIn, Effect::ApplyEdit) rather than assuming the focused one.

§Examples

use lattice_grammar::{Effect, EchoLevel, Register, YankKind};

// `yy` on "hello": yank the line, tell the user nothing.
let yank = Effect::Yank {
    register: Register::Unnamed,
    content: "hello\n".into(),
    kind: YankKind::Linewise,
    explicit_yank: true,
};

// Effects compose; the host applies `Many` children in order.
let e = Effect::Many(vec![
    yank,
    Effect::Echo { level: EchoLevel::Info, text: "1 line yanked".into() },
]);
assert!(!e.is_none());
assert!(Effect::None.is_none());

Variants§

§

None

Nothing to apply; the chord is consumed. Read-only commands and commands whose work already happened return this. Not forwarded to the renderer.

§

Declined

AP.0.2: the action DECLINES this chord — it did nothing, and the dispatcher should re-resolve the chord as if this action’s keymap layer weren’t present, falling through to the next binding (a lower-priority minor, the builtin/user layer, or Insert-mode self-insert). The with-eval-after-load of keymaps: a plugin action (auto-pair’s manual close key / backspace) declines when it has nothing to do, so the key still does whatever else is bound (completion nav, a normal backspace, a user remap). Distinct from None (a no-op that CONSUMES the chord).

§

Edits(Vec<AppliedEdit>)

Edits the grammar dispatcher has already applied to the focused document (the operator ran against the document actor). The host only routes the side effects: DocumentChanged (LSP didChange, syntax reparse, highlight shift, dot-repeat recording). It also moves the cursor to the first edit’s original_range.start – a following Effect::CursorMove / Effect::SelectionChange in the same Effect::Many overrides that.

Not for code outside the dispatcher: an edit that has not been applied yet goes through Effect::ApplyEdit.

§

ApplyEdit

CR.0: a generic “apply this edit to this buffer” primitive.

Mode-contributed action handlers (the snippet / lsp action_handlers() pattern) compute an edit against their own state — a diff hunk get/put, a conflict resolution — and hand it back through this effect. The host applies edit to target, routing through the active-document pipeline (LSP didChange + syntax reparse + highlight shift) when target is the focused buffer, or the peer-buffer registry handle otherwise; when cursor is Some, it then parks the active cursor at that position (line + byte) — column-precise, so a plugin action (auto-pair) can place the caret between an inserted pair, not only at a row start (AP.2). Native row-start callers pass Position::new(row, 0).

Distinct from Effect::Edits, which carries AppliedEdits the grammar dispatcher already applied to the active document (routed only for side effects). ApplyEdit carries a pending Edit the host has yet to apply, addressed at an explicit target.

The Effect vocabulary is the host boundary by design (feedback_effect_vocabulary_is_host_boundary): this lets a mode drive an arbitrary document edit without the host growing a feature-specific Action variant + do_<x> method per feature.

Deferred: the host translates it into an Action::ApplyEdit queued on the outcome’s next_actions, so it lands after the current effect batch has been applied, not in sequence with it.

Fields

§target: BufferId

The buffer to edit. Need not be focused.

§edit: Edit

The pending edit: a half-open (line, byte) range in target’s current (pre-edit) coordinates, plus what replaces it.

§cursor: Option<Position>

Where to leave the active cursor afterwards, in post-edit coordinates; None leaves it alone.

§

WriteToFile

XF.1: move text into a file the editor has not necessarily opened.

Design: cross-file-writes.md. The primitive org-archive-subtree, org-refile and org-capture were all blocked on — every one of them is “take text from here and put it in a different file”, and nothing in the vocabulary could say the second half. Effect::ApplyEdit addresses a BufferId, which a producer cannot learn for a file that has never been opened.

Through the document pipeline, not to disk. The host resolves path to a buffer, opening it in the background if needed and REUSING it if it is already open. So an open target sees the edit, u covers it, and the LSP hears about it — a direct write would leave the buffer and the disk disagreeing with nobody told.

The target is left modified, not saved. Emacs’s org-refile and org-archive-subtree both do; a plugin that silently writes files is a larger authority than one that edits buffers, and should be an explicit later decision if it is ever wanted.

Fields

§path: PathBuf

Absolute, or relative to the editor’s working directory.

When this effect arrives from a plugin it has already been checked against that plugin’s fs:write grant AT THE BOUNDARY, where the provenance is still known — the host’s applier deliberately cannot tell a plugin’s effect from a native mode’s, so the gate cannot live there (XF.4).

§anchor: FileAnchor

Where in the target the text lands.

§text: String

Inserted verbatim. A trailing newline is the producer’s business.

§cut: Option<Range>

When present, this range is removed from the buffer the action ran in — and ONLY after the insert has landed.

Folding the pair into one effect is what makes the bad outcome unrepresentable: as two effects, “insert succeeded, delete failed” duplicates the text and “delete succeeded, insert failed” LOSES it. The second is data loss from a keystroke. An effect cannot report failure at all (pinned by XF.0), so two ordered effects could not be made to depend on each other without changing every effect’s signature.

§create_parents: bool

OR.10: create missing parent directories rather than refusing.

Off by default, and that default is the rule rather than caution. The host refuses a missing parent because creating directories is a larger authority than creating a file, and a typo’d path must not silently build a tree (crate::Effect::WriteToFile’s applier says so in as many words). That stays true for every producer that does not ask.

A producer asks when the directory is part of the layout it owns rather than something the user typed. org-roam’s daily/YYYY-MM-DD.org is the case that forced this: the folder is named by an option with a default, no user ever types it, and without this the very first :org-roam-dailies-today on a fresh corpus fails — the one use where the feature has to work.

Still bounded: a plugin’s path is checked against its fs:write grant at the boundary before this is read, so asking widens what is created inside the grant and never what is reachable.

§save: bool

OC.9: persist the target to disk once the write has landed, instead of leaving the buffer modified.

Off by default, and the default is the rule. cross-file-writes.md §7 leaves a target open, listed and MODIFIED — which is what emacs’s org-refile and org-archive-subtree do, and the user writes it themselves after reviewing. Every producer that does not ask keeps exactly that.

A producer asks when its whole operation is “commit this somewhere”. org-capture is that case, and emacs agrees loudly: org-capture-finalize runs (unless (org-capture-get :no-save) (save-buffer)), so saving is the default there and :no-save is the opt-OUT. The asymmetry with refile is not an inconsistency — a refile moves text you are looking at, a capture files text you are finished with.

It is also what decides whether readers of the FILE ever see the write. The org agenda scans from disk, so an unsaved capture cannot appear in a refresh however correct the buffer is.

§7 recorded a save: bool as rejected (“the answer is uniformly no, which is an easier thing for a user to know”). That argument was sound for the producers that existed when it was written and wrong for capture, whose entire contract is durability; the flag keeps the uniform answer for everyone who does not opt in, and the design doc records the reversal rather than dropping the paragraph.

Only after a landed insert, and after any cut. The ordering cut documents extends here: a write that failed saves nothing, so this can never persist a half-applied effect.

§

SelectionChange(SelectionSet)

Replace the focused buffer’s selection set; motions return this with a collapsed primary (anchor == head). The host moves the cursor to the primary’s head. In Visual / Select mode it keeps the selection alive: a collapsed primary extends from the running Visual anchor (a motion), a non-collapsed one is adopted whole (a text object such as viw). Outside Visual only the cursor moves.

§

CursorMove(Position)

Move the focused buffer’s cursor to this (line, byte). The semantically-clean cursor-only jump — use this for navigation chords (]]/[[, ]c/[c, ]f/[f) rather than overloading SelectionChange with a collapsed cursor. The host writes editor.cursor; in Visual / Select mode it also extends the selection from the Visual anchor to the new position, exactly as a motion would.

Chord-time only. An async producer must use Effect::CursorMoveIn: by the time its result lands the focused buffer may be a different one.

§

CursorMoveIn

MG.18d: Effect::CursorMove addressed at a specific buffer — the host moves the cursor only while target is the focused buffer, and drops the effect otherwise.

The peer of Effect::ApplyEdit’s target field, for the cursor half. A chord-time CursorMove needs no target because the buffer it fired in is the focused one; an async producer has no such guarantee. Between a magit stage and the refresh that finishes it lie two git calls, and a q or C-^ in that window would otherwise land the jump in whatever buffer the user moved to — a caret teleporting in a file they were about to type in.

Dropping (rather than stashing for the buffer’s return) is deliberate: the position was computed against content that will have been rebuilt again by the time focus comes back, and a stale jump is worse than none. Producers that want position restored on return use marks / position history, which are per-buffer by construction.

Unlike CursorMove it does not touch a Visual selection.

Fields

§target: BufferId

The buffer position was computed against.

§position: Position

0-based (line, byte) in target.

§

Yank

Write text into a register (and the host’s yank ring). Every operator that captures text emits one: yank, delete, change, x.

The host always updates the unnamed register too, stores into register when it names one, and ignores the whole effect for Register::BlackHole. It moves no cursor and edits nothing.

Fields

§register: Register

Destination register; Register::Unnamed when none was named with "<x>.

§content: String

The captured text, verbatim. Linewise content ends with \n.

§kind: YankKind

Shape of the capture; decides how a later put lays it out.

§explicit_yank: bool

true when this write came from an explicit yank (y, yy, Visual y); false for the register writes that delete / change / x also perform. Drives the yank-only system-clipboard mirror (clipboard.md §5): the host mirrors to the OS clipboard only when this is an explicit yank (under the clipboard option) or the target is the +/* register, so an incidental delete never clobbers the clipboard.

§

EnterMode(ModalState)

Transition the modal state machine. Used by operators that change modes after committing edits (vim’s c -> Insert, future s, gv reselect Visual, etc.).

§

SaveBuffer

:w [path] – write the current buffer (to the given path, or the document’s known path).

Host-applied (works on the off-keystroke inbound path too). In an oil buffer it applies the listing’s edits to the filesystem instead.

Fields

§path: Option<PathBuf>

Write-as target. A user-typed path: ~ expands and a relative path joins the editor’s :cd directory. None writes the buffer’s own file (an error echo if it has none).

§

QuitEditor

:q[!] (scope = Pane) / :qa[!] (scope = All) – quit. force = true ignores dirty state. The two are distinct commands but one effect: quit is a single host operation parameterized by scope, exactly as force is a parameter. Pane closes the active pane when more than one is open and only shuts the editor on the last pane (vim’s :q); All ignores pane/tab count and shuts the editor outright (vim’s :qa). The dirty guard (unless forced) is identical for both and lives once in Editor::do_quit.

Peer-applied.

Fields

§force: bool

! – skip the dirty-buffer guard.

§scope: QuitScope

Pane (:q) or whole editor (:qa).

§

OpenBuffer

:e[!] [path] – swap the current document for the file at path. With path = None reload from the document’s existing path. force = true discards unsaved changes.

Peer-applied. A file already open in another buffer is activated, not reloaded; naming the focused buffer’s own file reloads it (and needs force when dirty); a directory opens a directory view.

Fields

§path: Option<PathBuf>

User-typed path (~ and :cd-relative resolved by the host); None reloads the focused document from its own path.

§force: bool

! – allow a reload that discards unsaved changes.

§

OpenBufferAt

M.10.3 bug fix (2026-06-03): atomic “open file + position cursor.” Used by mode-contributed jump handlers (search <CR>, lsp-references <CR>, future project-diff <CR>) so the cursor lands at the matched (row, byte) inside the newly-opened buffer on the FIRST render.

Necessary because Effect::SelectionChange runs synchronously in the host’s handle_effect (writes to editor.cursor against whatever buffer is active at that moment), while Effect::OpenBuffer is renderer- coupled — applied LATER by the TUI/GPUI peers via do_edit. The two can’t be ordered to land cursor on the new buffer without an atomic step. The peer renderer’s arm for this variant calls do_edit THEN set_selections_blocking in a single atomic block.

Pre-fix, <CR> on a search hit opened the file but landed at (0,0) because the host’s SelectionChange ran first (against the still-active multibuffer) and the later do_edit reset cursor for the freshly-loaded document.

Fields

§path: Option<PathBuf>

As Effect::OpenBuffer’s path.

§position: Position

Where the cursor lands in the opened buffer, 0-based (line, byte). Closed folds around it are opened.

§force: bool

As Effect::OpenBuffer’s force.

§content: Option<String>

CD.2: text for the buffer only when the file is not on disk. Reopening a file that exists never has its text replaced — a saved draft reopened must keep what was typed into it. The seed is an ordinary edit, so the buffer is modified and :q guards it.

§activate_minor: Option<String>

CD.2: a minor mode to activate alongside the major the path resolves, before the buffer is shown, so its keymap is live on the first keystroke. OC.7a’s reason: a plugin mode has no on_activate, so this is how a guest gives its buffer chords.

§

OpenInTarget

LM.0: open path at position in a target pane — split / vsplit / new tab. The peer-applied sibling of Effect::OpenBufferAt with a pane target: its peer arm runs Editor::prepare_open_target_pane(target) (the picker-accept split sequence) then Editor::open_buffer_at(path, position, …).

<CR> (current pane) still returns plain Effect::OpenBufferAt; this is what the listing majors’ <C-s>/<C-v>/<C-t> chords return so a file lands in a new split / vsplit / tab. target = Default is equivalent to OpenBufferAt and kept for uniformity.

Host/peer-only for now: no WIT mirror (a plugin opening in a split is a deliberate future WIT addition, see boundary_effect).

Fields

§path: Option<PathBuf>

As Effect::OpenBuffer’s path.

§position: Position

Where the cursor lands, 0-based (line, byte).

§target: OpenTarget

Which pane: current, new split, new vsplit, or new tab.

§

OilNavigate

LM.2: re-list an existing oil buffer view to dir in place — the peer-applied sibling of the directory-navigation half of the old do_oil_follow / do_oil_navigate_up. The applier reloads the directory snapshot (fs I/O), rewrites the buffer’s rope, and resets cursor/scroll. focus is the entry name to land the cursor on afterwards (the came-from directory for - parent-navigation); None lands at the top (a <CR> descent into a child).

Names view explicitly so many oil buffers stay independent: the applier touches only that buffer’s dir/snapshot/rope (design §3.2). Host/peer-only — no WIT mirror.

Fields

§view: BufferId

The oil buffer to re-list.

§dir: PathBuf

The directory it now lists.

§focus: Option<String>

Entry name to put the cursor on after the re-list; None = first row.

§

FileTreeToggle

LM.2: toggle the expansion of file-tree view’s directory entry at entry_index (the row under the cursor), re-rendering the tree’s rope — the peer-applied sibling of the directory half of do_file_tree_follow. Names view so trees stay independent. Host/peer-only — no WIT mirror.

Fields

§view: BufferId

The file-tree buffer.

§entry_index: u32

0-based index into the tree’s current entry list (the row under the cursor). Out of range is a silent no-op.

§

OpenExternalUri

BC.8c: open uri via the OS handler (open / xdg-open / explorer). Emitted by the LSP window/showDocument handler for external: true requests; generic enough to reuse for any “open this URL externally” need (gx, markdown / help external links). A plain URL string — no lsp_types leak into the grammar. Host-applied in Editor::handle_effect: the spawn is a host side-effect, and the showDocument bus drains off-keystroke through the generic inbound tick-callback (where peer-applied effects are not forwarded), so the work must run host-side.

Fields

§uri: String

Any URI the OS handler accepts (https:, file:, mailto:). A spawn failure is logged, not echoed.

§

OpenBufferAtColumn

BC.8c: host-applied atomic open + optional UTF-16-column cursor placement. Unlike Effect::OpenBufferAt (peer-applied, carrying a pre-converted byte offset), this runs entirely in Editor::handle_effect so it works on the off-keystroke async path: server-initiated window/showDocument drains through the generic inbound tick-callback, where peer-applied effects are discarded — the open must happen host-side (as the retired drain_inbound_show_documents did via do_edit).

column = None opens only, leaving the cursor where do_edit puts it (the no-selection showDocument case). Some positions the cursor at the UTF-16 code-unit column, converted to a byte offset against the opened line — the conversion needs the line text, which only exists post-open, which is why the column travels unconverted.

Fields

§path: Option<PathBuf>

As Effect::OpenBuffer’s path.

§column: Option<Utf16Pos>

Cursor target as an unconverted LSP position; None = open only.

§force: bool

As Effect::OpenBuffer’s force.

§

SpawnTerminal

I5.1 (Claude Code IDE peer): spawn a child process in a new BufferKind::Terminal buffer, optionally injecting extra environment and activating a minor mode on the new buffer. Host-applied (the open is irreducibly &mut Editor): the host calls do_terminal_spawn with cmd_line + env, then activates activate_minor (a minor mode, by its mode-id name) on the spawned buffer when set. :terminal reaches the same host path via the host-side AppEffect::TerminalSpawn; this grammar variant lets crate-owned ex-commands — the IDE peer’s :claude, which must inject CLAUDE_CODE_SSE_PORT + ENABLE_IDE_INTEGRATION — request the spawn through the Effect vocabulary (the host boundary) instead of a bespoke channel.

Fields

§cmd_line: Option<String>

Command line (program [args...]); None spawns $SHELL.

§cwd: Option<PathBuf>

PC.2: working directory to spawn in, overriding the active buffer’s project root for this spawn only.

lattice_terminal::SpawnConfig has carried a cwd since the terminal shipped (“None = inherit parent’s cwd”); this is the boundary catching up, so a producer that knows WHICH project it means can say so. None keeps PR.3’s behaviour exactly: spawn at the active buffer’s project root.

Binding at spawn is what lets several projects coexist — Command::cwd applies once, so a shell already running is the OS’s business and nothing resolved later can move it.

§env: Vec<(String, String)>

Extra environment injected on top of the inherited parent env.

§activate_minor: Option<String>

Minor mode to activate on the spawned buffer, by mode-id name (e.g. "claude-code-mode"); None leaves the buffer mode-bare.

§

TerminalInput(Vec<u8>)

D-fix.4 (Claude Code IDE peer): write raw bytes to the focused terminal’s PTY. Host-applied (do_terminal_input, irreducibly &mut Editor + the terminal registry). The IDE peer’s :claude-interrupt emits TerminalInput(vec![0x1b]) to forward an <Esc> to the running claude CLI — <Esc> can’t be sent by typing because the terminal’s modal layer consumes it for Insert→Normal, so an ex-command is the only interrupt path. Targets the active pane’s terminal (the focused claude session); a no-op (logged) when the active buffer isn’t a terminal.

§

SetOption

:set <option> – the host parses the option spec; the closure just hands the raw text through.

Fields

§spec: String

Everything after :set (name, noname, name=value, name?, …). Parsed and validated by the host’s config registry; a parse error is echoed and nothing is written.

§

SetLocalOption

:setlocal <option> – like SetOption but writes to the buffer-local override layer for the active buffer only, without touching the global config registry.

Fields

§spec: String

Same syntax as Effect::SetOption’s spec.

§

SetGlobalOption

:setglobal <option> – like SetOption but only writes the global config registry without updating any buffer-local override layers. Reads back the global value on :setglobal name?.

Fields

§spec: String

Same syntax as Effect::SetOption’s spec.

§

ClearSearchHighlight

:noh[lsearch] – clear the hlsearch overlay.

§

SetColorscheme(String)

:colorscheme <name> (T.9.b) – swap the active theme by name. The host looks name up in lattice_theme::builtin_themes() and calls ThemeRegistry::set_theme (palette + override swap), then emits RendererSignal::ThemeChanged so both renderers rebuild their caches. An unknown name echoes a host-side error. The closure only packages the name (it has no registry access).

§

Echo

Display a one-line message in the echo area. Also appended to *messages* and published as a message event; Trace / Debug are recorded but not shown.

Fields

§level: EchoLevel

Severity; decides colour and whether it is shown.

§text: String

The message. Keep it to one line.

§

ShowDiagnosticsPopup

L4b (lsp-architecture.md §15): show a cursor-anchored popup with the pre-formatted diagnostic lines for the cursor’s line. The owning mode (lsp-diagnostics-mode) formats lines in its gl handler; the host renders them through the hover popup pipeline (HelpContent → DisplayBufferRequest, PopupPlacement::CursorAnchored). Empty lines → host echoes “no diagnostics on line” instead of an empty popup. Each line is (text, severity_rank) where rank is Error = 0 … Hint = 3 (matching lattice_lsp’s severity_rank); the host colours each popup line by its severity via the matching Style::Diagnostic* highlight.

Fields

§lines: Vec<(String, u8)>

(text, severity_rank) per popup line; rank 0 = Error, 1 = Warning, 2 = Information, 3 = Hint.

§

Lsp(LspRequest)

L7 (lsp-architecture.md §16): fire a mode-owned LSP navigation request (K / gd / gD / gy / gI / gr / gx). The owning lsp-mode action_handlers() closure decides which request via LspRequest; the host’s editor.lsp_request dispatcher runs the (unchanged) async request substrate host-side. Host-applied — both renderers treat it as a host-handled no-op in their effect classifiers, and it neither mutates nor yanks. LspRequest::FollowLink is the one variant that yields RendererSignals synchronously (open buffer / OS handler); the apply arm extends out.renderer_signals.

§

EchoRegisters

:reg[isters] – the host formats and displays its own register state.

§

EchoMarks

:marks – the host formats and displays its own mark state.

§

Substitute

:[%]s/pat/repl/[g] – run substitute over the given scope, one edit per changed line. Echoes the count, or E486 when nothing matched.

Fields

§scope: SubstituteScope

Which lines.

§pattern: String

A fancy_regex pattern (Rust regex syntax plus look-around and backreferences) – not vim’s regex dialect. Empty is an error.

§replacement: String

Replacement template; $1 / ${name} expand capture groups.

§global: bool

g flag – every match on a line, not just the first.

§

Global

:g/pat/body (and :v/pat/body with inverted = true). body is a pre-parsed CommandInvocation – the parser front-end (lattice-host::excommand) compiles it once at :g parse time so the host doesn’t re-parse per matching line, and so body parse errors surface before :g fires.

Peer-applied. The host collects the matching lines from a snapshot first, then walks them bottom-up, placing the cursor at column 0 of each and dispatching body there, so edits never shift a line still to be visited. No WIT mirror (CommandInvocation has none).

Fields

§pattern: String

Matched as a literal substring today, unlike Effect::Substitute’s regex. Empty is an error.

§inverted: bool

:v / :g! – run on the lines that do not match.

§body: Box<CommandInvocation>

The command to run on each selected line.

§

DeleteCurrentLine

:d – delete the current line including its trailing newline. Distinct from the standard delete operator with a CurrentLine range, which preserves the newline (vim’s dd semantics differ from :d – §5.2.1).

§

DescribeCommand

:describe-command <name> (DESIGN.md §5.11). The host queries its CommandRegistry for the named entry and renders the metadata into a help overlay. Carried as a sentinel because the closure has no registry access.

anchor (optional) tells the host to scroll the help to a named anchor after rendering – used by the cmdline’s arg-aware <C-h> to land on arg:<name> directly.

Fields

§name: String

Canonical name (ex:write, motion:word-forward) or an ex-command alias.

§anchor: Option<String>

Help anchor to scroll to, e.g. arg:<name>; None = top.

§

DescribeBuffer

:describe-buffer. The host renders a snapshot of the current buffer’s view-relevant state (path, language, modal, cursor, dirty, line count, …).

§

Apropos

:apropos <pattern>. The host runs a substring search over every registered CommandSpec (name + doc) and renders the matches.

Fields

§pattern: String

Case-insensitive substring matched against names and docs.

§

DescribeKey

:describe-key <chord> (DESIGN.md §5.11). The host queries its keymap registry for every binding of chord (a chord may appear in multiple modes – Normal / Visual / Help, etc.) and renders them.

Fields

§chord: String

Canonical chord notation (<C-w>v, gg), as produced by the cmdline’s chord-capture slot.

§

ListKeymap

:keymap. The host renders the full default keymap grouped by mode.

§

BufferNext

:bn[ext] – cycle to the next open document buffer.

§

BufferPrev

:bp[rev] – cycle to the previous open document buffer.

§

FocusBuffer(u32)

CD.1: show the buffer with this id in the active pane.

The peer of Effect::ApplyEdit’s target: a plugin can edit a buffer it knows only by id, and this is how it shows one. Opening by path or by name cannot serve a buffer known only as an id — which is how a capture knows the buffer it was fired from.

Host-applied, so it works on the off-keystroke paths too. An id that no longer names a buffer is a no-op logged at debug!: a buffer closing between an action and its effect is an ordinary race, not a producer bug.

§

InvokeCommand

CD.3d: run a registered command after the effects before this one.

An action is dispatched with args typed; anything else runs as an ex line. The picker’s invoke-command outcome, as an effect.

Why an effect and not a host call. A plugin’s host calls run while its action runs — before the host applies any effect the action returns. Work that must happen only after a returned effect landed (capture deletes its draft only once the entry is filed) cannot be a host call; it goes here, after the effect it depends on. A write that does not land stops the batch, so this does not run.

Fields

§id: String

A registered command name. If it names a CommandKind::Action it is dispatched with args; otherwise id plus args is run as an ex line.

§args: Args

Arguments for the command (rendered onto the ex line in the ex-line case).

§

ListBuffers

:ls / :buffers – render every open document buffer in a help-style view.

§

ChangeDir(Option<String>)

:cd [path] – change the editor’s working directory (what relative paths in :e / :w resolve against). The path is user-typed: ~ expands, relative joins the current :cd directory. None changes to the user’s home directory.

§

PrintWorkingDir

:pwd – print the current working directory.

§

PrintProjectRoot

PR.2: :project-root – print the active buffer’s project root and which marker decided it. The introspection affordance for project resolution: “why is my terminal opening here” is otherwise only answerable by reading the source.

§

OpenBufferPicker

:b with no arg – open the vertico-style buffer switcher (DESIGN.md §5.9.7). The user types to filter, <CR> to switch. Type-aware completion in the cmdline can pre-fill the picker via :b <prefix> once that wiring lands; for now the no-arg form is the entry point.

§

OpenPicker

:picker <source> [args...] – canonical picker entry point. Dispatches by source against the host’s PickerRegistry. Unknown source ids surface as a host-side error echo; per-source arg shape is opaque to the grammar (the host re-parses args against the resolved source’s args_schema). Short per-source aliases like :files, :recent emit this same effect with the appropriate source set so the trait-driven dispatch + MRU pipeline runs uniformly.

Fields

§source: String

Registered picker-source name (files, recent, grep, …).

§args: Vec<String>

Raw whitespace-split arguments; the source re-parses them.

§root: Option<PathBuf>

PC.1: root this picker resolves against, overriding the active buffer’s project for this open only.

Why the CONTEXT and not an argument. A live source (grep) re-queries through on_query_changed, which receives the query and the context and NOT the open’s args — and the source is a shared &self generator with no per-open state. A root passed as an argument would therefore apply to the first query and silently revert to the workspace root on the next keystroke, which is worse than not having it. In the context it survives every re-query, and every root-sensitive source gets it without a per-source convention.

None is the ordinary case: resolve from the active buffer, exactly as before.

§fill_action: Option<String>

PC.11: this picker is being opened to answer a question, and the answer goes to the named command as its first argument.

The picker’s FillCaller outcome already means “hand this value to whoever opened me”; what it lacked was a destination a plugin can own. A guest owns none of the surfaces lattice_picker::FillTarget could name — not the document, not the : line, not a prompt — but it does own an ex-command, so that becomes the destination. Effect::OpenPrompt’s on_submit_action is the same shape for the same reason, and the asymmetry between the two (a guest could be handed a prompt’s answer but not a picker’s) is what this closes.

Not a flag that overrides the source’s accept. The source decides what accepting one of ITS candidates means; file-pick and dir-pick exist as separate sources precisely so that “supply a value” is a source’s own decision rather than a caller’s override. This names where a FillCaller lands, which is a question FillTarget already owns.

None leaves the existing behaviour untouched: the target is whatever surface was captured at open.

§query: Option<String>

CD.6a: text the picker’s query starts with. For a static source it narrows the rows from the first frame, as emacs’s completing-read initial input does; org-roam’s node insert seeds it from the active region. None is an empty prompt (or a live source’s own seed).

§

BufferDelete

:bd[elete][!] – close the active document buffer. force = true discards unsaved changes.

Fields

§force: bool

! – close even if modified.

§

OpenFileTree

:Tree [path] – open a file-tree buffer rooted at path. Absent = the document’s parent directory.

Fields

§root: Option<PathBuf>

Directory to root the tree at.

§

CloseFileTree

:TreeClose – dismiss the file-tree buffer.

§

OpenOil

:Oil [path] – open an oil buffer for path (flat editable listing). Absent = current document’s parent directory / cwd.

Fields

§dir: Option<PathBuf>

Directory to list.

§

DescribeOption

:describe-option NAME – render the option’s metadata in a help view.

Fields

§name: String

Option name as used with :set.

§

DescribeElement

:describe-element NAME / :describe-face NAME (T.9.d) – render a registered theme element’s metadata in a help view: owner, doc, the authoring (reference-form) StyleSpec default (palette keys + inherit parent), and the concrete resolved Style. The introspection counterpart of :describe-option / :describe-mode for theme elements. The host reads the ThemeRegistry::describe snapshot; an unknown name echoes an error.

Fields

§name: String

Theme element name (modeline.mode, diagnostic.error, …).

§

ListOptions

:options – list every registered option.

§

DescribePluginApi

:describe-plugin-api [<seam>] (PI.2) – render the plugin-API catalog. With seam = an interface name (host-services, picker-source, …), render that one interface’s functions + direction + capability. Without, render the same as :list-plugin-apis. The catalog is derived from wit/ at build time (lattice-plugin-api); the host holds no plugin runtime.

Fields

§seam: Option<String>

WIT interface name; None lists every interface.

§

ListPluginApis

:list-plugin-apis (PI.2) – list every plugin-API interface the wit/ package exposes, one row per interface (name + direction + capability + function count).

§

ExportPluginApi

:export-plugin-api [markdown|json] (PI.2b) – dump the whole plugin-API catalog into a savable synthetic buffer (*plugin-api.md* / *plugin-api.json*) the author saves with :w <path>. format defaults to markdown; json selects the machine-readable form. The host owns the dump + buffer open (the OpenSyntheticBuffer pattern).

Fields

§format: Option<String>

"markdown" (the default when None) or "json".

§

ListCommands

:list-commands (PI.3) – enumerate every registered command grouped by source layer (built-in / user config / plugin / …). The one introspection enumeration the help family was missing; a plugin group resolves the plugin id to its manifest name where the host knows it.

§

DescribePlugin

:describe-plugin <name> (PI.4, Facet B) – render one loaded plugin’s own documentation + contributions. The doc comes from the plugin (embedded WIT world doc / manifest doc), resolved once at load. Unknown / no-plugin-loaded echoes an error. Loaded-plugin enumeration is Phase-8-gated; the surface + registry seam exist now.

Fields

§name: String

The plugin’s manifest name.

§

ListPlugins

:list-plugins (PI.4) – list every loaded plugin (name + doc summary). Empty until the Phase-8 loader populates the registry.

§

OpenHover

:hover [text] – open a hover popup at the cursor with text as the markdown body. A manual / testing entry point; LSP hover (K) goes through Effect::Lsp instead.

Fields

§markdown: String

Markdown body of the popup.

§

DismissPopup

Dismiss the active popup, whatever its content. Content-agnostic (routes through dismiss_popup); produced today by :HoverClose.

This is the USER’s verb — “close what I am looking at”. It is the right one for a key the user pressed (:popup-dismiss, magit’s q, answering a permission prompt), because the popup they mean is the one on screen by definition. It is the WRONG one for a mode dismissing on its own schedule — see Effect::DismissPopupNamed.

§

DismissPopupNamed

Dismiss the active popup only if it is the named one — a no-op otherwise.

name is the popup buffer’s synthetic name, the same string Effect::OpenPopup was given.

Why the pair exists. There is one popup slot (Editor::popup_buffer), so Effect::DismissPopup means “close whatever is in it”. For a keypress that is exactly right. For a BACKGROUND dismissal it is a bug waiting to be written: between the moment a mode decided to close its popup and the moment the effect is applied, the slot may hold someone else’s — and the mode closes that instead, silently, with nothing in the type to warn it.

Which-key wrote that bug. Its timer-driven dismissal fired on every resolved chord, so any two-key sequence typed faster than the popup delay (zz, gg, dd, ci") tore down whatever hover or diagnostic popup happened to be showing. Naming the target makes the stale case a no-op instead of a wrong action.

Rule of thumb: a dismissal the user asked for is Effect::DismissPopup; a dismissal a mode decided on is this.

Fields

§name: String

The popup buffer’s synthetic name.

§

OpenPopup

Show a popup overlay at placement with the given focus (popup-api.md §4.3). Content-agnostic and data-only: the host idempotently ensures a popup buffer named name under major mode mode_id (Editor::open_popup_named) and the owning mode’s on_activate projects the content. Name-based, not id-based, because the emitters (the :ai-permission ex-command, the async tick callback) have no host access to register a buffer and supply a BufferId — a name keeps the effect vocabulary the host boundary, like OpenSyntheticBuffer.

Fields

§name: String

Synthetic name of the popup buffer; reused if it exists. Also the key Effect::DismissPopupNamed matches on.

§mode_id: String

Major mode for the buffer, by mode-id string; must be registered.

§placement: PopupPlacement

Where the popup floats (cursor-anchored, centred, …).

§focus: PopupFocus

Whether the popup takes keyboard focus.

§

OpenHelpTopic

:help [topic] – open a free-form help topic. With no topic the host renders the index (docs/user/README.md equivalent); with a topic the host looks it up in its help-topic registry and surfaces the body in a help buffer. Unknown topics echo an error.

Fields

§topic: Option<String>

Topic name; None opens the index.

§

ListDiagnostics

:diagnostics – render every workspace diagnostic in a help-style buffer with clickable per-entry source links (Phase 4.1.d.iv). The host queries its LspSupervisor::diagnostics() layer and formats.

§

ListErrors

CM.8: :clist / :cl — open the error list in a fuzzy picker (the flat browse-and-jump surface, parallel to :diagnostics). Complements :cnext (step) and :copen (the *problems* multibuffer). Host builds it from Editor::error_list.

§

NextDiagnostic

]d / :diag-next – move the cursor to the next diagnostic in the active buffer. Wraps to top.

§

PrevDiagnostic

[d / :diag-prev / :cprev – move the cursor to the previous diagnostic in the active buffer. Wraps to bottom.

§

OpenLspLog

:lsp-log [server] (Phase 4.1.g) – open the subsystem log buffer (*lsp*) when server_id is None, or the per-server log (*lsp:<server>*) when set.

Fields

§server_id: Option<String>

Server id (as in :lsp-status); None = the subsystem log.

§

OpenAiLog

:ai-log [provider] (AI-1b) – open the per-session AI log buffer (*ai:<provider>:<index>*). With no known session, echoes an info hint; with exactly one, opens it directly; with more (optionally narrowed by the session provider prefilter), raises a picker. Peer-applied via the host’s do_open_ai_log, exactly like Effect::OpenLspLog. The lattice-ai crate owns the :ai-log binding + this emission; the host owns only the generic ensure_named_synthetic_document + AiLogMode open.

Fields

§session: Option<String>

Provider-prefix filter over known sessions; None = all.

§

OpenSyntheticBuffer

Open (or focus) a named synthetic buffer under a given major mode – the generic primitive behind provider-owned buffer views (e.g. the ai-conversation *ai:opencode* buffer). The emitter (a mode’s command handler) supplies the buffer name + the mode id; the host owns only the generic ensure_named_synthetic_document open, so no provider-specific host method is added. mode_id is the mode’s string id (ModeId::new(&mode_id)); the mode must be registered at boot.

OC.7a adds content / cursor / activate_minor. A native mode fills its own buffer from on_activate; the modes WIT seam is declaration-only, so a PLUGIN mode has no such hook and a guest emitting this got a buffer it could never put text in. Effect::ApplyEdit is not the way out either — it names a target buffer id this effect does not hand back. All three are None for every pre-OC.7a emitter.

Fields

§name: String

Buffer name (*ai:opencode*); an existing buffer of that name is focused rather than recreated.

§mode_id: String

Major mode id; must be registered at boot.

§content: Option<String>

Seed text, applied BEFORE the buffer is shown so the first frame is the finished one. Ignored when the buffer already existed — a re-open must not overwrite what the user has typed, which is the difference between reopening a capture and losing one.

§cursor: Option<Position>

Where to leave the caret in content (org capture’s %?). Out-of-range is clamped, not refused: a template whose %? sits past its own text is a template bug that must not cost the capture.

§activate_minor: Option<String>

A minor to activate alongside the major. Mirrors Effect::SpawnTerminal’s activate_minor and exists for the same reason — the interesting behaviour rides a general-purpose major.

§

OpenSyntheticBufferAt

MG.50: Effect::OpenSyntheticBuffer + cursor placement, in one step.

The synthetic peer of Effect::OpenBufferAt, and it exists for exactly the same reason: the open is peer-applied while a cursor effect runs host-side against whatever buffer is active at that moment, so Many([open, move]) cannot land the caret on a buffer that does not exist yet. Emitted by magit’s <CR>, which opens a staged blob at the line the cursor was reading in the diff.

Fields

§mode_id: String
§position: Position

Cursor target in the opened buffer, 0-based (line, byte).

§

OpenMessages

:messages – open the *messages* buffer (the emacs *Messages* analogue). Renders a chronological view of every echo / minibuffer notification; live-tails as new entries arrive via the typed event bus.

§

OpenDashboard

:dashboard – open (or re-compose + activate) the *dashboard* launch page. The applier reads config, composes the enabled sections via the crate-owned DashboardRegistry service, and seeds a read-only BufferKind::Dashboard buffer. See docs/dev/architecture/dashboard.md §9.

§

ToggleLspTrace

:lsp-trace <server> – pure toggle of JSON-RPC tracing for the server. The trace buffer is opened separately via :lsp-trace-log <server> so peeking mid-stream doesn’t flip the toggle off.

Fields

§server_id: String

Server id (as in :lsp-status).

§

OpenLspTraceLog

:lsp-trace-log [server] – open the JSON-RPC trace ring (*lsp:<server>:trace*) in the active pane via the vertico picker (Phase 3). No arg = picker over every running instance; arg = pre-filter; single match short- circuits the picker. Independent of the trace toggle.

Fields

§server_id: Option<String>

Server-id filter; None = pick among all running servers.

§

LspStatus

:lsp-status – render every running server (id, root, pid, uptime, capability summary) in a help-style buffer.

§

LspDiagnosticsToErrorList

EP.4 (2026-08-10): :lsp-diagnostics-to-error-list – pull the current published diagnostics into the error list’s Lsp slice on demand.

The manual peer of the live feed gated by lsp.diagnostics-to-error-list. Useful when that option is off, and as a forced refresh after a server restart when it is on. Echoes the entry count, because this surfaces what servers have published – not a workspace scan – and an empty result must not be misread as a clean tree.

§

LspServerLogListing

:lsp-server-log – picker-style listing of every running server actor with workspace root + buffer count + capability summary, each row carrying exec: links to the per-server log + trace buffers. Use vim search (/query) to filter rows; press <CR> on a link to open. A real fuzzy picker arrives with the bundled fuzzy-finder plugin (Phase 8b).

§

LspRestart

:lsp-restart <server> – ask the LSP supervisor to restart the server. Runs asynchronously on the LSP runtime; the outcome is written to the LSP log.

Fields

§server_id: String

Server id (as in :lsp-status).

§

LspProgressCancel

:lsp-progress-cancel [server] – send window/workDoneProgress/cancel for every active, cancellable progress entry on the named server (or, with no arg, on every server currently attached to the active buffer). Non-cancellable entries are left alone — the host’s cancel is best-effort regardless. 4.4.c.

Fields

§server_id: Option<String>

Server id; None = every server attached to the active buffer.

§

LspExpandRegion

4.4.e: :lsp-expand-region – structural smart- expansion. First invocation fires textDocument/selectionRange at the cursor; each subsequent invocation walks one parent step outward in the cached chain. Enters Visual mode with the resolved range as the selection.

§

LspShrinkRegion

4.4.e: :lsp-shrink-region – inverse walk through the cached selection-range chain. No-op when the chain is empty / at the innermost step.

§

SetLspLogLevel

:lsp-log-level [server] <level> – set the subsystem- wide default min level (when server_id is None) or a per-server override.

Fields

§server_id: Option<String>

Server id; None = the subsystem-wide default.

§level: String

trace, debug, info, warn / warning or error; anything else is echoed as an error.

§

LspLogClear

:lsp-log-clear [server] – drop the ring’s records. None clears the subsystem-wide ring; a server id clears that ring.

Fields

§server_id: Option<String>

Server id; None = the subsystem-wide ring.

§

LspDocumentSymbol

:lsp-symbols – open a picker over the active document’s LSP symbol outline (textDocument/documentSymbol). Phase 4.2.e.

§

LspWorkspaceSymbol

:lsp-workspace-symbol [query] – open a picker over workspace-scoped symbols matching query (server-side substring filter). Phase 4.2.f.

Fields

§query: String

Query sent to the server; empty asks for everything.

§

LspIncomingCalls

:lsp-incoming-calls – 4.5.a. Prepares call-hierarchy items at the cursor, fans out callHierarchy/incomingCalls for the first item, opens the merged caller list as a vertico picker. “Who calls this function?”

§

LspOutgoingCalls

:lsp-outgoing-calls – 4.5.a. Symmetric peer of LspIncomingCalls. “What does this function call?”

§

LspSupertypes

:lsp-supertypes – 4.5.b. Same shape as LspIncomingCalls but for type relationships: prepares type-hierarchy items, fans out typeHierarchy/supertypes, opens the picker. “What does this type subtype?”

§

LspSubtypes

:lsp-subtypes – 4.5.b. Symmetric peer of LspSupertypes. “What subtypes this type?”

§

LspMoniker

:lsp-moniker – 4.5.g. Fires textDocument/moniker at the cursor; echoes the resulting moniker list (scheme + identifier + unique level + optional kind). Useful for indexers (SCIP / LSIF) + cross-repo navigation; the result surfaces as a one-line summary, not a picker.

§

LspCodeLens

:lsp-code-lens – 4.5.d. Open a picker over the cached textDocument/codeLens entries for the active buffer. Accept routes the chosen lens’s command through workspace/executeCommand (after a lazy codeLens/resolve if the lens arrived without a command).

§

LspColorPresentation

:lsp-color-presentation – 4.5.e. At the cursor, look up the color literal in the per-buffer documentColor cache and fire textDocument/colorPresentation to fetch alternative formats (named, rgb(), hex, etc.). Open a picker; accept splices the chosen alternative.

§

LspFormat

:lsp-format – run textDocument/formatting on the highest- priority server with documentFormattingProvider and apply the returned edits as one undo unit. Phase 4.3.

§

Format

IN.8b: :format – format the whole buffer through the availability cascade (LSP if a server advertises formatting, otherwise formatprg or the built-in per-language table).

Distinct from LspFormat, which is LSP-only by name and stays that way. :format is the LSP-INDEPENDENT command, which is why its generic name satisfies the ex-command naming rule rather than violating it: that rule exists because a generic name implies “works regardless of LSP”, and this one does.

§

LspFormatRange

:lsp-format-range – run textDocument/rangeFormatting over the active Visual selection (when in Visual mode) or the supplied line range. Apply edits atomically. Phase 4.3.

§

LspSignatureHelp

:lsp-signature-help (or trigger-character driven). Send textDocument/signatureHelp to attached servers; first non-empty response renders into the hover popup.

§

LspComplete

:lsp-complete – fire textDocument/completion at the cursor and open the merged item list as a vertico picker. Phase 4.2.g.

§

LspRename

:lsp-rename <new-name> – run textDocument/prepareRename (when advertised) then textDocument/rename; apply the returned WorkspaceEdit as one undo unit across every affected buffer. Phase 4.3.

Fields

§new_name: String

The new identifier.

§

LspCodeAction

:lsp-code-action – run textDocument/codeAction at the cursor / selection; open the merged item list as a vertico picker. Accept routes through resolve (when needed) and applies the action’s WorkspaceEdit / command. Phase 4.3.

§

ExpandSnippet

SN.3c.1: direct snippet expansion over a known trigger range. The mode-owned <C-x><C-s> handler (snippet-mode’s action_handlers()) does the word-prefix scan and emits this with replace_range = token-start..cursor; the host owns resolution + expansion (language detection, registry lookup, variable render, buffer splice + session install) via Editor::expand_snippet_from_range. A deliberate first-party effect: the typed Effect enum stays a host-owned vocabulary (feedback_effect_vocabulary_is_host_boundary) — the mode owns the trigger, the host owns the expansion mechanics. No-op (quiet info echo) when no snippet matches the prefix.

Fields

§replace_range: Range

The trigger text to replace, half-open (line, byte) range on a single line of the focused buffer; its text is the prefix looked up (active language first, then *).

§

ReloadSnippets

:reload-snippets – re-read every snippet file from disk and rebuild the per-language registry (Phase 4.2.g.4). Useful after editing a .code-snippets / .json file in the project’s snippet directory.

§

DescribeEvents

:describe-events – render a help buffer listing every registered event (M.5.3.c). Walks lattice_protocol::event_registry::EVENT_DESCRIPTORS and formats each as name :: source-crate :: doc.

§

DescribeDiff

:describe-diff – render a help buffer listing every active diff session (D.2.d). Walks the host’s DiffSubsystem::describe_sessions and formats each row as BufferId | Algorithm | Rev | Hunks | Watches.

§

DiffOpen

:diff (no args) – open an inline diff session for the active document against its on-disk content. D.3.a.1.

§

DiffOff

:diffoff[!] – close the active pane’s diff session (if any). v1 two-way semantics collapse :diffoff and :diffoff! to the same teardown (removing one side of a two-way diff leaves the other degenerate, so both drop the whole session). The bang is a forward-compat surface: D.6 (three-way merge) will distinguish per- participant removal (:diffoff) from full-session teardown (:diffoff!). The handler reads the session’s watch list as the source of truth for participants — the tab is not a grouping unit. D.3.a.1 / D.4.d.3.a.

Fields

§force: bool

!; currently identical to the plain form (see above).

§

Diffthis

:diffthis – stage the active pane for a two-pane diff; the second :diffthis invocation in a different pane completes the session (creates a DiffSession against the two live buffers, a PaneGroup with HunkRowMapper, and FillerRowProviders on each side). Same pane twice unstages. Third-pane staging errors out — v1 is two-way only; multi-way arrives with D.6 three-way merge. D.4.d.3.a.

§

Diffsplit

:diffsplit <file> [<remote>] – open <file> (and optionally <remote>) in new vertical splits and register a diff session between the current pane and the new pane(s).

  • One arg (:diffsplit base): two-way diff between the current pane (current side) and a new pane loading base (baseline side). D.4.d.3.b.
  • Two args (:diffsplit base remote): three-way merge with the current pane as “local”, a new pane loading base as the common ancestor, and a third new pane loading remote as the other side. D.6.c.

Composes vsplit + :edit <path> in each new pane + the appropriate registration helper (register_two_pane_diff for one arg, register_three_pane_diff for two). Empty first arg errors at parse time. Cursor lands in the first new pane (vim parity).

Fields

§path: PathBuf

The baseline (two-way) or common-ancestor (three-way) file.

§remote: Option<PathBuf>

The “other side” for a three-way merge; None = two-way.

§

DiffGetCmd

:diffget [<bufnr>] – pull the hunk under the cursor from the named (or auto-resolved) buffer side. D.6.d. target is the optional buffer number passed by the user; None means “the peer side” (two-way: unique; three-way: ambiguous — dispatch emits “target required”). The chord-driven do operator stays unit-variant Action::DiffGet; this ex-command variant is a parallel entry point for explicit-target invocations.

Fields

§target: Option<u32>

Buffer number to pull from; None = the unique peer side.

§

DiffPutCmd

:diffput [<bufnr>] – push the hunk under the cursor into the named (or auto-resolved) buffer side. D.6.d. Mirror of Self::DiffGetCmd but for the put direction.

Fields

§target: Option<u32>

Buffer number to push into; None = the unique peer side.

§

DiffAccept

:diff-accept – resolve the active pane’s diff session with lattice_diff::DiffOutcome::Accept. v1 semantics: equivalent to :diffoff + signal Accept on the session’s completion channel (if any). The buffer’s current content (whatever the user applied via do/dp or left alone) becomes the accepted resolution; plugins consuming the outcome commit from there. D.6.e.

§

DiffReject

:diff-reject – resolve the active pane’s diff session with lattice_diff::DiffOutcome::Reject. v1 semantics: equivalent to :diffoff! + signal Reject. Plugins consuming the outcome should revert any pre-session state. D.6.e.

§

DiffAcceptAll

:diff-accept-all — resolve EVERY pending review (each session with a bound completion) with lattice_diff::DiffOutcome::Accept. The bulk counterpart to :diff-accept for when several agent reviews are open at once.

§

DiffRejectAll

:diff-reject-all — resolve EVERY pending review with lattice_diff::DiffOutcome::Reject. Bulk counterpart to :diff-reject.

§

CloseSessionDiffs

D-fix.6: an IDE-peer connection’s close_tab — tear down (as a Reject) every programmatic diff session that THIS connection (origin_session) opened, regardless of how/where it is displayed. Host-applied: it fires each matching session’s bound completion oneshot with lattice_diff::DiffOutcome::Reject and closes its panes (the :diff-reject teardown, but targeted by origin_session rather than the active pane). If the connection opened no diff, the host falls back to closing the active buffer when tab_name matches its path (the legacy I3 file-close — the only remaining tab_name use, orthogonal to the diff teardown).

Fields

§origin_session: u64

The originating connection id; only diffs tagged with it are torn down (0 = none — a non-IDE producer’s diff is never matched). Cross-session isolation: connection A’s close can never affect connection B’s diffs.

§tab_name: String

The agent’s close-tab label, used ONLY for the legacy active-buffer file-close fallback when no diff matched.

§

CloseAllSessionDiffs

D-fix.6: an IDE-peer connection’s closeAllDiffTabs — tear down (as a Reject) every programmatic diff session origin_session opened. Same scoping as Self::CloseSessionDiffs but with no file-close fallback (it is unambiguously a diff-only bulk close).

Fields

§origin_session: u64

The originating connection id; scopes the bulk teardown.

§

NextHunk

]c / :hunk-next – jump cursor to the start of the next diff hunk on the current side (ranges[1]). Wraps to top. D.3.c.

§

PrevHunk

[c / :hunk-prev – jump cursor to the start of the previous diff hunk on the current side. Wraps to bottom. D.3.c.

§

DescribeEvent

:describe-event <name> – render the descriptor for a single registered event (M.5.3.c). The introspection counterpart of :describe-command for events.

Fields

§name: String

Event name as listed by :describe-events.

§

ListModes

:list-modes – render every registered mode in a help buffer (M.8). Groups by kind (Major / Minor); each row shows the mode’s id and current activation state on the active buffer. The mode counterpart of :options.

§

DescribeMode

:describe-mode <name> – render one mode’s metadata (M.8): id, kind, contributed option overrides, required capabilities, and current activation state on the active buffer. The introspection counterpart of :describe-command / :describe-option / :describe-event for modes.

Fields

§name: String

Mode id (rust-mode, magit-core-mode).

§

DescribeActiveModes

:describe-active-modes (<C-h>m) – render the mode stack live on the active buffer: the major plus every minor, each with the chords it contributes.

Distinct from Effect::DescribeMode, which describes one named mode whether or not it is active, and from Effect::ListModes, which lists every registered mode. This one answers “what is this buffer, and what can I press in it”.

Major-only would under-report by construction: the minor-mode convention deliberately pushes chords shared across majors out into a minor (magit’s gr / q / ]] live on magit-core-mode, not on each magit major), so the view is major + minors.

An additive variant rather than widening DescribeMode’s name to Option<String> — the WIT declaration is describe-mode(string) and widening it would break a published plugin API.

§

DescribeActiveBindings

:describe-bindings (<C-h>K) – the chords that can actually fire on the active buffer: builtin entries live in the current binding-mode, plus every active mode’s contributions.

Distinct from Effect::ListKeymap (:keymap), which renders the whole static catalog regardless of what is active. :keymap stays the exhaustive reference; this one answers “what can I press here”.

§

DescribeOptionResolution

:describe-option-resolution <name> – show which resolver layer (modal / buffer-local / mode contribution / typed-option / default) provides the resolved value for <name> on the active buffer (M.8). Helps debug surprising values when a mode’s contribution shadows a :set write or vice versa.

Fields

§name: String

Option name as used with :set.

§

Customize

:customize [name] – open the customize buffer (M.9). With no arg, opens the group + mode picker. With an arg ending in -mode, opens the focused view of that mode’s contributed options. Otherwise, opens the cross-mode group view (every option in the named group, sectioned by owning mode).

M.9.0 ships the read-only listing form. M.9.1 wires per-row navigation + Enter-to-edit; for now edits run via the existing :set machinery on the cmdline.

Fields

§name: Option<String>

Group or *-mode name; None = the picker.

§

Tutor

:tutor [N] – open the interactive tutor lesson N (default: 1) in a fresh editable buffer. The lesson content is embedded in the binary and copied to a temp file each time so the user starts fresh and can practice motions / operators on the file itself (vim-tutor pattern). Lessons are the docs/user/tutor/lesson-N.md files embedded by the host.

Fields

§lesson: Option<u32>

1-based lesson number; None = lesson 1.

§

ToggleMode

:<mode-name> – toggle a registered mode on the active buffer (M.5.1; mode-architecture §9.6.1). For minors: activate if inactive, deactivate if active. For majors: activate if not currently the major; reload (deactivate then re-activate) if it’s already the active major. Mode resolution is by name, not id object, because the grammar crate stays renderer-agnostic and doesn’t depend on lattice-mode.

Fields

§mode_name: String

Mode id string, e.g. "auto-pair-mode".

§

AppAction(AppEffect)

Free-form App-side effect produced by a CommandKind::Action dispatch. Carries an AppEffect – the typed App-side counterpart to the dispatcher-native variants above. The host’s apply_effect matches the inner AppEffect to drive chord-bound work that has no grammar concept attached (<Esc> exits Visual, <C-w>v splits a pane, o opens a line below). Slice 8.i wires this surface; see docs/dev/notes/8i-approach.md.

§

RecordJump

M.10.3 (2026-06-03): record the editor’s CURRENT cursor + active buffer onto the position-history ring as an AutoJump entry — vim’s jump-list semantics for “big motions” (gg, G, /, *, mark jumps, …). Used by mode-contributed jump actions (search <CR>, lsp-references <CR>, future project-diff <CR>) so <C-o> walks the user back to where they were before the jump. Must be the FIRST sub-effect inside an Effect::Many that also opens a new buffer — the host reads the cursor/active-buffer state at apply time, so after OpenBuffer lands the recorded entry would be the new doc’s start, not the pre-jump location.

§

Confirm

Open a yes/no confirmation dialog. The host shows a transient picker with prompt as the title; y dispatches yes_action with args, n / q / Esc dismisses.

IX.1: args is what makes the confirmed thing and the executed thing the same thing. Without it a yes-half has to re-derive its target when it fires, and the context it derives from is not stable across the wait — a background refresh can rebuild the buffer and move the cursor while the dialog is up, so the action lands on a different target than the prompt named. Carrying the target closes that window by construction.

Carry the payload, not a pointer to it: a path, a SHA, a synthesized patch — not a cursor row or a row span, which a rebuild invalidates. For patch-shaped payloads this also makes git apply’s context check refuse a stale one loudly instead of applying it somewhere plausible.

Args::None keeps the pre-IX.1 behaviour (the yes-half re-derives), so existing confirms are unaffected until migrated.

Why a name + Args rather than a CommandInvocation, which is otherwise the canonical “thing to execute” (design §5.2.1): this effect has to cross the plugin seam, and CommandInvocation has no WIT mirror — it is exactly why Effect::Global fails at the boundary. Args is mirrored and a name is a string, so this payload crosses. A name is also the plugin-native form: plugins register actions by name and cannot hold a host CommandId.

Fields

§prompt: String

The question shown as the dialog title.

§yes_action: String

Registered action name dispatched on y.

§args: Args

Arguments passed to yes_action (see above).

§

BuryBuffer

Return the active pane to the buffer it was displaying before a full-pane synthetic buffer took it over, and drop that buffer’s hold on the pane (“bury”, in vim’s sense — the buffer stays in the registry, it just stops being shown).

Distinct from Effect::DismissPopup on purpose. A popup floats over the pane: the underlying document is never swapped out, so dismissing one only has to drop the overlay. A synthetic buffer opened full-pane (magit’s views, oil, the plugin manager) genuinely replaced the pane’s buffer AND the editor’s active-document handle, so returning has to swap both back. magit’s q used DismissPopup for this and left the active document pointing at magit while the pane pointed at the file — the pane said one thing and the screen painted another.

A no-op when nothing was buried (no origin to return to), so a mode can bind it unconditionally.

§

KillBuffer

Effect::BuryBuffer, then delete the buffer that was buried: the pane returns to where it came from and the buffer is gone.

For a buffer whose life ends at a verb — a compose buffer on finish or cancel (magit’s commit, note and rebase-todo buffers; with-editor’s C-c C-c / C-c C-k). Bury alone keeps the buffer, and a synthetic buffer reopened by name is reused WITHOUT being re-seeded, so the next commit came back holding the previous message.

No dirty check: the emitting mode has decided the buffer is done. With no origin to return to it behaves as :bd!.

§

OpenTransient

Open a named transient picker menu. source is a name registered into a TransientSourceRegistry (lattice-picker) by the owning mode crate at boot — mirrors OpenPicker’s named-source shape. The registry, not this enum, holds the actual TransientSpec builder, since TransientSpec lives in a crate downstream of lattice-grammar. TR.3a: args are the arguments this open was requested with; they reach the builder as TransientContext::args.

Without them a menu cannot drill down — a row that opens a second menu has no way to say what it opened it FOR. Org’s capture menu has a row per template and the fields menu it opens must know which template it is collecting for; the only alternative is guest memory, which <Esc> never clears, so the next open would inherit the last one’s subject.

Args::None is a plain open, which is every native menu today.

Fields

§source: String

Registered transient-source name.

§args: Args

Context for the menu builder (see above).

§

OpenPrompt

Open a one-line minibuffer text prompt. prompt is shown as an info-level echo label; initial pre-seeds the input buffer’s content; on_submit_action names a registered action:* handler fired on <CR> with the typed text available as ActionContext::prompt_value (never a closure — same name-based-lookup convention as Confirm’s yes_action and OpenTransient’s source, so the variant stays a plain, serializable value with no crate carrying a callback type downstream of lattice-grammar). buffer_name, when set, becomes the synthetic prompt buffer’s name — callers use this to stash context for the submit handler to read back (mirrors how magit’s blame/rebase/revision modes encode their target in the buffer name); None uses a default unnamed prompt buffer. <Esc> cancels without firing anything.

Fields

§prompt: String

Label shown for the prompt.

§initial: String

Initial input text; may be empty.

§on_submit_action: String

Registered action:* name run on <CR>.

§buffer_name: Option<String>

Name for the prompt buffer (context for the submit handler); None = default.

§

Many(Vec<Effect>)

Several effects, applied in order (the host flattens nested Many). Host-applied children all run before any peer-applied child. A Effect::WriteToFile that fails stops the remainder; nothing else does. Order matters: Effect::RecordJump must precede the open it records, and a cursor effect must follow the edit it positions after.

Implementations§

Source§

impl Effect

Source

pub fn is_none(&self) -> bool

true only for Effect::None. An empty Effect::Many is not None, nor is Effect::Declined.

Trait Implementations§

Source§

impl Clone for Effect

Source§

fn clone(&self) -> Effect

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for Effect

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

§

impl<T> Instrument for T

§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided [Span], returning an Instrumented wrapper. Read more
§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
§

impl<T> WithSubscriber for T

§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a [WithDispatch] wrapper. Read more
§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a [WithDispatch] wrapper. Read more