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    #[cfg(test)]
225    pub fn delete_selection(&mut self) -> bool {
226        match self.selection() {
227            Some((start, end)) => self.replace_range(start, end, ""),
228            None => false,
229        }
230    }
231
232    /// Copy the selection into the yank buffer.
233    pub fn copy(&mut self) -> bool {
234        let Some((start, end)) = self.selection() else {
235            return false;
236        };
237        self.yank = self.text_in_range(start, end);
238        self.selection_anchor = None;
239        true
240    }
241
242    /// Move the selection into the yank buffer.
243    pub fn cut(&mut self) -> bool {
244        match self.selection() {
245            Some((start, end)) => self.yank_and_replace(start, end),
246            None => false,
247        }
248    }
249
250    /// Insert the yank buffer at the cursor.
251    pub fn paste(&mut self) -> bool {
252        if self.yank.is_empty() {
253            return false;
254        }
255        let yank = std::mem::take(&mut self.yank);
256        let inserted = self.insert_str(&yank);
257        self.yank = yank;
258        inserted
259    }
260
261    /// Undo the most recent edit.
262    pub fn undo(&mut self) -> bool {
263        let Some(edit) = self.history.pop_undo() else {
264            return false;
265        };
266        let inverse = edit.inverted();
267        self.apply_without_recording(&inverse);
268        self.history.push_redo(edit);
269        true
270    }
271
272    /// Redo the most recently undone edit.
273    pub fn redo(&mut self) -> bool {
274        let Some(edit) = self.history.pop_redo() else {
275            return false;
276        };
277        self.apply_without_recording(&edit);
278        self.history.push_undo(edit);
279        true
280    }
281
282    fn apply_without_recording(&mut self, edit: &Edit) {
283        let end = self.position_advanced(edit.start, &edit.removed);
284        self.remove_raw(edit.start, end);
285        self.insert_raw(edit.start, &edit.inserted);
286        self.cursor = self.clamp_position(edit.cursor_after);
287        self.selection_anchor = None;
288    }
289
290    /// Where `text` ends if it starts at `from`.
291    fn position_advanced(&self, (row, col): (usize, usize), text: &str) -> (usize, usize) {
292        if text.is_empty() {
293            return (row, col);
294        }
295        let mut lines = text.split('\n');
296        let first = lines.next().unwrap_or("");
297        let rest: Vec<&str> = lines.collect();
298        if rest.is_empty() {
299            (row, col + char_count(first))
300        } else {
301            (
302                row + rest.len(),
303                char_count(rest.last().expect("rest is non-empty")),
304            )
305        }
306    }
307
308    fn yank_and_replace(&mut self, start: (usize, usize), end: (usize, usize)) -> bool {
309        let text = self.text_in_range(start, end);
310        if text.is_empty() {
311            return false;
312        }
313        self.yank = text;
314        self.replace_range(start, end, "")
315    }
316
317    /// The text between two positions.
318    fn text_in_range(&self, (sr, sc): (usize, usize), (er, ec): (usize, usize)) -> String {
319        if sr == er {
320            let line = &self.lines[sr];
321            let sb = byte_of_char(line, sc);
322            let eb = byte_of_char(line, ec);
323            return line[sb..eb].to_string();
324        }
325        let mut out = self.lines[sr][byte_of_char(&self.lines[sr], sc)..].to_string();
326        for line in &self.lines[sr + 1..er] {
327            out.push('\n');
328            out.push_str(line);
329        }
330        out.push('\n');
331        out.push_str(&self.lines[er][..byte_of_char(&self.lines[er], ec)]);
332        out
333    }
334
335    /// The position one character before `pos`, or `None` at the start of the
336    /// buffer.
337    fn position_before(&self, (row, col): (usize, usize)) -> Option<(usize, usize)> {
338        if col > 0 {
339            Some((row, col - 1))
340        } else if row > 0 {
341            Some((row - 1, self.line_len(row - 1)))
342        } else {
343            None
344        }
345    }
346
347    /// The position one character after `pos`, or `None` at the end of the
348    /// buffer.
349    fn position_after(&self, (row, col): (usize, usize)) -> Option<(usize, usize)> {
350        if col < self.line_len(row) {
351            Some((row, col + 1))
352        } else if row + 1 < self.lines.len() {
353            Some((row + 1, 0))
354        } else {
355            None
356        }
357    }
358
359    /// Move the cursor, dropping any selection.
360    pub fn move_cursor(&mut self, movement: CursorMove) -> bool {
361        self.move_cursor_inner(movement, false)
362    }
363
364    /// Move the cursor, extending the selection from where it was.
365    pub fn move_cursor_selecting(&mut self, movement: CursorMove) -> bool {
366        self.move_cursor_inner(movement, true)
367    }
368
369    fn move_cursor_inner(&mut self, movement: CursorMove, selecting: bool) -> bool {
370        if selecting {
371            if self.selection_anchor.is_none() {
372                self.selection_anchor = Some(self.cursor);
373            }
374        } else {
375            self.selection_anchor = None;
376        }
377        let Some(target) = self.position_for(movement) else {
378            return false;
379        };
380        let changed = target != self.cursor;
381        self.cursor = target;
382        changed
383    }
384
385    /// Resolve a movement to an absolute position, or `None` when it cannot be
386    /// made (already at the edge of the buffer).
387    fn position_for(&self, movement: CursorMove) -> Option<(usize, usize)> {
388        let (row, col) = self.cursor;
389        let page = self.viewport.get().height.max(1) as usize;
390        Some(match movement {
391            CursorMove::Forward => self.position_after((row, col))?,
392            CursorMove::Back => self.position_before((row, col))?,
393            CursorMove::Up if self.wraps_now() => self.visual_step(false)?,
394            CursorMove::Down if self.wraps_now() => self.visual_step(true)?,
395            CursorMove::Up => {
396                let target = row.checked_sub(1)?;
397                (target, col.min(self.line_len(target)))
398            }
399            CursorMove::Down => {
400                let target = row + 1;
401                if target >= self.lines.len() {
402                    return None;
403                }
404                (target, col.min(self.line_len(target)))
405            }
406            CursorMove::Head => (row, 0),
407            CursorMove::End => (row, self.line_len(row)),
408            CursorMove::Top => (0, 0),
409            CursorMove::Bottom => {
410                let last = self.lines.len() - 1;
411                (last, self.line_len(last))
412            }
413            CursorMove::WordForward => {
414                let chars: Vec<char> = self.lines[row].chars().collect();
415                if col >= chars.len() {
416                    self.position_after((row, col))?
417                } else {
418                    (row, next_word_start(&chars, col))
419                }
420            }
421            CursorMove::WordBack => {
422                if col == 0 {
423                    self.position_before((row, col))?
424                } else {
425                    let chars: Vec<char> = self.lines[row].chars().collect();
426                    (row, prev_word_start(&chars, col))
427                }
428            }
429            CursorMove::UpBy(n) => {
430                let n = if n == 0 { page } else { n };
431                let target = row.saturating_sub(n);
432                (target, col.min(self.line_len(target)))
433            }
434            CursorMove::DownBy(n) => {
435                let n = if n == 0 { page } else { n };
436                let target = (row + n).min(self.lines.len() - 1);
437                (target, col.min(self.line_len(target)))
438            }
439            #[cfg(test)]
440            CursorMove::Jump(r, c) => self.clamp_position((r, c)),
441        })
442    }
443}