diff --git a/canvas/examples/canvas_gui_demo.rs b/canvas/examples/canvas_gui_demo.rs index 534718f..8010043 100644 --- a/canvas/examples/canvas_gui_demo.rs +++ b/canvas/examples/canvas_gui_demo.rs @@ -1,3 +1,5 @@ +// examples/canvas_gui_demo.rs + use std::io; use crossterm::{ event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers}, @@ -20,9 +22,7 @@ use canvas::{ state::{ActionContext, CanvasState}, theme::CanvasTheme, }, - config::CanvasConfig, - dispatcher::ActionDispatcher, - CanvasAction, + CanvasAction, execute, }; // Simple theme implementation @@ -49,8 +49,6 @@ struct DemoFormState { mode: AppMode, highlight_state: HighlightState, has_changes: bool, - ideal_cursor_column: usize, - last_action: Option, debug_message: String, } @@ -78,9 +76,7 @@ impl DemoFormState { mode: AppMode::ReadOnly, highlight_state: HighlightState::Off, has_changes: false, - ideal_cursor_column: 0, - last_action: None, - debug_message: "Ready".to_string(), + debug_message: "Ready - Use hjkl to move, w for next word, i to edit".to_string(), } } @@ -181,98 +177,125 @@ impl CanvasState for DemoFormState { } } -async fn run_app(terminal: &mut Terminal, mut state: DemoFormState, config: CanvasConfig) -> io::Result<()> { +/// Simple key mapping - users have full control! +async fn handle_key_press(key: KeyCode, modifiers: KeyModifiers, state: &mut DemoFormState) -> bool { + let is_edit_mode = state.mode == AppMode::Edit; + + // Handle quit first + if (key == KeyCode::Char('q') && modifiers.contains(KeyModifiers::CONTROL)) || + (key == KeyCode::Char('c') && modifiers.contains(KeyModifiers::CONTROL)) || + key == KeyCode::F(10) { + return false; // Signal to quit + } + + // Users directly map keys to actions - no configuration needed! + let action = match (state.mode, key, modifiers) { + // === READ-ONLY MODE KEYS === + (AppMode::ReadOnly, KeyCode::Char('h'), _) => Some(CanvasAction::MoveLeft), + (AppMode::ReadOnly, KeyCode::Char('j'), _) => Some(CanvasAction::MoveDown), + (AppMode::ReadOnly, KeyCode::Char('k'), _) => Some(CanvasAction::MoveUp), + (AppMode::ReadOnly, KeyCode::Char('l'), _) => Some(CanvasAction::MoveRight), + (AppMode::ReadOnly, KeyCode::Char('w'), _) => Some(CanvasAction::MoveWordNext), + (AppMode::ReadOnly, KeyCode::Char('b'), _) => Some(CanvasAction::MoveWordPrev), + (AppMode::ReadOnly, KeyCode::Char('e'), _) => Some(CanvasAction::MoveWordEnd), + (AppMode::ReadOnly, KeyCode::Char('0'), _) => Some(CanvasAction::MoveLineStart), + (AppMode::ReadOnly, KeyCode::Char('$'), _) => Some(CanvasAction::MoveLineEnd), + (AppMode::ReadOnly, KeyCode::Tab, _) => Some(CanvasAction::NextField), + (AppMode::ReadOnly, KeyCode::BackTab, _) => Some(CanvasAction::PrevField), + + // === EDIT MODE KEYS === + (AppMode::Edit, KeyCode::Left, _) => Some(CanvasAction::MoveLeft), + (AppMode::Edit, KeyCode::Right, _) => Some(CanvasAction::MoveRight), + (AppMode::Edit, KeyCode::Up, _) => Some(CanvasAction::MoveUp), + (AppMode::Edit, KeyCode::Down, _) => Some(CanvasAction::MoveDown), + (AppMode::Edit, KeyCode::Home, _) => Some(CanvasAction::MoveLineStart), + (AppMode::Edit, KeyCode::End, _) => Some(CanvasAction::MoveLineEnd), + (AppMode::Edit, KeyCode::Backspace, _) => Some(CanvasAction::DeleteBackward), + (AppMode::Edit, KeyCode::Delete, _) => Some(CanvasAction::DeleteForward), + (AppMode::Edit, KeyCode::Tab, _) => Some(CanvasAction::NextField), + (AppMode::Edit, KeyCode::BackTab, _) => Some(CanvasAction::PrevField), + + // Vim-style movement in edit mode (optional) + (AppMode::Edit, KeyCode::Char('h'), m) if m.contains(KeyModifiers::CONTROL) => Some(CanvasAction::MoveLeft), + (AppMode::Edit, KeyCode::Char('l'), m) if m.contains(KeyModifiers::CONTROL) => Some(CanvasAction::MoveRight), + + // Word movement with Ctrl in edit mode + (AppMode::Edit, KeyCode::Left, m) if m.contains(KeyModifiers::CONTROL) => Some(CanvasAction::MoveWordPrev), + (AppMode::Edit, KeyCode::Right, m) if m.contains(KeyModifiers::CONTROL) => Some(CanvasAction::MoveWordNext), + + // === MODE TRANSITIONS === + (AppMode::ReadOnly, KeyCode::Char('i'), _) => Some(CanvasAction::Custom("enter_edit_mode".to_string())), + (AppMode::ReadOnly, KeyCode::Char('a'), _) => { + // 'a' moves to end of line then enters edit mode + if let Ok(_) = execute(CanvasAction::MoveLineEnd, state).await { + Some(CanvasAction::Custom("enter_edit_mode".to_string())) + } else { + None + } + }, + (AppMode::ReadOnly, KeyCode::Char('v'), _) => Some(CanvasAction::Custom("enter_highlight_mode".to_string())), + (_, KeyCode::Esc, _) => Some(CanvasAction::Custom("enter_readonly_mode".to_string())), + + // === CHARACTER INPUT IN EDIT MODE === + (AppMode::Edit, KeyCode::Char(c), m) if !m.contains(KeyModifiers::CONTROL) && !m.contains(KeyModifiers::ALT) => { + Some(CanvasAction::InsertChar(c)) + }, + + // === ARROW KEYS IN READ-ONLY MODE === + (AppMode::ReadOnly, KeyCode::Left, _) => Some(CanvasAction::MoveLeft), + (AppMode::ReadOnly, KeyCode::Right, _) => Some(CanvasAction::MoveRight), + (AppMode::ReadOnly, KeyCode::Up, _) => Some(CanvasAction::MoveUp), + (AppMode::ReadOnly, KeyCode::Down, _) => Some(CanvasAction::MoveDown), + + _ => None, + }; + + // Execute the action if we found one + if let Some(action) = action { + match execute(action.clone(), state).await { + Ok(result) => { + if result.is_success() { + // Mark as changed for editing actions + if is_edit_mode { + match action { + CanvasAction::InsertChar(_) | CanvasAction::DeleteBackward | CanvasAction::DeleteForward => { + state.set_has_unsaved_changes(true); + } + _ => {} + } + } + + if let Some(msg) = result.message() { + state.debug_message = msg.to_string(); + } else { + state.debug_message = format!("Executed: {}", action.description()); + } + } else if let Some(msg) = result.message() { + state.debug_message = format!("Error: {}", msg); + } + } + Err(e) => { + state.debug_message = format!("Error executing action: {}", e); + } + } + } else { + state.debug_message = format!("Unhandled key: {:?} (mode: {:?})", key, state.mode); + } + + true // Continue running +} + +async fn run_app(terminal: &mut Terminal, mut state: DemoFormState) -> io::Result<()> { let theme = DemoTheme; loop { terminal.draw(|f| ui(f, &state, &theme))?; if let Event::Key(key) = event::read()? { - // Handle quit - if (key.code == KeyCode::Char('q') && key.modifiers.contains(KeyModifiers::CONTROL)) || - (key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL)) || - key.code == KeyCode::F(10) { + let should_continue = handle_key_press(key.code, key.modifiers, &mut state).await; + if !should_continue { break; } - - let is_edit_mode = state.mode == AppMode::Edit; - let mut handled = false; - - // First priority: Try to dispatch through config system - let mut ideal_cursor = state.ideal_cursor_column; - - if let Ok(Some(result)) = ActionDispatcher::dispatch_key( - key.code, - key.modifiers, - &mut state, - &mut ideal_cursor, - is_edit_mode, - false, - ).await { - state.ideal_cursor_column = ideal_cursor; - state.debug_message = format!("Config handled: {:?}", key.code); - - // Mark as changed for text modification keys in edit mode - if is_edit_mode { - match key.code { - KeyCode::Char(_) | KeyCode::Backspace | KeyCode::Delete => { - state.set_has_unsaved_changes(true); - } - _ => {} - } - } - handled = true; - } - - // Second priority: Handle character input in edit mode - if !handled && is_edit_mode { - if let KeyCode::Char(c) = key.code { - if !key.modifiers.contains(KeyModifiers::CONTROL) && !key.modifiers.contains(KeyModifiers::ALT) { - let action = CanvasAction::InsertChar(c); - let mut ideal_cursor = state.ideal_cursor_column; - if let Ok(_) = ActionDispatcher::dispatch_with_config( - action, - &mut state, - &mut ideal_cursor, - Some(&config), - ).await { - state.ideal_cursor_column = ideal_cursor; - state.set_has_unsaved_changes(true); - state.debug_message = format!("Inserted char: '{}'", c); - handled = true; - } - } - } - } - - // Third priority: Fallback mode transitions - if !handled { - match (state.mode, key.code) { - (AppMode::ReadOnly, KeyCode::Char('i') | KeyCode::Char('a') | KeyCode::Insert) => { - state.enter_edit_mode(); - if key.code == KeyCode::Char('a') { - state.cursor_pos = state.fields[state.current_field].len(); - } - state.debug_message = format!("Entered edit mode via {:?}", key.code); - handled = true; - } - (AppMode::ReadOnly, KeyCode::Char('v')) => { - state.enter_highlight_mode(); - state.debug_message = "Entered visual mode".to_string(); - handled = true; - } - (_, KeyCode::Esc) => { - state.enter_readonly_mode(); - state.debug_message = "Entered read-only mode".to_string(); - handled = true; - } - _ => {} - } - } - - if !handled { - state.debug_message = format!("Unhandled key: {:?}", key.code); - } } } @@ -313,15 +336,15 @@ fn ui(f: &mut Frame, state: &DemoFormState, theme: &DemoTheme) { format!("-- {} --", mode_text) }; - let position_text = format!("Field: {}/{} | Cursor: {} | Column: {}", + let position_text = format!("Field: {}/{} | Cursor: {} | Actions: {}", state.current_field + 1, state.fields.len(), state.cursor_pos, - state.ideal_cursor_column); + CanvasAction::movement_actions().len() + CanvasAction::editing_actions().len()); let help_text = match state.mode { - AppMode::ReadOnly => "hjkl/arrows: Move | Tab/Shift+Tab: Fields | w/b/e: Words | 0/$: Line | gg/G: File | i/a: Edit | v: Visual | F10: Quit", - AppMode::Edit => "Type to edit | hjkl/arrows: Move | Tab/Enter: Next field | Backspace/Delete: Delete | Home/End: Line | Esc: Normal | F10: Quit", + AppMode::ReadOnly => "hjkl/arrows: Move | Tab/Shift+Tab: Fields | w/b/e: Words | 0/$: Line | i/a: Edit | v: Visual | F10: Quit", + AppMode::Edit => "Type to edit | Arrows/Ctrl+arrows: Move | Tab: Next field | Backspace/Delete: Delete | Home/End: Line | Esc: Normal | F10: Quit", AppMode::Highlight => "hjkl/arrows: Select | w/b/e: Words | 0/$: Line | Esc: Normal | F10: Quit", _ => "Esc: Normal | F10: Quit", }; @@ -339,8 +362,6 @@ fn ui(f: &mut Frame, state: &DemoFormState, theme: &DemoTheme) { #[tokio::main] async fn main() -> Result<(), Box> { - let config = CanvasConfig::load(); - enable_raw_mode()?; let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; @@ -349,7 +370,7 @@ async fn main() -> Result<(), Box> { let state = DemoFormState::new(); - let res = run_app(&mut terminal, state, config).await; + let res = run_app(&mut terminal, state).await; disable_raw_mode()?; execute!( diff --git a/canvas/examples/generate_template.rs b/canvas/examples/generate_template.rs deleted file mode 100644 index 5b585c1..0000000 --- a/canvas/examples/generate_template.rs +++ /dev/null @@ -1,21 +0,0 @@ -// examples/generate_template.rs -use canvas::config::CanvasConfig; -use std::env; - -fn main() { - let args: Vec = env::args().collect(); - - if args.len() > 1 && args[1] == "clean" { - // Generate clean template with 80% active code - let template = CanvasConfig::generate_clean_template(); - println!("{}", template); - } else { - // Generate verbose template with descriptions (default) - let template = CanvasConfig::generate_template(); - println!("{}", template); - } -} - -// Usage: -// cargo run --example generate_template > canvas_config.toml -// cargo run --example generate_template clean > canvas_config_clean.toml diff --git a/canvas/src/autocomplete/actions.rs b/canvas/src/autocomplete/actions.rs index 310a4b6..2467268 100644 --- a/canvas/src/autocomplete/actions.rs +++ b/canvas/src/autocomplete/actions.rs @@ -3,126 +3,102 @@ use crate::canvas::state::{CanvasState, ActionContext}; use crate::autocomplete::state::AutocompleteCanvasState; use crate::canvas::actions::types::{CanvasAction, ActionResult}; -use crate::dispatcher::ActionDispatcher; // NEW: Use dispatcher directly -use crate::config::CanvasConfig; +use crate::canvas::actions::execute; use anyhow::Result; /// Version for states that implement rich autocomplete pub async fn execute_canvas_action_with_autocomplete( action: CanvasAction, state: &mut S, - ideal_cursor_column: &mut usize, - config: Option<&CanvasConfig>, + _ideal_cursor_column: &mut usize, // Keep for compatibility + _config: Option<&()>, // Remove CanvasConfig, keep for compatibility ) -> Result { - // 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) = handle_rich_autocomplete_action(action.clone(), state, &context) { - return Ok(result); - } - - // 2. Handle generic actions using the new dispatcher directly - let result = ActionDispatcher::dispatch_with_config(action.clone(), state, ideal_cursor_column, config).await?; - - // 3. AUTO-TRIGGER LOGIC: Check if we should activate/deactivate autocomplete - if let Some(cfg) = config { - if cfg.should_auto_trigger_autocomplete() { - match action { - CanvasAction::InsertChar(_) => { - let current_field = state.current_field(); - let current_input = state.get_current_input(); - - if state.supports_autocomplete(current_field) - && !state.is_autocomplete_active() - && current_input.len() >= 1 - { - state.activate_autocomplete(); - } - } - - CanvasAction::NextField | CanvasAction::PrevField => { - let current_field = state.current_field(); - - if state.supports_autocomplete(current_field) && !state.is_autocomplete_active() { - state.activate_autocomplete(); - } else if !state.supports_autocomplete(current_field) && state.is_autocomplete_active() { - state.deactivate_autocomplete(); - } - } - - _ => {} // No auto-trigger for other actions + // 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) + } + + _ => { + // 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) } } - - Ok(result) } -/// Handle rich autocomplete actions for AutocompleteCanvasState -fn handle_rich_autocomplete_action( +/// Handle autocomplete-specific actions (called from handle_feature_action) +pub async fn handle_autocomplete_action( action: CanvasAction, state: &mut S, _context: &ActionContext, -) -> Option { - match action { +) -> Result { + match action { CanvasAction::TriggerAutocomplete => { - let current_field = state.current_field(); - if state.supports_autocomplete(current_field) { - state.activate_autocomplete(); - Some(ActionResult::success_with_message("Autocomplete activated")) - } else { - Some(ActionResult::success_with_message("Autocomplete not supported for this field")) - } + // Manual trigger of autocomplete + state.trigger_autocomplete_suggestions().await; + Ok(ActionResult::success_with_message("Triggered autocomplete")) } CanvasAction::SuggestionUp => { - if state.is_autocomplete_ready() { - if let Some(autocomplete_state) = state.autocomplete_state_mut() { - autocomplete_state.select_previous(); - } - Some(ActionResult::success()) + // Navigate up in suggestions + if state.has_autocomplete_suggestions() { + state.move_suggestion_selection(-1); + Ok(ActionResult::success()) } else { - Some(ActionResult::success_with_message("No suggestions available")) + Ok(ActionResult::success_with_message("No suggestions available")) } } CanvasAction::SuggestionDown => { - if state.is_autocomplete_ready() { - if let Some(autocomplete_state) = state.autocomplete_state_mut() { - autocomplete_state.select_next(); - } - Some(ActionResult::success()) + // Navigate down in suggestions + if state.has_autocomplete_suggestions() { + state.move_suggestion_selection(1); + Ok(ActionResult::success()) } else { - Some(ActionResult::success_with_message("No suggestions available")) + Ok(ActionResult::success_with_message("No suggestions available")) } } CanvasAction::SelectSuggestion => { - if state.is_autocomplete_ready() { - if let Some(msg) = state.apply_autocomplete_selection() { - Some(ActionResult::success_with_message(&msg)) - } else { - Some(ActionResult::success_with_message("No suggestion selected")) - } + // 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")) } else { - Some(ActionResult::success_with_message("No suggestions available")) + Ok(ActionResult::success_with_message("No suggestion selected")) } } CanvasAction::ExitSuggestions => { - if state.is_autocomplete_active() { - state.deactivate_autocomplete(); - Some(ActionResult::success_with_message("Exited autocomplete")) - } else { - Some(ActionResult::success()) - } + // Cancel autocomplete + state.clear_autocomplete_suggestions(); + Ok(ActionResult::success_with_message("Cleared suggestions")) } - _ => None, // Not a rich autocomplete action + _ => { + // Not an autocomplete action + Ok(ActionResult::success_with_message("Not an autocomplete action")) + } } } diff --git a/canvas/src/canvas/actions/handlers/dispatcher.rs b/canvas/src/canvas/actions/handlers/dispatcher.rs new file mode 100644 index 0000000..31dc0d3 --- /dev/null +++ b/canvas/src/canvas/actions/handlers/dispatcher.rs @@ -0,0 +1,43 @@ +// src/canvas/actions/handlers/dispatcher.rs + +use crate::canvas::state::{CanvasState, ActionContext}; +use crate::canvas::actions::{CanvasAction, ActionResult}; +use crate::canvas::modes::AppMode; +use anyhow::Result; + +use super::{handle_edit_action, handle_readonly_action, handle_highlight_action}; + +/// Main action dispatcher - routes actions to mode-specific handlers +pub async fn dispatch_action( + action: CanvasAction, + state: &mut S, + ideal_cursor_column: &mut usize, +) -> Result { + // Check if the application wants to handle this action 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)); + } + + // Route to mode-specific handler + match state.current_mode() { + AppMode::Edit => { + handle_edit_action(action, state, ideal_cursor_column).await + } + AppMode::ReadOnly => { + handle_readonly_action(action, state, ideal_cursor_column).await + } + AppMode::Highlight => { + handle_highlight_action(action, state, ideal_cursor_column).await + } + AppMode::General | AppMode::Command => { + Ok(ActionResult::success_with_message("Mode does not handle canvas actions directly")) + } + } +} diff --git a/canvas/src/canvas/actions/handlers/edit.rs b/canvas/src/canvas/actions/handlers/edit.rs index 70f2cb8..b86a0ba 100644 --- a/canvas/src/canvas/actions/handlers/edit.rs +++ b/canvas/src/canvas/actions/handlers/edit.rs @@ -1,37 +1,30 @@ // src/canvas/actions/handlers/edit.rs //! Edit mode action handler -//! +//! //! Handles user input when in edit mode, supporting text entry, deletion, //! and cursor movement with edit-specific behavior (cursor can go past end of text). use crate::canvas::actions::types::{CanvasAction, ActionResult}; -use crate::config::introspection::{ActionHandlerIntrospection, HandlerCapabilities, ActionSpec}; use crate::canvas::actions::movement::*; use crate::canvas::state::CanvasState; -use crate::config::CanvasConfig; use anyhow::Result; /// Edit mode uses cursor-past-end behavior for text insertion const FOR_EDIT_MODE: bool = true; -/// Empty struct that implements edit mode capabilities -pub struct EditHandler; - /// Handle actions in edit mode with edit-specific cursor behavior -/// +/// /// Edit mode allows text modification and uses cursor positioning that can /// go past the end of existing text to facilitate insertion. -/// +/// /// # Arguments /// * `action` - The action to perform /// * `state` - Mutable canvas state /// * `ideal_cursor_column` - Desired column for vertical movement (maintained across line changes) -/// * `config` - Optional configuration for behavior customization pub async fn handle_edit_action( action: CanvasAction, state: &mut S, ideal_cursor_column: &mut usize, - config: Option<&CanvasConfig>, ) -> Result { match action { CanvasAction::InsertChar(c) => { @@ -187,25 +180,17 @@ pub async fn handle_edit_action( Ok(ActionResult::success()) } - // Field navigation with wrapping behavior + // Field navigation with simple wrapping behavior CanvasAction::NextField | CanvasAction::PrevField => { let current_field = state.current_field(); let total_fields = state.fields().len(); let new_field = match action { CanvasAction::NextField => { - if config.map_or(true, |c| c.behavior.wrap_around_fields) { - (current_field + 1) % total_fields // Wrap to first field - } else { - (current_field + 1).min(total_fields - 1) // Stop at last field - } + (current_field + 1) % total_fields // Simple wrap } CanvasAction::PrevField => { - if config.map_or(true, |c| c.behavior.wrap_around_fields) { - if current_field == 0 { total_fields - 1 } else { current_field - 1 } // Wrap to last field - } else { - current_field.saturating_sub(1) // Stop at first field - } + if current_field == 0 { total_fields - 1 } else { current_field - 1 } // Simple wrap } _ => unreachable!(), }; @@ -226,151 +211,3 @@ pub async fn handle_edit_action( } } } - -impl ActionHandlerIntrospection for EditHandler { - /// Report all actions this handler supports with examples and requirements - /// Used for automatic config generation and validation - fn introspect() -> HandlerCapabilities { - let mut actions = Vec::new(); - - // REQUIRED ACTIONS - These must be configured for edit mode to work properly - actions.push(ActionSpec { - name: "move_left".to_string(), - description: "Move cursor one position to the left".to_string(), - examples: vec!["Left".to_string(), "h".to_string()], - is_required: true, - }); - - actions.push(ActionSpec { - name: "move_right".to_string(), - description: "Move cursor one position to the right".to_string(), - examples: vec!["Right".to_string(), "l".to_string()], - is_required: true, - }); - - actions.push(ActionSpec { - name: "move_up".to_string(), - description: "Move to previous field or line".to_string(), - examples: vec!["Up".to_string(), "k".to_string()], - is_required: true, - }); - - actions.push(ActionSpec { - name: "move_down".to_string(), - description: "Move to next field or line".to_string(), - examples: vec!["Down".to_string(), "j".to_string()], - is_required: true, - }); - - actions.push(ActionSpec { - name: "delete_char_backward".to_string(), - description: "Delete character before cursor (Backspace)".to_string(), - examples: vec!["Backspace".to_string()], - is_required: true, - }); - - actions.push(ActionSpec { - name: "next_field".to_string(), - description: "Move to next input field".to_string(), - examples: vec!["Tab".to_string(), "Enter".to_string()], - is_required: true, - }); - - actions.push(ActionSpec { - name: "prev_field".to_string(), - description: "Move to previous input field".to_string(), - examples: vec!["Shift+Tab".to_string()], - is_required: true, - }); - - // OPTIONAL ACTIONS - These enhance functionality but aren't required - actions.push(ActionSpec { - name: "move_word_next".to_string(), - description: "Move cursor to start of next word".to_string(), - examples: vec!["Ctrl+Right".to_string(), "w".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "move_word_prev".to_string(), - description: "Move cursor to start of previous word".to_string(), - examples: vec!["Ctrl+Left".to_string(), "b".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "move_word_end".to_string(), - description: "Move cursor to end of current/next word".to_string(), - examples: vec!["e".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "move_word_end_prev".to_string(), - description: "Move cursor to end of previous word".to_string(), - examples: vec!["ge".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "move_line_start".to_string(), - description: "Move cursor to beginning of line".to_string(), - examples: vec!["Home".to_string(), "0".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "move_line_end".to_string(), - description: "Move cursor to end of line".to_string(), - examples: vec!["End".to_string(), "$".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "move_first_line".to_string(), - description: "Move to first field".to_string(), - examples: vec!["Ctrl+Home".to_string(), "gg".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "move_last_line".to_string(), - description: "Move to last field".to_string(), - examples: vec!["Ctrl+End".to_string(), "G".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "delete_char_forward".to_string(), - description: "Delete character after cursor (Delete key)".to_string(), - examples: vec!["Delete".to_string()], - is_required: false, - }); - - HandlerCapabilities { - mode_name: "edit".to_string(), - actions, - auto_handled: vec![ - "insert_char".to_string(), // Any printable character is inserted automatically - ], - } - } - - fn validate_capabilities() -> Result<(), String> { - // TODO: Could add runtime validation that the handler actually - // implements all the actions it claims to support - - // For now, just validate that we have the essential actions - let caps = Self::introspect(); - let required_count = caps.actions.iter().filter(|a| a.is_required).count(); - - if required_count < 7 { // We expect at least 7 required actions - return Err(format!( - "Edit handler claims only {} required actions, expected at least 7", - required_count - )); - } - - Ok(()) - } -} diff --git a/canvas/src/canvas/actions/handlers/highlight.rs b/canvas/src/canvas/actions/handlers/highlight.rs index 7227443..66b8604 100644 --- a/canvas/src/canvas/actions/handlers/highlight.rs +++ b/canvas/src/canvas/actions/handlers/highlight.rs @@ -1,16 +1,11 @@ // src/canvas/actions/handlers/highlight.rs use crate::canvas::actions::types::{CanvasAction, ActionResult}; -use crate::config::introspection::{ActionHandlerIntrospection, HandlerCapabilities, ActionSpec}; - use crate::canvas::actions::movement::*; use crate::canvas::state::CanvasState; -use crate::config::CanvasConfig; use anyhow::Result; const FOR_EDIT_MODE: bool = false; // Highlight mode uses read-only cursor behavior - -pub struct HighlightHandler; /// Handle actions in highlight/visual mode /// TODO: Implement selection logic and highlight-specific behaviors @@ -18,7 +13,6 @@ pub async fn handle_highlight_action( action: CanvasAction, state: &mut S, ideal_cursor_column: &mut usize, - config: Option<&CanvasConfig>, ) -> Result { match action { // Movement actions work similar to read-only mode but with selection @@ -93,8 +87,8 @@ pub async fn handle_highlight_action( } // Highlight mode doesn't handle editing actions - CanvasAction::InsertChar(_) | - CanvasAction::DeleteBackward | + CanvasAction::InsertChar(_) | + CanvasAction::DeleteBackward | CanvasAction::DeleteForward => { Ok(ActionResult::success_with_message("Action not available in highlight mode")) } @@ -108,98 +102,3 @@ pub async fn handle_highlight_action( } } } - -impl ActionHandlerIntrospection for HighlightHandler { - fn introspect() -> HandlerCapabilities { - let mut actions = Vec::new(); - - // For now, highlight mode uses similar movement to readonly - // but this will be discovered from actual implementation - - // REQUIRED ACTIONS - Basic movement in highlight mode - actions.push(ActionSpec { - name: "move_left".to_string(), - description: "Move cursor left and extend selection".to_string(), - examples: vec!["h".to_string(), "Left".to_string()], - is_required: true, - }); - - actions.push(ActionSpec { - name: "move_right".to_string(), - description: "Move cursor right and extend selection".to_string(), - examples: vec!["l".to_string(), "Right".to_string()], - is_required: true, - }); - - actions.push(ActionSpec { - name: "move_up".to_string(), - description: "Move up and extend selection".to_string(), - examples: vec!["k".to_string(), "Up".to_string()], - is_required: true, - }); - - actions.push(ActionSpec { - name: "move_down".to_string(), - description: "Move down and extend selection".to_string(), - examples: vec!["j".to_string(), "Down".to_string()], - is_required: true, - }); - - // OPTIONAL ACTIONS - Advanced highlight movement - actions.push(ActionSpec { - name: "move_word_next".to_string(), - description: "Move to next word and extend selection".to_string(), - examples: vec!["w".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "move_word_end".to_string(), - description: "Move to word end and extend selection".to_string(), - examples: vec!["e".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "move_word_prev".to_string(), - description: "Move to previous word and extend selection".to_string(), - examples: vec!["b".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "move_line_start".to_string(), - description: "Move to line start and extend selection".to_string(), - examples: vec!["0".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "move_line_end".to_string(), - description: "Move to line end and extend selection".to_string(), - examples: vec!["$".to_string()], - is_required: false, - }); - - HandlerCapabilities { - mode_name: "highlight".to_string(), - actions, - auto_handled: vec![], // Highlight mode has no auto-handled actions - } - } - - fn validate_capabilities() -> Result<(), String> { - let caps = Self::introspect(); - let required_count = caps.actions.iter().filter(|a| a.is_required).count(); - - if required_count < 4 { // We expect at least 4 required actions (basic movement) - return Err(format!( - "Highlight handler claims only {} required actions, expected at least 4", - required_count - )); - } - - Ok(()) - } -} - diff --git a/canvas/src/canvas/actions/handlers/mod.rs b/canvas/src/canvas/actions/handlers/mod.rs index 91c0b75..3bbbe8f 100644 --- a/canvas/src/canvas/actions/handlers/mod.rs +++ b/canvas/src/canvas/actions/handlers/mod.rs @@ -3,8 +3,9 @@ pub mod edit; pub mod readonly; pub mod highlight; +pub mod dispatcher; -// Re-export handler functions pub use edit::handle_edit_action; pub use readonly::handle_readonly_action; pub use highlight::handle_highlight_action; +pub use dispatcher::dispatch_action; diff --git a/canvas/src/canvas/actions/handlers/readonly.rs b/canvas/src/canvas/actions/handlers/readonly.rs index 2c3817a..19201bd 100644 --- a/canvas/src/canvas/actions/handlers/readonly.rs +++ b/canvas/src/canvas/actions/handlers/readonly.rs @@ -1,10 +1,8 @@ // src/canvas/actions/handlers/readonly.rs use crate::canvas::actions::types::{CanvasAction, ActionResult}; -use crate::config::introspection::{ActionHandlerIntrospection, HandlerCapabilities, ActionSpec}; use crate::canvas::actions::movement::*; use crate::canvas::state::CanvasState; -use crate::config::CanvasConfig; use anyhow::Result; const FOR_EDIT_MODE: bool = false; // Read-only mode flag @@ -14,7 +12,6 @@ pub async fn handle_readonly_action( action: CanvasAction, state: &mut S, ideal_cursor_column: &mut usize, - config: Option<&CanvasConfig>, ) -> Result { match action { CanvasAction::MoveLeft => { @@ -37,7 +34,7 @@ pub async fn handle_readonly_action( let current_field = state.current_field(); let new_field = current_field.saturating_sub(1); state.set_current_field(new_field); - + // Apply ideal cursor column with read-only bounds let current_input = state.get_current_input(); let new_pos = safe_cursor_position(current_input, *ideal_cursor_column, FOR_EDIT_MODE); @@ -51,10 +48,10 @@ pub async fn handle_readonly_action( if total_fields == 0 { return Ok(ActionResult::success_with_message("No fields to navigate")); } - + let new_field = (current_field + 1).min(total_fields - 1); state.set_current_field(new_field); - + // Apply ideal cursor column with read-only bounds let current_input = state.get_current_input(); let new_pos = safe_cursor_position(current_input, *ideal_cursor_column, FOR_EDIT_MODE); @@ -67,7 +64,7 @@ pub async fn handle_readonly_action( if total_fields == 0 { return Ok(ActionResult::success_with_message("No fields to navigate")); } - + state.set_current_field(0); let current_input = state.get_current_input(); let new_pos = safe_cursor_position(current_input, *ideal_cursor_column, FOR_EDIT_MODE); @@ -81,7 +78,7 @@ pub async fn handle_readonly_action( if total_fields == 0 { return Ok(ActionResult::success_with_message("No fields to navigate")); } - + let last_field = total_fields - 1; state.set_current_field(last_field); let current_input = state.get_current_input(); @@ -155,18 +152,10 @@ pub async fn handle_readonly_action( let new_field = match action { CanvasAction::NextField => { - if config.map_or(true, |c| c.behavior.wrap_around_fields) { - (current_field + 1) % total_fields - } else { - (current_field + 1).min(total_fields - 1) - } + (current_field + 1) % total_fields // Simple wrap } CanvasAction::PrevField => { - if config.map_or(true, |c| c.behavior.wrap_around_fields) { - if current_field == 0 { total_fields - 1 } else { current_field - 1 } - } else { - current_field.saturating_sub(1) - } + if current_field == 0 { total_fields - 1 } else { current_field - 1 } // Simple wrap } _ => unreachable!(), }; @@ -177,8 +166,8 @@ pub async fn handle_readonly_action( } // Read-only mode doesn't handle editing actions - CanvasAction::InsertChar(_) | - CanvasAction::DeleteBackward | + CanvasAction::InsertChar(_) | + CanvasAction::DeleteBackward | CanvasAction::DeleteForward => { Ok(ActionResult::success_with_message("Action not available in read-only mode")) } @@ -192,131 +181,3 @@ pub async fn handle_readonly_action( } } } - -pub struct ReadOnlyHandler; - -impl ActionHandlerIntrospection for ReadOnlyHandler { - fn introspect() -> HandlerCapabilities { - let mut actions = Vec::new(); - - // REQUIRED ACTIONS - Navigation is essential in read-only mode - actions.push(ActionSpec { - name: "move_left".to_string(), - description: "Move cursor one position to the left".to_string(), - examples: vec!["h".to_string(), "Left".to_string()], - is_required: true, - }); - - actions.push(ActionSpec { - name: "move_right".to_string(), - description: "Move cursor one position to the right".to_string(), - examples: vec!["l".to_string(), "Right".to_string()], - is_required: true, - }); - - actions.push(ActionSpec { - name: "move_up".to_string(), - description: "Move to previous field".to_string(), - examples: vec!["k".to_string(), "Up".to_string()], - is_required: true, - }); - - actions.push(ActionSpec { - name: "move_down".to_string(), - description: "Move to next field".to_string(), - examples: vec!["j".to_string(), "Down".to_string()], - is_required: true, - }); - - // OPTIONAL ACTIONS - Advanced navigation features - actions.push(ActionSpec { - name: "move_word_next".to_string(), - description: "Move cursor to start of next word".to_string(), - examples: vec!["w".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "move_word_prev".to_string(), - description: "Move cursor to start of previous word".to_string(), - examples: vec!["b".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "move_word_end".to_string(), - description: "Move cursor to end of current/next word".to_string(), - examples: vec!["e".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "move_word_end_prev".to_string(), - description: "Move cursor to end of previous word".to_string(), - examples: vec!["ge".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "move_line_start".to_string(), - description: "Move cursor to beginning of line".to_string(), - examples: vec!["0".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "move_line_end".to_string(), - description: "Move cursor to end of line".to_string(), - examples: vec!["$".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "move_first_line".to_string(), - description: "Move to first field".to_string(), - examples: vec!["gg".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "move_last_line".to_string(), - description: "Move to last field".to_string(), - examples: vec!["G".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "next_field".to_string(), - description: "Move to next input field".to_string(), - examples: vec!["Tab".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "prev_field".to_string(), - description: "Move to previous input field".to_string(), - examples: vec!["Shift+Tab".to_string()], - is_required: false, - }); - - HandlerCapabilities { - mode_name: "read_only".to_string(), - actions, - auto_handled: vec![], // Read-only mode has no auto-handled actions - } - } - - fn validate_capabilities() -> Result<(), String> { - let caps = Self::introspect(); - let required_count = caps.actions.iter().filter(|a| a.is_required).count(); - - if required_count < 4 { // We expect at least 4 required actions (basic movement) - return Err(format!( - "ReadOnly handler claims only {} required actions, expected at least 4", - required_count - )); - } - - Ok(()) - } -} diff --git a/canvas/src/canvas/actions/mod.rs b/canvas/src/canvas/actions/mod.rs index 6beba1b..803d179 100644 --- a/canvas/src/canvas/actions/mod.rs +++ b/canvas/src/canvas/actions/mod.rs @@ -1,8 +1,8 @@ // src/canvas/actions/mod.rs pub mod types; -pub mod movement; pub mod handlers; +pub mod movement; -// Re-export the main types -pub use types::{CanvasAction, ActionResult}; +// Re-export the main API +pub use types::{CanvasAction, ActionResult, execute}; diff --git a/canvas/src/canvas/actions/types.rs b/canvas/src/canvas/actions/types.rs index 433a4d5..1a606cb 100644 --- a/canvas/src/canvas/actions/types.rs +++ b/canvas/src/canvas/actions/types.rs @@ -1,108 +1,94 @@ // src/canvas/actions/types.rs +use crate::canvas::state::CanvasState; +use anyhow::Result; + +/// All available canvas actions #[derive(Debug, Clone, PartialEq)] pub enum CanvasAction { - // Character input - InsertChar(char), - - // Deletion - DeleteBackward, - DeleteForward, - - // Basic cursor movement + // Movement actions MoveLeft, MoveRight, MoveUp, MoveDown, - + + // Word movement + MoveWordNext, + MoveWordPrev, + MoveWordEnd, + MoveWordEndPrev, + // Line movement MoveLineStart, MoveLineEnd, - MoveFirstLine, - MoveLastLine, - - // Word movement - MoveWordNext, - MoveWordEnd, - MoveWordPrev, - MoveWordEndPrev, - - // Field navigation + + // Field movement NextField, PrevField, - + MoveFirstLine, + MoveLastLine, + + // Editing actions + InsertChar(char), + DeleteBackward, + DeleteForward, + // Autocomplete actions TriggerAutocomplete, SuggestionUp, SuggestionDown, SelectSuggestion, ExitSuggestions, - + // Custom actions Custom(String), } -impl CanvasAction { - /// Convert string action name to CanvasAction enum (config-driven) - pub fn from_string(action: &str) -> Self { - match action { - "delete_char_backward" => Self::DeleteBackward, - "delete_char_forward" => Self::DeleteForward, - "move_left" => Self::MoveLeft, - "move_right" => Self::MoveRight, - "move_up" => Self::MoveUp, - "move_down" => Self::MoveDown, - "move_line_start" => Self::MoveLineStart, - "move_line_end" => Self::MoveLineEnd, - "move_first_line" => Self::MoveFirstLine, - "move_last_line" => Self::MoveLastLine, - "move_word_next" => Self::MoveWordNext, - "move_word_end" => Self::MoveWordEnd, - "move_word_prev" => Self::MoveWordPrev, - "move_word_end_prev" => Self::MoveWordEndPrev, - "next_field" => Self::NextField, - "prev_field" => Self::PrevField, - "trigger_autocomplete" => Self::TriggerAutocomplete, - "suggestion_up" => Self::SuggestionUp, - "suggestion_down" => Self::SuggestionDown, - "select_suggestion" => Self::SelectSuggestion, - "exit_suggestions" => Self::ExitSuggestions, - _ => Self::Custom(action.to_string()), - } - } -} - -#[derive(Debug, Clone, PartialEq)] +/// Result type for canvas actions +#[derive(Debug, Clone)] pub enum ActionResult { - Success(Option), - HandledByFeature(String), - RequiresContext(String), + Success, + Message(String), + HandledByApp(String), + HandledByFeature(String), // Keep for compatibility Error(String), } impl ActionResult { pub fn success() -> Self { - Self::Success(None) + Self::Success } - + pub fn success_with_message(msg: &str) -> Self { - Self::Success(Some(msg.to_string())) + Self::Message(msg.to_string()) } - + + pub fn handled_by_app(msg: &str) -> Self { + Self::HandledByApp(msg.to_string()) + } + pub fn error(msg: &str) -> Self { - Self::Error(msg.into()) + Self::Error(msg.to_string()) } - + pub fn is_success(&self) -> bool { - matches!(self, Self::Success(_) | Self::HandledByFeature(_)) + matches!(self, Self::Success | Self::Message(_) | Self::HandledByApp(_) | Self::HandledByFeature(_)) } - + pub fn message(&self) -> Option<&str> { match self { - Self::Success(msg) => msg.as_deref(), - Self::HandledByFeature(msg) => Some(msg), - Self::RequiresContext(msg) => Some(msg), - Self::Error(msg) => Some(msg), + Self::Message(msg) | Self::HandledByApp(msg) | Self::HandledByFeature(msg) | Self::Error(msg) => Some(msg), + Self::Success => None, } } } + +/// Execute a canvas action on the given state +pub async fn execute( + action: CanvasAction, + state: &mut S, +) -> Result { + let mut ideal_cursor_column = 0; + + super::handlers::dispatch_action(action, state, &mut ideal_cursor_column).await +} diff --git a/canvas/src/canvas/mod.rs b/canvas/src/canvas/mod.rs index e68fac2..aa72003 100644 --- a/canvas/src/canvas/mod.rs +++ b/canvas/src/canvas/mod.rs @@ -1,18 +1,16 @@ // src/canvas/mod.rs + pub mod actions; pub mod gui; -pub mod modes; +pub mod modes; pub mod state; pub mod theme; -// Re-export commonly used canvas types +// Re-export main types for convenience pub use actions::{CanvasAction, ActionResult}; pub use modes::{AppMode, ModeManager, HighlightState}; pub use state::{CanvasState, ActionContext}; -// Re-export the main entry point -pub use crate::dispatcher::execute_canvas_action; - #[cfg(feature = "gui")] pub use theme::CanvasTheme; diff --git a/canvas/src/config/config.rs b/canvas/src/config/config.rs deleted file mode 100644 index 9db4bc9..0000000 --- a/canvas/src/config/config.rs +++ /dev/null @@ -1,665 +0,0 @@ -// src/config/config.rs -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use crossterm::event::{KeyCode, KeyModifiers}; -use anyhow::{Context, Result}; - -// Import from sibling modules -use super::registry::ActionRegistry; -use super::validation::{ConfigValidator, ValidationError, ValidationResult, ValidationWarning}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CanvasKeybindings { - pub edit: HashMap>, - pub read_only: HashMap>, - pub global: HashMap>, -} - -impl Default for CanvasKeybindings { - fn default() -> Self { - Self { - edit: HashMap::new(), - read_only: HashMap::new(), - global: HashMap::new(), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CanvasBehavior { - pub confirm_on_save: bool, - pub auto_indent: bool, - pub wrap_search: bool, - pub wrap_around_fields: bool, -} - -impl Default for CanvasBehavior { - fn default() -> Self { - Self { - confirm_on_save: true, - auto_indent: true, - wrap_search: true, - wrap_around_fields: true, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CanvasAppearance { - pub line_numbers: bool, - pub syntax_highlighting: bool, - pub current_line_highlight: bool, -} - -impl Default for CanvasAppearance { - fn default() -> Self { - Self { - line_numbers: true, - syntax_highlighting: true, - current_line_highlight: true, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CanvasConfig { - pub keybindings: CanvasKeybindings, - pub behavior: CanvasBehavior, - pub appearance: CanvasAppearance, -} - -impl Default for CanvasConfig { - fn default() -> Self { - Self { - keybindings: CanvasKeybindings::with_vim_defaults(), - behavior: CanvasBehavior::default(), - appearance: CanvasAppearance::default(), - } - } -} - -impl CanvasKeybindings { - /// Generate complete vim defaults from introspection system - /// This ensures defaults are always in sync with actual handler capabilities - pub fn with_vim_defaults() -> Self { - let registry = ActionRegistry::from_handlers(); - Self::generate_from_registry(®istry) - } - - /// Generate keybindings from action registry (used by both defaults and config generation) - /// This is the single source of truth for what keybindings should exist - fn generate_from_registry(registry: &ActionRegistry) -> Self { - let mut keybindings = Self::default(); - - // Generate keybindings for each mode discovered by introspection - for (mode_name, mode_registry) in ®istry.modes { - let mode_bindings = match mode_name.as_str() { - "edit" => &mut keybindings.edit, - "read_only" => &mut keybindings.read_only, - "highlight" => &mut keybindings.global, // Highlight actions go in global - _ => { - // Handle any future modes discovered by introspection - eprintln!("Warning: Unknown mode '{}' discovered by introspection", mode_name); - continue; - } - }; - - // Add ALL required actions - for (action_name, action_spec) in &mode_registry.required { - if !action_spec.examples.is_empty() { - mode_bindings.insert( - action_name.clone(), - action_spec.examples.clone() - ); - } - } - - // Add ALL optional actions - for (action_name, action_spec) in &mode_registry.optional { - if !action_spec.examples.is_empty() { - mode_bindings.insert( - action_name.clone(), - action_spec.examples.clone() - ); - } - } - } - - keybindings - } - - /// Generate a minimal fallback configuration if introspection fails - /// This should rarely be used, but provides safety net - fn minimal_fallback() -> Self { - let mut keybindings = Self::default(); - - // Absolute minimum required for basic functionality - keybindings.read_only.insert("move_left".to_string(), vec!["h".to_string()]); - keybindings.read_only.insert("move_right".to_string(), vec!["l".to_string()]); - keybindings.read_only.insert("move_up".to_string(), vec!["k".to_string()]); - keybindings.read_only.insert("move_down".to_string(), vec!["j".to_string()]); - - keybindings.edit.insert("delete_char_backward".to_string(), vec!["Backspace".to_string()]); - keybindings.edit.insert("move_left".to_string(), vec!["Left".to_string()]); - keybindings.edit.insert("move_right".to_string(), vec!["Right".to_string()]); - keybindings.edit.insert("move_up".to_string(), vec!["Up".to_string()]); - keybindings.edit.insert("move_down".to_string(), vec!["Down".to_string()]); - - keybindings - } - - /// Validate that generated keybindings match the current introspection state - /// This helps catch when handlers change but defaults become stale - pub fn validate_against_introspection(&self) -> Result<(), Vec> { - let registry = ActionRegistry::from_handlers(); - let expected = Self::generate_from_registry(®istry); - let mut errors = Vec::new(); - - // Check each mode - for (mode_name, expected_bindings) in [ - ("edit", &expected.edit), - ("read_only", &expected.read_only), - ("global", &expected.global), - ] { - let actual_bindings = match mode_name { - "edit" => &self.edit, - "read_only" => &self.read_only, - "global" => &self.global, - _ => continue, - }; - - // Check for missing actions - for action_name in expected_bindings.keys() { - if !actual_bindings.contains_key(action_name) { - errors.push(format!( - "Missing action '{}' in {} mode (expected by introspection)", - action_name, mode_name - )); - } - } - - // Check for unexpected actions - for action_name in actual_bindings.keys() { - if !expected_bindings.contains_key(action_name) { - errors.push(format!( - "Unexpected action '{}' in {} mode (not found in introspection)", - action_name, mode_name - )); - } - } - } - - if errors.is_empty() { - Ok(()) - } else { - Err(errors) - } - } -} - -impl CanvasConfig { - /// Enhanced load method with introspection validation - pub fn load() -> Self { - match Self::load_and_validate() { - Ok(config) => config, - Err(e) => { - eprintln!("Failed to load config file: {}", e); - eprintln!("Using auto-generated defaults from introspection"); - Self::default() - } - } - } - - /// Load and validate configuration with enhanced introspection checks - pub fn load_and_validate() -> Result { - // Try to load canvas_config.toml from current directory - let config = if let Ok(config) = Self::from_file(std::path::Path::new("canvas_config.toml")) { - config - } else { - // Use auto-generated defaults if file doesn't exist - eprintln!("Config file not found, using auto-generated defaults"); - Self::default() - }; - - // Validate the configuration against current introspection state - let registry = ActionRegistry::from_handlers(); - - // Validate handlers are working correctly - if let Err(handler_errors) = registry.validate_against_implementation() { - eprintln!("Handler validation warnings:"); - for error in handler_errors { - eprintln!(" - {}", error); - } - } - - // Validate the configuration against the dynamic registry - let validator = ConfigValidator::new(registry); - let validation_result = validator.validate_keybindings(&config.keybindings); - - if !validation_result.is_valid { - eprintln!("Configuration validation failed:"); - validator.print_validation_result(&validation_result); - } else if !validation_result.warnings.is_empty() { - eprintln!("Configuration validation warnings:"); - validator.print_validation_result(&validation_result); - } - - // Optional: Validate that our defaults match introspection - if let Err(sync_errors) = config.keybindings.validate_against_introspection() { - eprintln!("Default keybindings out of sync with introspection:"); - for error in sync_errors { - eprintln!(" - {}", error); - } - } - - Ok(config) - } - - /// Generate a complete configuration template that matches current defaults - /// This ensures the generated config file has the same content as defaults - pub fn generate_complete_template() -> String { - let registry = ActionRegistry::from_handlers(); - let defaults = CanvasKeybindings::generate_from_registry(®istry); - - let mut template = String::new(); - template.push_str("# Canvas Library Configuration\n"); - template.push_str("# Auto-generated from handler introspection\n"); - template.push_str("# This config contains ALL available actions\n\n"); - - // Generate sections for each mode - for (mode_name, bindings) in [ - ("read_only", &defaults.read_only), - ("edit", &defaults.edit), - ("global", &defaults.global), - ] { - if bindings.is_empty() { - continue; - } - - template.push_str(&format!("[keybindings.{}]\n", mode_name)); - - // Get mode registry for categorization - if let Some(mode_registry) = registry.get_mode_registry(mode_name) { - // Required actions first - let mut found_required = false; - for (action_name, keybindings) in bindings { - if mode_registry.required.contains_key(action_name) { - if !found_required { - template.push_str("# Required\n"); - found_required = true; - } - template.push_str(&format!("{} = {:?}\n", action_name, keybindings)); - } - } - - // Optional actions second - let mut found_optional = false; - for (action_name, keybindings) in bindings { - if mode_registry.optional.contains_key(action_name) { - if !found_optional { - template.push_str("# Optional\n"); - found_optional = true; - } - template.push_str(&format!("{} = {:?}\n", action_name, keybindings)); - } - } - } else { - // Fallback: just list all actions - for (action_name, keybindings) in bindings { - template.push_str(&format!("{} = {:?}\n", action_name, keybindings)); - } - } - - template.push('\n'); - } - - template - } - - /// Generate config that only contains actions different from defaults - /// Useful for minimal user configs - pub fn generate_minimal_template() -> String { - let defaults = CanvasKeybindings::with_vim_defaults(); - - let mut template = String::new(); - template.push_str("# Minimal Canvas Configuration\n"); - template.push_str("# Only uncomment and modify the keybindings you want to change\n"); - template.push_str("# All other actions will use their default vim-style keybindings\n\n"); - - for (mode_name, bindings) in [ - ("read_only", &defaults.read_only), - ("edit", &defaults.edit), - ("global", &defaults.global), - ] { - if bindings.is_empty() { - continue; - } - - template.push_str(&format!("# [keybindings.{}]\n", mode_name)); - - for (action_name, keybindings) in bindings { - template.push_str(&format!("# {} = {:?}\n", action_name, keybindings)); - } - - template.push('\n'); - } - - template - } - - /// Generate template from actual handler capabilities (legacy method for compatibility) - pub fn generate_template() -> String { - Self::generate_complete_template() - } - - /// Generate clean template from actual handler capabilities (legacy method for compatibility) - pub fn generate_clean_template() -> String { - let registry = ActionRegistry::from_handlers(); - - // Validate handlers first - if let Err(errors) = registry.validate_against_implementation() { - for error in errors { - eprintln!(" - {}", error); - } - } - - registry.generate_clean_template() - } - - /// Validate current configuration against actual implementation - pub fn validate(&self) -> ValidationResult { - let registry = ActionRegistry::from_handlers(); - let validator = ConfigValidator::new(registry); - validator.validate_keybindings(&self.keybindings) - } - - /// Print validation results for current config - pub fn print_validation(&self) { - let registry = ActionRegistry::from_handlers(); - let validator = ConfigValidator::new(registry); - let result = validator.validate_keybindings(&self.keybindings); - validator.print_validation_result(&result); - } - - /// Load from TOML string - pub fn from_toml(toml_str: &str) -> Result { - toml::from_str(toml_str) - .context("Failed to parse TOML configuration") - } - - /// Load from file - pub fn from_file(path: &std::path::Path) -> Result { - let contents = std::fs::read_to_string(path) - .context("Failed to read config file")?; - Self::from_toml(&contents) - } - - /// Check if autocomplete should auto-trigger (simple logic) - pub fn should_auto_trigger_autocomplete(&self) -> bool { - // If trigger_autocomplete keybinding exists anywhere, use manual mode only - // If no trigger_autocomplete keybinding, use auto-trigger mode - !self.has_trigger_autocomplete_keybinding() - } - - /// Check if user has configured manual trigger keybinding - pub fn has_trigger_autocomplete_keybinding(&self) -> bool { - self.keybindings.edit.contains_key("trigger_autocomplete") || - self.keybindings.read_only.contains_key("trigger_autocomplete") || - self.keybindings.global.contains_key("trigger_autocomplete") - } - - /// Get action for key in read-only mode - pub fn get_read_only_action(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> { - self.get_action_in_mode(&self.keybindings.read_only, key, modifiers) - .or_else(|| self.get_action_in_mode(&self.keybindings.global, key, modifiers)) - } - - /// Get action for key in edit mode - pub fn get_edit_action(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> { - self.get_action_in_mode(&self.keybindings.edit, key, modifiers) - .or_else(|| self.get_action_in_mode(&self.keybindings.global, key, modifiers)) - } - - /// Get action for key (mode-aware) - pub fn get_action_for_key(&self, key: KeyCode, modifiers: KeyModifiers, is_edit_mode: bool, _has_suggestions: bool) -> Option<&str> { - // Check mode-specific - if is_edit_mode { - self.get_edit_action(key, modifiers) - } else { - self.get_read_only_action(key, modifiers) - } - } - - fn get_action_in_mode<'a>(&self, mode_bindings: &'a HashMap>, key: KeyCode, modifiers: KeyModifiers) -> Option<&'a str> { - for (action, bindings) in mode_bindings { - for binding in bindings { - if self.matches_keybinding(binding, key, modifiers) { - return Some(action); - } - } - } - None - } - - fn matches_keybinding(&self, binding: &str, key: KeyCode, modifiers: KeyModifiers) -> bool { - // Special handling for shift+character combinations - if binding.to_lowercase().starts_with("shift+") { - let parts: Vec<&str> = binding.split('+').collect(); - if parts.len() == 2 && parts[1].len() == 1 { - let expected_lowercase = parts[1].chars().next().unwrap().to_lowercase().next().unwrap(); - let expected_uppercase = expected_lowercase.to_uppercase().next().unwrap(); - if let KeyCode::Char(actual_char) = key { - if actual_char == expected_uppercase && modifiers.contains(KeyModifiers::SHIFT) { - return true; - } - } - } - } - - // Handle Shift+Tab -> BackTab - if binding.to_lowercase() == "shift+tab" && key == KeyCode::BackTab && modifiers.is_empty() { - return true; - } - - // Handle multi-character bindings (all standard keys without modifiers) - if binding.len() > 1 && !binding.contains('+') { - return match binding.to_lowercase().as_str() { - // Navigation keys - "left" => key == KeyCode::Left, - "right" => key == KeyCode::Right, - "up" => key == KeyCode::Up, - "down" => key == KeyCode::Down, - "home" => key == KeyCode::Home, - "end" => key == KeyCode::End, - "pageup" | "pgup" => key == KeyCode::PageUp, - "pagedown" | "pgdn" => key == KeyCode::PageDown, - - // Editing keys - "insert" | "ins" => key == KeyCode::Insert, - "delete" | "del" => key == KeyCode::Delete, - "backspace" => key == KeyCode::Backspace, - - // Tab keys - "tab" => key == KeyCode::Tab, - "backtab" => key == KeyCode::BackTab, - - // Special keys - "enter" | "return" => key == KeyCode::Enter, - "escape" | "esc" => key == KeyCode::Esc, - "space" => key == KeyCode::Char(' '), - - // Function keys F1-F24 - "f1" => key == KeyCode::F(1), - "f2" => key == KeyCode::F(2), - "f3" => key == KeyCode::F(3), - "f4" => key == KeyCode::F(4), - "f5" => key == KeyCode::F(5), - "f6" => key == KeyCode::F(6), - "f7" => key == KeyCode::F(7), - "f8" => key == KeyCode::F(8), - "f9" => key == KeyCode::F(9), - "f10" => key == KeyCode::F(10), - "f11" => key == KeyCode::F(11), - "f12" => key == KeyCode::F(12), - "f13" => key == KeyCode::F(13), - "f14" => key == KeyCode::F(14), - "f15" => key == KeyCode::F(15), - "f16" => key == KeyCode::F(16), - "f17" => key == KeyCode::F(17), - "f18" => key == KeyCode::F(18), - "f19" => key == KeyCode::F(19), - "f20" => key == KeyCode::F(20), - "f21" => key == KeyCode::F(21), - "f22" => key == KeyCode::F(22), - "f23" => key == KeyCode::F(23), - "f24" => key == KeyCode::F(24), - - // Lock keys (may not work reliably in all terminals) - "capslock" => key == KeyCode::CapsLock, - "scrolllock" => key == KeyCode::ScrollLock, - "numlock" => key == KeyCode::NumLock, - - // System keys - "printscreen" => key == KeyCode::PrintScreen, - "pause" => key == KeyCode::Pause, - "menu" => key == KeyCode::Menu, - "keypadbegin" => key == KeyCode::KeypadBegin, - - // Media keys (rarely supported but included for completeness) - "mediaplay" => key == KeyCode::Media(crossterm::event::MediaKeyCode::Play), - "mediapause" => key == KeyCode::Media(crossterm::event::MediaKeyCode::Pause), - "mediaplaypause" => key == KeyCode::Media(crossterm::event::MediaKeyCode::PlayPause), - "mediareverse" => key == KeyCode::Media(crossterm::event::MediaKeyCode::Reverse), - "mediastop" => key == KeyCode::Media(crossterm::event::MediaKeyCode::Stop), - "mediafastforward" => key == KeyCode::Media(crossterm::event::MediaKeyCode::FastForward), - "mediarewind" => key == KeyCode::Media(crossterm::event::MediaKeyCode::Rewind), - "mediatracknext" => key == KeyCode::Media(crossterm::event::MediaKeyCode::TrackNext), - "mediatrackprevious" => key == KeyCode::Media(crossterm::event::MediaKeyCode::TrackPrevious), - "mediarecord" => key == KeyCode::Media(crossterm::event::MediaKeyCode::Record), - "medialowervolume" => key == KeyCode::Media(crossterm::event::MediaKeyCode::LowerVolume), - "mediaraisevolume" => key == KeyCode::Media(crossterm::event::MediaKeyCode::RaiseVolume), - "mediamutevolume" => key == KeyCode::Media(crossterm::event::MediaKeyCode::MuteVolume), - - // Modifier keys (these work better as part of combinations) - "leftshift" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::LeftShift), - "leftcontrol" | "leftctrl" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::LeftControl), - "leftalt" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::LeftAlt), - "leftsuper" | "leftwindows" | "leftcmd" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::LeftSuper), - "lefthyper" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::LeftHyper), - "leftmeta" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::LeftMeta), - "rightshift" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::RightShift), - "rightcontrol" | "rightctrl" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::RightControl), - "rightalt" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::RightAlt), - "rightsuper" | "rightwindows" | "rightcmd" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::RightSuper), - "righthyper" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::RightHyper), - "rightmeta" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::RightMeta), - "isolevel3shift" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::IsoLevel3Shift), - "isolevel5shift" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::IsoLevel5Shift), - - // Multi-key sequences need special handling - "gg" => false, // This needs sequence handling - _ => { - // Handle single characters and punctuation - if binding.len() == 1 { - if let Some(c) = binding.chars().next() { - key == KeyCode::Char(c) - } else { - false - } - } else { - false - } - } - }; - } - - // Handle modifier combinations (like "Ctrl+F5", "Alt+Shift+A") - let parts: Vec<&str> = binding.split('+').collect(); - let mut expected_modifiers = KeyModifiers::empty(); - let mut expected_key = None; - - for part in parts { - match part.to_lowercase().as_str() { - // Modifiers - "ctrl" | "control" => expected_modifiers |= KeyModifiers::CONTROL, - "shift" => expected_modifiers |= KeyModifiers::SHIFT, - "alt" => expected_modifiers |= KeyModifiers::ALT, - "super" | "windows" | "cmd" => expected_modifiers |= KeyModifiers::SUPER, - "hyper" => expected_modifiers |= KeyModifiers::HYPER, - "meta" => expected_modifiers |= KeyModifiers::META, - - // Navigation keys - "left" => expected_key = Some(KeyCode::Left), - "right" => expected_key = Some(KeyCode::Right), - "up" => expected_key = Some(KeyCode::Up), - "down" => expected_key = Some(KeyCode::Down), - "home" => expected_key = Some(KeyCode::Home), - "end" => expected_key = Some(KeyCode::End), - "pageup" | "pgup" => expected_key = Some(KeyCode::PageUp), - "pagedown" | "pgdn" => expected_key = Some(KeyCode::PageDown), - - // Editing keys - "insert" | "ins" => expected_key = Some(KeyCode::Insert), - "delete" | "del" => expected_key = Some(KeyCode::Delete), - "backspace" => expected_key = Some(KeyCode::Backspace), - - // Tab keys - "tab" => expected_key = Some(KeyCode::Tab), - "backtab" => expected_key = Some(KeyCode::BackTab), - - // Special keys - "enter" | "return" => expected_key = Some(KeyCode::Enter), - "escape" | "esc" => expected_key = Some(KeyCode::Esc), - "space" => expected_key = Some(KeyCode::Char(' ')), - - // Function keys - "f1" => expected_key = Some(KeyCode::F(1)), - "f2" => expected_key = Some(KeyCode::F(2)), - "f3" => expected_key = Some(KeyCode::F(3)), - "f4" => expected_key = Some(KeyCode::F(4)), - "f5" => expected_key = Some(KeyCode::F(5)), - "f6" => expected_key = Some(KeyCode::F(6)), - "f7" => expected_key = Some(KeyCode::F(7)), - "f8" => expected_key = Some(KeyCode::F(8)), - "f9" => expected_key = Some(KeyCode::F(9)), - "f10" => expected_key = Some(KeyCode::F(10)), - "f11" => expected_key = Some(KeyCode::F(11)), - "f12" => expected_key = Some(KeyCode::F(12)), - "f13" => expected_key = Some(KeyCode::F(13)), - "f14" => expected_key = Some(KeyCode::F(14)), - "f15" => expected_key = Some(KeyCode::F(15)), - "f16" => expected_key = Some(KeyCode::F(16)), - "f17" => expected_key = Some(KeyCode::F(17)), - "f18" => expected_key = Some(KeyCode::F(18)), - "f19" => expected_key = Some(KeyCode::F(19)), - "f20" => expected_key = Some(KeyCode::F(20)), - "f21" => expected_key = Some(KeyCode::F(21)), - "f22" => expected_key = Some(KeyCode::F(22)), - "f23" => expected_key = Some(KeyCode::F(23)), - "f24" => expected_key = Some(KeyCode::F(24)), - - // Lock keys - "capslock" => expected_key = Some(KeyCode::CapsLock), - "scrolllock" => expected_key = Some(KeyCode::ScrollLock), - "numlock" => expected_key = Some(KeyCode::NumLock), - - // System keys - "printscreen" => expected_key = Some(KeyCode::PrintScreen), - "pause" => expected_key = Some(KeyCode::Pause), - "menu" => expected_key = Some(KeyCode::Menu), - "keypadbegin" => expected_key = Some(KeyCode::KeypadBegin), - - // Single character (letters, numbers, punctuation) - part => { - if part.len() == 1 { - if let Some(c) = part.chars().next() { - expected_key = Some(KeyCode::Char(c)); - } - } - } - } - } - - modifiers == expected_modifiers && Some(key) == expected_key - } -} diff --git a/canvas/src/config/introspection.rs b/canvas/src/config/introspection.rs deleted file mode 100644 index 3986b4e..0000000 --- a/canvas/src/config/introspection.rs +++ /dev/null @@ -1,93 +0,0 @@ -// src/config/introspection.rs -//! Handler capability introspection system -//! -//! This module provides traits and utilities for handlers to report their capabilities, -//! enabling automatic configuration generation and validation. - -use std::collections::HashMap; - -/// Specification for a single action that a handler can perform -#[derive(Debug, Clone)] -pub struct ActionSpec { - /// Action name (e.g., "move_left", "delete_char_backward") - pub name: String, - /// Human-readable description of what this action does - pub description: String, - /// Example keybindings for this action (e.g., ["Left", "h"]) - pub examples: Vec, - /// Whether this action is required for the handler to function properly - pub is_required: bool, -} - -/// Complete capability description for a single handler -#[derive(Debug, Clone)] -pub struct HandlerCapabilities { - /// Mode name this handler operates in (e.g., "edit", "read_only") - pub mode_name: String, - /// All actions this handler can perform - pub actions: Vec, - /// Actions handled automatically without configuration (e.g., "insert_char") - pub auto_handled: Vec, -} - -/// Trait that handlers implement to report their capabilities -/// -/// This enables the configuration system to automatically discover what actions -/// are available and validate user configurations against actual implementations. -pub trait ActionHandlerIntrospection { - /// Return complete capability information for this handler - fn introspect() -> HandlerCapabilities; - - /// Validate that this handler actually supports its claimed actions - /// Override this to add custom validation logic - fn validate_capabilities() -> Result<(), String> { - Ok(()) // Default: assume handler is valid - } -} - -/// Discovers capabilities from all registered handlers -pub struct HandlerDiscovery; - -impl HandlerDiscovery { - /// Discover capabilities from all known handlers - /// Add new handlers to this function as they are created - pub fn discover_all() -> HashMap { - let mut capabilities = HashMap::new(); - - // Register all known handlers here - let edit_caps = crate::canvas::actions::handlers::edit::EditHandler::introspect(); - capabilities.insert("edit".to_string(), edit_caps); - - let readonly_caps = crate::canvas::actions::handlers::readonly::ReadOnlyHandler::introspect(); - capabilities.insert("read_only".to_string(), readonly_caps); - - let highlight_caps = crate::canvas::actions::handlers::highlight::HighlightHandler::introspect(); - capabilities.insert("highlight".to_string(), highlight_caps); - - capabilities - } - - /// Validate all handlers support their claimed capabilities - pub fn validate_all_handlers() -> Result<(), Vec> { - let mut errors = Vec::new(); - - // Validate each handler - if let Err(e) = crate::canvas::actions::handlers::edit::EditHandler::validate_capabilities() { - errors.push(format!("Edit handler: {}", e)); - } - - if let Err(e) = crate::canvas::actions::handlers::readonly::ReadOnlyHandler::validate_capabilities() { - errors.push(format!("ReadOnly handler: {}", e)); - } - - if let Err(e) = crate::canvas::actions::handlers::highlight::HighlightHandler::validate_capabilities() { - errors.push(format!("Highlight handler: {}", e)); - } - - if errors.is_empty() { - Ok(()) - } else { - Err(errors) - } - } -} diff --git a/canvas/src/config/mod.rs b/canvas/src/config/mod.rs deleted file mode 100644 index d9afd7a..0000000 --- a/canvas/src/config/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -// src/config/mod.rs - -mod registry; -mod config; -mod validation; -pub mod introspection; - -// Re-export everything from the main config module -pub use registry::*; -pub use validation::*; -pub use config::*; -pub use introspection::*; diff --git a/canvas/src/config/registry.rs b/canvas/src/config/registry.rs deleted file mode 100644 index 7070149..0000000 --- a/canvas/src/config/registry.rs +++ /dev/null @@ -1,135 +0,0 @@ -// src/config/registry.rs - -use std::collections::HashMap; -use crate::config::introspection::{HandlerDiscovery, ActionSpec, HandlerCapabilities}; - -#[derive(Debug, Clone)] -pub struct ModeRegistry { - pub required: HashMap, - pub optional: HashMap, - pub auto_handled: Vec, -} - -#[derive(Debug, Clone)] -pub struct ActionRegistry { - pub modes: HashMap, -} - -impl ActionRegistry { - /// NEW: Create registry by discovering actual handler capabilities - pub fn from_handlers() -> Self { - let handler_capabilities = HandlerDiscovery::discover_all(); - let mut modes = HashMap::new(); - - for (mode_name, capabilities) in handler_capabilities { - let mode_registry = Self::build_mode_registry(capabilities); - modes.insert(mode_name, mode_registry); - } - - Self { modes } - } - - /// Build a mode registry from handler capabilities - fn build_mode_registry(capabilities: HandlerCapabilities) -> ModeRegistry { - let mut required = HashMap::new(); - let mut optional = HashMap::new(); - - for action_spec in capabilities.actions { - if action_spec.is_required { - required.insert(action_spec.name.clone(), action_spec); - } else { - optional.insert(action_spec.name.clone(), action_spec); - } - } - - ModeRegistry { - required, - optional, - auto_handled: capabilities.auto_handled, - } - } - - /// Validate that the registry matches the actual implementation - pub fn validate_against_implementation(&self) -> Result<(), Vec> { - HandlerDiscovery::validate_all_handlers() - } - - pub fn get_mode_registry(&self, mode: &str) -> Option<&ModeRegistry> { - self.modes.get(mode) - } - - pub fn all_known_actions(&self) -> Vec { - let mut actions = Vec::new(); - - for registry in self.modes.values() { - actions.extend(registry.required.keys().cloned()); - actions.extend(registry.optional.keys().cloned()); - } - - actions.sort(); - actions.dedup(); - actions - } - - pub fn generate_config_template(&self) -> String { - let mut template = String::new(); - template.push_str("# Canvas Library Configuration Template\n"); - template.push_str("# Generated automatically from actual handler capabilities\n\n"); - - for (mode_name, registry) in &self.modes { - template.push_str(&format!("[keybindings.{}]\n", mode_name)); - - if !registry.required.is_empty() { - template.push_str("# REQUIRED ACTIONS - These must be configured\n"); - for (name, spec) in ®istry.required { - template.push_str(&format!("# {}\n", spec.description)); - template.push_str(&format!("{} = {:?}\n\n", name, spec.examples)); - } - } - - if !registry.optional.is_empty() { - template.push_str("# OPTIONAL ACTIONS - Configure these if you want them enabled\n"); - for (name, spec) in ®istry.optional { - template.push_str(&format!("# {}\n", spec.description)); - template.push_str(&format!("# {} = {:?}\n\n", name, spec.examples)); - } - } - - if !registry.auto_handled.is_empty() { - template.push_str("# AUTO-HANDLED - These are handled automatically, don't configure:\n"); - for auto_action in ®istry.auto_handled { - template.push_str(&format!("# {} (automatic)\n", auto_action)); - } - template.push('\n'); - } - } - - template - } - - pub fn generate_clean_template(&self) -> String { - let mut template = String::new(); - - for (mode_name, registry) in &self.modes { - template.push_str(&format!("[keybindings.{}]\n", mode_name)); - - if !registry.required.is_empty() { - template.push_str("# Required\n"); - for (name, spec) in ®istry.required { - template.push_str(&format!("{} = {:?}\n", name, spec.examples)); - } - } - - if !registry.optional.is_empty() { - template.push_str("# Optional\n"); - for (name, spec) in ®istry.optional { - template.push_str(&format!("{} = {:?}\n", name, spec.examples)); - } - } - - template.push('\n'); - } - - template - } -} diff --git a/canvas/src/config/validation.rs b/canvas/src/config/validation.rs deleted file mode 100644 index 4673d19..0000000 --- a/canvas/src/config/validation.rs +++ /dev/null @@ -1,278 +0,0 @@ -// src/config/validation.rs - -use std::collections::HashMap; -use thiserror::Error; -use crate::config::registry::{ActionRegistry, ModeRegistry}; -use crate::config::CanvasKeybindings; - -#[derive(Error, Debug)] -pub enum ValidationError { - #[error("Missing required action '{action}' in {mode} mode")] - MissingRequired { - action: String, - mode: String, - suggestion: String, - }, - - #[error("Unknown action '{action}' in {mode} mode")] - UnknownAction { - action: String, - mode: String, - similar: Vec, - }, - - #[error("Multiple validation errors")] - Multiple(Vec), -} - -#[derive(Debug)] -pub struct ValidationWarning { - pub message: String, - pub suggestion: Option, -} - -#[derive(Debug)] -pub struct ValidationResult { - pub errors: Vec, - pub warnings: Vec, - pub is_valid: bool, -} - -impl ValidationResult { - pub fn new() -> Self { - Self { - errors: Vec::new(), - warnings: Vec::new(), - is_valid: true, - } - } - - pub fn add_error(&mut self, error: ValidationError) { - self.errors.push(error); - self.is_valid = false; - } - - pub fn add_warning(&mut self, warning: ValidationWarning) { - self.warnings.push(warning); - } - - pub fn merge(&mut self, other: ValidationResult) { - self.errors.extend(other.errors); - self.warnings.extend(other.warnings); - if !other.is_valid { - self.is_valid = false; - } - } -} - -pub struct ConfigValidator { - registry: ActionRegistry, -} - -impl ConfigValidator { - // FIXED: Accept registry parameter to match config.rs calls - pub fn new(registry: ActionRegistry) -> Self { - Self { - registry, - } - } - - pub fn validate_keybindings(&self, keybindings: &CanvasKeybindings) -> ValidationResult { - let mut result = ValidationResult::new(); - - // Validate each mode that exists in the registry - if let Some(edit_registry) = self.registry.get_mode_registry("edit") { - result.merge(self.validate_mode_bindings( - "edit", - &keybindings.edit, - edit_registry - )); - } - - if let Some(readonly_registry) = self.registry.get_mode_registry("read_only") { - result.merge(self.validate_mode_bindings( - "read_only", - &keybindings.read_only, - readonly_registry - )); - } - - // Skip suggestions mode if not discovered by introspection - // (autocomplete is separate concern as requested) - - // Skip global mode if not discovered by introspection - // (can be added later if needed) - - result - } - - fn validate_mode_bindings( - &self, - mode_name: &str, - bindings: &HashMap>, - registry: &ModeRegistry - ) -> ValidationResult { - let mut result = ValidationResult::new(); - - // Check for missing required actions - for (action_name, spec) in ®istry.required { - if !bindings.contains_key(action_name) { - result.add_error(ValidationError::MissingRequired { - action: action_name.clone(), - mode: mode_name.to_string(), - suggestion: format!( - "Add to config: {} = {:?}", - action_name, - spec.examples - ), - }); - } - } - - // Check for unknown actions - let all_known: std::collections::HashSet<_> = registry.required.keys() - .chain(registry.optional.keys()) - .collect(); - - for action_name in bindings.keys() { - if !all_known.contains(action_name) { - let similar = self.find_similar_actions(action_name, &all_known); - result.add_error(ValidationError::UnknownAction { - action: action_name.clone(), - mode: mode_name.to_string(), - similar, - }); - } - } - - // Check for empty keybinding arrays - for (action_name, key_list) in bindings { - if key_list.is_empty() { - result.add_warning(ValidationWarning { - message: format!( - "Action '{}' in {} mode has empty keybinding list", - action_name, mode_name - ), - suggestion: Some(format!( - "Either add keybindings or remove the action from config" - )), - }); - } - } - - // Warn about auto-handled actions that shouldn't be in config - for auto_action in ®istry.auto_handled { - if bindings.contains_key(auto_action) { - result.add_warning(ValidationWarning { - message: format!( - "Action '{}' in {} mode is auto-handled and shouldn't be in config", - auto_action, mode_name - ), - suggestion: Some(format!( - "Remove '{}' from config - it's handled automatically", - auto_action - )), - }); - } - } - - result - } - - fn find_similar_actions(&self, action: &str, known_actions: &std::collections::HashSet<&String>) -> Vec { - let mut similar = Vec::new(); - - for known in known_actions { - if self.is_similar(action, known) { - similar.push(known.to_string()); - } - } - - similar.sort(); - similar.truncate(3); // Limit to 3 suggestions - similar - } - - fn is_similar(&self, a: &str, b: &str) -> bool { - // Simple similarity check - could be improved with proper edit distance - let a_lower = a.to_lowercase(); - let b_lower = b.to_lowercase(); - - // Check if one contains the other - if a_lower.contains(&b_lower) || b_lower.contains(&a_lower) { - return true; - } - - // Check for common prefixes - let common_prefixes = ["move_", "delete_", "suggestion_"]; - for prefix in &common_prefixes { - if a_lower.starts_with(prefix) && b_lower.starts_with(prefix) { - return true; - } - } - - false - } - - pub fn print_validation_result(&self, result: &ValidationResult) { - if result.is_valid && result.warnings.is_empty() { - println!("✅ Canvas configuration is valid!"); - return; - } - - if !result.errors.is_empty() { - println!("❌ Canvas configuration has errors:"); - for error in &result.errors { - match error { - ValidationError::MissingRequired { action, mode, suggestion } => { - println!(" • Missing required action '{}' in {} mode", action, mode); - println!(" 💡 {}", suggestion); - } - ValidationError::UnknownAction { action, mode, similar } => { - println!(" • Unknown action '{}' in {} mode", action, mode); - if !similar.is_empty() { - println!(" 💡 Did you mean: {}", similar.join(", ")); - } - } - ValidationError::Multiple(_) => { - println!(" • Multiple errors occurred"); - } - } - println!(); - } - } - - if !result.warnings.is_empty() { - println!("⚠️ Canvas configuration has warnings:"); - for warning in &result.warnings { - println!(" • {}", warning.message); - if let Some(suggestion) = &warning.suggestion { - println!(" 💡 {}", suggestion); - } - println!(); - } - } - - if !result.is_valid { - println!("🔧 To generate a config template, use:"); - println!(" CanvasConfig::generate_template()"); - } - } - - pub fn generate_missing_config(&self, keybindings: &CanvasKeybindings) -> String { - let mut config = String::new(); - let validation = self.validate_keybindings(keybindings); - - for error in &validation.errors { - if let ValidationError::MissingRequired { action, mode, suggestion } = error { - if config.is_empty() { - config.push_str(&format!("# Missing required actions for canvas\n\n")); - config.push_str(&format!("[keybindings.{}]\n", mode)); - } - config.push_str(&format!("{}\n", suggestion)); - } - } - - config - } -} diff --git a/canvas/src/dispatcher.rs b/canvas/src/dispatcher.rs deleted file mode 100644 index 09a284d..0000000 --- a/canvas/src/dispatcher.rs +++ /dev/null @@ -1,110 +0,0 @@ -// src/dispatcher.rs - -use crate::canvas::state::{CanvasState, ActionContext}; -use crate::canvas::actions::{CanvasAction, ActionResult}; -use crate::canvas::actions::handlers::{handle_edit_action, handle_readonly_action, handle_highlight_action}; -use crate::canvas::modes::AppMode; -use crate::config::CanvasConfig; -use crossterm::event::{KeyCode, KeyModifiers}; - -/// Main entry point for executing canvas actions -pub async fn execute_canvas_action( - action: CanvasAction, - state: &mut S, - ideal_cursor_column: &mut usize, - config: Option<&CanvasConfig>, -) -> anyhow::Result { - ActionDispatcher::dispatch_with_config(action, state, ideal_cursor_column, config).await -} - -/// High-level action dispatcher that routes actions to mode-specific handlers -pub struct ActionDispatcher; - -impl ActionDispatcher { - /// Dispatch any action to the appropriate mode handler - pub async fn dispatch( - action: CanvasAction, - state: &mut S, - ideal_cursor_column: &mut usize, - ) -> anyhow::Result { - let config = CanvasConfig::load(); - Self::dispatch_with_config(action, state, ideal_cursor_column, Some(&config)).await - } - - /// Dispatch action with provided config - pub async fn dispatch_with_config( - action: CanvasAction, - state: &mut S, - ideal_cursor_column: &mut usize, - config: Option<&CanvasConfig>, - ) -> anyhow::Result { - // Check for feature-specific handling 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)); - } - - // Route to mode-specific handler - match state.current_mode() { - AppMode::Edit => { - handle_edit_action(action, state, ideal_cursor_column, config).await - } - AppMode::ReadOnly => { - handle_readonly_action(action, state, ideal_cursor_column, config).await - } - AppMode::Highlight => { - handle_highlight_action(action, state, ideal_cursor_column, config).await - } - AppMode::General | AppMode::Command => { - // These modes might not handle canvas actions directly - Ok(ActionResult::success_with_message("Mode does not handle canvas actions")) - } - } - } - - /// Quick action dispatch from KeyCode using config - pub async fn dispatch_key( - key: KeyCode, - modifiers: KeyModifiers, - state: &mut S, - ideal_cursor_column: &mut usize, - is_edit_mode: bool, - has_suggestions: bool, - ) -> anyhow::Result> { - let config = CanvasConfig::load(); - - if let Some(action_name) = config.get_action_for_key(key, modifiers, is_edit_mode, has_suggestions) { - let action = CanvasAction::from_string(action_name); - let result = Self::dispatch_with_config(action, state, ideal_cursor_column, Some(&config)).await?; - Ok(Some(result)) - } else { - Ok(None) - } - } - - /// Batch dispatch multiple actions - pub async fn dispatch_batch( - actions: Vec, - state: &mut S, - ideal_cursor_column: &mut usize, - ) -> anyhow::Result> { - let mut results = Vec::new(); - for action in actions { - let result = Self::dispatch(action, state, ideal_cursor_column).await?; - let is_success = result.is_success(); - results.push(result); - - // Stop on first error - if !is_success { - break; - } - } - Ok(results) - } -} diff --git a/canvas/src/lib.rs b/canvas/src/lib.rs index 26c5a40..5cd0143 100644 --- a/canvas/src/lib.rs +++ b/canvas/src/lib.rs @@ -1,11 +1,16 @@ // src/lib.rs + pub mod canvas; -pub mod autocomplete; -pub mod config; -pub mod dispatcher; +// pub mod autocomplete; +pub mod dispatcher; // Keep for compatibility // Re-export the main API for easy access -pub use dispatcher::{execute_canvas_action, ActionDispatcher}; -pub use canvas::actions::{CanvasAction, ActionResult}; +pub use canvas::actions::{CanvasAction, ActionResult, execute}; pub use canvas::state::{CanvasState, ActionContext}; pub use canvas::modes::{AppMode, HighlightState, ModeManager}; + +// Keep legacy exports for compatibility +pub use dispatcher::{execute_canvas_action, ActionDispatcher}; + +// Re-export result type for convenience +pub type Result = anyhow::Result; diff --git a/client/config.toml b/client/config.toml index d370570..ab3ae25 100644 --- a/client/config.toml +++ b/client/config.toml @@ -50,7 +50,7 @@ move_right = ["l", "Right"] move_down = ["j", "Down"] # Optional move_line_end = ["$"] -move_word_next = ["w"] +# move_word_next = ["w"] next_field = ["Tab"] move_word_prev = ["b"] move_word_end = ["e"]