autocomplete separate traits, one for autocomplete one for canvas purely
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user