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