Skip to main content

repl_lang/
editor.rs

1//! A terminal-agnostic single-line editor with history and a kill buffer.
2
3use alloc::{collections::VecDeque, string::String};
4
5use crate::{
6    Edit,
7    text::{next_unit, prev_unit, text_width, word_left, word_right},
8};
9
10/// The editing state of one input line: its text, the cursor, a kill buffer,
11/// and a bounded history of submitted lines.
12///
13/// `Editor` is the "line editing" part of a REPL with the terminal taken out.
14/// It holds no file descriptors and draws nothing. The host translates key
15/// events into [`Edit`] commands, calls [`apply`](Editor::apply), and redraws
16/// from [`line`](Editor::line) and [`column`](Editor::column) when `apply`
17/// reports a change. On Enter it calls [`submit`](Editor::submit), which hands
18/// back the finished line and records it in the history. That split makes the
19/// editor identical on every platform and testable without a terminal.
20///
21/// Positions are byte offsets into [`line`](Editor::line) and always fall on a
22/// `char` boundary, so slicing the line at [`cursor`](Editor::cursor) is
23/// always valid. The cursor moves in display units (a visible character plus
24/// any combining marks attached to it), so it never separates a letter from its
25/// accent.
26///
27/// # Memory
28///
29/// The line, the kill buffer, and every history slot are reused rather than
30/// reallocated. Once the history has filled to capacity, submitting a line
31/// recycles the oldest slot's buffer, so a long session settles into editing
32/// and submitting without touching the allocator except to grow a buffer past
33/// its previous high-water mark.
34///
35/// # Examples
36///
37/// ```
38/// use repl_lang::{Edit, Editor};
39///
40/// let mut editor = Editor::new();
41/// editor.insert("let x = 1");
42/// assert_eq!(editor.submit(), "let x = 1");
43/// assert_eq!(editor.line(), "");
44///
45/// // The submitted line is one step back in the history.
46/// assert!(editor.apply(Edit::HistoryPrev));
47/// assert_eq!(editor.line(), "let x = 1");
48/// ```
49#[derive(Clone, Debug)]
50pub struct Editor {
51    /// The line being edited.
52    line: String,
53    /// Byte offset of the cursor in `line`; always on a `char` boundary.
54    cursor: usize,
55    /// Text removed by the last kill command, for `Yank`.
56    kill: String,
57    /// The last line returned by `submit`; its buffer becomes the next line's.
58    submitted: String,
59    /// Submitted lines, oldest first.
60    history: VecDeque<String>,
61    /// Maximum number of history entries kept.
62    capacity: usize,
63    /// Index into `history` while browsing it with `HistoryPrev`/`HistoryNext`.
64    browsing: Option<usize>,
65    /// The entry being shown was the oldest and has since been dropped to make
66    /// room, so `browsing` is `Some(0)` but the shown entry sits just before
67    /// index 0: `HistoryNext` goes to index 0 and `HistoryPrev` has nowhere
68    /// to go.
69    shown_dropped: bool,
70    /// The line being typed when browsing started, restored when browsing ends.
71    draft: String,
72}
73
74impl Default for Editor {
75    #[inline]
76    fn default() -> Self {
77        Self::new()
78    }
79}
80
81impl Editor {
82    /// The number of history entries [`Editor::new`] keeps.
83    pub const DEFAULT_HISTORY: usize = 1000;
84
85    /// Creates an empty editor that keeps the last
86    /// [`DEFAULT_HISTORY`](Editor::DEFAULT_HISTORY) submitted lines.
87    ///
88    /// # Examples
89    ///
90    /// ```
91    /// use repl_lang::Editor;
92    ///
93    /// let editor = Editor::new();
94    /// assert_eq!(editor.line(), "");
95    /// assert_eq!(editor.cursor(), 0);
96    /// assert_eq!(editor.history().len(), 0);
97    /// ```
98    #[must_use]
99    pub fn new() -> Self {
100        Self::with_history(Self::DEFAULT_HISTORY)
101    }
102
103    /// Creates an empty editor that keeps at most `capacity` history entries.
104    ///
105    /// When the history is full, submitting a line drops the oldest entry. A
106    /// capacity of `0` disables history: nothing is recorded and the history
107    /// commands do nothing. No history memory is reserved up front; slots are
108    /// allocated as lines are submitted, then recycled once the limit is
109    /// reached.
110    ///
111    /// # Examples
112    ///
113    /// ```
114    /// use repl_lang::Editor;
115    ///
116    /// let mut editor = Editor::with_history(2);
117    /// for line in ["a", "b", "c"] {
118    ///     editor.insert(line);
119    ///     editor.submit();
120    /// }
121    /// // Only the two most recent lines are kept, oldest first.
122    /// assert!(editor.history().eq(["b", "c"]));
123    /// ```
124    #[must_use]
125    pub fn with_history(capacity: usize) -> Self {
126        Self {
127            line: String::new(),
128            cursor: 0,
129            kill: String::new(),
130            submitted: String::new(),
131            history: VecDeque::new(),
132            capacity,
133            browsing: None,
134            shown_dropped: false,
135            draft: String::new(),
136        }
137    }
138
139    /// The text of the line being edited.
140    ///
141    /// # Examples
142    ///
143    /// ```
144    /// use repl_lang::Editor;
145    ///
146    /// let mut editor = Editor::new();
147    /// editor.insert("1 + 2");
148    /// assert_eq!(editor.line(), "1 + 2");
149    /// ```
150    #[inline]
151    #[must_use]
152    pub fn line(&self) -> &str {
153        &self.line
154    }
155
156    /// The cursor position as a byte offset into [`line`](Editor::line).
157    ///
158    /// The offset is always on a `char` boundary, so `&line[..cursor]` and
159    /// `&line[cursor..]` are always valid. Use it to find the word under the
160    /// cursor for completion; use [`column`](Editor::column) to place the
161    /// terminal cursor.
162    ///
163    /// # Examples
164    ///
165    /// ```
166    /// use repl_lang::{Edit, Editor};
167    ///
168    /// let mut editor = Editor::new();
169    /// editor.insert("pri");
170    /// assert_eq!(editor.cursor(), 3);
171    ///
172    /// // The text before the cursor is the prefix to complete.
173    /// let prefix = &editor.line()[..editor.cursor()];
174    /// assert_eq!(prefix, "pri");
175    ///
176    /// assert!(editor.apply(Edit::Home));
177    /// assert_eq!(editor.cursor(), 0);
178    /// ```
179    #[inline]
180    #[must_use]
181    pub fn cursor(&self) -> usize {
182        self.cursor
183    }
184
185    /// The cursor's position in terminal columns, counted from the start of the
186    /// line.
187    ///
188    /// This is the display width of the text before the cursor: wide characters
189    /// (CJK ideographs, most emoji) count two columns and combining marks count
190    /// none. After drawing the prompt and the line, a host moves the terminal
191    /// cursor to `prompt_width + column()`.
192    ///
193    /// The width is computed on each call, in time linear in the text before
194    /// the cursor.
195    ///
196    /// # Examples
197    ///
198    /// ```
199    /// use repl_lang::Editor;
200    ///
201    /// let mut editor = Editor::new();
202    /// editor.insert("x = 世界");
203    /// assert_eq!(editor.cursor(), 10); // bytes: 4 ASCII + 2 × 3
204    /// assert_eq!(editor.column(), 8);  // columns: 4 ASCII + 2 × 2
205    /// ```
206    #[must_use]
207    pub fn column(&self) -> usize {
208        let before = &self.line[..self.cursor];
209        // A recalled multi-line entry: count from the start of the cursor's
210        // own line.
211        let start = before.rfind('\n').map_or(0, |at| at + 1);
212        text_width(&before[start..])
213    }
214
215    /// Applies one editing command and reports whether the line or the cursor
216    /// changed.
217    ///
218    /// A command that cannot do anything — moving left at the start of the
219    /// line, deleting at the end, yanking an empty kill buffer, inserting a
220    /// control or bidirectional formatting character — leaves the editor untouched and returns `false`, so
221    /// the host can skip a redraw or ring the bell.
222    ///
223    /// The kill commands replace the kill buffer with the text they remove;
224    /// a kill that removes nothing leaves the buffer as it was.
225    ///
226    /// # Examples
227    ///
228    /// ```
229    /// use repl_lang::{Edit, Editor};
230    ///
231    /// let mut editor = Editor::new();
232    /// editor.insert("hello world");
233    ///
234    /// assert!(editor.apply(Edit::KillWordLeft));
235    /// assert_eq!(editor.line(), "hello ");
236    ///
237    /// assert!(editor.apply(Edit::Home));
238    /// assert!(editor.apply(Edit::Yank));
239    /// assert_eq!(editor.line(), "worldhello ");
240    /// assert_eq!(editor.cursor(), 5); // just past the yanked text
241    ///
242    /// // Commands with nothing to do report `false`.
243    /// assert!(editor.apply(Edit::Home));
244    /// assert!(!editor.apply(Edit::Left));
245    /// assert!(!editor.apply(Edit::Insert('\t'))); // control characters are refused
246    /// ```
247    pub fn apply(&mut self, edit: Edit) -> bool {
248        match edit {
249            Edit::Insert(c) => self.insert_char(c),
250            Edit::Backspace => self.backspace(),
251            Edit::Delete => self.delete(),
252            Edit::Left => self.move_to(prev_unit(&self.line, self.cursor)),
253            Edit::Right => self.move_to(next_unit(&self.line, self.cursor)),
254            Edit::WordLeft => self.move_to(word_left(&self.line, self.cursor)),
255            Edit::WordRight => self.move_to(word_right(&self.line, self.cursor)),
256            Edit::Home => self.move_to(0),
257            Edit::End => self.move_to(self.line.len()),
258            Edit::KillToStart => self.kill_range(0, self.cursor),
259            Edit::KillToEnd => self.kill_range(self.cursor, self.line.len()),
260            Edit::KillWordLeft => self.kill_range(word_left(&self.line, self.cursor), self.cursor),
261            Edit::KillWordRight => {
262                self.kill_range(self.cursor, word_right(&self.line, self.cursor))
263            }
264            Edit::Yank => self.yank(),
265            Edit::HistoryPrev => self.history_prev(),
266            Edit::HistoryNext => self.history_next(),
267        }
268    }
269
270    /// Inserts `text` at the cursor, as a paste would, and reports whether
271    /// anything was inserted.
272    ///
273    /// Control characters and bidirectional formatting characters in `text`
274    /// are dropped, exactly as [`Edit::Insert`] refuses them one at a time;
275    /// everything else is
276    /// inserted in a single pass. A host that receives a multi-line paste
277    /// should split it into lines and submit them one by one.
278    ///
279    /// # Examples
280    ///
281    /// ```
282    /// use repl_lang::{Edit, Editor};
283    ///
284    /// let mut editor = Editor::new();
285    /// editor.insert("fn main() {}");
286    /// assert!(editor.apply(Edit::Left));
287    /// editor.insert(" body ");
288    /// assert_eq!(editor.line(), "fn main() { body }");
289    ///
290    /// // Control characters are dropped from pasted text.
291    /// let mut editor = Editor::new();
292    /// editor.insert("a\tb\u{1b}c");
293    /// assert_eq!(editor.line(), "abc");
294    /// assert!(!editor.insert("\n"));
295    /// ```
296    pub fn insert(&mut self, text: &str) -> bool {
297        let before = self.line.len();
298        for segment in text.split(is_refused) {
299            self.line.insert_str(self.cursor, segment);
300            self.cursor += segment.len();
301        }
302        self.line.len() != before
303    }
304
305    /// Empties the line and moves the cursor to the start, keeping the kill
306    /// buffer and the history. Returns `true` if the line was not already
307    /// empty.
308    ///
309    /// This is the editor half of an interrupt (`Ctrl-C`): abandon what is
310    /// being typed and start over. If the history was being browsed, browsing
311    /// ends and the line parked when it began is dropped as well.
312    ///
313    /// # Examples
314    ///
315    /// ```
316    /// use repl_lang::Editor;
317    ///
318    /// let mut editor = Editor::new();
319    /// editor.insert("half-typed");
320    /// assert!(editor.clear());
321    /// assert_eq!(editor.line(), "");
322    /// assert!(!editor.clear());
323    /// ```
324    pub fn clear(&mut self) -> bool {
325        self.forget_draft();
326        let changed = !self.line.is_empty();
327        self.line.clear();
328        self.cursor = 0;
329        changed
330    }
331
332    /// Finishes the line: returns its text, records it in the history, and
333    /// leaves the editor empty for the next line.
334    ///
335    /// A line is recorded unless it is blank (empty or only whitespace) or
336    /// identical to the most recent history entry, and only if the history
337    /// capacity is not `0`.
338    ///
339    /// Submitting while browsing the history submits the recalled line as
340    /// edited; the line parked when browsing began is dropped. Either way the
341    /// next [`HistoryPrev`](Edit::HistoryPrev) starts from the newest entry.
342    ///
343    /// The returned text stays borrowed from the editor, which keeps the
344    /// buffer for reuse: the next submit recycles it as the following line.
345    ///
346    /// # Examples
347    ///
348    /// ```
349    /// use repl_lang::Editor;
350    ///
351    /// let mut editor = Editor::new();
352    /// editor.insert("1 + 1");
353    /// assert_eq!(editor.submit(), "1 + 1");
354    ///
355    /// // Blank lines and immediate repeats are not recorded.
356    /// editor.insert("   ");
357    /// assert_eq!(editor.submit(), "   ");
358    /// editor.insert("1 + 1");
359    /// editor.submit();
360    /// assert_eq!(editor.history().len(), 1);
361    /// ```
362    pub fn submit(&mut self) -> &str {
363        self.forget_draft();
364        core::mem::swap(&mut self.line, &mut self.submitted);
365        self.line.clear();
366        self.cursor = 0;
367        // Browsing ended above, so recording cannot move a browse position.
368        let _dropped = record(&mut self.history, self.capacity, &self.submitted);
369        &self.submitted
370    }
371
372    /// Records `line` in the history as if it had been submitted, without
373    /// touching the line being edited.
374    ///
375    /// Use it to restore a history saved from an earlier session. The same
376    /// rules as [`submit`](Editor::submit) apply: blank lines and repeats of
377    /// the newest entry are skipped, and the oldest entry is dropped once the
378    /// history is full.
379    ///
380    /// Bidirectional formatting characters are removed, and so are control
381    /// characters, except the two a multi-line entry
382    /// is made of: newlines and tabs are kept, so an entry such as
383    /// `"let x =\n\t42"` comes back as it was entered instead of with its lines
384    /// run together. A `"\r\n"` or a lone `'\r'` is kept as one `'\n'`.
385    /// Everything else — escape (`ESC`), the other C0 and C1 controls, and
386    /// `DEL` — is removed, so a recalled entry can never put a terminal
387    /// control sequence into the line.
388    ///
389    /// Recording while the history is being browsed keeps the browse on the
390    /// entry it shows. If that entry is the oldest and is dropped to make
391    /// room, `HistoryPrev` has nothing older to go to and `HistoryNext` moves
392    /// to the oldest entry that is left.
393    ///
394    /// # Examples
395    ///
396    /// ```
397    /// use repl_lang::{Edit, Editor};
398    ///
399    /// let saved = "let x = 1\nx * 2\n";
400    /// let mut editor = Editor::new();
401    /// for line in saved.lines() {
402    ///     editor.add_history(line);
403    /// }
404    /// assert!(editor.apply(Edit::HistoryPrev));
405    /// assert_eq!(editor.line(), "x * 2");
406    ///
407    /// // A multi-line entry keeps its lines; escape sequences lose their `ESC`.
408    /// editor.add_history("fn f() {\r\n\treturn 1\r\n}\u{1b}[0m");
409    /// assert_eq!(editor.history().next_back(), Some("fn f() {\n\treturn 1\n}[0m"));
410    /// ```
411    pub fn add_history(&mut self, line: &str) {
412        if line.contains(|c: char| is_refused(c) && c != '\n' && c != '\t') {
413            let clean = recallable(line);
414            self.record(&clean);
415        } else {
416            self.record(line);
417        }
418    }
419
420    /// The history, oldest entry first.
421    ///
422    /// Iterate it in reverse for newest-first, or write it out line by line to
423    /// persist the history between sessions.
424    ///
425    /// # Examples
426    ///
427    /// ```
428    /// use repl_lang::Editor;
429    ///
430    /// let mut editor = Editor::new();
431    /// for line in ["first", "second"] {
432    ///     editor.insert(line);
433    ///     editor.submit();
434    /// }
435    /// assert_eq!(editor.history().next_back(), Some("second"));
436    /// let saved: Vec<&str> = editor.history().collect();
437    /// assert_eq!(saved, ["first", "second"]);
438    /// ```
439    pub fn history(&self) -> impl DoubleEndedIterator<Item = &str> + ExactSizeIterator + '_ {
440        self.history.iter().map(String::as_str)
441    }
442
443    /// Moves the cursor, reporting whether it moved.
444    #[inline]
445    fn move_to(&mut self, pos: usize) -> bool {
446        let moved = pos != self.cursor;
447        self.cursor = pos;
448        moved
449    }
450
451    fn insert_char(&mut self, c: char) -> bool {
452        if is_refused(c) {
453            return false;
454        }
455        self.line.insert(self.cursor, c);
456        self.cursor += c.len_utf8();
457        true
458    }
459
460    fn backspace(&mut self) -> bool {
461        if self.cursor == 0 {
462            return false;
463        }
464        let start = prev_unit(&self.line, self.cursor);
465        self.line.replace_range(start..self.cursor, "");
466        self.cursor = start;
467        true
468    }
469
470    fn delete(&mut self) -> bool {
471        let end = next_unit(&self.line, self.cursor);
472        if end == self.cursor {
473            return false;
474        }
475        self.line.replace_range(self.cursor..end, "");
476        true
477    }
478
479    /// Moves `line[start..end]` into the kill buffer and leaves the cursor at
480    /// `start`.
481    fn kill_range(&mut self, start: usize, end: usize) -> bool {
482        if start == end {
483            return false;
484        }
485        self.kill.clear();
486        self.kill.push_str(&self.line[start..end]);
487        self.line.replace_range(start..end, "");
488        self.cursor = start;
489        true
490    }
491
492    fn yank(&mut self) -> bool {
493        if self.kill.is_empty() {
494            return false;
495        }
496        self.line.insert_str(self.cursor, &self.kill);
497        self.cursor += self.kill.len();
498        true
499    }
500
501    fn history_prev(&mut self) -> bool {
502        let target = match self.browsing {
503            None => match self.history.len().checked_sub(1) {
504                Some(newest) => {
505                    // Park the line being typed; `history_next` brings it back.
506                    core::mem::swap(&mut self.line, &mut self.draft);
507                    newest
508                }
509                None => return false,
510            },
511            Some(0) => return false,
512            Some(index) => index - 1,
513        };
514        self.load(target);
515        true
516    }
517
518    fn history_next(&mut self) -> bool {
519        match self.browsing {
520            None => false,
521            Some(_) if self.shown_dropped => {
522                self.load(0);
523                true
524            }
525            Some(index) if index + 1 < self.history.len() => {
526                self.load(index + 1);
527                true
528            }
529            Some(_) => {
530                self.restore_draft();
531                true
532            }
533        }
534    }
535
536    /// Replaces the line with a copy of history entry `index`, cursor at the end.
537    fn load(&mut self, index: usize) {
538        self.line.clear();
539        if let Some(entry) = self.history.get(index) {
540            self.line.push_str(entry);
541        }
542        self.cursor = self.line.len();
543        self.browsing = Some(index);
544        self.shown_dropped = false;
545    }
546
547    /// Records `line` in the history, keeping an in-progress browse on the
548    /// entry it shows.
549    fn record(&mut self, line: &str) {
550        if record(&mut self.history, self.capacity, line) {
551            // The oldest entry was dropped, so every index shifts down by one.
552            // If the dropped entry is the one being shown, the browse now sits
553            // just before index 0.
554            match self.browsing {
555                Some(0) => self.shown_dropped = true,
556                Some(index) => self.browsing = Some(index - 1),
557                None => {}
558            }
559        }
560    }
561
562    /// Ends history browsing by bringing back the line that was being typed
563    /// when it started.
564    fn restore_draft(&mut self) {
565        self.shown_dropped = false;
566        if self.browsing.take().is_some() {
567            core::mem::swap(&mut self.line, &mut self.draft);
568            self.draft.clear();
569            self.cursor = self.line.len();
570        }
571    }
572
573    /// Ends history browsing and drops the parked draft: the recalled line,
574    /// edited or not, is what the user chose to keep.
575    fn forget_draft(&mut self) {
576        self.browsing = None;
577        self.shown_dropped = false;
578        self.draft.clear();
579    }
580}
581
582/// `line` as a history entry may hold it: newlines and tabs kept, `"\r\n"` and
583/// a lone `'\r'` turned into `'\n'`, and every other control character and
584/// every bidirectional formatting character removed.
585fn recallable(line: &str) -> String {
586    let mut clean = String::with_capacity(line.len());
587    let mut chars = line.chars().peekable();
588    while let Some(c) = chars.next() {
589        match c {
590            '\r' if chars.peek() == Some(&'\n') => {}
591            '\r' | '\n' => clean.push('\n'),
592            '\t' => clean.push('\t'),
593            c if is_refused(c) => {}
594            c => clean.push(c),
595        }
596    }
597    clean
598}
599
600/// Appends `line` to `history` under the editor's recording rules, recycling
601/// the oldest entry's buffer once `capacity` is reached. Returns `true` if the
602/// oldest entry was dropped to make room.
603fn record(history: &mut VecDeque<String>, capacity: usize, line: &str) -> bool {
604    if capacity == 0
605        || line.trim().is_empty()
606        || history.back().is_some_and(|newest| newest == line)
607    {
608        return false;
609    }
610    let dropped = history.len() >= capacity;
611    let mut slot = if dropped {
612        history.pop_front().unwrap_or_default()
613    } else {
614        String::new()
615    };
616    slot.clear();
617    slot.push_str(line);
618    history.push_back(slot);
619    dropped
620}
621
622/// Characters that never enter the line by typing or pasting, and that a
623/// history entry drops (apart from the newlines and tabs it keeps): every
624/// control character, and the bidirectional formatting characters. The
625/// latter are invisible and reorder how the text around them is displayed,
626/// so a line holding them can show something other than what it contains.
627fn is_refused(c: char) -> bool {
628    c.is_control()
629        || matches!(
630            c,
631            '\u{061C}' | '\u{200E}' | '\u{200F}' | '\u{202A}'..='\u{202E}' | '\u{2066}'..='\u{2069}'
632        )
633}
634
635#[cfg(test)]
636#[allow(unused_results)]
637mod tests {
638    use super::*;
639
640    fn typed(text: &str) -> Editor {
641        let mut editor = Editor::new();
642        assert!(editor.insert(text));
643        editor
644    }
645
646    #[test]
647    fn test_insert_char_advances_cursor_by_utf8_len() {
648        let mut editor = Editor::new();
649        assert!(editor.apply(Edit::Insert('é')));
650        assert_eq!(editor.cursor(), 2);
651        assert!(editor.apply(Edit::Insert('世')));
652        assert_eq!(editor.cursor(), 5);
653        assert_eq!(editor.line(), "é世");
654    }
655
656    #[test]
657    fn test_insert_control_char_is_refused() {
658        let mut editor = Editor::new();
659        for c in ['\n', '\r', '\t', '\u{1b}', '\u{7f}', '\u{85}'] {
660            assert!(!editor.apply(Edit::Insert(c)), "{c:?} was accepted");
661        }
662        assert_eq!(editor.line(), "");
663    }
664
665    #[test]
666    fn test_insert_in_middle_keeps_tail() {
667        let mut editor = typed("ac");
668        assert!(editor.apply(Edit::Left));
669        assert!(editor.apply(Edit::Insert('b')));
670        assert_eq!(editor.line(), "abc");
671        assert_eq!(editor.cursor(), 2);
672    }
673
674    #[test]
675    fn test_insert_str_only_controls_returns_false() {
676        let mut editor = Editor::new();
677        assert!(!editor.insert("\n\t\r"));
678        assert!(!editor.insert(""));
679    }
680
681    #[test]
682    fn test_backspace_at_start_returns_false() {
683        let mut editor = typed("x");
684        assert!(editor.apply(Edit::Home));
685        assert!(!editor.apply(Edit::Backspace));
686        assert_eq!(editor.line(), "x");
687    }
688
689    #[test]
690    fn test_backspace_removes_whole_combining_unit() {
691        let mut editor = typed("ae\u{0301}");
692        assert!(editor.apply(Edit::Backspace));
693        assert_eq!(editor.line(), "a");
694        assert_eq!(editor.cursor(), 1);
695    }
696
697    #[test]
698    fn test_delete_at_end_returns_false() {
699        let mut editor = typed("x");
700        assert!(!editor.apply(Edit::Delete));
701    }
702
703    #[test]
704    fn test_delete_removes_unit_under_cursor() {
705        let mut editor = typed("世界");
706        assert!(editor.apply(Edit::Home));
707        assert!(editor.apply(Edit::Delete));
708        assert_eq!(editor.line(), "界");
709        assert_eq!(editor.cursor(), 0);
710    }
711
712    #[test]
713    fn test_left_right_at_bounds_return_false() {
714        let mut editor = typed("ab");
715        assert!(!editor.apply(Edit::Right));
716        assert!(!editor.apply(Edit::End));
717        assert!(editor.apply(Edit::Home));
718        assert!(!editor.apply(Edit::Left));
719        assert!(!editor.apply(Edit::Home));
720    }
721
722    #[test]
723    fn test_column_counts_wide_and_zero_width() {
724        let editor = typed("a世e\u{0301}");
725        assert_eq!(editor.column(), 4);
726    }
727
728    #[test]
729    fn test_kill_to_end_then_yank_round_trips() {
730        let mut editor = typed("abc def");
731        assert!(editor.apply(Edit::WordLeft));
732        assert!(editor.apply(Edit::KillToEnd));
733        assert_eq!(editor.line(), "abc ");
734        assert!(!editor.apply(Edit::KillToEnd)); // nothing after the cursor
735        assert!(editor.apply(Edit::Yank));
736        assert_eq!(editor.line(), "abc def");
737    }
738
739    #[test]
740    fn test_empty_kill_keeps_previous_kill_buffer() {
741        let mut editor = typed("one two");
742        assert!(editor.apply(Edit::KillWordLeft));
743        assert!(!editor.apply(Edit::KillWordRight)); // at end: kills nothing
744        assert!(editor.apply(Edit::Yank));
745        assert_eq!(editor.line(), "one two");
746    }
747
748    #[test]
749    fn test_kill_word_right_from_start() {
750        let mut editor = typed("  alpha beta");
751        assert!(editor.apply(Edit::Home));
752        assert!(editor.apply(Edit::KillWordRight));
753        assert_eq!(editor.line(), " beta");
754        assert_eq!(editor.cursor(), 0);
755    }
756
757    #[test]
758    fn test_yank_with_empty_kill_buffer_returns_false() {
759        let mut editor = Editor::new();
760        assert!(!editor.apply(Edit::Yank));
761    }
762
763    #[test]
764    fn test_submit_returns_line_and_resets() {
765        let mut editor = typed("print 1");
766        assert_eq!(editor.submit(), "print 1");
767        assert_eq!(editor.line(), "");
768        assert_eq!(editor.cursor(), 0);
769        assert!(editor.history().eq(["print 1"]));
770    }
771
772    #[test]
773    fn test_submit_skips_blank_and_duplicate() {
774        let mut editor = Editor::new();
775        for line in ["a", "a", "", "  ", "b", "a"] {
776            editor.insert(line);
777            editor.submit();
778        }
779        assert!(editor.history().eq(["a", "b", "a"]));
780    }
781
782    #[test]
783    fn test_history_capacity_zero_records_nothing() {
784        let mut editor = Editor::with_history(0);
785        editor.insert("x");
786        assert_eq!(editor.submit(), "x");
787        assert_eq!(editor.history().len(), 0);
788        assert!(!editor.apply(Edit::HistoryPrev));
789    }
790
791    #[test]
792    fn test_add_history_strips_control_characters_but_keeps_lines() {
793        let mut editor = Editor::new();
794        editor.add_history("a\tb\u{1b}[0m\u{7f}\u{9b}");
795        editor.add_history("\n\t"); // only whitespace: blank, skipped
796        editor.add_history("one\r\ntwo\rthree\n");
797        assert!(editor.history().eq(["a\tb[0m", "one\ntwo\nthree\n"]));
798        assert!(editor.apply(Edit::HistoryPrev));
799        assert_eq!(editor.line(), "one\ntwo\nthree\n");
800        assert!(editor.apply(Edit::HistoryPrev));
801        assert!(
802            !editor
803                .line()
804                .contains(|c: char| c.is_control() && c != '\t' && c != '\n')
805        );
806    }
807
808    #[test]
809    fn test_add_history_multi_line_entry_does_not_merge_tokens() {
810        // 1.0.0 stored "let x =\n42" as "let x =42" and "a\nb" as "ab".
811        let mut editor = Editor::new();
812        editor.add_history("a\nb");
813        assert!(editor.apply(Edit::HistoryPrev));
814        assert_eq!(editor.submit(), "a\nb");
815    }
816
817    #[test]
818    fn test_recalled_multi_line_entry_edits_by_line() {
819        let mut editor = Editor::new();
820        editor.add_history("ab\n\tcd");
821        assert!(editor.apply(Edit::HistoryPrev));
822        // The cursor's column counts from the start of its own line; the tab
823        // is one column.
824        assert_eq!(editor.column(), 3);
825        assert!(editor.apply(Edit::Home));
826        assert!(editor.apply(Edit::WordRight));
827        assert_eq!(editor.cursor(), 2);
828        assert_eq!(editor.column(), 2);
829        // The newline is its own unit: one step over it, one backspace for it.
830        assert!(editor.apply(Edit::Right));
831        assert_eq!((editor.cursor(), editor.column()), (3, 0));
832        assert!(editor.apply(Edit::Backspace));
833        assert_eq!(editor.line(), "ab\tcd");
834        assert_eq!(editor.column(), 2);
835    }
836
837    #[test]
838    fn test_history_ring_drops_oldest_at_capacity() {
839        let mut editor = Editor::with_history(3);
840        for line in ["1", "2", "3", "4", "5"] {
841            editor.add_history(line);
842        }
843        assert!(editor.history().eq(["3", "4", "5"]));
844    }
845
846    #[test]
847    fn test_history_prev_next_restores_draft() {
848        let mut editor = Editor::new();
849        editor.add_history("old");
850        editor.add_history("new");
851        editor.insert("draft");
852
853        assert!(editor.apply(Edit::HistoryPrev));
854        assert_eq!(editor.line(), "new");
855        assert!(editor.apply(Edit::HistoryPrev));
856        assert_eq!(editor.line(), "old");
857        assert!(!editor.apply(Edit::HistoryPrev)); // oldest reached
858        assert!(editor.apply(Edit::HistoryNext));
859        assert_eq!(editor.line(), "new");
860        assert!(editor.apply(Edit::HistoryNext));
861        assert_eq!(editor.line(), "draft");
862        assert_eq!(editor.cursor(), 5);
863        assert!(!editor.apply(Edit::HistoryNext)); // not browsing any more
864    }
865
866    #[test]
867    fn test_history_prev_on_empty_history_returns_false() {
868        let mut editor = typed("x");
869        assert!(!editor.apply(Edit::HistoryPrev));
870        assert_eq!(editor.line(), "x");
871    }
872
873    #[test]
874    fn test_editing_recalled_entry_leaves_history_intact() {
875        let mut editor = Editor::new();
876        editor.add_history("abc");
877        assert!(editor.apply(Edit::HistoryPrev));
878        assert!(editor.apply(Edit::Backspace));
879        assert_eq!(editor.line(), "ab");
880        assert!(editor.history().eq(["abc"]));
881        assert_eq!(editor.submit(), "ab");
882        assert!(editor.history().eq(["abc", "ab"]));
883    }
884
885    #[test]
886    fn test_submit_while_browsing_submits_recalled_line() {
887        let mut editor = Editor::new();
888        editor.add_history("again");
889        editor.insert("draft");
890        assert!(editor.apply(Edit::HistoryPrev));
891        assert_eq!(editor.submit(), "again");
892        // The draft was discarded with the browse; the duplicate not recorded.
893        assert_eq!(editor.line(), "");
894        assert!(editor.history().eq(["again"]));
895    }
896
897    #[test]
898    fn test_add_history_while_browsing_at_capacity_keeps_entry() {
899        let mut editor = Editor::with_history(2);
900        editor.add_history("a");
901        editor.add_history("b");
902        assert!(editor.apply(Edit::HistoryPrev)); // on "b" (index 1)
903        editor.add_history("c"); // drops "a"; "b" is now index 0
904        assert!(editor.apply(Edit::HistoryNext));
905        assert_eq!(editor.line(), "c");
906    }
907
908    #[test]
909    fn test_clear_ends_browse_and_empties_line() {
910        let mut editor = Editor::new();
911        editor.add_history("x");
912        editor.insert("draft");
913        assert!(editor.apply(Edit::HistoryPrev));
914        assert!(editor.clear());
915        assert_eq!(editor.line(), "");
916        assert!(!editor.apply(Edit::HistoryNext));
917    }
918
919    #[test]
920    fn test_submit_steady_state_reuses_buffers() {
921        let mut editor = Editor::with_history(2);
922        for line in ["aaaa", "bbbb", "cccc"] {
923            editor.insert(line);
924            editor.submit();
925        }
926        // Once the ring is full, a submit recycles the oldest slot.
927        let before = editor
928            .history()
929            .map(str::as_ptr)
930            .collect::<alloc::vec::Vec<_>>();
931        editor.insert("dddd");
932        editor.submit();
933        let after = editor
934            .history()
935            .map(str::as_ptr)
936            .collect::<alloc::vec::Vec<_>>();
937        assert_eq!(after[0], before[1]);
938        assert_eq!(after[1], before[0]);
939    }
940}