Skip to main content

sequoia_buffer/
piece.rs

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("zażółć");
332
333        buf.insert(3, "X");
334        assert_eq!(buf.to_string(), "zaXżółć");
335        assert_eq!(buf.substring(3..4), "");
336        let removed = buf.delete(2..3);
337        assert_eq!(removed, "X");
338        assert_eq!(buf.to_string(), "zażółć");
339        buf.check_invariants();
340    }
341
342    #[test]
343    fn multiline_edit_keeps_line_index() {
344        let mut buf = TextBuffer::from("a\nb\nc");
345        buf.insert(2, "X\nY");
346        assert_eq!(buf.to_string(), "a\nX\nYb\nc");
347        assert_eq!(buf.lines().starts(), &[0, 2, 4, 7]);
348        buf.delete(2..5);
349        assert_eq!(buf.to_string(), "a\nb\nc");
350        assert_eq!(buf.lines().starts(), &[0, 2, 4]);
351        buf.check_invariants();
352    }
353
354    #[test]
355    fn replace_returns_removed() {
356        let mut buf = TextBuffer::from("foo bar");
357        let removed = buf.replace(4..7, "qux");
358        assert_eq!(removed, "bar");
359        assert_eq!(buf.to_string(), "foo qux");
360        buf.check_invariants();
361    }
362
363    #[test]
364    #[allow(clippy::reversed_empty_ranges)]
365    fn substring_clamps() {
366        let buf = TextBuffer::from("hello");
367        assert_eq!(buf.substring(1..4), "ell");
368        assert_eq!(buf.substring(3..99), "lo");
369        assert_eq!(buf.substring(99..100), "");
370        assert_eq!(buf.substring(4..1), "");
371    }
372
373    #[test]
374    fn adjacent_inserts_coalesce() {
375        let mut buf = TextBuffer::new();
376        buf.insert(0, "a");
377        buf.insert(1, "b");
378        buf.insert(2, "c");
379        assert_eq!(buf.to_string(), "abc");
380        assert_eq!(buf.piece_count(), 1);
381        buf.check_invariants();
382    }
383
384    #[test]
385    fn crlf_and_emoji_survive_edits() {
386        let mut buf = TextBuffer::from("a\r\nb");
387        buf.insert(1, "👨‍👩‍👧");
388
389        assert_eq!(buf.len_chars(), 4 + 5);
390        assert_eq!(buf.substring(0..1), "a");
391        buf.delete(0..1);
392        assert_eq!(buf.to_string(), "👨‍👩‍👧\r\nb");
393        buf.check_invariants();
394    }
395
396    #[test]
397    fn stress_script_matches_string() {
398        let mut buf = TextBuffer::from("The quick brown fox\njumps over\nthe lazy dog");
399        let mut reference = buf.to_string();
400        let ops: Vec<(usize, usize, &str)> = vec![
401            (4, 9, "slow"),
402            (0, 0, ">>> "),
403            (10, 10, "\nnew line\n"),
404            (0, 4, ""),
405            (20, 30, "🦀🦀"),
406        ];
407        for (start, end, ins) in ops {
408            let b = start.min(buf.len_bytes());
409            let e = end.min(buf.len_bytes()).max(b);
410            reference.replace_range(b..e, ins);
411            buf.replace(b..e, ins);
412            assert_eq!(buf.to_string(), reference);
413            buf.check_invariants();
414        }
415    }
416}