diff --git a/canvas/canvas_config.toml b/canvas/canvas_config.toml index 8f5ce4d..7f561c0 100644 --- a/canvas/canvas_config.toml +++ b/canvas/canvas_config.toml @@ -24,7 +24,7 @@ move_word_end_prev = ["ge"] move_line_start = ["0"] move_line_end = ["$"] move_first_line = ["gg"] -move_last_line = ["G"] +move_last_line = ["shift+g"] next_field = ["Tab"] prev_field = ["Shift+Tab"] diff --git a/canvas/src/actions/edit.rs b/canvas/src/actions/edit.rs index a665f3a..6ddfe41 100644 --- a/canvas/src/actions/edit.rs +++ b/canvas/src/actions/edit.rs @@ -1,6 +1,6 @@ // canvas/src/actions/edit.rs -use crate::state::{CanvasState, ActionContext}; +use crate::state::{CanvasState, AutocompleteCanvasState, ActionContext}; use crate::actions::types::{CanvasAction, ActionResult}; use crossterm::event::{KeyCode, KeyEvent}; use anyhow::Result; @@ -13,7 +13,7 @@ pub async fn execute_canvas_action( ) -> Result { // 1. Try feature-specific handler first let context = ActionContext { - key_code: None, // We don't need KeyCode anymore since action is typed + key_code: None, ideal_cursor_column: *ideal_cursor_column, current_input: state.get_current_input().to_string(), current_field: state.current_field(), @@ -23,7 +23,7 @@ pub async fn execute_canvas_action( return Ok(ActionResult::HandledByFeature(result)); } - // 2. Handle autocomplete actions + // 2. Handle autocomplete actions (falls back to legacy methods) if let Some(result) = handle_autocomplete_action(&action, state)? { return Ok(result); } @@ -32,6 +32,33 @@ pub async fn execute_canvas_action( handle_generic_canvas_action(action, state, ideal_cursor_column).await } +/// Version for states that implement rich autocomplete +pub async fn execute_canvas_action_with_autocomplete( + action: CanvasAction, + state: &mut S, + ideal_cursor_column: &mut usize, +) -> Result { + // 1. Try feature-specific handler first + let context = ActionContext { + key_code: None, + ideal_cursor_column: *ideal_cursor_column, + current_input: state.get_current_input().to_string(), + current_field: state.current_field(), + }; + + if let Some(result) = state.handle_feature_action(&action, &context) { + return Ok(ActionResult::HandledByFeature(result)); + } + + // 2. Handle rich autocomplete actions + if let Some(result) = handle_rich_autocomplete_action(&action, state)? { + return Ok(result); + } + + // 3. Handle generic canvas actions + handle_generic_canvas_action(action, state, ideal_cursor_column).await +} + /// Legacy function for string-based actions (backwards compatibility) pub async fn execute_edit_action( action: &str, @@ -56,10 +83,78 @@ pub async fn execute_edit_action( Ok(result.message().unwrap_or("").to_string()) } -/// Handle autocomplete-related actions +/// Handle autocomplete actions for basic CanvasState (uses legacy methods) fn handle_autocomplete_action( action: &CanvasAction, state: &mut S, +) -> Result> { + match action { + CanvasAction::TriggerAutocomplete => { + // For basic CanvasState, just return an error or no-op + Ok(Some(ActionResult::error("Autocomplete not supported - implement AutocompleteCanvasState for rich autocomplete"))) + } + + CanvasAction::SuggestionDown => { + // Try legacy suggestions + if let Some(suggestions) = state.get_suggestions() { + if !suggestions.is_empty() { + let current = state.get_selected_suggestion_index().unwrap_or(0); + let next = (current + 1) % suggestions.len(); + state.set_selected_suggestion_index(Some(next)); + return Ok(Some(ActionResult::success())); + } + } + Ok(None) + } + + CanvasAction::SuggestionUp => { + // Try legacy suggestions + if let Some(suggestions) = state.get_suggestions() { + if !suggestions.is_empty() { + let current = state.get_selected_suggestion_index().unwrap_or(0); + let prev = if current == 0 { suggestions.len() - 1 } else { current - 1 }; + state.set_selected_suggestion_index(Some(prev)); + return Ok(Some(ActionResult::success())); + } + } + Ok(None) + } + + CanvasAction::SelectSuggestion => { + // Try legacy suggestions + if let Some(suggestions) = state.get_suggestions() { + if let Some(index) = state.get_selected_suggestion_index() { + if let Some(selected) = suggestions.get(index) { + // Clone the string first to avoid borrowing issues + let selected_text = selected.clone(); + + // Now we can mutate state without holding any references + *state.get_current_input_mut() = selected_text.clone(); + state.set_current_cursor_pos(selected_text.len()); + state.set_has_unsaved_changes(true); + state.deactivate_suggestions(); + return Ok(Some(ActionResult::success_with_message( + format!("Selected: {}", selected_text) + ))); + } + } + } + Ok(None) + } + + CanvasAction::ExitSuggestions => { + state.deactivate_suggestions(); + Ok(Some(ActionResult::success_with_message("Suggestions cancelled"))) + } + + _ => Ok(None), + } +} + +/// Handle rich autocomplete actions for AutocompleteCanvasState +fn handle_rich_autocomplete_action( + action: &CanvasAction, + state: &mut S, ) -> Result> { match action { CanvasAction::TriggerAutocomplete => { @@ -78,7 +173,7 @@ fn handle_autocomplete_action( return Ok(Some(ActionResult::success())); } } - Ok(None) // Not handled - no active autocomplete + Ok(None) } CanvasAction::SuggestionUp => { @@ -88,7 +183,7 @@ fn handle_autocomplete_action( return Ok(Some(ActionResult::success())); } } - Ok(None) // Not handled - no active autocomplete + Ok(None) } CanvasAction::SelectSuggestion => { @@ -99,7 +194,7 @@ fn handle_autocomplete_action( return Ok(Some(ActionResult::error("No suggestion selected"))); } } - Ok(None) // Not handled - no active autocomplete + Ok(None) } CanvasAction::ExitSuggestions => { @@ -107,11 +202,11 @@ fn handle_autocomplete_action( state.deactivate_autocomplete(); Ok(Some(ActionResult::success_with_message("Autocomplete cancelled"))) } else { - Ok(None) // Not handled - autocomplete not active + Ok(None) } } - _ => Ok(None), // Not an autocomplete action + _ => Ok(None), } } @@ -336,7 +431,6 @@ async fn handle_generic_canvas_action( Ok(ActionResult::error(format!("Unknown or unhandled custom action: {}", action_str))) } - // Autocomplete actions should have been handled above CanvasAction::TriggerAutocomplete | CanvasAction::SuggestionUp | CanvasAction::SuggestionDown | CanvasAction::SelectSuggestion | CanvasAction::ExitSuggestions => { Ok(ActionResult::error("Autocomplete action not handled properly")) @@ -344,8 +438,7 @@ async fn handle_generic_canvas_action( } } -// Word movement helper functions (unchanged from previous implementation) - +// Word movement helper functions #[derive(PartialEq)] enum CharType { Whitespace, diff --git a/canvas/src/state.rs b/canvas/src/state.rs index e4e6d6b..3cdea73 100644 --- a/canvas/src/state.rs +++ b/canvas/src/state.rs @@ -1,7 +1,6 @@ // canvas/src/state.rs use crate::actions::CanvasAction; -use crate::autocomplete::{AutocompleteState, SuggestionItem}; /// Context passed to feature-specific action handlers #[derive(Debug)] @@ -32,8 +31,74 @@ pub trait CanvasState { fn has_unsaved_changes(&self) -> bool; fn set_has_unsaved_changes(&mut self, changed: bool); - // --- AUTOCOMPLETE SUPPORT (NEW) --- + // --- LEGACY AUTOCOMPLETE SUPPORT (for backwards compatibility) --- + /// Legacy suggestion support (deprecated - use AutocompleteCanvasState for rich features) + fn get_suggestions(&self) -> Option<&[String]> { + None + } + + /// Legacy selected suggestion index (deprecated) + fn get_selected_suggestion_index(&self) -> Option { + None + } + + /// Legacy suggestion index setter (deprecated) + fn set_selected_suggestion_index(&mut self, _index: Option) { + // Default: no-op + } + + /// Legacy activate suggestions (deprecated) + fn activate_suggestions(&mut self, _suggestions: Vec) { + // Default: no-op + } + + /// Legacy deactivate suggestions (deprecated) + fn deactivate_suggestions(&mut self) { + // Default: no-op + } + + // --- Feature-specific action handling --- + + /// Feature-specific action handling (NEW: Type-safe) + fn handle_feature_action(&mut self, _action: &CanvasAction, _context: &ActionContext) -> Option { + None // Default: no feature-specific handling + } + + /// Legacy string-based action handling (for backwards compatibility) + fn handle_feature_action_legacy(&mut self, action: &str, context: &ActionContext) -> Option { + // Convert string to typed action and delegate + let typed_action = match action { + "insert_char" => { + // This is tricky - we need the char from the KeyCode in context + if let Some(crossterm::event::KeyCode::Char(c)) = context.key_code { + CanvasAction::InsertChar(c) + } else { + CanvasAction::Custom(action.to_string()) + } + } + _ => CanvasAction::from_string(action), + }; + self.handle_feature_action(&typed_action, context) + } + + // --- Display Overrides (for links, computed values, etc.) --- + + fn get_display_value_for_field(&self, index: usize) -> &str { + self.inputs() + .get(index) + .map(|s| s.as_str()) + .unwrap_or("") + } + + fn has_display_override(&self, _index: usize) -> bool { + false + } +} + +/// OPTIONAL extension trait for states that want rich autocomplete functionality. +/// Only implement this if you need the new autocomplete features. +pub trait AutocompleteCanvasState: CanvasState { /// Associated type for suggestion data (e.g., Hit, String, CustomType) type SuggestionData: Clone + Send + 'static; @@ -43,12 +108,12 @@ pub trait CanvasState { } /// Get autocomplete state (read-only) - fn autocomplete_state(&self) -> Option<&AutocompleteState> { + fn autocomplete_state(&self) -> Option<&crate::autocomplete::AutocompleteState> { None // Default: no autocomplete state } /// Get autocomplete state (mutable) - fn autocomplete_state_mut(&mut self) -> Option<&mut AutocompleteState> { + fn autocomplete_state_mut(&mut self) -> Option<&mut crate::autocomplete::AutocompleteState> { None // Default: no autocomplete state } @@ -68,7 +133,7 @@ pub trait CanvasState { } /// CLIENT API: Set suggestions (called after async fetch completes) - fn set_autocomplete_suggestions(&mut self, suggestions: Vec>) { + fn set_autocomplete_suggestions(&mut self, suggestions: Vec>) { if let Some(state) = self.autocomplete_state_mut() { state.set_suggestions(suggestions); } @@ -122,69 +187,4 @@ pub trait CanvasState { None } } - - // --- LEGACY AUTOCOMPLETE SUPPORT (for backwards compatibility) --- - - /// Legacy suggestion support (deprecated - use autocomplete_state instead) - fn get_suggestions(&self) -> Option<&[String]> { - None - } - - /// Legacy selected suggestion index (deprecated) - fn get_selected_suggestion_index(&self) -> Option { - self.autocomplete_state() - .and_then(|state| state.selected_index) - } - - /// Legacy suggestion index setter (deprecated) - fn set_selected_suggestion_index(&mut self, _index: Option) { - // Deprecated - canvas manages selection internally - } - - /// Legacy activate suggestions (deprecated) - fn activate_suggestions(&mut self, _suggestions: Vec) { - // Deprecated - use set_autocomplete_suggestions instead - } - - /// Legacy deactivate suggestions (deprecated) - fn deactivate_suggestions(&mut self) { - self.deactivate_autocomplete(); - } - - // --- Feature-specific action handling --- - - /// Feature-specific action handling (NEW: Type-safe) - fn handle_feature_action(&mut self, _action: &CanvasAction, _context: &ActionContext) -> Option { - None // Default: no feature-specific handling - } - - /// Legacy string-based action handling (for backwards compatibility) - fn handle_feature_action_legacy(&mut self, action: &str, context: &ActionContext) -> Option { - // Convert string to typed action and delegate - let typed_action = match action { - "insert_char" => { - // This is tricky - we need the char from the KeyCode in context - if let Some(crossterm::event::KeyCode::Char(c)) = context.key_code { - CanvasAction::InsertChar(c) - } else { - CanvasAction::Custom(action.to_string()) - } - } - _ => CanvasAction::from_string(action), - }; - self.handle_feature_action(&typed_action, context) - } - - // --- Display Overrides (for links, computed values, etc.) --- - - fn get_display_value_for_field(&self, index: usize) -> &str { - self.inputs() - .get(index) - .map(|s| s.as_str()) - .unwrap_or("") - } - - fn has_display_override(&self, _index: usize) -> bool { - false - } }