diff --git a/canvas/src/autocomplete/actions.rs b/canvas/src/autocomplete/actions.rs new file mode 100644 index 0000000..2dafa65 --- /dev/null +++ b/canvas/src/autocomplete/actions.rs @@ -0,0 +1,93 @@ +// 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 anyhow::Result; + +/// 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 +} + +/// Handle rich autocomplete actions for AutocompleteCanvasState +fn handle_rich_autocomplete_action( + action: &CanvasAction, + state: &mut S, +) -> Result> { + match action { + CanvasAction::TriggerAutocomplete => { + if state.supports_autocomplete(state.current_field()) { + state.activate_autocomplete(); + Ok(Some(ActionResult::success_with_message("Autocomplete activated - fetching suggestions..."))) + } else { + Ok(Some(ActionResult::error("Autocomplete not supported for this field"))) + } + } + + CanvasAction::SuggestionDown => { + if state.is_autocomplete_ready() { + if let Some(autocomplete_state) = state.autocomplete_state_mut() { + autocomplete_state.select_next(); + return Ok(Some(ActionResult::success())); + } + } + 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))); + } else { + return Ok(Some(ActionResult::error("No suggestion selected"))); + } + } + Ok(None) + } + + CanvasAction::ExitSuggestions => { + if state.is_autocomplete_active() { + state.deactivate_autocomplete(); + Ok(Some(ActionResult::success_with_message("Autocomplete cancelled"))) + } else { + Ok(None) + } + } + + _ => Ok(None), + } +} diff --git a/canvas/src/autocomplete/gui.rs b/canvas/src/autocomplete/gui.rs index 5fba042..ace8c08 100644 --- a/canvas/src/autocomplete/gui.rs +++ b/canvas/src/autocomplete/gui.rs @@ -8,7 +8,7 @@ use ratatui::{ Frame, }; -use crate::autocomplete::AutocompleteState; +use crate::autocomplete::types::AutocompleteState; #[cfg(feature = "gui")] use crate::canvas::theme::CanvasTheme; diff --git a/canvas/src/autocomplete/mod.rs b/canvas/src/autocomplete/mod.rs index c058ee4..26583b9 100644 --- a/canvas/src/autocomplete/mod.rs +++ b/canvas/src/autocomplete/mod.rs @@ -1,8 +1,10 @@ // src/autocomplete/mod.rs pub mod types; pub mod gui; -pub mod state; // Add this line +pub mod state; +pub mod actions; // Re-export autocomplete types pub use types::{SuggestionItem, AutocompleteState}; -pub use state::AutocompleteCanvasState; // Add this line +pub use state::AutocompleteCanvasState; +pub use actions::execute_canvas_action_with_autocomplete; diff --git a/canvas/src/canvas/actions/edit.rs b/canvas/src/canvas/actions/edit.rs index a750b86..142cd46 100644 --- a/canvas/src/canvas/actions/edit.rs +++ b/canvas/src/canvas/actions/edit.rs @@ -1,9 +1,7 @@ // canvas/src/actions/edit.rs use crate::canvas::state::{CanvasState, ActionContext}; -use crate::autocomplete::state::AutocompleteCanvasState; use crate::canvas::actions::types::{CanvasAction, ActionResult}; -use crossterm::event::{KeyCode, KeyEvent}; use anyhow::Result; /// Execute a typed canvas action on any CanvasState implementation @@ -12,7 +10,6 @@ pub async fn execute_canvas_action( 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, @@ -24,195 +21,11 @@ pub async fn execute_canvas_action( return Ok(ActionResult::HandledByFeature(result)); } - // 2. Handle autocomplete actions (falls back to legacy methods) - if let Some(result) = handle_autocomplete_action(&action, state)? { - return Ok(result); - } - - // 3. Handle generic canvas actions 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, - key: KeyEvent, - state: &mut S, - ideal_cursor_column: &mut usize, -) -> Result { - let typed_action = match action { - "insert_char" => { - if let KeyCode::Char(c) = key.code { - CanvasAction::InsertChar(c) - } else { - return Ok("Error: insert_char called without a char key.".to_string()); - } - } - _ => CanvasAction::from_string(action), - }; - - let result = execute_canvas_action(typed_action, state, ideal_cursor_column).await?; - - // Convert ActionResult back to string for backwards compatibility - Ok(result.message().unwrap_or("").to_string()) -} - -/// 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 => { - if state.supports_autocomplete(state.current_field()) { - state.activate_autocomplete(); - Ok(Some(ActionResult::success_with_message("Autocomplete activated - fetching suggestions..."))) - } else { - Ok(Some(ActionResult::error("Autocomplete not supported for this field"))) - } - } - - CanvasAction::SuggestionDown => { - if state.is_autocomplete_ready() { - if let Some(autocomplete_state) = state.autocomplete_state_mut() { - autocomplete_state.select_next(); - return Ok(Some(ActionResult::success())); - } - } - 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))); - } else { - return Ok(Some(ActionResult::error("No suggestion selected"))); - } - } - Ok(None) - } - - CanvasAction::ExitSuggestions => { - if state.is_autocomplete_active() { - state.deactivate_autocomplete(); - Ok(Some(ActionResult::success_with_message("Autocomplete cancelled"))) - } else { - Ok(None) - } - } - - _ => Ok(None), - } -} - /// Handle core canvas actions with full type safety -async fn handle_generic_canvas_action( +pub async fn handle_generic_canvas_action( action: CanvasAction, state: &mut S, ideal_cursor_column: &mut usize, @@ -432,9 +245,10 @@ async fn handle_generic_canvas_action( Ok(ActionResult::error(format!("Unknown or unhandled 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 action not handled properly")) + Ok(ActionResult::error("Autocomplete actions should be handled by autocomplete module")) } } } diff --git a/canvas/src/canvas/actions/mod.rs b/canvas/src/canvas/actions/mod.rs index 2eda674..4b993ca 100644 --- a/canvas/src/canvas/actions/mod.rs +++ b/canvas/src/canvas/actions/mod.rs @@ -1,8 +1,7 @@ -// canvas/src/actions/mod.rs - +// canvas/src/canvas/actions/mod.rs pub mod types; pub mod edit; // Re-export the main types for convenience pub use types::{CanvasAction, ActionResult}; -pub use edit::{execute_canvas_action, execute_edit_action}; +pub use edit::execute_canvas_action; // Remove execute_edit_action diff --git a/canvas/src/canvas/mod.rs b/canvas/src/canvas/mod.rs index 4729640..d4f9fbd 100644 --- a/canvas/src/canvas/mod.rs +++ b/canvas/src/canvas/mod.rs @@ -3,12 +3,15 @@ pub mod actions; pub mod modes; pub mod gui; pub mod theme; -pub mod state; // Add this line +pub mod state; // Re-export commonly used canvas types pub use actions::{CanvasAction, ActionResult}; pub use modes::{AppMode, ModeManager, HighlightState}; -pub use state::{CanvasState, ActionContext}; // Add this line +pub use state::{CanvasState, ActionContext}; #[cfg(feature = "gui")] pub use theme::CanvasTheme; + +#[cfg(feature = "gui")] +pub use gui::render_canvas; diff --git a/canvas/src/canvas/state.rs b/canvas/src/canvas/state.rs index 4998372..29eb075 100644 --- a/canvas/src/canvas/state.rs +++ b/canvas/src/canvas/state.rs @@ -31,33 +31,6 @@ pub trait CanvasState { fn has_unsaved_changes(&self) -> bool; fn set_has_unsaved_changes(&mut self, changed: bool); - // --- 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) @@ -65,25 +38,7 @@ pub trait CanvasState { 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) diff --git a/canvas/src/lib.rs b/canvas/src/lib.rs index 544d37c..c25e34b 100644 --- a/canvas/src/lib.rs +++ b/canvas/src/lib.rs @@ -3,17 +3,3 @@ pub mod canvas; pub mod autocomplete; pub mod config; pub mod dispatcher; -pub mod suggestions; // Keep for backwards compatibility - -// Re-export from modules -pub use canvas::{CanvasAction, ActionResult, AppMode, ModeManager, HighlightState}; - -#[cfg(feature = "gui")] -pub use canvas::CanvasTheme; - -pub use autocomplete::{SuggestionItem, AutocompleteState}; -pub use dispatcher::ActionDispatcher; -pub use canvas::state::{CanvasState, ActionContext}; // Fixed path - -// Backwards compatibility -pub use suggestions::SuggestionState; diff --git a/canvas/src/suggestions.rs b/canvas/src/suggestions.rs deleted file mode 100644 index 9da86f2..0000000 --- a/canvas/src/suggestions.rs +++ /dev/null @@ -1,67 +0,0 @@ -// canvas/src/suggestions.rs - -/// Generic suggestion system that can be implemented by any CanvasState -#[derive(Debug, Clone)] -pub struct SuggestionState { - pub suggestions: Vec, - pub selected_index: Option, - pub is_active: bool, - pub trigger_chars: Vec, // Characters that trigger suggestions -} - -impl Default for SuggestionState { - fn default() -> Self { - Self { - suggestions: Vec::new(), - selected_index: None, - is_active: false, - trigger_chars: vec![], // No auto-trigger by default - } - } -} - -impl SuggestionState { - pub fn new(trigger_chars: Vec) -> Self { - Self { - trigger_chars, - ..Default::default() - } - } - - pub fn activate_with_suggestions(&mut self, suggestions: Vec) { - self.suggestions = suggestions; - self.is_active = !self.suggestions.is_empty(); - self.selected_index = if self.is_active { Some(0) } else { None }; - } - - pub fn deactivate(&mut self) { - self.suggestions.clear(); - self.selected_index = None; - self.is_active = false; - } - - pub fn select_next(&mut self) { - if !self.suggestions.is_empty() { - let current = self.selected_index.unwrap_or(0); - self.selected_index = Some((current + 1) % self.suggestions.len()); - } - } - - pub fn select_previous(&mut self) { - if !self.suggestions.is_empty() { - let current = self.selected_index.unwrap_or(0); - self.selected_index = Some( - if current == 0 { self.suggestions.len() - 1 } else { current - 1 } - ); - } - } - - pub fn get_selected(&self) -> Option<&String> { - self.selected_index - .and_then(|idx| self.suggestions.get(idx)) - } - - pub fn should_trigger(&self, c: char) -> bool { - self.trigger_chars.contains(&c) - } -} diff --git a/client/src/components/form/form.rs b/client/src/components/form/form.rs index 0f90a66..8a60c06 100644 --- a/client/src/components/form/form.rs +++ b/client/src/components/form/form.rs @@ -1,7 +1,7 @@ // src/components/form/form.rs use crate::components::common::autocomplete; use crate::config::colors::themes::Theme; -use canvas::{CanvasState, render_canvas, HighlightState}; // CHANGED: Import HighlightState from canvas +use canvas::canvas::{CanvasState, render_canvas, HighlightState}; use crate::state::pages::form::FormState; use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Margin, Rect},