autocomplete now working

This commit is contained in:
Priec
2025-07-31 22:25:43 +02:00
parent 828a63c30c
commit c594c35b37
7 changed files with 686 additions and 101 deletions

View File

@@ -1,66 +1,35 @@
// src/autocomplete/actions.rs
use crate::canvas::state::{CanvasState, ActionContext};
use crate::canvas::state::CanvasState;
use crate::autocomplete::state::AutocompleteCanvasState;
use crate::canvas::actions::types::{CanvasAction, ActionResult};
use crate::canvas::actions::execute;
use anyhow::Result;
/// Version for states that implement rich autocomplete
pub async fn execute_canvas_action_with_autocomplete<S: CanvasState + AutocompleteCanvasState>(
/// 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<S: CanvasState + AutocompleteCanvasState>(
action: CanvasAction,
state: &mut S,
_ideal_cursor_column: &mut usize, // Keep for compatibility
_config: Option<&()>, // Remove CanvasConfig, keep for compatibility
) -> Result<ActionResult> {
// Check for autocomplete-specific actions first
match &action {
CanvasAction::InsertChar(_) => {
// Character insertion - execute then potentially trigger autocomplete
let result = execute(action, state).await?;
// Check if we should trigger autocomplete after character insertion
if state.should_trigger_autocomplete() {
state.trigger_autocomplete_suggestions().await;
}
Ok(result)
}
// === AUTOCOMPLETE-SPECIFIC ACTIONS ===
_ => {
// For other actions, clear suggestions and execute
let result = execute(action, state).await?;
// Clear autocomplete on navigation/other actions
match action {
CanvasAction::MoveLeft | CanvasAction::MoveRight |
CanvasAction::MoveUp | CanvasAction::MoveDown |
CanvasAction::NextField | CanvasAction::PrevField => {
state.clear_autocomplete_suggestions();
}
_ => {}
}
Ok(result)
}
}
}
/// Handle autocomplete-specific actions (called from handle_feature_action)
pub async fn handle_autocomplete_action<S: CanvasState + AutocompleteCanvasState>(
action: CanvasAction,
state: &mut S,
_context: &ActionContext,
) -> Result<ActionResult> {
match action {
CanvasAction::TriggerAutocomplete => {
// Manual trigger of autocomplete
state.trigger_autocomplete_suggestions().await;
Ok(ActionResult::success_with_message("Triggered autocomplete"))
if state.supports_autocomplete(state.current_field()) {
state.trigger_autocomplete_suggestions().await;
Ok(ActionResult::success_with_message("Triggered autocomplete"))
} else {
Ok(ActionResult::success_with_message("Autocomplete not supported for this field"))
}
}
CanvasAction::SuggestionUp => {
// Navigate up in suggestions
if state.has_autocomplete_suggestions() {
state.move_suggestion_selection(-1);
Ok(ActionResult::success())
@@ -70,7 +39,6 @@ pub async fn handle_autocomplete_action<S: CanvasState + AutocompleteCanvasState
}
CanvasAction::SuggestionDown => {
// Navigate down in suggestions
if state.has_autocomplete_suggestions() {
state.move_suggestion_selection(1);
Ok(ActionResult::success())
@@ -80,25 +48,123 @@ pub async fn handle_autocomplete_action<S: CanvasState + AutocompleteCanvasState
}
CanvasAction::SelectSuggestion => {
// Accept the selected suggestion
if let Some(suggestion) = state.get_selected_suggestion() {
state.apply_suggestion(&suggestion);
state.clear_autocomplete_suggestions();
Ok(ActionResult::success_with_message("Applied suggestion"))
if let Some(message) = state.apply_selected_suggestion() {
Ok(ActionResult::success_with_message(&message))
} else {
Ok(ActionResult::success_with_message("No suggestion selected"))
Ok(ActionResult::success_with_message("No suggestion to select"))
}
}
CanvasAction::ExitSuggestions => {
// Cancel autocomplete
state.clear_autocomplete_suggestions();
Ok(ActionResult::success_with_message("Cleared suggestions"))
Ok(ActionResult::success_with_message("Closed autocomplete"))
}
// === TEXT INSERTION WITH AUTO-TRIGGER ===
CanvasAction::InsertChar(_) => {
// First, execute the character insertion normally
let result = execute(action, state).await?;
// After successful insertion, check if we should auto-trigger autocomplete
if result.is_success() && state.should_trigger_autocomplete() {
state.trigger_autocomplete_suggestions().await;
}
Ok(result)
}
// === NAVIGATION/EDITING ACTIONS (clear autocomplete first) ===
CanvasAction::MoveLeft | CanvasAction::MoveRight |
CanvasAction::MoveUp | CanvasAction::MoveDown |
CanvasAction::NextField | CanvasAction::PrevField |
CanvasAction::DeleteBackward | CanvasAction::DeleteForward => {
// Clear autocomplete when navigating/editing
if state.is_autocomplete_active() {
state.clear_autocomplete_suggestions();
}
// Execute the action normally
execute(action, state).await
}
// === ALL OTHER ACTIONS (normal execution) ===
_ => {
// Not an autocomplete action
Ok(ActionResult::success_with_message("Not an autocomplete action"))
// For all other actions, just execute normally
execute(action, state).await
}
}
}
/// 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<String> {
/// // Try autocomplete first
/// 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<S: CanvasState + AutocompleteCanvasState>(
action: &CanvasAction,
state: &S,
) -> Option<String> {
match action {
CanvasAction::TriggerAutocomplete => {
if state.supports_autocomplete(state.current_field()) {
if state.is_autocomplete_active() {
Some("Autocomplete already active".to_string())
} else {
None // Let execute_with_autocomplete handle it
}
} else {
Some("Autocomplete not available for this field".to_string())
}
}
CanvasAction::SuggestionUp | CanvasAction::SuggestionDown => {
if state.is_autocomplete_active() {
None // Let execute_with_autocomplete handle navigation
} else {
Some("No autocomplete suggestions to navigate".to_string())
}
}
CanvasAction::SelectSuggestion => {
if state.has_autocomplete_suggestions() {
None // Let execute_with_autocomplete handle selection
} else {
Some("No suggestion to select".to_string())
}
}
CanvasAction::ExitSuggestions => {
if state.is_autocomplete_active() {
None // Let execute_with_autocomplete handle exit
} else {
Some("No autocomplete to close".to_string())
}
}
_ => None // Not an autocomplete action
}
}
/// Legacy compatibility function - kept for backward compatibility
/// This is the old function signature, now it just wraps the new system
#[deprecated(note = "Use execute_with_autocomplete instead")]
pub async fn execute_canvas_action_with_autocomplete<S: CanvasState + AutocompleteCanvasState>(
action: CanvasAction,
state: &mut S,
_ideal_cursor_column: &mut usize, // Ignored - new system manages this internally
_config: Option<&()>, // Ignored - no more config system
) -> Result<ActionResult> {
execute_with_autocomplete(action, state).await
}

View File

@@ -1,4 +1,4 @@
// canvas/src/autocomplete/gui.rs
// src/autocomplete/gui.rs
#[cfg(feature = "gui")]
use ratatui::{
@@ -8,6 +8,7 @@ use ratatui::{
Frame,
};
// Use the correct import from our types module
use crate::autocomplete::types::AutocompleteState;
#[cfg(feature = "gui")]
@@ -18,12 +19,12 @@ use unicode_width::UnicodeWidthStr;
/// Render autocomplete dropdown - call this AFTER rendering canvas
#[cfg(feature = "gui")]
pub fn render_autocomplete_dropdown<T: CanvasTheme>(
pub fn render_autocomplete_dropdown<T: CanvasTheme, D: Clone + Send + 'static>(
f: &mut Frame,
frame_area: Rect,
input_rect: Rect,
theme: &T,
autocomplete_state: &AutocompleteState<impl Clone + Send + 'static>,
autocomplete_state: &AutocompleteState<D>,
) {
if !autocomplete_state.is_active {
return;
@@ -68,12 +69,12 @@ fn render_loading_indicator<T: CanvasTheme>(
/// Show actual suggestions list
#[cfg(feature = "gui")]
fn render_suggestions_dropdown<T: CanvasTheme>(
fn render_suggestions_dropdown<T: CanvasTheme, D: Clone + Send + 'static>(
f: &mut Frame,
frame_area: Rect,
input_rect: Rect,
theme: &T,
autocomplete_state: &AutocompleteState<impl Clone + Send + 'static>,
autocomplete_state: &AutocompleteState<D>,
) {
let display_texts: Vec<&str> = autocomplete_state.suggestions
.iter()

View File

@@ -1,10 +1,22 @@
// src/autocomplete/mod.rs
pub mod types;
pub mod gui;
pub mod state;
pub mod actions;
// Re-export autocomplete types
#[cfg(feature = "gui")]
pub mod gui;
// Re-export the main autocomplete API
pub use types::{SuggestionItem, AutocompleteState};
pub use state::AutocompleteCanvasState;
pub use actions::execute_canvas_action_with_autocomplete;
// Re-export the new action functions
pub use actions::{
execute_with_autocomplete,
handle_autocomplete_feature_action,
};
// Re-export GUI functions if available
#[cfg(feature = "gui")]
pub use gui::render_autocomplete_dropdown;

View File

@@ -1,14 +1,22 @@
// canvas/src/state.rs
// src/autocomplete/state.rs
use crate::canvas::state::CanvasState;
/// 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
/// 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
pub trait AutocompleteCanvasState: CanvasState {
/// Associated type for suggestion data (e.g., Hit, String, CustomType)
type SuggestionData: Clone + Send + 'static;
/// Check if a field supports autocomplete
/// Check if a field supports autocomplete (user decides which fields)
fn supports_autocomplete(&self, _field_index: usize) -> bool {
false // Default: no autocomplete support
}
@@ -23,74 +31,152 @@ pub trait AutocompleteCanvasState: CanvasState {
None // Default: no autocomplete state
}
/// CLIENT API: Activate autocomplete for current field
// === PUBLIC API METHODS (called by library) ===
/// Activate autocomplete for current field (shows loading spinner)
fn activate_autocomplete(&mut self) {
let current_field = self.current_field(); // Get field first
let current_field = self.current_field();
if let Some(state) = self.autocomplete_state_mut() {
state.activate(current_field); // Then use it
state.activate(current_field);
}
}
/// CLIENT API: Deactivate autocomplete
/// Deactivate autocomplete (hides dropdown)
fn deactivate_autocomplete(&mut self) {
if let Some(state) = self.autocomplete_state_mut() {
state.deactivate();
}
}
/// CLIENT API: Set suggestions (called after async fetch completes)
/// Set suggestions (called after your async fetch completes)
fn set_autocomplete_suggestions(&mut self, suggestions: Vec<crate::autocomplete::SuggestionItem<Self::SuggestionData>>) {
if let Some(state) = self.autocomplete_state_mut() {
state.set_suggestions(suggestions);
}
}
/// CLIENT API: Set loading state
/// Set loading state (show/hide spinner)
fn set_autocomplete_loading(&mut self, loading: bool) {
if let Some(state) = self.autocomplete_state_mut() {
state.is_loading = loading;
}
}
/// Check if autocomplete is currently active
// === QUERY METHODS ===
/// Check if autocomplete is currently active/visible
fn is_autocomplete_active(&self) -> bool {
self.autocomplete_state()
.map(|state| state.is_active)
.unwrap_or(false)
}
/// Check if autocomplete is ready for interaction
/// Check if autocomplete has suggestions ready for navigation
fn is_autocomplete_ready(&self) -> bool {
self.autocomplete_state()
.map(|state| state.is_ready())
.unwrap_or(false)
}
/// INTERNAL: Apply selected autocomplete value to current field
fn apply_autocomplete_selection(&mut self) -> Option<String> {
// First, get the selected value and display text (if any)
let selection_info = if let Some(state) = self.autocomplete_state() {
state.get_selected().map(|selected| {
(selected.value_to_store.clone(), selected.display_text.clone())
})
} else {
None
};
/// Check if there are available suggestions
fn has_autocomplete_suggestions(&self) -> bool {
self.autocomplete_state()
.map(|state| !state.suggestions.is_empty())
.unwrap_or(false)
}
// Apply the selection if we have one
if let Some((value, display)) = selection_info {
// Apply the value to current field
*self.get_current_input_mut() = value;
self.set_has_unsaved_changes(true);
// === USER-IMPLEMENTABLE METHODS ===
// Deactivate autocomplete
if let Some(state_mut) = self.autocomplete_state_mut() {
state_mut.deactivate();
/// Check if autocomplete should be triggered automatically (e.g., after typing 2+ chars)
/// Override this to implement your own trigger logic
fn should_trigger_autocomplete(&self) -> bool {
let current_input = self.get_current_input();
let current_field = self.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
///
/// 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;
// 3. Convert to suggestions: let suggestions = results.into_suggestions();
// 4. Set suggestions: self.set_autocomplete_suggestions(suggestions);
}
// === INTERNAL NAVIGATION METHODS (called by library actions) ===
/// Clear autocomplete suggestions and hide dropdown
fn clear_autocomplete_suggestions(&mut self) {
self.deactivate_autocomplete();
}
/// Move selection up/down in suggestions list
fn move_suggestion_selection(&mut self, direction: i32) {
if let Some(state) = self.autocomplete_state_mut() {
if direction > 0 {
state.select_next();
} else {
state.select_previous();
}
}
}
Some(format!("Selected: {}", display))
/// Get currently selected suggestion for display/application
fn get_selected_suggestion(&self) -> Option<crate::autocomplete::SuggestionItem<Self::SuggestionData>> {
self.autocomplete_state()?
.get_selected()
.cloned()
}
/// Apply the selected suggestion to the current field
fn apply_suggestion(&mut self, suggestion: &crate::autocomplete::SuggestionItem<Self::SuggestionData>) {
// 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();
}
/// Apply the currently selected suggestion (convenience method)
fn apply_selected_suggestion(&mut self) -> Option<String> {
if let Some(suggestion) = self.get_selected_suggestion() {
let display_text = suggestion.display_text.clone();
self.apply_suggestion(&suggestion);
Some(format!("Applied: {}", display_text))
} else {
None
}
}
// === LEGACY COMPATIBILITY ===
/// INTERNAL: Apply selected autocomplete value to current field (legacy method)
fn apply_autocomplete_selection(&mut self) -> Option<String> {
self.apply_selected_suggestion()
}
}

View File

@@ -1,4 +1,4 @@
// src/lib.rs - Updated to conditionally include autocomplete
// src/lib.rs
pub mod canvas;
@@ -23,8 +23,9 @@ pub use autocomplete::{
AutocompleteCanvasState,
AutocompleteState,
SuggestionItem,
actions::execute_with_autocomplete,
execute_with_autocomplete,
handle_autocomplete_feature_action,
};
#[cfg(all(feature = "gui", feature = "autocomplete"))]
pub use autocomplete::gui::render_autocomplete_dropdown;
pub use autocomplete::render_autocomplete_dropdown;