Skip to main content

lattice_runtime/
document.rs

1//! M.0 (2026-05-31): `Document` trait — the handle-layer
2//! abstraction over a buffer that the rest of the editor talks
3//! to.
4//!
5//! Today's `RopeDocumentHandle` (a rope-backed actor handle) impls
6//! this trait; M.1 lands `MultibufferDocumentHandle` as a sibling
7//! impl composing N source handles. Dispatch / motion / render
8//! code paths hold `Arc<dyn Document>` so they serve both kinds
9//! without kind-branching at the buffer boundary.
10//!
11//! See `docs/dev/architecture/multibuffer-views.md` §3.1 for the
12//! design and the Path-A/B/C alternatives that were considered
13//! and rejected.
14//!
15//! ## Slot replacement is the only path
16//!
17//! There is intentionally no `replace(...)` method on this trait.
18//! Per the M.0 design (§3.1 "Why no `replace`"), "the active
19//! document changes" is expressed by replacing the slot's
20//! `Arc<dyn Document>` with a freshly spawned handle, not by
21//! mutating the existing handle in place. The old handle drops
22//! when its last Arc reference goes away; the actor task exits
23//! cleanly through its mailbox-close branch. One uniform code
24//! path for `:edit foo` / `:edit bar` / regular ↔ multibuffer
25//! transitions / `:b N` switches.
26//!
27//! ## Object safety
28//!
29//! Trait is `dyn`-safe: `Arc<dyn Document>` is the canonical
30//! reference shape. All methods take `&self` (writes return
31//! `Pending<T>` which round-trips the impl's internal write
32//! path — actor mpsc for `RopeDocumentHandle`, fan-out for
33//! `MultibufferDocumentHandle`).
34//!
35//! Most read methods carry default implementations derived from
36//! `snapshot()` so impls only need to provide `snapshot()` plus
37//! the write methods. `MultibufferDocumentHandle` may override
38//! `text()` if it can compose more cheaply than `snapshot()
39//! .text()`.
40
41use std::path::PathBuf;
42use std::sync::Arc;
43
44use lattice_grammar::{CancellationToken, CommandInvocation, Effect};
45use lattice_protocol::edit::Edit;
46use lattice_protocol::ids::DocumentId;
47use lattice_protocol::position::Position;
48use lattice_protocol::selection::SelectionSet;
49
50use crate::actor::AppliedEdit;
51use crate::handle::RopeDocumentHandle;
52use crate::pending::Pending;
53use crate::snapshot::{DocumentSnapshot, SnapshotCache};
54
55/// N.1.4b (2026-06-10): a sharable, thread-safe scope resolver
56/// handle threaded through grammar dispatch. The host wraps the
57/// active document's `Arc<SyntaxSnapshot>` (which impls
58/// `lattice_grammar::ScopeResolver`) in this alias, bundles it into a
59/// [`DispatchEnv`], and passes it to [`Document::dispatch_with_env`];
60/// the actor hands it to `execute_with_env` so tree-sitter text objects
61/// (af/ac/aa/al) resolve against the live tree. `Send + Sync`
62/// because the actor message crosses the dispatch channel onto the
63/// document-actor thread. The bare `ScopeResolver` trait lives in
64/// `lattice-grammar` (tree-sitter-agnostic); this threading-layer
65/// alias adds the `Arc + Send + Sync` shape per the handle
66/// convention (cf. `CommandRegistryHandle`).
67pub type ScopeResolverHandle = Arc<dyn lattice_grammar::ScopeResolver + Send + Sync>;
68
69/// IN.7: the same handle convention for the `=` operator's per-line
70/// indent source. Owned + `Send + Sync` so it can cross the actor
71/// channel; the grammar side sees a bare `&dyn IndentResolver`.
72pub type IndentResolverHandle = Arc<dyn lattice_grammar::IndentResolver + Send + Sync>;
73
74/// VM.3i: the same handle convention for `zj` / `zk`'s fold edges. The host
75/// builds one from its fold table for a dispatch; the grammar sees a bare
76/// `&dyn FoldResolver`.
77pub type FoldResolverHandle = Arc<dyn lattice_grammar::FoldResolver + Send + Sync>;
78/// VM.3e: owned mark table for `'x` / `` `x ``, like [`FoldResolverHandle`].
79pub type MarkResolverHandle = Arc<dyn lattice_grammar::MarkResolver + Send + Sync>;
80/// VM.3f: owned window lines for `H` / `M` / `L`, like [`FoldResolverHandle`].
81pub type ViewportResolverHandle = Arc<dyn lattice_grammar::ViewportResolver + Send + Sync>;
82/// VM.3g-2: owned display geometry for `gj` / `gk` / `g0` / `g$`.
83pub type DisplayResolverHandle = Arc<dyn lattice_grammar::DisplayResolver + Send + Sync>;
84
85/// N.1.6 (2026-06-10): the owned, thread-safe per-dispatch environment
86/// for text objects, carried across the actor channel. Bundles the
87/// inputs a text object's `apply` may read — the tree-sitter
88/// `scope_resolver` (`af`/`ac`, N.1.4) and the `comment_syntax`
89/// (`aC`/`iC`, N.1.6) — so the dispatch seam threads ONE value instead
90/// of a widening parameter list (the long-term-fit choice over parallel
91/// params). The actor borrows these into a `lattice_grammar::GrammarEnv`
92/// at the `execute_with_env` call. `Default` is the no-input case.
93#[derive(Clone, Default)]
94pub struct DispatchEnv {
95    pub scope_resolver: Option<ScopeResolverHandle>,
96    pub comment_syntax: Option<Arc<lattice_grammar::CommentSyntax>>,
97    /// OT.4: the buffer's tree-sitter snapshot, type-erased so
98    /// `lattice-runtime` stays syntax-free (the plugin trampoline
99    /// downcasts it back to an `Arc<SyntaxSnapshot>`).
100    ///
101    /// Distinct from `scope_resolver` rather than replacing it, and the
102    /// distinction is why this field had to exist. The resolver answers
103    /// "what scope encloses this point" for the NATIVE structural objects
104    /// and is deliberately erased to that one question — the concrete
105    /// snapshot cannot be recovered from it, so it cannot mint a plugin's
106    /// `tree-snapshot` resource.
107    ///
108    /// TS.1 left this out because grammar *actions* were the only
109    /// tree-snapshot consumer, and those dispatch through the host's Action
110    /// gate rather than this channel. OT.1 made motions and text objects
111    /// consumers too, and they come through here — so without this field
112    /// org's `ar` / `]]` / `g{` received `none` on every real keystroke
113    /// while every test that built a `GrammarEnv` by hand passed.
114    pub syntax: Option<Arc<dyn std::any::Any + Send + Sync>>,
115    /// IN.0: one level of indentation, resolved by the host from
116    /// `shiftwidth` / `expandtab` / `tabstop`. Read by `>` / `<`.
117    /// `Default` is the registered option defaults, so the actor path
118    /// indents like an unconfigured buffer rather than not at all.
119    pub indent: lattice_core::IndentUnit,
120    /// IN.7: per-line indent depth for `=`. `None` means no
121    /// structural source, and `=` reindents nothing rather than
122    /// guessing.
123    pub indent_resolver: Option<IndentResolverHandle>,
124    /// RF.2: the buffer's `textwidth`, for the reflow operator. Carried
125    /// here for the reason `syntax` and `selection` had to be: `gq`
126    /// reaches the grammar through the ACTOR path on every real
127    /// keystroke, so a field missing here is a field the operator never
128    /// sees no matter how well the direct path is tested.
129    ///
130    /// `WrapWidth`, not `usize`, so this struct keeps its derived
131    /// `Default` and still means 80 rather than 0.
132    pub textwidth: lattice_core::WrapWidth,
133    /// RF.5b: which formatting intents this buffer handles natively.
134    /// Carried for the reason `textwidth` is: `=` and `gq` reach the
135    /// grammar through the ACTOR path on every real keystroke.
136    pub native_format: lattice_grammar::registry::NativeFormatIntents,
137    /// OS.2: the active region — the Visual/Select selection extent, or `None`
138    /// outside Visual/Select.
139    ///
140    /// Carried here for the reason `syntax` above had to be: an action reached
141    /// through the ACTOR path builds its `ActionContext` from this env, so a
142    /// hard `None` would make a plugin's Visual action see no region on every
143    /// real keystroke while every test that hand-built a `GrammarEnv` passed.
144    /// That is the OT.4 failure verbatim, and it is cheaper to carry the field
145    /// than to rediscover it.
146    pub selection: Option<lattice_protocol::position::Range>,
147    /// VM.3c: the last `f` / `F` / `t` / `T`, so `;` and `,` can be motions.
148    ///
149    /// Same reasoning as every field above it: `;` is an ordinary motion on
150    /// the keystroke path, so it comes through the actor, so the state it
151    /// needs has to travel with the dispatch. `None` is the pre-first-`f`
152    /// state and makes `;` a no-op, which is what vim does.
153    pub last_find: Option<lattice_grammar::LastFind>,
154    /// VM.3i: the fold edges `zj` / `zk` step between. Same reasoning as
155    /// `last_find`: they're motions on the keystroke path, so they come
156    /// through the actor. `None` for a buffer with no folds.
157    pub fold_resolver: Option<FoldResolverHandle>,
158    /// VM.3d-2: the last search, for `n` / `N` / `*` / `#`, which are motions
159    /// on the keystroke path and so come through the actor too.
160    pub last_search: Option<lattice_grammar::LastSearch>,
161    /// VM.3e: the mark table, for `'x` / `` `x ``, which come through the
162    /// actor like every keystroke motion. `None` when no mark is set.
163    pub marks: Option<MarkResolverHandle>,
164    /// VM.3f: the lines the window shows, for `H` / `M` / `L`. The host builds
165    /// it only when the invocation is one of them.
166    pub viewport: Option<ViewportResolverHandle>,
167    /// VM.3f: `!startofline` — `H` / `M` / `L` keep the cursor's column.
168    pub nostartofline: bool,
169    /// VM.3f: `scrolloff`, the margin `H` / `L` stop short of the window's
170    /// edges by. `0` puts them on the first and last lines shown.
171    pub scrolloff: u32,
172    /// VM.3g-1: vim's `curswant`, the column `j` / `k` aim for across short
173    /// lines. Carried like every other keystroke input: `j` comes through the
174    /// actor on every press.
175    pub curswant: Option<lattice_grammar::Curswant>,
176    /// VM.3g-2: display geometry for `gj` / `gk` / `g0` / `g$`, built only when
177    /// the invocation is one of them.
178    pub display: Option<DisplayResolverHandle>,
179    /// VM.3g-3: where a motion reports the goal column it AIMED at, which for
180    /// `gj` / `gk` is not the column it reached — the display row it lands on
181    /// may be too short, and vim keeps the aim (measured: a clamped `gj`
182    /// landing at column 100 records `curswant` 160).
183    ///
184    /// Owned rather than borrowed, unlike its `GrammarEnv` counterpart, and
185    /// that is forced rather than chosen: this struct crosses the `Document`
186    /// trait into a `Pending<Effect>`, so a borrow could not outlive the
187    /// future. The actor borrows it back out at the `execute_with_env` call,
188    /// which is the same shape every other field here already has.
189    ///
190    /// `None` — the default — discards the report, which is right for every
191    /// caller that keeps no goal column.
192    pub curswant_out: Option<Arc<std::sync::Mutex<Option<lattice_grammar::Curswant>>>>,
193}
194
195/// Handle-layer abstraction over a buffer. See module docs.
196///
197/// `Debug` is a supertrait so containers holding `dyn Document`
198/// (e.g., `lattice_host::buffer_registry::DocumentEntry`) can
199/// derive `Debug` without hand-rolling per-field formatters.
200pub trait Document: Send + Sync + 'static + std::fmt::Debug {
201    // ---- Reads (wait-free, snapshot-backed) ----
202
203    /// Load the current snapshot. The returned `Arc` lives as
204    /// long as the caller needs it; subsequent publishes don't
205    /// invalidate it.
206    fn snapshot(&self) -> Arc<DocumentSnapshot>;
207
208    /// Per-thread cache for hot loops that load the snapshot
209    /// many times between edits. The cache reduces per-load cost
210    /// from ~17 ns to ~2 ns when the writer hasn't published
211    /// since the last load.
212    fn snapshot_cache(&self) -> SnapshotCache;
213
214    fn id(&self) -> DocumentId {
215        self.snapshot().id
216    }
217
218    /// Rendered text. Allocates — prefer `snapshot().buffer
219    /// .as_string()` on a held snapshot when looping.
220    fn text(&self) -> String {
221        self.snapshot().text()
222    }
223
224    fn path(&self) -> Option<PathBuf> {
225        self.snapshot().path.as_ref().map(|a| (**a).clone())
226    }
227
228    fn dirty(&self) -> bool {
229        self.snapshot().dirty
230    }
231
232    fn version(&self) -> u64 {
233        self.snapshot().version
234    }
235
236    fn text_version(&self) -> u64 {
237        self.snapshot().text_version
238    }
239
240    fn selections(&self) -> Arc<SelectionSet> {
241        self.snapshot().selections.clone()
242    }
243
244    // ---- Writes (enqueue + Pending) ----
245
246    fn apply_edit(&self, edit: Edit) -> Pending<AppliedEdit>;
247
248    fn apply_edit_batch(&self, edits: Vec<Edit>) -> Pending<Vec<AppliedEdit>>;
249
250    fn undo(&self) -> Pending<Vec<AppliedEdit>>;
251
252    fn redo(&self) -> Pending<Vec<AppliedEdit>>;
253
254    fn save(&self) -> Pending<PathBuf>;
255
256    fn save_as(&self, path: PathBuf) -> Pending<()>;
257
258    fn set_selections(&self, selections: SelectionSet) -> Pending<()>;
259
260    /// Open an undo-coalescing group so a run of edits (a vim insert
261    /// session: `i`/`a`/`o`/`cw` .. `<Esc>`) collapses into a single
262    /// undo unit until [`Self::end_undo_group`]. Fire-and-forget:
263    /// ordering against the following edits is the impl's responsibility
264    /// (the actor mailbox is FIFO), so there is nothing to await.
265    ///
266    /// Default no-op. Buffer kinds without a first-class undo stack --
267    /// the `MultibufferDocumentHandle` and the default placeholder
268    /// handle -- ignore grouping; `RopeDocumentHandle` overrides both to
269    /// signal its actor. A no-op default keeps grouping non-regressive
270    /// for those kinds (their edits remain individually undoable, as
271    /// today) without every impl having to opt in.
272    fn begin_undo_group(&self) {}
273
274    /// Close the group opened by [`Self::begin_undo_group`]. Default
275    /// no-op; see that method.
276    fn end_undo_group(&self) {}
277
278    // ---- Grammar dispatch ----
279
280    /// Dispatch a [`CommandInvocation`] through the impl's
281    /// internal grammar execution path. `MultibufferDocument
282    /// Handle` (M.1) routes the invocation through its
283    /// row-translation table to the underlying source(s).
284    fn dispatch_with_cancel(
285        &self,
286        invocation: CommandInvocation,
287        cursor: Position,
288        cancel: CancellationToken,
289    ) -> Pending<Effect>;
290
291    /// N.1.4b / N.1.6 (2026-06-10): dispatch carrying a [`DispatchEnv`]
292    /// (tree-sitter `scope_resolver` for af/ac/aa/al + `comment_syntax`
293    /// for aC/iC) so the grammar resolves those text objects against the
294    /// caller's live syntax + language. The default impl ignores the env
295    /// and delegates to [`Self::dispatch_with_cancel`] -- correct for
296    /// buffer kinds with no syntax / no leader (oil, terminal,
297    /// plain-language). `RopeDocumentHandle` overrides it to forward the
298    /// env into the actor's `execute_with_env` call; the multibuffer
299    /// overrides `dispatch_with_cancel` (N.1.5) and ignores this env.
300    fn dispatch_with_env(
301        &self,
302        invocation: CommandInvocation,
303        cursor: Position,
304        cancel: CancellationToken,
305        env: DispatchEnv,
306    ) -> Pending<Effect> {
307        let _ = env;
308        self.dispatch_with_cancel(invocation, cursor, cancel)
309    }
310
311    /// Convenience: dispatch with a never-cancelled token.
312    fn dispatch(&self, invocation: CommandInvocation, cursor: Position) -> Pending<Effect> {
313        self.dispatch_with_cancel(invocation, cursor, CancellationToken::never())
314    }
315
316    /// K.4.6 follow-up (2026-06-02): per-composed-row source line
317    /// number lookup for the gutter. `None` (default) = identity:
318    /// the composed-row index IS the source line number, so the
319    /// gutter formats `composed_row` directly. `Some(arr)` =
320    /// composed→source map: `arr[composed_row]` gives the source
321    /// line number to display.
322    ///
323    /// Regular `RopeDocumentHandle` keeps the default None impl —
324    /// for a single-file buffer, composed_row == source_row.
325    /// `MultibufferDocumentHandle` (lattice-multibuffer) overrides
326    /// to return the flattened `RowTranslation` so the gutter
327    /// shows the original file's line numbers (e.g. 429, 430,
328    /// 432 — skipping non-hit rows) rather than the meaningless
329    /// composed indices (0, 1, 2).
330    ///
331    /// Substrate-aligned per [[feedback_buffers_no_special_case]]:
332    /// the publisher reads `self.document.display_line_numbers()`
333    /// uniformly across all BufferKinds; no renderer-side or
334    /// publish-side kind branch needed.
335    fn display_line_numbers(&self) -> Option<Arc<[u32]>> {
336        None
337    }
338
339    /// K.4.7: per-excerpt highlight entries for multibuffer panes.
340    /// Default returns empty — regular single-file documents carry
341    /// no excerpt structure.  `MultibufferDocumentHandle` overrides
342    /// to return one entry per excerpt that has a `SyntaxHandle`.
343    ///
344    /// The cells worker calls this uniformly on every pane's document;
345    /// no `BufferKind` branch needed in `publish_render_state`.
346    fn excerpt_highlights(&self) -> Vec<lattice_cells::ExcerptHighlight> {
347        Vec::new()
348    }
349
350    /// Monotonic counter that bumps whenever the excerpt highlight set
351    /// changes (new sources added, lang registry wired).  Default: 0.
352    fn excerpt_syntax_version(&self) -> u64 {
353        0
354    }
355}
356
357/// M.0 (2026-05-31): the active-document slot held by
358/// `Editor.document`. Wraps an `Arc<dyn Document>` so that:
359///
360/// * The slot can hold either a regular rope-backed handle
361///   (today: `RopeDocumentHandle`, renamed to `RopeDocumentHandle`
362///   in M.0 Phase E) or a `MultibufferDocumentHandle` (M.1)
363///   without kind-branching at the use site — dispatch /
364///   motion / render code paths just call `Document` trait
365///   methods through `Deref<Target = dyn Document>`.
366///
367/// * The slot impls `Default` (initialised to a placeholder
368///   rope handle via `RopeDocumentHandle::default()`) so consumers
369///   that `#[derive(Default)]` over a struct containing this
370///   field work without hand-rolling the impl. The
371///   placeholder's actor receiver is closed immediately at
372///   construction, so any traffic sent through the placeholder
373///   reports `RuntimeError::ActorGone` — production code
374///   overwrites the slot before any real traffic flows.
375///
376/// * The newtype is cheap to clone (one atomic refcount bump
377///   on the inner `Arc`).
378///
379/// `Arc<dyn Document>` directly cannot implement `Default`
380/// (orphan rule on `Arc` + `dyn Trait: !Sized`), so this
381/// newtype is the smallest viable wrapper that keeps the
382/// derive ergonomics intact without forcing a manual
383/// `impl Default` over every struct that holds the slot.
384#[derive(Clone)]
385pub struct ActiveDocument(Arc<dyn Document>);
386
387impl ActiveDocument {
388    /// Wrap a concrete document handle. The handle must impl
389    /// `Document` (today: `RopeDocumentHandle` / future
390    /// `RopeDocumentHandle`; M.1: `MultibufferDocumentHandle`).
391    pub fn new<D: Document>(handle: D) -> Self {
392        Self(Arc::new(handle))
393    }
394
395    /// Wrap an already-`Arc`'d handle. Useful when an
396    /// `Arc<dyn Document>` is constructed elsewhere (e.g., by
397    /// the buffer registry) and the slot just takes ownership
398    /// of it.
399    pub fn from_arc(arc: Arc<dyn Document>) -> Self {
400        Self(arc)
401    }
402
403    /// Cheap clone of the inner `Arc`. Use when handing the
404    /// reference off to a long-lived consumer that wants to
405    /// store it independently.
406    pub fn as_arc(&self) -> Arc<dyn Document> {
407        Arc::clone(&self.0)
408    }
409}
410
411impl Default for ActiveDocument {
412    fn default() -> Self {
413        Self(Arc::new(RopeDocumentHandle::default()))
414    }
415}
416
417impl std::ops::Deref for ActiveDocument {
418    type Target = dyn Document;
419
420    fn deref(&self) -> &Self::Target {
421        &*self.0
422    }
423}
424
425impl std::fmt::Debug for ActiveDocument {
426    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
427        f.debug_tuple("ActiveDocument").field(&self.0.id()).finish()
428    }
429}