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 stays at that insertion point.
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 { insert_pos } else { end_pos }
257    }
258
259    /// Paste text after the given cursor.
260    ///
261    /// When `linewise` is true, insertion happens at the beginning of the next
262    /// logical line (clamped at the buffer boundary), and the returned cursor
263    /// stays at the insertion point.
264    /// When `linewise` is false, insertion happens after the cursor char on the
265    /// current line (or at line end when already at EOL), and the returned cursor
266    /// is at the end of inserted text.
267    pub fn paste_after(&mut self, cursor: Pos, text: &str, linewise: bool) -> Pos {
268        let insert_pos = if linewise {
269            let line = self.clamp_line(cursor.line);
270            let target_line = (line + 1).min(self.len_lines());
271            self.clamp_pos(Pos::new(target_line, 0))
272        } else {
273            let line = self.clamp_line(cursor.line);
274            let line_len = self.line_len_chars(line);
275            let col = if cursor.col < line_len {
276                cursor.col.saturating_add(1)
277            } else {
278                line_len
279            };
280            Pos::new(line, col)
281        };
282
283        let end_pos = self.insert(insert_pos, text);
284        if linewise { insert_pos } else { end_pos }
285    }
286
287    /// Move a contiguous line range up by one line.
288    ///
289    /// Returns the moved range after the operation, or `None` when movement is
290    /// not possible (for example when the range already starts at line 0).
291    pub fn move_line_range_up_once(
292        &mut self,
293        start_line: usize,
294        end_line_inclusive: usize,
295    ) -> Option<(usize, usize)> {
296        let (start, end) = self.normalized_line_range(start_line, end_line_inclusive);
297        if start == 0 {
298            return None;
299        }
300
301        let first = start - 1;
302        let last = end;
303        let mut entries = self.collect_line_entries(first, last);
304        entries.rotate_left(1);
305        let mut replacement = entries.join("\n");
306        if last + 1 < self.len_lines() {
307            replacement.push('\n');
308        }
309
310        let replace_start = self.line_to_char(first);
311        let replace_end = self.line_full_end_char(last);
312        self.rope.remove(replace_start..replace_end);
313        self.rope.insert(replace_start, &replacement);
314
315        Some((start - 1, end - 1))
316    }
317
318    /// Move a contiguous line range up by up to `count` lines.
319    ///
320    /// Returns the moved range after all possible steps, or `None` when the range
321    /// cannot be moved up at all.
322    pub fn move_line_range_up(
323        &mut self,
324        start_line: usize,
325        end_line_inclusive: usize,
326        count: usize,
327    ) -> Option<(usize, usize)> {
328        if count == 0 {
329            return None;
330        }
331
332        let mut current = self.normalized_line_range(start_line, end_line_inclusive);
333        let mut moved = false;
334        for _ in 0..count {
335            let Some(next) = self.move_line_range_up_once(current.0, current.1) else {
336                break;
337            };
338            current = next;
339            moved = true;
340        }
341
342        if moved { Some(current) } else { None }
343    }
344
345    /// Move a contiguous line range down by one line.
346    ///
347    /// Returns the moved range after the operation, or `None` when movement is
348    /// not possible (for example when the range already ends at the final line).
349    pub fn move_line_range_down_once(
350        &mut self,
351        start_line: usize,
352        end_line_inclusive: usize,
353    ) -> Option<(usize, usize)> {
354        let (start, end) = self.normalized_line_range(start_line, end_line_inclusive);
355        if end + 1 >= self.len_lines() {
356            return None;
357        }
358
359        let first = start;
360        let last = end + 1;
361        let mut entries = self.collect_line_entries(first, last);
362        entries.rotate_right(1);
363        let mut replacement = entries.join("\n");
364        if last + 1 < self.len_lines() {
365            replacement.push('\n');
366        }
367
368        let replace_start = self.line_to_char(first);
369        let replace_end = self.line_full_end_char(last);
370        self.rope.remove(replace_start..replace_end);
371        self.rope.insert(replace_start, &replacement);
372
373        Some((start + 1, end + 1))
374    }
375
376    /// Move a contiguous line range down by up to `count` lines.
377    ///
378    /// Returns the moved range after all possible steps, or `None` when the range
379    /// cannot be moved down at all.
380    pub fn move_line_range_down(
381        &mut self,
382        start_line: usize,
383        end_line_inclusive: usize,
384        count: usize,
385    ) -> Option<(usize, usize)> {
386        if count == 0 {
387            return None;
388        }
389
390        let mut current = self.normalized_line_range(start_line, end_line_inclusive);
391        let mut moved = false;
392        for _ in 0..count {
393            let Some(next) = self.move_line_range_down_once(current.0, current.1) else {
394                break;
395            };
396            current = next;
397            moved = true;
398        }
399
400        if moved { Some(current) } else { None }
401    }
402
403    /// Indent each line in a contiguous span by `count` tab characters.
404    ///
405    /// Returns `(line, chars_added)` for every touched line.
406    pub fn indent_line_span(
407        &mut self,
408        start_line: usize,
409        end_line_inclusive: usize,
410        count: usize,
411    ) -> Vec<(usize, usize)> {
412        if count == 0 {
413            return Vec::new();
414        }
415
416        let (start, end) = self.normalized_line_range(start_line, end_line_inclusive);
417        let indent = "\t".repeat(count);
418        let mut added_by_line = Vec::with_capacity(end.saturating_sub(start) + 1);
419        for line in start..=end {
420            let _ = self.insert(Pos::new(line, 0), &indent);
421            added_by_line.push((line, count));
422        }
423        added_by_line
424    }
425
426    /// Outdent each line in a contiguous span by up to `count` levels.
427    ///
428    /// One outdent level removes either one leading tab or up to four leading spaces.
429    /// Returns `(line, chars_removed)` for every touched line.
430    pub fn outdent_line_span(
431        &mut self,
432        start_line: usize,
433        end_line_inclusive: usize,
434        count: usize,
435    ) -> Vec<(usize, usize)> {
436        const TAB_STOP: usize = 4;
437
438        if count == 0 {
439            return Vec::new();
440        }
441
442        let (start, end) = self.normalized_line_range(start_line, end_line_inclusive);
443        let mut removed_by_line = Vec::with_capacity(end.saturating_sub(start) + 1);
444
445        for line in start..=end {
446            let text = self.line_string(line);
447            let chars: Vec<char> = text.chars().collect();
448            let mut idx = 0usize;
449            let mut levels_left = count;
450            while levels_left > 0 && idx < chars.len() {
451                if chars[idx] == '\t' {
452                    idx += 1;
453                    levels_left -= 1;
454                    continue;
455                }
456
457                let mut spaces = 0usize;
458                while idx + spaces < chars.len() && chars[idx + spaces] == ' ' && spaces < TAB_STOP
459                {
460                    spaces += 1;
461                }
462                if spaces == 0 {
463                    break;
464                }
465
466                idx += spaces;
467                levels_left -= 1;
468            }
469
470            if idx > 0 {
471                let _ = self.delete_range(Pos::new(line, 0), Pos::new(line, idx));
472            }
473            removed_by_line.push((line, idx));
474        }
475
476        removed_by_line
477    }
478
479    fn normalized_line_range(
480        &self,
481        start_line: usize,
482        end_line_inclusive: usize,
483    ) -> (usize, usize) {
484        let (start, end) = if start_line <= end_line_inclusive {
485            (start_line, end_line_inclusive)
486        } else {
487            (end_line_inclusive, start_line)
488        };
489        let start = self.clamp_line(start);
490        let end = self.clamp_line(end.max(start));
491        (start, end)
492    }
493
494    fn collect_line_entries(&self, start_line: usize, end_line_inclusive: usize) -> Vec<String> {
495        let mut entries = Vec::with_capacity(end_line_inclusive.saturating_sub(start_line) + 1);
496        for line in start_line..=end_line_inclusive {
497            entries.push(self.line_string(line));
498        }
499        entries
500    }
501}