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}