Skip to main content

datui_lib/widgets/textarea/
edit.rs

1//! Editing primitives and cursor movement for [`TextArea`].
2//!
3//! Every mutation funnels through [`TextArea::replace_range`], which swaps one
4//! contiguous range for one string and records the delta on the undo stack.
5//! Building each operation out of that single primitive is what keeps undo
6//! exact: the inverse of a replacement is another replacement.
7
8use super::cursor::{CursorMove, next_word_start, prev_word_start};
9use super::history::Edit;
10use super::{TextArea, byte_of_char, char_count, split_lines};
11
12impl TextArea {
13    /// Replace `[start, end)` with `text`, recording it for undo.
14    ///
15    /// Returns whether the buffer changed. The cursor lands at the end of the
16    /// inserted text.
17    fn replace_range(&mut self, start: (usize, usize), end: (usize, usize), text: &str) -> bool {
18        let start = self.clamp_position(start);
19        let end = self.clamp_position(end);
20        if start == end && text.is_empty() {
21            return false;
22        }
23        // Normalise first so that what is recorded for undo is exactly what
24        // lands in the buffer, even if the caller pasted CRLF line endings.
25        let inserted = split_lines(text).join("\n");
26        let cursor_before = self.cursor;
27        let removed = self.remove_raw(start, end);
28        let cursor_after = self.insert_raw(start, &inserted);
29        self.cursor = cursor_after;
30        self.selection_anchor = None;
31        self.history.push(Edit {
32            start,
33            removed,
34            inserted,
35            cursor_before,
36            cursor_after,
37        });
38        true
39    }
40
41    /// Delete `[start, end)` without touching the undo stack, returning the
42    /// text that was there.
43    fn remove_raw(&mut self, (sr, sc): (usize, usize), (er, ec): (usize, usize)) -> String {
44        if (sr, sc) == (er, ec) {
45            return String::new();
46        }
47        let sb = byte_of_char(&self.lines[sr], sc);
48        let eb = byte_of_char(&self.lines[er], ec);
49        if sr == er {
50            let removed = self.lines[sr][sb..eb].to_string();
51            self.lines[sr].replace_range(sb..eb, "");
52            return removed;
53        }
54        let mut removed = self.lines[sr][sb..].to_string();
55        for line in &self.lines[sr + 1..er] {
56            removed.push('\n');
57            removed.push_str(line);
58        }
59        removed.push('\n');
60        removed.push_str(&self.lines[er][..eb]);
61
62        let tail = self.lines[er][eb..].to_string();
63        self.lines[sr].truncate(sb);
64        self.lines[sr].push_str(&tail);
65        self.lines.drain(sr + 1..=er);
66        removed
67    }
68
69    /// Insert `text` at a position without touching the undo stack, returning
70    /// the position just past what was inserted.
71    fn insert_raw(&mut self, (row, col): (usize, usize), text: &str) -> (usize, usize) {
72        if text.is_empty() {
73            return (row, col);
74        }
75        let byte = byte_of_char(&self.lines[row], col);
76        let tail = self.lines[row].split_off(byte);
77        let mut chunks = split_lines(text);
78        let first = chunks.remove(0);
79        self.lines[row].push_str(&first);
80
81        if chunks.is_empty() {
82            let end = (row, col + char_count(&first));
83            self.lines[row].push_str(&tail);
84            return end;
85        }
86
87        let end_row = row + chunks.len();
88        let end_col = char_count(chunks.last().expect("chunks is non-empty"));
89        let last = chunks.len() - 1;
90        chunks[last].push_str(&tail);
91        for (offset, line) in chunks.into_iter().enumerate() {
92            self.lines.insert(row + 1 + offset, line);
93        }
94        (end_row, end_col)
95    }
96
97    /// The range an edit should operate on: the selection if there is one,
98    /// otherwise the empty range at the cursor.
99    fn edit_range(&self) -> ((usize, usize), (usize, usize)) {
100        self.selection().unwrap_or((self.cursor, self.cursor))
101    }
102
103    /// Insert one character, replacing the selection if there is one.
104    pub fn insert_char(&mut self, c: char) -> bool {
105        let mut buf = [0u8; 4];
106        let (start, end) = self.edit_range();
107        self.replace_range(start, end, c.encode_utf8(&mut buf))
108    }
109
110    /// Insert a string, replacing the selection if there is one. Newlines in
111    /// `text` split lines.
112    pub fn insert_str(&mut self, text: &str) -> bool {
113        let (start, end) = self.edit_range();
114        self.replace_range(start, end, text)
115    }
116
117    /// Replace the `count` characters before the cursor, on its line, with
118    /// `text`: a completion taking over the word typed so far.
119    pub fn replace_before_cursor(&mut self, count: usize, text: &str) -> bool {
120        let (row, col) = self.cursor;
121        self.replace_range((row, col.saturating_sub(count)), (row, col), text)
122    }
123
124    /// Split the current line at the cursor.
125    pub fn insert_newline(&mut self) -> bool {
126        let (start, end) = self.edit_range();
127        self.replace_range(start, end, "\n")
128    }
129
130    /// Insert spaces up to the next tab stop.
131    pub fn insert_tab(&mut self) -> bool {
132        let width = self.tab_len - (self.cursor.1 % self.tab_len);
133        let spaces = " ".repeat(width);
134        self.insert_str(&spaces)
135    }
136
137    /// Delete the selection, or the character before the cursor.
138    pub fn delete_prev_char(&mut self) -> bool {
139        if let Some((start, end)) = self.selection() {
140            return self.replace_range(start, end, "");
141        }
142        let Some(prev) = self.position_before(self.cursor) else {
143            return false;
144        };
145        self.replace_range(prev, self.cursor, "")
146    }
147
148    /// Delete the selection, or the character after the cursor.
149    pub fn delete_next_char(&mut self) -> bool {
150        if let Some((start, end)) = self.selection() {
151            return self.replace_range(start, end, "");
152        }
153        let Some(next) = self.position_after(self.cursor) else {
154            return false;
155        };
156        self.replace_range(self.cursor, next, "")
157    }
158
159    /// Delete backwards to the start of the previous word, yanking it.
160    pub fn delete_prev_word(&mut self) -> bool {
161        if let Some((start, end)) = self.selection() {
162            return self.yank_and_replace(start, end);
163        }
164        let (row, col) = self.cursor;
165        if col == 0 {
166            return self.delete_prev_char();
167        }
168        let chars: Vec<char> = self.lines[row].chars().collect();
169        let target = prev_word_start(&chars, col);
170        self.yank_and_replace((row, target), self.cursor)
171    }
172
173    /// Delete forwards to the start of the next word, yanking it.
174    pub fn delete_next_word(&mut self) -> bool {
175        if let Some((start, end)) = self.selection() {
176            return self.yank_and_replace(start, end);
177        }
178        let (row, col) = self.cursor;
179        let chars: Vec<char> = self.lines[row].chars().collect();
180        if col >= chars.len() {
181            return self.delete_next_char();
182        }
183        let target = next_word_start(&chars, col);
184        self.yank_and_replace(self.cursor, (row, target))
185    }
186
187    /// Delete from the cursor to the end of the line, yanking it. At the end of
188    /// a line this joins the line below, matching readline's `kill-line`. A
189    /// selection is killed instead, as the word kills do.
190    pub fn delete_line_by_end(&mut self) -> bool {
191        if let Some((start, end)) = self.selection() {
192            return self.yank_and_replace(start, end);
193        }
194        let (row, col) = self.cursor;
195        let len = self.line_len(row);
196        if col < len {
197            return self.yank_and_replace(self.cursor, (row, len));
198        }
199        if row + 1 < self.lines.len() {
200            return self.replace_range(self.cursor, (row + 1, 0), "");
201        }
202        false
203    }
204
205    /// Delete from the start of the line to the cursor, yanking it. At the
206    /// start of a line this joins onto the line above. A selection is killed
207    /// instead.
208    pub fn delete_line_by_head(&mut self) -> bool {
209        if let Some((start, end)) = self.selection() {
210            return self.yank_and_replace(start, end);
211        }
212        let (row, col) = self.cursor;
213        if col > 0 {
214            return self.yank_and_replace((row, 0), self.cursor);
215        }
216        if row > 0 {
217            let prev_len = self.line_len(row - 1);
218            return self.replace_range((row - 1, prev_len), self.cursor, "");
219        }
220        false
221    }
222
223    /// Delete the active selection, if any.
224    pub fn delete_selection(&mut self) -> bool {
225        match self.selection() {
226            Some((start, end)) => self.replace_range(start, end, ""),
227            None => false,
228        }
229    }
230
231    /// Copy the selection into the yank buffer.
232    pub fn copy(&mut self) -> bool {
233        let Some((start, end)) = self.selection() else {
234            return false;
235        };
236        self.yank = self.text_in_range(start, end);
237        self.selection_anchor = None;
238        true
239    }
240
241    /// Move the selection into the yank buffer.
242    pub fn cut(&mut self) -> bool {
243        match self.selection() {
244            Some((start, end)) => self.yank_and_replace(start, end),
245            None => false,
246        }
247    }
248
249    /// Insert the yank buffer at the cursor.
250    pub fn paste(&mut self) -> bool {
251        if self.yank.is_empty() {
252            return false;
253        }
254        let yank = std::mem::take(&mut self.yank);
255        let inserted = self.insert_str(&yank);
256        self.yank = yank;
257        inserted
258    }
259
260    /// Undo the most recent edit.
261    pub fn undo(&mut self) -> bool {
262        let Some(edit) = self.history.pop_undo() else {
263            return false;
264        };
265        let inverse = edit.inverted();
266        self.apply_without_recording(&inverse);
267        self.history.push_redo(edit);
268        true
269    }
270
271    /// Redo the most recently undone edit.
272    pub fn redo(&mut self) -> bool {
273        let Some(edit) = self.history.pop_redo() else {
274            return false;
275        };
276        self.apply_without_recording(&edit);
277        self.history.push_undo(edit);
278        true
279    }
280
281    fn apply_without_recording(&mut self, edit: &Edit) {
282        let end = self.position_advanced(edit.start, &edit.removed);
283        self.remove_raw(edit.start, end);
284        self.insert_raw(edit.start, &edit.inserted);
285        self.cursor = self.clamp_position(edit.cursor_after);
286        self.selection_anchor = None;
287    }
288
289    /// Where `text` ends if it starts at `from`.
290    fn position_advanced(&self, (row, col): (usize, usize), text: &str) -> (usize, usize) {
291        if text.is_empty() {
292            return (row, col);
293        }
294        let mut lines = text.split('\n');
295        let first = lines.next().unwrap_or("");
296        let rest: Vec<&str> = lines.collect();
297        if rest.is_empty() {
298            (row, col + char_count(first))
299        } else {
300            (
301                row + rest.len(),
302                char_count(rest.last().expect("rest is non-empty")),
303            )
304        }
305    }
306
307    fn yank_and_replace(&mut self, start: (usize, usize), end: (usize, usize)) -> bool {
308        let text = self.text_in_range(start, end);
309        if text.is_empty() {
310            return false;
311        }
312        self.yank = text;
313        self.replace_range(start, end, "")
314    }
315
316    /// The text between two positions.
317    fn text_in_range(&self, (sr, sc): (usize, usize), (er, ec): (usize, usize)) -> String {
318        if sr == er {
319            let line = &self.lines[sr];
320            let sb = byte_of_char(line, sc);
321            let eb = byte_of_char(line, ec);
322            return line[sb..eb].to_string();
323        }
324        let mut out = self.lines[sr][byte_of_char(&self.lines[sr], sc)..].to_string();
325        for line in &self.lines[sr + 1..er] {
326            out.push('\n');
327            out.push_str(line);
328        }
329        out.push('\n');
330        out.push_str(&self.lines[er][..byte_of_char(&self.lines[er], ec)]);
331        out
332    }
333
334    /// The position one character before `pos`, or `None` at the start of the
335    /// buffer.
336    fn position_before(&self, (row, col): (usize, usize)) -> Option<(usize, usize)> {
337        if col > 0 {
338            Some((row, col - 1))
339        } else if row > 0 {
340            Some((row - 1, self.line_len(row - 1)))
341        } else {
342            None
343        }
344    }
345
346    /// The position one character after `pos`, or `None` at the end of the
347    /// buffer.
348    fn position_after(&self, (row, col): (usize, usize)) -> Option<(usize, usize)> {
349        if col < self.line_len(row) {
350            Some((row, col + 1))
351        } else if row + 1 < self.lines.len() {
352            Some((row + 1, 0))
353        } else {
354            None
355        }
356    }
357
358    /// Move the cursor, dropping any selection.
359    pub fn move_cursor(&mut self, movement: CursorMove) -> bool {
360        self.move_cursor_inner(movement, false)
361    }
362
363    /// Move the cursor, extending the selection from where it was.
364    pub fn move_cursor_selecting(&mut self, movement: CursorMove) -> bool {
365        self.move_cursor_inner(movement, true)
366    }
367
368    fn move_cursor_inner(&mut self, movement: CursorMove, selecting: bool) -> bool {
369        if selecting {
370            if self.selection_anchor.is_none() {
371                self.selection_anchor = Some(self.cursor);
372            }
373        } else {
374            self.selection_anchor = None;
375        }
376        let Some(target) = self.position_for(movement) else {
377            return false;
378        };
379        let changed = target != self.cursor;
380        self.cursor = target;
381        changed
382    }
383
384    /// Resolve a movement to an absolute position, or `None` when it cannot be
385    /// made (already at the edge of the buffer).
386    fn position_for(&self, movement: CursorMove) -> Option<(usize, usize)> {
387        let (row, col) = self.cursor;
388        let page = self.viewport.get().height.max(1) as usize;
389        Some(match movement {
390            CursorMove::Forward => self.position_after((row, col))?,
391            CursorMove::Back => self.position_before((row, col))?,
392            CursorMove::Up if self.wraps_now() => self.visual_step(false)?,
393            CursorMove::Down if self.wraps_now() => self.visual_step(true)?,
394            CursorMove::Up => {
395                let target = row.checked_sub(1)?;
396                (target, col.min(self.line_len(target)))
397            }
398            CursorMove::Down => {
399                let target = row + 1;
400                if target >= self.lines.len() {
401                    return None;
402                }
403                (target, col.min(self.line_len(target)))
404            }
405            CursorMove::Head => (row, 0),
406            CursorMove::End => (row, self.line_len(row)),
407            CursorMove::Top => (0, 0),
408            CursorMove::Bottom => {
409                let last = self.lines.len() - 1;
410                (last, self.line_len(last))
411            }
412            CursorMove::WordForward => {
413                let chars: Vec<char> = self.lines[row].chars().collect();
414                if col >= chars.len() {
415                    self.position_after((row, col))?
416                } else {
417                    (row, next_word_start(&chars, col))
418                }
419            }
420            CursorMove::WordBack => {
421                if col == 0 {
422                    self.position_before((row, col))?
423                } else {
424                    let chars: Vec<char> = self.lines[row].chars().collect();
425                    (row, prev_word_start(&chars, col))
426                }
427            }
428            CursorMove::UpBy(n) => {
429                let n = if n == 0 { page } else { n };
430                let target = row.saturating_sub(n);
431                (target, col.min(self.line_len(target)))
432            }
433            CursorMove::DownBy(n) => {
434                let n = if n == 0 { page } else { n };
435                let target = (row + n).min(self.lines.len() - 1);
436                (target, col.min(self.line_len(target)))
437            }
438            CursorMove::Jump(r, c) => self.clamp_position((r, c)),
439        })
440    }
441}