decorations
Direction: guest implements this interface · Capability: none (pure data / dispatch) · Worlds: decorations-plugin (exports)
The decoration producer API (plugin-host.md §5 decorations, PH7.9), mirroring Mode::gutter_decorations + GutterDecoration (lattice-mode). A WASM decoration provider exports this interface; the host calls its gutter-decorations producer off the render path on a trigger (edit / scroll / diagnostic change), caches the returned list<gutter-decoration> per buffer, and the renderer reads the cache.
Producer, not per-frame (the completion PH7.6 fork). The native Mode::gutter_decorations is a SYNCHRONOUS trait the renderer reads every frame — a WASM mode cannot satisfy it inline (that would be per-frame WASM, a paramount-#1 violation, §7 rule 7). So the seam is an ASYNC producer whose result the host caches; the renderer never calls WASM on the tick. The matching / layout of the cached decorations into physical gutter columns stays native (the host builds the snapshot).
Uses
decoration-contextfromtypesgutter-decorationfromtypes
Functions (1)
gutter-decorations
gutter-decorations: func(ctx: decoration-context) -> result<list<gutter-decoration>, string>
Produce the per-line gutter decorations for a buffer. ctx is the owned projection (buffer id / path / line count, §4.2); bulk buffer text (a diff producer's input) rides host-services / the deferred document handle, not the context. Async — a produce call suspends the guest, never the render path. An err string is logged and the provider contributes no decorations for this trigger (graceful, §8) — the cached snapshot keeps its prior value so cues never flicker mid-refresh.
Example — Return diff, severity and named-sign gutter marks, erring on an empty buffer · crates/lattice-plugin-host/tests/fixtures/decorations-guest/src/lib.rs
fn gutter_decorations(ctx: DecorationContext) -> Result<Vec<GutterDecoration>, String> {
if ctx.line_count == 0 {
// Graceful: nothing to decorate → a typed guest err, not a trap.
return Err("empty buffer: no decorations".to_string());
}
Ok(vec![
GutterDecoration::Diff(GutterDiff {
line: 0,
kind: GutterDiffKind::Change,
}),
GutterDecoration::Severity(GutterSeverity {
line: 1,
level: GutterSeverityLevel::Error,
}),
// Keyed off `line_count` — proves the context crossed in.
GutterDecoration::Diff(GutterDiff {
line: ctx.line_count - 1,
kind: GutterDiffKind::Add,
}),
// SG.3b: a sign placement, by NAME. The host interns the name to a
// `SignId` at the boundary — a guest has no id to carry, which is
// exactly what lets the native placement stay `Copy`.
GutterDecoration::Sign(GutterSign {
line: 2,
name: "fixture.mark".to_string(),
}),
// A name nothing defined. It must be SKIPPED while everything
// around it still crosses — if this failed the batch, one
// unregistered sign would take the plugin's diff and severity
// marks down with it.
GutterDecoration::Sign(GutterSign {
line: 3,
name: "fixture.undefined".to_string(),
}),
])
}