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