Skip to main content

escriba_buffer/
buffer.rs

1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3
4use escriba_core::{BufferId, Edit, EditKind, Position, Range};
5use ropey::Rope;
6use serde::{Deserialize, Serialize};
7
8use crate::encoding::Encoding;
9use crate::error::BufferError;
10use crate::line_ending::LineEnding;
11use crate::undo::{UndoEntry, UndoTree};
12
13/// A single open text buffer.
14/// A monotonic counter of TEXT changes to one buffer.
15///
16/// Deliberately distinct from `escriba-core`'s `EditGen`, which is a REFRESH
17/// generation: that one bumps on every action, including pure cursor moves,
18/// because its job is telling the renderer when to repaint. Keying staleness
19/// on it would mark every cached offset stale after any keypress — noise, not
20/// staleness.
21///
22/// This one bumps if and only if the rope changed, which is the question an
23/// offset actually needs answered: "is the text I was measured against still
24/// the text?"
25#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
26pub struct TextRev(pub u64);
27
28impl TextRev {
29    #[must_use]
30    pub const fn next(self) -> Self {
31        Self(self.0.wrapping_add(1))
32    }
33}
34
35#[derive(Debug, Clone)]
36pub struct Buffer {
37    pub id: BufferId,
38    pub path: Option<PathBuf>,
39    pub rope: Rope,
40    pub modified: bool,
41    pub encoding: Encoding,
42    pub line_ending: LineEnding,
43    pub undo: UndoTree,
44    /// Bumped by every successful [`Buffer::apply`]. Private so it can only
45    /// advance through a real mutation — a settable revision is a revision
46    /// that lies.
47    text_rev: TextRev,
48}
49
50impl Buffer {
51    #[must_use]
52    pub fn empty(id: BufferId) -> Self {
53        Self {
54            id,
55            path: None,
56            rope: Rope::new(),
57            modified: false,
58            encoding: Encoding::default(),
59            line_ending: LineEnding::default(),
60            undo: UndoTree::new(),
61            text_rev: TextRev::default(),
62        }
63    }
64
65    /// This buffer's current text revision — the token an offset measured
66    /// against it should carry.
67    #[must_use]
68    pub const fn text_rev(&self) -> TextRev {
69        self.text_rev
70    }
71
72    #[must_use]
73    pub fn from_str(id: BufferId, src: &str) -> Self {
74        let line_ending = LineEnding::detect(src);
75        Self {
76            id,
77            path: None,
78            rope: Rope::from_str(src),
79            modified: false,
80            encoding: Encoding::default(),
81            line_ending,
82            undo: UndoTree::new(),
83            text_rev: TextRev::default(),
84        }
85    }
86
87    pub fn open(id: BufferId, path: impl AsRef<Path>) -> Result<Self, BufferError> {
88        let path = path.as_ref().to_path_buf();
89        let src = if path.exists() {
90            std::fs::read_to_string(&path)?
91        } else {
92            String::new()
93        };
94        let line_ending = LineEnding::detect(&src);
95        Ok(Self {
96            id,
97            path: Some(path),
98            rope: Rope::from_str(&src),
99            modified: false,
100            encoding: Encoding::default(),
101            line_ending,
102            undo: UndoTree::new(),
103            text_rev: TextRev::default(),
104        })
105    }
106
107    pub fn save(&mut self) -> Result<(), BufferError> {
108        let path = self.path.clone().ok_or(BufferError::NoPath)?;
109        let mut src = String::new();
110        for line in self.rope.lines() {
111            src.push_str(&line.to_string());
112        }
113        std::fs::write(&path, src)?;
114        self.modified = false;
115        Ok(())
116    }
117
118    pub fn save_as(&mut self, path: impl AsRef<Path>) -> Result<(), BufferError> {
119        self.path = Some(path.as_ref().to_path_buf());
120        self.save()
121    }
122
123    // ── Queries ────────────────────────────────────────────────────
124
125    #[must_use]
126    pub fn line_count(&self) -> u32 {
127        u32::try_from(self.rope.len_lines()).unwrap_or(u32::MAX)
128    }
129
130    #[must_use]
131    pub fn byte_count(&self) -> usize {
132        self.rope.len_bytes()
133    }
134
135    #[must_use]
136    pub fn char_count(&self) -> usize {
137        self.rope.len_chars()
138    }
139
140    pub fn line(&self, n: u32) -> Option<String> {
141        let n = n as usize;
142        if n >= self.rope.len_lines() {
143            return None;
144        }
145        Some(self.rope.line(n).to_string())
146    }
147
148    pub fn line_len_chars(&self, n: u32) -> u32 {
149        let n = n as usize;
150        if n >= self.rope.len_lines() {
151            return 0;
152        }
153        let line = self.rope.line(n);
154        let mut len = line.len_chars();
155        // Strip trailing newline from char count for cursor-placement purposes.
156        if line.chars().last().is_some_and(|c| c == '\n' || c == '\r') {
157            len = len.saturating_sub(1);
158        }
159        u32::try_from(len).unwrap_or(u32::MAX)
160    }
161
162    /// Clamp a position to valid coordinates inside this buffer.
163    #[must_use]
164    pub fn clamp(&self, pos: Position) -> Position {
165        let line = pos.line.min(self.line_count().saturating_sub(1));
166        let col = pos.column.min(self.line_len_chars(line));
167        Position::new(line, col)
168    }
169
170    pub fn position_to_char(&self, pos: Position) -> Result<usize, BufferError> {
171        let line = pos.line as usize;
172        if line >= self.rope.len_lines() {
173            return Err(BufferError::InvalidPosition {
174                line: pos.line,
175                column: pos.column,
176                total_lines: self.line_count(),
177            });
178        }
179        let line_start = self.rope.line_to_char(line);
180        let line_slice = self.rope.line(line);
181        let max_col = line_slice.len_chars();
182        let col = (pos.column as usize).min(max_col);
183        Ok(line_start + col)
184    }
185
186    #[must_use]
187    pub fn char_to_position(&self, ch: usize) -> Position {
188        let line = self.rope.char_to_line(ch.min(self.rope.len_chars()));
189        let line_start = self.rope.line_to_char(line);
190        let col = ch.saturating_sub(line_start);
191        Position::new(
192            u32::try_from(line).unwrap_or(u32::MAX),
193            u32::try_from(col).unwrap_or(u32::MAX),
194        )
195    }
196
197    pub fn slice(&self, range: Range) -> Result<String, BufferError> {
198        let r = range.normalized();
199        let a = self.position_to_char(r.start)?;
200        let b = self.position_to_char(r.end)?;
201        Ok(self.rope.slice(a..b).to_string())
202    }
203
204    // ── Mutation ───────────────────────────────────────────────────
205
206    pub fn apply(&mut self, edit: &Edit) -> Result<UndoEntry, BufferError> {
207        let range = edit.range.normalized();
208        let start_char = self.position_to_char(range.start)?;
209        let end_char = self.position_to_char(range.end)?;
210        let previous_text = self.rope.slice(start_char..end_char).to_string();
211
212        // The rope is about to change, so every offset measured against the
213        // old text expires here. Bumping AFTER the fallible prelude means a
214        // rejected edit does not invalidate anything.
215        self.text_rev = self.text_rev.next();
216
217        let (inserted_len_chars, reverse_kind) = match &edit.kind {
218            EditKind::Insert { text } => {
219                self.rope.insert(start_char, text);
220                (text.chars().count(), EditKind::Delete)
221            }
222            EditKind::Delete => {
223                self.rope.remove(start_char..end_char);
224                (
225                    0usize,
226                    EditKind::Insert {
227                        text: previous_text.clone(),
228                    },
229                )
230            }
231            EditKind::Replace { text } => {
232                self.rope.remove(start_char..end_char);
233                self.rope.insert(start_char, text);
234                (
235                    text.chars().count(),
236                    EditKind::Replace {
237                        text: previous_text.clone(),
238                    },
239                )
240            }
241        };
242        self.modified = true;
243
244        // Compute the reverse edit's range — where the insertion now sits.
245        let reverse_end_char = start_char + inserted_len_chars;
246        let reverse_range = Range::new(
247            self.char_to_position(start_char),
248            self.char_to_position(reverse_end_char),
249        );
250        let reverse_edit = Edit {
251            range: reverse_range,
252            kind: reverse_kind,
253        };
254        let entry = UndoEntry {
255            applied: edit.clone(),
256            reverse: reverse_edit,
257        };
258        self.undo.push(entry.clone());
259        Ok(entry)
260    }
261
262    pub fn undo(&mut self) -> Result<UndoEntry, BufferError> {
263        let entry = self.undo.pop_undo().ok_or(BufferError::NothingToUndo)?;
264        // Apply reverse edit without pushing a new undo entry.
265        let r = entry.reverse.range.normalized();
266        let a = self.position_to_char(r.start)?;
267        let b = self.position_to_char(r.end)?;
268        match &entry.reverse.kind {
269            EditKind::Insert { text } => {
270                self.rope.insert(a, text);
271            }
272            EditKind::Delete => {
273                self.rope.remove(a..b);
274            }
275            EditKind::Replace { text } => {
276                self.rope.remove(a..b);
277                self.rope.insert(a, text);
278            }
279        }
280        self.modified = true;
281        Ok(entry)
282    }
283
284    pub fn redo(&mut self) -> Result<UndoEntry, BufferError> {
285        let entry = self.undo.pop_redo().ok_or(BufferError::NothingToRedo)?;
286        let r = entry.applied.range.normalized();
287        let a = self.position_to_char(r.start)?;
288        let b = self.position_to_char(r.end)?;
289        match &entry.applied.kind {
290            EditKind::Insert { text } => {
291                self.rope.insert(a, text);
292            }
293            EditKind::Delete => {
294                self.rope.remove(a..b);
295            }
296            EditKind::Replace { text } => {
297                self.rope.remove(a..b);
298                self.rope.insert(a, text);
299            }
300        }
301        self.modified = true;
302        Ok(entry)
303    }
304
305    #[must_use]
306    pub fn to_string(&self) -> String {
307        self.rope.to_string()
308    }
309}
310
311/// A registry of open buffers keyed by id — phase 1 single-threaded view.
312#[derive(Debug, Default, Clone)]
313pub struct BufferSet {
314    buffers: HashMap<BufferId, Buffer>,
315    next_id: u64,
316}
317
318impl BufferSet {
319    #[must_use]
320    pub fn new() -> Self {
321        Self::default()
322    }
323
324    pub fn next_id(&mut self) -> BufferId {
325        self.next_id += 1;
326        BufferId(self.next_id)
327    }
328
329    pub fn open(&mut self, path: impl AsRef<Path>) -> Result<BufferId, BufferError> {
330        let id = self.next_id();
331        let buf = Buffer::open(id, path)?;
332        self.buffers.insert(id, buf);
333        Ok(id)
334    }
335
336    pub fn scratch(&mut self, src: &str) -> BufferId {
337        let id = self.next_id();
338        self.buffers.insert(id, Buffer::from_str(id, src));
339        id
340    }
341
342    #[must_use]
343    pub fn get(&self, id: BufferId) -> Option<&Buffer> {
344        self.buffers.get(&id)
345    }
346
347    pub fn get_mut(&mut self, id: BufferId) -> Option<&mut Buffer> {
348        self.buffers.get_mut(&id)
349    }
350
351    #[must_use]
352    pub fn ids(&self) -> Vec<BufferId> {
353        let mut v: Vec<_> = self.buffers.keys().copied().collect();
354        v.sort();
355        v
356    }
357}
358
359#[derive(Debug, Clone, Serialize, Deserialize)]
360pub struct BufferSummary {
361    pub id: BufferId,
362    pub path: Option<PathBuf>,
363    pub line_count: u32,
364    pub modified: bool,
365}
366
367#[cfg(test)]
368mod tests {
369    use super::*;
370    use escriba_core::{Edit, Position, Range};
371
372    fn buf(src: &str) -> Buffer {
373        Buffer::from_str(BufferId(1), src)
374    }
375
376    #[test]
377    fn empty_has_one_line() {
378        let b = Buffer::empty(BufferId(1));
379        assert_eq!(b.line_count(), 1);
380    }
381
382    #[test]
383    fn line_counts() {
384        let b = buf("a\nb\nc\n");
385        assert_eq!(b.line_count(), 4); // trailing \n leaves a final empty line
386    }
387
388    #[test]
389    fn position_char_round_trip() {
390        let b = buf("hello\nworld\n");
391        let p = Position::new(1, 3);
392        let c = b.position_to_char(p).unwrap();
393        assert_eq!(b.char_to_position(c), p);
394    }
395
396    #[test]
397    fn insert_then_undo() {
398        let mut b = buf("hello");
399        let e = Edit::insert(Position::new(0, 5), " world");
400        b.apply(&e).unwrap();
401        assert_eq!(b.to_string(), "hello world");
402        b.undo().unwrap();
403        assert_eq!(b.to_string(), "hello");
404    }
405
406    #[test]
407    fn delete_then_redo() {
408        let mut b = buf("hello world");
409        let e = Edit::delete(Range::new(Position::new(0, 5), Position::new(0, 11)));
410        b.apply(&e).unwrap();
411        assert_eq!(b.to_string(), "hello");
412        b.undo().unwrap();
413        assert_eq!(b.to_string(), "hello world");
414        b.redo().unwrap();
415        assert_eq!(b.to_string(), "hello");
416    }
417
418    #[test]
419    fn replace_is_delete_plus_insert() {
420        let mut b = buf("hello world");
421        let e = Edit::replace(
422            Range::new(Position::new(0, 6), Position::new(0, 11)),
423            "tatara",
424        );
425        b.apply(&e).unwrap();
426        assert_eq!(b.to_string(), "hello tatara");
427        b.undo().unwrap();
428        assert_eq!(b.to_string(), "hello world");
429    }
430
431    #[test]
432    fn clamp_constrains_position() {
433        let b = buf("ab\ncd");
434        assert_eq!(b.line_count(), 2);
435        assert_eq!(b.clamp(Position::new(0, 99)), Position::new(0, 2));
436        assert_eq!(b.clamp(Position::new(99, 0)), Position::new(1, 0));
437    }
438
439    #[test]
440    fn slice_returns_text() {
441        let b = buf("hello world");
442        let s = b
443            .slice(Range::new(Position::new(0, 6), Position::new(0, 11)))
444            .unwrap();
445        assert_eq!(s, "world");
446    }
447
448    #[test]
449    fn save_round_trip() {
450        let dir = tempfile::tempdir().unwrap();
451        let path = dir.path().join("demo.txt");
452        let mut b = Buffer::from_str(BufferId(1), "hello\n");
453        b.save_as(&path).unwrap();
454        let b2 = Buffer::open(BufferId(2), &path).unwrap();
455        assert_eq!(b2.to_string(), "hello\n");
456    }
457
458    #[test]
459    fn buffer_set_tracks_ids() {
460        let mut set = BufferSet::new();
461        let a = set.scratch("one");
462        let b = set.scratch("two");
463        assert_ne!(a, b);
464        assert_eq!(set.ids().len(), 2);
465        assert_eq!(set.get(a).unwrap().to_string(), "one");
466    }
467}
468
469#[cfg(test)]
470mod text_rev_tests {
471    use super::*;
472    use escriba_core::{Edit, Position, Range};
473
474    fn buf(src: &str) -> Buffer {
475        Buffer::from_str(BufferId(0), src)
476    }
477
478    #[test]
479    fn a_fresh_buffer_starts_at_revision_zero() {
480        assert_eq!(buf("hello").text_rev(), TextRev(0));
481    }
482
483    #[test]
484    fn an_applied_edit_advances_the_revision() {
485        let mut b = buf("hello");
486        let before = b.text_rev();
487        b.apply(&Edit::insert(Position::new(0, 0), "X".to_string()))
488            .expect("insert applies");
489        assert_ne!(
490            b.text_rev(),
491            before,
492            "a text change must expire old offsets"
493        );
494    }
495
496    #[test]
497    fn each_edit_advances_it_again() {
498        let mut b = buf("hello");
499        let mut seen = vec![b.text_rev()];
500        for _ in 0..3 {
501            b.apply(&Edit::insert(Position::new(0, 0), "X".to_string()))
502                .expect("insert applies");
503            let now = b.text_rev();
504            assert!(!seen.contains(&now), "revisions must not repeat: {now:?}");
505            seen.push(now);
506        }
507    }
508
509    #[test]
510    fn a_rejected_edit_does_not_advance_the_revision() {
511        // The property that makes this usable for staleness: an edit that never
512        // touched the rope must not invalidate offsets that are still correct.
513        let mut b = buf("hello");
514        let before = b.text_rev();
515        let out_of_range = Range {
516            start: Position::new(99, 0),
517            end: Position::new(99, 1),
518        };
519        assert!(
520            b.apply(&Edit::delete(out_of_range)).is_err(),
521            "the edit must fail"
522        );
523        assert_eq!(b.text_rev(), before, "a failed edit changed no text");
524    }
525
526    #[test]
527    fn reading_the_buffer_does_not_advance_the_revision() {
528        // Contrast with `EditGen`, which bumps on every action. If merely
529        // looking moved this counter it would be a refresh generation again.
530        let b = buf("hello");
531        let before = b.text_rev();
532        let _ = b.to_string();
533        let _ = b.text_rev();
534        assert_eq!(b.text_rev(), before);
535    }
536}