autocomplete separate traits, one for autocomplete one for canvas purely

This commit is contained in:
Priec
2025-07-29 22:31:35 +02:00
parent b4d1572c79
commit 46a85e4b4a
3 changed files with 176 additions and 83 deletions

View File

@@ -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"]

View File

@@ -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<S: CanvasState>(
) -> Result<ActionResult> {
// 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<S: CanvasState>(
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<S: CanvasState>(
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<S: CanvasState + AutocompleteCanvasState>(
action: CanvasAction,
state: &mut S,
ideal_cursor_column: &mut usize,
) -> Result<ActionResult> {
// 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<S: CanvasState>(
action: &str,
@@ -56,10 +83,78 @@ pub async fn execute_edit_action<S: CanvasState>(
Ok(result.message().unwrap_or("").to_string())
}
/// Handle autocomplete-related actions
/// Handle autocomplete actions for basic CanvasState (uses legacy methods)
fn handle_autocomplete_action<S: CanvasState>(
action: &CanvasAction,
state: &mut S,
) -> Result<Option<ActionResult>> {
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<S: CanvasState + AutocompleteCanvasState>(
action: &CanvasAction,
state: &mut S,
) -> Result<Option<ActionResult>> {
match action {
CanvasAction::TriggerAutocomplete => {
@@ -78,7 +173,7 @@ fn handle_autocomplete_action<S: CanvasState>(
return Ok(Some(ActionResult::success()));
}
}
Ok(None) // Not handled - no active autocomplete
Ok(None)
}
CanvasAction::SuggestionUp => {
@@ -88,7 +183,7 @@ fn handle_autocomplete_action<S: CanvasState>(
return Ok(Some(ActionResult::success()));
}
}
Ok(None) // Not handled - no active autocomplete
Ok(None)
}
CanvasAction::SelectSuggestion => {
@@ -99,7 +194,7 @@ fn handle_autocomplete_action<S: CanvasState>(
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<S: CanvasState>(
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<S: CanvasState>(
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<S: CanvasState>(
}
}
// Word movement helper functions (unchanged from previous implementation)
// Word movement helper functions
#[derive(PartialEq)]
enum CharType {
Whitespace,

View File

@@ -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<usize> {
None
}
/// Legacy suggestion index setter (deprecated)
fn set_selected_suggestion_index(&mut self, _index: Option<usize>) {
// Default: no-op
}
/// Legacy activate suggestions (deprecated)
fn activate_suggestions(&mut self, _suggestions: Vec<String>) {
// 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<String> {
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<String> {
// 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<Self::SuggestionData>> {
fn autocomplete_state(&self) -> Option<&crate::autocomplete::AutocompleteState<Self::SuggestionData>> {
None // Default: no autocomplete state
}
/// Get autocomplete state (mutable)
fn autocomplete_state_mut(&mut self) -> Option<&mut AutocompleteState<Self::SuggestionData>> {
fn autocomplete_state_mut(&mut self) -> Option<&mut crate::autocomplete::AutocompleteState<Self::SuggestionData>> {
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<SuggestionItem<Self::SuggestionData>>) {
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);
}
@@ -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<usize> {
self.autocomplete_state()
.and_then(|state| state.selected_index)
}
/// Legacy suggestion index setter (deprecated)
fn set_selected_suggestion_index(&mut self, _index: Option<usize>) {
// Deprecated - canvas manages selection internally
}
/// Legacy activate suggestions (deprecated)
fn activate_suggestions(&mut self, _suggestions: Vec<String>) {
// 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<String> {
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<String> {
// 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
}
}