Skip to main content

redox_core/buffer/text_buffer/
editing.rs

1//! Editing operations for `TextBuffer`.
2//!
3//! This file is meant to be included as part of the `buffer::text_buffer` module
4//! and adds editing-focused methods via an `impl TextBuffer` block.
5//!
6//! Design goals:
7//! - keep public methods small and composable
8//! - use char indices (ropey’s primary indexing model) internally
9//! - return updated `Pos`/`Selection` to make call sites explicit
10//! - keep it easy to extend later (undo/redo, transactions, multiple cursors, etc.)
11
12use ropey::Rope;
13
14use crate::buffer::{Edit, Pos, Selection, TextBuffer};
15
16impl TextBuffer {
17    /// Insert `text` at the given logical position.
18    ///
19    /// Returns the new cursor position (at the end of inserted text).
20    ///
21    /// NOTE: This is a primitive operation I can build higher-level commands on top of
22    /// (e.g. replace-selection-then-insert, paste, auto-indent, etc).
23    pub fn insert(&mut self, pos: Pos, text: &str) -> Pos {
24        let at = self.pos_to_char(pos);
25        self.rope.insert(at, text);
26
27        // Compute end position by converting at + inserted_chars.
28        // We avoid `text.chars().count()` to keep indexing consistent with ropey.
29        let inserted_chars = Rope::from_str(text).len_chars();
30        self.char_to_pos(at + inserted_chars)
31    }
32
33    /// Delete a range between two positions (order-independent).
34    ///
35    /// Returns the resulting cursor position (at the start of deletion).
36    pub fn delete_range(&mut self, a: Pos, b: Pos) -> Pos {
37        let start = self.pos_to_char(crate::buffer::util::min_pos(self, a, b));
38        let end = self.pos_to_char(crate::buffer::util::max_pos(self, a, b));
39
40        if start < end {
41            self.rope.remove(start..end);
42        }
43
44        self.char_to_pos(start)
45    }
46
47    /// Delete the selection (if any). Returns `(new_cursor, did_delete)`.
48    pub fn delete_selection(&mut self, sel: Selection) -> (Pos, bool) {
49        if sel.is_empty() {
50            return (self.clamp_pos(sel.cursor), false);
51        }
52
53        let (start, end) = sel.ordered();
54        let new_cursor = self.delete_range(start, end);
55        (new_cursor, true)
56    }
57
58    /// Backspace behavior:
59    /// - if the selection is non-empty, delete it
60    /// - otherwise delete the char before the cursor (if any)
61    ///
62    /// Returns an empty selection at the updated cursor.
63    pub fn backspace(&mut self, sel: Selection) -> Selection {
64        if !sel.is_empty() {
65            let (cursor, _) = self.delete_selection(sel);
66            return Selection::empty(cursor);
67        }
68
69        let cursor = self.clamp_pos(sel.cursor);
70        let at = self.pos_to_char(cursor);
71        if at == 0 {
72            return Selection::empty(cursor);
73        }
74
75        let start = at - 1;
76        self.rope.remove(start..at);
77        let new_cursor = self.char_to_pos(start);
78        Selection::empty(new_cursor)
79    }
80
81    /// Delete (forward) behavior:
82    /// - if the selection is non-empty, delete it
83    /// - otherwise delete the char at the cursor (if any)
84    ///
85    /// Returns an empty selection at the updated cursor.
86    pub fn delete(&mut self, sel: Selection) -> Selection {
87        if !sel.is_empty() {
88            let (cursor, _) = self.delete_selection(sel);
89            return Selection::empty(cursor);
90        }
91
92        let cursor = self.clamp_pos(sel.cursor);
93        let at = self.pos_to_char(cursor);
94        let maxc = self.len_chars();
95
96        if at >= maxc {
97            return Selection::empty(cursor);
98        }
99
100        self.rope.remove(at..at + 1);
101        let new_cursor = self.char_to_pos(at);
102        Selection::empty(new_cursor)
103    }
104
105    /// Insert a newline at the cursor (or replace the selection).
106    ///
107    /// Returns an empty selection at the updated cursor.
108    pub fn insert_newline(&mut self, sel: Selection) -> Selection {
109        if !sel.is_empty() {
110            let (start, end) = sel.ordered();
111            let cursor = self.delete_range(start, end);
112            let new_cursor = self.insert(cursor, "\n");
113            return Selection::empty(new_cursor);
114        }
115
116        let cursor = self.clamp_pos(sel.cursor);
117        let new_cursor = self.insert(cursor, "\n");
118        Selection::empty(new_cursor)
119    }
120
121    /// Apply an `Edit` expressed in char indices.
122    ///
123    /// NOTE: This is intended as a low-level building block for future undo/redo
124    /// so I can store `Edit`s, invert them, and replay them.
125    ///
126    /// Returns the resulting cursor position (end of inserted text, or start of deletion).
127    pub fn apply_edit(&mut self, edit: Edit) -> Pos {
128        let maxc = self.len_chars();
129        let start = edit.range.start.min(maxc);
130        let end = edit.range.end.min(maxc);
131        let (start, end) = if start <= end {
132            (start, end)
133        } else {
134            (end, start)
135        };
136
137        if start < end {
138            self.rope.remove(start..end);
139        }
140
141        if !edit.insert.is_empty() {
142            self.rope.insert(start, &edit.insert);
143            let inserted_chars = Rope::from_str(&edit.insert).len_chars();
144            self.char_to_pos(start + inserted_chars)
145        } else {
146            self.char_to_pos(start)
147        }
148    }
149
150    /// Replace the current selection with `text` (if selection is empty, behaves like insert).
151    /// This is a convenience method that a bunch of editor actions can use.
152    ///
153    /// Returns an empty selection at the updated cursor.
154    pub fn replace_selection(&mut self, sel: Selection, text: &str) -> Selection {
155        if !sel.is_empty() {
156            let (start, end) = sel.ordered();
157            let cursor = self.delete_range(start, end);
158            let cursor = self.insert(cursor, text);
159            Selection::empty(cursor)
160        } else {
161            let cursor = self.insert(sel.cursor, text);
162            Selection::empty(cursor)
163        }
164    }
165}