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