From ce07105eea995ac643dfef838a0be11336bf2efa Mon Sep 17 00:00:00 2001 From: Priec Date: Thu, 14 Aug 2025 00:08:18 +0200 Subject: [PATCH] more vim functionality added --- canvas/examples/canvas_cursor_auto.rs | 49 ++- canvas/src/canvas/actions/movement/word.rs | 107 ++++++ canvas/src/editor.rs | 381 +++++++++++++++++++++ 3 files changed, 534 insertions(+), 3 deletions(-) diff --git a/canvas/examples/canvas_cursor_auto.rs b/canvas/examples/canvas_cursor_auto.rs index 3831a65..ccc9cee 100644 --- a/canvas/examples/canvas_cursor_auto.rs +++ b/canvas/examples/canvas_cursor_auto.rs @@ -323,6 +323,26 @@ impl AutoCursorFormEditor { } result } + + fn move_WORD_next(&mut self) { + self.editor.move_WORD_next(); + self.update_visual_selection(); + } + + fn move_WORD_prev(&mut self) { + self.editor.move_WORD_prev(); + self.update_visual_selection(); + } + + fn move_WORD_end(&mut self) { + self.editor.move_WORD_end(); + self.update_visual_selection(); + } + + fn move_WORD_end_prev(&mut self) { + self.editor.move_WORD_end_prev(); + self.update_visual_selection(); + } } // Demo form data with interesting text for cursor demonstration @@ -543,6 +563,29 @@ fn handle_key_press( } } + (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('W'), _) => { + editor.move_WORD_next(); + editor.set_debug_message("W: next WORD start".to_string()); + editor.clear_command_buffer(); + } + (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('B'), _) => { + editor.move_WORD_prev(); + editor.set_debug_message("B: previous WORD start".to_string()); + editor.clear_command_buffer(); + } + (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('E'), _) => { + // Check if this is 'gE' command + if editor.get_command_buffer() == "g" { + editor.move_WORD_end_prev(); + editor.set_debug_message("gE: previous WORD end".to_string()); + editor.clear_command_buffer(); + } else { + editor.move_WORD_end(); + editor.set_debug_message("E: WORD end".to_string()); + editor.clear_command_buffer(); + } + } + // Line movement (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('0'), _) | (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Home, _) => { @@ -752,9 +795,9 @@ fn render_status_and_help( } } else { "🎯 CURSOR-STYLE DEMO: Normal █ | Insert | | Visual blinking█\n\ - Normal: hjkl/arrows=move, w/b/e=words, 0/$=line, gg/G=first/last\n\ - i/a/A=insert, v/b=visual, x/X=delete, ?=info\n\ - F1=demo manual cursor, F2=restore automatic" + Normal: hjkl/arrows=move, w/b/e=words, W/B/E=WORDS, 0/$=line, gg/G=first/last\n\ + i/a/A/o/O=insert, v/V=visual, x/X=delete, ?=info\n\ + F1=demo manual cursor, F2=restore automatic" } } AppMode::Edit => { diff --git a/canvas/src/canvas/actions/movement/word.rs b/canvas/src/canvas/actions/movement/word.rs index 1b82210..6ec120c 100644 --- a/canvas/src/canvas/actions/movement/word.rs +++ b/canvas/src/canvas/actions/movement/word.rs @@ -144,3 +144,110 @@ pub fn find_prev_word_end(text: &str, current_pos: usize) -> usize { 0 } } + +// Add these functions to your word movement module + +/// Find the start of the next WORD (whitespace-separated) +pub fn find_next_WORD_start(text: &str, current_pos: usize) -> usize { + let chars: Vec = text.chars().collect(); + if chars.is_empty() || current_pos >= chars.len() { + return text.chars().count(); + } + + let mut pos = current_pos; + + // If we're on non-whitespace, skip to end of current WORD + while pos < chars.len() && !chars[pos].is_whitespace() { + pos += 1; + } + + // Skip whitespace to find start of next WORD + while pos < chars.len() && chars[pos].is_whitespace() { + pos += 1; + } + + pos +} + +/// Find the start of the previous WORD (whitespace-separated) +pub fn find_prev_WORD_start(text: &str, current_pos: usize) -> usize { + let chars: Vec = text.chars().collect(); + if chars.is_empty() || current_pos == 0 { + return 0; + } + + let mut pos = current_pos.saturating_sub(1); + + // Skip whitespace backwards + while pos > 0 && chars[pos].is_whitespace() { + pos -= 1; + } + + // Find start of current WORD by going back while non-whitespace + while pos > 0 && !chars[pos - 1].is_whitespace() { + pos -= 1; + } + + pos +} + +/// Find the end of the current/next WORD (whitespace-separated) +pub fn find_WORD_end(text: &str, current_pos: usize) -> usize { + let chars: Vec = text.chars().collect(); + if chars.is_empty() { + return 0; + } + + let mut pos = current_pos; + + // If we're on whitespace, skip to start of next WORD + while pos < chars.len() && chars[pos].is_whitespace() { + pos += 1; + } + + // If we reached end, return it + if pos >= chars.len() { + return chars.len(); + } + + // Find end of current WORD (last non-whitespace char) + while pos < chars.len() && !chars[pos].is_whitespace() { + pos += 1; + } + + // Return position of last character in WORD + pos.saturating_sub(1) +} + +/// Find the end of the previous WORD (whitespace-separated) +pub fn find_prev_WORD_end(text: &str, current_pos: usize) -> usize { + let chars: Vec = text.chars().collect(); + if chars.is_empty() || current_pos == 0 { + return 0; + } + + let mut pos = current_pos.saturating_sub(1); + + // Skip whitespace backwards + while pos > 0 && chars[pos].is_whitespace() { + pos -= 1; + } + + // If we hit start of text and it's whitespace, return 0 + if pos == 0 && chars[0].is_whitespace() { + return 0; + } + + // Skip back to start of current WORD, then forward to end + while pos > 0 && !chars[pos - 1].is_whitespace() { + pos -= 1; + } + + // Now find end of this WORD + while pos < chars.len() && !chars[pos].is_whitespace() { + pos += 1; + } + + // Return position of last character in WORD + pos.saturating_sub(1) +} diff --git a/canvas/src/editor.rs b/canvas/src/editor.rs index 2bf8d6c..d40df38 100644 --- a/canvas/src/editor.rs +++ b/canvas/src/editor.rs @@ -78,6 +78,69 @@ fn find_last_word_start_in_field(text: &str) -> usize { pos } + +// Helper function to find the end of the last WORD in a field +fn find_last_WORD_end_in_field(text: &str) -> usize { + let chars: Vec = text.chars().collect(); + if chars.is_empty() { + return 0; + } + + let mut pos = chars.len().saturating_sub(1); + + // Skip trailing whitespace + while pos > 0 && chars[pos].is_whitespace() { + pos -= 1; + } + + // If the whole field is whitespace, return 0 + if pos == 0 && chars[0].is_whitespace() { + return 0; + } + + // We're now at the end of the last WORD + pos +} + + +/// Helper: Find start of last WORD in a field (for cross-field B movement) +fn find_last_WORD_start_in_field(text: &str) -> usize { + if text.is_empty() { + return 0; + } + + let chars: Vec = text.chars().collect(); + if chars.is_empty() { + return 0; + } + + let mut pos = chars.len().saturating_sub(1); + + // Skip trailing whitespace + while pos > 0 && chars[pos].is_whitespace() { + pos -= 1; + } + + // If the whole field is whitespace, return 0 + if pos == 0 && chars[0].is_whitespace() { + return 0; + } + + // Now we're on a non-whitespace character + // Find the start of this WORD by going backwards while chars are non-whitespace + while pos > 0 { + let prev_char = chars[pos - 1]; + + // Stop if we hit whitespace (WORD boundary) + if prev_char.is_whitespace() { + break; + } + pos -= 1; + } + + pos +} + impl FormEditor { /// Convert a char index to a byte index in a string fn char_to_byte_index(s: &str, char_idx: usize) -> usize { @@ -1508,6 +1571,308 @@ impl FormEditor { } } + /// Move to start of next WORD (vim W) - can cross field boundaries + pub fn move_WORD_next(&mut self) { + use crate::canvas::actions::movement::word::find_next_WORD_start; + let current_text = self.current_text(); + + if current_text.is_empty() { + // Empty field - try to move to next field + if self.move_down().is_ok() { + // Successfully moved to next field, try to find first WORD + let new_text = self.current_text(); + if !new_text.is_empty() { + let first_WORD_pos = if new_text.chars().next().map_or(false, |c| !c.is_whitespace()) { + // Field starts with non-whitespace, go to position 0 + 0 + } else { + // Field starts with whitespace, find first WORD + find_next_WORD_start(new_text, 0) + }; + let is_edit_mode = self.ui_state.current_mode == AppMode::Edit; + let char_len = new_text.chars().count(); + let final_pos = if is_edit_mode { + first_WORD_pos.min(char_len) + } else { + first_WORD_pos.min(char_len.saturating_sub(1)) + }; + self.ui_state.cursor_pos = final_pos; + self.ui_state.ideal_cursor_column = final_pos; + } + } + return; + } + + let current_pos = self.ui_state.cursor_pos; + let new_pos = find_next_WORD_start(current_text, current_pos); + + // Check if we've hit the end of the current field + if new_pos >= current_text.chars().count() { + // At end of field - jump to next field and start from beginning + if self.move_down().is_ok() { + // Successfully moved to next field + let new_text = self.current_text(); + if new_text.is_empty() { + // New field is empty, cursor stays at 0 + self.ui_state.cursor_pos = 0; + self.ui_state.ideal_cursor_column = 0; + } else { + // Find first WORD in new field + let first_WORD_pos = if new_text.chars().next().map_or(false, |c| !c.is_whitespace()) { + // Field starts with non-whitespace, go to position 0 + 0 + } else { + // Field starts with whitespace, find first WORD + find_next_WORD_start(new_text, 0) + }; + let is_edit_mode = self.ui_state.current_mode == AppMode::Edit; + let char_len = new_text.chars().count(); + let final_pos = if is_edit_mode { + first_WORD_pos.min(char_len) + } else { + first_WORD_pos.min(char_len.saturating_sub(1)) + }; + self.ui_state.cursor_pos = final_pos; + self.ui_state.ideal_cursor_column = final_pos; + } + } + // If move_down() failed, we stay where we are (at end of last field) + } else { + // Normal WORD movement within current field + let is_edit_mode = self.ui_state.current_mode == AppMode::Edit; + let char_len = current_text.chars().count(); + let final_pos = if is_edit_mode { + new_pos.min(char_len) + } else { + new_pos.min(char_len.saturating_sub(1)) + }; + + self.ui_state.cursor_pos = final_pos; + self.ui_state.ideal_cursor_column = final_pos; + } + } + + /// Move to start of previous WORD (vim B) - can cross field boundaries + pub fn move_WORD_prev(&mut self) { + use crate::canvas::actions::movement::word::find_prev_WORD_start; + let current_text = self.current_text(); + + if current_text.is_empty() { + // Empty field - try to move to previous field and find last WORD + let current_field = self.ui_state.current_field; + if self.move_up().is_ok() { + // Check if we actually moved to a different field + if self.ui_state.current_field != current_field { + let new_text = self.current_text(); + if !new_text.is_empty() { + let last_WORD_start = find_last_WORD_start_in_field(new_text); + self.ui_state.cursor_pos = last_WORD_start; + self.ui_state.ideal_cursor_column = last_WORD_start; + } + } + } + return; + } + + let current_pos = self.ui_state.cursor_pos; + + // Special case: if we're at position 0, jump to previous field + if current_pos == 0 { + let current_field = self.ui_state.current_field; + if self.move_up().is_ok() { + // Check if we actually moved to a different field + if self.ui_state.current_field != current_field { + let new_text = self.current_text(); + if !new_text.is_empty() { + let last_WORD_start = find_last_WORD_start_in_field(new_text); + self.ui_state.cursor_pos = last_WORD_start; + self.ui_state.ideal_cursor_column = last_WORD_start; + } + } + } + return; + } + + // Try to find previous WORD in current field + let new_pos = find_prev_WORD_start(current_text, current_pos); + + // Check if we actually moved + if new_pos < current_pos { + // Normal WORD movement within current field - we found a previous WORD + self.ui_state.cursor_pos = new_pos; + self.ui_state.ideal_cursor_column = new_pos; + } else { + // We didn't move (probably at start of first WORD), try previous field + let current_field = self.ui_state.current_field; + if self.move_up().is_ok() { + // Check if we actually moved to a different field + if self.ui_state.current_field != current_field { + let new_text = self.current_text(); + if !new_text.is_empty() { + let last_WORD_start = find_last_WORD_start_in_field(new_text); + self.ui_state.cursor_pos = last_WORD_start; + self.ui_state.ideal_cursor_column = last_WORD_start; + } + } + } + } + } + + /// Move to end of current/next WORD (vim E) - can cross field boundaries + pub fn move_WORD_end(&mut self) { + use crate::canvas::actions::movement::word::find_WORD_end; + let current_text = self.current_text(); + + if current_text.is_empty() { + // Empty field - try to move to next field (but don't recurse) + if self.move_down().is_ok() { + let new_text = self.current_text(); + if !new_text.is_empty() { + // Find first WORD end in new field + let first_WORD_end = find_WORD_end(new_text, 0); + let is_edit_mode = self.ui_state.current_mode == AppMode::Edit; + let char_len = new_text.chars().count(); + let final_pos = if is_edit_mode { + first_WORD_end.min(char_len) + } else { + first_WORD_end.min(char_len.saturating_sub(1)) + }; + self.ui_state.cursor_pos = final_pos; + self.ui_state.ideal_cursor_column = final_pos; + } + } + return; + } + + let current_pos = self.ui_state.cursor_pos; + let char_len = current_text.chars().count(); + let new_pos = find_WORD_end(current_text, current_pos); + + // Check if we didn't move or hit the end of the field + if new_pos == current_pos && current_pos + 1 < char_len { + // Try next character and find WORD end from there + let next_pos = find_WORD_end(current_text, current_pos + 1); + if next_pos < char_len { + let is_edit_mode = self.ui_state.current_mode == AppMode::Edit; + let final_pos = if is_edit_mode { + next_pos.min(char_len) + } else { + next_pos.min(char_len.saturating_sub(1)) + }; + self.ui_state.cursor_pos = final_pos; + self.ui_state.ideal_cursor_column = final_pos; + return; + } + } + + // If we're at or near the end of the field, try next field (but don't recurse) + if new_pos >= char_len.saturating_sub(1) { + if self.move_down().is_ok() { + // Find first WORD end in new field + let new_text = self.current_text(); + if !new_text.is_empty() { + let first_WORD_end = find_WORD_end(new_text, 0); + let is_edit_mode = self.ui_state.current_mode == AppMode::Edit; + let new_char_len = new_text.chars().count(); + let final_pos = if is_edit_mode { + first_WORD_end.min(new_char_len) + } else { + first_WORD_end.min(new_char_len.saturating_sub(1)) + }; + self.ui_state.cursor_pos = final_pos; + self.ui_state.ideal_cursor_column = final_pos; + } + } + } else { + // Normal WORD end movement within current field + let is_edit_mode = self.ui_state.current_mode == AppMode::Edit; + let final_pos = if is_edit_mode { + new_pos.min(char_len) + } else { + new_pos.min(char_len.saturating_sub(1)) + }; + + self.ui_state.cursor_pos = final_pos; + self.ui_state.ideal_cursor_column = final_pos; + } + } + + /// Move to end of previous WORD (vim gE) - can cross field boundaries + pub fn move_WORD_end_prev(&mut self) { + use crate::canvas::actions::movement::word::{find_prev_WORD_end, find_WORD_end}; + let current_text = self.current_text(); + + if current_text.is_empty() { + // Empty field - try to move to previous field (but don't recurse) + let current_field = self.ui_state.current_field; + if self.move_up().is_ok() { + // Check if we actually moved to a different field + if self.ui_state.current_field != current_field { + let new_text = self.current_text(); + if !new_text.is_empty() { + // Find end of last WORD in the field + let char_len = new_text.chars().count(); + // Start from end and find the last WORD end + let last_WORD_end = find_last_WORD_end_in_field(new_text); + self.ui_state.cursor_pos = last_WORD_end; + self.ui_state.ideal_cursor_column = last_WORD_end; + } + } + } + return; + } + + let current_pos = self.ui_state.cursor_pos; + + // Special case: if we're at position 0, jump to previous field (but don't recurse) + if current_pos == 0 { + let current_field = self.ui_state.current_field; + if self.move_up().is_ok() { + // Check if we actually moved to a different field + if self.ui_state.current_field != current_field { + let new_text = self.current_text(); + if !new_text.is_empty() { + let last_WORD_end = find_last_WORD_end_in_field(new_text); + self.ui_state.cursor_pos = last_WORD_end; + self.ui_state.ideal_cursor_column = last_WORD_end; + } + } + } + return; + } + + let new_pos = find_prev_WORD_end(current_text, current_pos); + + // Check if we didn't move significantly (near start of field) + if new_pos == current_pos || new_pos <= 1 { + // Try to jump to previous field (but don't recurse) + let current_field = self.ui_state.current_field; + if self.move_up().is_ok() { + // Check if we actually moved to a different field + if self.ui_state.current_field != current_field { + let new_text = self.current_text(); + if !new_text.is_empty() { + let last_WORD_end = find_last_WORD_end_in_field(new_text); + self.ui_state.cursor_pos = last_WORD_end; + self.ui_state.ideal_cursor_column = last_WORD_end; + } + } + } + } else { + // Normal WORD movement within current field + let is_edit_mode = self.ui_state.current_mode == AppMode::Edit; + let char_len = current_text.chars().count(); + let final_pos = if is_edit_mode { + new_pos.min(char_len) + } else { + new_pos.min(char_len.saturating_sub(1)) + }; + + self.ui_state.cursor_pos = final_pos; + self.ui_state.ideal_cursor_column = final_pos; + } + } + /// Delete character before cursor (vim x in insert mode / backspace) pub fn delete_backward(&mut self) -> Result<()> { if self.ui_state.current_mode != AppMode::Edit { @@ -1903,6 +2268,22 @@ impl FormEditor { self.move_word_end_prev(); } + pub fn move_WORD_next_with_selection(&mut self) { + self.move_WORD_next(); + } + + pub fn move_WORD_end_with_selection(&mut self) { + self.move_WORD_end(); + } + + pub fn move_WORD_prev_with_selection(&mut self) { + self.move_WORD_prev(); + } + + pub fn move_WORD_end_prev_with_selection(&mut self) { + self.move_WORD_end_prev(); + } + pub fn move_line_start_with_selection(&mut self) { self.move_line_start(); }