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#[derive(Debug, Clone)]
15pub struct Buffer {
16    pub id: BufferId,
17    pub path: Option<PathBuf>,
18    pub rope: Rope,
19    pub modified: bool,
20    pub encoding: Encoding,
21    pub line_ending: LineEnding,
22    pub undo: UndoTree,
23}
24
25impl Buffer {
26    #[must_use]
27    pub fn empty(id: BufferId) -> Self {
28        Self {
29            id,
30            path: None,
31            rope: Rope::new(),
32            modified: false,
33            encoding: Encoding::default(),
34            line_ending: LineEnding::default(),
35            undo: UndoTree::new(),
36        }
37    }
38
39    #[must_use]
40    pub fn from_str(id: BufferId, src: &str) -> Self {
41        let line_ending = LineEnding::detect(src);
42        Self {
43            id,
44            path: None,
45            rope: Rope::from_str(src),
46            modified: false,
47            encoding: Encoding::default(),
48            line_ending,
49            undo: UndoTree::new(),
50        }
51    }
52
53    pub fn open(id: BufferId, path: impl AsRef<Path>) -> Result<Self, BufferError> {
54        let path = path.as_ref().to_path_buf();
55        let src = if path.exists() {
56            std::fs::read_to_string(&path)?
57        } else {
58            String::new()
59        };
60        let line_ending = LineEnding::detect(&src);
61        Ok(Self {
62            id,
63            path: Some(path),
64            rope: Rope::from_str(&src),
65            modified: false,
66            encoding: Encoding::default(),
67            line_ending,
68            undo: UndoTree::new(),
69        })
70    }
71
72    pub fn save(&mut self) -> Result<(), BufferError> {
73        let path = self.path.clone().ok_or(BufferError::NoPath)?;
74        let mut src = String::new();
75        for line in self.rope.lines() {
76            src.push_str(&line.to_string());
77        }
78        std::fs::write(&path, src)?;
79        self.modified = false;
80        Ok(())
81    }
82
83    pub fn save_as(&mut self, path: impl AsRef<Path>) -> Result<(), BufferError> {
84        self.path = Some(path.as_ref().to_path_buf());
85        self.save()
86    }
87
88    // ── Queries ────────────────────────────────────────────────────
89
90    #[must_use]
91    pub fn line_count(&self) -> u32 {
92        u32::try_from(self.rope.len_lines()).unwrap_or(u32::MAX)
93    }
94
95    #[must_use]
96    pub fn byte_count(&self) -> usize {
97        self.rope.len_bytes()
98    }
99
100    #[must_use]
101    pub fn char_count(&self) -> usize {
102        self.rope.len_chars()
103    }
104
105    pub fn line(&self, n: u32) -> Option<String> {
106        let n = n as usize;
107        if n >= self.rope.len_lines() {
108            return None;
109        }
110        Some(self.rope.line(n).to_string())
111    }
112
113    pub fn line_len_chars(&self, n: u32) -> u32 {
114        let n = n as usize;
115        if n >= self.rope.len_lines() {
116            return 0;
117        }
118        let line = self.rope.line(n);
119        let mut len = line.len_chars();
120        // Strip trailing newline from char count for cursor-placement purposes.
121        if line.chars().last().is_some_and(|c| c == '\n' || c == '\r') {
122            len = len.saturating_sub(1);
123        }
124        u32::try_from(len).unwrap_or(u32::MAX)
125    }
126
127    /// Clamp a position to valid coordinates inside this buffer.
128    #[must_use]
129    pub fn clamp(&self, pos: Position) -> Position {
130        let line = pos.line.min(self.line_count().saturating_sub(1));
131        let col = pos.column.min(self.line_len_chars(line));
132        Position::new(line, col)
133    }
134
135    pub fn position_to_char(&self, pos: Position) -> Result<usize, BufferError> {
136        let line = pos.line as usize;
137        if line >= self.rope.len_lines() {
138            return Err(BufferError::InvalidPosition {
139                line: pos.line,
140                column: pos.column,
141                total_lines: self.line_count(),
142            });
143        }
144        let line_start = self.rope.line_to_char(line);
145        let line_slice = self.rope.line(line);
146        let max_col = line_slice.len_chars();
147        let col = (pos.column as usize).min(max_col);
148        Ok(line_start + col)
149    }
150
151    #[must_use]
152    pub fn char_to_position(&self, ch: usize) -> Position {
153        let line = self.rope.char_to_line(ch.min(self.rope.len_chars()));
154        let line_start = self.rope.line_to_char(line);
155        let col = ch.saturating_sub(line_start);
156        Position::new(
157            u32::try_from(line).unwrap_or(u32::MAX),
158            u32::try_from(col).unwrap_or(u32::MAX),
159        )
160    }
161
162    pub fn slice(&self, range: Range) -> Result<String, BufferError> {
163        let r = range.normalized();
164        let a = self.position_to_char(r.start)?;
165        let b = self.position_to_char(r.end)?;
166        Ok(self.rope.slice(a..b).to_string())
167    }
168
169    // ── Mutation ───────────────────────────────────────────────────
170
171    pub fn apply(&mut self, edit: &Edit) -> Result<UndoEntry, BufferError> {
172        let range = edit.range.normalized();
173        let start_char = self.position_to_char(range.start)?;
174        let end_char = self.position_to_char(range.end)?;
175        let previous_text = self.rope.slice(start_char..end_char).to_string();
176
177        let (inserted_len_chars, reverse_kind) = match &edit.kind {
178            EditKind::Insert { text } => {
179                self.rope.insert(start_char, text);
180                (text.chars().count(), EditKind::Delete)
181            }
182            EditKind::Delete => {
183                self.rope.remove(start_char..end_char);
184                (
185                    0usize,
186                    EditKind::Insert {
187                        text: previous_text.clone(),
188                    },
189                )
190            }
191            EditKind::Replace { text } => {
192                self.rope.remove(start_char..end_char);
193                self.rope.insert(start_char, text);
194                (
195                    text.chars().count(),
196                    EditKind::Replace {
197                        text: previous_text.clone(),
198                    },
199                )
200            }
201        };
202        self.modified = true;
203
204        // Compute the reverse edit's range — where the insertion now sits.
205        let reverse_end_char = start_char + inserted_len_chars;
206        let reverse_range = Range::new(
207            self.char_to_position(start_char),
208            self.char_to_position(reverse_end_char),
209        );
210        let reverse_edit = Edit {
211            range: reverse_range,
212            kind: reverse_kind,
213        };
214        let entry = UndoEntry {
215            applied: edit.clone(),
216            reverse: reverse_edit,
217        };
218        self.undo.push(entry.clone());
219        Ok(entry)
220    }
221
222    pub fn undo(&mut self) -> Result<UndoEntry, BufferError> {
223        let entry = self.undo.pop_undo().ok_or(BufferError::NothingToUndo)?;
224        // Apply reverse edit without pushing a new undo entry.
225        let r = entry.reverse.range.normalized();
226        let a = self.position_to_char(r.start)?;
227        let b = self.position_to_char(r.end)?;
228        match &entry.reverse.kind {
229            EditKind::Insert { text } => {
230                self.rope.insert(a, text);
231            }
232            EditKind::Delete => {
233                self.rope.remove(a..b);
234            }
235            EditKind::Replace { text } => {
236                self.rope.remove(a..b);
237                self.rope.insert(a, text);
238            }
239        }
240        self.modified = true;
241        Ok(entry)
242    }
243
244    pub fn redo(&mut self) -> Result<UndoEntry, BufferError> {
245        let entry = self.undo.pop_redo().ok_or(BufferError::NothingToRedo)?;
246        let r = entry.applied.range.normalized();
247        let a = self.position_to_char(r.start)?;
248        let b = self.position_to_char(r.end)?;
249        match &entry.applied.kind {
250            EditKind::Insert { text } => {
251                self.rope.insert(a, text);
252            }
253            EditKind::Delete => {
254                self.rope.remove(a..b);
255            }
256            EditKind::Replace { text } => {
257                self.rope.remove(a..b);
258                self.rope.insert(a, text);
259            }
260        }
261        self.modified = true;
262        Ok(entry)
263    }
264
265    #[must_use]
266    pub fn to_string(&self) -> String {
267        self.rope.to_string()
268    }
269}
270
271/// A registry of open buffers keyed by id — phase 1 single-threaded view.
272#[derive(Debug, Default, Clone)]
273pub struct BufferSet {
274    buffers: HashMap<BufferId, Buffer>,
275    next_id: u64,
276}
277
278impl BufferSet {
279    #[must_use]
280    pub fn new() -> Self {
281        Self::default()
282    }
283
284    pub fn next_id(&mut self) -> BufferId {
285        self.next_id += 1;
286        BufferId(self.next_id)
287    }
288
289    pub fn open(&mut self, path: impl AsRef<Path>) -> Result<BufferId, BufferError> {
290        let id = self.next_id();
291        let buf = Buffer::open(id, path)?;
292        self.buffers.insert(id, buf);
293        Ok(id)
294    }
295
296    pub fn scratch(&mut self, src: &str) -> BufferId {
297        let id = self.next_id();
298        self.buffers.insert(id, Buffer::from_str(id, src));
299        id
300    }
301
302    #[must_use]
303    pub fn get(&self, id: BufferId) -> Option<&Buffer> {
304        self.buffers.get(&id)
305    }
306
307    pub fn get_mut(&mut self, id: BufferId) -> Option<&mut Buffer> {
308        self.buffers.get_mut(&id)
309    }
310
311    #[must_use]
312    pub fn ids(&self) -> Vec<BufferId> {
313        let mut v: Vec<_> = self.buffers.keys().copied().collect();
314        v.sort();
315        v
316    }
317}
318
319#[derive(Debug, Clone, Serialize, Deserialize)]
320pub struct BufferSummary {
321    pub id: BufferId,
322    pub path: Option<PathBuf>,
323    pub line_count: u32,
324    pub modified: bool,
325}
326
327#[cfg(test)]
328mod tests {
329    use super::*;
330    use escriba_core::{Edit, Position, Range};
331
332    fn buf(src: &str) -> Buffer {
333        Buffer::from_str(BufferId(1), src)
334    }
335
336    #[test]
337    fn empty_has_one_line() {
338        let b = Buffer::empty(BufferId(1));
339        assert_eq!(b.line_count(), 1);
340    }
341
342    #[test]
343    fn line_counts() {
344        let b = buf("a\nb\nc\n");
345        assert_eq!(b.line_count(), 4); // trailing \n leaves a final empty line
346    }
347
348    #[test]
349    fn position_char_round_trip() {
350        let b = buf("hello\nworld\n");
351        let p = Position::new(1, 3);
352        let c = b.position_to_char(p).unwrap();
353        assert_eq!(b.char_to_position(c), p);
354    }
355
356    #[test]
357    fn insert_then_undo() {
358        let mut b = buf("hello");
359        let e = Edit::insert(Position::new(0, 5), " world");
360        b.apply(&e).unwrap();
361        assert_eq!(b.to_string(), "hello world");
362        b.undo().unwrap();
363        assert_eq!(b.to_string(), "hello");
364    }
365
366    #[test]
367    fn delete_then_redo() {
368        let mut b = buf("hello world");
369        let e = Edit::delete(Range::new(Position::new(0, 5), Position::new(0, 11)));
370        b.apply(&e).unwrap();
371        assert_eq!(b.to_string(), "hello");
372        b.undo().unwrap();
373        assert_eq!(b.to_string(), "hello world");
374        b.redo().unwrap();
375        assert_eq!(b.to_string(), "hello");
376    }
377
378    #[test]
379    fn replace_is_delete_plus_insert() {
380        let mut b = buf("hello world");
381        let e = Edit::replace(
382            Range::new(Position::new(0, 6), Position::new(0, 11)),
383            "tatara",
384        );
385        b.apply(&e).unwrap();
386        assert_eq!(b.to_string(), "hello tatara");
387        b.undo().unwrap();
388        assert_eq!(b.to_string(), "hello world");
389    }
390
391    #[test]
392    fn clamp_constrains_position() {
393        let b = buf("ab\ncd");
394        assert_eq!(b.line_count(), 2);
395        assert_eq!(b.clamp(Position::new(0, 99)), Position::new(0, 2));
396        assert_eq!(b.clamp(Position::new(99, 0)), Position::new(1, 0));
397    }
398
399    #[test]
400    fn slice_returns_text() {
401        let b = buf("hello world");
402        let s = b
403            .slice(Range::new(Position::new(0, 6), Position::new(0, 11)))
404            .unwrap();
405        assert_eq!(s, "world");
406    }
407
408    #[test]
409    fn save_round_trip() {
410        let dir = tempfile::tempdir().unwrap();
411        let path = dir.path().join("demo.txt");
412        let mut b = Buffer::from_str(BufferId(1), "hello\n");
413        b.save_as(&path).unwrap();
414        let b2 = Buffer::open(BufferId(2), &path).unwrap();
415        assert_eq!(b2.to_string(), "hello\n");
416    }
417
418    #[test]
419    fn buffer_set_tracks_ids() {
420        let mut set = BufferSet::new();
421        let a = set.scratch("one");
422        let b = set.scratch("two");
423        assert_ne!(a, b);
424        assert_eq!(set.ids().len(), 2);
425        assert_eq!(set.get(a).unwrap().to_string(), "one");
426    }
427}