types
Direction: shared types only (not called directly) · Capability: none (pure data / dispatch) · Worlds: auto-pair-plugin (imports), comment-plugin (imports), completion-source-plugin (imports), context-plugin (imports), decorations-plugin (imports), events-plugin (imports), grammar-plugin (imports), media-plugin (imports), multibuffer-view-plugin (imports), picker-source-plugin (imports), plugin (imports), project-plugin (imports), scanned-excerpt-source-plugin (imports), transient-source-plugin (imports), treesitter-context-plugin (imports)
Shared boundary records/variants — the owned, WIT-serializable mirrors of the native grammar + picker/completion types (plugin-host.md §4). Every interface that crosses one of these uses it from here; the host round-trips native ↔ these generated types via the WitBoundary adapter trait (boundary.rs, PH7.3a). Bulk rope text never rides these records — it crosses via the buffer document resource handle (PH7.3c).
Populated incrementally across PH7.3: args/arg-value (PH7.3a), raw-candidate + picker-accept-outcome (PH7.3a), the effect variant mirror (PH7.3b). Types whose native form carries a nested CommandInvocation (e.g. arg-value::invocation) are deferred to the command mirror (§4.1) and cross as a typed error until then.
Functions (0)
(none — a shared type interface)
Types (147)
variant arg-value
variant arg-value {
string(string),
char(char),
bool(bool),
int(s64),
pattern(string),
chord(string),
raw(string),
}
Mirrors lattice_grammar::args::ArgValue. The native Invocation(Box<CommandInvocation>) variant is intentionally absent until the command mirror lands (§4.1); crossing it before then is a typed WitBoundary error, never a lossy encoding.
variant args
variant args {
none,
char(char),
string(string),
bytes(list<u8>),
list(list<arg-value>),
}
Mirrors lattice_grammar::args::Args. bytes is the msgpack escape hatch (Args::Bytes) retained for now; typed calls prefer list.
variant candidate-kind
variant candidate-kind {
command,
option,
file,
directory,
pattern,
buffer,
register,
mark,
chord,
plain,
extension(u32),
}
Mirrors lattice_completion::candidate::CandidateKind.
Cases
commandoptionfiledirectorypatternbufferregistermarkchordplainextension:u32— Plugin-defined kind; the u32 is the registered kind tag.
record candidate-file
record candidate-file {
path: string,
is-dir: bool,
size: option<u64>,
}
record candidate-option
record candidate-option {
name: string,
current-value: string,
doc: string,
}
record candidate-option-value
record candidate-option-value {
option-name: string,
value: string,
doc: string,
}
record candidate-chord
record candidate-chord {
chord: string,
mode-label: string,
doc: string,
}
record candidate-register
record candidate-register {
name: char,
preview: string,
}
record candidate-mark
record candidate-mark {
name: char,
position: string,
}
record candidate-extension
record candidate-extension {
kind-id: u32,
payload: list<u8>,
}
variant candidate-data
variant candidate-data {
file(candidate-file),
option(candidate-option),
option-value(candidate-option-value),
chord(candidate-chord),
register(candidate-register),
mark(candidate-mark),
plain,
extension(candidate-extension),
}
Mirrors lattice_completion::candidate::CandidateData. The native Command { .., source: SourceLocation } variant is intentionally absent: SourceLocation is recursive (DotRepeat(Box<Self>)), which a WIT variant cannot express directly, and command candidates are a native-generator concern, not a plugin one. Crossing it is a typed WitBoundary error until the provenance mirror lands — never lossy.
Cases
file:candidate-fileoption:candidate-optionoption-value:candidate-option-valuechord:candidate-chordregister:candidate-registermark:candidate-markplainextension:candidate-extension— Plugin-defined arbitrary payload (theExtensionhatch): thekind-idroutes to the registering plugin's annotator, which decodespayload.
variant special-key
variant special-key {
esc,
enter,
tab,
backspace,
space,
up,
down,
left,
right,
home,
end,
page-up,
page-down,
insert,
delete,
f(u8),
}
Mirrors lattice_protocol::chord::SpecialKey. f carries the function-key number (1..=24; 0 is invalid and rejected at the boundary).
variant key-kind
variant key-kind {
char(char),
special(special-key),
}
Mirrors lattice_protocol::chord::KeyKind.
Cases
char:charspecial:special-key
record key-chord
record key-chord {
key: key-kind,
mods: u8,
}
Mirrors lattice_protocol::chord::KeyChord. mods is the raw KeyMods bitfield (Ctrl=1, Shift=2, Alt=4, Super=8) — structural, not lossy.
Fields
key:key-kindmods:u8
record annotation-segment
record annotation-segment {
text: string,
slot: string,
}
Mirrors lattice_completion::candidate::AnnotationSegment — one run of marginalia text sharing a theme slot key.
record annotation-custom
record annotation-custom {
text: string,
slot: string,
}
Payload of annotation::custom (the plugin escape hatch): pre-formatted text + a theme slot key.
record annotation-styled
record annotation-styled {
category: string,
segments: list<annotation-segment>,
}
Payload of annotation::styled (§8: a multi-slot column cell) — a category key plus per-segment slot-keyed runs (file-permission strings, size+unit, …).
variant annotation
variant annotation {
kind(string),
doc-snippet(string),
keybinding(list<key-chord>),
source(string),
custom(annotation-custom),
styled(annotation-styled),
}
Mirrors lattice_completion::candidate::Annotation (whole closed enum).
Cases
kind:stringdoc-snippet:stringkeybinding:list<key-chord>source:stringcustom:annotation-customstyled:annotation-styled
record display-span
record display-span {
start: u32,
end: u32,
slot: string,
}
PS.1: a styled run of a candidate's display text.
start / end are BYTE offsets into display, half-open. A range that is out of bounds, inverted, or not on a UTF-8 boundary is dropped with a warning naming the source — one bad span must not cost a row its other runs, and must never panic the picker.
slot is a capture or theme-element name, resolved host-side through exactly the path a highlights.scm capture takes (name_to_style_with_theme): a builtin category (keyword, text.title.1, comment) wins first, and any other name resolves against the theme registry as a Style::Element — which is how a plugin's own registered element (org.todo.WAITING) reaches a picker row. An unresolvable name renders unstyled rather than failing the row.
Naming a style rather than carrying one is deliberate, and follows annotation-custom.slot: a Style is a closed Rust enum plus an interned element id, and neither crosses an ABI meaningfully. A name does, and it means a guest's picker row is coloured by the SAME vocabulary — and the same active colourscheme — as the buffer it came from.
record raw-candidate
record raw-candidate {
text: string,
insert-text: option<string>,
display: string,
source: option<string>,
kind: candidate-kind,
data: candidate-data,
annotations: list<annotation>,
display-spans: list<display-span>,
}
Mirrors the crossable core of lattice_completion::candidate::RawCandidate plus marginalia (PH7.4a). accept_action remains host-only (#[serde(skip)], reconstructed host-side, §4.4); annotations crosses so plugin sources contribute themed columns. source is the optional source id.
PS.1: display-spans now crosses too. It was host-only on the reasoning that render-time fields are "re-derived host-side when needed" — which holds for a grep hit (the host has the path and the line, and re-derives through the preview highlighter) and does not hold for a row that is not a line of a file. An org-roam node title is a headline's text with no stars and no file line to parse, so there was nothing to re-derive from and every plugin picker row rendered plain, with no way for the plugin that owns the domain to say otherwise.
Fields
text:stringinsert-text:option<string>— OR.7: what to insert on accept when that differs from the text the query matched.none⇒ inserttext. A completion source that offers a human-readable label but inserts machine syntax (org-roam offering a node title and inserting an[[id:…][…]]link) needs both, and matching against the machine syntax instead would score every candidate on its id.display:stringsource:option<string>kind:candidate-kinddata:candidate-dataannotations:list<annotation>display-spans:list<display-span>— PS.1: styled runs overdisplay. Empty for every source that does not style itself, which is the pre-PS.1 behaviour exactly.
record jump-target
record jump-target {
buffer-id: u32,
line: u32,
col: u32,
}
record location
record location {
path: string,
line: u32,
col: u32,
}
record command-ref
record command-ref {
id: string,
args: args,
}
Fields
id:stringargs:args
record lsp-code-action-ref
record lsp-code-action-ref {
handle: u64,
index: u32,
}
variant picker-accept-outcome
variant picker-accept-outcome {
open-file(string),
switch-buffer(u32),
jump-in-buffer(jump-target),
jump-to-mark(char),
jump-to-location(location),
invoke-command(command-ref),
paste-register(char),
expand-snippet(string),
open-lsp-log(string),
open-lsp-trace-log(string),
apply-lsp-code-action(lsp-code-action-ref),
apply-lsp-completion(u32),
apply-colorscheme(string),
no-op,
}
Mirrors lattice_picker::outcome::PickerAcceptOutcome. All flat pure data; paths cross as strings; invoke-command reuses args.
Cases
open-file:stringswitch-buffer:u32jump-in-buffer:jump-targetjump-to-mark:charjump-to-location:locationinvoke-command:command-refpaste-register:charexpand-snippet:stringopen-lsp-log:stringopen-lsp-trace-log:stringapply-lsp-code-action:lsp-code-action-refapply-lsp-completion:u32apply-colorscheme:stringno-op
record position
record position {
line: u32,
byte: u32,
}
Mirrors lattice_protocol::position::Position.
record range
record range {
start: position,
end: position,
}
Mirrors lattice_protocol::position::Range.
Fields
variant edit-kind
variant edit-kind {
replace(string),
}
Mirrors lattice_protocol::edit::EditKind.
record edit
record edit {
range: range,
kind: edit-kind,
}
Mirrors lattice_protocol::edit::Edit.
Fields
record edit-delta
record edit-delta {
start-byte: u32,
old-end-byte: u32,
new-end-byte: u32,
start-position: position,
old-end-position: position,
new-end-position: position,
}
Mirrors lattice_protocol::edit::EditDelta.
Fields
start-byte:u32old-end-byte:u32new-end-byte:u32start-position:positionold-end-position:positionnew-end-position:position
record applied-edit
record applied-edit {
original-range: range,
inserted-range: range,
replaced-text: string,
inserted-text: string,
delta: edit-delta,
}
Mirrors lattice_core::buffer::AppliedEdit.
Fields
original-range:rangeinserted-range:rangereplaced-text:stringinserted-text:stringdelta:edit-delta
variant visual-mode
variant visual-mode {
charwise,
linewise,
blockwise,
}
Mirrors lattice_protocol::selection::VisualMode.
record selection
record selection {
anchor: position,
head: position,
visual: option<visual-mode>,
}
Mirrors lattice_protocol::selection::Selection.
Fields
record selection-set
record selection-set {
selections: list<selection>,
primary: u32,
}
Mirrors lattice_protocol::selection::SelectionSet (reconstructed via SelectionSet::from_parts). Always non-empty; primary indexes selections.
variant visual-kind
variant visual-kind {
charwise,
linewise,
blockwise,
}
Mirrors lattice_grammar::modal::VisualKind.
variant search-direction
variant search-direction {
forward,
backward,
}
Mirrors lattice_grammar::modal::SearchDirection.
variant modal-state
variant modal-state {
normal,
insert,
visual(visual-kind),
select(visual-kind),
operator-pending,
command,
search(search-direction),
replace,
prompt,
}
Mirrors lattice_grammar::modal::ModalState.
Cases
normalinsertvisual:visual-kindselect:visual-kindoperator-pendingcommandsearch:search-directionreplaceprompt
variant register
variant register {
unnamed,
named(char),
system,
black-hole,
expression,
read-only(char),
numbered(u8),
}
Mirrors lattice_grammar::register::Register.
variant yank-kind
variant yank-kind {
charwise,
linewise,
blockwise,
}
Mirrors lattice_grammar::effect::YankKind.
variant quit-scope
variant quit-scope {
pane,
all,
}
Mirrors lattice_grammar::effect::QuitScope.
variant echo-level
variant echo-level {
trace,
debug,
info,
warn,
error,
}
Mirrors lattice_grammar::effect::EchoLevel.
variant substitute-scope
variant substitute-scope {
current-line,
whole,
}
Mirrors lattice_grammar::effect::SubstituteScope.
record utf16-pos
record utf16-pos {
line: u32,
col: u32,
}
Mirrors lattice_grammar::effect::Utf16Pos.
variant lsp-request
variant lsp-request {
hover,
definition,
declaration,
type-definition,
implementation,
references,
follow-link,
}
Mirrors lattice_grammar::effect::LspRequest.
enum popup-placement
enum popup-placement {
centered,
cursor-anchored,
minibuffer-band,
}
Mirrors lattice_core::ui::popup::PopupPlacement.
WK.5 added minibuffer-band (full pane width, flush to its bottom edge — which-key's placement). Mirrored here rather than collapsed to centered at the boundary because this enum's contract is that it IS the mirror: a placement reachable natively but not from a plugin is an arbitrary gap in the canonical API (paramount #2).
enum popup-focus
enum popup-focus {
steal,
passive,
}
Mirrors lattice_core::ui::popup::PopupFocus.
record open-popup-payload
record open-popup-payload {
name: string,
mode-id: string,
placement: popup-placement,
focus: popup-focus,
}
Payload for the open-popup effect (popup-api.md §4.3). Name-based: the host ensures a popup buffer named name under major mode mode-id.
Fields
name:stringmode-id:stringplacement:popup-placementfocus:popup-focus
record confirm-payload
record confirm-payload {
prompt: string,
yes-action: string,
args: args,
}
IX.3: the payload of effect.confirm.
yes-action names an action the guest (or host) registered; the host resolves it through the command registry when the user answers y. A name, not a command id — a guest cannot hold a host-internal id, and names are what a plugin registers under.
args is what the yes-action receives when it fires, so the confirmed target and the executed target are the same thing. Without it the yes-half must re-derive its target at answer time from context that may have changed while the dialog was open.
Fields
prompt:string— Shown as the dialog's title. Name the target in it — a question has to be answerable without dismissing it to go look.yes-action:string— Action name dispatched ony.n/q/ Esc dismiss and dispatch nothing.args:args— Arguments handed toyes-action, positional against its declaredargs-schema.
record open-transient-payload
record open-transient-payload {
source: string,
args: args,
}
IX.5: the payload of effect.open-prompt.
A one-line minibuffer prompt. On submit the host dispatches on-submit-action, handing it the typed text — the action reads it from its context's prompt-value, not from args, because the value is what the user typed rather than what the caller chose. TR.3a: what effect::open-transient carries.
Fields
source:string— The registered source name.args:args— Arguments for this open, handed to the builder astransient-context.args.args::nonefor a plain open.
record open-prompt-payload
record open-prompt-payload {
prompt: string,
initial: string,
on-submit-action: string,
buffer-name: option<string>,
}
Fields
prompt:string— Shown before the input area.initial:string— Pre-filled text; empty for a blank prompt.on-submit-action:string— Action dispatched on submit. Escape dismisses and dispatches nothing.buffer-name:option<string>— Optional synthetic name for the prompt buffer. Callers that need to smuggle state through a multi-step flow encode it here;nonegets the default name.
variant file-anchor
variant file-anchor {
end,
start,
line(u32),
}
XF.4: where in a target file a write-to-file lands.
A position, not a range, and the asymmetry with apply-edit is deliberate: for its own buffer a guest holds borrow<document> and can compute a range that means something; for a file it has never read, a range would be a guess. These are the three positions namable without reading. Insert-only also means a guest cannot silently destroy content in a file the user was not looking at.
Cases
end— After the last line. The common case — archive, refile and capture all append.start— Before the first line.line:u32— Before this 0-based line. Past the end clamps toendrather than failing: a guest computing a line from a file it has not read can legitimately be off, and refusing to file the text at all is worse than filing it at the end.
record write-to-file-payload
record write-to-file-payload {
path: string,
anchor: file-anchor,
text: string,
cut: option<range>,
create-parents: bool,
save: bool,
}
XF.4: move text into a file the editor may not have open.
The primitive org-archive-subtree, org-refile and org-capture were blocked on. apply-edit addresses a buffer-id, which a guest cannot learn for a file that has never been opened.
The write goes through the document pipeline, not to disk. The host resolves path to a buffer, reusing one already open — so the user's unsaved changes are what the write lands on, u covers it, and the LSP hears about it. The target is left MODIFIED, not saved: a plugin that silently writes files is a larger authority than one that edits buffers.
Fields
path:string— Absolute, or relative to the editor's working directory.Checked against this plugin's
fs:writegrant, host-side, at the boundary — a path outside it is refused and echoed, and the effect never reaches the editor. The check runs here rather than at the applier because the applier cannot tell a plugin's effect from a native mode's; only the boundary still knows whose this is.anchor:file-anchortext:string— Inserted verbatim. A trailing newline is the guest's business — except that the host supplies a line break when appending to a target whose last line has none, which the guest cannot know.cut:option<range>— When present, this range is removed from the buffer the action ran in — and ONLY after the insert has landed.One effect rather than two, because as two the failure modes are "the text exists twice" and "the text is gone". The second is data loss from a keystroke, and an effect cannot report failure, so two ordered effects could not be made to depend on each other.
create-parents:bool— OR.10: create missing parent directories rather than refusing.False is the rule, not 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. That stays true for every guest that does not ask.
A guest asks when the directory is part of the LAYOUT IT OWNS rather than something the user typed. org-roam's
daily/YYYY-MM-DD.orgis 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-todayon a fresh corpus fails — the one use where the feature has to work.Still bounded by
path'sfs:writecheck above, which runs BEFORE this is read. Asking widens what is created inside the grant, never what is reachable outside it.save:bool— OC.9: persist the target to disk once the write has landed, rather than leaving the buffer modified.False is the rule.
cross-file-writes.md§7 leaves a target open, listed and MODIFIED, and that is what emacs'sorg-refileandorg-archive-subtreedo: the user reviews the change and writes it themselves. Every guest that does not ask keeps that behaviour.A guest asks when its whole operation is "commit this somewhere", and org-capture is that case —
org-capture.el's finalize runs(unless (org-capture-get :no-save) (save-buffer)), so saving is emacs's DEFAULT there and:no-saveexists to opt out of it. The asymmetry with refile is not an inconsistency in either editor: a refile moves text you are looking at, a capture files text you are done with.It also decides whether anything that reads the FILE can see the write. The agenda scan reads from disk, so an unsaved capture is invisible to a refresh no matter how correct the buffer is.
Only after a landed insert, and after
cut— the orderingcutalready documents extends to this. A write that failed saves nothing, so the flag can never persist a half-applied effect. Bounded by the samefs:writegrant aspath: this reaches disk only where the guest could already have created the file.
record apply-edit-payload
record apply-edit-payload {
target: u32,
edit: edit,
cursor: option<position>,
}
Fields
target:u32— The targetBufferId(its inneru32).edit:editcursor:option<position>— Where to park the caret after the edit — a column-preciseposition(line + byte), so a plugin action can place it between an inserted pair, not only at a row start (AP.2).noneleaves the caret put.
record yank-payload
record yank-payload {
register: register,
content: string,
kind: yank-kind,
explicit-yank: bool,
}
Fields
register:registercontent:stringkind:yank-kindexplicit-yank:bool—truefor an explicit yank (y/yy/Visualy);falsefor the register writes delete/change/xalso perform. Drives the yank-only system-clipboard mirror (clipboard.md §5).
record quit-payload
record quit-payload {
force: bool,
scope: quit-scope,
}
Fields
force:boolscope:quit-scope
record open-buffer-payload
record open-buffer-payload {
path: option<string>,
force: bool,
}
record open-buffer-at-payload
record open-buffer-at-payload {
path: option<string>,
position: position,
force: bool,
content: option<string>,
activate-minor: option<string>,
}
Fields
path:option<string>position:positionforce:boolcontent:option<string>— CD.2: seed text, applied only when the file is NOT on disk — reopening an existing file never replaces what is in it.activate-minor:option<string>— CD.2: a minor to activate alongside the major the path resolves, before the buffer is shown. The file-backed peer ofopen-synthetic-buffer-payload's field, for OC.7a's reason.
record open-buffer-at-column-payload
record open-buffer-at-column-payload {
path: option<string>,
column: option<utf16-pos>,
force: bool,
}
record open-synthetic-buffer-payload
record open-synthetic-buffer-payload {
name: string,
mode-id: string,
content: option<string>,
cursor: option<position>,
activate-minor: option<string>,
}
OC.7a: content / cursor / activate-minor close a hole a guest could not work around.
A native mode fills its own synthetic buffer from on_activate. The modes seam is DECLARATION-ONLY — a guest exports register-modes and nothing else — so a plugin mode has no such hook, and a guest that emitted this effect got a buffer it could never put text in. The other route, effect.apply-edit, needs the target's buffer-id, which this effect does not hand back and which the guest cannot look up.
So the payload carries what the guest would otherwise have to write: the text, where to leave the caret in it, and a minor to ride the major. Every field is optional and omitting all three is the pre-OC.7a behaviour exactly.
Fields
name:stringmode-id:string— The buffer's MAJOR mode.content:option<string>— Seed text, applied before the buffer is shown so the first frame is the finished one — a buffer that appears empty and fills a tick later is the content-jump the UX contract vetoes.noneleaves the buffer empty (the mode fills it, or nothing does). Ignored when the buffer already existed: a re-open must not overwrite what the user has typed into it, which is the difference between reopening a capture and losing one.cursor:option<position>— Where to park the caret incontent— org capture's%?point. Out of range is clamped rather than refused; a template whose%?sits past its own text is a template bug that must not cost the user the capture.activate-minor:option<string>— A minor to activate on the buffer alongside its major. Mirrorsspawn-terminal-payload.activate-minor, and exists for the same reason: the interesting behaviour belongs to a minor that rides a general-purpose major. An org capture buffer IS an org buffer — it wants org's grammar, motions and folding — so its major isorg-modeand only theC-c C-c/C-c C-kfinalize/abort pair is capture-specific. Naming the minor here is what keeps those chords off every other org buffer.
record spawn-terminal-payload
record spawn-terminal-payload {
cwd: option<string>,
cmd-line: option<string>,
env: list<tuple<string, string>>,
activate-minor: option<string>,
}
Fields
cwd:option<string>— PC.2: working directory to spawn in, overriding the active buffer's project root for this spawn only.lattice_terminal::SpawnConfighas carried acwdsince 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.nonekeeps PR.3's behaviour exactly.cmd-line:option<string>env:list<tuple<string, string>>activate-minor:option<string>
record echo-payload
record echo-payload {
level: echo-level,
text: string,
}
Fields
level:echo-leveltext:string
record substitute-payload
record substitute-payload {
scope: substitute-scope,
pattern: string,
replacement: string,
global: bool,
}
Fields
scope:substitute-scopepattern:stringreplacement:stringglobal:bool
record describe-command-payload
record describe-command-payload {
name: string,
anchor: option<string>,
}
record open-picker-payload
record open-picker-payload {
source: string,
args: list<string>,
root: option<string>,
fill-action: option<string>,
query: option<string>,
}
Fields
source:stringargs:list<string>root:option<string>— PC.1: the root this picker resolves against, overriding the active buffer's project for this open only.Why the context and not an argument. A
livesource (grep) re-queries throughon-query-changed, which sees the query and the context and NOT the open's args — and a source is a shared generator with no per-open state. A root passed as an argument would apply to the first query and silently revert to the workspace root on the next keystroke, which is worse than not having it.noneresolves 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 ex-command as its first argument.picker-accept-outcome'sfill-calleralready means "hand this value to whoever opened me". What it lacked was a destination a GUEST can own: the host's fill targets are the document, the:line, a prompt, a transient argument and another picker's query, and a plugin owns none of them. It does own an ex-command.open-prompt-payload.on-submit-actionis this shape already, for this reason — the asymmetry between the two, where a guest could be handed a prompt's answer but not a picker's, is what this closes.Not an override of the source's own accept. A source decides what accepting one of its candidates means;
file-pickanddir-pickexist as separate sources precisely so "supply a value" is the source's decision rather than the caller's. This names where such a value lands.noneleaves the target as whatever surface was captured at open.query:option<string>— CD.6a: text the query starts with. For a static source it narrows the rows from the first frame (emacs'scompleting-readinitial input); org-roam's node insert seeds it from the active region. Appended last, so earlier fields keep their positions.
record set-lsp-log-level-payload
record set-lsp-log-level-payload {
server-id: option<string>,
level: string,
}
record diffsplit-payload
record diffsplit-payload {
path: string,
remote: option<string>,
}
record close-session-diffs-payload
record close-session-diffs-payload {
origin-session: u64,
tab-name: string,
}
variant viewport-pos
variant viewport-pos {
top,
middle,
bottom,
}
Mirrors lattice_grammar::app_effect::ViewportPos (H/M/L).
variant scroll-pos
variant scroll-pos {
top,
center,
bottom,
}
Mirrors lattice_grammar::app_effect::ScrollPos (zt/zz/zb).
variant pane-direction
variant pane-direction {
left,
down,
up,
right,
}
Mirrors lattice_grammar::app_effect::PaneDirection.
variant insert-line-edit
variant insert-line-edit {
cursor-line-start,
cursor-line-end,
cursor-char-left,
cursor-char-right,
delete-word-backward,
delete-to-line-start,
kill-to-line-end,
indent-line,
dedent-line,
}
Mirrors lattice_grammar::app_effect::InsertLineEdit — the <C-a>, <C-e>, <C-b>, <C-f>, <C-w>, <C-u>, <C-k>, <C-t>, <C-d> readline/vim line-editing family within Insert mode.
variant hscroll
variant hscroll {
columns(bool),
half-screen(bool),
cursor-to-edge(bool),
}
Mirrors lattice_grammar::app_effect::HScroll (vim z{l,h,L,H,s,e}). Each arm's bool is the native struct field: columns/half-screen carry right, cursor-to-edge carries end.
record narrow-lines-payload
record narrow-lines-payload {
start-line: u32,
end-line: u32,
}
Mirrors AppEffect::NarrowLines and AppEffect::CreateFold: a pre-resolved inclusive 0-based line span.
enum format-intent
enum format-intent {
indent,
reflow,
reformat,
}
RF.5b: which of the three formatting jobs a range wants done.
Mirrors lattice_core::FormatIntent. Separate values rather than one "format" because they are not substitutable: an indent that reflows is destructive, and a reformatter asked to reflow prose mostly does nothing. See docs/dev/architecture/text-reflow.md §2.
Cases
indent— Leading whitespace only (=).reflow— Line breaks within a paragraph (gq/gw).reformat— Anything the formatter likes (:format,g=).
record format-range-payload
record format-range-payload {
intent: format-intent,
start-line: u32,
end-line: u32,
}
Mirrors AppEffect::FormatRange — an operator has resolved its range and the buffer's chain says a non-native provider owns it.
Fields
intent:format-intentstart-line:u32end-line:u32
record open-provider-view-payload
record open-provider-view-payload {
provider: string,
argument: option<string>,
scan-args: list<string>,
}
AG.1: what app-effect::open-provider-view carries.
provider is the name a provider registered on the generic provider-view seam ("agenda", "search").
argument is the host-interpreted parameter: a root for the agenda, a query for search. One free-text string rather than the full recursive Args shape a command handler receives, because mirroring that enum here would cost a second args encoding on the boundary to express cases no provider has. A native caller passing anything richer is refused with a typed error rather than silently flattened, which is the NarrowTrigger precedent.
scan-args (OA.11a) is the guest-interpreted one, passed through to a scan source's begin verbatim and never read by the host.
Two slots because they have two owners, and conflating them breaks. The host must understand argument — it does the walk, and it replaces the source's roots with it. So a guest sending a command key down that slot would set the scan root to a path that does not exist and quietly cover nothing. scan-args is the channel for anything only the guest can read.
Empty scan-args reproduces the pre-OA.11a boundary exactly: the payload maps to Args::None / Args::String, so every existing trigger is unchanged.
variant app-effect
variant app-effect {
quit,
match-bracket,
toggle-case-at-cursor,
open-line-below,
open-line-above,
search-next,
search-previous,
jump-history-back,
jump-history-forward,
pane-history-back,
pane-history-forward,
walk-mark-history-back,
walk-mark-history-forward,
tag-stack-pop,
open-fold-at-cursor,
close-fold-at-cursor,
toggle-fold-at-cursor,
open-all-folds,
close-all-folds,
cycle-fold-at-cursor,
cycle-folds-global,
goto-parent-fold,
delete-fold-at-cursor,
goto-next-fold,
goto-prev-fold,
toggle-fold-enable,
open-folds-recursively,
close-folds-recursively,
delete-folds-recursively,
undo,
redo,
repeat-last-change,
page-down,
page-up,
half-page-down,
half-page-up,
scroll-line-up,
scroll-line-down,
redraw-screen,
open-command-picker,
enter-command-line,
oil-navigate-up,
reselect-last-visual,
swap-visual-ends,
paste-after,
paste-before,
enter-append,
enter-insert-first-non-blank,
enter-append-end-of-line,
display-line-down,
display-line-up,
display-line-start,
display-line-end,
create-fold-from-visual,
delete-char-backward,
completion-trigger,
exit-visual,
replace-undo-last,
enter-mode(modal-state),
enter-visual(visual-kind),
enter-select(visual-kind),
enter-search(search-direction),
search-word-under-cursor(search-direction),
jump-viewport(viewport-pos),
scroll-cursor-to(scroll-pos),
horizontal-scroll(hscroll),
insert-line-edit(insert-line-edit),
join-lines(bool),
find-repeat(bool),
insert-newline,
insert-tab,
overwrite-char(char),
set-mark(char),
jump-to-mark-line(char),
jump-to-mark-exact(char),
select-register(register),
start-macro-record(char),
play-macro(char),
play-last-macro,
absorb-operator-prefix(u64),
split-pane-horizontal,
split-pane-vertical,
close-pane,
only-pane,
toggle-zoom-pane,
navigate-pane(pane-direction),
next-pane,
prev-pane,
next-tab,
prev-tab,
go-to-tab(u32),
new-tab,
new-tab-at(string),
terminal-spawn(option<string>),
terminal-spawn-in-new-tab(option<string>),
move-pane-to-new-tab,
close-tab,
only-tab,
move-tab(u32),
picker-accept-in-split,
picker-accept-in-vsplit,
picker-accept-in-tab,
equalize-panes,
grow-pane-height,
shrink-pane-height,
grow-pane-width,
shrink-pane-width,
completion-next,
completion-prev,
completion-accept,
completion-cancel,
completion-cancel-and-exit-insert,
completion-toggle-docs,
completion-docs-scroll-down,
completion-docs-scroll-up,
completion-accept-then-insert(char),
snippet-next-placeholder,
snippet-prev-placeholder,
completion-filter-to-source(string),
completion-filter-clear,
diff-get,
diff-put,
tutor-advance,
tutor-retreat,
multibuffer-expand(s32),
narrow-widen,
narrow-lines(narrow-lines-payload),
create-fold(narrow-lines-payload),
format-range(format-range-payload),
search-trigger(string),
search-refresh,
open-provider-view(open-provider-view-payload),
}
Mirrors lattice_grammar::app_effect::AppEffect (PH7.3b2). NarrowTrigger is absent by design (recursive Range; see the note above).
Cases
quitmatch-brackettoggle-case-at-cursoropen-line-belowopen-line-abovesearch-nextsearch-previousjump-history-backjump-history-forwardpane-history-backpane-history-forwardwalk-mark-history-backwalk-mark-history-forwardtag-stack-popopen-fold-at-cursorclose-fold-at-cursortoggle-fold-at-cursoropen-all-foldsclose-all-foldscycle-fold-at-cursorcycle-folds-globalgoto-parent-folddelete-fold-at-cursorgoto-next-foldgoto-prev-foldtoggle-fold-enableopen-folds-recursivelyclose-folds-recursivelydelete-folds-recursivelyundoredorepeat-last-changepage-downpage-uphalf-page-downhalf-page-upscroll-line-upscroll-line-downredraw-screenopen-command-pickerenter-command-lineoil-navigate-upreselect-last-visualswap-visual-endspaste-afterpaste-beforeenter-appendenter-insert-first-non-blankenter-append-end-of-linedisplay-line-downdisplay-line-updisplay-line-startdisplay-line-endcreate-fold-from-visualdelete-char-backwardcompletion-triggerexit-visualreplace-undo-lastenter-mode:modal-stateenter-visual:visual-kindenter-select:visual-kindenter-search:search-directionsearch-word-under-cursor:search-directionjump-viewport:viewport-posscroll-cursor-to:scroll-poshorizontal-scroll:hscrollinsert-line-edit:insert-line-editjoin-lines:boolfind-repeat:boolinsert-newlineinsert-taboverwrite-char:charset-mark:charjump-to-mark-line:charjump-to-mark-exact:charselect-register:registerstart-macro-record:charplay-macro:charplay-last-macroabsorb-operator-prefix:u64split-pane-horizontalsplit-pane-verticalclose-paneonly-panetoggle-zoom-panenavigate-pane:pane-directionnext-paneprev-panenext-tabprev-tabgo-to-tab:u32new-tabnew-tab-at:stringterminal-spawn:option<string>terminal-spawn-in-new-tab:option<string>move-pane-to-new-tabclose-tabonly-tabmove-tab:u32picker-accept-in-splitpicker-accept-in-vsplitpicker-accept-in-tabequalize-panesgrow-pane-heightshrink-pane-heightgrow-pane-widthshrink-pane-widthcompletion-nextcompletion-prevcompletion-acceptcompletion-cancelcompletion-cancel-and-exit-insertcompletion-toggle-docscompletion-docs-scroll-downcompletion-docs-scroll-upcompletion-accept-then-insert:charsnippet-next-placeholdersnippet-prev-placeholdercompletion-filter-to-source:stringcompletion-filter-cleardiff-getdiff-puttutor-advancetutor-retreatmultibuffer-expand:s32narrow-widennarrow-lines:narrow-lines-payloadcreate-fold:narrow-lines-payload— VM.3h: vim'szfoperator. A closed fold over the span.format-range:format-range-payload— RF.5b: hand a resolved line range to the buffer'sformat.{indent,reflow,reformat}chain. Emitted by=,gqandg=when the winning rung is notnative; the host runs the provider asynchronously and applies a minimal edit set.search-trigger:stringsearch-refreshopen-provider-view:open-provider-view-payload— AG.1: open a registered provider's multibuffer view by name.Withheld from this mirror until now, on the reasoning that letting a plugin open any registered provider by name is a capability question belonging with the host's capability model. The precedent had already answered it:
effect::open-pickerandeffect::open-transientboth let a guest open any registered source by name, ungated, and this is the same authority in the same shape. Withholding it did not withhold the capability — it only made the one seam that needed it borrow a host ex-command instead.What that borrowing cost is the reason this landed: a plugin whose trigger is a host command cannot name it. The agenda's
:agendatherefore had a generic name for a feature every user callsorg-agenda, and the plugin could not fix that from its own side.
variant effect
variant effect {
none,
declined,
edits(list<applied-edit>),
apply-edit(apply-edit-payload),
write-to-file(write-to-file-payload),
selection-change(selection-set),
cursor-move(position),
confirm(confirm-payload),
open-prompt(open-prompt-payload),
open-transient(open-transient-payload),
yank(yank-payload),
enter-mode(modal-state),
save-buffer(option<string>),
quit-editor(quit-payload),
open-buffer(open-buffer-payload),
open-buffer-at(open-buffer-at-payload),
open-external-uri(string),
open-buffer-at-column(open-buffer-at-column-payload),
spawn-terminal(spawn-terminal-payload),
terminal-input(list<u8>),
set-option(string),
set-local-option(string),
set-global-option(string),
clear-search-highlight,
set-colorscheme(string),
echo(echo-payload),
show-diagnostics-popup(list<tuple<string, u8>>),
lsp(lsp-request),
echo-registers,
echo-marks,
substitute(substitute-payload),
delete-current-line,
describe-command(describe-command-payload),
describe-buffer,
apropos(string),
describe-key(string),
list-keymap,
buffer-next,
buffer-prev,
list-buffers,
open-buffer-picker,
open-picker(open-picker-payload),
buffer-delete(bool),
open-file-tree(option<string>),
close-file-tree,
open-oil(option<string>),
describe-option(string),
describe-element(string),
list-options,
describe-plugin-api(option<string>),
list-plugin-apis,
export-plugin-api(option<string>),
list-commands,
describe-plugin(string),
list-plugins,
open-hover(string),
dismiss-popup,
dismiss-popup-named(string),
open-popup(open-popup-payload),
open-help-topic(option<string>),
list-diagnostics,
next-diagnostic,
prev-diagnostic,
open-lsp-log(option<string>),
open-messages,
open-dashboard,
toggle-lsp-trace(string),
open-lsp-trace-log(option<string>),
lsp-status,
lsp-server-log-listing,
lsp-restart(string),
lsp-progress-cancel(option<string>),
lsp-expand-region,
lsp-shrink-region,
set-lsp-log-level(set-lsp-log-level-payload),
lsp-log-clear(option<string>),
lsp-document-symbol,
lsp-workspace-symbol(string),
lsp-incoming-calls,
lsp-outgoing-calls,
lsp-supertypes,
lsp-subtypes,
lsp-moniker,
lsp-code-lens,
lsp-color-presentation,
lsp-format,
lsp-format-range,
lsp-signature-help,
lsp-complete,
lsp-rename(string),
lsp-code-action,
expand-snippet(range),
reload-snippets,
describe-events,
describe-diff,
diff-open,
diff-off(bool),
diffthis,
diffsplit(diffsplit-payload),
diff-get-cmd(option<u32>),
diff-put-cmd(option<u32>),
diff-accept,
diff-reject,
diff-accept-all,
diff-reject-all,
close-session-diffs(close-session-diffs-payload),
close-all-session-diffs(u64),
next-hunk,
prev-hunk,
describe-event(string),
list-modes,
describe-mode(string),
describe-active-modes,
describe-active-bindings,
describe-option-resolution(string),
customize(option<string>),
tutor(option<u32>),
toggle-mode(string),
app-action(app-effect),
record-jump,
open-ai-log(option<string>),
open-synthetic-buffer(open-synthetic-buffer-payload),
focus-buffer(u32),
invoke-command(command-ref),
}
Mirrors lattice_grammar::effect::Effect (§4.4). Every arm is pure data; Many/Global/AppAction are absent by design (see the note above).
Cases
nonedeclined— AP.0.2: the action DECLINES the chord (it did nothing) — the dispatcher re-resolves as if this action's keymap layer weren't there, falling through to the next binding. Distinct fromnone(a no-op that consumes the chord). A guest returns[declined]to fall through.edits:list<applied-edit>apply-edit:apply-edit-payloadwrite-to-file:write-to-file-payload— XF.4: move text into another file. Seewrite-to-file-payload.selection-change:selection-setcursor-move:positionconfirm:confirm-payload— IX.3: ask the user a yes/no question, then dispatch an action. Available to plugins because asking the user something is table stakes, not an advanced capability.open-prompt:open-prompt-payload— IX.5: ask the user for a line of text, then dispatch an action with it. The other half of "a plugin can collect input" — without it a guest can only ask yes/no questions.open-transient:open-transient-payload— IX.6: open a named transient menu. The payload names the source the owning crate registered with theTransientSourceRegistry, not a menu structure — the menu is built host-side from that registration, so a guest opens its own menu by naming it rather than by shipping a spec across on every press.TR.3a: it also carries the ARGUMENTS the open was requested with, which reach the builder as
transient-context.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, and the builder would have to keep the answer in guest memory where nothing clears it.yank:yank-payloadenter-mode:modal-statesave-buffer:option<string>quit-editor:quit-payloadopen-buffer:open-buffer-payloadopen-buffer-at:open-buffer-at-payloadopen-external-uri:stringopen-buffer-at-column:open-buffer-at-column-payloadspawn-terminal:spawn-terminal-payloadterminal-input:list<u8>set-option:stringset-local-option:stringset-global-option:stringclear-search-highlightset-colorscheme:stringecho:echo-payloadshow-diagnostics-popup:list<tuple<string, u8>>lsp:lsp-requestecho-registersecho-markssubstitute:substitute-payloaddelete-current-linedescribe-command:describe-command-payloaddescribe-bufferapropos:stringdescribe-key:stringlist-keymapbuffer-nextbuffer-prevlist-buffersopen-buffer-pickeropen-picker:open-picker-payloadbuffer-delete:boolopen-file-tree:option<string>close-file-treeopen-oil:option<string>describe-option:stringdescribe-element:stringlist-optionsdescribe-plugin-api:option<string>— PI.2: plugin-API introspection help effects.list-plugin-apisexport-plugin-api:option<string>list-commandsdescribe-plugin:stringlist-pluginsopen-hover:stringdismiss-popupdismiss-popup-named:string— Dismiss the popup only if it is the named one; a no-op otherwise.dismiss-popupis the user's verb ("close what I am looking at"); this is the one a mode uses when it dismisses on its own schedule, where the slot may hold someone else's popup by the time the effect lands. SeeEffect::DismissPopupNamedfor the bug that motivated it.open-popup:open-popup-payloadopen-help-topic:option<string>list-diagnosticsnext-diagnosticprev-diagnosticopen-lsp-log:option<string>open-messagesopen-dashboardtoggle-lsp-trace:stringopen-lsp-trace-log:option<string>lsp-statuslsp-server-log-listinglsp-restart:stringlsp-progress-cancel:option<string>lsp-expand-regionlsp-shrink-regionset-lsp-log-level:set-lsp-log-level-payloadlsp-log-clear:option<string>lsp-document-symbollsp-workspace-symbol:stringlsp-incoming-callslsp-outgoing-callslsp-supertypeslsp-subtypeslsp-monikerlsp-code-lenslsp-color-presentationlsp-formatlsp-format-rangelsp-signature-helplsp-completelsp-rename:stringlsp-code-actionexpand-snippet:rangereload-snippetsdescribe-eventsdescribe-diffdiff-opendiff-off:booldiffthisdiffsplit:diffsplit-payloaddiff-get-cmd:option<u32>diff-put-cmd:option<u32>diff-acceptdiff-rejectdiff-accept-alldiff-reject-allclose-session-diffs:close-session-diffs-payloadclose-all-session-diffs:u64next-hunkprev-hunkdescribe-event:stringlist-modesdescribe-mode:stringdescribe-active-modesdescribe-active-bindingsdescribe-option-resolution:stringcustomize:option<string>tutor:option<u32>toggle-mode:stringapp-action:app-effectrecord-jumpopen-ai-log:option<string>open-synthetic-buffer:open-synthetic-buffer-payloadfocus-buffer:u32— CD.1: show the buffer with this id in the active pane. The peer ofapply-edit'starget: a guest can edit a buffer it knows only by id, and this shows one. An id that no longer names a buffer is a no-op. Appended last so existing variant indices do not move.invoke-command:command-ref— CD.3d: run a registered command — an action with typed args, else an ex line — AFTER the effects before it in the same batch. A write-to-file that does not land stops the batch, so work that must follow a successful write (deleting what was filed) goes here rather than in a host call, which runs before any effect is applied.
variant arg-kind
variant arg-kind {
string,
char,
bool,
int,
pattern,
chord,
body,
raw,
}
Mirrors lattice_grammar::args::ArgKind.
variant arg-default
variant arg-default {
required,
none,
literal(arg-value),
use-selection,
use-cursor-word,
use-last-response,
}
Mirrors lattice_grammar::args::ArgDefault. literal reuses arg-value.
Cases
requirednoneliteral:arg-valueuse-selectionuse-cursor-worduse-last-response
record arg-spec
record arg-spec {
name: string,
kind: arg-kind,
doc: string,
prompt: string,
default: arg-default,
completion: option<string>,
picker: option<string>,
}
Mirrors lattice_grammar::args::ArgSpec. Native name/doc/prompt/ completion are &'static str; the host adapter interns the plugin's owned strings at registration (Box::leak, bounded by loaded-source count — see the slice plan note on hot-reload).
Fields
name:stringkind:arg-kinddoc:stringprompt:stringdefault:arg-defaultcompletion:option<string>— A registered COMPLETION source (gen:files, ...) — inline candidates as the user types this argument.picker:option<string>— YR.6: a registered PICKER source offered for this argument.Two fields because they name two registries, which is a decision rather than an oversight: a completion source is engine-shaped, a picker source is surface-shaped and needs
PickerContext. An argument may set both —<Tab>completes inline,<C-x><C-o>opens the picker on the same question.
record multibuffer-view-excerpt
record multibuffer-view-excerpt {
path: string,
start-line: u32,
end-line: u32,
header: string,
match-count: option<u32>,
}
Mirrors lattice_picker::source::PickerSourceSpec. live opts the source out of the picker's fuzzy refilter (the source owns filtering). One row of a plugin-owned multibuffer view.
path names a FILE, not a buffer id. Excerpt carries a BufferId and only the host can mint one — so the host opens (or reuses) the document and adds it as this excerpt's source. A path is the stable name both sides already share; effect::write-to-file resolves paths to buffers for the same reason.
Fields
path:stringstart-line:u32— 0-based, inclusive ofend-line, matchingscanned-excerpt.end-line:u32header:string— Rendered above this excerpt. Empty renders no header row, which is the entire grouping mechanism: a group is "title on the first excerpt of a run, empty on the rest". A pull guest computes the runs itself, because unlike a scan guest it can see the whole ordered set.match-count:option<u32>— The· N matchesbadge beside the header.none⇒ no badge.
variant multibuffer-view-input
variant multibuffer-view-input {
pull,
scan,
}
Where a view's rows come from. Declared on the spec rather than per build call: the host must know BEFORE it calls anything, because a scan view needs the walk driven and a pull view needs build invoked. Per-call, the host would have to call build to learn it should not have. It also matches the seam next door, where extensions and view-mode are load-time facts and only roots is per-scan.
Cases
pull— The guest already knows the answer — an index lookup, a computed set. The host callsbuildand renders what comes back.scan— The host walks, reads and parses; the guest classifies each file throughscanned-excerpt-source.No payload, deliberately. An earlier draft carried the file extensions here and that was a second source of truth: a scan source already declares its own
extensions(), and the walk reads them from the live sources. Two places to say it is one place to get it wrong.Kept as a separate input rather than folded into
pullbecause the two are different COST MODELS, not two spellings of one. The host must read each file anyway to build the source document, so it reads once, parses once, and hands over text and tree: a 1-2 ms parse for a 217 ns copy (benches/agenda_scan_input.rs), and the guest needs no filesystem capability at all. A pull-only world makes the guest discover and read files itself, and reading node text back through the tree seam costs ~50 us per file — 200x the copy it avoided.
record multibuffer-view-spec
record multibuffer-view-spec {
id: string,
doc: string,
buffer-name: string,
view-mode: option<string>,
reuse: bool,
input: multibuffer-view-input,
}
A view a plugin owns.
Fields
id:string— The provider name.app-effect::open-provider-viewand the view's owngrboth reach it by this.doc:string— Shown in:describe-commandand the provider listing.buffer-name:string— The view's buffer name — the GUEST's to choose (*agenda*,*org-roam-backlinks*). Host constants are what made the agenda's identity un-ownable.view-mode:option<string>— A minor mode to activate on the view, by name. This is where the view's chords and their handler bodies live, and it is a property of the VIEW rather than of how its rows were found. A name that is not registered warns through the ordinary activation path rather than failing the open — the rows are still worth showing.reuse:bool— Reuse one buffer across triggers (a second:agendare-scans into the same buffer) or open a fresh view each time.input:multibuffer-view-input
record multibuffer-view-result
record multibuffer-view-result {
excerpts: list<multibuffer-view-excerpt>,
summary: string,
}
What build returns.
Fields
excerpts:list<multibuffer-view-excerpt>— In FINAL order — seemultibuffer-view-source.build.summary:string— The headerline's terminal summary ("42 backlinks"). Returned with the rows rather than fetched by a second call: one crossing, and the count is a fact the guest already has.
record picker-source-spec
record picker-source-spec {
id: string,
doc: string,
args-schema: list<arg-spec>,
args-hint: string,
live: bool,
create-label: option<string>,
rooted: bool,
delete-command: option<string>,
}
Fields
id:stringdoc:stringargs-schema:list<arg-spec>args-hint:stringlive:boolcreate-label:option<string>— OR.5: when set, the picker offers one synthetic create row whenever the query is non-empty — the offer to make the thing the user was looking for and did not find.%sin the label is replaced by the query.Two things about the row are load-bearing and neither is obvious. It appears whenever the query is non-empty, NOT only when nothing matches: offering it only on zero matches makes it impossible to create Rust while Rust Async exists, which is precisely when you most want to. And it is pinned last and never ranked, because a create row that could sort above a real match would let
<CR>produce a duplicate through ranking noise — destructive rather than merely wrong.Accepting it hands
routing-payload::create(query)to this source'saccept, which decides what creation means.none— every source but roam's — behaves exactly as before.rooted:bool— PP.2: these results are scoped to a project / workspace root, so the picker prompt names the root it is operating on (files ~/src/lattice>).A DECLARATION, not an inference. The host resolves a root for every picker open —
picker-context.workspace-rootis always filled — so it could show one everywhere, and a path on a list that spans every open project is noise on the one line the user reads to know what they are looking at. Only the source knows whether the root is part of what its list MEANS.Set it when the answer to "would these results be different in another project?" is yes.
falseis the answer for a list that is global, buffer-local, or registry-wide — and for a source whose QUERY already names a path, where a root beside it would be a second and staler answer to the same question.delete-command:option<string>— PD.1:<C-d>— the ex-command that REMOVES the selected row from whatever backs this list, invoked with that row's routing argument.noneleaves<C-d>doing nothing.The SOURCE owns the verb and the host owns only the key.
<C-s>/<C-v>/<C-t>are host concerns — it knows how to open a thing in a split without asking. Deletion is not: only the source knows that removing a row from a project list means forgetting a root. Naming a command is how a source says so, and it is the same routing its rows already take, so declaring this needs no new seam and no new capability.Not destructive to the filesystem, and must not be. A project list forgets a path; deleting a directory is oil's job and the file tree's. A source whose delete verb touched disk would make
<C-d>mean two very different things depending on which picker had focus.
record generate-context
record generate-context {
prefix: string,
case-sensitive: bool,
line-before-cursor: string,
language: string,
}
Mirrors the crossable core of lattice_completion::traits::GenerateContext (PH7.6). buffer (&Buffer) + registry (&CommandRegistry) do NOT cross — a plugin generator that needs buffer text waits for the document handle (the picker-source precedent); v1 carries the query prefix + the case-sensitivity flag, which is all a produce-then-match_and_rank generator needs (matching stays native — the sync-pipeline + paramount-#1 reason a plugin completion source is a GENERATOR, not four trait objects). OR.7 widened this. prefix alone cannot answer whether a source applies — org-roam's node source offers link targets only inside [[…]], and the anchor scan stops at [, so the prefix it receives is indistinguishable from an ordinary word. The alternative was a host-side link-context flag beside path-context, i.e. teaching the host one plugin's syntax; line-before-cursor lets every source answer for itself and the host stay ignorant.
Fields
prefix:stringcase-sensitive:boolline-before-cursor:string— The cursor's line from its start up to the cursor, verbatim. The replacement region is still[anchor, cursor]— a source whose insert would cover more than the prefix must check that this string ends with its own opener plusprefix, and decline otherwise, or it will splice over the wrong span.language:string— The buffer's language id ("org","rust", …); empty when no language is detected. A source registered by a plugin is offered to every buffer, so this is how it scopes itself to its own.
record completion-source-spec
record completion-source-spec {
id: string,
doc: string,
accepts-non-word-query: bool,
}
A completion source's identity — the (name, doc) pair insert_generator stamps (registry.rs). Mirrors nothing structural; the host interns the owned strings at registration like picker-source-spec.
Fields
id:stringdoc:stringaccepts-non-word-query:bool— OR.7: keep the popup open when the query picks up a non-word character. Default behaviour dismisses there — right for identifier completion, wrong for a source completing phrases (org-roam node titles contain spaces, and the popup used to close at the first one).
variant open-target
variant open-target {
default,
split,
vsplit,
tab,
}
Mirrors lattice_picker::outcome::OpenTarget (<CR>/<C-s>/<C-v>/<C-t>).
record resolve-diff-payload
record resolve-diff-payload {
primary: u32,
accept: bool,
}
record lsp-instance-payload
record lsp-instance-payload {
server-id: string,
workspace: string,
}
record show-message-action-payload
record show-message-action-payload {
request-id: u32,
action-index: u32,
}
record ai-session-payload
record ai-session-payload {
provider: string,
index: u32,
}
Mirrors lattice_picker::RoutingPayload::AiSession — the (provider, index) key for opening the per-session AI log buffer.
variant routing-payload
variant routing-payload {
buffer(u32),
resolve-diff(resolve-diff-payload),
lsp-instance(lsp-instance-payload),
lsp-location(location),
lsp-completion(u32),
lsp-code-action(u32),
open-file(string),
jump-in-buffer(jump-target),
invoke-command(command-ref),
paste-register(char),
jump-to-mark(char),
expand-snippet(string),
accept-show-message-action(show-message-action-payload),
lsp-code-lens(u32),
color-presentation(u32),
colorscheme(string),
ai-session(ai-session-payload),
pane-history-entry(u32),
file-location(location),
create(string),
}
Mirrors lattice_picker::RoutingPayload — the opaque token a source emits per candidate and consumes in accept. All flat pure data; paths cross as strings (a non-UTF-8 path is a typed error, §4.4).
Cases
buffer:u32resolve-diff:resolve-diff-payloadlsp-instance:lsp-instance-payloadlsp-location:locationlsp-completion:u32lsp-code-action:u32open-file:stringjump-in-buffer:jump-targetinvoke-command:command-refpaste-register:charjump-to-mark:charexpand-snippet:stringaccept-show-message-action:show-message-action-payloadlsp-code-lens:u32color-presentation:u32colorscheme:stringai-session:ai-session-payloadpane-history-entry:u32file-location:location— OR.6: a place in a file on disk. The peerpicker-accept-outcome'sjump-to-locationalready had and this side lacked — without it a row standing for a position in a file can only carryopen-file, which drops the line and lands at the top. Distinct fromlsp-location, which is the same shape under a name that says where it came from.create:string— OR.5: the query the user typed, carried by the picker's synthetic create row. Verbatim — spaces and non-ASCII included — because the source is creating something the USER named, and trimming here would be the picker having an opinion about a namespace it does not own.
record buffer-entry
record buffer-entry {
id: u32,
kind-label: string,
path: option<string>,
title: string,
dirty: bool,
}
Mirrors lattice_picker::context::BufferEntry. kind-label is a display string — the picker seam stays oblivious to BufferKind (CLAUDE.md rule).
variant position-source
variant position-source {
auto-jump,
explicit-mark,
plugin-push,
named-mark(char),
}
Mirrors lattice_picker::context::PositionSource.
record position-entry
record position-entry {
buffer-id: u32,
line: u32,
col: u32,
source: position-source,
}
Mirrors lattice_picker::context::PositionEntry.
Fields
buffer-id:u32line:u32col:u32source:position-source
record symbol-location
record symbol-location {
name: string,
line: u32,
col: u32,
}
One tree-sitter symbol location (name, line, byte-col) — the owned form of ActiveBufferSnapshot::syntax_symbols.
record active-buffer-snapshot
record active-buffer-snapshot {
buffer-id: u32,
path: option<string>,
language: option<string>,
cursor: position,
selection: option<tuple<position, position>>,
syntax-symbols: list<symbol-location>,
}
Mirrors lattice_picker::context::ActiveBufferSnapshot (metadata only). buffer (the rope) + syntax_highlights are deferred to the document resource wiring (PH7.4c); a fuzzy-finder needs neither.
Fields
buffer-id:u32path:option<string>language:option<string>cursor:positionselection:option<tuple<position, position>>syntax-symbols:list<symbol-location>
record picker-context
record picker-context {
active-buffer: active-buffer-snapshot,
workspace-root: string,
recent-files: list<string>,
position-history: list<position-entry>,
buffers: list<buffer-entry>,
marks: list<tuple<char, position>>,
registers: list<tuple<string, string>>,
}
Mirrors lattice_picker::context::PickerContext (the owned projection).
Fields
active-buffer:active-buffer-snapshotworkspace-root:stringrecent-files:list<string>position-history:list<position-entry>buffers:list<buffer-entry>marks:list<tuple<char, position>>registers:list<tuple<string, string>>
record transient-context
record transient-context {
major-mode: option<string>,
minor-modes: list<string>,
buffer: option<u32>,
args: args,
}
Mirrors lattice_picker::TransientContext — where the menu was opened from, so a builder can vary its rows. Host→guest only (a project_* fn, the picker-context precedent); the guest never sends one back.
Deliberately NOT the cursor or the selection: a builder produces rows, it does not act. Each row's action receives its own ActionContext at FIRE time, when those are current.
Fields
major-mode:option<string>— The active major's id, if the buffer has one. Emacs magit's:if-modequestion.minor-modes:list<string>— The active minor ids — the looser:if-derivedfamily test. A separate field frommajor-modeon purpose: a flat list of active mode ids can only answer one of the two questions.buffer:option<u32>— The buffer the menu was opened over.nonemid-boot, where a builder degrades exactly as it does for the mode fields.args:args— TR.3a: the arguments the open carried (effect::open-transient's payload).This is what lets a menu DRILL DOWN: org's capture menu has a row per template, and the fields menu that row opens needs to know which template it is collecting for. The alternative is guest memory, which
<Esc>never clears — the next open would inherit the last one's subject.
record transient-action
record transient-action {
command: string,
args: args,
}
An action row's target: a command name plus the arguments this particular row fires it with.
A name, not an id, because a CommandId is host-issued and a plugin must not be able to forge one — the host resolves the name against the CommandRegistry at build time, and an unresolvable name drops that row rather than failing the menu.
The args are per-ROW, which is the whole reason the slot exists: the menu-wide TransientState projection cannot distinguish rows that differ only in a parameter, and that is the shape a plugin menu has (one row per capture template). args::none for a row that wants the state projection instead, which is what every native row does.
Fields
command:stringargs:args
record transient-argument
record transient-argument {
name: string,
default: option<string>,
prompt: string,
}
TR.3b: a field the menu collects before anything fires.
Pressing its key PARKS the whole menu, opens a one-line prompt, writes the answer into the menu's state under name, and puts the menu back — <Esc> cancels the value with the menu untouched. That mechanism is the host's (PendingTransientArgument → resume_parked_transient) and magit's argument rows already use it; this record is only what lets a guest declare one.
It is what makes a template's %^{Question} expressible: several named answers collected before one write, with the menu as the surface throughout rather than a run of prompts the user cannot go back into.
Fields
name:string— Key in the menu's state this answer lands under. Also what names it when the fired row's command has noargs-schema— the rows' order is then the schema.default:option<string>— Pre-filled on first ask;nonefor an empty field. A re-edit always seeds with the value already held.prompt:string— The prompt's label.
variant transient-item-kind
variant transient-item-kind {
action(transient-action),
argument(transient-argument),
dismiss,
}
Mirrors lattice_picker::TransientItemKind. v1 crosses three of its six variants; submenu / flag / variable are deferred with reasons in plugin-transients.md §5.
Cases
action:transient-action— Fires a command and closes the menu.argument:transient-argument— Collects a named value into the menu's state (TR.3b).dismiss— Closes the menu without firing anything. Free, and a menu with noqis a trap.
record transient-item
record transient-item {
key: list<string>,
label: string,
description: string,
kind: transient-item-kind,
}
One row. key is a list of STRINGS, not chars: magit binds multi-key rows (, k, = f) and the resolver walks them one keystroke at a time, so a plugin gets the same expressiveness.
Fields
key:list<string>label:stringdescription:stringkind:transient-item-kind
record transient-group
record transient-group {
label: string,
items: list<transient-item>,
}
A named group of rows — one header, its items, one separator.
record transient-spec
record transient-spec {
title: string,
groups: list<transient-group>,
footer: option<string>,
}
Mirrors lattice_picker::TransientSpec, minus preview: that is a Box<dyn Fn(&TransientState) -> String> and a closure has no WIT form, so a guest-built menu has no live preview pane. Stated here rather than discovered at bindgen.
type count
type count = u32;
Mirrors lattice_grammar::command::Count — a repeat count. Count(1) is the bare invocation; has-explicit-count on the motion context disambiguates G from 1G.
enum latency-class
enum latency-class {
reflex,
display,
background,
}
Mirrors lattice_grammar::command::LatencyClass (§5.2.5 budget class).
variant surface-form
variant surface-form {
keyword,
delimiter(string),
}
Mirrors lattice_grammar::registry::SurfaceForm. delimiter carries the canonical-syntax hint shown when the keyword form is (deliberately) a hard error (:s/pat/repl/, :g/pat/body).
record motion-context
record motion-context {
buffer-id: u32,
from: position,
count: count,
has-explicit-count: bool,
args: args,
}
Mirrors lattice_grammar::registry::MotionContext (owned projection). from is the cursor the motion evaluates from; buffer-id is the active buffer's registry identity (mode-state lookups). Buffer text + the tree-sitter resolver ride the document handle, not this record.
Fields
record motion-result
record motion-result {
target: position,
linewise: bool,
}
Mirrors lattice_grammar::registry::MotionResult. linewise expands the resolved range to whole lines.
Fields
target:positionlinewise:bool
record operator-context
record operator-context {
buffer-id: u32,
range: range,
linewise: bool,
register: register,
count: count,
args: args,
}
Mirrors lattice_grammar::registry::OperatorContext (owned projection). The &mut Document is NOT a field — document mutation is expressed by the returned effect (§4.5), and the operator reads text through the document handle. range is the operator's target span (the lattice_protocol position range, which crosses; the recursive grammar Range is a dispatcher concern the guest never sees).
Fields
buffer-id:u32— CM.3: the buffer being operated on — thetargetanapply-editeffect names.action-contextandmotion-contexthave always carried it; an operator did not, because a NATIVE operator mutates the document in place and never needs to name it. A plugin operator holds a read-only handle and must ask the host to apply, so without this it can read its range and never change it.range:rangelinewise:boolregister:registercount:countargs:args
record text-object-context
record text-object-context {
at: position,
count: count,
args: args,
}
Mirrors lattice_grammar::registry::TextObjectContext (owned projection). at is the cursor; buffer text + the scope/comment env ride the document handle.
Fields
record ex-command-context
record ex-command-context {
bang: bool,
args: args,
register: register,
count: count,
cursor: position,
buffer-id: u32,
}
Mirrors lattice_grammar::registry::ExCommandContext (owned projection). The native range: option<lattice_grammar::range::Range> is ABSENT by design: the grammar Range is recursive (RangeBound::Offset { base: Box<..> }) and carries a plugin RangeId, which a WIT record cannot express (the Global / NarrowTrigger precedent). A v1 ex-command plugin gets bang / args / register / count; the resolved range lands with the range mirror.
Fields
bang:boolargs:argsregister:registercount:countcursor:position— OC.10: where the caret sits when the:line is submitted, and the buffer it was submitted from — the twoaction-contextbelow already carries.They are here because
apply-ex-commandreturnslist<effect>andeffect.apply-editnames atargetbuffer id. Without these a guest could be handed that vocabulary and had no way to build a value for it, which is a seam that looks usable and is not. The nativeExCommandContextgainedbuffer-idat MR.2 on the same reasoning — "a command reached that way was seeing strictly less than the same command reached by a chord" — and the mirror simply never followed.buffer-id:u32
record action-context
record action-context {
args: args,
register: register,
count: count,
cursor: position,
buffer-id: u32,
selection: option<range>,
}
Mirrors lattice_grammar::registry::ActionContext (owned projection) — the count/register prefixes typed before a chord-bound action.
Fields
args:argsregister:registercount:countcursor:position— Where the caret sits when the action fires (AP.0.1) — the action's equivalent ofmotion-context.from. A plugin action pairs it with theborrow<document>handleapply-actionreceives to read the buffer around the cursor.buffer-id:u32— The active buffer's id (AP.2) — thetargeta plugin action names in anapply-editeffect. Mirrorsmotion-context.buffer-id.selection:option<range>— OS.2: the active region when the action fired from a Visual/Select chord;nonein Normal and on every non-chord firing path. Carries no visual kind — the row span is what every consumer reads.The
ex-command-contextprecedent (OC.10): a command reached one way must not see less than the same command reached another. The native peer islattice_mode::ActionContext::selection(MG.18e), and both are filled from ONE host resolver.
record motion-spec
record motion-spec {
jump: bool,
exclusive: bool,
args-schema: list<arg-spec>,
}
Mirrors lattice_grammar::registry::MotionSpec (metadata; apply is a guest export, not a field).
record operator-spec
record operator-spec {
repeatable: bool,
args-schema: list<arg-spec>,
blockwise-per-row: bool,
post-motion-char: bool,
chord: option<string>,
doubled: option<string>,
}
Mirrors lattice_grammar::registry::OperatorSpec (metadata; apply is a guest export). blockwise-per-row is the block-visual dispatch hint.
Fields
repeatable:boolargs-schema:list<arg-spec>blockwise-per-row:boolpost-motion-char:bool— When true, the operator's keymap bindings need a trailing character wildcard after each motion path (e.g. surround'sys{motion}{char}captures the wrapping char).chord:option<string>— CM.2: the chord that invokes this operator, in vim notation (gc,zn).noneregisters the operator without keys — it is then reachable only by name, through the palette or an ex-command.Declared HERE rather than through the
keymapseam because the operator-pending states are not plugin-bindable and cannot be: the host composes motion targets, text-object pendings and find-char pendings around an operator, and that composition needs host-resolved builtins. A plugin says which keys it wants; the host builds the same surface a native operator gets. Binding the chord throughregister-bindinginstead would fire the operator with no motion AND kill its doubled form, because a bound prefix kills its longer chords.doubled:option<string>— The TRAILING key of the doubled, linewise form —cforgcc,UforgUU,dfordd. Not the whole chord.nonebinds no doubled form, which is right for operators that have none: vim has nozff, and binding one would shadow the longerzff{char}.
record text-object-spec
record text-object-spec {
args-schema: list<arg-spec>,
}
Mirrors lattice_grammar::registry::TextObjectSpec (metadata; apply is a guest export).
record ex-command-spec
record ex-command-spec {
latency-class: latency-class,
accepts-bang: bool,
accepts-range: bool,
args-schema: list<arg-spec>,
surface-form: surface-form,
}
Mirrors lattice_grammar::registry::ExCommandSpec (metadata; parse_args
applyare two guest exports, not fields).
Fields
latency-class:latency-classaccepts-bang:boolaccepts-range:boolargs-schema:list<arg-spec>surface-form:surface-form
record action-spec
record action-spec {
args-schema: list<arg-spec>,
}
Mirrors lattice_grammar::registry::ActionSpec (metadata; apply is a guest export).
record event-applied-edit
record event-applied-edit {
original-range: range,
inserted-range: range,
replaced-text: string,
inserted-text: string,
}
Mirrors lattice_protocol::event::AppliedEdit. Distinct from the PH7.3b applied-edit record: the event form carries NO delta (the tree-sitter re-parse delta is a document-actor concern, not published to observers).
Fields
enum event-kind
enum event-kind {
document-opened,
document-closed,
before-save,
document-saved,
document-changed,
selections-changed,
modal-mode-changed,
before-quit,
option-changed,
major-entered,
major-exiting,
minor-activated,
minor-deactivated,
plugin,
pre-plugin-loaded,
plugin-loaded,
plugin-unloaded,
files-changed,
}
Mirrors lattice_protocol::EventKind — the discriminator a subscription filters on. Each arm pairs 1:1 with an event variant arm.
Cases
document-openeddocument-closedbefore-savedocument-saveddocument-changedselections-changedmodal-mode-changedbefore-quitoption-changedmajor-enteredmajor-exitingminor-activatedminor-deactivatedplugin— Discriminator for EVERY plugin-defined event (PH7.8b). All plugin events share this one kind; the per-eventnameis not a bus discriminator — a subscriber filters by name in itson-event.pre-plugin-loaded— OA.14d: a named plugin is about to run the load-time exports that read its OWN options. Delivery is awaited — the loader does not continue the load until every handler has returned — which is what lets aninit.rsset-optionreach a value the plugin consumes at load.plugin-loadedis too late for those.plugin-loaded— CI.1: plugin-lifecycle signals delivered to guests. Aninit.rssubscribes toplugin-loaded(filtering by name in its handler) to run deferred config against a now-present plugin (with-eval-after-load).plugin-unloadedfiles-changed— OR.2: a directory this plugin asked the host to watch changed. One kind for every watch; the host addresses each batch to the plugin that armed it, so subscribing to this kind never surfaces another plugin's watch.
record event-filter
record event-filter {
kinds: option<list<event-kind>>,
path-globs: option<list<string>>,
major-modes: option<list<string>>,
minor-modes: option<list<string>>,
}
Mirrors lattice_runtime::EventFilter — the DECLARATIVE subset a plugin can express at subscribe time. kinds = none is the wildcard (every kind); path-globs / major-modes AND-combine on top (each none is unconstrained), matching the native EF.1 semantics. The native predicate (an arbitrary Rust closure) does NOT cross — a plugin that needs custom logic filters inside its on-event handler (the grammar typed-error-defer precedent).
Fields
kinds:option<list<event-kind>>path-globs:option<list<string>>major-modes:option<list<string>>minor-modes:option<list<string>>— Restrict minor-mode lifecycle events to ones naming these minors — the peer ofmajor-modes, and NOT the same field.minor-activated/minor-deactivatedcarry the MINOR's name, so amajor-modesconstraint rejects every one of them. Without this a subscriber wanting one specific minor had to subscribe unfiltered and compare names in its handler — which wakes the plugin's task for every minor activation in every buffer, to do nothing.Separate rather than merged into one
modeslist because there are far more minors than majors and the two ask different questions:major-modesmeans the buffer is entering one of these majors, this means this specific minor turned on. A merged field would answer both at once and let a subscription fire on a name collision across the two namespaces.Constraining both matches NOTHING, since no event carries both names — the honest reading of "a major event AND a minor event".
record event-plugin-lifecycle
record event-plugin-lifecycle {
name: string,
id: u32,
}
Event::PluginLoaded / Event::PluginUnloaded payload (CI.1). name is the plugin's manifest id (what a handler matches on); id the host-issued numeric plugin id.
record event-document-opened
record event-document-opened {
id: u64,
path: option<string>,
version: u64,
text: string,
}
Event::DocumentOpened payload. path is none for scratch buffers.
record event-document-path
record event-document-path {
id: u64,
path: string,
}
Event::BeforeSave / Event::DocumentSaved payload — both always carry a concrete path (a save target).
record event-document-changed
record event-document-changed {
id: u64,
path: option<string>,
version: u64,
edits: list<event-applied-edit>,
}
Event::DocumentChanged payload. path is none for scratch buffers.
record event-selections-changed
record event-selections-changed {
id: u64,
version: u64,
selections: selection-set,
}
Event::SelectionsChanged payload.
Fields
id:u64version:u64selections:selection-set
record event-modal-mode-changed
record event-modal-mode-changed {
from-state: string,
to-state: string,
}
Event::ModalModeChanged payload — the previous / next modal-state labels.
record event-option-changed
record event-option-changed {
name: string,
old: option<string>,
new-value: string,
}
Event::OptionChanged payload. old is none on the first publish after registration (default init, no prior value).
record event-mode-lifecycle
record event-mode-lifecycle {
buffer: u64,
mode: string,
}
Event::{MajorEntered,MajorExiting,MinorActivated,MinorDeactivated} payload — the buffer + the mode's canonical name.
record event-plugin
record event-plugin {
name: string,
payload: list<u8>,
}
Event::Plugin payload (PH7.8b) — a plugin-defined event. name is the plugin's event identifier (declared via host-services register-event); payload is opaque MessagePack the plugin owns and the host NEVER interprets. The host is a thin router: it moves the bytes, it does not parse them (the boundary discipline the plugin host rests on). The ergonomic typed wrapper (#[derive(PluginEvent)]) is a guest-side SDK layer OVER this opaque wire (PH7.8b.3) — the wire is identical with or without it.
variant event
variant event {
document-opened(event-document-opened),
document-closed(u64),
before-save(event-document-path),
document-saved(event-document-path),
document-changed(event-document-changed),
selections-changed(event-selections-changed),
modal-mode-changed(event-modal-mode-changed),
before-quit,
option-changed(event-option-changed),
major-entered(event-mode-lifecycle),
major-exiting(event-mode-lifecycle),
minor-activated(event-mode-lifecycle),
minor-deactivated(event-mode-lifecycle),
plugin(event-plugin),
pre-plugin-loaded(string),
plugin-loaded(event-plugin-lifecycle),
plugin-unloaded(event-plugin-lifecycle),
files-changed(list<string>),
}
Mirrors lattice_protocol::Event (owned; delivered to on-event). Each multi-field arm carries an explicit payload record (the effect mirror precedent); ids cross as u64, paths as string (a non-UTF-8 path is a typed boundary error, never lossy). Text-bearing arms (document-opened) carry the initial content the native event already clones for observers.
Cases
document-opened:event-document-openeddocument-closed:u64before-save:event-document-pathdocument-saved:event-document-pathdocument-changed:event-document-changedselections-changed:event-selections-changedmodal-mode-changed:event-modal-mode-changedbefore-quitoption-changed:event-option-changedmajor-entered:event-mode-lifecyclemajor-exiting:event-mode-lifecycleminor-activated:event-mode-lifecycleminor-deactivated:event-mode-lifecycleplugin:event-pluginpre-plugin-loaded:string— OA.14d: the manifest id of the plugin whose load-time exports are about to run. No numeric id: the plugin has not finished loading, so the id its contributions will carry is not yet settled — and a handler matches on the name anyway.plugin-loaded:event-plugin-lifecycleplugin-unloaded:event-plugin-lifecyclefiles-changed:list<string>— OR.2: absolute paths that changed under a directory this plugin watches, coalesced — agit pullrewriting two hundred files arrives as ONE delivery carrying two hundred paths. Deduplicated and sorted; a removal is reported as a change (the consumer stats it), because an index that cannot see deletions offers destinations that no longer exist. A non-UTF-8 path is skipped rather than failing the batch —walk's rule, forwalk's reason.No plugin id crosses: a guest only ever receives its own watch.
enum gutter-diff-kind
enum gutter-diff-kind {
add,
remove,
change,
conflict,
}
Mirrors lattice_mode::GutterDiffKind — the diff-sign column.
enum gutter-severity-level
enum gutter-severity-level {
hint,
info,
warning,
error,
}
Mirrors lattice_mode::GutterSeverityLevel — the diagnostic column (ascending severity; max() selects the most severe).
record gutter-diff
record gutter-diff {
line: u32,
kind: gutter-diff-kind,
}
GutterDecoration::Diff { line, kind } payload.
Fields
line:u32kind:gutter-diff-kind
record gutter-severity
record gutter-severity {
line: u32,
level: gutter-severity-level,
}
GutterDecoration::Severity { line, level } payload.
Fields
line:u32level:gutter-severity-level
record gutter-sign
record gutter-sign {
line: u32,
name: string,
}
SG.3b: GutterDecoration::Sign { line, sign } payload — vim's :sign place.
Carries the definition's NAME, not an id. A guest has no id to carry: ids are interned by the host and the resolution happens ONCE, at this boundary, off the render path — which is exactly what keeps the native placement Copy and free of a per-line String.
name is the plugin's own namespaced name as define-sign returned it (debugger.breakpoint). A name nothing has defined resolves to nothing and the placement is SKIPPED — the same answer the native path gives an unknown id, because a definition that has not registered yet is recoverable and failing the whole batch would take the plugin's other marks down with it.
variant gutter-decoration
variant gutter-decoration {
diff(gutter-diff),
severity(gutter-severity),
sign(gutter-sign),
}
Mirrors lattice_mode::GutterDecoration — one per-line gutter cell a provider contributes. Each arm maps to one physical gutter column.
Cases
diff:gutter-diffseverity:gutter-severitysign:gutter-sign
record decoration-context
record decoration-context {
buffer-id: u64,
path: option<string>,
line-count: u32,
}
The owned projection of lattice_mode::DecorationCtx (host→guest). The native ctx is buffer_id + a ServiceRegistry of render-state snapshots (host-owned, can't cross); the projection carries the owned scalars a v1 producer computes from — buffer id / path / line count. Bulk buffer text (a diff producer's input) rides host-services or the deferred document handle (the picker/grammar precedent), NOT this record.
enum media-fit
enum media-fit {
contain,
width,
}
How a media block's intrinsic size maps into its box. Mirrors lattice_cells::MediaFit.
Cases
contain— Scale down to fit, preserving aspect ratio; never scale up.width— Scale to the pane width, up or down; the height follows.
record media-block
record media-block {
anchor-line: u32,
path: string,
alt: option<string>,
fit: media-fit,
}
One inline media block a guest wants drawn.
The guest names a FILE and a LINE; it never sends pixels. That keeps the fs:read decision host-side — the host decides whether this plugin may read that path — and stops a plugin putting arbitrary bytes on screen. It also avoids copying a decoded image across the boundary per load.
Note what is ABSENT: any notion of size. The host resolves the intrinsic dimensions and computes the reserved rows, so sizing policy lives in one place and a guest cannot reserve arbitrary vertical space.
Fields
anchor-line:u32— 0-based source line the block hangs below.path:string— Path to the image. Relative paths resolve against the buffer's own directory, which is what an org[[file:diagram.png]]means.alt:option<string>— What a renderer that cannot draw shows instead, and what a screen reader reads.nonefalls back to the file name — never nothing, because a blank box tells the user nothing about what is missing.fit:media-fit
record context-scope
record context-scope {
scope-start: u32,
scope-end: u32,
header-start: u32,
header-end: u32,
}
One structural scope: the range it spans, plus the line span that NAMES it. Mirrors lattice_cells::context::ContextScope exactly (TC.1).
header-start ..= header-end is normally one line and spans several when a signature wraps. All four are inclusive, 0-based source lines.
A scope is a pure function of the parse tree — no viewport, no cursor, no options. That is what lets the guest compute the set once per parse and the host resolve it per pane afterwards without another guest call, which is the whole reason this seam returns scopes rather than finished rows.
record context-request
record context-request {
buffer-id: u64,
path: option<string>,
line-count: u32,
}
The owned projection handed to a context producer (host→guest), same shape rule as decoration-context (§4.2): owned scalars only, bulk text and structure ride handles instead.
Deliberately NOT reusing decoration-context even though the fields coincide today: the two describe different things (a decoration trigger vs a context request), and sharing the record would make a field one seam needs into ABI churn for the other.
No language and no parse-version field: the guest reads the language off the tree-snapshot it is handed (so the two can never disagree), and the parse version is host-side cache bookkeeping the guest has no use for.
enum ui-zone
enum ui-zone {
left,
center,
right,
}
A modeline zone (mirrors lattice_mode::modeline::Zone).
record ui-notification
record ui-notification {
level: echo-level,
message: string,
}
A user notification a plugin emits — reuses the echo-level severity the effect.echo path already carries.
Fields
level:echo-levelmessage:string