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//! - support both single edits and batched edit application
11
12use crate::buffer::{Edit, EditBatchSummary, Pos, Selection, TextBuffer};
13
14impl TextBuffer {
15    /// Insert `text` at the given logical position.
16    ///
17    /// Returns the new cursor position (at the end of inserted text).
18    ///
19    /// This is a primitive operation for higher-level editing commands.
20    pub fn insert(&mut self, pos: Pos, text: &str) -> Pos {
21        let at = self.pos_to_char(pos);
22        self.rope.insert(at, text);
23
24        let inserted_chars = text.chars().count();
25        self.char_to_pos(at + inserted_chars)
26    }
27
28    /// Delete a range between two positions (order-independent).
29    ///
30    /// Returns the resulting cursor position (at the start of deletion).
31    pub fn delete_range(&mut self, a: Pos, b: Pos) -> Pos {
32        let start = self.pos_to_char(crate::buffer::util::min_pos(self, a, b));
33        let end = self.pos_to_char(crate::buffer::util::max_pos(self, a, b));
34
35        if start < end {
36            self.rope.remove(start..end);
37        }
38
39        self.char_to_pos(start)
40    }
41
42    /// Delete the selection (if any). Returns `(new_cursor, did_delete)`.
43    pub fn delete_selection(&mut self, sel: Selection) -> (Pos, bool) {
44        if sel.is_empty() {
45            return (self.clamp_pos(sel.cursor), false);
46        }
47
48        let (start, end) = sel.ordered();
49        let new_cursor = self.delete_range(start, end);
50        (new_cursor, true)
51    }
52
53    /// Backspace behavior:
54    /// - if the selection is non-empty, delete it
55    /// - otherwise delete the char before the cursor (if any)
56    ///
57    /// Returns an empty selection at the updated cursor.
58    pub fn backspace(&mut self, sel: Selection) -> Selection {
59        if !sel.is_empty() {
60            let (cursor, _) = self.delete_selection(sel);
61            return Selection::empty(cursor);
62        }
63
64        let cursor = self.clamp_pos(sel.cursor);
65        let at = self.pos_to_char(cursor);
66        if at == 0 {
67            return Selection::empty(cursor);
68        }
69
70        let start = at - 1;
71        self.rope.remove(start..at);
72        let new_cursor = self.char_to_pos(start);
73        Selection::empty(new_cursor)
74    }
75
76    /// Delete (forward) behavior:
77    /// - if the selection is non-empty, delete it
78    /// - otherwise delete the char at the cursor (if any)
79    ///
80    /// Returns an empty selection at the updated cursor.
81    pub fn delete(&mut self, sel: Selection) -> Selection {
82        if !sel.is_empty() {
83            let (cursor, _) = self.delete_selection(sel);
84            return Selection::empty(cursor);
85        }
86
87        let cursor = self.clamp_pos(sel.cursor);
88        let at = self.pos_to_char(cursor);
89        let maxc = self.len_chars();
90
91        if at >= maxc {
92            return Selection::empty(cursor);
93        }
94
95        self.rope.remove(at..at + 1);
96        let new_cursor = self.char_to_pos(at);
97        Selection::empty(new_cursor)
98    }
99
100    /// Insert a newline at the cursor (or replace the selection).
101    ///
102    /// Returns an empty selection at the updated cursor.
103    pub fn insert_newline(&mut self, sel: Selection) -> Selection {
104        if !sel.is_empty() {
105            let (start, end) = sel.ordered();
106            let cursor = self.delete_range(start, end);
107            let new_cursor = self.insert(cursor, "\n");
108            return Selection::empty(new_cursor);
109        }
110
111        let cursor = self.clamp_pos(sel.cursor);
112        let new_cursor = self.insert(cursor, "\n");
113        Selection::empty(new_cursor)
114    }
115
116    /// Apply an `Edit` expressed in char indices.
117    ///
118    /// Returns the resulting cursor position (end of inserted text, or start of deletion).
119    pub fn apply_edit(&mut self, edit: Edit) -> Pos {
120        let maxc = self.len_chars();
121        let start = edit.range.start.min(maxc);
122        let end = edit.range.end.min(maxc);
123        let (start, end) = if start <= end {
124            (start, end)
125        } else {
126            (end, start)
127        };
128
129        if start < end {
130            self.rope.remove(start..end);
131        }
132
133        if !edit.insert.is_empty() {
134            self.rope.insert(start, &edit.insert);
135            let inserted_chars = edit.insert.chars().count();
136            self.char_to_pos(start + inserted_chars)
137        } else {
138            self.char_to_pos(start)
139        }
140    }
141
142    /// Apply multiple edits sequentially and return a transaction-style summary.
143    ///
144    /// Edits are applied in input order against the current buffer state.
145    pub fn apply_edits(&mut self, edits: &[Edit]) -> EditBatchSummary {
146        let mut changed_start = usize::MAX;
147        let mut changed_end = 0usize;
148        let mut cursor = self.char_to_pos(self.len_chars());
149
150        for edit in edits {
151            let maxc = self.len_chars();
152            let start = edit.range.start.min(maxc);
153            let end = edit.range.end.min(maxc);
154            let (start, _) = if start <= end {
155                (start, end)
156            } else {
157                (end, start)
158            };
159
160            cursor = self.apply_edit(edit.clone());
161            let cursor_char = self.pos_to_char(cursor);
162
163            changed_start = changed_start.min(start);
164            changed_end = changed_end.max(cursor_char.max(start));
165        }
166
167        if edits.is_empty() {
168            let cursor = self.char_to_pos(self.len_chars());
169            let at = self.pos_to_char(cursor);
170            return EditBatchSummary {
171                changed_range: at..at,
172                cursor,
173                edits_applied: 0,
174            };
175        }
176
177        EditBatchSummary {
178            changed_range: changed_start..changed_end,
179            cursor,
180            edits_applied: edits.len(),
181        }
182    }
183
184    /// Replace the current selection with `text` (if selection is empty, behaves like insert).
185    /// This is a convenience method that a bunch of editor actions can use.
186    ///
187    /// Returns an empty selection at the updated cursor.
188    pub fn replace_selection(&mut self, sel: Selection, text: &str) -> Selection {
189        if !sel.is_empty() {
190            let (start, end) = sel.ordered();
191            let cursor = self.delete_range(start, end);
192            let cursor = self.insert(cursor, text);
193            Selection::empty(cursor)
194        } else {
195            let cursor = self.insert(sel.cursor, text);
196            Selection::empty(cursor)
197        }
198    }
199
200    /// Paste text before the given cursor.
201    ///
202    /// When `linewise` is true, insertion happens at the start of the current line
203    /// and the returned cursor stays at that insertion point.
204    /// When `linewise` is false, insertion happens at the cursor and the returned
205    /// cursor is at the end of inserted text.
206    pub fn paste_before(&mut self, cursor: Pos, text: &str, linewise: bool) -> Pos {
207        let insert_pos = if linewise {
208            let line = self.clamp_line(cursor.line);
209            self.clamp_pos(Pos::new(line, 0))
210        } else {
211            self.clamp_pos(cursor)
212        };
213
214        let end_pos = self.insert(insert_pos, text);
215        if linewise { insert_pos } else { end_pos }
216    }
217
218    /// Paste text after the given cursor.
219    ///
220    /// When `linewise` is true, insertion happens at the beginning of the next
221    /// logical line (clamped at the buffer boundary), and the returned cursor
222    /// stays at the insertion point.
223    /// When `linewise` is false, insertion happens after the cursor char on the
224    /// current line (or at line end when already at EOL), and the returned cursor
225    /// is at the end of inserted text.
226    pub fn paste_after(&mut self, cursor: Pos, text: &str, linewise: bool) -> Pos {
227        let insert_pos = if linewise {
228            let line = self.clamp_line(cursor.line);
229            let target_line = (line + 1).min(self.len_lines());
230            self.clamp_pos(Pos::new(target_line, 0))
231        } else {
232            let line = self.clamp_line(cursor.line);
233            let line_len = self.line_len_chars(line);
234            let col = if cursor.col < line_len {
235                cursor.col.saturating_add(1)
236            } else {
237                line_len
238            };
239            Pos::new(line, col)
240        };
241
242        let end_pos = self.insert(insert_pos, text);
243        if linewise { insert_pos } else { end_pos }
244    }
245
246    /// Move a contiguous line range up by one line.
247    ///
248    /// Returns the moved range after the operation, or `None` when movement is
249    /// not possible (for example when the range already starts at line 0).
250    pub fn move_line_range_up_once(
251        &mut self,
252        start_line: usize,
253        end_line_inclusive: usize,
254    ) -> Option<(usize, usize)> {
255        let (start, end) = self.normalized_line_range(start_line, end_line_inclusive);
256        if start == 0 {
257            return None;
258        }
259
260        let first = start - 1;
261        let last = end;
262        let mut entries = self.collect_line_entries(first, last);
263        entries.rotate_left(1);
264        let mut replacement = entries.join("\n");
265        if last + 1 < self.len_lines() {
266            replacement.push('\n');
267        }
268
269        let replace_start = self.line_to_char(first);
270        let replace_end = self.line_full_end_char(last);
271        self.rope.remove(replace_start..replace_end);
272        self.rope.insert(replace_start, &replacement);
273
274        Some((start - 1, end - 1))
275    }
276
277    /// Move a contiguous line range up by up to `count` lines.
278    ///
279    /// Returns the moved range after all possible steps, or `None` when the range
280    /// cannot be moved up at all.
281    pub fn move_line_range_up(
282        &mut self,
283        start_line: usize,
284        end_line_inclusive: usize,
285        count: usize,
286    ) -> Option<(usize, usize)> {
287        if count == 0 {
288            return None;
289        }
290
291        let mut current = self.normalized_line_range(start_line, end_line_inclusive);
292        let mut moved = false;
293        for _ in 0..count {
294            let Some(next) = self.move_line_range_up_once(current.0, current.1) else {
295                break;
296            };
297            current = next;
298            moved = true;
299        }
300
301        if moved { Some(current) } else { None }
302    }
303
304    /// Move a contiguous line range down by one line.
305    ///
306    /// Returns the moved range after the operation, or `None` when movement is
307    /// not possible (for example when the range already ends at the final line).
308    pub fn move_line_range_down_once(
309        &mut self,
310        start_line: usize,
311        end_line_inclusive: usize,
312    ) -> Option<(usize, usize)> {
313        let (start, end) = self.normalized_line_range(start_line, end_line_inclusive);
314        if end + 1 >= self.len_lines() {
315            return None;
316        }
317
318        let first = start;
319        let last = end + 1;
320        let mut entries = self.collect_line_entries(first, last);
321        entries.rotate_right(1);
322        let mut replacement = entries.join("\n");
323        if last + 1 < self.len_lines() {
324            replacement.push('\n');
325        }
326
327        let replace_start = self.line_to_char(first);
328        let replace_end = self.line_full_end_char(last);
329        self.rope.remove(replace_start..replace_end);
330        self.rope.insert(replace_start, &replacement);
331
332        Some((start + 1, end + 1))
333    }
334
335    /// Move a contiguous line range down by up to `count` lines.
336    ///
337    /// Returns the moved range after all possible steps, or `None` when the range
338    /// cannot be moved down at all.
339    pub fn move_line_range_down(
340        &mut self,
341        start_line: usize,
342        end_line_inclusive: usize,
343        count: usize,
344    ) -> Option<(usize, usize)> {
345        if count == 0 {
346            return None;
347        }
348
349        let mut current = self.normalized_line_range(start_line, end_line_inclusive);
350        let mut moved = false;
351        for _ in 0..count {
352            let Some(next) = self.move_line_range_down_once(current.0, current.1) else {
353                break;
354            };
355            current = next;
356            moved = true;
357        }
358
359        if moved { Some(current) } else { None }
360    }
361
362    /// Indent each line in a contiguous span by `count` tab characters.
363    ///
364    /// Returns `(line, chars_added)` for every touched line.
365    pub fn indent_line_span(
366        &mut self,
367        start_line: usize,
368        end_line_inclusive: usize,
369        count: usize,
370    ) -> Vec<(usize, usize)> {
371        if count == 0 {
372            return Vec::new();
373        }
374
375        let (start, end) = self.normalized_line_range(start_line, end_line_inclusive);
376        let indent = "\t".repeat(count);
377        let mut added_by_line = Vec::with_capacity(end.saturating_sub(start) + 1);
378        for line in start..=end {
379            let _ = self.insert(Pos::new(line, 0), &indent);
380            added_by_line.push((line, count));
381        }
382        added_by_line
383    }
384
385    /// Outdent each line in a contiguous span by up to `count` levels.
386    ///
387    /// One outdent level removes either one leading tab or up to four leading spaces.
388    /// Returns `(line, chars_removed)` for every touched line.
389    pub fn outdent_line_span(
390        &mut self,
391        start_line: usize,
392        end_line_inclusive: usize,
393        count: usize,
394    ) -> Vec<(usize, usize)> {
395        const TAB_STOP: usize = 4;
396
397        if count == 0 {
398            return Vec::new();
399        }
400
401        let (start, end) = self.normalized_line_range(start_line, end_line_inclusive);
402        let mut removed_by_line = Vec::with_capacity(end.saturating_sub(start) + 1);
403
404        for line in start..=end {
405            let text = self.line_string(line);
406            let chars: Vec<char> = text.chars().collect();
407            let mut idx = 0usize;
408            let mut levels_left = count;
409            while levels_left > 0 && idx < chars.len() {
410                if chars[idx] == '\t' {
411                    idx += 1;
412                    levels_left -= 1;
413                    continue;
414                }
415
416                let mut spaces = 0usize;
417                while idx + spaces < chars.len() && chars[idx + spaces] == ' ' && spaces < TAB_STOP
418                {
419                    spaces += 1;
420                }
421                if spaces == 0 {
422                    break;
423                }
424
425                idx += spaces;
426                levels_left -= 1;
427            }
428
429            if idx > 0 {
430                let _ = self.delete_range(Pos::new(line, 0), Pos::new(line, idx));
431            }
432            removed_by_line.push((line, idx));
433        }
434
435        removed_by_line
436    }
437
438    fn normalized_line_range(
439        &self,
440        start_line: usize,
441        end_line_inclusive: usize,
442    ) -> (usize, usize) {
443        let (start, end) = if start_line <= end_line_inclusive {
444            (start_line, end_line_inclusive)
445        } else {
446            (end_line_inclusive, start_line)
447        };
448        let start = self.clamp_line(start);
449        let end = self.clamp_line(end.max(start));
450        (start, end)
451    }
452
453    fn collect_line_entries(&self, start_line: usize, end_line_inclusive: usize) -> Vec<String> {
454        let mut entries = Vec::with_capacity(end_line_inclusive.saturating_sub(start_line) + 1);
455        for line in start_line..=end_line_inclusive {
456            entries.push(self.line_string(line));
457        }
458        entries
459    }
460}