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}