aboutsummaryrefslogtreecommitdiffstats
path: root/default-plugins/sequence/src/ui/text_input.rs
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2026-02-17 15:43:28 +0100
committerGitHub <noreply@github.com>2026-02-17 15:43:28 +0100
commit4e66a0beff5e41bda1dbde521fa1d2858c46fe7b (patch)
tree380bee5eeac56bea2ebf341cccbe69615585e394 /default-plugins/sequence/src/ui/text_input.rs
parentFix startup message and cache file creation (#4705) (diff)
downloadsl0ppij-4e66a0beff5e41bda1dbde521fa1d2858c46fe7b.tar.gz
sl0ppij-4e66a0beff5e41bda1dbde521fa1d2858c46fe7b.tar.xz
sl0ppij-4e66a0beff5e41bda1dbde521fa1d2858c46fe7b.zip
Revamp the sequence plugin (#4713)
* initial pipe interface * initial open_x_pane_in_new_tab plugin api implementation * fix: send CommandPaneExited for command panes opened as initial_terminals in new tabs from plugins * open panes in place of specific pane id plugin commands - initial implementation * sequence and plugin api ux * focus self when starting * combine ui and pipe paths * highlight selected pane * initial sequence removal of editing capabilities * fix: translate plugin paths in api * properly trim commands when loaded from pipe or pasted text * minor ux fixes * get ctrl c to work properly * behavior fixes * more ui fixes * fix tests * close tab and end sequence properly * default to AND * zellij sequence alias * add alias * consolidate paths * rustfmt * fix ui padding * add pr * fix: reset scrolling when resetting terminal state
Diffstat (limited to 'default-plugins/sequence/src/ui/text_input.rs')
-rw-r--r--default-plugins/sequence/src/ui/text_input.rs997
1 files changed, 0 insertions, 997 deletions
diff --git a/default-plugins/sequence/src/ui/text_input.rs b/default-plugins/sequence/src/ui/text_input.rs
deleted file mode 100644
index 2694e17a..00000000
--- a/default-plugins/sequence/src/ui/text_input.rs
+++ /dev/null
@@ -1,997 +0,0 @@
-use zellij_tile::prelude::*;
-
-const MAX_UNDO_STACK_SIZE: usize = 100;
-
-/// Action returned by TextInput after handling a key event
-#[derive(Debug, Clone, PartialEq)]
-#[cfg_attr(test, derive(Eq))]
-pub enum InputAction {
- /// Continue editing
- Continue,
- /// User pressed Enter to submit
- Submit,
- /// User pressed Esc to cancel
- Cancel,
- /// User pressed Tab to request completion
- Complete,
- /// Key was not handled by the input
- NoAction,
-}
-
-/// A reusable text input component with cursor support and standard editing keybindings
-#[derive(Debug, Clone)]
-pub struct TextInput {
- buffer: String,
- cursor_position: usize, // Character position (0-based), NOT byte position
- undo_stack: Vec<(String, usize)>, // (buffer, cursor) snapshots
- redo_stack: Vec<(String, usize)>,
- last_edit_was_insert: bool, // For coalescing consecutive inserts
-}
-
-impl TextInput {
- /// Create a new TextInput with the given initial text
- /// Cursor is positioned at the end of the text
- pub fn new(initial_text: String) -> Self {
- let cursor_position = initial_text.chars().count();
- Self {
- buffer: initial_text,
- cursor_position,
- undo_stack: Vec::new(),
- redo_stack: Vec::new(),
- last_edit_was_insert: false,
- }
- }
-
- /// Create an empty TextInput
- pub fn empty() -> Self {
- Self::new(String::new())
- }
-
- /// Get the current text
- pub fn get_text(&self) -> &str {
- &self.buffer
- }
-
- /// Get the cursor position (in characters, not bytes)
- pub fn get_cursor_position(&self) -> usize {
- self.cursor_position
- }
-
- /// Check if the input is empty
- pub fn is_empty(&self) -> bool {
- self.buffer.is_empty()
- }
-
- /// Get a shorthand for cursor_position
- pub fn cursor_position(&self) -> usize {
- self.cursor_position
- }
-
- /// Get mutable access to the underlying buffer for direct manipulation
- pub fn get_text_mut(&mut self) -> &mut String {
- &mut self.buffer
- }
-
- /// Set the text and move cursor to the end
- pub fn set_text(&mut self, text: String) {
- self.break_coalescing();
- self.save_undo_state();
- self.cursor_position = text.chars().count();
- self.buffer = text;
- }
-
- /// Set cursor position (clamped to text length)
- pub fn set_cursor_position(&mut self, pos: usize) {
- let text_len = self.buffer.chars().count();
- self.cursor_position = pos.min(text_len);
- }
-
- /// Clear all text and reset cursor
- pub fn clear(&mut self) {
- self.break_coalescing();
- self.save_undo_state();
- self.buffer.clear();
- self.cursor_position = 0;
- }
-
- /// Insert a character at the current cursor position
- pub fn insert_char(&mut self, c: char) {
- self.save_undo_state_unless_coalescing();
- // Convert cursor position (char index) to byte index
- let byte_index = self.char_index_to_byte_index(self.cursor_position);
- self.buffer.insert(byte_index, c);
- self.cursor_position += 1;
- }
-
- /// Delete the character before the cursor (backspace)
- pub fn backspace(&mut self) {
- if self.cursor_position > 0 {
- self.break_coalescing();
- self.save_undo_state();
- self.cursor_position -= 1;
- let byte_index = self.char_index_to_byte_index(self.cursor_position);
- self.buffer.remove(byte_index);
- }
- }
-
- /// Delete the character at the cursor position (delete key)
- pub fn delete(&mut self) {
- let len = self.buffer.chars().count();
- if self.cursor_position < len {
- self.break_coalescing();
- self.save_undo_state();
- let byte_index = self.char_index_to_byte_index(self.cursor_position);
- self.buffer.remove(byte_index);
- }
- }
-
- /// Delete the word before the cursor (Ctrl/Alt + Backspace)
- pub fn delete_word_backward(&mut self) {
- if self.cursor_position == 0 {
- return;
- }
-
- self.break_coalescing();
- self.save_undo_state();
-
- let old_position = self.cursor_position;
- self.move_word_left();
- let new_position = self.cursor_position;
-
- // Delete from new position to old position
- let start_byte = self.char_index_to_byte_index(new_position);
- let end_byte = self.char_index_to_byte_index(old_position);
- self.buffer.drain(start_byte..end_byte);
- }
-
- /// Delete the word after the cursor (Ctrl/Alt + Delete)
- pub fn delete_word_forward(&mut self) {
- let chars: Vec<char> = self.buffer.chars().collect();
- let len = chars.len();
-
- if self.cursor_position >= len {
- return;
- }
-
- self.break_coalescing();
- self.save_undo_state();
-
- let start_position = self.cursor_position;
- let mut end_position = start_position;
-
- // Skip the current word
- while end_position < len && !chars[end_position].is_whitespace() {
- end_position += 1;
- }
-
- // Skip any whitespace after the word
- while end_position < len && chars[end_position].is_whitespace() {
- end_position += 1;
- }
-
- // Delete from start to end position
- let start_byte = self.char_index_to_byte_index(start_position);
- let end_byte = self.char_index_to_byte_index(end_position);
- self.buffer.drain(start_byte..end_byte);
- }
-
- /// Move cursor one position to the left
- pub fn move_left(&mut self) {
- if self.cursor_position > 0 {
- self.break_coalescing();
- self.cursor_position -= 1;
- }
- }
-
- /// Move cursor one position to the right
- pub fn move_right(&mut self) {
- let len = self.buffer.chars().count();
- if self.cursor_position < len {
- self.break_coalescing();
- self.cursor_position += 1;
- }
- }
-
- /// Move cursor to the start of the text (Ctrl-A / Home)
- pub fn move_to_start(&mut self) {
- self.break_coalescing();
- self.cursor_position = 0;
- }
-
- /// Move cursor to the end of the text (Ctrl-E / End)
- pub fn move_to_end(&mut self) {
- self.break_coalescing();
- self.cursor_position = self.buffer.chars().count();
- }
-
- /// Move cursor to the start of the previous word (Ctrl/Alt + Left)
- pub fn move_word_left(&mut self) {
- if self.cursor_position == 0 {
- return;
- }
-
- self.break_coalescing();
-
- let chars: Vec<char> = self.buffer.chars().collect();
- let mut pos = self.cursor_position;
-
- // Skip any whitespace immediately to the left
- while pos > 0 && chars[pos - 1].is_whitespace() {
- pos -= 1;
- }
-
- // Skip the word characters
- while pos > 0 && !chars[pos - 1].is_whitespace() {
- pos -= 1;
- }
-
- self.cursor_position = pos;
- }
-
- /// Move cursor to the start of the next word (Ctrl/Alt + Right)
- pub fn move_word_right(&mut self) {
- let chars: Vec<char> = self.buffer.chars().collect();
- let len = chars.len();
-
- if self.cursor_position >= len {
- return;
- }
-
- self.break_coalescing();
-
- let mut pos = self.cursor_position;
-
- // Skip the current word
- while pos < len && !chars[pos].is_whitespace() {
- pos += 1;
- }
-
- // Skip any whitespace
- while pos < len && chars[pos].is_whitespace() {
- pos += 1;
- }
-
- self.cursor_position = pos;
- }
-
- /// Handle a key event and return the appropriate action
- /// This is the main entry point for key handling
- pub fn handle_key(&mut self, key: KeyWithModifier) -> InputAction {
- // Check for Ctrl modifiers
- if key.has_modifiers(&[KeyModifier::Ctrl]) {
- match key.bare_key {
- BareKey::Char('a') => {
- self.move_to_start();
- return InputAction::Continue;
- },
- BareKey::Char('e') => {
- self.move_to_end();
- return InputAction::Continue;
- },
- BareKey::Char('c') => {
- // Ctrl-C clears the prompt
- return InputAction::Cancel;
- },
- BareKey::Char('z') => {
- // Ctrl-Z: Undo
- self.undo();
- return InputAction::Continue;
- },
- BareKey::Char('y') => {
- // Ctrl-Y: Redo
- self.redo();
- return InputAction::Continue;
- },
- BareKey::Left => {
- self.move_word_left();
- return InputAction::Continue;
- },
- BareKey::Right => {
- self.move_word_right();
- return InputAction::Continue;
- },
- BareKey::Backspace => {
- self.delete_word_backward();
- return InputAction::Continue;
- },
- BareKey::Delete => {
- self.delete_word_forward();
- return InputAction::Continue;
- },
- _ => {},
- }
- }
-
- // Check for Ctrl+Shift modifiers (alternative redo: Ctrl+Shift+Z)
- if key.has_modifiers(&[KeyModifier::Ctrl, KeyModifier::Shift]) {
- match key.bare_key {
- BareKey::Char('Z') => {
- // Ctrl-Shift-Z: Redo (alternative)
- self.redo();
- return InputAction::Continue;
- },
- _ => {},
- }
- }
-
- // Check for Alt modifiers
- if key.has_modifiers(&[KeyModifier::Alt]) {
- match key.bare_key {
- BareKey::Left => {
- self.move_word_left();
- return InputAction::Continue;
- },
- BareKey::Right => {
- self.move_word_right();
- return InputAction::Continue;
- },
- BareKey::Backspace => {
- self.delete_word_backward();
- return InputAction::Continue;
- },
- BareKey::Delete => {
- self.delete_word_forward();
- return InputAction::Continue;
- },
- _ => {},
- }
- }
-
- // Handle bare keys (no modifiers)
- match key.bare_key {
- BareKey::Enter => InputAction::Submit,
- BareKey::Esc => InputAction::Cancel,
- BareKey::Tab => InputAction::Complete,
- BareKey::Backspace => {
- self.backspace();
- InputAction::Continue
- },
- BareKey::Delete => {
- self.delete();
- InputAction::Continue
- },
- BareKey::Left => {
- self.move_left();
- InputAction::Continue
- },
- BareKey::Right => {
- self.move_right();
- InputAction::Continue
- },
- BareKey::Home => {
- self.move_to_start();
- InputAction::Continue
- },
- BareKey::End => {
- self.move_to_end();
- InputAction::Continue
- },
- BareKey::Char(c) => {
- self.insert_char(c);
- InputAction::Continue
- },
- _ => InputAction::NoAction,
- }
- }
-
- /// Helper: Convert character index to byte index
- fn char_index_to_byte_index(&self, char_index: usize) -> usize {
- self.buffer
- .char_indices()
- .nth(char_index)
- .map(|(byte_idx, _)| byte_idx)
- .unwrap_or(self.buffer.len())
- }
-
- /// Save current state to undo stack before making changes
- fn save_undo_state(&mut self) {
- if self.undo_stack.len() >= MAX_UNDO_STACK_SIZE {
- self.undo_stack.remove(0);
- }
- self.undo_stack
- .push((self.buffer.clone(), self.cursor_position));
- self.redo_stack.clear();
- }
-
- /// Save state only if not coalescing with previous insert
- fn save_undo_state_unless_coalescing(&mut self) {
- if !self.last_edit_was_insert {
- self.save_undo_state();
- }
- self.last_edit_was_insert = true;
- }
-
- /// Mark that a non-insert edit occurred (breaks coalescing)
- fn break_coalescing(&mut self) {
- self.last_edit_was_insert = false;
- }
-
- /// Undo last change
- pub fn undo(&mut self) -> bool {
- if let Some((buffer, cursor)) = self.undo_stack.pop() {
- self.redo_stack
- .push((self.buffer.clone(), self.cursor_position));
- self.buffer = buffer;
- self.cursor_position = cursor;
- self.break_coalescing();
- true
- } else {
- false
- }
- }
-
- /// Redo last undone change
- pub fn redo(&mut self) -> bool {
- if let Some((buffer, cursor)) = self.redo_stack.pop() {
- self.undo_stack
- .push((self.buffer.clone(), self.cursor_position));
- self.buffer = buffer;
- self.cursor_position = cursor;
- self.break_coalescing();
- true
- } else {
- false
- }
- }
-
- /// Check if undo is available
- pub fn can_undo(&self) -> bool {
- !self.undo_stack.is_empty()
- }
-
- /// Check if redo is available
- pub fn can_redo(&self) -> bool {
- !self.redo_stack.is_empty()
- }
-
- pub fn drain_text(&mut self) -> String {
- self.cursor_position = 0;
- self.buffer.drain(..).collect()
- }
-}
-
-// run with:
-// cargo test --lib --target x86_64-unknown-linux-gnu
-//
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_new_and_empty() {
- let input = TextInput::new("hello".to_string());
- assert_eq!(input.get_text(), "hello");
- assert_eq!(input.get_cursor_position(), 5);
-
- let empty = TextInput::empty();
- assert_eq!(empty.get_text(), "");
- assert_eq!(empty.get_cursor_position(), 0);
- }
-
- #[test]
- fn test_insert_char() {
- let mut input = TextInput::new("helo".to_string());
- input.cursor_position = 3; // Position after "hel"
- input.insert_char('l');
- assert_eq!(input.get_text(), "hello");
- assert_eq!(input.get_cursor_position(), 4);
- }
-
- #[test]
- fn test_backspace() {
- let mut input = TextInput::new("hello".to_string());
- input.backspace();
- assert_eq!(input.get_text(), "hell");
- assert_eq!(input.get_cursor_position(), 4);
-
- // Backspace at start does nothing
- input.cursor_position = 0;
- input.backspace();
- assert_eq!(input.get_text(), "hell");
- assert_eq!(input.get_cursor_position(), 0);
- }
-
- #[test]
- fn test_delete() {
- let mut input = TextInput::new("hello".to_string());
- input.cursor_position = 0;
- input.delete();
- assert_eq!(input.get_text(), "ello");
- assert_eq!(input.get_cursor_position(), 0);
-
- // Delete at end does nothing
- input.move_to_end();
- input.delete();
- assert_eq!(input.get_text(), "ello");
- }
-
- #[test]
- fn test_cursor_movement() {
- let mut input = TextInput::new("hello".to_string());
- assert_eq!(input.get_cursor_position(), 5);
-
- input.move_left();
- assert_eq!(input.get_cursor_position(), 4);
-
- input.move_right();
- assert_eq!(input.get_cursor_position(), 5);
-
- input.move_to_start();
- assert_eq!(input.get_cursor_position(), 0);
-
- input.move_to_end();
- assert_eq!(input.get_cursor_position(), 5);
- }
-
- #[test]
- fn test_unicode_support() {
- let mut input = TextInput::new("hello 🦀 world".to_string());
- assert_eq!(input.get_cursor_position(), 13); // 13 characters
-
- input.cursor_position = 6; // After "hello "
- input.insert_char('🐱');
- assert_eq!(input.get_text(), "hello 🐱🦀 world");
- }
-
- #[test]
- fn test_word_jump_right() {
- let mut input = TextInput::new("hello world foo bar".to_string());
- input.cursor_position = 0;
-
- // Jump from start to "world"
- input.move_word_right();
- assert_eq!(input.get_cursor_position(), 6); // After "hello "
-
- // Jump to "foo"
- input.move_word_right();
- assert_eq!(input.get_cursor_position(), 12); // After "world "
-
- // Jump to "bar"
- input.move_word_right();
- assert_eq!(input.get_cursor_position(), 16); // After "foo "
-
- // Jump to end
- input.move_word_right();
- assert_eq!(input.get_cursor_position(), 19); // At end
- }
-
- #[test]
- fn test_word_jump_left() {
- let mut input = TextInput::new("hello world foo bar".to_string());
- input.move_to_end();
- assert_eq!(input.get_cursor_position(), 19);
-
- // Jump back to "bar"
- input.move_word_left();
- assert_eq!(input.get_cursor_position(), 16); // Start of "bar"
-
- // Jump back to "foo"
- input.move_word_left();
- assert_eq!(input.get_cursor_position(), 12); // Start of "foo"
-
- // Jump back to "world"
- input.move_word_left();
- assert_eq!(input.get_cursor_position(), 6); // Start of "world"
-
- // Jump back to "hello"
- input.move_word_left();
- assert_eq!(input.get_cursor_position(), 0); // Start of "hello"
- }
-
- #[test]
- fn test_word_jump_with_multiple_spaces() {
- let mut input = TextInput::new("hello world".to_string());
- input.cursor_position = 0;
-
- // Jump over multiple spaces
- input.move_word_right();
- assert_eq!(input.get_cursor_position(), 8); // After "hello ", at start of "world"
-
- // Jump back should skip spaces
- input.move_word_left();
- assert_eq!(input.get_cursor_position(), 0); // Back to start of "hello"
- }
-
- #[test]
- fn test_word_jump_boundaries() {
- let mut input = TextInput::new("test".to_string());
-
- // At start - word left does nothing
- input.cursor_position = 0;
- input.move_word_left();
- assert_eq!(input.get_cursor_position(), 0);
-
- // At end - word right does nothing
- input.move_to_end();
- let end_pos = input.get_cursor_position();
- input.move_word_right();
- assert_eq!(input.get_cursor_position(), end_pos);
- }
-
- #[test]
- fn test_up_down_arrows() {
- let mut input = TextInput::new("hello world".to_string());
-
- // Start in the middle
- input.cursor_position = 5;
- assert_eq!(input.get_cursor_position(), 5);
-
- // Up arrow should go to start
- input.move_to_start();
- assert_eq!(input.get_cursor_position(), 0);
-
- // Move back to middle
- input.cursor_position = 5;
-
- // Down arrow should go to end
- input.move_to_end();
- assert_eq!(input.get_cursor_position(), 11);
- }
-
- #[test]
- fn test_delete_word_backward() {
- let mut input = TextInput::new("hello world foo".to_string());
-
- // Delete "foo" from end
- input.move_to_end();
- input.delete_word_backward();
- assert_eq!(input.get_text(), "hello world ");
- assert_eq!(input.get_cursor_position(), 12);
-
- // Delete "world "
- input.delete_word_backward();
- assert_eq!(input.get_text(), "hello ");
- assert_eq!(input.get_cursor_position(), 6);
-
- // Delete "hello "
- input.delete_word_backward();
- assert_eq!(input.get_text(), "");
- assert_eq!(input.get_cursor_position(), 0);
-
- // Delete on empty buffer does nothing
- input.delete_word_backward();
- assert_eq!(input.get_text(), "");
- assert_eq!(input.get_cursor_position(), 0);
- }
-
- #[test]
- fn test_delete_word_backward_middle() {
- let mut input = TextInput::new("hello world foo".to_string());
-
- // Position in middle of "world"
- input.cursor_position = 8; // After "hello wo"
- input.delete_word_backward();
- assert_eq!(input.get_text(), "hello rld foo");
- assert_eq!(input.get_cursor_position(), 6); // After "hello "
- }
-
- #[test]
- fn test_delete_word_forward() {
- let mut input = TextInput::new("hello world foo".to_string());
-
- // Delete "hello " from start
- input.cursor_position = 0;
- input.delete_word_forward();
- assert_eq!(input.get_text(), "world foo");
- assert_eq!(input.get_cursor_position(), 0);
-
- // Delete "world "
- input.delete_word_forward();
- assert_eq!(input.get_text(), "foo");
- assert_eq!(input.get_cursor_position(), 0);
-
- // Delete "foo"
- input.delete_word_forward();
- assert_eq!(input.get_text(), "");
- assert_eq!(input.get_cursor_position(), 0);
-
- // Delete on empty buffer does nothing
- input.delete_word_forward();
- assert_eq!(input.get_text(), "");
- assert_eq!(input.get_cursor_position(), 0);
- }
-
- #[test]
- fn test_delete_word_forward_middle() {
- let mut input = TextInput::new("hello world foo".to_string());
-
- // Position in middle of "world"
- input.cursor_position = 8; // After "hello wo"
- input.delete_word_forward();
- assert_eq!(input.get_text(), "hello wofoo");
- assert_eq!(input.get_cursor_position(), 8); // Same position, text deleted forward
- }
-
- #[test]
- fn test_delete_word_with_multiple_spaces() {
- let mut input = TextInput::new("hello world".to_string());
-
- // Delete forward includes trailing spaces
- input.cursor_position = 0;
- input.delete_word_forward();
- assert_eq!(input.get_text(), "world");
- assert_eq!(input.get_cursor_position(), 0);
- }
-
- #[test]
- fn test_undo_redo_basic() {
- let mut input = TextInput::empty();
-
- // Type "hello"
- input.insert_char('h');
- input.insert_char('e');
- input.insert_char('l');
- input.insert_char('l');
- input.insert_char('o');
- assert_eq!(input.get_text(), "hello");
-
- // Undo should remove all characters (coalesced into one undo entry)
- assert!(input.can_undo());
- assert!(input.undo());
- assert_eq!(input.get_text(), "");
- assert_eq!(input.get_cursor_position(), 0);
-
- // Redo should restore "hello"
- assert!(input.can_redo());
- assert!(input.redo());
- assert_eq!(input.get_text(), "hello");
- assert_eq!(input.get_cursor_position(), 5);
- }
-
- #[test]
- fn test_undo_coalescing_breaks_on_cursor_move() {
- let mut input = TextInput::empty();
-
- // Type "he"
- input.insert_char('h');
- input.insert_char('e');
-
- // Move cursor to start (breaks coalescing)
- input.move_to_start();
-
- // Type "llo"
- input.insert_char('l');
- input.insert_char('l');
- input.insert_char('o');
-
- assert_eq!(input.get_text(), "llohe");
-
- // First undo removes "llo" (second coalesced group)
- input.undo();
- assert_eq!(input.get_text(), "he");
-
- // Second undo removes "he" (first coalesced group)
- input.undo();
- assert_eq!(input.get_text(), "");
- }
-
- #[test]
- fn test_undo_backspace() {
- let mut input = TextInput::new("hello".to_string());
-
- // Backspace once
- input.backspace();
- assert_eq!(input.get_text(), "hell");
-
- // Undo should restore "hello"
- input.undo();
- assert_eq!(input.get_text(), "hello");
- assert_eq!(input.get_cursor_position(), 5);
- }
-
- #[test]
- fn test_undo_delete() {
- let mut input = TextInput::new("hello".to_string());
- input.cursor_position = 0;
-
- // Delete first character
- input.delete();
- assert_eq!(input.get_text(), "ello");
-
- // Undo should restore "hello"
- input.undo();
- assert_eq!(input.get_text(), "hello");
- assert_eq!(input.get_cursor_position(), 0);
- }
-
- #[test]
- fn test_undo_word_delete() {
- let mut input = TextInput::new("hello world".to_string());
-
- // Delete "world" backward
- input.delete_word_backward();
- assert_eq!(input.get_text(), "hello ");
-
- // Undo should restore "hello world"
- input.undo();
- assert_eq!(input.get_text(), "hello world");
- assert_eq!(input.get_cursor_position(), 11);
- }
-
- #[test]
- fn test_undo_clear() {
- let mut input = TextInput::new("hello world".to_string());
-
- // Clear the buffer
- input.clear();
- assert_eq!(input.get_text(), "");
-
- // Undo should restore the text
- input.undo();
- assert_eq!(input.get_text(), "hello world");
- }
-
- #[test]
- fn test_undo_set_text() {
- let mut input = TextInput::new("hello".to_string());
-
- // Replace with new text
- input.set_text("goodbye".to_string());
- assert_eq!(input.get_text(), "goodbye");
-
- // Undo should restore "hello"
- input.undo();
- assert_eq!(input.get_text(), "hello");
- }
-
- #[test]
- fn test_redo_clears_on_new_edit() {
- let mut input = TextInput::empty();
-
- // Type "hello"
- input.insert_char('h');
- input.insert_char('e');
- input.insert_char('l');
- input.insert_char('l');
- input.insert_char('o');
-
- // Undo
- input.undo();
- assert_eq!(input.get_text(), "");
- assert!(input.can_redo());
-
- // Make a new edit (should clear redo stack)
- input.insert_char('x');
- assert!(!input.can_redo());
- }
-
- #[test]
- fn test_multiple_undo_redo() {
- let mut input = TextInput::empty();
-
- // First edit: type "hello"
- for c in "hello".chars() {
- input.insert_char(c);
- }
-
- // Break coalescing
- input.move_left();
-
- // Second edit: type "world"
- for c in "world".chars() {
- input.insert_char(c);
- }
-
- assert_eq!(input.get_text(), "hellworldo");
-
- // Undo "world"
- input.undo();
- assert_eq!(input.get_text(), "hello");
-
- // Undo "hello"
- input.undo();
- assert_eq!(input.get_text(), "");
-
- // Redo "hello"
- input.redo();
- assert_eq!(input.get_text(), "hello");
-
- // Redo "world"
- input.redo();
- assert_eq!(input.get_text(), "hellworldo");
- }
-
- #[test]
- fn test_undo_stack_limit() {
- let mut input = TextInput::empty();
-
- // Perform 102 separate edits (breaking coalescing each time)
- for _i in 0..102 {
- input.backspace(); // Break coalescing
- input.insert_char('x');
- }
-
- // Should have at most 100 undo entries
- let mut undo_count = 0;
- while input.undo() {
- undo_count += 1;
- }
-
- // We should have 100 undo entries (the stack limit)
- // Plus the final state change from the last coalescing break
- assert!(
- undo_count <= 100,
- "Undo count should be at most 100, got {}",
- undo_count
- );
- }
-
- #[test]
- fn test_undo_redo_empty_stack() {
- let mut input = TextInput::empty();
-
- // Undo on empty stack should return false
- assert!(!input.can_undo());
- assert!(!input.undo());
-
- // Redo on empty stack should return false
- assert!(!input.can_redo());
- assert!(!input.redo());
- }
-
- #[test]
- fn test_undo_restores_cursor_position() {
- let mut input = TextInput::new("hello world".to_string());
-
- // Move cursor to position 5 (before "world")
- input.cursor_position = 5;
-
- // Insert a space
- input.insert_char(' ');
- assert_eq!(input.get_text(), "hello world");
- assert_eq!(input.get_cursor_position(), 6);
-
- // Undo should restore both text and cursor position
- input.undo();
- assert_eq!(input.get_text(), "hello world");
- assert_eq!(input.get_cursor_position(), 5);
- }
-
- #[test]
- fn test_coalescing_consecutive_inserts() {
- let mut input = TextInput::empty();
-
- // Type several characters
- input.insert_char('a');
- input.insert_char('b');
- input.insert_char('c');
-
- assert_eq!(input.get_text(), "abc");
-
- // Single undo should remove all three (they were coalesced)
- input.undo();
- assert_eq!(input.get_text(), "");
-
- // No more undo available
- assert!(!input.can_undo());
- }
-
- #[test]
- fn test_backspace_breaks_coalescing() {
- let mut input = TextInput::empty();
-
- // Type "ab"
- input.insert_char('a');
- input.insert_char('b');
-
- // Backspace
- input.backspace();
- assert_eq!(input.get_text(), "a");
-
- // Type "c"
- input.insert_char('c');
- assert_eq!(input.get_text(), "ac");
-
- // Undo should remove just "c"
- input.undo();
- assert_eq!(input.get_text(), "a");
-
- // Undo should remove backspace operation
- input.undo();
- assert_eq!(input.get_text(), "ab");
-
- // Undo should remove "ab"
- input.undo();
- assert_eq!(input.get_text(), "");
- }
-}