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        text_width(&self.line[..self.cursor])
203    }
204
205    /// Applies one editing command and reports whether the line or the cursor
206    /// changed.
207    ///
208    /// A command that cannot do anything — moving left at the start of the
209    /// line, deleting at the end, yanking an empty kill buffer, inserting a
210    /// control character — leaves the editor untouched and returns `false`, so
211    /// the host can skip a redraw or ring the bell.
212    ///
213    /// The kill commands replace the kill buffer with the text they remove;
214    /// a kill that removes nothing leaves the buffer as it was.
215    ///
216    /// # Examples
217    ///
218    /// ```
219    /// use repl_lang::{Edit, Editor};
220    ///
221    /// let mut editor = Editor::new();
222    /// editor.insert("hello world");
223    ///
224    /// assert!(editor.apply(Edit::KillWordLeft));
225    /// assert_eq!(editor.line(), "hello ");
226    ///
227    /// assert!(editor.apply(Edit::Home));
228    /// assert!(editor.apply(Edit::Yank));
229    /// assert_eq!(editor.line(), "worldhello ");
230    /// assert_eq!(editor.cursor(), 5); // just past the yanked text
231    ///
232    /// // Commands with nothing to do report `false`.
233    /// assert!(editor.apply(Edit::Home));
234    /// assert!(!editor.apply(Edit::Left));
235    /// assert!(!editor.apply(Edit::Insert('\t'))); // control characters are refused
236    /// ```
237    pub fn apply(&mut self, edit: Edit) -> bool {
238        match edit {
239            Edit::Insert(c) => self.insert_char(c),
240            Edit::Backspace => self.backspace(),
241            Edit::Delete => self.delete(),
242            Edit::Left => self.move_to(prev_unit(&self.line, self.cursor)),
243            Edit::Right => self.move_to(next_unit(&self.line, self.cursor)),
244            Edit::WordLeft => self.move_to(word_left(&self.line, self.cursor)),
245            Edit::WordRight => self.move_to(word_right(&self.line, self.cursor)),
246            Edit::Home => self.move_to(0),
247            Edit::End => self.move_to(self.line.len()),
248            Edit::KillToStart => self.kill_range(0, self.cursor),
249            Edit::KillToEnd => self.kill_range(self.cursor, self.line.len()),
250            Edit::KillWordLeft => self.kill_range(word_left(&self.line, self.cursor), self.cursor),
251            Edit::KillWordRight => {
252                self.kill_range(self.cursor, word_right(&self.line, self.cursor))
253            }
254            Edit::Yank => self.yank(),
255            Edit::HistoryPrev => self.history_prev(),
256            Edit::HistoryNext => self.history_next(),
257        }
258    }
259
260    /// Inserts `text` at the cursor, as a paste would, and reports whether
261    /// anything was inserted.
262    ///
263    /// Control characters in `text` are dropped, exactly as
264    /// [`Edit::Insert`] refuses them one at a time; everything else is
265    /// inserted in a single pass. A host that receives a multi-line paste
266    /// should split it into lines and submit them one by one.
267    ///
268    /// # Examples
269    ///
270    /// ```
271    /// use repl_lang::{Edit, Editor};
272    ///
273    /// let mut editor = Editor::new();
274    /// editor.insert("fn main() {}");
275    /// assert!(editor.apply(Edit::Left));
276    /// editor.insert(" body ");
277    /// assert_eq!(editor.line(), "fn main() { body }");
278    ///
279    /// // Control characters are dropped from pasted text.
280    /// let mut editor = Editor::new();
281    /// editor.insert("a\tb\u{1b}c");
282    /// assert_eq!(editor.line(), "abc");
283    /// assert!(!editor.insert("\n"));
284    /// ```
285    pub fn insert(&mut self, text: &str) -> bool {
286        let before = self.line.len();
287        for segment in text.split(char::is_control) {
288            self.line.insert_str(self.cursor, segment);
289            self.cursor += segment.len();
290        }
291        self.line.len() != before
292    }
293
294    /// Empties the line and moves the cursor to the start, keeping the kill
295    /// buffer and the history. Returns `true` if the line was not already
296    /// empty.
297    ///
298    /// This is the editor half of an interrupt (`Ctrl-C`): abandon what is
299    /// being typed and start over. If the history was being browsed, browsing
300    /// ends and the line parked when it began is dropped as well.
301    ///
302    /// # Examples
303    ///
304    /// ```
305    /// use repl_lang::Editor;
306    ///
307    /// let mut editor = Editor::new();
308    /// editor.insert("half-typed");
309    /// assert!(editor.clear());
310    /// assert_eq!(editor.line(), "");
311    /// assert!(!editor.clear());
312    /// ```
313    pub fn clear(&mut self) -> bool {
314        self.forget_draft();
315        let changed = !self.line.is_empty();
316        self.line.clear();
317        self.cursor = 0;
318        changed
319    }
320
321    /// Finishes the line: returns its text, records it in the history, and
322    /// leaves the editor empty for the next line.
323    ///
324    /// A line is recorded unless it is blank (empty or only whitespace) or
325    /// identical to the most recent history entry, and only if the history
326    /// capacity is not `0`.
327    ///
328    /// Submitting while browsing the history submits the recalled line as
329    /// edited; the line parked when browsing began is dropped. Either way the
330    /// next [`HistoryPrev`](Edit::HistoryPrev) starts from the newest entry.
331    ///
332    /// The returned text stays borrowed from the editor, which keeps the
333    /// buffer for reuse: the next submit recycles it as the following line.
334    ///
335    /// # Examples
336    ///
337    /// ```
338    /// use repl_lang::Editor;
339    ///
340    /// let mut editor = Editor::new();
341    /// editor.insert("1 + 1");
342    /// assert_eq!(editor.submit(), "1 + 1");
343    ///
344    /// // Blank lines and immediate repeats are not recorded.
345    /// editor.insert("   ");
346    /// assert_eq!(editor.submit(), "   ");
347    /// editor.insert("1 + 1");
348    /// editor.submit();
349    /// assert_eq!(editor.history().len(), 1);
350    /// ```
351    pub fn submit(&mut self) -> &str {
352        self.forget_draft();
353        core::mem::swap(&mut self.line, &mut self.submitted);
354        self.line.clear();
355        self.cursor = 0;
356        record(
357            &mut self.history,
358            self.capacity,
359            &self.submitted,
360            &mut self.browsing,
361        );
362        &self.submitted
363    }
364
365    /// Records `line` in the history as if it had been submitted, without
366    /// touching the line being edited.
367    ///
368    /// Use it to restore a history saved from an earlier session. The same
369    /// rules as [`submit`](Editor::submit) apply: blank lines and repeats of
370    /// the newest entry are skipped, and the oldest entry is dropped once the
371    /// history is full. Control characters are removed first, exactly as
372    /// [`insert`](Editor::insert) removes them, so a recalled entry can never
373    /// put into the line something that could not have been typed there.
374    ///
375    /// # Examples
376    ///
377    /// ```
378    /// use repl_lang::{Edit, Editor};
379    ///
380    /// let saved = "let x = 1\nx * 2\n";
381    /// let mut editor = Editor::new();
382    /// for line in saved.lines() {
383    ///     editor.add_history(line);
384    /// }
385    /// assert!(editor.apply(Edit::HistoryPrev));
386    /// assert_eq!(editor.line(), "x * 2");
387    /// ```
388    pub fn add_history(&mut self, line: &str) {
389        if line.contains(char::is_control) {
390            let clean: String = line.chars().filter(|c| !c.is_control()).collect();
391            record(&mut self.history, self.capacity, &clean, &mut self.browsing);
392        } else {
393            record(&mut self.history, self.capacity, line, &mut self.browsing);
394        }
395    }
396
397    /// The history, oldest entry first.
398    ///
399    /// Iterate it in reverse for newest-first, or write it out line by line to
400    /// persist the history between sessions.
401    ///
402    /// # Examples
403    ///
404    /// ```
405    /// use repl_lang::Editor;
406    ///
407    /// let mut editor = Editor::new();
408    /// for line in ["first", "second"] {
409    ///     editor.insert(line);
410    ///     editor.submit();
411    /// }
412    /// assert_eq!(editor.history().next_back(), Some("second"));
413    /// let saved: Vec<&str> = editor.history().collect();
414    /// assert_eq!(saved, ["first", "second"]);
415    /// ```
416    pub fn history(&self) -> impl DoubleEndedIterator<Item = &str> + ExactSizeIterator + '_ {
417        self.history.iter().map(String::as_str)
418    }
419
420    /// Moves the cursor, reporting whether it moved.
421    #[inline]
422    fn move_to(&mut self, pos: usize) -> bool {
423        let moved = pos != self.cursor;
424        self.cursor = pos;
425        moved
426    }
427
428    fn insert_char(&mut self, c: char) -> bool {
429        if c.is_control() {
430            return false;
431        }
432        self.line.insert(self.cursor, c);
433        self.cursor += c.len_utf8();
434        true
435    }
436
437    fn backspace(&mut self) -> bool {
438        if self.cursor == 0 {
439            return false;
440        }
441        let start = prev_unit(&self.line, self.cursor);
442        self.line.replace_range(start..self.cursor, "");
443        self.cursor = start;
444        true
445    }
446
447    fn delete(&mut self) -> bool {
448        let end = next_unit(&self.line, self.cursor);
449        if end == self.cursor {
450            return false;
451        }
452        self.line.replace_range(self.cursor..end, "");
453        true
454    }
455
456    /// Moves `line[start..end]` into the kill buffer and leaves the cursor at
457    /// `start`.
458    fn kill_range(&mut self, start: usize, end: usize) -> bool {
459        if start == end {
460            return false;
461        }
462        self.kill.clear();
463        self.kill.push_str(&self.line[start..end]);
464        self.line.replace_range(start..end, "");
465        self.cursor = start;
466        true
467    }
468
469    fn yank(&mut self) -> bool {
470        if self.kill.is_empty() {
471            return false;
472        }
473        self.line.insert_str(self.cursor, &self.kill);
474        self.cursor += self.kill.len();
475        true
476    }
477
478    fn history_prev(&mut self) -> bool {
479        let target = match self.browsing {
480            None => match self.history.len().checked_sub(1) {
481                Some(newest) => {
482                    // Park the line being typed; `history_next` brings it back.
483                    core::mem::swap(&mut self.line, &mut self.draft);
484                    newest
485                }
486                None => return false,
487            },
488            Some(0) => return false,
489            Some(index) => index - 1,
490        };
491        self.load(target);
492        true
493    }
494
495    fn history_next(&mut self) -> bool {
496        match self.browsing {
497            None => false,
498            Some(index) if index + 1 < self.history.len() => {
499                self.load(index + 1);
500                true
501            }
502            Some(_) => {
503                self.restore_draft();
504                true
505            }
506        }
507    }
508
509    /// Replaces the line with a copy of history entry `index`, cursor at the end.
510    fn load(&mut self, index: usize) {
511        self.line.clear();
512        if let Some(entry) = self.history.get(index) {
513            self.line.push_str(entry);
514        }
515        self.cursor = self.line.len();
516        self.browsing = Some(index);
517    }
518
519    /// Ends history browsing by bringing back the line that was being typed
520    /// when it started.
521    fn restore_draft(&mut self) {
522        if self.browsing.take().is_some() {
523            core::mem::swap(&mut self.line, &mut self.draft);
524            self.draft.clear();
525            self.cursor = self.line.len();
526        }
527    }
528
529    /// Ends history browsing and drops the parked draft: the recalled line,
530    /// edited or not, is what the user chose to keep.
531    fn forget_draft(&mut self) {
532        self.browsing = None;
533        self.draft.clear();
534    }
535}
536
537/// Appends `line` to `history` under the editor's recording rules, recycling
538/// the oldest entry's buffer once `capacity` is reached.
539fn record(
540    history: &mut VecDeque<String>,
541    capacity: usize,
542    line: &str,
543    browsing: &mut Option<usize>,
544) {
545    if capacity == 0
546        || line.trim().is_empty()
547        || history.back().is_some_and(|newest| newest == line)
548    {
549        return;
550    }
551    let mut slot = if history.len() >= capacity {
552        // Keep an in-progress browse pointing at the same entry after the shift.
553        *browsing = browsing.map(|index| index.saturating_sub(1));
554        history.pop_front().unwrap_or_default()
555    } else {
556        String::new()
557    };
558    slot.clear();
559    slot.push_str(line);
560    history.push_back(slot);
561}
562
563#[cfg(test)]
564#[allow(unused_results)]
565mod tests {
566    use super::*;
567
568    fn typed(text: &str) -> Editor {
569        let mut editor = Editor::new();
570        assert!(editor.insert(text));
571        editor
572    }
573
574    #[test]
575    fn test_insert_char_advances_cursor_by_utf8_len() {
576        let mut editor = Editor::new();
577        assert!(editor.apply(Edit::Insert('é')));
578        assert_eq!(editor.cursor(), 2);
579        assert!(editor.apply(Edit::Insert('世')));
580        assert_eq!(editor.cursor(), 5);
581        assert_eq!(editor.line(), "é世");
582    }
583
584    #[test]
585    fn test_insert_control_char_is_refused() {
586        let mut editor = Editor::new();
587        for c in ['\n', '\r', '\t', '\u{1b}', '\u{7f}', '\u{85}'] {
588            assert!(!editor.apply(Edit::Insert(c)), "{c:?} was accepted");
589        }
590        assert_eq!(editor.line(), "");
591    }
592
593    #[test]
594    fn test_insert_in_middle_keeps_tail() {
595        let mut editor = typed("ac");
596        assert!(editor.apply(Edit::Left));
597        assert!(editor.apply(Edit::Insert('b')));
598        assert_eq!(editor.line(), "abc");
599        assert_eq!(editor.cursor(), 2);
600    }
601
602    #[test]
603    fn test_insert_str_only_controls_returns_false() {
604        let mut editor = Editor::new();
605        assert!(!editor.insert("\n\t\r"));
606        assert!(!editor.insert(""));
607    }
608
609    #[test]
610    fn test_backspace_at_start_returns_false() {
611        let mut editor = typed("x");
612        assert!(editor.apply(Edit::Home));
613        assert!(!editor.apply(Edit::Backspace));
614        assert_eq!(editor.line(), "x");
615    }
616
617    #[test]
618    fn test_backspace_removes_whole_combining_unit() {
619        let mut editor = typed("ae\u{0301}");
620        assert!(editor.apply(Edit::Backspace));
621        assert_eq!(editor.line(), "a");
622        assert_eq!(editor.cursor(), 1);
623    }
624
625    #[test]
626    fn test_delete_at_end_returns_false() {
627        let mut editor = typed("x");
628        assert!(!editor.apply(Edit::Delete));
629    }
630
631    #[test]
632    fn test_delete_removes_unit_under_cursor() {
633        let mut editor = typed("世界");
634        assert!(editor.apply(Edit::Home));
635        assert!(editor.apply(Edit::Delete));
636        assert_eq!(editor.line(), "界");
637        assert_eq!(editor.cursor(), 0);
638    }
639
640    #[test]
641    fn test_left_right_at_bounds_return_false() {
642        let mut editor = typed("ab");
643        assert!(!editor.apply(Edit::Right));
644        assert!(!editor.apply(Edit::End));
645        assert!(editor.apply(Edit::Home));
646        assert!(!editor.apply(Edit::Left));
647        assert!(!editor.apply(Edit::Home));
648    }
649
650    #[test]
651    fn test_column_counts_wide_and_zero_width() {
652        let editor = typed("a世e\u{0301}");
653        assert_eq!(editor.column(), 4);
654    }
655
656    #[test]
657    fn test_kill_to_end_then_yank_round_trips() {
658        let mut editor = typed("abc def");
659        assert!(editor.apply(Edit::WordLeft));
660        assert!(editor.apply(Edit::KillToEnd));
661        assert_eq!(editor.line(), "abc ");
662        assert!(!editor.apply(Edit::KillToEnd)); // nothing after the cursor
663        assert!(editor.apply(Edit::Yank));
664        assert_eq!(editor.line(), "abc def");
665    }
666
667    #[test]
668    fn test_empty_kill_keeps_previous_kill_buffer() {
669        let mut editor = typed("one two");
670        assert!(editor.apply(Edit::KillWordLeft));
671        assert!(!editor.apply(Edit::KillWordRight)); // at end: kills nothing
672        assert!(editor.apply(Edit::Yank));
673        assert_eq!(editor.line(), "one two");
674    }
675
676    #[test]
677    fn test_kill_word_right_from_start() {
678        let mut editor = typed("  alpha beta");
679        assert!(editor.apply(Edit::Home));
680        assert!(editor.apply(Edit::KillWordRight));
681        assert_eq!(editor.line(), " beta");
682        assert_eq!(editor.cursor(), 0);
683    }
684
685    #[test]
686    fn test_yank_with_empty_kill_buffer_returns_false() {
687        let mut editor = Editor::new();
688        assert!(!editor.apply(Edit::Yank));
689    }
690
691    #[test]
692    fn test_submit_returns_line_and_resets() {
693        let mut editor = typed("print 1");
694        assert_eq!(editor.submit(), "print 1");
695        assert_eq!(editor.line(), "");
696        assert_eq!(editor.cursor(), 0);
697        assert!(editor.history().eq(["print 1"]));
698    }
699
700    #[test]
701    fn test_submit_skips_blank_and_duplicate() {
702        let mut editor = Editor::new();
703        for line in ["a", "a", "", "  ", "b", "a"] {
704            editor.insert(line);
705            editor.submit();
706        }
707        assert!(editor.history().eq(["a", "b", "a"]));
708    }
709
710    #[test]
711    fn test_history_capacity_zero_records_nothing() {
712        let mut editor = Editor::with_history(0);
713        editor.insert("x");
714        assert_eq!(editor.submit(), "x");
715        assert_eq!(editor.history().len(), 0);
716        assert!(!editor.apply(Edit::HistoryPrev));
717    }
718
719    #[test]
720    fn test_add_history_strips_control_characters() {
721        let mut editor = Editor::new();
722        editor.add_history("a\tb\u{1b}[0m");
723        editor.add_history("\n\t"); // nothing typable left: blank, skipped
724        assert!(editor.history().eq(["ab[0m"]));
725        assert!(editor.apply(Edit::HistoryPrev));
726        assert!(!editor.line().contains(char::is_control));
727    }
728
729    #[test]
730    fn test_history_ring_drops_oldest_at_capacity() {
731        let mut editor = Editor::with_history(3);
732        for line in ["1", "2", "3", "4", "5"] {
733            editor.add_history(line);
734        }
735        assert!(editor.history().eq(["3", "4", "5"]));
736    }
737
738    #[test]
739    fn test_history_prev_next_restores_draft() {
740        let mut editor = Editor::new();
741        editor.add_history("old");
742        editor.add_history("new");
743        editor.insert("draft");
744
745        assert!(editor.apply(Edit::HistoryPrev));
746        assert_eq!(editor.line(), "new");
747        assert!(editor.apply(Edit::HistoryPrev));
748        assert_eq!(editor.line(), "old");
749        assert!(!editor.apply(Edit::HistoryPrev)); // oldest reached
750        assert!(editor.apply(Edit::HistoryNext));
751        assert_eq!(editor.line(), "new");
752        assert!(editor.apply(Edit::HistoryNext));
753        assert_eq!(editor.line(), "draft");
754        assert_eq!(editor.cursor(), 5);
755        assert!(!editor.apply(Edit::HistoryNext)); // not browsing any more
756    }
757
758    #[test]
759    fn test_history_prev_on_empty_history_returns_false() {
760        let mut editor = typed("x");
761        assert!(!editor.apply(Edit::HistoryPrev));
762        assert_eq!(editor.line(), "x");
763    }
764
765    #[test]
766    fn test_editing_recalled_entry_leaves_history_intact() {
767        let mut editor = Editor::new();
768        editor.add_history("abc");
769        assert!(editor.apply(Edit::HistoryPrev));
770        assert!(editor.apply(Edit::Backspace));
771        assert_eq!(editor.line(), "ab");
772        assert!(editor.history().eq(["abc"]));
773        assert_eq!(editor.submit(), "ab");
774        assert!(editor.history().eq(["abc", "ab"]));
775    }
776
777    #[test]
778    fn test_submit_while_browsing_submits_recalled_line() {
779        let mut editor = Editor::new();
780        editor.add_history("again");
781        editor.insert("draft");
782        assert!(editor.apply(Edit::HistoryPrev));
783        assert_eq!(editor.submit(), "again");
784        // The draft was discarded with the browse; the duplicate not recorded.
785        assert_eq!(editor.line(), "");
786        assert!(editor.history().eq(["again"]));
787    }
788
789    #[test]
790    fn test_add_history_while_browsing_at_capacity_keeps_entry() {
791        let mut editor = Editor::with_history(2);
792        editor.add_history("a");
793        editor.add_history("b");
794        assert!(editor.apply(Edit::HistoryPrev)); // on "b" (index 1)
795        editor.add_history("c"); // drops "a"; "b" is now index 0
796        assert!(editor.apply(Edit::HistoryNext));
797        assert_eq!(editor.line(), "c");
798    }
799
800    #[test]
801    fn test_clear_ends_browse_and_empties_line() {
802        let mut editor = Editor::new();
803        editor.add_history("x");
804        editor.insert("draft");
805        assert!(editor.apply(Edit::HistoryPrev));
806        assert!(editor.clear());
807        assert_eq!(editor.line(), "");
808        assert!(!editor.apply(Edit::HistoryNext));
809    }
810
811    #[test]
812    fn test_submit_steady_state_reuses_buffers() {
813        let mut editor = Editor::with_history(2);
814        for line in ["aaaa", "bbbb", "cccc"] {
815            editor.insert(line);
816            editor.submit();
817        }
818        // Once the ring is full, a submit recycles the oldest slot.
819        let before = editor
820            .history()
821            .map(str::as_ptr)
822            .collect::<alloc::vec::Vec<_>>();
823        editor.insert("dddd");
824        editor.submit();
825        let after = editor
826            .history()
827            .map(str::as_ptr)
828            .collect::<alloc::vec::Vec<_>>();
829        assert_eq!(after[0], before[1]);
830        assert_eq!(after[1], before[0]);
831    }
832}