diff --git a/canvas/examples/suggestions2.rs b/canvas/examples/suggestions2.rs index 7af120a..6615ece 100644 --- a/canvas/examples/suggestions2.rs +++ b/canvas/examples/suggestions2.rs @@ -217,6 +217,16 @@ impl AutoCursorFormEditor { Ok(result?) } + // === SUGGESTIONS CONTROL WRAPPERS === + + fn open_suggestions(&mut self, field_index: usize) { + self.editor.open_suggestions(field_index); + } + + fn close_suggestions(&mut self) { + self.editor.close_suggestions(); + } + // === MODE TRANSITIONS WITH AUTOMATIC CURSOR MANAGEMENT === fn enter_edit_mode(&mut self) { @@ -541,33 +551,21 @@ async fn handle_key_press( match (mode, key, modifiers) { // === SUGGESTIONS HANDLING === - - // Tab: Trigger or navigate suggestions (_, KeyCode::Tab, _) => { if editor.is_suggestions_active() { + // Cycle through suggestions editor.suggestions_next(); editor.set_debug_message("📍 Next suggestion".to_string()); } else if editor.data_provider().supports_suggestions(editor.current_field()) { - let field_names = ["Fruit", "Job", "Language", "Country", "Color"]; - let field_name = field_names.get(editor.current_field()).unwrap_or(&"Unknown"); - + // Open suggestions explicitly + editor.open_suggestions(editor.current_field()); match editor.trigger_suggestions(suggestions_provider).await { Ok(_) => { - let current_text = editor.current_text(); - if editor.suggestions().is_empty() { - if current_text.is_empty() { - editor.set_debug_message(format!("🔍 No {} suggestions available", field_name.to_lowercase())); - } else { - editor.set_debug_message(format!("🔍 No {} matches for '{}'", field_name.to_lowercase(), current_text)); - } - } else { - if current_text.is_empty() { - editor.set_debug_message(format!("✨ {} {} suggestions!", editor.suggestions().len(), field_name.to_lowercase())); - } else { - editor.set_debug_message(format!("✨ {} {} matches for '{}'!", editor.suggestions().len(), field_name.to_lowercase(), current_text)); - } - } editor.update_inline_completion(); + editor.set_debug_message(format!( + "✨ {} suggestions loaded", + editor.suggestions().len() + )); } Err(e) => { editor.set_debug_message(format!("❌ Suggestion error: {}", e)); @@ -595,6 +593,26 @@ async fn handle_key_press( } } + // Escape: Close suggestions or exit mode + (_, KeyCode::Esc, _) => { + if editor.is_suggestions_active() { + editor.close_suggestions(); + editor.set_debug_message("❌ Suggestions closed".to_string()); + } else { + match mode { + AppMode::Edit => { + editor.exit_edit_mode(); + } + AppMode::Highlight => { + editor.exit_visual_mode(); + } + _ => { + editor.clear_command_buffer(); + } + } + } + } + // === MODE TRANSITIONS WITH AUTOMATIC CURSOR MANAGEMENT === (AppMode::ReadOnly, KeyCode::Char('i'), _) => { editor.enter_edit_mode(); // 🎯 Automatic: cursor becomes bar | @@ -628,22 +646,6 @@ async fn handle_key_press( editor.clear_command_buffer(); } - // Escape: Exit any mode back to normal (and cancel suggestions) - (_, KeyCode::Esc, _) => { - match mode { - AppMode::Edit => { - editor.exit_edit_mode(); // Exit insert mode (suggestions auto-cancelled) - } - AppMode::Highlight => { - editor.exit_visual_mode(); // Exit visual mode - } - _ => { - // Already in normal mode, just clear command buffer - editor.clear_command_buffer(); - } - } - } - // === CURSOR MANAGEMENT DEMONSTRATION === (AppMode::ReadOnly, KeyCode::F(1), _) => { editor.demo_manual_cursor_control()?; @@ -669,37 +671,21 @@ async fn handle_key_press( } (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('j'), _) | (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Down, _) => { + editor.close_suggestions(); // ⬅ close dropdown editor.move_down(); let field_names = ["Fruit", "Job", "Language", "Country", "Color"]; let field_name = field_names.get(editor.current_field()).unwrap_or(&"Field"); editor.set_debug_message(format!("↓ moved to {} field", field_name)); editor.clear_command_buffer(); - - // Auto-show suggestions when entering a suggestion-enabled field with existing text - if editor.data_provider().supports_suggestions(editor.current_field()) { - let current_text = editor.current_text(); - if !current_text.is_empty() { - let _ = editor.trigger_suggestions(suggestions_provider).await; - editor.update_inline_completion(); - } - } } (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('k'), _) | (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Up, _) => { + editor.close_suggestions(); // ⬅ close dropdown editor.move_up(); let field_names = ["Fruit", "Job", "Language", "Country", "Color"]; let field_name = field_names.get(editor.current_field()).unwrap_or(&"Field"); editor.set_debug_message(format!("↑ moved to {} field", field_name)); editor.clear_command_buffer(); - - // Auto-show suggestions when entering a suggestion-enabled field with existing text - if editor.data_provider().supports_suggestions(editor.current_field()) { - let current_text = editor.current_text(); - if !current_text.is_empty() { - let _ = editor.trigger_suggestions(suggestions_provider).await; - editor.update_inline_completion(); - } - } } // Word movement @@ -765,9 +751,11 @@ async fn handle_key_press( editor.move_right(); } (AppMode::Edit, KeyCode::Up, _) => { + editor.close_suggestions(); editor.move_up(); } (AppMode::Edit, KeyCode::Down, _) => { + editor.close_suggestions(); editor.move_down(); } (AppMode::Edit, KeyCode::Home, _) => { diff --git a/canvas/src/canvas/state.rs b/canvas/src/canvas/state.rs index fe2c709..e2f4f3e 100644 --- a/canvas/src/canvas/state.rs +++ b/canvas/src/canvas/state.rs @@ -19,7 +19,7 @@ pub struct EditorState { // Selection state (for vim visual mode) pub(crate) selection: SelectionState, - + // Validation state (only available with validation feature) #[cfg(feature = "validation")] pub(crate) validation: crate::validation::ValidationState, @@ -114,7 +114,7 @@ impl EditorState { pub fn selection_state(&self) -> &SelectionState { &self.selection } - + /// Get validation state (for user's business logic) /// Only available when the 'validation' feature is enabled #[cfg(feature = "validation")] @@ -134,7 +134,12 @@ impl EditorState { } } - pub(crate) fn set_cursor(&mut self, position: usize, max_position: usize, for_edit_mode: bool) { + pub(crate) fn set_cursor( + &mut self, + position: usize, + max_position: usize, + for_edit_mode: bool, + ) { if for_edit_mode { // Edit mode: can go past end for insertion self.cursor_pos = position.min(max_position); @@ -145,6 +150,7 @@ impl EditorState { self.ideal_cursor_column = self.cursor_pos; } + /// Legacy internal activation (still used internally if needed) pub(crate) fn activate_suggestions(&mut self, field_index: usize) { self.suggestions.is_active = true; self.suggestions.is_loading = true; @@ -153,6 +159,7 @@ impl EditorState { self.suggestions.completion_text = None; } + /// Legacy internal deactivation pub(crate) fn deactivate_suggestions(&mut self) { self.suggestions.is_active = false; self.suggestions.is_loading = false; @@ -160,6 +167,24 @@ impl EditorState { self.suggestions.selected_index = None; self.suggestions.completion_text = None; } + + /// Explicitly open suggestions — should only be called on Tab + pub(crate) fn open_suggestions(&mut self, field_index: usize) { + self.suggestions.is_active = true; + self.suggestions.is_loading = true; + self.suggestions.active_field = Some(field_index); + self.suggestions.selected_index = None; + self.suggestions.completion_text = None; + } + + /// Explicitly close suggestions — should be called on Esc or field change + pub(crate) fn close_suggestions(&mut self) { + self.suggestions.is_active = false; + self.suggestions.is_loading = false; + self.suggestions.active_field = None; + self.suggestions.selected_index = None; + self.suggestions.completion_text = None; + } } impl Default for EditorState { diff --git a/canvas/src/editor.rs b/canvas/src/editor.rs index 1e0fa2d..396a587 100644 --- a/canvas/src/editor.rs +++ b/canvas/src/editor.rs @@ -152,6 +152,22 @@ impl FormEditor { &self.ui_state } + /// Mutable access to UI state for internal crate use only. + pub(crate) fn ui_state_mut(&mut self) -> &mut EditorState { + &mut self.ui_state + } + + /// Open the suggestions UI for `field_index` (UI-only; does not fetch). + pub fn open_suggestions(&mut self, field_index: usize) { + self.ui_state.open_suggestions(field_index); + } + + /// Close suggestions UI and clear the current suggestion results. + pub fn close_suggestions(&mut self) { + self.ui_state.close_suggestions(); + self.suggestions.clear(); + } + /// Set external validation state for a field (Feature 5) #[cfg(feature = "validation")] pub fn set_external_validation(