diff --git a/client/src/modes/canvas/edit.rs b/client/src/modes/canvas/edit.rs index 449b685..77003ff 100644 --- a/client/src/modes/canvas/edit.rs +++ b/client/src/modes/canvas/edit.rs @@ -319,11 +319,20 @@ async fn execute_edit_action( "move_word_end" => { let current_input = state.get_current_input(); if !current_input.is_empty() { - let new_pos = - find_word_end(current_input, state.current_cursor_pos()); - let final_pos = new_pos.min(current_input.len()); - state.set_current_cursor_pos(final_pos); - *ideal_cursor_column = final_pos; + let current_pos = state.current_cursor_pos(); + let new_pos = find_word_end(current_input, current_pos); + + // If already at word end, jump to next word's end + let final_pos = if new_pos == current_pos { + find_word_end(current_input, new_pos + 1) + } else { + new_pos + }; + + let max_valid_index = current_input.len().saturating_sub(1); + let clamped_pos = final_pos.min(max_valid_index); + state.set_current_cursor_pos(clamped_pos); + *ideal_cursor_column = clamped_pos; } Ok("".to_string()) } @@ -400,19 +409,17 @@ fn find_word_end(text: &str, current_pos: usize) -> usize { if len == 0 { return 0; } - if current_pos >= len { - return len; + + let mut pos = current_pos.min(len - 1); + + // If already at whitespace, find next word first + if get_char_type(chars[pos]) == CharType::Whitespace { + pos = find_next_word_start(text, pos); } - let mut pos = current_pos; - - if get_char_type(chars[pos]) == CharType::Whitespace { - while pos < len && get_char_type(chars[pos]) == CharType::Whitespace { - pos += 1; - } - if pos == len { - return len; - } + // Now find end of current/next word + if pos >= len { + return len.saturating_sub(1); } let word_type = get_char_type(chars[pos]); @@ -420,11 +427,8 @@ fn find_word_end(text: &str, current_pos: usize) -> usize { pos += 1; } - if pos > current_pos && pos > 0 { - pos - 1 - } else { - current_pos.min(len.saturating_sub(1)) - } + // Return last character of the word + pos.saturating_sub(1).min(len.saturating_sub(1)) } fn find_prev_word_start(text: &str, current_pos: usize) -> usize { diff --git a/client/src/modes/canvas/read_only.rs b/client/src/modes/canvas/read_only.rs index 9c8d900..670c161 100644 --- a/client/src/modes/canvas/read_only.rs +++ b/client/src/modes/canvas/read_only.rs @@ -296,18 +296,21 @@ async fn execute_action( "move_word_end" => { let current_input = state.get_current_input(); if !current_input.is_empty() { - // 1. Find the index of the last character of the target word - let new_pos = - find_word_end(current_input, state.current_cursor_pos()); + let current_pos = state.current_cursor_pos(); + let new_pos = find_word_end(current_input, current_pos); + + // Only move if we're not already at the found position + let final_pos = if new_pos != current_pos { + new_pos + } else { + // If already at a word end, jump to next word's end + find_word_end(current_input, new_pos + 1) + }; - // 2. Clamp the position for Read-Only mode - // max_valid_index is the index of the VERY LAST character in the input string let max_valid_index = current_input.len().saturating_sub(1); - let final_pos = new_pos.min(max_valid_index); - - // 3. Set the cursor - state.set_current_cursor_pos(final_pos); - *ideal_cursor_column = final_pos; + let clamped_pos = final_pos.min(max_valid_index); + state.set_current_cursor_pos(clamped_pos); + *ideal_cursor_column = clamped_pos; } Ok("".to_string()) } @@ -394,29 +397,62 @@ fn find_next_word_start(text: &str, current_pos: usize) -> usize { pos } -fn find_word_end(text: &str, current_pos: usize) -> usize { +fn find_next_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.min(chars.len().saturating_sub(1)); - if get_char_type(chars[pos]) == CharType::Whitespace { - while pos + 1 < chars.len() && get_char_type(chars[pos + 1]) == CharType::Whitespace { + // Find start of next word + let next_start = find_next_word_start(text, current_pos); + + // Find end of that word + if next_start >= chars.len() { + return chars.len().saturating_sub(1); + } + + let mut pos = next_start; + let word_type = get_char_type(chars[pos]); + + while pos < chars.len() && get_char_type(chars[pos]) == word_type { + pos += 1; + } + + pos.saturating_sub(1).min(chars.len().saturating_sub(1)) +} + +fn find_word_end(text: &str, current_pos: usize) -> usize { + let chars: Vec = text.chars().collect(); + let len = chars.len(); + if len == 0 { + return 0; + } + + let mut pos = current_pos.min(len - 1); + let original_pos = pos; + + // First try to find end of current word + let current_type = get_char_type(chars[pos]); + if current_type != CharType::Whitespace { + // Move forward to word end + while pos < len && get_char_type(chars[pos]) == current_type { pos += 1; } - if pos + 1 >= chars.len() || get_char_type(chars[pos + 1]) == CharType::Whitespace { - return pos; - } - pos += 1; + return pos.saturating_sub(1); } + // If in whitespace, find next word's end + pos = find_next_word_start(text, pos); + if pos >= len { + return len.saturating_sub(1); + } + let word_type = get_char_type(chars[pos]); - while pos + 1 < chars.len() && get_char_type(chars[pos + 1]) == word_type { + while pos < len && get_char_type(chars[pos]) == word_type { pos += 1; } - - pos + + pos.saturating_sub(1).min(len.saturating_sub(1)) } fn find_prev_word_start(text: &str, current_pos: usize) -> usize {