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}