1use std::ops::Range;
2
3use crate::lines::LineIndex;
4
5#[derive(Clone, Copy, PartialEq, Eq, Debug)]
6enum Source {
7 Original,
8 Add,
9}
10
11#[derive(Clone, Copy, Debug)]
12struct Piece {
13 src: Source,
14 start: usize,
15 len: usize,
16}
17
18impl Piece {
19 fn end(&self) -> usize {
20 self.start + self.len
21 }
22}
23
24#[derive(Clone, Debug)]
25pub struct TextBuffer {
26 original: String,
27 add: String,
28 pieces: Vec<Piece>,
29 lines: LineIndex,
30 chars: usize,
31}
32
33impl Default for TextBuffer {
34 fn default() -> Self {
35 Self::new()
36 }
37}
38
39impl TextBuffer {
40 pub fn new() -> Self {
41 Self {
42 original: String::new(),
43 add: String::new(),
44 pieces: Vec::new(),
45 lines: LineIndex::from_str(""),
46 chars: 0,
47 }
48 }
49
50 pub fn from(text: impl Into<String>) -> Self {
51 let original = text.into();
52 let chars = original.chars().count();
53 let lines = LineIndex::from_str(&original);
54 let pieces = if original.is_empty() {
55 Vec::new()
56 } else {
57 vec![Piece {
58 src: Source::Original,
59 start: 0,
60 len: original.len(),
61 }]
62 };
63 Self {
64 original,
65 add: String::new(),
66 pieces,
67 lines,
68 chars,
69 }
70 }
71
72 pub fn len_bytes(&self) -> usize {
73 self.pieces.iter().map(|p| p.len).sum()
74 }
75
76 pub fn len_chars(&self) -> usize {
77 self.chars
78 }
79
80 pub fn is_empty(&self) -> bool {
81 self.pieces.is_empty()
82 }
83
84 pub fn lines(&self) -> &LineIndex {
85 &self.lines
86 }
87
88 pub fn to_string(&self) -> String {
89 let mut out = String::with_capacity(self.len_bytes());
90 for chunk in self.chunks() {
91 out.push_str(chunk);
92 }
93 out
94 }
95
96 pub fn chunks(&self) -> impl Iterator<Item = &str> {
97 let original = self.original.as_str();
98 let add = self.add.as_str();
99 self.pieces.iter().map(move |p| match p.src {
100 Source::Original => &original[p.start..p.end()],
101 Source::Add => &add[p.start..p.end()],
102 })
103 }
104
105 pub fn chunk_offsets(&self) -> Vec<(usize, &str)> {
106 let mut out = Vec::with_capacity(self.pieces.len());
107 let mut cursor = 0usize;
108 for chunk in self.chunks() {
109 out.push((cursor, chunk));
110 cursor += chunk.len();
111 }
112 out
113 }
114
115 pub fn chars(&self) -> impl Iterator<Item = char> + '_ {
116 self.chunks().flat_map(str::chars)
117 }
118
119 pub fn substring(&self, range: Range<usize>) -> String {
120 let len = self.len_bytes();
121 let mut start = range.start.min(len);
122 let mut end = range.end.min(len).max(start);
123 start = self.snap(start);
124 end = self.snap(end).max(start);
125 let mut out = String::new();
126 let mut cursor = 0usize;
127 for chunk in self.chunks() {
128 let chunk_end = cursor + chunk.len();
129 if chunk_end > start && cursor < end {
130 let from = start.saturating_sub(cursor);
131 let to = (end - cursor).min(chunk.len());
132 if from < to {
133 out.push_str(&chunk[from..to]);
134 }
135 }
136 cursor = chunk_end;
137 if cursor >= end {
138 break;
139 }
140 }
141 out
142 }
143
144 pub fn snap(&self, offset: usize) -> usize {
145 let offset = offset.min(self.len_bytes());
146 let mut cursor = 0usize;
147 for chunk in self.chunks() {
148 let chunk_end = cursor + chunk.len();
149 if offset < chunk_end {
150 let mut b = offset - cursor;
151 while b > 0 && !chunk.is_char_boundary(b) {
152 b -= 1;
153 }
154 return cursor + b;
155 }
156 cursor = chunk_end;
157 }
158 offset
159 }
160
161 pub fn insert(&mut self, offset: usize, text: &str) {
162 if text.is_empty() {
163 return;
164 }
165 let offset = self.snap(offset);
166 let at = self.split_at(offset);
167 let added_chars = text.chars().count();
168
169 if at > 0
170 && let Some(prev) = self.pieces.get_mut(at - 1)
171 && prev.src == Source::Add
172 && prev.end() == self.add.len()
173 {
174 self.add.push_str(text);
175 prev.len += text.len();
176 self.finish_edit(offset, 0, text, added_chars);
177 return;
178 }
179 let base = self.add.len();
180 self.add.push_str(text);
181 self.pieces.insert(
182 at,
183 Piece {
184 src: Source::Add,
185 start: base,
186 len: text.len(),
187 },
188 );
189 self.normalize(at);
190 self.finish_edit(offset, 0, text, added_chars);
191 }
192
193 pub fn delete(&mut self, range: Range<usize>) -> String {
194 let len = self.len_bytes();
195 let mut start = range.start.min(len);
196 let mut end = range.end.min(len).max(start);
197 start = self.snap(start);
198 end = self.snap(end).max(start);
199 if start == end {
200 return String::new();
201 }
202 let removed = self.substring(start..end);
203 let removed_chars = removed.chars().count();
204 let lo = self.split_at(start);
205 let hi = self.split_at(end);
206 self.pieces.drain(lo..hi);
207 self.normalize(lo.saturating_sub(1));
208 self.lines.apply_edit(start, end - start, "");
209 self.chars -= removed_chars;
210 removed
211 }
212
213 pub fn replace(&mut self, range: Range<usize>, text: &str) -> String {
214 let len = self.len_bytes();
215 let start = range.start.min(len);
216 let end = range.end.min(len).max(start);
217 if text.is_empty() {
218 return self.delete(start..end);
219 }
220 let removed = self.delete(start..end);
221 self.insert(start, text);
222 removed
223 }
224
225 fn split_at(&mut self, offset: usize) -> usize {
226 let mut cursor = 0usize;
227 for i in 0..self.pieces.len() {
228 let len = self.pieces[i].len;
229 let end = cursor + len;
230 if offset < end {
231 if offset > cursor {
232 let second = Piece {
233 src: self.pieces[i].src,
234 start: self.pieces[i].start + (offset - cursor),
235 len: end - offset,
236 };
237 self.pieces[i].len = offset - cursor;
238 self.pieces.insert(i + 1, second);
239 return i + 1;
240 }
241 return i;
242 }
243 cursor = end;
244 }
245 self.pieces.len()
246 }
247
248 fn normalize(&mut self, mut at: usize) {
249 at = at.min(self.pieces.len().saturating_sub(1));
250
251 while at > 0 {
252 let (prev, cur) = (self.pieces[at - 1], self.pieces[at]);
253 if prev.src == cur.src && prev.end() == cur.start {
254 self.pieces[at - 1].len += cur.len;
255 self.pieces.remove(at);
256 at -= 1;
257 } else {
258 break;
259 }
260 }
261
262 if at + 1 < self.pieces.len() {
263 let (cur, next) = (self.pieces[at], self.pieces[at + 1]);
264 if cur.src == next.src && cur.end() == next.start {
265 self.pieces[at].len += next.len;
266 self.pieces.remove(at + 1);
267 }
268 }
269 self.pieces.retain(|p| p.len > 0);
270 }
271
272 fn finish_edit(
273 &mut self,
274 start: usize,
275 deleted_len: usize,
276 inserted: &str,
277 added_chars: usize,
278 ) {
279 self.lines.apply_edit(start, deleted_len, inserted);
280 self.chars += added_chars;
281 }
282
283 #[cfg(test)]
284 fn piece_count(&self) -> usize {
285 self.pieces.len()
286 }
287
288 #[cfg(test)]
289 fn check_invariants(&self) {
290 let mut bytes = 0usize;
291 let mut chars = 0usize;
292 for chunk in self.chunks() {
293 bytes += chunk.len();
294 chars += chunk.chars().count();
295 }
296 assert_eq!(bytes, self.len_bytes());
297 assert_eq!(chars, self.chars);
298 assert_eq!(self.lines.len(), bytes);
299 let fresh = LineIndex::from_str(&self.to_string());
300 assert_eq!(&self.lines, &fresh);
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 #[test]
309 fn empty_roundtrip() {
310 let buf = TextBuffer::new();
311 assert!(buf.is_empty());
312 assert_eq!(buf.to_string(), "");
313 assert_eq!(buf.len_bytes(), 0);
314 assert_eq!(buf.len_chars(), 0);
315 assert_eq!(buf.lines().line_count(), 1);
316 }
317
318 #[test]
319 fn insert_delete_roundtrip() {
320 let mut buf = TextBuffer::from("hello world");
321 buf.insert(5, ",");
322 assert_eq!(buf.to_string(), "hello, world");
323 let removed = buf.delete(5..6);
324 assert_eq!(removed, ",");
325 assert_eq!(buf.to_string(), "hello world");
326 buf.check_invariants();
327 }
328
329 #[test]
330 fn offsets_snap_to_char_boundaries() {
331 let mut buf = TextBuffer::from("turn yellow");
332
333 buf.insert(3, "X");
334 assert_eq!(buf.to_string(), "zaXyellow");
335 let removed = buf.delete(2..3);
336 assert_eq!(removed, "X");
337 assert_eq!(buf.to_string(), "turn yellow");
338 buf.check_invariants();
339 }
340
341 #[test]
342 fn multiline_edit_keeps_line_index() {
343 let mut buf = TextBuffer::from("a\nb\nc");
344 buf.insert(2, "X\nY");
345 assert_eq!(buf.to_string(), "a\nX\nYb\nc");
346 assert_eq!(buf.lines().starts(), &[0, 2, 4, 7]);
347 buf.delete(2..5);
348 assert_eq!(buf.to_string(), "a\nb\nc");
349 assert_eq!(buf.lines().starts(), &[0, 2, 4]);
350 buf.check_invariants();
351 }
352
353 #[test]
354 fn replace_returns_removed() {
355 let mut buf = TextBuffer::from("foo bar");
356 let removed = buf.replace(4..7, "qux");
357 assert_eq!(removed, "bar");
358 assert_eq!(buf.to_string(), "foo qux");
359 buf.check_invariants();
360 }
361
362 #[test]
363 #[allow(clippy::reversed_empty_ranges)]
364 fn substring_clamps() {
365 let buf = TextBuffer::from("hello");
366 assert_eq!(buf.substring(1..4), "ell");
367 assert_eq!(buf.substring(3..99), "lo");
368 assert_eq!(buf.substring(99..100), "");
369 assert_eq!(buf.substring(4..1), "");
370 }
371
372 #[test]
373 fn adjacent_inserts_coalesce() {
374 let mut buf = TextBuffer::new();
375 buf.insert(0, "a");
376 buf.insert(1, "b");
377 buf.insert(2, "c");
378 assert_eq!(buf.to_string(), "abc");
379 assert_eq!(buf.piece_count(), 1);
380 buf.check_invariants();
381 }
382
383 #[test]
384 fn crlf_and_emoji_survive_edits() {
385 let mut buf = TextBuffer::from("a\r\nb");
386 buf.insert(1, "👨👩👧");
387
388 assert_eq!(buf.len_chars(), 4 + 5);
389 assert_eq!(buf.substring(0..1), "a");
390 buf.delete(0..1);
391 assert_eq!(buf.to_string(), "👨👩👧\r\nb");
392 buf.check_invariants();
393 }
394
395 #[test]
396 fn stress_script_matches_string() {
397 let mut buf = TextBuffer::from("The quick brown fox\njumps over\nthe lazy dog");
398 let mut reference = buf.to_string();
399 let ops: Vec<(usize, usize, &str)> = vec![
400 (4, 9, "slow"),
401 (0, 0, ">>> "),
402 (10, 10, "\nnew line\n"),
403 (0, 4, ""),
404 (20, 30, "🦀🦀"),
405 ];
406 for (start, end, ins) in ops {
407 let b = start.min(buf.len_bytes());
408 let e = end.min(buf.len_bytes()).max(b);
409 reference.replace_range(b..e, ins);
410 buf.replace(b..e, ins);
411 assert_eq!(buf.to_string(), reference);
412 buf.check_invariants();
413 }
414 }
415}