buffer
Direction: guest calls into the host through it · Capability: none (pure data / dispatch) · Worlds: auto-pair-plugin (imports), comment-plugin (imports), grammar-plugin (imports), plugin (imports), project-plugin (imports), treesitter-context-plugin (imports)
Mirrors the native Document / Buffer read seam (plugin-host.md §4.2, §9.6). The host owns the buffer; the guest gets a document resource handle and calls back for the text slices it needs, so bulk rope text never crosses the boundary. The owned buffer-snapshot record carries the non-bulk metadata — the borrows-projected form of lattice_picker::context::ActiveBufferSnapshot. Populated at PH7.3c; the guest→host call through the canonical ABI is exercised at PH7.3d/PH7.4.
Uses
Functions (0)
(none outside its resources — see Resources below)
Resources
resource document
A host-owned, point-in-time view of a document's text (PH7.3c, decision A: backed by an Arc<DocumentSnapshot>). Because it is a snapshot, edits landing after the handle is minted never shift byte ranges under the guest mid-read (the §4.2 mutation-under-read hazard).
document.byte-len
byte-len: func() -> u64
Total byte length.
document.get-text-range
get-text-range: func(r: range) -> result<string, string>
The text of the [start, end) byte range. err on an out-of-range or end < start range (mirrors Buffer::slice). Only the requested range is sliced out of the rope — the whole document never crosses ("zero-copy at the slice level", §9.6).
Example — Read the one byte after the caret, treating a read error as nothing there · plugins/auto-pair/src/lib.rs
/// The single byte after the caret (empty string at EOL / on a read error —
/// which just means "nothing to step over", so insert).
fn char_after(ctx: &ActionContext, doc: &Document) -> String {
doc.get_text_range(Range {
start: ctx.cursor,
end: one_right(ctx.cursor),
})
.unwrap_or_default()
}
document.line
line: func(n: u32) -> option<string>
Line n (0-based) as text without its trailing newline (matching Buffer::line), or none when n is past the last line.
Example — Read the first line of an operator's range (and the buffer's path), erring if it is gone · crates/lattice-plugin-host/tests/fixtures/grammar-guest/src/lib.rs
let line = doc
.line(ctx.range.start.line)
.ok_or_else(|| format!("fixture: no line {}", ctx.range.start.line))?;
// The PATH as well as the text. An operator's handle was minted
// with `path: None` at first, so `document.path()` answered
// `none` for every real file — invisible until a plugin asked.
let path = doc.path().unwrap_or_else(|| "<none>".to_string());
Ok(vec![Effect::Echo(EchoPayload {
level: EchoLevel::Info,
text: format!("op|{path}|{line}"),
})])
document.line-count
line-count: func() -> u32
Lines the document has: "a\nb\n" is two lines, and so is "a\nb" — a trailing newline terminates the last line rather than starting another. Safe as the bound of a 0..line-count walk calling line.
document.path
path: func() -> option<string>
OM.6b: the file this document is backed by, absolute. none for a buffer with no file on disk — a scratch buffer, a synthetic one, or a file whose path is not UTF-8 (it cannot cross as a string, and one oddly-named file must not fail the call).
Why the resource and not a context field. "Which file am I editing" is a question every content-aware guest asks, and a guest asking it always holds a document. On a context it would have to be re-added to motion-context, text-object-context and ex-command-context in turn, and every dispatch would pay the string clone whether or not the guest read it.
Snapshot semantics, like every other method here: this is the path as of the handle's mint. A set-path landing mid-action is invisible, which is the same trade get-text-range already makes and for the same reason.
Example — Derive a sibling <file>_archive path from the buffer's own path and append to it · crates/lattice-plugin-host/tests/fixtures/grammar-guest/src/lib.rs
let Some(mine) = doc.path() else {
return Err("fixture: this buffer has no file".to_string());
};
Ok(vec![Effect::WriteToFile(WriteToFilePayload {
path: format!("{mine}_archive"),
anchor: FileAnchor::End,
text: "* Archived beside me\n".to_string(),
cut: None,
create_parents: false,
save: false,
})])
Types (1)
record buffer-snapshot
record buffer-snapshot {
buffer-id: u32,
path: option<string>,
language: option<string>,
cursor: position,
selection: option<tuple<position, position>>,
}
The owned projection of ActiveBufferSnapshot's metadata (§4.2). Bulk text is NOT a field here — it rides the document handle. selection is (anchor, head) when a Visual selection is active.
Fields
buffer-id:u32path:option<string>language:option<string>cursor:positionselection:option<tuple<position, position>>