diff --git a/Cargo.lock b/Cargo.lock index 577a663..2f3692c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -475,6 +475,7 @@ name = "canvas" version = "0.4.2" dependencies = [ "anyhow", + "async-trait", "common", "crossterm", "ratatui", diff --git a/canvas/Cargo.toml b/canvas/Cargo.toml index 4a5bc51..3f1549d 100644 --- a/canvas/Cargo.toml +++ b/canvas/Cargo.toml @@ -22,6 +22,7 @@ thiserror = { workspace = true } tracing = "0.1.41" tracing-subscriber = "0.3.19" +async-trait = { workspace = true, optional = true } [dev-dependencies] tokio-test = "0.4.4" @@ -29,7 +30,7 @@ tokio-test = "0.4.4" [features] default = [] gui = ["ratatui"] -autocomplete = ["tokio"] +autocomplete = ["tokio", "async-trait"] [[example]] name = "autocomplete" diff --git a/canvas/examples/autocomplete.rs b/canvas/examples/autocomplete.rs index 7cc927d..955638f 100644 --- a/canvas/examples/autocomplete.rs +++ b/canvas/examples/autocomplete.rs @@ -24,8 +24,8 @@ use canvas::{ theme::CanvasTheme, }, autocomplete::{ - AutocompleteCanvasState, - AutocompleteState, + AutocompleteCanvasState, + AutocompleteState, SuggestionItem, execute_with_autocomplete, handle_autocomplete_feature_action, @@ -33,6 +33,9 @@ use canvas::{ CanvasAction, }; +// Add the async_trait import +use async_trait::async_trait; + // Simple theme implementation #[derive(Clone)] struct DemoTheme; @@ -64,7 +67,7 @@ struct AutocompleteFormState { mode: AppMode, has_changes: bool, debug_message: String, - + // Autocomplete state autocomplete: AutocompleteState, } @@ -97,14 +100,14 @@ impl AutocompleteFormState { impl CanvasState for AutocompleteFormState { fn current_field(&self) -> usize { self.current_field } fn current_cursor_pos(&self) -> usize { self.cursor_pos } - fn set_current_field(&mut self, index: usize) { + fn set_current_field(&mut self, index: usize) { self.current_field = index.min(self.fields.len().saturating_sub(1)); // Clear autocomplete when changing fields if self.is_autocomplete_active() { self.clear_autocomplete_suggestions(); } } - fn set_current_cursor_pos(&mut self, pos: usize) { + fn set_current_cursor_pos(&mut self, pos: usize) { let max_pos = if self.mode == AppMode::Edit { self.fields[self.current_field].len() } else { @@ -146,6 +149,8 @@ impl CanvasState for AutocompleteFormState { } } +// Add the #[async_trait] attribute to the implementation +#[async_trait] impl AutocompleteCanvasState for AutocompleteFormState { type SuggestionData = EmailSuggestion; @@ -165,9 +170,9 @@ impl AutocompleteCanvasState for AutocompleteFormState { fn should_trigger_autocomplete(&self) -> bool { let current_input = self.get_current_input(); let current_field = self.current_field(); - + // Trigger for email field when we have "@" and at least 1 more character - self.supports_autocomplete(current_field) && + self.supports_autocomplete(current_field) && current_input.contains('@') && current_input.len() > current_input.find('@').unwrap_or(0) + 1 && !self.is_autocomplete_active() @@ -181,7 +186,7 @@ impl AutocompleteCanvasState for AutocompleteFormState { // 2. Get current input for querying let query = self.get_current_input().to_string(); - + // 3. Extract domain part from email let domain_part = if let Some(at_pos) = query.find('@') { query[at_pos + 1..].to_string() @@ -195,19 +200,19 @@ impl AutocompleteCanvasState for AutocompleteFormState { let suggestions = tokio::task::spawn_blocking(move || { // Simulate network delay std::thread::sleep(std::time::Duration::from_millis(200)); - + // Create mock suggestions based on domain input let popular_domains = vec![ ("gmail.com", "Gmail"), - ("yahoo.com", "Yahoo Mail"), + ("yahoo.com", "Yahoo Mail"), ("outlook.com", "Outlook"), ("hotmail.com", "Hotmail"), ("company.com", "Company Email"), ("university.edu", "University"), ]; - + let mut results = Vec::new(); - + for (domain, provider) in popular_domains { if domain.starts_with(&domain_part) || domain_part.is_empty() { let full_email = format!("{}@{}", email_prefix, domain); @@ -221,7 +226,7 @@ impl AutocompleteCanvasState for AutocompleteFormState { )); } } - + results }).await.unwrap_or_default(); @@ -246,7 +251,7 @@ async fn handle_key_press(key: KeyCode, modifiers: KeyModifiers, state: &mut Aut Some(CanvasAction::NextField) // Normal tab } } - + KeyCode::BackTab => { if state.is_autocomplete_active() { Some(CanvasAction::SuggestionUp) @@ -254,7 +259,7 @@ async fn handle_key_press(key: KeyCode, modifiers: KeyModifiers, state: &mut Aut Some(CanvasAction::PrevField) } } - + KeyCode::Enter => { if state.is_autocomplete_active() { Some(CanvasAction::SelectSuggestion) // Apply suggestion @@ -262,7 +267,7 @@ async fn handle_key_press(key: KeyCode, modifiers: KeyModifiers, state: &mut Aut Some(CanvasAction::NextField) } } - + KeyCode::Esc => { if state.is_autocomplete_active() { Some(CanvasAction::ExitSuggestions) // Close autocomplete @@ -280,12 +285,12 @@ async fn handle_key_press(key: KeyCode, modifiers: KeyModifiers, state: &mut Aut KeyCode::End => Some(CanvasAction::MoveLineEnd), KeyCode::Backspace => Some(CanvasAction::DeleteBackward), KeyCode::Delete => Some(CanvasAction::DeleteForward), - + // Character input KeyCode::Char(c) if !modifiers.contains(KeyModifiers::CONTROL) => { Some(CanvasAction::InsertChar(c)) } - + _ => None, }; @@ -371,7 +376,7 @@ fn ui(f: &mut Frame, state: &AutocompleteFormState, theme: &DemoTheme) { }; let status_lines = vec![ - Line::from(Span::raw(format!("Mode: {:?} | Field: {}/{} | Cursor: {}", + Line::from(Span::raw(format!("Mode: {:?} | Field: {}/{} | Cursor: {}", state.mode, state.current_field + 1, state.fields.len(), state.cursor_pos))), Line::from(Span::raw(format!("Autocomplete: {}", autocomplete_status))), Line::from(Span::raw(state.debug_message.clone())), diff --git a/canvas/src/autocomplete/actions.rs b/canvas/src/autocomplete/actions.rs index 223535b..4c86e6c 100644 --- a/canvas/src/autocomplete/actions.rs +++ b/canvas/src/autocomplete/actions.rs @@ -8,18 +8,18 @@ use anyhow::Result; /// Enhanced execute function for states that support autocomplete /// This is the main entry point for autocomplete-aware canvas execution -/// +/// /// Use this instead of canvas::execute() if you want autocomplete behavior: /// ```rust /// execute_with_autocomplete(action, &mut state).await?; /// ``` -pub async fn execute_with_autocomplete( +pub async fn execute_with_autocomplete( action: CanvasAction, state: &mut S, ) -> Result { match &action { // === AUTOCOMPLETE-SPECIFIC ACTIONS === - + CanvasAction::TriggerAutocomplete => { if state.supports_autocomplete(state.current_field()) { state.trigger_autocomplete_suggestions().await; @@ -61,7 +61,7 @@ pub async fn execute_with_autocomplete } // === TEXT INSERTION WITH AUTO-TRIGGER === - + CanvasAction::InsertChar(_) => { // First, execute the character insertion normally let result = execute(action, state).await?; @@ -75,7 +75,7 @@ pub async fn execute_with_autocomplete } // === NAVIGATION/EDITING ACTIONS (clear autocomplete first) === - + CanvasAction::MoveLeft | CanvasAction::MoveRight | CanvasAction::MoveUp | CanvasAction::MoveDown | CanvasAction::NextField | CanvasAction::PrevField | @@ -84,13 +84,13 @@ pub async fn execute_with_autocomplete if state.is_autocomplete_active() { state.clear_autocomplete_suggestions(); } - + // Execute the action normally execute(action, state).await } // === ALL OTHER ACTIONS (normal execution) === - + _ => { // For all other actions, just execute normally execute(action, state).await @@ -99,7 +99,7 @@ pub async fn execute_with_autocomplete } /// Helper function to integrate autocomplete actions with CanvasState.handle_feature_action() -/// +/// /// Use this in your CanvasState implementation like this: /// ```rust /// fn handle_feature_action(&mut self, action: &CanvasAction, context: &ActionContext) -> Option { @@ -107,12 +107,12 @@ pub async fn execute_with_autocomplete /// if let Some(result) = handle_autocomplete_feature_action(action, self) { /// return Some(result); /// } -/// +/// /// // Handle your other custom actions... /// None /// } /// ``` -pub fn handle_autocomplete_feature_action( +pub fn handle_autocomplete_feature_action( action: &CanvasAction, state: &S, ) -> Option { @@ -160,7 +160,7 @@ pub fn handle_autocomplete_feature_action( +pub async fn execute_canvas_action_with_autocomplete( action: CanvasAction, state: &mut S, _ideal_cursor_column: &mut usize, // Ignored - new system manages this internally diff --git a/canvas/src/autocomplete/state.rs b/canvas/src/autocomplete/state.rs index 2cb6b73..46b0905 100644 --- a/canvas/src/autocomplete/state.rs +++ b/canvas/src/autocomplete/state.rs @@ -1,17 +1,19 @@ // src/autocomplete/state.rs use crate::canvas::state::CanvasState; +use async_trait::async_trait; /// OPTIONAL extension trait for states that want rich autocomplete functionality. /// Only implement this if you need the new autocomplete features. -/// +/// /// # User Workflow: /// 1. User presses trigger key (Tab, Ctrl+K, etc.) -/// 2. User's key mapping calls CanvasAction::TriggerAutocomplete +/// 2. User's key mapping calls CanvasAction::TriggerAutocomplete /// 3. Library calls your trigger_autocomplete_suggestions() method /// 4. You implement async fetching logic in that method /// 5. You call set_autocomplete_suggestions() with results /// 6. Library manages UI state and navigation +#[async_trait] pub trait AutocompleteCanvasState: CanvasState { /// Associated type for suggestion data (e.g., Hit, String, CustomType) type SuggestionData: Clone + Send + 'static; @@ -92,34 +94,39 @@ pub trait AutocompleteCanvasState: CanvasState { fn should_trigger_autocomplete(&self) -> bool { let current_input = self.get_current_input(); let current_field = self.current_field(); - - self.supports_autocomplete(current_field) && + + self.supports_autocomplete(current_field) && current_input.len() >= 2 && // Default: trigger after 2 chars !self.is_autocomplete_active() } /// **USER MUST IMPLEMENT**: Trigger autocomplete suggestions (async) /// This is where you implement your API calls, caching, etc. - /// + /// /// # Example Implementation: /// ```rust - /// async fn trigger_autocomplete_suggestions(&mut self) { - /// self.activate_autocomplete(); // Show loading state + /// #[async_trait] + /// impl AutocompleteCanvasState for MyState { + /// type SuggestionData = MyData; /// - /// let query = self.get_current_input().to_string(); - /// let suggestions = my_api.search(&query).await.unwrap_or_default(); - /// - /// self.set_autocomplete_suggestions(suggestions); + /// async fn trigger_autocomplete_suggestions(&mut self) { + /// self.activate_autocomplete(); // Show loading state + /// + /// let query = self.get_current_input().to_string(); + /// let suggestions = my_api.search(&query).await.unwrap_or_default(); + /// + /// self.set_autocomplete_suggestions(suggestions); + /// } /// } /// ``` async fn trigger_autocomplete_suggestions(&mut self) { // Activate autocomplete UI self.activate_autocomplete(); - + // Default: just show loading state // User should override this to do actual async fetching self.set_autocomplete_loading(true); - + // In a real implementation, you'd: // 1. Get current input: let query = self.get_current_input(); // 2. Make API call: let results = api.search(query).await; @@ -157,7 +164,7 @@ pub trait AutocompleteCanvasState: CanvasState { // Apply the value to current field *self.get_current_input_mut() = suggestion.value_to_store.clone(); self.set_has_unsaved_changes(true); - + // Clear autocomplete self.clear_autocomplete_suggestions(); }