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}