editline/lib.rs
1//! Platform-agnostic line editor with history and full editing capabilities.
2//!
3//! This library provides a flexible line editing system with complete separation between
4//! I/O operations and editing logic. This design enables usage across various platforms
5//! and I/O systems without modification to the core logic.
6//!
7//! # Features
8//!
9//! - **Full line editing**: Insert, delete, cursor movement
10//! - **Word-aware navigation**: Ctrl+Left/Right, Alt+Backspace, Ctrl+Delete
11//! - **Command history**: 50-entry circular buffer with up/down navigation
12//! - **Smart history**: Automatically skips duplicates and empty lines
13//! - **Cross-platform**: Unix (termios/ANSI) and Windows (Console API) implementations included
14//! - **Sync and Async**: Both blocking and async APIs available via feature flags
15//! - **Zero global state**: All state is explicitly managed
16//! - **Type-safe**: Strong typing with Result-based error handling
17//!
18//! # Quick Start (Sync)
19//!
20//! ```no_run
21//! use editline::{LineEditor, terminals::StdioTerminal};
22//!
23//! let mut editor = LineEditor::new(1024, 50); // buffer size, history size
24//! let mut terminal = StdioTerminal::new();
25//!
26//! loop {
27//! print!("> ");
28//! std::io::Write::flush(&mut std::io::stdout()).unwrap();
29//!
30//! match editor.read_line(&mut terminal) {
31//! Ok(line) => {
32//! if line == "exit" {
33//! break;
34//! }
35//! println!("You typed: {}", line);
36//! }
37//! Err(e) => {
38//! eprintln!("Error: {}", e);
39//! break;
40//! }
41//! }
42//! }
43//! ```
44//!
45//! # Quick Start (Async)
46//!
47//! ```ignore
48//! use editline::{AsyncLineEditor, terminals::EmbassyUsbTerminal};
49//!
50//! let mut editor = AsyncLineEditor::new(1024, 50);
51//! let mut terminal = EmbassyUsbTerminal::new(usb_class);
52//!
53//! loop {
54//! let _ = terminal.write(b"> ").await;
55//! let _ = terminal.flush().await;
56//!
57//! match editor.read_line(&mut terminal).await {
58//! Ok(line) => {
59//! if line == "exit" {
60//! break;
61//! }
62//! defmt::info!("You typed: {}", line);
63//! }
64//! Err(e) => {
65//! defmt::error!("Error: {:?}", e);
66//! break;
67//! }
68//! }
69//! }
70//! ```
71//!
72//! # Architecture
73//!
74//! The library is organized around several components:
75//!
76//! - **Shared Components** (work with both sync and async):
77//! - [`LineBuffer`]: Manages text buffer and cursor position
78//! - [`History`]: Circular buffer for command history
79//! - [`KeyEvent`]: Key event enumeration
80//! - [`Error`]: Error type
81//!
82//! - **Sync API** (feature = "sync", default):
83//! - [`Terminal`]: Blocking I/O trait
84//! - [`LineEditor`]: Blocking line editor
85//!
86//! - **Async API** (feature = "async"):
87//! - [`AsyncTerminal`]: Async I/O trait
88//! - [`AsyncLineEditor`]: Async line editor
89
90#![cfg_attr(not(feature = "std"), no_std)]
91
92extern crate alloc;
93
94use alloc::string::{String, ToString};
95use alloc::vec::Vec;
96use core::fmt;
97use core::option::Option::{self, Some, None};
98use core::convert::From;
99
100/// Error type for editline operations
101#[derive(Debug)]
102pub enum Error {
103 /// I/O error occurred
104 Io(&'static str),
105 /// Invalid UTF-8 data
106 InvalidUtf8,
107 /// End of file
108 Eof,
109 /// Operation interrupted
110 Interrupted,
111}
112
113impl fmt::Display for Error {
114 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115 match self {
116 Error::Io(msg) => {
117 f.write_str("I/O error: ")?;
118 f.write_str(msg)
119 }
120 Error::InvalidUtf8 => f.write_str("Invalid UTF-8"),
121 Error::Eof => f.write_str("End of file"),
122 Error::Interrupted => f.write_str("Interrupted"),
123 }
124 }
125}
126
127#[cfg(feature = "std")]
128impl From<std::io::Error> for Error {
129 fn from(e: std::io::Error) -> Self {
130 use std::io::ErrorKind;
131 match e.kind() {
132 ErrorKind::UnexpectedEof => Error::Eof,
133 ErrorKind::Interrupted => Error::Interrupted,
134 _ => Error::Io("I/O error"),
135 }
136 }
137}
138
139#[cfg(feature = "std")]
140impl From<Error> for std::io::Error {
141 fn from(e: Error) -> Self {
142 use std::io::{Error as IoError, ErrorKind};
143 match e {
144 Error::Io(msg) => IoError::new(ErrorKind::Other, msg),
145 Error::InvalidUtf8 => IoError::new(ErrorKind::InvalidData, "Invalid UTF-8"),
146 Error::Eof => IoError::new(ErrorKind::UnexpectedEof, "End of file"),
147 Error::Interrupted => IoError::new(ErrorKind::Interrupted, "Interrupted"),
148 }
149 }
150}
151
152impl From<core::str::Utf8Error> for Error {
153 fn from(_: core::str::Utf8Error) -> Self {
154 Error::InvalidUtf8
155 }
156}
157
158/// Result type for editline operations
159pub type Result<T> = core::result::Result<T, Error>;
160
161/// Key events that can be processed by the line editor.
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163pub enum KeyEvent {
164 /// Normal printable character
165 Normal(char),
166 /// Left arrow
167 Left,
168 /// Right arrow
169 Right,
170 /// Up arrow (history previous)
171 Up,
172 /// Down arrow (history next)
173 Down,
174 /// Home key
175 Home,
176 /// End key
177 End,
178 /// Backspace
179 Backspace,
180 /// Delete
181 Delete,
182 /// Enter/Return
183 Enter,
184 /// Ctrl+Left (word left)
185 CtrlLeft,
186 /// Ctrl+Right (word right)
187 CtrlRight,
188 /// Ctrl+Delete (delete word right)
189 CtrlDelete,
190 /// Alt+Backspace (delete word left)
191 AltBackspace,
192}
193
194/// Text buffer with cursor tracking for line editing operations.
195///
196/// Manages the actual text being edited and the cursor position within it.
197/// Supports UTF-8 text and provides methods for character/word manipulation.
198///
199/// This struct is typically not used directly - instead use [`LineEditor`] which
200/// provides the high-level editing interface.
201pub struct LineBuffer {
202 buffer: Vec<u8>,
203 cursor_pos: usize,
204}
205
206impl LineBuffer {
207 /// Creates a new line buffer with the specified capacity.
208 ///
209 /// # Arguments
210 ///
211 /// * `capacity` - Initial capacity for the internal buffer in bytes
212 ///
213 /// # Examples
214 ///
215 /// ```
216 /// use editline::LineBuffer;
217 ///
218 /// let buffer = LineBuffer::new(1024);
219 /// assert!(buffer.is_empty());
220 /// ```
221 pub fn new(capacity: usize) -> Self {
222 Self {
223 buffer: Vec::with_capacity(capacity),
224 cursor_pos: 0,
225 }
226 }
227
228 /// Clears the buffer and resets the cursor to the start.
229 pub fn clear(&mut self) {
230 self.buffer.clear();
231 self.cursor_pos = 0;
232 }
233
234 /// Returns the length of the buffer in bytes.
235 ///
236 /// Note: For UTF-8 text, this is the byte count, not the character count.
237 pub fn len(&self) -> usize {
238 self.buffer.len()
239 }
240
241 /// Returns `true` if the buffer is empty.
242 pub fn is_empty(&self) -> bool {
243 self.buffer.is_empty()
244 }
245
246 /// Returns the current cursor position in bytes from the start.
247 pub fn cursor_pos(&self) -> usize {
248 self.cursor_pos
249 }
250
251 /// Returns the buffer contents as a UTF-8 string slice.
252 ///
253 /// # Errors
254 ///
255 /// Returns `Err` if the buffer contains invalid UTF-8.
256 pub fn as_str(&self) -> Result<&str> {
257 core::str::from_utf8(&self.buffer).map_err(|_| Error::InvalidUtf8)
258 }
259
260 /// Returns the buffer contents as a byte slice.
261 pub fn as_bytes(&self) -> &[u8] {
262 &self.buffer
263 }
264
265 /// Inserts a character at the cursor position, moving the cursor forward.
266 ///
267 /// Supports UTF-8 characters. The cursor advances by the byte length of the character.
268 pub fn insert_char(&mut self, c: char) {
269 let mut buf = [0; 4];
270 let bytes = c.encode_utf8(&mut buf).as_bytes();
271
272 for &byte in bytes {
273 self.buffer.insert(self.cursor_pos, byte);
274 self.cursor_pos += 1;
275 }
276 }
277
278 /// Deletes the character before the cursor (backspace operation).
279 ///
280 /// Returns `true` if a character was deleted, `false` if the cursor is at the start.
281 pub fn delete_before_cursor(&mut self) -> bool {
282 if self.cursor_pos > 0 {
283 self.cursor_pos -= 1;
284 self.buffer.remove(self.cursor_pos);
285 true
286 } else {
287 false
288 }
289 }
290
291 /// Deletes the character at the cursor (delete key operation).
292 ///
293 /// Returns `true` if a character was deleted, `false` if the cursor is at the end.
294 pub fn delete_at_cursor(&mut self) -> bool {
295 if self.cursor_pos < self.buffer.len() {
296 self.buffer.remove(self.cursor_pos);
297 true
298 } else {
299 false
300 }
301 }
302
303 /// Moves the cursor one position to the left.
304 ///
305 /// Returns `true` if the cursor moved, `false` if already at the start.
306 pub fn move_cursor_left(&mut self) -> bool {
307 if self.cursor_pos > 0 {
308 self.cursor_pos -= 1;
309 true
310 } else {
311 false
312 }
313 }
314
315 /// Moves the cursor one position to the right.
316 ///
317 /// Returns `true` if the cursor moved, `false` if already at the end.
318 pub fn move_cursor_right(&mut self) -> bool {
319 if self.cursor_pos < self.buffer.len() {
320 self.cursor_pos += 1;
321 true
322 } else {
323 false
324 }
325 }
326
327 /// Moves the cursor to the start of the line.
328 ///
329 /// Returns the number of positions the cursor moved.
330 pub fn move_cursor_to_start(&mut self) -> usize {
331 let old_pos = self.cursor_pos;
332 self.cursor_pos = 0;
333 old_pos
334 }
335
336 /// Moves the cursor to the end of the line.
337 ///
338 /// Returns the number of positions the cursor moved.
339 pub fn move_cursor_to_end(&mut self) -> usize {
340 let old_pos = self.cursor_pos;
341 self.cursor_pos = self.buffer.len();
342 self.buffer.len() - old_pos
343 }
344
345 /// Find start of word to the left
346 fn find_word_start_left(&self) -> usize {
347 if self.cursor_pos == 0 {
348 return 0;
349 }
350
351 let mut pos = self.cursor_pos;
352
353 // Skip any trailing whitespace first
354 while pos > 0 && is_whitespace(self.buffer[pos - 1]) {
355 pos -= 1;
356 }
357
358 if pos == 0 {
359 return 0;
360 }
361
362 // Now we're on a non-whitespace character
363 // Skip characters of the same type (word chars or symbols)
364 let is_word = is_word_char(self.buffer[pos - 1]);
365 while pos > 0 {
366 let c = self.buffer[pos - 1];
367 if is_whitespace(c) {
368 break;
369 }
370 if is_word != is_word_char(c) {
371 break;
372 }
373 pos -= 1;
374 }
375
376 pos
377 }
378
379 /// Find start of word to the right
380 fn find_word_start_right(&self) -> usize {
381 if self.cursor_pos >= self.buffer.len() {
382 return self.buffer.len();
383 }
384
385 let mut pos = self.cursor_pos;
386
387 // Skip characters of the same type (word chars or symbols)
388 let is_word = is_word_char(self.buffer[pos]);
389 while pos < self.buffer.len() {
390 let c = self.buffer[pos];
391 if is_whitespace(c) {
392 break;
393 }
394 if is_word != is_word_char(c) {
395 break;
396 }
397 pos += 1;
398 }
399
400 // Skip whitespace
401 while pos < self.buffer.len() && is_whitespace(self.buffer[pos]) {
402 pos += 1;
403 }
404
405 pos
406 }
407
408 /// Moves the cursor to the start of the previous word.
409 ///
410 /// Words are defined as sequences of alphanumeric characters and underscores.
411 /// Symbols (like `+`, `-`, `*`) are treated as separate words. Only whitespace
412 /// is skipped when navigating between words.
413 ///
414 /// Returns the number of positions the cursor moved.
415 pub fn move_cursor_word_left(&mut self) -> usize {
416 let target = self.find_word_start_left();
417 let moved = self.cursor_pos - target;
418 self.cursor_pos = target;
419 moved
420 }
421
422 /// Moves the cursor to the start of the next word.
423 ///
424 /// Words are defined as sequences of alphanumeric characters and underscores.
425 /// Symbols (like `+`, `-`, `*`) are treated as separate words. Only whitespace
426 /// is skipped when navigating between words.
427 ///
428 /// Returns the number of positions the cursor moved.
429 pub fn move_cursor_word_right(&mut self) -> usize {
430 let target = self.find_word_start_right();
431 let moved = target - self.cursor_pos;
432 self.cursor_pos = target;
433 moved
434 }
435
436 /// Deletes the word to the left of the cursor (Alt+Backspace operation).
437 ///
438 /// Returns the number of bytes deleted.
439 pub fn delete_word_left(&mut self) -> usize {
440 let target = self.find_word_start_left();
441 let count = self.cursor_pos - target;
442
443 for _ in 0..count {
444 if self.cursor_pos > 0 {
445 self.cursor_pos -= 1;
446 self.buffer.remove(self.cursor_pos);
447 }
448 }
449
450 count
451 }
452
453 /// Deletes the word to the right of the cursor (Ctrl+Delete operation).
454 ///
455 /// Returns the number of bytes deleted.
456 pub fn delete_word_right(&mut self) -> usize {
457 let target = self.find_word_start_right();
458 let count = target - self.cursor_pos;
459
460 for _ in 0..count {
461 if self.cursor_pos < self.buffer.len() {
462 self.buffer.remove(self.cursor_pos);
463 }
464 }
465
466 count
467 }
468
469 /// Loads text into the buffer, replacing existing content.
470 ///
471 /// The cursor is positioned at the end of the loaded text.
472 /// Used internally for history navigation.
473 pub fn load(&mut self, text: &str) {
474 self.buffer.clear();
475 self.buffer.extend_from_slice(text.as_bytes());
476 self.cursor_pos = self.buffer.len();
477 }
478}
479
480/// Check if a byte is a word character (alphanumeric or underscore).
481fn is_word_char(c: u8) -> bool {
482 c.is_ascii_alphanumeric() || c == b'_'
483}
484
485/// Check if a byte is whitespace (space or tab).
486fn is_whitespace(c: u8) -> bool {
487 c == b' ' || c == b'\t'
488}
489
490/// Command history manager with circular buffer storage.
491///
492/// Maintains a fixed-size history of entered commands with automatic
493/// duplicate and empty-line filtering. Supports bidirectional navigation
494/// and preserves the current line when browsing history.
495///
496/// # Examples
497///
498/// ```
499/// use editline::History;
500///
501/// let mut hist = History::new(50);
502/// hist.add("first command");
503/// hist.add("second command");
504///
505/// // Navigate through history
506/// assert_eq!(hist.previous(""), Some("second command"));
507/// assert_eq!(hist.previous(""), Some("first command"));
508/// ```
509pub struct History {
510 entries: Vec<String>,
511 capacity: usize,
512 current_entry: usize,
513 viewing_entry: Option<usize>,
514 saved_line: Option<String>,
515}
516
517impl History {
518 /// Creates a new history buffer with the specified capacity.
519 ///
520 /// When the capacity is reached, the oldest entries are overwritten.
521 ///
522 /// # Arguments
523 ///
524 /// * `capacity` - Maximum number of history entries to store
525 pub fn new(capacity: usize) -> Self {
526 Self {
527 entries: Vec::with_capacity(capacity),
528 capacity,
529 current_entry: 0,
530 viewing_entry: None,
531 saved_line: None,
532 }
533 }
534
535 /// Adds a line to the history.
536 ///
537 /// Empty lines (including whitespace-only) and consecutive duplicates are automatically skipped.
538 /// When the buffer is full, the oldest entry is overwritten.
539 ///
540 /// # Arguments
541 ///
542 /// * `line` - The command line to add to history
543 pub fn add(&mut self, line: &str) {
544 let trimmed = line.trim();
545
546 // Skip empty or whitespace-only lines
547 if trimmed.is_empty() {
548 return;
549 }
550
551 // Skip if same as most recent (after trimming)
552 if let Some(last) = self.entries.last() {
553 if last == trimmed {
554 return;
555 }
556 }
557
558 if self.entries.len() < self.capacity {
559 self.entries.push(trimmed.to_string());
560 self.current_entry = self.entries.len() - 1;
561 } else {
562 // Circular buffer - overwrite oldest
563 self.current_entry = (self.current_entry + 1) % self.capacity;
564 self.entries[self.current_entry] = trimmed.to_string();
565 }
566
567 self.viewing_entry = None;
568 self.saved_line = None;
569 }
570
571 /// Navigates to the previous (older) history entry.
572 ///
573 /// On the first call, saves `current_line` so it can be restored when
574 /// navigating forward past the most recent entry.
575 ///
576 /// # Arguments
577 ///
578 /// * `current_line` - The current line content to save (only used on first call)
579 ///
580 /// # Returns
581 ///
582 /// `Some(&str)` with the previous history entry, or `None` if at the oldest entry.
583 pub fn previous(&mut self, current_line: &str) -> Option<&str> {
584 if self.entries.is_empty() {
585 return None;
586 }
587
588 match self.viewing_entry {
589 None => {
590 // First time - save current line and start at most recent
591 self.saved_line = Some(current_line.to_string());
592 self.viewing_entry = Some(self.current_entry);
593 Some(&self.entries[self.current_entry])
594 }
595 Some(idx) => {
596 // Go further back
597 if self.entries.len() < self.capacity {
598 // Haven't filled buffer yet
599 if idx > 0 {
600 let prev = idx - 1;
601 self.viewing_entry = Some(prev);
602 Some(&self.entries[prev])
603 } else {
604 None
605 }
606 } else {
607 // Buffer is full
608 let prev = (idx + self.capacity - 1) % self.capacity;
609 if prev == self.current_entry {
610 None
611 } else {
612 self.viewing_entry = Some(prev);
613 Some(&self.entries[prev])
614 }
615 }
616 }
617 }
618 }
619
620 /// Navigates to the next (newer) history entry.
621 ///
622 /// When reaching the end of history, returns the saved current line
623 /// that was passed to the first [`previous`](Self::previous) call.
624 ///
625 /// # Returns
626 ///
627 /// `Some(&str)` with the next history entry or saved line, or `None` if
628 /// not currently viewing history.
629 pub fn next_entry(&mut self) -> Option<&str> {
630 match self.viewing_entry {
631 None => None,
632 Some(idx) => {
633 if self.entries.len() < self.capacity {
634 // Haven't filled buffer yet
635 if idx < self.entries.len() - 1 {
636 let next = idx + 1;
637 self.viewing_entry = Some(next);
638 Some(&self.entries[next])
639 } else {
640 // Reached the end, return saved line
641 self.viewing_entry = None;
642 self.saved_line.as_deref()
643 }
644 } else {
645 // Buffer is full
646 let next = (idx + 1) % self.capacity;
647 if next == (self.current_entry + 1) % self.capacity {
648 // Reached the end, return saved line
649 self.viewing_entry = None;
650 self.saved_line.as_deref()
651 } else {
652 self.viewing_entry = Some(next);
653 Some(&self.entries[next])
654 }
655 }
656 }
657 }
658 }
659
660 /// Resets the history view to the current line.
661 ///
662 /// Called when the user starts typing to exit history browsing mode.
663 pub fn reset_view(&mut self) {
664 self.viewing_entry = None;
665 }
666}
667
668// Sync editor module
669#[cfg(feature = "sync")]
670mod sync_editor;
671
672#[cfg(feature = "sync")]
673pub use sync_editor::{Terminal, LineEditor};
674
675// Async editor module
676#[cfg(feature = "async")]
677mod async_editor;
678
679#[cfg(feature = "async")]
680pub use async_editor::{AsyncTerminal, AsyncLineEditor};
681
682// Re-export terminal implementations
683#[cfg(any(feature = "std", feature = "microbit", feature = "rp_pico_usb", feature = "rp_pico2_usb", feature = "embassy_usb"))]
684pub mod terminals;
685
686#[cfg(test)]
687mod tests {
688 use super::*;
689
690 // LineBuffer tests
691 #[test]
692 fn test_line_buffer_insert() {
693 let mut buf = LineBuffer::new(100);
694 buf.insert_char('h');
695 buf.insert_char('i');
696 assert_eq!(buf.as_str().unwrap(), "hi");
697 assert_eq!(buf.cursor_pos(), 2);
698 assert_eq!(buf.len(), 2);
699 }
700
701 #[test]
702 fn test_line_buffer_backspace() {
703 let mut buf = LineBuffer::new(100);
704 buf.insert_char('h');
705 buf.insert_char('i');
706 assert!(buf.delete_before_cursor());
707 assert_eq!(buf.as_str().unwrap(), "h");
708 assert_eq!(buf.cursor_pos(), 1);
709 }
710
711 #[test]
712 fn test_line_buffer_delete() {
713 let mut buf = LineBuffer::new(100);
714 buf.insert_char('h');
715 buf.insert_char('i');
716 buf.move_cursor_left();
717 assert!(buf.delete_at_cursor());
718 assert_eq!(buf.as_str().unwrap(), "h");
719 assert_eq!(buf.cursor_pos(), 1);
720 }
721
722 #[test]
723 fn test_line_buffer_cursor_movement() {
724 let mut buf = LineBuffer::new(100);
725 buf.insert_char('h');
726 buf.insert_char('e');
727 buf.insert_char('y');
728 assert_eq!(buf.cursor_pos(), 3);
729
730 assert!(buf.move_cursor_left());
731 assert_eq!(buf.cursor_pos(), 2);
732
733 assert!(buf.move_cursor_right());
734 assert_eq!(buf.cursor_pos(), 3);
735
736 assert!(!buf.move_cursor_right()); // at end
737 }
738
739 #[test]
740 fn test_line_buffer_home_end() {
741 let mut buf = LineBuffer::new(100);
742 buf.insert_char('h');
743 buf.insert_char('e');
744 buf.insert_char('y');
745
746 buf.move_cursor_to_start();
747 assert_eq!(buf.cursor_pos(), 0);
748
749 buf.move_cursor_to_end();
750 assert_eq!(buf.cursor_pos(), 3);
751 }
752
753 #[test]
754 fn test_line_buffer_word_navigation() {
755 let mut buf = LineBuffer::new(100);
756 for c in "hello world test".chars() {
757 buf.insert_char(c);
758 }
759
760 // At end: "hello world test|"
761 buf.move_cursor_word_left();
762 assert_eq!(buf.cursor_pos(), 12); // "hello world |test"
763
764 buf.move_cursor_word_left();
765 assert_eq!(buf.cursor_pos(), 6); // "hello |world test"
766
767 buf.move_cursor_word_right();
768 assert_eq!(buf.cursor_pos(), 12); // "hello world |test"
769 }
770
771 #[test]
772 fn test_line_buffer_delete_word() {
773 let mut buf = LineBuffer::new(100);
774 for c in "hello world".chars() {
775 buf.insert_char(c);
776 }
777
778 buf.delete_word_left();
779 assert_eq!(buf.as_str().unwrap(), "hello ");
780
781 buf.delete_word_left();
782 assert_eq!(buf.as_str().unwrap(), "");
783 }
784
785 #[test]
786 fn test_line_buffer_delete_word_right() {
787 let mut buf = LineBuffer::new(100);
788 for c in "hello world".chars() {
789 buf.insert_char(c);
790 }
791 buf.move_cursor_to_start();
792
793 buf.delete_word_right();
794 assert_eq!(buf.as_str().unwrap(), "world");
795 }
796
797 #[test]
798 fn test_line_buffer_insert_middle() {
799 let mut buf = LineBuffer::new(100);
800 buf.insert_char('h');
801 buf.insert_char('e');
802 buf.move_cursor_left();
803 buf.insert_char('x');
804 assert_eq!(buf.as_str().unwrap(), "hxe");
805 assert_eq!(buf.cursor_pos(), 2);
806 }
807
808 #[test]
809 fn test_word_navigation_with_symbols() {
810 let mut buf = LineBuffer::new(100);
811 for c in "3 + 5".chars() {
812 buf.insert_char(c);
813 }
814 // Cursor at end: "3 + 5|"
815
816 // Move left by word - should stop at '5'
817 buf.move_cursor_word_left();
818 assert_eq!(buf.cursor_pos(), 4); // Before '5'
819
820 // Move left by word - should stop at '+'
821 buf.move_cursor_word_left();
822 assert_eq!(buf.cursor_pos(), 2); // Before '+'
823
824 // Move left by word - should stop at '3'
825 buf.move_cursor_word_left();
826 assert_eq!(buf.cursor_pos(), 0); // Before '3'
827
828 // Move right by word - should stop after '3'
829 buf.move_cursor_word_right();
830 assert_eq!(buf.cursor_pos(), 2); // After '3 ', before '+'
831
832 // Move right by word - should stop after '+'
833 buf.move_cursor_word_right();
834 assert_eq!(buf.cursor_pos(), 4); // After '+ ', before '5'
835 }
836
837 #[test]
838 fn test_delete_word_with_symbols() {
839 let mut buf = LineBuffer::new(100);
840 for c in "3 + 5".chars() {
841 buf.insert_char(c);
842 }
843 // Cursor at end: "3 + 5|"
844
845 // Delete word left - should delete '5'
846 buf.delete_word_left();
847 assert_eq!(buf.as_str().unwrap(), "3 + ");
848
849 // Delete word left - should delete '+'
850 buf.delete_word_left();
851 assert_eq!(buf.as_str().unwrap(), "3 ");
852 }
853
854 // History tests
855 #[test]
856 fn test_history_add() {
857 let mut hist = History::new(10);
858 hist.add("first");
859 hist.add("second");
860
861 assert_eq!(hist.previous(""), Some("second"));
862 assert_eq!(hist.previous(""), Some("first"));
863 assert_eq!(hist.previous(""), None); // no more
864 }
865
866 #[test]
867 fn test_history_skip_empty() {
868 let mut hist = History::new(10);
869 hist.add("first");
870 hist.add("");
871 hist.add("second");
872
873 assert_eq!(hist.previous(""), Some("second"));
874 assert_eq!(hist.previous(""), Some("first"));
875 assert_eq!(hist.previous(""), None);
876 }
877
878 #[test]
879 fn test_history_skip_duplicates() {
880 let mut hist = History::new(10);
881 hist.add("test");
882 hist.add("test"); // should be skipped
883 hist.add("other");
884
885 assert_eq!(hist.previous(""), Some("other"));
886 assert_eq!(hist.previous(""), Some("test"));
887 assert_eq!(hist.previous(""), None);
888 }
889
890 #[test]
891 fn test_history_navigation() {
892 let mut hist = History::new(10);
893 hist.add("first");
894 hist.add("second");
895 hist.add("third");
896
897 // Go back through history
898 assert_eq!(hist.previous(""), Some("third"));
899 assert_eq!(hist.previous(""), Some("second"));
900
901 // Go forward
902 assert_eq!(hist.next_entry(), Some("third"));
903 assert_eq!(hist.next_entry(), Some("")); // returns saved line (empty string)
904 }
905
906 #[test]
907 fn test_history_saves_current_line() {
908 let mut hist = History::new(10);
909 hist.add("first");
910 hist.add("second");
911
912 // Start typing something
913 assert_eq!(hist.previous("hello"), Some("second"));
914 assert_eq!(hist.previous("hello"), Some("first"));
915
916 // Navigate back forward
917 assert_eq!(hist.next_entry(), Some("second"));
918 assert_eq!(hist.next_entry(), Some("hello")); // restored!
919 }
920
921 #[test]
922 fn test_history_down_without_up() {
923 let mut hist = History::new(10);
924 hist.add("first");
925
926 // Down without going up first should do nothing
927 assert_eq!(hist.next_entry(), None);
928 }
929
930 #[test]
931 fn test_history_circular_buffer() {
932 let mut hist = History::new(3);
933 hist.add("first");
934 hist.add("second");
935 hist.add("third");
936 hist.add("fourth"); // overwrites "first"
937
938 assert_eq!(hist.previous(""), Some("fourth"));
939 assert_eq!(hist.previous(""), Some("third"));
940 assert_eq!(hist.previous(""), Some("second"));
941 assert_eq!(hist.previous(""), None); // "first" was overwritten
942 }
943
944 #[test]
945 fn test_history_reset_view() {
946 let mut hist = History::new(10);
947 hist.add("first");
948 hist.add("second");
949
950 assert_eq!(hist.previous(""), Some("second"));
951 hist.reset_view();
952
953 // After reset, previous() should start from most recent again
954 assert_eq!(hist.previous(""), Some("second"));
955 }
956
957 #[test]
958 fn test_line_buffer_utf8() {
959 let mut buf = LineBuffer::new(100);
960 buf.insert_char('ä');
961 buf.insert_char('ö');
962 buf.insert_char('ü');
963 assert_eq!(buf.as_str().unwrap(), "äöü");
964 assert_eq!(buf.len(), 6); // UTF-8 bytes
965 }
966
967 #[test]
968 fn test_line_buffer_load() {
969 let mut buf = LineBuffer::new(100);
970 buf.insert_char('x');
971 buf.load("hello world");
972 assert_eq!(buf.as_str().unwrap(), "hello world");
973 assert_eq!(buf.cursor_pos(), 11);
974 }
975}