diff --git a/canvas/examples/canvas_textarea_cursor_auto.rs b/canvas/examples/canvas_textarea_cursor_auto.rs index c477050..2fed485 100644 --- a/canvas/examples/canvas_textarea_cursor_auto.rs +++ b/canvas/examples/canvas_textarea_cursor_auto.rs @@ -22,7 +22,7 @@ compile_error!( use std::io; use crossterm::{ event::{ - self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, + self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyModifiers, }, execute, terminal::{ @@ -47,11 +47,11 @@ use canvas::{ }; /// Enhanced TextArea that demonstrates automatic cursor management +/// Now uses direct FormEditor method calls via Deref! struct AutoCursorTextArea { textarea: TextAreaState, has_unsaved_changes: bool, debug_message: String, - mode: AppMode, command_buffer: String, } @@ -66,7 +66,8 @@ Try different modes:\n\ \n\ Navigation commands:\n\ • hjkl or arrow keys: move cursor\n\ -• i: enter insert mode\n\ +• i/a/A/o/O: enter insert mode\n\ +• w/b/e/W/B/E: word movements\n\ • Esc: return to normal mode\n\ \n\ Watch how the terminal cursor changes automatically!\n\ @@ -81,7 +82,6 @@ Press ? for help, F1/F2 for manual cursor control demo."; textarea, has_unsaved_changes: false, debug_message: "🎯 Automatic Cursor Demo - cursor-style feature enabled!".to_string(), - mode: AppMode::ReadOnly, command_buffer: String::new(), } } @@ -89,20 +89,21 @@ Press ? for help, F1/F2 for manual cursor control demo."; // === MODE TRANSITIONS WITH AUTOMATIC CURSOR MANAGEMENT === fn enter_insert_mode(&mut self) -> std::io::Result<()> { - self.mode = AppMode::Edit; + self.textarea.enter_edit_mode(); // 🎯 Direct FormEditor method call via Deref! CursorManager::update_for_mode(AppMode::Edit)?; // 🎯 Automatic: cursor becomes bar | self.debug_message = "✏️ INSERT MODE - Cursor: Steady Bar |".to_string(); Ok(()) } fn enter_append_mode(&mut self) -> std::io::Result<()> { - // Move cursor to end of current line, then enter insert mode - self.send_key_to_textarea(KeyCode::End, KeyModifiers::NONE); - self.enter_insert_mode() + self.textarea.enter_append_mode(); // 🎯 Direct FormEditor method call! + CursorManager::update_for_mode(AppMode::Edit)?; + self.debug_message = "✏️ INSERT (append) - Cursor: Steady Bar |".to_string(); + Ok(()) } fn exit_to_normal_mode(&mut self) -> std::io::Result<()> { - self.mode = AppMode::ReadOnly; + self.textarea.exit_edit_mode(); // 🎯 Direct FormEditor method call! CursorManager::update_for_mode(AppMode::ReadOnly)?; // 🎯 Automatic: cursor becomes steady block self.debug_message = "🔒 NORMAL MODE - Cursor: Steady Block █".to_string(); Ok(()) @@ -119,7 +120,7 @@ Press ? for help, F1/F2 for manual cursor control demo."; fn restore_automatic_cursor(&mut self) -> std::io::Result<()> { // Restore automatic cursor based on current mode - CursorManager::update_for_mode(self.mode)?; + CursorManager::update_for_mode(self.textarea.mode())?; // 🎯 Direct method call! self.debug_message = "🎯 Restored automatic cursor management".to_string(); Ok(()) } @@ -128,85 +129,93 @@ Press ? for help, F1/F2 for manual cursor control demo."; fn handle_textarea_input(&mut self, key: KeyEvent) { self.textarea.input(key); - if key.code != KeyCode::Left && key.code != KeyCode::Right - && key.code != KeyCode::Up && key.code != KeyCode::Down - && key.code != KeyCode::Home && key.code != KeyCode::End { - self.has_unsaved_changes = true; - } + self.has_unsaved_changes = true; } - fn send_key_to_textarea(&mut self, code: KeyCode, modifiers: KeyModifiers) { - let key_event = KeyEvent { - code, - modifiers, - kind: KeyEventKind::Press, - state: crossterm::event::KeyEventState::NONE, - }; - self.textarea.input(key_event); - } - - // === MOVEMENT OPERATIONS (for normal mode) === + // === MOVEMENT OPERATIONS (using direct FormEditor methods!) === fn move_left(&mut self) { - self.send_key_to_textarea(KeyCode::Left, KeyModifiers::NONE); + self.textarea.move_left(); // 🎯 Direct FormEditor method call! self.update_debug_for_movement("← left"); } fn move_right(&mut self) { - self.send_key_to_textarea(KeyCode::Right, KeyModifiers::NONE); + self.textarea.move_right(); // 🎯 Direct FormEditor method call! self.update_debug_for_movement("→ right"); } fn move_up(&mut self) { - self.send_key_to_textarea(KeyCode::Up, KeyModifiers::NONE); + self.textarea.move_up(); // 🎯 Direct FormEditor method call! self.update_debug_for_movement("↑ up"); } fn move_down(&mut self) { - self.send_key_to_textarea(KeyCode::Down, KeyModifiers::NONE); + self.textarea.move_down(); // 🎯 Direct FormEditor method call! self.update_debug_for_movement("↓ down"); } fn move_word_next(&mut self) { - // Use Alt+f for word forward (from textarea implementation) - self.send_key_to_textarea(KeyCode::Char('f'), KeyModifiers::ALT); + self.textarea.move_word_next(); // 🎯 Direct FormEditor method call! self.update_debug_for_movement("w: next word"); } fn move_word_prev(&mut self) { - // Use Alt+b for word backward (from textarea implementation) - self.send_key_to_textarea(KeyCode::Char('b'), KeyModifiers::ALT); + self.textarea.move_word_prev(); // 🎯 Direct FormEditor method call! self.update_debug_for_movement("b: previous word"); } fn move_word_end(&mut self) { - // Use Alt+e for word end (from textarea implementation) - self.send_key_to_textarea(KeyCode::Char('e'), KeyModifiers::ALT); + self.textarea.move_word_end(); // 🎯 Direct FormEditor method call! self.update_debug_for_movement("e: word end"); } + fn move_word_end_prev(&mut self) { + self.textarea.move_word_end_prev(); // 🎯 Direct FormEditor method call! + self.update_debug_for_movement("ge: previous word end"); + } + fn move_line_start(&mut self) { - self.send_key_to_textarea(KeyCode::Home, KeyModifiers::NONE); + self.textarea.move_line_start(); // 🎯 Direct FormEditor method call! self.update_debug_for_movement("0: line start"); } fn move_line_end(&mut self) { - self.send_key_to_textarea(KeyCode::End, KeyModifiers::NONE); + self.textarea.move_line_end(); // 🎯 Direct FormEditor method call! self.update_debug_for_movement("$: line end"); } fn move_first_line(&mut self) { - // Move to very beginning of text - self.send_key_to_textarea(KeyCode::Char('a'), KeyModifiers::CONTROL); + self.textarea.move_first_line(); // 🎯 Direct FormEditor method call! self.update_debug_for_movement("gg: first line"); } fn move_last_line(&mut self) { - // Move to very end of text - self.send_key_to_textarea(KeyCode::Char('e'), KeyModifiers::CONTROL); + self.textarea.move_last_line(); // 🎯 Direct FormEditor method call! self.update_debug_for_movement("G: last line"); } + // === BIG WORD MOVEMENTS === + + fn move_big_word_next(&mut self) { + self.textarea.move_big_word_next(); // 🎯 Direct FormEditor method call! + self.update_debug_for_movement("W: next WORD"); + } + + fn move_big_word_prev(&mut self) { + self.textarea.move_big_word_prev(); // 🎯 Direct FormEditor method call! + self.update_debug_for_movement("B: previous WORD"); + } + + fn move_big_word_end(&mut self) { + self.textarea.move_big_word_end(); // 🎯 Direct FormEditor method call! + self.update_debug_for_movement("E: WORD end"); + } + + fn move_big_word_end_prev(&mut self) { + self.textarea.move_big_word_end_prev(); // 🎯 Direct FormEditor method call! + self.update_debug_for_movement("gE: previous WORD end"); + } + fn update_debug_for_movement(&mut self, action: &str) { self.debug_message = action.to_string(); } @@ -214,15 +223,39 @@ Press ? for help, F1/F2 for manual cursor control demo."; // === DELETE OPERATIONS === fn delete_char_forward(&mut self) { - self.send_key_to_textarea(KeyCode::Delete, KeyModifiers::NONE); - self.has_unsaved_changes = true; - self.debug_message = "x: deleted character".to_string(); + if let Ok(_) = self.textarea.delete_forward() { // 🎯 Direct FormEditor method call! + self.has_unsaved_changes = true; + self.debug_message = "x: deleted character".to_string(); + } } fn delete_char_backward(&mut self) { - self.send_key_to_textarea(KeyCode::Backspace, KeyModifiers::NONE); - self.has_unsaved_changes = true; - self.debug_message = "X: deleted character backward".to_string(); + if let Ok(_) = self.textarea.delete_backward() { // 🎯 Direct FormEditor method call! + self.has_unsaved_changes = true; + self.debug_message = "X: deleted character backward".to_string(); + } + } + + // === VIM-STYLE EDITING === + + fn open_line_below(&mut self) -> anyhow::Result<()> { + let result = self.textarea.open_line_below(); // 🎯 Textarea-specific override! + if result.is_ok() { + CursorManager::update_for_mode(AppMode::Edit)?; + self.debug_message = "✏️ INSERT (open line below) - Cursor: Steady Bar |".to_string(); + self.has_unsaved_changes = true; + } + result + } + + fn open_line_above(&mut self) -> anyhow::Result<()> { + let result = self.textarea.open_line_above(); // 🎯 Textarea-specific override! + if result.is_ok() { + CursorManager::update_for_mode(AppMode::Edit)?; + self.debug_message = "✏️ INSERT (open line above) - Cursor: Steady Bar |".to_string(); + self.has_unsaved_changes = true; + } + result } // === COMMAND BUFFER HANDLING === @@ -246,7 +279,7 @@ Press ? for help, F1/F2 for manual cursor control demo."; // === GETTERS === fn mode(&self) -> AppMode { - self.mode + self.textarea.mode() // 🎯 Direct FormEditor method call! } fn debug_message(&self) -> &str { @@ -262,8 +295,11 @@ Press ? for help, F1/F2 for manual cursor control demo."; } fn get_cursor_info(&self) -> String { - // Since we can't access the internal editor, we'll provide a simpler status - format!("Textarea cursor positioned") + format!( + "Line {}, Col {}", + self.textarea.current_field() + 1, // 🎯 Direct FormEditor method call! + self.textarea.cursor_position() + 1 // 🎯 Direct FormEditor method call! + ) } } @@ -299,6 +335,20 @@ fn handle_key_press( editor.clear_command_buffer(); } + // Vim o/O commands + (AppMode::ReadOnly, KeyCode::Char('o'), _) => { + if let Err(e) = editor.open_line_below() { + editor.set_debug_message(format!("Error opening line below: {}", e)); + } + editor.clear_command_buffer(); + } + (AppMode::ReadOnly, KeyCode::Char('O'), _) => { + if let Err(e) = editor.open_line_above() { + editor.set_debug_message(format!("Error opening line above: {}", e)); + } + editor.clear_command_buffer(); + } + // Escape: Exit any mode back to normal (AppMode::Edit, KeyCode::Esc, _) => { editor.exit_to_normal_mode()?; @@ -350,9 +400,7 @@ fn handle_key_press( } (AppMode::ReadOnly, KeyCode::Char('e'), _) => { if editor.get_command_buffer() == "g" { - // TODO: Implement ge (previous word end) - editor.move_word_prev(); - editor.set_debug_message("ge: previous word end (simplified)".to_string()); + editor.move_word_end_prev(); editor.clear_command_buffer(); } else { editor.move_word_end(); @@ -360,6 +408,25 @@ fn handle_key_press( } } + // Big word movement (vim W/B/E commands) + (AppMode::ReadOnly, KeyCode::Char('W'), _) => { + editor.move_big_word_next(); + editor.clear_command_buffer(); + } + (AppMode::ReadOnly, KeyCode::Char('B'), _) => { + editor.move_big_word_prev(); + editor.clear_command_buffer(); + } + (AppMode::ReadOnly, KeyCode::Char('E'), _) => { + if editor.get_command_buffer() == "g" { + editor.move_big_word_end_prev(); + editor.clear_command_buffer(); + } else { + editor.move_big_word_end(); + editor.clear_command_buffer(); + } + } + // Line movement (AppMode::ReadOnly, KeyCode::Char('0'), _) | (AppMode::ReadOnly, KeyCode::Home, _) => { @@ -468,7 +535,7 @@ fn render_textarea( .title("🎯 Textarea with Automatic Cursor Management"); let textarea_widget = TextArea::default().block(block.clone()); - + f.render_stateful_widget(textarea_widget, area, &mut editor.textarea); // Set cursor position for terminal cursor @@ -518,8 +585,8 @@ fn render_status_and_help( } } else { "🎯 CURSOR-STYLE DEMO: Normal █ | Insert | \n\ - Normal: hjkl/arrows=move, w/b/e=words, 0/$=line, g/G=first/last\n\ - i/a/A=insert, x/X=delete, ?=info\n\ + Normal: hjkl/arrows=move, w/b/e=words, W/B/E=WORDS, 0/$=line, g/G=first/last\n\ + i/a/A/o/O=insert, x/X=delete, ?=info\n\ F1=demo manual cursor, F2=restore automatic, Ctrl+Q=quit" } } diff --git a/canvas/src/textarea/provider.rs b/canvas/src/textarea/provider.rs index e78d310..59c9040 100644 --- a/canvas/src/textarea/provider.rs +++ b/canvas/src/textarea/provider.rs @@ -90,6 +90,27 @@ impl TextAreaProvider { self.lines[prev_idx].push_str(&curr); Some((prev_idx, prev_len)) } + + pub fn insert_blank_line_after(&mut self, idx: usize) -> usize { + let clamped = idx.min(self.lines.len()); + let insert_at = if clamped >= self.lines.len() { + self.lines.len() + } else { + clamped + 1 + }; + if insert_at == self.lines.len() { + self.lines.push(String::new()); + } else { + self.lines.insert(insert_at, String::new()); + } + insert_at + } + + pub fn insert_blank_line_before(&mut self, idx: usize) -> usize { + let insert_at = idx.min(self.lines.len()); + self.lines.insert(insert_at, String::new()); + insert_at + } } impl DataProvider for TextAreaProvider { diff --git a/canvas/src/textarea/state.rs b/canvas/src/textarea/state.rs index 13caf2f..d02b8b2 100644 --- a/canvas/src/textarea/state.rs +++ b/canvas/src/textarea/state.rs @@ -1,9 +1,12 @@ // src/textarea/state.rs +use std::ops::{Deref, DerefMut}; + +use anyhow::Result; +use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; + use crate::editor::FormEditor; use crate::textarea::provider::TextAreaProvider; -use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; - #[cfg(feature = "gui")] use ratatui::{layout::Rect, widgets::Block}; @@ -30,6 +33,21 @@ impl Default for TextAreaState { } } +// Expose the entire FormEditor API directly on TextAreaState +impl Deref for TextAreaState { + type Target = TextAreaEditor; + + fn deref(&self) -> &Self::Target { + &self.editor + } +} + +impl DerefMut for TextAreaState { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.editor + } +} + impl TextAreaState { pub fn from_text>(text: S) -> Self { let provider = TextAreaProvider::from_text(text); @@ -47,7 +65,6 @@ impl TextAreaState { pub fn set_text>(&mut self, text: S) { self.editor.data_provider_mut().set_text(text); - // Reset to first line and col 0 self.editor.ui_state.current_field = 0; self.editor.ui_state.cursor_pos = 0; self.editor.ui_state.ideal_cursor_column = 0; @@ -61,29 +78,30 @@ impl TextAreaState { self.placeholder = Some(s.into()); } - // Editing primitives specific to multi-line buffer + // Textarea-specific primitive: split at cursor pub fn insert_newline(&mut self) { - let line_idx = self.editor.current_field(); - let col = self.editor.cursor_position(); + let line_idx = self.current_field(); + let col = self.cursor_position(); let new_idx = self .editor .data_provider_mut() .split_line_at(line_idx, col); - let _ = self.editor.transition_to_field(new_idx); - self.editor.move_line_start(); - self.editor.enter_edit_mode(); + let _ = self.transition_to_field(new_idx); + self.move_line_start(); + self.enter_edit_mode(); } + // Textarea-specific primitive: backspace with line join at start-of-line pub fn backspace(&mut self) { - let col = self.editor.cursor_position(); + let col = self.cursor_position(); if col > 0 { - let _ = self.editor.delete_backward(); + let _ = self.delete_backward(); return; } - let line_idx = self.editor.current_field(); + let line_idx = self.current_field(); if line_idx == 0 { return; } @@ -93,19 +111,20 @@ impl TextAreaState { .data_provider_mut() .join_with_prev(line_idx) { - let _ = self.editor.transition_to_field(prev_idx); - self.editor.set_cursor_position(new_col); - self.editor.enter_edit_mode(); + let _ = self.transition_to_field(prev_idx); + self.set_cursor_position(new_col); + self.enter_edit_mode(); } } + // Textarea-specific primitive: delete or join with next line at EOL pub fn delete_forward_or_join(&mut self) { - let line_idx = self.editor.current_field(); - let line_len = self.editor.current_text().chars().count(); - let col = self.editor.cursor_position(); + let line_idx = self.current_field(); + let line_len = self.current_text().chars().count(); + let col = self.cursor_position(); if col < line_len { - let _ = self.editor.delete_forward(); + let _ = self.delete_forward(); return; } @@ -114,12 +133,40 @@ impl TextAreaState { .data_provider_mut() .join_with_next(line_idx) { - self.editor.set_cursor_position(new_col); - self.editor.enter_edit_mode(); + self.set_cursor_position(new_col); + self.enter_edit_mode(); } } - // Drive the editor from key events + // Override for multiline: insert new blank line below and enter insert mode. + pub fn open_line_below(&mut self) -> Result<()> { + let line_idx = self.current_field(); + let new_idx = self + .editor + .data_provider_mut() + .insert_blank_line_after(line_idx); + + self.transition_to_field(new_idx)?; + self.move_line_start(); + self.enter_edit_mode(); + Ok(()) + } + + // Override for multiline: insert new blank line above and enter insert mode. + pub fn open_line_above(&mut self) -> Result<()> { + let line_idx = self.current_field(); + let new_idx = self + .editor + .data_provider_mut() + .insert_blank_line_before(line_idx); + + self.transition_to_field(new_idx)?; + self.move_line_start(); + self.enter_edit_mode(); + Ok(()) + } + + // Drive from KeyEvent; you can still call all FormEditor methods directly pub fn input(&mut self, key: KeyEvent) { if key.kind != KeyEventKind::Press { return; @@ -131,49 +178,43 @@ impl TextAreaState { (KeyCode::Delete, _) => self.delete_forward_or_join(), (KeyCode::Left, _) => { - let _ = self.editor.move_left(); + let _ = self.move_left(); } (KeyCode::Right, _) => { - let _ = self.editor.move_right(); + let _ = self.move_right(); } (KeyCode::Up, _) => { - let _ = self.editor.move_up(); + let _ = self.move_up(); } (KeyCode::Down, _) => { - let _ = self.editor.move_down(); + let _ = self.move_down(); } (KeyCode::Home, _) | (KeyCode::Char('a'), KeyModifiers::CONTROL) => { - self.editor.move_line_start(); + self.move_line_start(); } (KeyCode::End, _) | (KeyCode::Char('e'), KeyModifiers::CONTROL) => { - self.editor.move_line_end(); + self.move_line_end(); } // Optional: word motions - (KeyCode::Char('b'), KeyModifiers::ALT) => { - self.editor.move_word_prev(); - } - (KeyCode::Char('f'), KeyModifiers::ALT) => { - self.editor.move_word_next(); - } - (KeyCode::Char('e'), KeyModifiers::ALT) => { - self.editor.move_word_end(); - } + (KeyCode::Char('b'), KeyModifiers::ALT) => self.move_word_prev(), + (KeyCode::Char('f'), KeyModifiers::ALT) => self.move_word_next(), + (KeyCode::Char('e'), KeyModifiers::ALT) => self.move_word_end(), - // Insert printable characters + // Printable characters (KeyCode::Char(c), m) if m.is_empty() => { - self.editor.enter_edit_mode(); - let _ = self.editor.insert_char(c); + self.enter_edit_mode(); + let _ = self.insert_char(c); } - // Tab: insert 4 spaces (simple default) + // Simple Tab policy (KeyCode::Tab, _) => { - self.editor.enter_edit_mode(); + self.enter_edit_mode(); for _ in 0..4 { - let _ = self.editor.insert_char(' '); + let _ = self.insert_char(' '); } } @@ -185,20 +226,19 @@ impl TextAreaState { #[cfg(feature = "gui")] pub fn cursor(&self, area: Rect, block: Option<&Block<'_>>) -> (u16, u16) { let inner = if let Some(b) = block { b.inner(area) } else { area }; - let line_idx = self.editor.current_field() as u16; + let line_idx = self.current_field() as u16; let y = inner.y + line_idx.saturating_sub(self.scroll_y); - let current_line = self.editor.current_text(); - let col = self.editor.display_cursor_position(); + let current_line = self.current_text(); + let col = self.display_cursor_position(); let mut x_off: u16 = 0; for (i, ch) in current_line.chars().enumerate() { if i >= col { break; } - x_off = x_off.saturating_add( - UnicodeWidthChar::width(ch).unwrap_or(0) as u16, - ); + x_off = x_off + .saturating_add(UnicodeWidthChar::width(ch).unwrap_or(0) as u16); } let x = inner.x.saturating_add(x_off); (x, y) @@ -214,7 +254,7 @@ impl TextAreaState { if inner.height == 0 { return; } - let line_idx = self.editor.current_field() as u16; + let line_idx = self.current_field() as u16; if line_idx < self.scroll_y { self.scroll_y = line_idx; } else if line_idx >= self.scroll_y + inner.height {