diff --git a/client/config.toml b/client/config.toml index d6d3919..274e79a 100644 --- a/client/config.toml +++ b/client/config.toml @@ -13,6 +13,12 @@ move_left = ["h"] move_right = ["l"] move_up = ["k", "Up"] move_down = ["j", "Down"] +move_word_next = ["w"] # Move to beginning of next word +move_word_end = ["e"] # Move to end of current/next word +move_word_prev = ["b"] # Move to beginning of previous word +move_word_end_prev = ["ge"] # Move to end of previous word +move_line_start = ["0"] # Move to beginning of line +move_line_end = ["$"] # Move to end of line [colors] theme = "dark" diff --git a/client/src/config.rs b/client/src/config.rs index 7be38f9..9d63fd8 100644 --- a/client/src/config.rs +++ b/client/src/config.rs @@ -107,4 +107,15 @@ impl Config { false } } + + pub fn has_key_for_action(&self, action: &str, key_char: char) -> bool { + if let Some(bindings) = self.keybindings.get(action) { + for binding in bindings { + if binding == &key_char.to_string() { + return true; + } + } + } + false + } } diff --git a/client/src/ui/handlers/event.rs b/client/src/ui/handlers/event.rs index 996cccf..5616dab 100644 --- a/client/src/ui/handlers/event.rs +++ b/client/src/ui/handlers/event.rs @@ -16,6 +16,13 @@ pub struct EventHandler { pub ideal_cursor_column: usize, } +#[derive(PartialEq)] +enum CharType { + Whitespace, + Alphanumeric, + Punctuation, +} + impl EventHandler { pub fn new() -> Self { EventHandler { @@ -95,11 +102,6 @@ impl EventHandler { ]; let current_input = form_state.get_current_input(); - let max_cursor_pos = if !self.is_edit_mode && !current_input.is_empty() { - current_input.len() - 1 - } else { - current_input.len() - }; let max_cursor_pos = if !self.is_edit_mode && !current_input.is_empty() { current_input.len() - 1 // In readonly mode, limit to last character } else { @@ -213,6 +215,55 @@ impl EventHandler { form_state.current_cursor_pos = self.ideal_cursor_column.min(max_cursor_pos); return Ok((false, "".to_string())); } + "move_word_next" => { + let current_input = form_state.get_current_input(); + if !current_input.is_empty() { + let new_pos = self.find_next_word_start(current_input, form_state.current_cursor_pos); + form_state.current_cursor_pos = new_pos.min(current_input.len().saturating_sub(1)); + self.ideal_cursor_column = form_state.current_cursor_pos; + } + return Ok((false, "".to_string())); + } + "move_word_end" => { + let current_input = form_state.get_current_input(); + if !current_input.is_empty() { + let new_pos = self.find_word_end(current_input, form_state.current_cursor_pos); + form_state.current_cursor_pos = new_pos.min(current_input.len().saturating_sub(1)); + self.ideal_cursor_column = form_state.current_cursor_pos; + } + return Ok((false, "".to_string())); + } + "move_word_prev" => { + let current_input = form_state.get_current_input(); + if !current_input.is_empty() { + let new_pos = self.find_prev_word_start(current_input, form_state.current_cursor_pos); + form_state.current_cursor_pos = new_pos; + self.ideal_cursor_column = form_state.current_cursor_pos; + } + return Ok((false, "".to_string())); + } + "move_word_end_prev" => { + let current_input = form_state.get_current_input(); + if !current_input.is_empty() { + let new_pos = self.find_prev_word_end(current_input, form_state.current_cursor_pos); + form_state.current_cursor_pos = new_pos; + self.ideal_cursor_column = form_state.current_cursor_pos; + } + return Ok((false, "".to_string())); + } + "move_line_start" => { + form_state.current_cursor_pos = 0; + self.ideal_cursor_column = form_state.current_cursor_pos; + return Ok((false, "".to_string())); + } + "move_line_end" => { + let current_input = form_state.get_current_input(); + if !current_input.is_empty() { + form_state.current_cursor_pos = current_input.len() - 1; + self.ideal_cursor_column = form_state.current_cursor_pos; + } + return Ok((false, "".to_string())); + } _ => {} } } @@ -485,4 +536,139 @@ impl EventHandler { self.edit_mode_cooldown = false; Ok((false, self.command_message.clone())) } + + + // Helper function to determine character type + fn get_char_type(c: char) -> CharType { + if c.is_whitespace() { + CharType::Whitespace + } else if c.is_alphanumeric() { + CharType::Alphanumeric + } else { + CharType::Punctuation + } + } + + // Find the beginning of the next word from current position + fn find_next_word_start(&self, text: &str, current_pos: usize) -> usize { + let chars: Vec = text.chars().collect(); + if chars.is_empty() || current_pos >= chars.len() { + return current_pos; + } + + let mut pos = current_pos; + + // Step 1: Skip current word/set of similar characters + let initial_type = Self::get_char_type(chars[pos]); + while pos < chars.len() && Self::get_char_type(chars[pos]) == initial_type { + pos += 1; + } + + // Step 2: Skip any whitespace + while pos < chars.len() && Self::get_char_type(chars[pos]) == CharType::Whitespace { + pos += 1; + } + + pos + } + + // Find the end of the current/next word + fn find_word_end(&self, text: &str, current_pos: usize) -> usize { + let chars: Vec = text.chars().collect(); + if chars.is_empty() || current_pos >= chars.len() - 1 { + return if chars.is_empty() { 0 } else { chars.len() - 1 }; + } + + let mut pos = current_pos; + + // Skip whitespace to find the next word + while pos < chars.len() && Self::get_char_type(chars[pos]) == CharType::Whitespace { + pos += 1; + } + + if pos == chars.len() { + return pos.saturating_sub(1); + } + + let word_type = Self::get_char_type(chars[pos]); + + // Move to the end of this word + while pos < chars.len() - 1 && Self::get_char_type(chars[pos + 1]) == word_type { + pos += 1; + } + + pos + } + + // Find the beginning of the previous word + fn find_prev_word_start(&self, 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); + + // Step 1: Skip any whitespace backward + while pos > 0 && Self::get_char_type(chars[pos]) == CharType::Whitespace { + pos -= 1; + } + + if Self::get_char_type(chars[pos]) != CharType::Whitespace { + // Step 2: Find the beginning of this word + let word_type = Self::get_char_type(chars[pos]); + while pos > 0 && Self::get_char_type(chars[pos - 1]) == word_type { + pos -= 1; + } + } + + pos + } + + // Find the end of the previous word + fn find_prev_word_end(&self, text: &str, current_pos: usize) -> usize { + let chars: Vec = text.chars().collect(); + if chars.is_empty() || current_pos <= 1 { + return 0; + } + + let mut pos = current_pos.saturating_sub(1); + + // Step 1: Skip any whitespace backward + while pos > 0 && Self::get_char_type(chars[pos]) == CharType::Whitespace { + pos -= 1; + } + + // If we hit a non-whitespace character, find the beginning of this word + if pos > 0 && Self::get_char_type(chars[pos]) != CharType::Whitespace { + let word_type = Self::get_char_type(chars[pos]); + + // Step 2: Skip backward past this word + while pos > 0 && Self::get_char_type(chars[pos - 1]) == word_type { + pos -= 1; + } + + // Step 3: Skip whitespace before this word + while pos > 0 && Self::get_char_type(chars[pos - 1]) == CharType::Whitespace { + pos -= 1; + } + + // Step 4: Find end of previous word + if pos > 0 { + pos -= 1; + let prev_word_type = Self::get_char_type(chars[pos]); + while pos > 0 && Self::get_char_type(chars[pos - 1]) == prev_word_type { + pos -= 1; + } + + // Find the end of this word + while pos < chars.len() - 1 && + Self::get_char_type(chars[pos + 1]) == prev_word_type { + pos += 1; + } + } + } + + pos + } }