Skip to main content

fission_text_engine/
buffer.rs

1//! Rope-backed text buffer with revision tracking.
2
3use ropey::Rope;
4use std::fmt;
5use std::ops::Range;
6
7/// A text buffer backed by a [`ropey::Rope`].
8///
9/// Every mutating operation increments an internal **revision** counter so that
10/// downstream systems (layout caches, syntax highlights, diagnostics, etc.) can
11/// cheaply detect stale data.
12#[derive(Clone)]
13pub struct TextBuffer {
14    rope: Rope,
15    revision: u64,
16}
17
18// ── Constructors ────────────────────────────────────────────────────────────
19
20impl TextBuffer {
21    /// Create an empty buffer.
22    pub fn new() -> Self {
23        Self {
24            rope: Rope::new(),
25            revision: 0,
26        }
27    }
28
29    /// Create a buffer pre-populated with `text`.
30    pub fn from_str(text: &str) -> Self {
31        Self {
32            rope: Rope::from_str(text),
33            revision: 0,
34        }
35    }
36}
37
38impl Default for TextBuffer {
39    fn default() -> Self {
40        Self::new()
41    }
42}
43
44// ── Read-only queries ───────────────────────────────────────────────────────
45
46impl TextBuffer {
47    /// Return a reference to the underlying rope.
48    pub fn text(&self) -> &Rope {
49        &self.rope
50    }
51
52    /// Total length in bytes (UTF-8).
53    pub fn len_bytes(&self) -> usize {
54        self.rope.len_bytes()
55    }
56
57    /// Total length in Unicode characters (grapheme-unaware; counts `char`s).
58    pub fn len_chars(&self) -> usize {
59        self.rope.len_chars()
60    }
61
62    /// Number of lines.  A trailing `\n` implies an additional empty final
63    /// line, matching the convention used by most editors.
64    pub fn len_lines(&self) -> usize {
65        self.rope.len_lines()
66    }
67
68    /// Return the contents of `line_idx` (0-based) as a `ropey::RopeSlice`,
69    /// including the line terminator if present.
70    ///
71    /// # Panics
72    ///
73    /// Panics if `line_idx >= self.len_lines()`.
74    pub fn line(&self, line_idx: usize) -> ropey::RopeSlice<'_> {
75        self.rope.line(line_idx)
76    }
77
78    /// Return an arbitrary byte-offset range as a `RopeSlice`.
79    ///
80    /// Both bounds are byte offsets and must lie on `char` boundaries.
81    ///
82    /// # Panics
83    ///
84    /// Panics if the range is out of bounds or not on char boundaries.
85    pub fn slice(&self, byte_range: Range<usize>) -> ropey::RopeSlice<'_> {
86        let start_char = self.rope.byte_to_char(byte_range.start);
87        let end_char = self.rope.byte_to_char(byte_range.end);
88        self.rope.slice(start_char..end_char)
89    }
90
91    /// Monotonically increasing revision counter.  Incremented on every
92    /// mutation (`insert`, `delete`, `replace`).
93    pub fn revision(&self) -> u64 {
94        self.revision
95    }
96
97    /// `true` when the buffer contains no characters.
98    pub fn is_empty(&self) -> bool {
99        self.len_chars() == 0
100    }
101}
102
103// ── Mutations ───────────────────────────────────────────────────────────────
104
105impl TextBuffer {
106    /// Insert `text` at the given **byte offset**.
107    ///
108    /// # Panics
109    ///
110    /// Panics if `byte_offset` is out of bounds or not on a char boundary.
111    pub fn insert(&mut self, byte_offset: usize, text: &str) {
112        let char_idx = self.rope.byte_to_char(byte_offset);
113        self.rope.insert(char_idx, text);
114        self.revision += 1;
115    }
116
117    /// Delete the byte range `start..end`.
118    ///
119    /// # Panics
120    ///
121    /// Panics if the range is out of bounds or not on char boundaries.
122    pub fn delete(&mut self, byte_range: Range<usize>) {
123        let start_char = self.rope.byte_to_char(byte_range.start);
124        let end_char = self.rope.byte_to_char(byte_range.end);
125        self.rope.remove(start_char..end_char);
126        self.revision += 1;
127    }
128
129    /// Replace the byte range `start..end` with `text`.
130    ///
131    /// Equivalent to a `delete` followed by an `insert` but only bumps the
132    /// revision once.
133    pub fn replace(&mut self, byte_range: Range<usize>, text: &str) {
134        let start_char = self.rope.byte_to_char(byte_range.start);
135        let end_char = self.rope.byte_to_char(byte_range.end);
136        self.rope.remove(start_char..end_char);
137        self.rope.insert(start_char, text);
138        self.revision += 1;
139    }
140}
141
142// ── Display / Debug ─────────────────────────────────────────────────────────
143
144impl fmt::Display for TextBuffer {
145    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146        for chunk in self.rope.chunks() {
147            f.write_str(chunk)?;
148        }
149        Ok(())
150    }
151}
152
153impl fmt::Debug for TextBuffer {
154    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155        f.debug_struct("TextBuffer")
156            .field("len_bytes", &self.len_bytes())
157            .field("len_chars", &self.len_chars())
158            .field("len_lines", &self.len_lines())
159            .field("revision", &self.revision)
160            .finish()
161    }
162}