lattice_core/buffers.rs
1//! Buffer-kind discriminator for the multi-buffer foundation
2//! (DESIGN.md §5.9).
3//!
4//! Phase 1 wiring: every concrete buffer type the App can hold a
5//! cursor in (today: a code [`Document`] and an optional
6//! `HelpBuffer`) is tagged with a [`BufferKind`]. The App carries
7//! one `active_buffer: BufferKind` which decides where keystrokes
8//! land -- a `j` in Normal mode resolves the same `line_down`
9//! motion against the active buffer, regardless of kind.
10//!
11//! [`BufferId`] is a stable, monotonically-allocated handle. v1 only
12//! has at most one buffer per kind, but the id scaffolding lands now
13//! so position-history entries (§5.1.1) can identify "which buffer"
14//! once Phase B.1.c spawns multiple Document buffers.
15//!
16//! [`Document`]: crate::Document
17
18use std::sync::atomic::{AtomicU32, Ordering};
19
20/// Which kind of buffer the App's input pipeline currently routes
21/// to. The chord grammar, motions, and position history are shared;
22/// kind only matters at a few discrete decision points: which cursor
23/// a motion mutates, whether mutating actions are accepted (most
24/// non-document kinds are read-only), and which buffer-local
25/// bindings apply (Help binds `<CR>` to follow-link, FileTree binds
26/// `<CR>` to follow-entry, etc.).
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
28pub enum BufferKind {
29 /// The user's edit-target -- one [`Document`] today.
30 ///
31 /// [`Document`]: crate::Document
32 #[default]
33 Document,
34 /// A `:describe-*` / `:apropos` / `:keymap` view. Read-only;
35 /// motions / yank work, edits don't.
36 Help,
37 /// Filesystem hierarchy view (DESIGN.md §5.9 buffer-as-content).
38 /// Rope-backed with one rendered line per visible entry; `<CR>`
39 /// on a directory toggles expansion, on a file opens it as a
40 /// new Document buffer.
41 FileTree,
42 /// Flat editable directory listing (oil.nvim-style).
43 /// Writable — operators and motions run against the oil rope;
44 /// `:w` diffs the rope against its snapshot and executes
45 /// renames/deletes/creates on disk.
46 Oil,
47 /// PTY-backed terminal buffer (issue #40, T1 / Terminal-mode).
48 /// Owns a child process via `lattice-terminal::PtyHandle`; the
49 /// reader task publishes `TerminalSnapshot`s the renderer paints
50 /// as a cell grid. Two sub-states (T2): Normal-in-terminal (vim
51 /// motions over the scrollback) and Terminal-Insert (keystrokes
52 /// encoded → PTY stdin). NOT editable via the standard rope
53 /// operator path — `is_read_only()` returns true so the
54 /// operator dispatcher leaves it alone.
55 Terminal,
56 /// `*messages*` audit-log buffer (DESIGN.md §5.10.6 / emacs'
57 /// `*Messages*` analogue). Rope-backed like [`Document`]
58 /// internally — its mode (`messages-mode`) contributes
59 /// `ReadOnly = true` so the dispatcher gates keystrokes, and
60 /// `NoFile = true` so `:q` skips its dirty check. The kind is
61 /// distinct so introspection (`:ls`, modeline) doesn't
62 /// conflate the transcript with user-edited documents.
63 ///
64 /// [`Document`]: crate::Document
65 Messages,
66 /// Composed view over N source buffers,
67 /// owned by `lattice_multibuffer::MultibufferDocumentHandle`.
68 /// Read-only in M.1; edit propagation lands in M.3.
69 /// `MultibufferMode` (the major mode registered against this
70 /// kind in M.2.b.2) owns the typed handle as its per-buffer
71 /// context Guard, plus the `]e` / `[e` / `]E` / `[E` motion
72 /// keymap. See `docs/dev/architecture/multibuffer-views.md`
73 /// §3.6.
74 ///
75 /// Slice: M.2.b.1 (2026-05-31).
76 Multibuffer,
77 /// `*dashboard*` launch page (DB.2, `docs/dev/architecture/dashboard.md`).
78 /// Rope-backed like [`Document`]; its major mode (`dashboard-mode`)
79 /// contributes `ReadOnly = true` + `NoFile = true` so the dispatcher
80 /// gates keystrokes and `:q` skips its dirty check. Behaviourally a
81 /// read-only, link-bearing, help-style buffer — it is grouped with
82 /// [`BufferKind::Help`] in the `<CR>`-follow / Esc-dismiss gates
83 /// (`input.rs`, `dispatch.rs`) and reuses the help link machinery, while
84 /// staying a distinct kind so `:ls` / introspection don't conflate it
85 /// with the transcript or user documents.
86 ///
87 /// [`Document`]: crate::Document
88 Dashboard,
89}
90
91impl BufferKind {
92 /// Whether mutating operators (delete, change, paste, insert)
93 /// are accepted on this kind. Only [`BufferKind::Document`] and
94 /// [`BufferKind::Oil`] are writable; Terminal mutates via its
95 /// own PTY-stdin path, not through rope operators. `Messages`
96 /// is read-only at the dispatcher level (`messages-mode`
97 /// contributes `ReadOnly`); subsystem appends bypass via the
98 /// edit-batch path.
99 pub fn is_read_only(self) -> bool {
100 matches!(
101 self,
102 BufferKind::Help
103 | BufferKind::FileTree
104 | BufferKind::Terminal
105 | BufferKind::Messages
106 | BufferKind::Multibuffer
107 | BufferKind::Dashboard
108 )
109 }
110
111 /// Short label for echo-area diagnostics.
112 pub fn label(self) -> &'static str {
113 match self {
114 BufferKind::Document => "document",
115 BufferKind::Help => "help",
116 BufferKind::FileTree => "file-tree",
117 BufferKind::Oil => "oil",
118 BufferKind::Terminal => "terminal",
119 BufferKind::Messages => "messages",
120 BufferKind::Multibuffer => "multibuffer",
121 BufferKind::Dashboard => "dashboard",
122 }
123 }
124}
125
126/// Stable monotonic handle identifying one buffer instance. Two
127/// buffers with the same [`BufferKind`] still have distinct ids.
128/// The App allocates these via [`BufferId::next`] at buffer-
129/// creation time and stores them on each buffer + on every
130/// position-history entry.
131#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
132pub struct BufferId(pub u32);
133
134impl BufferId {
135 /// Allocate a fresh id. Process-global, monotonic, never
136 /// recycled (collision impossible inside one process lifetime
137 /// short of 2^32 buffer creations).
138 pub fn next() -> Self {
139 static NEXT: AtomicU32 = AtomicU32::new(1);
140 Self(NEXT.fetch_add(1, Ordering::Relaxed))
141 }
142}
143
144/// Vim-style per-buffer flags (DESIGN.md §5.9). The shape is fixed
145/// now so additions don't churn every call site; v1 ships with
146/// `listed` populated (`:bn` / `:bp` / `:ls` skip unlisted buffers
147/// once the wiring lands) and `hidden` reserved for "keep loaded
148/// without a window" semantics. Both default to "normal buffer"
149/// (listed = true, hidden = false).
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151pub struct BufferFlags {
152 /// Whether the buffer appears in `:bn` / `:bp` / `:ls`.
153 /// Vim's "unlisted" buffer (`:setlocal nobuflisted`).
154 pub listed: bool,
155 /// Whether the buffer stays loaded even when no window shows
156 /// it. Vim's `'hidden'` option per buffer. v1 doesn't gc on
157 /// pane close, so this is informational; future cleanup
158 /// passes will read it.
159 pub hidden: bool,
160 /// Transient popup-backing buffer. Stronger than
161 /// `listed: false` — an ephemeral buffer is invisible to `:ls`
162 /// ENTIRELY (not just shown with a `u` marker like an unlisted
163 /// buffer), never appears in `:bn` / `:bp`, and is garbage-
164 /// collected when its owning popup dismisses. Used by content
165 /// popups that join the registry to render through the compose
166 /// seam (completion docs today; hover/signature already ride the
167 /// floating-popup slot). Default `false` (a normal buffer).
168 ///
169 /// Slice: PU.5.
170 pub ephemeral: bool,
171}
172
173impl Default for BufferFlags {
174 fn default() -> Self {
175 Self {
176 listed: true,
177 hidden: false,
178 ephemeral: false,
179 }
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn document_is_writable() {
189 assert!(!BufferKind::Document.is_read_only());
190 }
191
192 #[test]
193 fn help_is_read_only() {
194 assert!(BufferKind::Help.is_read_only());
195 }
196
197 #[test]
198 fn file_tree_is_read_only() {
199 assert!(BufferKind::FileTree.is_read_only());
200 }
201
202 #[test]
203 fn buffer_id_is_monotonic() {
204 let a = BufferId::next();
205 let b = BufferId::next();
206 assert!(b.0 > a.0);
207 }
208
209 #[test]
210 fn default_kind_is_document() {
211 assert_eq!(BufferKind::default(), BufferKind::Document);
212 }
213
214 #[test]
215 fn oil_is_writable() {
216 assert!(!BufferKind::Oil.is_read_only());
217 }
218
219 #[test]
220 fn oil_label() {
221 assert_eq!(BufferKind::Oil.label(), "oil");
222 }
223}