Skip to main content

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}