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    /// Replace the leading whitespace on `line`.
117    ///
118    /// Returns `(removed_chars, added_chars)` when the line changed.
119    pub fn replace_line_indent(&mut self, line: usize, indent: &str) -> Option<(usize, usize)> {
120        let line = self.clamp_line(line);
121        let text = self.line_string(line);
122        let existing_chars = text
123            .chars()
124            .take_while(|ch| *ch == ' ' || *ch == '\t')
125            .count();
126        let existing: String = text.chars().take(existing_chars).collect();
127        if existing == indent {
128            return None;
129        }
130
131        let _ = self.delete_range(Pos::new(line, 0), Pos::new(line, existing_chars));
132        let _ = self.insert(Pos::new(line, 0), indent);
133        Some((existing_chars, indent.chars().count()))
134    }
135
136    /// Remove trailing spaces and tabs from every line in the buffer.
137    ///
138    /// Returns `true` when at least one line was changed.
139    pub fn trim_trailing_whitespace(&mut self) -> bool {
140        let mut changed = false;
141
142        for line in (0..self.len_lines()).rev() {
143            let text = self.line_string(line);
144            let line_len = text.chars().count();
145            let trimmed_len = text.trim_end_matches([' ', '\t']).chars().count();
146            if trimmed_len == line_len {
147                continue;
148            }
149
150            let _ = self.delete_range(Pos::new(line, trimmed_len), Pos::new(line, line_len));
151            changed = true;
152        }
153
154        changed
155    }
156
157    /// Apply an `Edit` expressed in char indices.
158    ///
159    /// Returns the resulting cursor position (end of inserted text, or start of deletion).
160    pub fn apply_edit(&mut self, edit: Edit) -> Pos {
161        let maxc = self.len_chars();
162        let start = edit.range.start.min(maxc);
163        let end = edit.range.end.min(maxc);
164        let (start, end) = if start <= end {
165            (start, end)
166        } else {
167            (end, start)
168        };
169
170        if start < end {
171            self.rope.remove(start..end);
172        }
173
174        if !edit.insert.is_empty() {
175            self.rope.insert(start, &edit.insert);
176            let inserted_chars = edit.insert.chars().count();
177            self.char_to_pos(start + inserted_chars)
178        } else {
179            self.char_to_pos(start)
180        }
181    }
182
183    /// Apply multiple edits sequentially and return a transaction-style summary.
184    ///
185    /// Edits are applied in input order against the current buffer state.
186    pub fn apply_edits(&mut self, edits: &[Edit]) -> EditBatchSummary {
187        let mut changed_start = usize::MAX;
188        let mut changed_end = 0usize;
189        let mut cursor = self.char_to_pos(self.len_chars());
190
191        for edit in edits {
192            let maxc = self.len_chars();
193            let start = edit.range.start.min(maxc);
194            let end = edit.range.end.min(maxc);
195            let (start, _) = if start <= end {
196                (start, end)
197            } else {
198                (end, start)
199            };
200
201            cursor = self.apply_edit(edit.clone());
202            let cursor_char = self.pos_to_char(cursor);
203
204            changed_start = changed_start.min(start);
205            changed_end = changed_end.max(cursor_char.max(start));
206        }
207
208        if edits.is_empty() {
209            let cursor = self.char_to_pos(self.len_chars());
210            let at = self.pos_to_char(cursor);
211            return EditBatchSummary {
212                changed_range: at..at,
213                cursor,
214                edits_applied: 0,
215            };
216        }
217
218        EditBatchSummary {
219            changed_range: changed_start..changed_end,
220            cursor,
221            edits_applied: edits.len(),
222        }
223    }
224
225    /// Replace the current selection with `text` (if selection is empty, behaves like insert).
226    /// This is a convenience method that a bunch of editor actions can use.
227    ///
228    /// Returns an empty selection at the updated cursor.
229    pub fn replace_selection(&mut self, sel: Selection, text: &str) -> Selection {
230        if !sel.is_empty() {
231            let (start, end) = sel.ordered();
232            let cursor = self.delete_range(start, end);
233            let cursor = self.insert(cursor, text);
234            Selection::empty(cursor)
235        } else {
236            let cursor = self.insert(sel.cursor, text);
237            Selection::empty(cursor)
238        }
239    }
240
241    /// Paste text before the given cursor.
242    ///
243    /// When `linewise` is true, insertion happens at the start of the current line
244    /// and the returned cursor moves to the end of inserted text.
245    /// When `linewise` is false, insertion happens at the cursor and the returned
246    /// cursor is at the end of inserted text.
247    pub fn paste_before(&mut self, cursor: Pos, text: &str, linewise: bool) -> Pos {
248        let insert_pos = if linewise {
249            let line = self.clamp_line(cursor.line);
250            self.clamp_pos(Pos::new(line, 0))
251        } else {
252            self.clamp_pos(cursor)
253        };
254
255        let end_pos = self.insert(insert_pos, text);
256        if linewise {
257            linewise_paste_cursor(end_pos)
258        } else {
259            end_pos
260        }
261    }
262
263    /// Paste text after the given cursor.
264    ///
265    /// When `linewise` is true, insertion happens at the beginning of the next
266    /// logical line (clamped at the buffer boundary), and the returned cursor
267    /// moves to the end of inserted text.
268    /// When `linewise` is false, insertion happens after the cursor char on the
269    /// current line (or at line end when already at EOL), and the returned cursor
270    /// is at the end of inserted text.
271    pub fn paste_after(&mut self, cursor: Pos, text: &str, linewise: bool) -> Pos {
272        let insert_pos = if linewise {
273            let line = self.clamp_line(cursor.line);
274            let target_line = (line + 1).min(self.len_lines());
275            self.clamp_pos(Pos::new(target_line, 0))
276        } else {
277            let line = self.clamp_line(cursor.line);
278            let line_len = self.line_len_chars(line);
279            let col = if cursor.col < line_len {
280                cursor.col.saturating_add(1)
281            } else {
282                line_len
283            };
284            Pos::new(line, col)
285        };
286
287        let end_pos = self.insert(insert_pos, text);
288        if linewise {
289            linewise_paste_cursor(end_pos)
290        } else {
291            end_pos
292        }
293    }
294
295    /// Move a contiguous line range up by one line.
296    ///
297    /// Returns the moved range after the operation, or `None` when movement is
298    /// not possible (for example when the range already starts at line 0).
299    pub fn move_line_range_up_once(
300        &mut self,
301        start_line: usize,
302        end_line_inclusive: usize,
303    ) -> Option<(usize, usize)> {
304        let (start, end) = self.normalized_line_range(start_line, end_line_inclusive);
305        if start == 0 {
306            return None;
307        }
308
309        let first = start - 1;
310        let last = end;
311        let mut entries = self.collect_line_entries(first, last);
312        entries.rotate_left(1);
313        let mut replacement = entries.join("\n");
314        if last + 1 < self.len_lines() {
315            replacement.push('\n');
316        }
317
318        let replace_start = self.line_to_char(first);
319        let replace_end = self.line_full_end_char(last);
320        self.rope.remove(replace_start..replace_end);
321        self.rope.insert(replace_start, &replacement);
322
323        Some((start - 1, end - 1))
324    }
325
326    /// Move a contiguous line range up by up to `count` lines.
327    ///
328    /// Returns the moved range after all possible steps, or `None` when the range
329    /// cannot be moved up at all.
330    pub fn move_line_range_up(
331        &mut self,
332        start_line: usize,
333        end_line_inclusive: usize,
334        count: usize,
335    ) -> Option<(usize, usize)> {
336        if count == 0 {
337            return None;
338        }
339
340        let mut current = self.normalized_line_range(start_line, end_line_inclusive);
341        let mut moved = false;
342        for _ in 0..count {
343            let Some(next) = self.move_line_range_up_once(current.0, current.1) else {
344                break;
345            };
346            current = next;
347            moved = true;
348        }
349
350        if moved { Some(current) } else { None }
351    }
352
353    /// Move a contiguous line range down by one line.
354    ///
355    /// Returns the moved range after the operation, or `None` when movement is
356    /// not possible (for example when the range already ends at the final line).
357    pub fn move_line_range_down_once(
358        &mut self,
359        start_line: usize,
360        end_line_inclusive: usize,
361    ) -> Option<(usize, usize)> {
362        let (start, end) = self.normalized_line_range(start_line, end_line_inclusive);
363        if end + 1 >= self.len_lines() {
364            return None;
365        }
366
367        let first = start;
368        let last = end + 1;
369        let mut entries = self.collect_line_entries(first, last);
370        entries.rotate_right(1);
371        let mut replacement = entries.join("\n");
372        if last + 1 < self.len_lines() {
373            replacement.push('\n');
374        }
375
376        let replace_start = self.line_to_char(first);
377        let replace_end = self.line_full_end_char(last);
378        self.rope.remove(replace_start..replace_end);
379        self.rope.insert(replace_start, &replacement);
380
381        Some((start + 1, end + 1))
382    }
383
384    /// Move a contiguous line range down by up to `count` lines.
385    ///
386    /// Returns the moved range after all possible steps, or `None` when the range
387    /// cannot be moved down at all.
388    pub fn move_line_range_down(
389        &mut self,
390        start_line: usize,
391        end_line_inclusive: usize,
392        count: usize,
393    ) -> Option<(usize, usize)> {
394        if count == 0 {
395            return None;
396        }
397
398        let mut current = self.normalized_line_range(start_line, end_line_inclusive);
399        let mut moved = false;
400        for _ in 0..count {
401            let Some(next) = self.move_line_range_down_once(current.0, current.1) else {
402                break;
403            };
404            current = next;
405            moved = true;
406        }
407
408        if moved { Some(current) } else { None }
409    }
410
411    /// Indent each line in a contiguous span by `count` tab characters.
412    ///
413    /// Returns `(line, chars_added)` for every touched line.
414    pub fn indent_line_span(
415        &mut self,
416        start_line: usize,
417        end_line_inclusive: usize,
418        count: usize,
419    ) -> Vec<(usize, usize)> {
420        if count == 0 {
421            return Vec::new();
422        }
423
424        let (start, end) = self.normalized_line_range(start_line, end_line_inclusive);
425        let indent = "\t".repeat(count);
426        let mut added_by_line = Vec::with_capacity(end.saturating_sub(start) + 1);
427        for line in start..=end {
428            let _ = self.insert(Pos::new(line, 0), &indent);
429            added_by_line.push((line, count));
430        }
431        added_by_line
432    }
433
434    /// Outdent each line in a contiguous span by up to `count` levels.
435    ///
436    /// One outdent level removes either one leading tab or up to four leading spaces.
437    /// Returns `(line, chars_removed)` for every touched line.
438    pub fn outdent_line_span(
439        &mut self,
440        start_line: usize,
441        end_line_inclusive: usize,
442        count: usize,
443    ) -> Vec<(usize, usize)> {
444        const TAB_STOP: usize = 4;
445
446        if count == 0 {
447            return Vec::new();
448        }
449
450        let (start, end) = self.normalized_line_range(start_line, end_line_inclusive);
451        let mut removed_by_line = Vec::with_capacity(end.saturating_sub(start) + 1);
452
453        for line in start..=end {
454            let text = self.line_string(line);
455            let chars: Vec<char> = text.chars().collect();
456            let mut idx = 0usize;
457            let mut levels_left = count;
458            while levels_left > 0 && idx < chars.len() {
459                if chars[idx] == '\t' {
460                    idx += 1;
461                    levels_left -= 1;
462                    continue;
463                }
464
465                let mut spaces = 0usize;
466                while idx + spaces < chars.len() && chars[idx + spaces] == ' ' && spaces < TAB_STOP
467                {
468                    spaces += 1;
469                }
470                if spaces == 0 {
471                    break;
472                }
473
474                idx += spaces;
475                levels_left -= 1;
476            }
477
478            if idx > 0 {
479                let _ = self.delete_range(Pos::new(line, 0), Pos::new(line, idx));
480            }
481            removed_by_line.push((line, idx));
482        }
483
484        removed_by_line
485    }
486
487    fn normalized_line_range(
488        &self,
489        start_line: usize,
490        end_line_inclusive: usize,
491    ) -> (usize, usize) {
492        let (start, end) = if start_line <= end_line_inclusive {
493            (start_line, end_line_inclusive)
494        } else {
495            (end_line_inclusive, start_line)
496        };
497        let start = self.clamp_line(start);
498        let end = self.clamp_line(end.max(start));
499        (start, end)
500    }
501
502    fn collect_line_entries(&self, start_line: usize, end_line_inclusive: usize) -> Vec<String> {
503        let mut entries = Vec::with_capacity(end_line_inclusive.saturating_sub(start_line) + 1);
504        for line in start_line..=end_line_inclusive {
505            entries.push(self.line_string(line));
506        }
507        entries
508    }
509}
510
511fn linewise_paste_cursor(end_pos: Pos) -> Pos {
512    Pos::new(end_pos.line.saturating_sub(1), 0)
513}