From 4e0338276f41be61370701ce72f61ba264e6f514 Mon Sep 17 00:00:00 2001 From: Priec Date: Wed, 30 Jul 2025 17:16:20 +0200 Subject: [PATCH] autotrigger vs manual trigger --- Cargo.lock | 2 + canvas/Cargo.toml | 3 + canvas/src/autocomplete/actions.rs | 108 +++++--- canvas/src/canvas/actions/edit.rs | 381 ++++++++++------------------- canvas/src/canvas/actions/mod.rs | 2 +- canvas/src/canvas/actions/types.rs | 166 ++----------- canvas/src/config.rs | 14 ++ canvas/src/dispatcher.rs | 5 +- client/canvas_config.toml | 1 + 9 files changed, 253 insertions(+), 429 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9b02be2..8ef8c22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -482,6 +482,8 @@ dependencies = [ "tokio", "tokio-test", "toml", + "tracing", + "tracing-subscriber", "unicode-width 0.2.0", ] diff --git a/canvas/Cargo.toml b/canvas/Cargo.toml index 422ce22..9f19db2 100644 --- a/canvas/Cargo.toml +++ b/canvas/Cargo.toml @@ -19,6 +19,9 @@ toml = { workspace = true } serde = { workspace = true } unicode-width.workspace = true +tracing = "0.1.41" +tracing-subscriber = "0.3.19" + [dev-dependencies] tokio-test = "0.4.4" diff --git a/canvas/src/autocomplete/actions.rs b/canvas/src/autocomplete/actions.rs index 2dafa65..2c4403a 100644 --- a/canvas/src/autocomplete/actions.rs +++ b/canvas/src/autocomplete/actions.rs @@ -1,9 +1,10 @@ -// src/autocomplete/actions.rs +// canvas/src/autocomplete/actions.rs use crate::canvas::state::{CanvasState, ActionContext}; use crate::autocomplete::state::AutocompleteCanvasState; use crate::canvas::actions::types::{CanvasAction, ActionResult}; -use crate::canvas::actions::edit::handle_generic_canvas_action; // Import the core function +use crate::canvas::actions::edit::handle_generic_canvas_action; +use crate::config::CanvasConfig; use anyhow::Result; /// Version for states that implement rich autocomplete @@ -11,6 +12,7 @@ pub async fn execute_canvas_action_with_autocomplete, ) -> Result { // 1. Try feature-specific handler first let context = ActionContext { @@ -20,31 +22,77 @@ pub async fn execute_canvas_action_with_autocomplete { + println!("AUTO-T on Ins"); + let current_field = state.current_field(); + let current_input = state.get_current_input(); + + if state.supports_autocomplete(current_field) + && !state.is_autocomplete_active() + && current_input.len() >= 1 + { + println!("ACT AUTOC"); + state.activate_autocomplete(); + } + } + + CanvasAction::NextField | CanvasAction::PrevField => { + println!("AUTO-T on nav"); + let current_field = state.current_field(); + + if state.supports_autocomplete(current_field) && !state.is_autocomplete_active() { + state.activate_autocomplete(); + } else if !state.supports_autocomplete(current_field) && state.is_autocomplete_active() { + state.deactivate_autocomplete(); + } + } + + _ => {} // No auto-trigger for other actions + } + } + } + + Ok(result) } /// Handle rich autocomplete actions for AutocompleteCanvasState fn handle_rich_autocomplete_action( - action: &CanvasAction, + action: CanvasAction, state: &mut S, -) -> Result> { + _context: &ActionContext, +) -> Option { match action { CanvasAction::TriggerAutocomplete => { - if state.supports_autocomplete(state.current_field()) { + let current_field = state.current_field(); + if state.supports_autocomplete(current_field) { state.activate_autocomplete(); - Ok(Some(ActionResult::success_with_message("Autocomplete activated - fetching suggestions..."))) + Some(ActionResult::success_with_message("Autocomplete activated")) } else { - Ok(Some(ActionResult::error("Autocomplete not supported for this field"))) + Some(ActionResult::success_with_message("Autocomplete not supported for this field")) + } + } + + CanvasAction::SuggestionUp => { + if state.is_autocomplete_ready() { + if let Some(autocomplete_state) = state.autocomplete_state_mut() { + autocomplete_state.select_previous(); + } + Some(ActionResult::success()) + } else { + Some(ActionResult::success_with_message("No suggestions available")) } } @@ -52,42 +100,34 @@ fn handle_rich_autocomplete_action( if state.is_autocomplete_ready() { if let Some(autocomplete_state) = state.autocomplete_state_mut() { autocomplete_state.select_next(); - return Ok(Some(ActionResult::success())); } + Some(ActionResult::success()) + } else { + Some(ActionResult::success_with_message("No suggestions available")) } - Ok(None) - } - - CanvasAction::SuggestionUp => { - if state.is_autocomplete_ready() { - if let Some(autocomplete_state) = state.autocomplete_state_mut() { - autocomplete_state.select_previous(); - return Ok(Some(ActionResult::success())); - } - } - Ok(None) } CanvasAction::SelectSuggestion => { if state.is_autocomplete_ready() { - if let Some(message) = state.apply_autocomplete_selection() { - return Ok(Some(ActionResult::success_with_message(message))); + if let Some(msg) = state.apply_autocomplete_selection() { + Some(ActionResult::success_with_message(&msg)) } else { - return Ok(Some(ActionResult::error("No suggestion selected"))); + Some(ActionResult::success_with_message("No suggestion selected")) } + } else { + Some(ActionResult::success_with_message("No suggestions available")) } - Ok(None) } CanvasAction::ExitSuggestions => { if state.is_autocomplete_active() { state.deactivate_autocomplete(); - Ok(Some(ActionResult::success_with_message("Autocomplete cancelled"))) + Some(ActionResult::success_with_message("Exited autocomplete")) } else { - Ok(None) + Some(ActionResult::success()) } } - _ => Ok(None), + _ => None, // Not a rich autocomplete action } } diff --git a/canvas/src/canvas/actions/edit.rs b/canvas/src/canvas/actions/edit.rs index 142cd46..8a0cc99 100644 --- a/canvas/src/canvas/actions/edit.rs +++ b/canvas/src/canvas/actions/edit.rs @@ -1,7 +1,8 @@ -// canvas/src/actions/edit.rs +// canvas/src/canvas/actions/edit.rs use crate::canvas::state::{CanvasState, ActionContext}; use crate::canvas::actions::types::{CanvasAction, ActionResult}; +use crate::config::CanvasConfig; use anyhow::Result; /// Execute a typed canvas action on any CanvasState implementation @@ -9,6 +10,7 @@ pub async fn execute_canvas_action( action: CanvasAction, state: &mut S, ideal_cursor_column: &mut usize, + config: Option<&CanvasConfig>, ) -> Result { let context = ActionContext { key_code: None, @@ -21,7 +23,7 @@ pub async fn execute_canvas_action( return Ok(ActionResult::HandledByFeature(result)); } - handle_generic_canvas_action(action, state, ideal_cursor_column).await + handle_generic_canvas_action(action, state, ideal_cursor_column, config).await } /// Handle core canvas actions with full type safety @@ -29,126 +31,84 @@ pub async fn handle_generic_canvas_action( action: CanvasAction, state: &mut S, ideal_cursor_column: &mut usize, + config: Option<&CanvasConfig>, ) -> Result { match action { CanvasAction::InsertChar(c) => { let cursor_pos = state.current_cursor_pos(); - let field_value = state.get_current_input_mut(); - let mut chars: Vec = field_value.chars().collect(); + let input = state.get_current_input_mut(); + input.insert(cursor_pos, c); + state.set_current_cursor_pos(cursor_pos + 1); + state.set_has_unsaved_changes(true); + *ideal_cursor_column = cursor_pos + 1; + Ok(ActionResult::success()) + } - if cursor_pos <= chars.len() { - chars.insert(cursor_pos, c); - *field_value = chars.into_iter().collect(); - state.set_current_cursor_pos(cursor_pos + 1); - state.set_has_unsaved_changes(true); - *ideal_cursor_column = state.current_cursor_pos(); - Ok(ActionResult::success()) - } else { - Ok(ActionResult::error("Invalid cursor position for character insertion")) - } + CanvasAction::NextField | CanvasAction::PrevField => { + let old_field = state.current_field(); + let total_fields = state.fields().len(); + + // Perform field navigation + let new_field = match action { + CanvasAction::NextField => { + if config.map_or(true, |c| c.behavior.wrap_around_fields) { + (old_field + 1) % total_fields + } else { + (old_field + 1).min(total_fields - 1) + } + } + CanvasAction::PrevField => { + if config.map_or(true, |c| c.behavior.wrap_around_fields) { + if old_field == 0 { total_fields - 1 } else { old_field - 1 } + } else { + old_field.saturating_sub(1) + } + } + _ => unreachable!(), + }; + + state.set_current_field(new_field); + *ideal_cursor_column = state.current_cursor_pos(); + Ok(ActionResult::success()) } CanvasAction::DeleteBackward => { - if state.current_cursor_pos() > 0 { - let cursor_pos = state.current_cursor_pos(); - let field_value = state.get_current_input_mut(); - let mut chars: Vec = field_value.chars().collect(); - - if cursor_pos <= chars.len() { - chars.remove(cursor_pos - 1); - *field_value = chars.into_iter().collect(); - let new_pos = cursor_pos - 1; - state.set_current_cursor_pos(new_pos); - state.set_has_unsaved_changes(true); - *ideal_cursor_column = new_pos; - } + let cursor_pos = state.current_cursor_pos(); + if cursor_pos > 0 { + let input = state.get_current_input_mut(); + input.remove(cursor_pos - 1); + state.set_current_cursor_pos(cursor_pos - 1); + state.set_has_unsaved_changes(true); + *ideal_cursor_column = cursor_pos - 1; } Ok(ActionResult::success()) } CanvasAction::DeleteForward => { let cursor_pos = state.current_cursor_pos(); - let field_value = state.get_current_input_mut(); - let mut chars: Vec = field_value.chars().collect(); - - if cursor_pos < chars.len() { - chars.remove(cursor_pos); - *field_value = chars.into_iter().collect(); + let input = state.get_current_input_mut(); + if cursor_pos < input.len() { + input.remove(cursor_pos); state.set_has_unsaved_changes(true); - *ideal_cursor_column = cursor_pos; - } - Ok(ActionResult::success()) - } - - CanvasAction::NextField => { - let num_fields = state.fields().len(); - if num_fields > 0 { - let current_field = state.current_field(); - let new_field = (current_field + 1) % num_fields; - state.set_current_field(new_field); - let current_input = state.get_current_input(); - let max_pos = current_input.len(); - state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos)); - } - Ok(ActionResult::success()) - } - - CanvasAction::PrevField => { - let num_fields = state.fields().len(); - if num_fields > 0 { - let current_field = state.current_field(); - let new_field = if current_field == 0 { - num_fields - 1 - } else { - current_field - 1 - }; - state.set_current_field(new_field); - let current_input = state.get_current_input(); - let max_pos = current_input.len(); - state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos)); } Ok(ActionResult::success()) } CanvasAction::MoveLeft => { - let new_pos = state.current_cursor_pos().saturating_sub(1); - state.set_current_cursor_pos(new_pos); - *ideal_cursor_column = new_pos; + let cursor_pos = state.current_cursor_pos(); + if cursor_pos > 0 { + state.set_current_cursor_pos(cursor_pos - 1); + *ideal_cursor_column = cursor_pos - 1; + } Ok(ActionResult::success()) } CanvasAction::MoveRight => { + let cursor_pos = state.current_cursor_pos(); let current_input = state.get_current_input(); - let current_pos = state.current_cursor_pos(); - if current_pos < current_input.len() { - let new_pos = current_pos + 1; - state.set_current_cursor_pos(new_pos); - *ideal_cursor_column = new_pos; - } - Ok(ActionResult::success()) - } - - CanvasAction::MoveUp => { - let num_fields = state.fields().len(); - if num_fields > 0 { - let current_field = state.current_field(); - let new_field = current_field.saturating_sub(1); - state.set_current_field(new_field); - let current_input = state.get_current_input(); - let max_pos = current_input.len(); - state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos)); - } - Ok(ActionResult::success()) - } - - CanvasAction::MoveDown => { - let num_fields = state.fields().len(); - if num_fields > 0 { - let new_field = (state.current_field() + 1).min(num_fields - 1); - state.set_current_field(new_field); - let current_input = state.get_current_input(); - let max_pos = current_input.len(); - state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos)); + if cursor_pos < current_input.len() { + state.set_current_cursor_pos(cursor_pos + 1); + *ideal_cursor_column = cursor_pos + 1; } Ok(ActionResult::success()) } @@ -160,43 +120,55 @@ pub async fn handle_generic_canvas_action( } CanvasAction::MoveLineEnd => { - let current_input = state.get_current_input(); - let new_pos = current_input.len(); - state.set_current_cursor_pos(new_pos); - *ideal_cursor_column = new_pos; + let end_pos = state.get_current_input().len(); + state.set_current_cursor_pos(end_pos); + *ideal_cursor_column = end_pos; + Ok(ActionResult::success()) + } + + CanvasAction::MoveUp => { + // For single-line fields, move to previous field + let current_field = state.current_field(); + if current_field > 0 { + state.set_current_field(current_field - 1); + *ideal_cursor_column = state.current_cursor_pos(); + } + Ok(ActionResult::success()) + } + + CanvasAction::MoveDown => { + // For single-line fields, move to next field + let current_field = state.current_field(); + let total_fields = state.fields().len(); + if current_field < total_fields - 1 { + state.set_current_field(current_field + 1); + *ideal_cursor_column = state.current_cursor_pos(); + } Ok(ActionResult::success()) } CanvasAction::MoveFirstLine => { - let num_fields = state.fields().len(); - if num_fields > 0 { - state.set_current_field(0); - let current_input = state.get_current_input(); - let max_pos = current_input.len(); - state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos)); - } - Ok(ActionResult::success_with_message("Moved to first field")) + state.set_current_field(0); + state.set_current_cursor_pos(0); + *ideal_cursor_column = 0; + Ok(ActionResult::success()) } CanvasAction::MoveLastLine => { - let num_fields = state.fields().len(); - if num_fields > 0 { - let new_field = num_fields - 1; - state.set_current_field(new_field); - let current_input = state.get_current_input(); - let max_pos = current_input.len(); - state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos)); - } - Ok(ActionResult::success_with_message("Moved to last field")) + let last_field = state.fields().len() - 1; + state.set_current_field(last_field); + let end_pos = state.get_current_input().len(); + state.set_current_cursor_pos(end_pos); + *ideal_cursor_column = end_pos; + Ok(ActionResult::success()) } CanvasAction::MoveWordNext => { let current_input = state.get_current_input(); if !current_input.is_empty() { let new_pos = find_next_word_start(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; + state.set_current_cursor_pos(new_pos); + *ideal_cursor_column = new_pos; } Ok(ActionResult::success()) } @@ -204,19 +176,9 @@ pub async fn handle_generic_canvas_action( CanvasAction::MoveWordEnd => { let current_input = state.get_current_input(); if !current_input.is_empty() { - let current_pos = state.current_cursor_pos(); - let new_pos = find_word_end(current_input, current_pos); - - 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; + let new_pos = find_word_end(current_input, state.current_cursor_pos()); + state.set_current_cursor_pos(new_pos); + *ideal_cursor_column = new_pos; } Ok(ActionResult::success()) } @@ -231,148 +193,61 @@ pub async fn handle_generic_canvas_action( Ok(ActionResult::success()) } - CanvasAction::MoveWordEndPrev => { - let current_input = state.get_current_input(); - if !current_input.is_empty() { - let new_pos = find_prev_word_end(current_input, state.current_cursor_pos()); - state.set_current_cursor_pos(new_pos); - *ideal_cursor_column = new_pos; - } - Ok(ActionResult::success_with_message("Moved to previous word end")) - } - CanvasAction::Custom(action_str) => { - Ok(ActionResult::error(format!("Unknown or unhandled custom action: {}", action_str))) + Ok(ActionResult::success_with_message(&format!("Custom action: {}", action_str))) } - // Autocomplete actions are handled by the autocomplete module - CanvasAction::TriggerAutocomplete | CanvasAction::SuggestionUp | CanvasAction::SuggestionDown | - CanvasAction::SelectSuggestion | CanvasAction::ExitSuggestions => { - Ok(ActionResult::error("Autocomplete actions should be handled by autocomplete module")) - } + _ => Ok(ActionResult::success_with_message("Action not implemented")), } } -// Word movement 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 { +// Helper functions for word navigation +fn find_next_word_start(text: &str, cursor_pos: usize) -> usize { let chars: Vec = text.chars().collect(); - let len = chars.len(); - if len == 0 || current_pos >= len { - return len; - } - - let mut pos = current_pos; - let initial_type = get_char_type(chars[pos]); - - while pos < len && get_char_type(chars[pos]) == initial_type { + let mut pos = cursor_pos; + + // Skip current word + while pos < chars.len() && chars[pos].is_alphanumeric() { pos += 1; } - - while pos < len && get_char_type(chars[pos]) == CharType::Whitespace { + + // Skip whitespace + while pos < chars.len() && chars[pos].is_whitespace() { pos += 1; } - + pos } -fn find_word_end(text: &str, current_pos: usize) -> usize { +fn find_word_end(text: &str, cursor_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); - - if get_char_type(chars[pos]) == CharType::Whitespace { - 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 < len && get_char_type(chars[pos]) == word_type { + let mut pos = cursor_pos; + + // Move to end of current word + while pos < chars.len() && chars[pos].is_alphanumeric() { pos += 1; } - - pos.saturating_sub(1).min(len.saturating_sub(1)) -} - -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 pos == 0 && get_char_type(chars[pos]) == CharType::Whitespace { - return 0; - } - - 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 { +fn find_prev_word_start(text: &str, cursor_pos: usize) -> usize { + if cursor_pos == 0 { + return 0; + } + let chars: Vec = text.chars().collect(); - let len = chars.len(); - if len == 0 || current_pos == 0 { - return 0; - } - - let mut pos = current_pos.saturating_sub(1); - - while pos > 0 && get_char_type(chars[pos]) == CharType::Whitespace { + let mut pos = cursor_pos.saturating_sub(1); + + // Skip whitespace + while pos > 0 && chars[pos].is_whitespace() { pos -= 1; } - - if pos == 0 && get_char_type(chars[pos]) == CharType::Whitespace { - return 0; - } - if pos == 0 && get_char_type(chars[pos]) != CharType::Whitespace { - return 0; - } - - let word_type = get_char_type(chars[pos]); - while pos > 0 && get_char_type(chars[pos - 1]) == word_type { + + // Skip to start of word + while pos > 0 && chars[pos - 1].is_alphanumeric() { pos -= 1; } - - while pos > 0 && get_char_type(chars[pos - 1]) == CharType::Whitespace { - pos -= 1; - } - - if pos > 0 { - pos - 1 - } else { - 0 - } + + pos } diff --git a/canvas/src/canvas/actions/mod.rs b/canvas/src/canvas/actions/mod.rs index 4b993ca..42bf355 100644 --- a/canvas/src/canvas/actions/mod.rs +++ b/canvas/src/canvas/actions/mod.rs @@ -4,4 +4,4 @@ pub mod edit; // Re-export the main types for convenience pub use types::{CanvasAction, ActionResult}; -pub use edit::execute_canvas_action; // Remove execute_edit_action +pub use edit::execute_canvas_action; diff --git a/canvas/src/canvas/actions/types.rs b/canvas/src/canvas/actions/types.rs index f73b1de..45188bf 100644 --- a/canvas/src/canvas/actions/types.rs +++ b/canvas/src/canvas/actions/types.rs @@ -1,9 +1,6 @@ -// canvas/src/actions/types.rs +// src/canvas/actions/types.rs -use crossterm::event::KeyCode; - -/// All possible canvas actions, type-safe and exhaustive -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq)] pub enum CanvasAction { // Character input InsertChar(char), @@ -28,37 +25,43 @@ pub enum CanvasAction { MoveWordNext, MoveWordEnd, MoveWordPrev, - MoveWordEndPrev, // Field navigation NextField, PrevField, - // AUTOCOMPLETE ACTIONS (NEW) - /// Manually trigger autocomplete for current field + // Autocomplete actions TriggerAutocomplete, - /// Move to next suggestion SuggestionUp, - /// Move to previous suggestion SuggestionDown, - /// Select the currently highlighted suggestion SelectSuggestion, - /// Cancel/exit autocomplete mode ExitSuggestions, - // Custom actions (escape hatch for feature-specific behavior) + // Custom actions Custom(String), } impl CanvasAction { - /// Convert a string action to typed action (for backwards compatibility during migration) + pub fn from_key(key: crossterm::event::KeyCode) -> Option { + match key { + crossterm::event::KeyCode::Char(c) => Some(Self::InsertChar(c)), + crossterm::event::KeyCode::Backspace => Some(Self::DeleteBackward), + crossterm::event::KeyCode::Delete => Some(Self::DeleteForward), + crossterm::event::KeyCode::Left => Some(Self::MoveLeft), + crossterm::event::KeyCode::Right => Some(Self::MoveRight), + crossterm::event::KeyCode::Up => Some(Self::MoveUp), + crossterm::event::KeyCode::Down => Some(Self::MoveDown), + crossterm::event::KeyCode::Home => Some(Self::MoveLineStart), + crossterm::event::KeyCode::End => Some(Self::MoveLineEnd), + crossterm::event::KeyCode::Tab => Some(Self::NextField), + crossterm::event::KeyCode::BackTab => Some(Self::PrevField), + _ => None, + } + } + + // Backward compatibility method pub fn from_string(action: &str) -> Self { match action { - "insert_char" => { - // This is a bit tricky - we need the char from context - // For now, we'll use Custom until we refactor the call sites - Self::Custom(action.to_string()) - } "delete_char_backward" => Self::DeleteBackward, "delete_char_forward" => Self::DeleteForward, "move_left" => Self::MoveLeft, @@ -72,10 +75,8 @@ impl CanvasAction { "move_word_next" => Self::MoveWordNext, "move_word_end" => Self::MoveWordEnd, "move_word_prev" => Self::MoveWordPrev, - "move_word_end_prev" => Self::MoveWordEndPrev, "next_field" => Self::NextField, "prev_field" => Self::PrevField, - // Autocomplete actions "trigger_autocomplete" => Self::TriggerAutocomplete, "suggestion_up" => Self::SuggestionUp, "suggestion_down" => Self::SuggestionDown, @@ -84,94 +85,13 @@ impl CanvasAction { _ => Self::Custom(action.to_string()), } } - - /// Get string representation (for logging, debugging) - pub fn as_str(&self) -> &str { - match self { - Self::InsertChar(_) => "insert_char", - Self::DeleteBackward => "delete_char_backward", - Self::DeleteForward => "delete_char_forward", - Self::MoveLeft => "move_left", - Self::MoveRight => "move_right", - Self::MoveUp => "move_up", - Self::MoveDown => "move_down", - Self::MoveLineStart => "move_line_start", - Self::MoveLineEnd => "move_line_end", - Self::MoveFirstLine => "move_first_line", - Self::MoveLastLine => "move_last_line", - Self::MoveWordNext => "move_word_next", - Self::MoveWordEnd => "move_word_end", - Self::MoveWordPrev => "move_word_prev", - Self::MoveWordEndPrev => "move_word_end_prev", - Self::NextField => "next_field", - Self::PrevField => "prev_field", - // Autocomplete actions - Self::TriggerAutocomplete => "trigger_autocomplete", - Self::SuggestionUp => "suggestion_up", - Self::SuggestionDown => "suggestion_down", - Self::SelectSuggestion => "select_suggestion", - Self::ExitSuggestions => "exit_suggestions", - Self::Custom(s) => s, - } - } - - /// Create action from KeyCode for common cases - pub fn from_key(key: KeyCode) -> Option { - match key { - KeyCode::Char(c) => Some(Self::InsertChar(c)), - KeyCode::Backspace => Some(Self::DeleteBackward), - KeyCode::Delete => Some(Self::DeleteForward), - KeyCode::Left => Some(Self::MoveLeft), - KeyCode::Right => Some(Self::MoveRight), - KeyCode::Up => Some(Self::MoveUp), - KeyCode::Down => Some(Self::MoveDown), - KeyCode::Home => Some(Self::MoveLineStart), - KeyCode::End => Some(Self::MoveLineEnd), - KeyCode::Tab => Some(Self::NextField), - KeyCode::BackTab => Some(Self::PrevField), - _ => None, - } - } - - /// Check if this action modifies content - pub fn is_modifying(&self) -> bool { - matches!(self, - Self::InsertChar(_) | - Self::DeleteBackward | - Self::DeleteForward | - Self::SelectSuggestion - ) - } - - /// Check if this action moves the cursor - pub fn is_movement(&self) -> bool { - matches!(self, - Self::MoveLeft | Self::MoveRight | Self::MoveUp | Self::MoveDown | - Self::MoveLineStart | Self::MoveLineEnd | Self::MoveFirstLine | Self::MoveLastLine | - Self::MoveWordNext | Self::MoveWordEnd | Self::MoveWordPrev | Self::MoveWordEndPrev | - Self::NextField | Self::PrevField - ) - } - - /// Check if this is a suggestion-related action - pub fn is_suggestion(&self) -> bool { - matches!(self, - Self::TriggerAutocomplete | Self::SuggestionUp | Self::SuggestionDown | - Self::SelectSuggestion | Self::ExitSuggestions - ) - } } -/// Result of executing a canvas action -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq)] pub enum ActionResult { - /// Action completed successfully, optional message for user feedback Success(Option), - /// Action was handled by custom feature logic HandledByFeature(String), - /// Action requires additional context or cannot be performed RequiresContext(String), - /// Action failed with error message Error(String), } @@ -180,11 +100,11 @@ impl ActionResult { Self::Success(None) } - pub fn success_with_message(msg: impl Into) -> Self { - Self::Success(Some(msg.into())) + pub fn success_with_message(msg: &str) -> Self { + Self::Success(Some(msg.to_string())) } - pub fn error(msg: impl Into) -> Self { + pub fn error(msg: &str) -> Self { Self::Error(msg.into()) } @@ -201,37 +121,3 @@ impl ActionResult { } } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_action_from_string() { - assert_eq!(CanvasAction::from_string("move_left"), CanvasAction::MoveLeft); - assert_eq!(CanvasAction::from_string("delete_char_backward"), CanvasAction::DeleteBackward); - assert_eq!(CanvasAction::from_string("trigger_autocomplete"), CanvasAction::TriggerAutocomplete); - assert_eq!(CanvasAction::from_string("unknown"), CanvasAction::Custom("unknown".to_string())); - } - - #[test] - fn test_action_from_key() { - assert_eq!(CanvasAction::from_key(KeyCode::Char('a')), Some(CanvasAction::InsertChar('a'))); - assert_eq!(CanvasAction::from_key(KeyCode::Left), Some(CanvasAction::MoveLeft)); - assert_eq!(CanvasAction::from_key(KeyCode::Backspace), Some(CanvasAction::DeleteBackward)); - assert_eq!(CanvasAction::from_key(KeyCode::F(1)), None); - } - - #[test] - fn test_action_properties() { - assert!(CanvasAction::InsertChar('a').is_modifying()); - assert!(!CanvasAction::MoveLeft.is_modifying()); - - assert!(CanvasAction::MoveLeft.is_movement()); - assert!(!CanvasAction::InsertChar('a').is_movement()); - - assert!(CanvasAction::SuggestionUp.is_suggestion()); - assert!(CanvasAction::TriggerAutocomplete.is_suggestion()); - assert!(!CanvasAction::MoveLeft.is_suggestion()); - } -} diff --git a/canvas/src/config.rs b/canvas/src/config.rs index f5e027c..4aba1ac 100644 --- a/canvas/src/config.rs +++ b/canvas/src/config.rs @@ -180,6 +180,20 @@ impl CanvasConfig { Self::from_toml(&contents) } + /// NEW: Check if autocomplete should auto-trigger (simple logic) + pub fn should_auto_trigger_autocomplete(&self) -> bool { + // If trigger_autocomplete keybinding exists anywhere, use manual mode only + // If no trigger_autocomplete keybinding, use auto-trigger mode + !self.has_trigger_autocomplete_keybinding() + } + + /// NEW: Check if user has configured manual trigger keybinding + pub fn has_trigger_autocomplete_keybinding(&self) -> bool { + self.keybindings.edit.contains_key("trigger_autocomplete") || + self.keybindings.read_only.contains_key("trigger_autocomplete") || + self.keybindings.global.contains_key("trigger_autocomplete") + } + /// Get action for key in read-only mode pub fn get_read_only_action(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> { self.get_action_in_mode(&self.keybindings.read_only, key, modifiers) diff --git a/canvas/src/dispatcher.rs b/canvas/src/dispatcher.rs index 064e9b1..78332c0 100644 --- a/canvas/src/dispatcher.rs +++ b/canvas/src/dispatcher.rs @@ -2,6 +2,7 @@ use crate::canvas::state::CanvasState; use crate::canvas::actions::{CanvasAction, ActionResult, execute_canvas_action}; +use crate::config::CanvasConfig; /// High-level action dispatcher that coordinates between different action types pub struct ActionDispatcher; @@ -13,7 +14,9 @@ impl ActionDispatcher { state: &mut S, ideal_cursor_column: &mut usize, ) -> anyhow::Result { - execute_canvas_action(action, state, ideal_cursor_column).await + + // Load config once here instead of threading it everywhere + execute_canvas_action(action, state, ideal_cursor_column, Some(&CanvasConfig::load())).await } /// Quick action dispatch from KeyCode diff --git a/client/canvas_config.toml b/client/canvas_config.toml index 6cb6c61..147ad93 100644 --- a/client/canvas_config.toml +++ b/client/canvas_config.toml @@ -42,6 +42,7 @@ move_word_next = ["Ctrl+Right"] move_word_prev = ["Ctrl+Left"] next_field = ["Tab"] prev_field = ["Shift+Tab"] +trigger_autocomplete = ["Ctrl+p"] # Suggestion/autocomplete keybindings [keybindings.suggestions]