Skip to main content

twrite_core/
buffer.rs

1use std::ops::Range;
2use std::path::Path;
3
4use ropey::Rope;
5
6use crate::{
7    coordinates::Point,
8    error::{EditorError, Result},
9    history::{Edit, History, Transaction},
10};
11
12/// A text buffer that manages document contents, cursor position,
13/// and undo/redo history.
14///
15/// `EditorBuffer` stores its text in a [`Rope`], making insertion,
16/// deletion, and line-based operations efficient for an editor.
17///
18/// Cursor positions are represented internally as byte offsets.
19///
20/// # Examples
21///
22/// ```
23/// use twrite_core::EditorBuffer;
24///
25/// let buffer = EditorBuffer::new("Hello, world!");
26///
27/// assert_eq!(buffer.len_bytes(), 13);
28/// assert_eq!(buffer.len_lines(), 1);
29/// assert_eq!(buffer.cursor_offset(), 0);
30/// ```
31#[derive(Debug)]
32pub struct EditorBuffer {
33    text: Rope,
34    cursor: usize,
35    history: History,
36    version: usize,
37}
38
39impl EditorBuffer {
40    /// Creates a new editor buffer containing `initial_text`.
41    ///
42    /// The cursor is initially positioned at byte offset `0`, and the
43    /// undo/redo history starts empty.
44    pub fn new(initial_text: &str) -> Self {
45        Self {
46            text: Rope::from_str(initial_text),
47            cursor: 0,
48            history: History::default(),
49            version: 0,
50        }
51    }
52
53    /// Returns the monotonic document version, incremented on every text modification.
54    pub fn version(&self) -> usize {
55        self.version
56    }
57
58    /// Returns a reference to the underlying text.
59    ///
60    /// The returned [`Rope`] can be used to inspect the document without
61    /// copying its contents.
62    pub fn text(&self) -> &Rope {
63        &self.text
64    }
65
66    /// Returns the current cursor position as a byte offset.
67    ///
68    /// The cursor is always maintained at a valid UTF-8 character boundary.
69    pub fn cursor_offset(&self) -> usize {
70        self.cursor
71    }
72
73    /// Returns the total number of bytes in the document.
74    pub fn len_bytes(&self) -> usize {
75        self.text.len_bytes()
76    }
77
78    /// Returns the number of lines in the document.
79    pub fn len_lines(&self) -> usize {
80        self.text.len_lines()
81    }
82
83    /// Returns the contents of a line as a [`String`].
84    ///
85    /// Returns an empty string if `line_idx` is outside the document.
86    pub fn line_to_string(&self, line_idx: usize) -> String {
87        if line_idx >= self.text.len_lines() {
88            return String::new();
89        }
90
91        self.text.line(line_idx).to_string()
92    }
93
94    /// Converts a byte offset into a [`Point`].
95    ///
96    /// The returned point contains a zero-based row and a byte-based
97    /// column. If `offset` is beyond the end of the document, it is
98    /// clamped to the document's end.
99    pub fn offset_to_point(&self, offset: usize) -> Point {
100        let clamped = offset.min(self.text.len_bytes());
101        let row = self.text.byte_to_line(clamped);
102        let line_start_byte = self.text.line_to_byte(row);
103        let column = clamped - line_start_byte;
104
105        Point::new(row, column)
106    }
107
108    /// Converts a [`Point`] into a byte offset.
109    ///
110    /// If the row is outside the document, the returned offset points to
111    /// the end of the document. If the column exceeds the length of the
112    /// line, it is clamped to the end of that line.
113    pub fn point_to_offset(&self, point: Point) -> usize {
114        if point.row >= self.text.len_lines() {
115            return self.text.len_bytes();
116        }
117        let line_start_byte = self.text.line_to_byte(point.row);
118        let line_len = self.text.line(point.row).len_bytes();
119        let col = point.column.min(line_len);
120        line_start_byte + col
121    }
122
123    /// Returns the current cursor position as a [`Point`].
124    pub fn cursor_point(&self) -> Point {
125        self.offset_to_point(self.cursor)
126    }
127
128    /// Sets the cursor to the given byte offset.
129    ///
130    /// The offset is clamped to the document's bounds.
131    ///
132    /// The resulting cursor position is kept on a valid UTF-8 character
133    /// boundary.
134    pub fn set_cursor_offset(&mut self, offset: usize) {
135        let offset = offset.min(self.text.len_bytes());
136        self.cursor = self.text.char_to_byte(self.text.byte_to_char(offset));
137    }
138
139    /// Sets the cursor to the given document position.
140    ///
141    /// The row and column are clamped to the document's bounds.
142    pub fn set_cursor_point(&mut self, point: Point) {
143        self.cursor = self.point_to_offset(point);
144    }
145
146    /// Moves the cursor one character to the right.
147    ///
148    /// Does nothing if the cursor is already at the end of the document.
149    pub fn move_cursor_right(&mut self) {
150        if self.cursor < self.text.len_bytes() {
151            let char_idx = self.text.byte_to_char(self.cursor);
152            let next_char = (char_idx + 1).min(self.text.len_chars());
153            self.cursor = self.text.char_to_byte(next_char);
154        }
155    }
156
157    /// Moves the cursor one line upward.
158    ///
159    /// The column is preserved when possible. If the target line is shorter,
160    /// the cursor is placed at the end of that line.
161    pub fn move_cursor_up(&mut self) {
162        let point = self.cursor_point();
163        if point.row > 0 {
164            self.set_cursor_point(Point::new(point.row - 1, point.column));
165        }
166    }
167
168    /// Moves the cursor one line downward.
169    ///
170    /// The column is preserved when possible. If the target line is shorter,
171    /// the cursor is placed at the end of that line.
172    pub fn move_cursor_down(&mut self) {
173        let point = self.cursor_point();
174        if point.row + 1 < self.text.len_lines() {
175            self.set_cursor_point(Point::new(point.row + 1, point.column));
176        }
177    }
178
179    /// Moves the cursor one character to the left.
180    ///
181    /// Does nothing if the cursor is already at the beginning of the
182    /// document.
183    pub fn move_cursor_left(&mut self) {
184        if self.cursor > 0 {
185            let char_idx = self.text.byte_to_char(self.cursor);
186            self.cursor = self.text.char_to_byte(char_idx - 1);
187        }
188    }
189
190    /// Returns the byte offset of the previous word start relative to current cursor.
191    pub fn prev_word_offset(&self) -> usize {
192        crate::movement::find_prev_word_start(&self.text, self.cursor)
193    }
194
195    /// Returns the byte offset of the next word end relative to current cursor.
196    pub fn next_word_offset(&self) -> usize {
197        crate::movement::find_next_word_end(&self.text, self.cursor)
198    }
199
200    /// Returns the byte offset of the start of the current line.
201    pub fn line_start_offset(&self) -> usize {
202        crate::movement::find_line_start(&self.text, self.cursor)
203    }
204
205    /// Returns the byte offset of the end of the current line (excluding trailing newline).
206    pub fn line_end_offset(&self) -> usize {
207        crate::movement::find_line_end(&self.text, self.cursor)
208    }
209
210    /// Returns the byte range of the word, punctuation token, or whitespace run containing `offset`.
211    pub fn word_range_at(&self, offset: usize) -> Range<usize> {
212        crate::movement::find_word_range_at(&self.text, offset)
213    }
214
215    /// Returns the byte range of the full line containing `offset`, including
216    /// any trailing line terminator.
217    pub fn line_range_at(&self, offset: usize) -> Range<usize> {
218        crate::movement::find_line_range_at(&self.text, offset)
219    }
220
221    /// Moves the cursor to the start of the previous word.
222    pub fn move_cursor_prev_word(&mut self) {
223        self.cursor = self.prev_word_offset();
224    }
225
226    /// Moves the cursor to the end of the next word.
227    pub fn move_cursor_next_word(&mut self) {
228        self.cursor = self.next_word_offset();
229    }
230
231    /// Moves the cursor to the beginning of the current line.
232    pub fn move_cursor_line_start(&mut self) {
233        self.cursor = self.line_start_offset();
234    }
235
236    /// Moves the cursor to the end of the current line.
237    pub fn move_cursor_line_end(&mut self) {
238        self.cursor = self.line_end_offset();
239    }
240
241    /// Deletes the text from the previous word boundary up to the cursor.
242    ///
243    /// Returns `true` if text was deleted, or `false` if the cursor was already at the beginning.
244    pub fn delete_prev_word(&mut self) -> bool {
245        let target = self.prev_word_offset();
246        if target < self.cursor {
247            self.delete_range(target..self.cursor);
248            true
249        } else {
250            false
251        }
252    }
253
254    /// Deletes the text from the cursor up to the next word boundary.
255    ///
256    /// Returns `true` if text was deleted, or `false` if the cursor was already at the end.
257    pub fn delete_next_word(&mut self) -> bool {
258        let target = self.next_word_offset();
259        if self.cursor < target {
260            self.delete_range(self.cursor..target);
261            true
262        } else {
263            false
264        }
265    }
266
267    /// Inserts `text` at the current cursor position.
268    ///
269    /// The inserted text becomes a single undoable transaction, and the
270    /// cursor is moved to the end of the inserted text.
271    ///
272    /// Inserting new text after undoing clears the redo history.
273    pub fn insert(&mut self, text: &str) {
274        let previous_cursor = self.cursor;
275        let char_idx = self.text.byte_to_char(self.cursor);
276        self.text.insert(char_idx, text);
277        self.cursor += text.len();
278
279        let tx = Transaction {
280            edits: vec![Edit {
281                bytes_range: previous_cursor..previous_cursor,
282                inserted_text: text.to_string(),
283                deleted_text: String::new(),
284            }],
285            previous_cursor,
286            resulting_cursor: self.cursor,
287        };
288
289        self.history.undo_stack.push(tx);
290        self.history.redo_stack.clear();
291        self.version += 1;
292    }
293
294    /// Deletes the character immediately before the cursor.
295    ///
296    /// If the cursor is at the beginning of the document, this method does
297    /// nothing.
298    ///
299    /// The deleted character is recorded as an undoable transaction and
300    /// the cursor moves to the beginning of the deleted character.
301    ///
302    /// Inserting new text after undoing clears the redo history.
303    pub fn backspace(&mut self) {
304        if self.cursor == 0 {
305            return;
306        }
307
308        let char_idx = self.text.byte_to_char(self.cursor);
309        let previous_char_byte = self.text.char_to_byte(char_idx - 1);
310        let range_to_delete = previous_char_byte..self.cursor;
311        let deleted_text = self.text.byte_slice(range_to_delete.clone()).to_string();
312
313        let previous_cursor = self.cursor;
314        self.text.remove((char_idx - 1)..char_idx);
315        self.cursor = previous_char_byte;
316
317        let tx = Transaction {
318            edits: vec![Edit {
319                bytes_range: range_to_delete,
320                inserted_text: String::new(),
321                deleted_text,
322            }],
323            previous_cursor,
324            resulting_cursor: self.cursor,
325        };
326
327        self.history.undo_stack.push(tx);
328        self.history.redo_stack.clear();
329        self.version += 1;
330    }
331
332    /// Deletes the character at the current cursor position.
333    ///
334    /// If the cursor is at the end of the document, this method does nothing.
335    /// The cursor remains at the same byte offset after the deletion.
336    ///
337    /// The deleted text is recorded as a transaction so the operation can be
338    /// undone and redone.
339    pub fn delete(&mut self) {
340        if self.cursor >= self.text.len_bytes() {
341            return;
342        }
343
344        let char_idx = self.text.byte_to_char(self.cursor);
345        let next_char = char_idx + 1;
346
347        let end = self.text.char_to_byte(next_char);
348        let byte_range = self.cursor..end;
349        let deleted_text = self.text.byte_slice(byte_range.clone()).to_string();
350
351        self.text.remove(char_idx..next_char);
352
353        let tx = Transaction {
354            edits: vec![Edit {
355                bytes_range: byte_range,
356                inserted_text: String::new(),
357                deleted_text,
358            }],
359            previous_cursor: self.cursor,
360            resulting_cursor: self.cursor,
361        };
362
363        self.history.undo_stack.push(tx);
364        self.history.redo_stack.clear();
365        self.version += 1;
366    }
367
368    /// Deletes the text within `range`.
369    ///
370    /// The deletion is recorded as an undoable transaction and the cursor
371    /// is set to the start of `range`.
372    pub fn delete_range(&mut self, range: Range<usize>) {
373        let start = range.start.min(self.text.len_bytes());
374        let end = range.end.min(self.text.len_bytes());
375        if start >= end {
376            return;
377        }
378
379        let start_char = self.text.byte_to_char(start);
380        let end_char = self.text.byte_to_char(end);
381        let deleted_text = self.text.byte_slice(start..end).to_string();
382        let previous_cursor = self.cursor;
383
384        self.text.remove(start_char..end_char);
385        self.cursor = start;
386
387        let tx = Transaction {
388            edits: vec![Edit {
389                bytes_range: start..end,
390                inserted_text: String::new(),
391                deleted_text,
392            }],
393            previous_cursor,
394            resulting_cursor: self.cursor,
395        };
396
397        self.history.undo_stack.push(tx);
398        self.history.redo_stack.clear();
399        self.version += 1;
400    }
401
402    /// Replaces the text within `range` with `text`.
403    ///
404    /// If `range` is empty, this is equivalent to [`Self::insert`].
405    pub fn replace_range(&mut self, range: Range<usize>, text: &str) {
406        let start = range.start.min(self.text.len_bytes());
407        let end = range.end.min(self.text.len_bytes());
408        if start == end {
409            self.cursor = start;
410            self.insert(text);
411            return;
412        }
413
414        let start_char = self.text.byte_to_char(start);
415        let end_char = self.text.byte_to_char(end);
416        let deleted_text = self.text.byte_slice(start..end).to_string();
417        let previous_cursor = self.cursor;
418
419        self.text.remove(start_char..end_char);
420        self.text.insert(start_char, text);
421        self.cursor = start + text.len();
422
423        let tx = Transaction {
424            edits: vec![Edit {
425                bytes_range: start..end,
426                inserted_text: text.to_string(),
427                deleted_text,
428            }],
429            previous_cursor,
430            resulting_cursor: self.cursor,
431        };
432
433        self.history.undo_stack.push(tx);
434        self.history.redo_stack.clear();
435        self.version += 1;
436    }
437
438    /// Applies multiple non-overlapping replacements as a single undoable transaction.
439    ///
440    /// `replacements` holds `(range, replacement_text)` pairs. They are applied
441    /// back-to-front so earlier byte offsets stay valid, recorded as one
442    /// [`Transaction`](crate::history::Transaction), and undone/redone together.
443    /// Returns the number of replacements applied. Overlapping, empty, or
444    /// out-of-bounds ranges are skipped. A no-op leaves the version untouched.
445    pub fn replace_many(&mut self, replacements: Vec<(Range<usize>, String)>) -> usize {
446        let len = self.text.len_bytes();
447        let mut valid: Vec<(usize, usize, String)> = Vec::with_capacity(replacements.len());
448        for (range, text) in replacements {
449            if range.start >= range.end || range.end > len {
450                continue;
451            }
452            if !self.is_char_boundary(range.start) || !self.is_char_boundary(range.end) {
453                continue;
454            }
455            valid.push((range.start, range.end, text));
456        }
457        if valid.is_empty() {
458            return 0;
459        }
460        valid.sort_by_key(|(start, _, _)| *start);
461        // Matches from a single scan never overlap, but callers may pass
462        // arbitrary ranges: keep the first of any overlapping pair.
463        let mut dedup: Vec<(usize, usize, String)> = Vec::with_capacity(valid.len());
464        for (start, end, text) in valid {
465            if let Some((_, last_end, _)) = dedup.last()
466                && start < *last_end
467            {
468                continue;
469            }
470            dedup.push((start, end, text));
471        }
472        if dedup.is_empty() {
473            return 0;
474        }
475
476        let previous_cursor = self.cursor;
477        // Apply back-to-front so earlier byte offsets stay valid, then store
478        // the edits ascending; undo/redo both walk descending (see below).
479        let mut edits: Vec<Edit> = Vec::with_capacity(dedup.len());
480        for (start, end, text) in dedup.iter().rev() {
481            let deleted_text = self.text.byte_slice(*start..*end).to_string();
482            let start_char = self.text.byte_to_char(*start);
483            let end_char = self.text.byte_to_char(*end);
484            self.text.remove(start_char..end_char);
485            self.text.insert(start_char, text);
486            edits.push(Edit {
487                bytes_range: *start..*end,
488                inserted_text: text.clone(),
489                deleted_text,
490            });
491        }
492        edits.reverse();
493
494        // Cursor tracks the end of the last replacement: earlier edits shift
495        // it by the sum of their length deltas.
496        let mut shift: i64 = 0;
497        for edit in &edits[..edits.len() - 1] {
498            shift += edit.inserted_text.len() as i64
499                - (edit.bytes_range.end - edit.bytes_range.start) as i64;
500        }
501        let last = &edits[edits.len() - 1];
502        let new_cursor = (last.bytes_range.start as i64 + shift + last.inserted_text.len() as i64)
503            .max(0) as usize;
504        self.cursor = new_cursor.min(self.text.len_bytes());
505
506        let tx = Transaction {
507            edits,
508            previous_cursor,
509            resulting_cursor: self.cursor,
510        };
511        let applied = tx.edits.len();
512
513        self.history.undo_stack.push(tx);
514        self.history.redo_stack.clear();
515        self.version += 1;
516        applied
517    }
518
519    /// Undoes the most recent transaction.
520    ///
521    /// If there is no transaction to undo, this method does nothing.
522    /// The undone transaction is moved to the redo stack.
523    pub fn undo(&mut self) {
524        if let Some(tx) = self.history.undo_stack.pop() {
525            // Stored ranges are original-document coordinates. Undone
526            // descending, each edit's inserted text sits at its stored start
527            // plus the length deltas of all still-applied earlier edits.
528            let mut prefix = Vec::with_capacity(tx.edits.len() + 1);
529            prefix.push(0i64);
530            for edit in &tx.edits {
531                let delta = edit.inserted_text.len() as i64
532                    - (edit.bytes_range.end - edit.bytes_range.start) as i64;
533                prefix.push(prefix.last().copied().unwrap_or(0) + delta);
534            }
535            for (index, edit) in tx.edits.iter().enumerate().rev() {
536                let start = (edit.bytes_range.start as i64 + prefix[index]).max(0) as usize;
537                let end = start + edit.inserted_text.len();
538
539                if end > start {
540                    let start_char = self.text.byte_to_char(start);
541                    let end_char = self.text.byte_to_char(end);
542                    self.text.remove(start_char..end_char);
543                }
544                if !edit.deleted_text.is_empty() {
545                    let start_char = self.text.byte_to_char(start);
546                    self.text.insert(start_char, &edit.deleted_text);
547                }
548            }
549            self.cursor = tx.previous_cursor;
550            self.history.redo_stack.push(tx);
551            self.version += 1;
552        }
553    }
554
555    /// Returns whether an undo transaction is available.
556    pub fn can_undo(&self) -> bool {
557        !self.history.undo_stack.is_empty()
558    }
559
560    /// Returns whether a redo transaction is available.
561    pub fn can_redo(&self) -> bool {
562        !self.history.redo_stack.is_empty()
563    }
564
565    /// Redoes the most recently undone transaction.
566    ///
567    /// If there is no transaction to redo, this method does nothing.
568    /// The redone transaction is moved back to the undo stack.
569    pub fn redo(&mut self) {
570        if let Some(tx) = self.history.redo_stack.pop() {
571            // Descending (like `undo`): higher offsets are re-applied first so
572            // earlier stored ranges stay valid for multi-edit transactions.
573            for edit in tx.edits.iter().rev() {
574                let start = edit.bytes_range.start;
575                let end = start + edit.deleted_text.len();
576
577                if end > start {
578                    let start_char = self.text.byte_to_char(start);
579                    let end_char = self.text.byte_to_char(end);
580                    self.text.remove(start_char..end_char);
581                }
582                if !edit.inserted_text.is_empty() {
583                    let start_char = self.text.byte_to_char(start);
584                    self.text.insert(start_char, &edit.inserted_text);
585                }
586            }
587            self.cursor = tx.resulting_cursor;
588            self.history.undo_stack.push(tx);
589            self.version += 1;
590        }
591    }
592
593    /// Checks whether `offset` falls on a valid UTF-8 character boundary.
594    pub fn is_char_boundary(&self, offset: usize) -> bool {
595        if offset > self.text.len_bytes() {
596            return false;
597        }
598        let char_idx = self.text.byte_to_char(offset);
599        self.text.char_to_byte(char_idx) == offset
600    }
601
602    /// Validates that `offset` is within bounds and lies on a UTF-8 character boundary.
603    pub fn validate_offset(&self, offset: usize) -> Result<()> {
604        let len = self.text.len_bytes();
605        if offset > len {
606            return Err(EditorError::OutOfBounds { offset, len });
607        }
608        if !self.is_char_boundary(offset) {
609            return Err(EditorError::InvalidCharBoundary { offset });
610        }
611        Ok(())
612    }
613
614    /// Validates that `range` is well-formed, within bounds, and on UTF-8 character boundaries.
615    pub fn validate_range(&self, range: &Range<usize>) -> Result<()> {
616        let len = self.text.len_bytes();
617        if range.start > range.end || range.end > len {
618            return Err(EditorError::InvalidRange {
619                range: range.clone(),
620                len,
621            });
622        }
623        if !self.is_char_boundary(range.start) {
624            return Err(EditorError::InvalidCharBoundary {
625                offset: range.start,
626            });
627        }
628        if !self.is_char_boundary(range.end) {
629            return Err(EditorError::InvalidCharBoundary { offset: range.end });
630        }
631        Ok(())
632    }
633
634    /// Attempts to read the text of the given `row`, returning an error if out of bounds.
635    pub fn try_line_to_string(&self, row: usize) -> Result<String> {
636        let total_lines = self.text.len_lines();
637        if row >= total_lines {
638            return Err(EditorError::InvalidRow { row, total_lines });
639        }
640        Ok(self.text.line(row).to_string())
641    }
642
643    /// Attempts to replace the text within `range`, validating bounds and UTF-8 boundaries.
644    pub fn try_replace_range(&mut self, range: Range<usize>, text: &str) -> Result<()> {
645        self.validate_range(&range)?;
646        self.replace_range(range, text);
647        Ok(())
648    }
649
650    /// Attempts to delete the text within `range`, validating bounds and UTF-8 boundaries.
651    pub fn try_delete_range(&mut self, range: Range<usize>) -> Result<()> {
652        self.validate_range(&range)?;
653        self.delete_range(range);
654        Ok(())
655    }
656
657    /// Loads document text directly from a file path.
658    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
659        let content = std::fs::read_to_string(path)?;
660        Ok(Self::new(&content))
661    }
662
663    /// Saves the current buffer contents to a file path.
664    pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
665        std::fs::write(path, self.text.to_string())?;
666        Ok(())
667    }
668}