From 43e86abf1fba5bb3c34fdf0414323c9ee7552a25 Mon Sep 17 00:00:00 2001 From: filipriec Date: Fri, 28 Feb 2025 15:04:48 +0100 Subject: [PATCH] edit mode is now generalized --- client/config.toml | 12 +- client/src/modes/handlers/edit.rs | 496 +++++++++++++++++++++++++----- 2 files changed, 421 insertions(+), 87 deletions(-) diff --git a/client/config.toml b/client/config.toml index 6b668fa..1388df4 100644 --- a/client/config.toml +++ b/client/config.toml @@ -6,14 +6,14 @@ quit = [":q", "ctrl+q"] force_quit = [":q!", "ctrl+shift+q"] save_and_quit = [":wq", "ctrl+shift+s"] -enter_command_mode = [":", "shift+;"] -exit_command_mode = ["ctrl+g", "5"] +enter_command_mode = [":", "ctrl+;"] +exit_command_mode = ["ctrl+g", "esc"] # MODE SPECIFIC # READ ONLY MODE enter_edit_mode_before = ["i"] enter_edit_mode_after = ["a"] -previous_entry = ["q"] +previous_entry = ["left","q"] next_entry = ["right","1"] move_left = ["h"] @@ -30,7 +30,11 @@ move_first_line = ["gg"] move_last_line = ["x"] # EDIT MODE -exit_edit_mode = ["esc", "ctrl+e"] +exit_edit_mode = ["esc","ctrl+e"] +delete_char_forward = ["delete"] +delete_char_backward = ["backspace"] +next_field = ["tab", "enter"] +prev_field = ["shift+tab", "backtab"] [colors] theme = "dark" diff --git a/client/src/modes/handlers/edit.rs b/client/src/modes/handlers/edit.rs index e906f41..e141d56 100644 --- a/client/src/modes/handlers/edit.rs +++ b/client/src/modes/handlers/edit.rs @@ -5,6 +5,7 @@ use crate::tui::terminal::AppTerminal; use crate::config::config::Config; use crate::ui::handlers::form::FormState; use crate::modes::handlers::command_mode::handle_command_event; +use crate::config::key_sequences::KeySequenceTracker; pub async fn handle_edit_event( key: KeyEvent, @@ -46,88 +47,85 @@ pub async fn handle_edit_event( return Ok((false, "".to_string())); } - // Regular edit mode handling + // Try to match against action mappings first + let mut key_sequence_tracker = KeySequenceTracker::new(800); + + if key.modifiers.is_empty() { + key_sequence_tracker.add_key(key.code); + let sequence = key_sequence_tracker.get_sequence(); + + // Try to match the current sequence against all bindings + if let Some(action) = config.matches_key_sequence_generalized(&sequence) { + let result = execute_edit_action( + action, + form_state, + is_edit_mode, + edit_mode_cooldown, + ideal_cursor_column, + command_message, + app_terminal, + ).await?; + return Ok((false, result)); + } + + // Check if this might be a prefix of a longer sequence + if config.is_key_sequence_prefix(&sequence) { + // If it's a prefix, wait for more keys + return Ok((false, command_message.clone())); + } + + // Since it's not part of a multi-key sequence, check for a direct action + if sequence.len() == 1 && !config.is_key_sequence_prefix(&sequence) { + // Try to handle it as a single key + if let Some(action) = config.get_action_for_key(key.code, key.modifiers) { + let result = execute_edit_action( + action, + form_state, + is_edit_mode, + edit_mode_cooldown, + ideal_cursor_column, + command_message, + app_terminal, + ).await?; + return Ok((false, result)); + } + } + } else { + // If modifiers are pressed, check for direct key bindings + if let Some(action) = config.get_action_for_key(key.code, key.modifiers) { + let result = execute_edit_action( + action, + form_state, + is_edit_mode, + edit_mode_cooldown, + ideal_cursor_column, + command_message, + app_terminal, + ).await?; + return Ok((false, result)); + } + } + + // If not handled by action mappings, handle edit-specific behavior + handle_edit_specific_input( + key, + form_state, + ideal_cursor_column, + ); + + *edit_mode_cooldown = false; + Ok((false, command_message.clone())) +} + +// Handle edit-specific key input (character input, backspace, delete) +fn handle_edit_specific_input( + key: KeyEvent, + form_state: &mut FormState, + ideal_cursor_column: &mut usize, +) { match key.code { - KeyCode::Left => { - form_state.current_cursor_pos = form_state.current_cursor_pos.saturating_sub(1); - *ideal_cursor_column = form_state.current_cursor_pos; - return Ok((false, "".to_string())); - } - KeyCode::Right => { - let current_input = form_state.get_current_input(); - if form_state.current_cursor_pos < current_input.len() { - form_state.current_cursor_pos += 1; - *ideal_cursor_column = form_state.current_cursor_pos; - } - return Ok((false, "".to_string())); - } - KeyCode::Esc => { - if config.is_exit_edit_mode(key.code, key.modifiers) { - if form_state.has_unsaved_changes { - *command_message = "Unsaved changes! Use :w to save or :q! to discard".to_string(); - return Ok((false, command_message.clone())); - } - *is_edit_mode = false; - *edit_mode_cooldown = true; - *command_message = "Read-only mode".to_string(); - - let current_input = form_state.get_current_input(); - if !current_input.is_empty() && form_state.current_cursor_pos >= current_input.len() { - form_state.current_cursor_pos = current_input.len() - 1; - *ideal_cursor_column = form_state.current_cursor_pos; - } - - return Ok((false, command_message.clone())); - } - } - KeyCode::Down => { - form_state.current_field = (form_state.current_field + 1) % form_state.fields.len(); - let current_input = form_state.get_current_input(); - let max_cursor_pos = current_input.len(); - form_state.current_cursor_pos = (*ideal_cursor_column).min(max_cursor_pos); - } - KeyCode::Up => { - if form_state.current_field == 0 { - form_state.current_field = form_state.fields.len() - 1; - } else { - form_state.current_field = form_state.current_field.saturating_sub(1); - } - let current_input = form_state.get_current_input(); - let max_cursor_pos = current_input.len(); - form_state.current_cursor_pos = (*ideal_cursor_column).min(max_cursor_pos); - } - KeyCode::Tab => { - if key.modifiers.contains(KeyModifiers::SHIFT) { - if form_state.current_field == 0 { - form_state.current_field = form_state.fields.len() - 1; - } else { - form_state.current_field = form_state.current_field.saturating_sub(1); - } - } else { - form_state.current_field = (form_state.current_field + 1) % form_state.fields.len(); - } - let current_input = form_state.get_current_input(); - let max_cursor_pos = current_input.len(); - form_state.current_cursor_pos = (*ideal_cursor_column).min(max_cursor_pos); - } - KeyCode::BackTab => { - if form_state.current_field == 0 { - form_state.current_field = form_state.fields.len() - 1; - } else { - form_state.current_field = form_state.current_field.saturating_sub(1); - } - let current_input = form_state.get_current_input(); - let max_cursor_pos = current_input.len(); - form_state.current_cursor_pos = (*ideal_cursor_column).min(max_cursor_pos); - } - KeyCode::Enter => { - form_state.current_field = (form_state.current_field + 1) % form_state.fields.len(); - let current_input = form_state.get_current_input(); - let max_cursor_pos = current_input.len(); - form_state.current_cursor_pos = (*ideal_cursor_column).min(max_cursor_pos); - } KeyCode::Char(c) => { - // In edit mode, ':' is just a normal character + // Character input let cursor_pos = form_state.current_cursor_pos; let field_value = form_state.get_current_input_mut(); let mut chars: Vec = field_value.chars().collect(); @@ -140,6 +138,7 @@ pub async fn handle_edit_event( } } KeyCode::Backspace => { + // Delete character backward if form_state.current_cursor_pos > 0 { let cursor_pos = form_state.current_cursor_pos; let field_value = form_state.get_current_input_mut(); @@ -154,6 +153,7 @@ pub async fn handle_edit_event( } } KeyCode::Delete => { + // Delete character forward let cursor_pos = form_state.current_cursor_pos; let field_value = form_state.get_current_input_mut(); let chars: Vec = field_value.chars().collect(); @@ -164,9 +164,339 @@ pub async fn handle_edit_event( form_state.has_unsaved_changes = true; } } - _ => {} + KeyCode::Tab => { + // Tab key special handling + if key.modifiers.contains(KeyModifiers::SHIFT) { + if form_state.current_field == 0 { + form_state.current_field = form_state.fields.len() - 1; + } else { + form_state.current_field = form_state.current_field.saturating_sub(1); + } + } else { + form_state.current_field = (form_state.current_field + 1) % form_state.fields.len(); + } + let current_input = form_state.get_current_input(); + let max_cursor_pos = current_input.len(); + form_state.current_cursor_pos = (*ideal_cursor_column).min(max_cursor_pos); + } + KeyCode::Enter => { + // Enter key moves to next field + form_state.current_field = (form_state.current_field + 1) % form_state.fields.len(); + let current_input = form_state.get_current_input(); + let max_cursor_pos = current_input.len(); + form_state.current_cursor_pos = (*ideal_cursor_column).min(max_cursor_pos); + } + _ => {} // Ignore other keys + } +} + +async fn execute_edit_action( + action: &str, + form_state: &mut FormState, + is_edit_mode: &mut bool, + edit_mode_cooldown: &mut bool, + ideal_cursor_column: &mut usize, + command_message: &mut String, + app_terminal: &mut AppTerminal, +) -> Result> { + match action { + // Navigation actions + "move_left" => { + form_state.current_cursor_pos = form_state.current_cursor_pos.saturating_sub(1); + *ideal_cursor_column = form_state.current_cursor_pos; + Ok("".to_string()) + } + "move_right" => { + let current_input = form_state.get_current_input(); + if form_state.current_cursor_pos < current_input.len() { + form_state.current_cursor_pos += 1; + *ideal_cursor_column = form_state.current_cursor_pos; + } + Ok("".to_string()) + } + "move_up" => { + if form_state.current_field == 0 { + form_state.current_field = form_state.fields.len() - 1; + } else { + form_state.current_field = form_state.current_field.saturating_sub(1); + } + let current_input = form_state.get_current_input(); + let max_cursor_pos = current_input.len(); + form_state.current_cursor_pos = (*ideal_cursor_column).min(max_cursor_pos); + Ok("".to_string()) + } + "move_down" => { + form_state.current_field = (form_state.current_field + 1) % form_state.fields.len(); + let current_input = form_state.get_current_input(); + let max_cursor_pos = current_input.len(); + form_state.current_cursor_pos = (*ideal_cursor_column).min(max_cursor_pos); + Ok("".to_string()) + } + "move_line_start" => { + form_state.current_cursor_pos = 0; + *ideal_cursor_column = form_state.current_cursor_pos; + Ok("".to_string()) + } + "move_line_end" => { + let current_input = form_state.get_current_input(); + form_state.current_cursor_pos = current_input.len(); + *ideal_cursor_column = form_state.current_cursor_pos; + Ok("".to_string()) + } + "move_first_line" => { + form_state.current_field = 0; + let current_input = form_state.get_current_input(); + let max_cursor_pos = current_input.len(); + form_state.current_cursor_pos = (*ideal_cursor_column).min(max_cursor_pos); + Ok("Moved to first line".to_string()) + } + "move_last_line" => { + form_state.current_field = form_state.fields.len() - 1; + let current_input = form_state.get_current_input(); + let max_cursor_pos = current_input.len(); + form_state.current_cursor_pos = (*ideal_cursor_column).min(max_cursor_pos); + Ok("Moved to last line".to_string()) + } + // Mode actions + "exit_edit_mode" => { + if form_state.has_unsaved_changes { + *command_message = "Unsaved changes! Use :w to save or :q! to discard".to_string(); + return Ok(command_message.clone()); + } + *is_edit_mode = false; + *edit_mode_cooldown = true; + *command_message = "Read-only mode".to_string(); + app_terminal.set_cursor_style(crossterm::cursor::SetCursorStyle::SteadyBlock)?; + + let current_input = form_state.get_current_input(); + if !current_input.is_empty() && form_state.current_cursor_pos >= current_input.len() { + form_state.current_cursor_pos = current_input.len() - 1; + *ideal_cursor_column = form_state.current_cursor_pos; + } + + Ok(command_message.clone()) + } + // Word movement actions + "move_word_next" => { + let current_input = form_state.get_current_input(); + if !current_input.is_empty() { + let new_pos = find_next_word_start(current_input, form_state.current_cursor_pos); + form_state.current_cursor_pos = new_pos.min(current_input.len()); + *ideal_cursor_column = form_state.current_cursor_pos; + } + Ok("".to_string()) + } + "move_word_end" => { + let current_input = form_state.get_current_input(); + if !current_input.is_empty() { + let new_pos = find_word_end(current_input, form_state.current_cursor_pos); + form_state.current_cursor_pos = new_pos.min(current_input.len()); + *ideal_cursor_column = form_state.current_cursor_pos; + } + Ok("".to_string()) + } + "move_word_prev" => { + let current_input = form_state.get_current_input(); + if !current_input.is_empty() { + let new_pos = find_prev_word_start(current_input, form_state.current_cursor_pos); + form_state.current_cursor_pos = new_pos; + *ideal_cursor_column = form_state.current_cursor_pos; + } + Ok("".to_string()) + } + "move_word_end_prev" => { + let current_input = form_state.get_current_input(); + if !current_input.is_empty() { + let new_pos = find_prev_word_end(current_input, form_state.current_cursor_pos); + form_state.current_cursor_pos = new_pos; + *ideal_cursor_column = form_state.current_cursor_pos; + } + Ok("".to_string()) + } + // Edit-specific actions (if you want to add keybindings for these operations) + "delete_char_forward" => { + let cursor_pos = form_state.current_cursor_pos; + let field_value = form_state.get_current_input_mut(); + let chars: Vec = field_value.chars().collect(); + if cursor_pos < chars.len() { + let mut new_chars = chars.clone(); + new_chars.remove(cursor_pos); + *field_value = new_chars.into_iter().collect(); + form_state.has_unsaved_changes = true; + } + Ok("".to_string()) + } + "delete_char_backward" => { + if form_state.current_cursor_pos > 0 { + let cursor_pos = form_state.current_cursor_pos; + let field_value = form_state.get_current_input_mut(); + let mut chars: Vec = field_value.chars().collect(); + if cursor_pos <= chars.len() && cursor_pos > 0 { + chars.remove(cursor_pos - 1); + *field_value = chars.into_iter().collect(); + form_state.current_cursor_pos = cursor_pos - 1; + *ideal_cursor_column = form_state.current_cursor_pos; + form_state.has_unsaved_changes = true; + } + } + Ok("".to_string()) + } + "next_field" => { + form_state.current_field = (form_state.current_field + 1) % form_state.fields.len(); + let current_input = form_state.get_current_input(); + let max_cursor_pos = current_input.len(); + form_state.current_cursor_pos = (*ideal_cursor_column).min(max_cursor_pos); + Ok("".to_string()) + } + "prev_field" => { + if form_state.current_field == 0 { + form_state.current_field = form_state.fields.len() - 1; + } else { + form_state.current_field = form_state.current_field.saturating_sub(1); + } + let current_input = form_state.get_current_input(); + let max_cursor_pos = current_input.len(); + form_state.current_cursor_pos = (*ideal_cursor_column).min(max_cursor_pos); + Ok("".to_string()) + } + // Add more edit mode actions as needed + _ => Ok(format!("Unknown action: {}", action)), + } +} + +// Reuse these character and word navigation helper functions +#[derive(PartialEq)] +enum CharType { + Whitespace, + Alphanumeric, + Punctuation, +} + +fn get_char_type(c: char) -> CharType { + if c.is_whitespace() { + CharType::Whitespace + } else if c.is_alphanumeric() { + CharType::Alphanumeric + } else { + CharType::Punctuation + } +} + +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 current_pos; } - *edit_mode_cooldown = false; - Ok((false, command_message.clone())) + let mut pos = current_pos; + let initial_type = get_char_type(chars[pos]); + while pos < chars.len() && get_char_type(chars[pos]) == initial_type { + pos += 1; + } + + while pos < chars.len() && get_char_type(chars[pos]) == CharType::Whitespace { + pos += 1; + } + + pos +} + +fn find_word_end(text: &str, current_pos: usize) -> usize { + let chars: Vec = text.chars().collect(); + if chars.is_empty() { + return 0; + } + + if current_pos >= chars.len() - 1 { + return chars.len() - 1; + } + + let mut pos = current_pos; + + if get_char_type(chars[pos]) == CharType::Whitespace { + while pos < chars.len() && get_char_type(chars[pos]) == CharType::Whitespace { + pos += 1; + } + } else { + let current_type = get_char_type(chars[pos]); + if pos + 1 < chars.len() && get_char_type(chars[pos + 1]) != current_type { + pos += 1; + while pos < chars.len() && get_char_type(chars[pos]) == CharType::Whitespace { + pos += 1; + } + } + } + + if pos >= chars.len() { + return chars.len() - 1; + } + + let word_type = get_char_type(chars[pos]); + while pos + 1 < chars.len() && get_char_type(chars[pos + 1]) == word_type { + pos += 1; + } + + pos +} + +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); + + while pos > 0 && get_char_type(chars[pos]) == CharType::Whitespace { + pos -= 1; + } + + if get_char_type(chars[pos]) != CharType::Whitespace { + let word_type = get_char_type(chars[pos]); + while pos > 0 && get_char_type(chars[pos - 1]) == word_type { + pos -= 1; + } + } + + pos +} + +fn find_prev_word_end(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); + + while pos > 0 && get_char_type(chars[pos]) == CharType::Whitespace { + pos -= 1; + } + + if pos > 0 && get_char_type(chars[pos]) != CharType::Whitespace { + let word_type = get_char_type(chars[pos]); + + while pos > 0 && get_char_type(chars[pos - 1]) == word_type { + pos -= 1; + } + + while pos > 0 && get_char_type(chars[pos - 1]) == CharType::Whitespace { + pos -= 1; + } + + if pos > 0 { + pos -= 1; + let prev_word_type = get_char_type(chars[pos]); + while pos > 0 && get_char_type(chars[pos - 1]) == prev_word_type { + pos -= 1; + } + + while pos < chars.len() - 1 && + get_char_type(chars[pos + 1]) == prev_word_type { + pos += 1; + } + } + } + + pos }