Skip to main content

hjkl_buffer/
content.rs

1//! Per-document text content. Arc-shareable across multiple [`crate::Buffer`]
2//! views.
3//!
4//! [`Content`] owns everything that belongs to the document itself:
5//!
6//! - The `text` rope (text content).
7//! - The `dirty_gen` render-cache generation counter.
8//! - Manual folds (`folds`).
9//!
10//! [`crate::Buffer`] is the per-window wrapper. It holds an
11//! `Arc<Mutex<Content>>` plus the per-window cursor. Two `Buffer`
12//! instances that share one `Content` see the same text and folds, but
13//! each moves its cursor independently.
14//!
15//! ## Concurrency
16//!
17//! Held inside `Arc<Mutex<Content>>` so multiple `Buffer` views can share
18//! one document safely. `Mutex` (not `RefCell`) because the engine's
19//! `Cursor`, `Query`, `BufferEdit`, and `Search` traits require `Send`,
20//! and `RefCell` is `!Send`. Lock contention is near-zero in the
21//! single-threaded app loop; the Mutex is essentially a free `Send`
22//! adapter.
23
24use crate::folds::Fold;
25
26/// Per-document state shared across all [`crate::Buffer`] views of the
27/// same file. Wrap in `Arc<Mutex<Content>>` and pass to
28/// [`crate::Buffer::new_view`] to create an additional window onto the
29/// same content.
30///
31/// Uses a `ropey::Rope` for O(log N) edits and O(1) byte-length queries.
32/// The rope always contains at least one logical line: a freshly constructed
33/// `Content` holds an empty rope (which `ropey` reports as 1 line) so
34/// cursor positions never need an "is the buffer empty?" branch.
35///
36/// ## Line semantics
37///
38/// `ropey::Rope::len_lines()` and `split('\n').count()` agree for all inputs:
39/// - `""` → 1 line
40/// - `"foo\n"` → 2 lines (trailing empty line)
41/// - `"a\nb\n"` → 3 lines
42///
43/// `Rope::line(i)` returns a `RopeSlice` that includes the trailing `\n`
44/// for non-final lines. Public accessors strip it before returning `String`.
45pub struct Content {
46    /// Rope-backed document text. Always non-empty: `ropey::Rope::new()`
47    /// (an empty rope) reports `len_lines() == 1`, satisfying the "at least
48    /// one row" invariant without a separate sentinel.
49    pub(crate) text: ropey::Rope,
50    /// Bumps on every mutation; render cache keys against this so a
51    /// per-row `Line` gets recomputed when its source row changes.
52    pub(crate) dirty_gen: u64,
53    /// Manual folds — closed ranges hide rows in the render path.
54    /// `pub(crate)` so the [`crate::folds`] module can read/write
55    /// directly (same visibility as before the split).
56    pub(crate) folds: Vec<Fold>,
57    /// Cached `rope.to_string()` keyed by the `dirty_gen` at build time.
58    /// Multiple per-tick consumers (syntax submit, LSP notify, git
59    /// signature, dirty hash) all need the joined document; rebuilding
60    /// per consumer was ~4× the line-clone + alloc cost per keystroke
61    /// on a 400-line file (visible as insert-mode lag).
62    pub(crate) cached_joined: Option<(u64, std::sync::Arc<String>)>,
63    /// Cached canonical byte length keyed by `dirty_gen` at compute time.
64    /// `Rope::len_bytes()` is O(1) but holding the cache avoids even that
65    /// small overhead on repeated callers within the same tick.
66    pub(crate) cached_byte_len: Option<(u64, usize)>,
67}
68
69impl Default for Content {
70    fn default() -> Self {
71        Self::new()
72    }
73}
74
75impl Content {
76    /// New empty content with one empty row.
77    pub fn new() -> Self {
78        Self {
79            text: ropey::Rope::new(),
80            dirty_gen: 0,
81            folds: Vec::new(),
82            cached_joined: None,
83            cached_byte_len: None,
84        }
85    }
86
87    /// Build content from a flat string. Splits on `\n`; a trailing
88    /// `\n` produces a trailing empty line (matches ropey's own convention).
89    #[allow(clippy::should_implement_trait)]
90    pub fn from_str(text: &str) -> Self {
91        Self {
92            text: ropey::Rope::from_str(text),
93            dirty_gen: 0,
94            folds: Vec::new(),
95            cached_joined: None,
96            cached_byte_len: None,
97        }
98    }
99}