binocular/preview/rich_text/
buffer.rs1use std::ops::Range;
2
3#[derive(Debug, Clone, PartialEq, Eq)]
4pub struct TextBuffer {
5 content: String,
6 line_ranges: Vec<(usize, usize)>,
7}
8
9impl TextBuffer {
10 pub fn new(content: String) -> Self {
11 let line_ranges = build_line_ranges(&content);
12 Self {
13 content,
14 line_ranges,
15 }
16 }
17
18 pub fn as_str(&self) -> &str {
19 &self.content
20 }
21
22 pub fn len_bytes(&self) -> usize {
23 self.content.len()
24 }
25
26 pub fn line_ranges(&self) -> &[(usize, usize)] {
27 &self.line_ranges
28 }
29
30 pub fn line_range(&self, line_idx: usize) -> Option<(usize, usize)> {
31 self.line_ranges.get(line_idx).copied()
32 }
33
34 pub fn line_count(&self) -> usize {
35 self.line_ranges.len()
36 }
37
38 pub fn line_slice(&self, line_idx: usize) -> Option<&str> {
39 let (start, end) = self.line_range(line_idx)?;
40 Some(&self.content[start..end])
41 }
42
43 pub fn byte_index(&self, line: usize, char_idx: usize) -> usize {
44 let Some((start, end)) = self.line_range(line) else {
45 return self.content.len();
46 };
47
48 let line_text = &self.content[start..end];
49 let mut byte_offset = 0;
50 let mut current_char = 0;
51
52 for (idx, c) in line_text.char_indices() {
53 if current_char == char_idx {
54 return start + idx;
55 }
56 current_char += 1;
57 byte_offset = idx + c.len_utf8();
58 }
59
60 if current_char == char_idx {
61 start + byte_offset
62 } else {
63 end
64 }
65 }
66
67 pub fn line_char_from_byte(&self, byte_idx: usize) -> (usize, usize) {
68 let line_idx = self
69 .line_ranges
70 .iter()
71 .position(|(start, end)| byte_idx >= *start && byte_idx <= *end)
72 .unwrap_or_else(|| self.line_ranges.len().saturating_sub(1));
73
74 let Some((start, end)) = self.line_range(line_idx) else {
75 return (0, 0);
76 };
77 let clamped = byte_idx.clamp(start, end);
78 let char_idx = self.content[start..clamped].chars().count();
79 (line_idx, char_idx)
80 }
81
82 pub fn line_len_chars(&self, line_idx: usize) -> usize {
83 self.line_slice(line_idx)
84 .map(|line| {
85 line.trim_end_matches('\n')
86 .trim_end_matches('\r')
87 .chars()
88 .count()
89 })
90 .unwrap_or(0)
91 }
92
93 pub fn char_at_byte(&self, byte_idx: usize) -> Option<char> {
94 self.content.get(byte_idx..)?.chars().next()
95 }
96
97 pub fn char_before_byte(&self, byte_idx: usize) -> Option<(usize, char)> {
98 self.content.get(..byte_idx)?.char_indices().last()
99 }
100
101 pub fn apply_edit(&mut self, edit: &TextEdit) -> bool {
102 match edit.kind {
103 TextEditKind::Insert => self.insert(edit.byte_idx, &edit.text),
104 TextEditKind::Delete => {
105 let end = edit.byte_idx + edit.text.len();
106 if self.content.get(edit.byte_idx..end) != Some(edit.text.as_str()) {
107 return false;
108 }
109 self.delete_range(edit.byte_idx..end).is_some()
110 }
111 }
112 }
113
114 pub fn insert_char(&mut self, byte_idx: usize, c: char) -> Option<TextEdit> {
115 let mut text = String::new();
116 text.push(c);
117 self.insert_text(byte_idx, text)
118 }
119
120 pub fn insert_text(&mut self, byte_idx: usize, text: String) -> Option<TextEdit> {
121 if !self.insert(byte_idx, &text) {
122 return None;
123 }
124 Some(TextEdit::insert(byte_idx, text))
125 }
126
127 pub fn delete_char_before(&mut self, byte_idx: usize) -> Option<TextEdit> {
128 let (start, c) = self.char_before_byte(byte_idx)?;
129 let end = start + c.len_utf8();
130 let deleted = self.delete_range(start..end)?;
131 Some(TextEdit::delete(start, deleted))
132 }
133
134 pub fn delete_char_at(&mut self, byte_idx: usize) -> Option<TextEdit> {
135 let c = self.char_at_byte(byte_idx)?;
136 let end = byte_idx + c.len_utf8();
137 let deleted = self.delete_range(byte_idx..end)?;
138 Some(TextEdit::delete(byte_idx, deleted))
139 }
140
141 pub fn delete_range(&mut self, range: Range<usize>) -> Option<String> {
142 if range.start > range.end || range.end > self.content.len() {
143 return None;
144 }
145 let removed = self.content.get(range.clone())?.to_string();
146 self.content.replace_range(range, "");
147 self.rebuild_line_ranges();
148 Some(removed)
149 }
150
151 fn insert(&mut self, byte_idx: usize, text: &str) -> bool {
152 if byte_idx > self.content.len() || !self.content.is_char_boundary(byte_idx) {
153 return false;
154 }
155 self.content.insert_str(byte_idx, text);
156 self.rebuild_line_ranges();
157 true
158 }
159
160 fn rebuild_line_ranges(&mut self) {
161 self.line_ranges = build_line_ranges(&self.content);
162 }
163}
164
165fn build_line_ranges(content: &str) -> Vec<(usize, usize)> {
166 let mut ranges = Vec::new();
167 let mut start = 0;
168
169 for (idx, byte) in content.as_bytes().iter().enumerate() {
170 if *byte == b'\n' {
171 ranges.push((start, idx + 1));
172 start = idx + 1;
173 }
174 }
175
176 if start < content.len() {
177 ranges.push((start, content.len()));
178 } else if start == content.len() && start > 0 {
179 ranges.push((start, start));
180 }
181
182 if ranges.is_empty() {
183 ranges.push((0, 0));
184 }
185
186 ranges
187}
188
189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
190pub enum TextEditKind {
191 Insert,
192 Delete,
193}
194
195#[derive(Debug, Clone, PartialEq, Eq)]
196pub struct TextEdit {
197 pub kind: TextEditKind,
198 pub byte_idx: usize,
199 pub text: String,
200}
201
202impl TextEdit {
203 pub fn insert(byte_idx: usize, text: String) -> Self {
204 Self {
205 kind: TextEditKind::Insert,
206 byte_idx,
207 text,
208 }
209 }
210
211 pub fn delete(byte_idx: usize, text: String) -> Self {
212 Self {
213 kind: TextEditKind::Delete,
214 byte_idx,
215 text,
216 }
217 }
218}
219
220#[derive(Debug, Clone, PartialEq, Eq)]
221pub struct TextUndoFrame {
222 pub undo_edit: TextEdit,
223 pub redo_edit: TextEdit,
224 pub before_cursor: (usize, usize),
225 pub after_cursor: (usize, usize),
226}
227
228impl TextUndoFrame {
229 pub fn from_forward_edit(
230 edit: TextEdit,
231 before_cursor: (usize, usize),
232 after_cursor: (usize, usize),
233 ) -> Self {
234 let inverse = match edit.kind {
235 TextEditKind::Insert => TextEdit::delete(edit.byte_idx, edit.text.clone()),
236 TextEditKind::Delete => TextEdit::insert(edit.byte_idx, edit.text.clone()),
237 };
238 Self {
239 undo_edit: inverse,
240 redo_edit: edit,
241 before_cursor,
242 after_cursor,
243 }
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 #[test]
252 fn insert_and_delete_update_line_ranges() {
253 let mut buffer = TextBuffer::new("abc\ndef".to_string());
254 buffer.insert_char(3, '\n');
255 assert_eq!(buffer.line_count(), 3);
256 assert_eq!(buffer.line_slice(1), Some("\n"));
257
258 let deleted = buffer.delete_char_before(4).unwrap();
259 assert_eq!(deleted.text, "\n");
260 assert_eq!(buffer.as_str(), "abc\ndef");
261 assert_eq!(buffer.line_count(), 2);
262 }
263
264 #[test]
265 fn delete_range_returns_removed_text() {
266 let mut buffer = TextBuffer::new("hello world".to_string());
267 let removed = buffer.delete_range(5..11).unwrap();
268 assert_eq!(removed, " world");
269 assert_eq!(buffer.as_str(), "hello");
270 }
271
272 #[test]
273 fn byte_index_uses_line_context() {
274 let buffer = TextBuffer::new("ab\ncd".to_string());
275 assert_eq!(buffer.byte_index(1, 1), 4);
276 assert_eq!(buffer.line_char_from_byte(4), (1, 1));
277 }
278
279 #[test]
280 fn delete_range_handles_multi_line_edits() {
281 let mut buffer = TextBuffer::new("one\ntwo\nthree".to_string());
282 let start = buffer.byte_index(0, 2);
283 let end = buffer.byte_index(1, 2);
284 let removed = buffer.delete_range(start..end).unwrap();
285
286 assert_eq!(removed, "e\ntw");
287 assert_eq!(buffer.as_str(), "ono\nthree");
288 assert_eq!(buffer.line_count(), 2);
289 }
290}