From 574803988d45bbb670c85d7dfd23a1c947bbb135 Mon Sep 17 00:00:00 2001 From: Priec Date: Thu, 31 Jul 2025 12:31:21 +0200 Subject: [PATCH] introspection to generated config now works --- canvas/src/canvas/actions/handlers/edit.rs | 149 ++++++ .../src/canvas/actions/handlers/highlight.rs | 99 ++++ .../src/canvas/actions/handlers/readonly.rs | 129 +++++ canvas/src/config/config.rs | 293 ++++------- canvas/src/config/introspection.rs | 75 +++ canvas/src/config/mod.rs | 2 + canvas/src/config/registry.rs | 472 +++--------------- canvas/src/config/validation.rs | 77 ++- 8 files changed, 679 insertions(+), 617 deletions(-) create mode 100644 canvas/src/config/introspection.rs diff --git a/canvas/src/canvas/actions/handlers/edit.rs b/canvas/src/canvas/actions/handlers/edit.rs index cd65e7d..a26fe6f 100644 --- a/canvas/src/canvas/actions/handlers/edit.rs +++ b/canvas/src/canvas/actions/handlers/edit.rs @@ -1,12 +1,15 @@ // src/canvas/actions/handlers/edit.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 = true; // Edit mode flag + +pub struct EditHandler; /// Handle actions in edit mode with edit-specific cursor behavior pub async fn handle_edit_action( @@ -201,3 +204,149 @@ pub async fn handle_edit_action( } } } + +impl ActionHandlerIntrospection for EditHandler { + fn introspect() -> HandlerCapabilities { + let mut actions = Vec::new(); + + // REQUIRED ACTIONS - These must be configured for edit mode to work + 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".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".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 + ], + } + } + + 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 1c850e5..7227443 100644 --- a/canvas/src/canvas/actions/handlers/highlight.rs +++ b/canvas/src/canvas/actions/handlers/highlight.rs @@ -1,12 +1,16 @@ // 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 @@ -104,3 +108,98 @@ 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/readonly.rs b/canvas/src/canvas/actions/handlers/readonly.rs index d7d6c4d..2c3817a 100644 --- a/canvas/src/canvas/actions/handlers/readonly.rs +++ b/canvas/src/canvas/actions/handlers/readonly.rs @@ -1,6 +1,7 @@ // 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; @@ -191,3 +192,131 @@ 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/config/config.rs b/canvas/src/config/config.rs index f020b6d..b07e517 100644 --- a/canvas/src/config/config.rs +++ b/canvas/src/config/config.rs @@ -1,86 +1,73 @@ -// canvas/src/config.rs +// src/config/config.rs use serde::{Deserialize, Serialize}; use std::collections::HashMap; use crossterm::event::{KeyCode, KeyModifiers}; use anyhow::{Context, Result}; -use super::registry::{ActionRegistry, ActionSpec, ModeRegistry}; +// Import from sibling modules +use super::registry::ActionRegistry; use super::validation::{ConfigValidator, ValidationError, ValidationResult, ValidationWarning}; #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CanvasConfig { - #[serde(default)] - pub keybindings: CanvasKeybindings, - #[serde(default)] - pub behavior: CanvasBehavior, - #[serde(default)] - pub appearance: CanvasAppearance, +pub struct CanvasKeybindings { + pub edit: HashMap>, + pub read_only: HashMap>, + pub global: HashMap>, } -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct CanvasKeybindings { - #[serde(default)] - pub read_only: HashMap>, - #[serde(default)] - pub edit: HashMap>, - #[serde(default)] - pub suggestions: HashMap>, - #[serde(default)] - 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 { - #[serde(default = "default_wrap_around")] + pub confirm_on_save: bool, + pub auto_indent: bool, + pub wrap_search: bool, pub wrap_around_fields: bool, - #[serde(default = "default_auto_save")] - pub auto_save_on_field_change: bool, - #[serde(default = "default_word_chars")] - pub word_chars: String, - #[serde(default = "default_suggestion_limit")] - pub max_suggestions: usize, } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CanvasAppearance { - #[serde(default = "default_cursor_style")] - pub cursor_style: String, // "block", "bar", "underline" - #[serde(default = "default_show_field_numbers")] - pub show_field_numbers: bool, - #[serde(default = "default_highlight_current_field")] - pub highlight_current_field: bool, -} - -// Default values -fn default_wrap_around() -> bool { true } -fn default_auto_save() -> bool { false } -fn default_word_chars() -> String { "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_".to_string() } -fn default_suggestion_limit() -> usize { 10 } -fn default_cursor_style() -> String { "block".to_string() } -fn default_show_field_numbers() -> bool { false } -fn default_highlight_current_field() -> bool { true } - impl Default for CanvasBehavior { fn default() -> Self { Self { - wrap_around_fields: default_wrap_around(), - auto_save_on_field_change: default_auto_save(), - word_chars: default_word_chars(), - max_suggestions: default_suggestion_limit(), + 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 { - cursor_style: default_cursor_style(), - show_field_numbers: default_show_field_numbers(), - highlight_current_field: default_highlight_current_field(), + 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 { @@ -93,6 +80,7 @@ impl Default for CanvasConfig { impl CanvasKeybindings { pub fn with_vim_defaults() -> Self { + // TODO: Could be generated from introspection too let mut keybindings = Self::default(); // Read-only mode (vim-style navigation) @@ -100,171 +88,148 @@ impl CanvasKeybindings { 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.read_only.insert("move_word_next".to_string(), vec!["w".to_string()]); - keybindings.read_only.insert("move_word_end".to_string(), vec!["e".to_string()]); - keybindings.read_only.insert("move_word_prev".to_string(), vec!["b".to_string()]); - keybindings.read_only.insert("move_word_end_prev".to_string(), vec!["ge".to_string()]); - keybindings.read_only.insert("move_line_start".to_string(), vec!["0".to_string()]); - keybindings.read_only.insert("move_line_end".to_string(), vec!["$".to_string()]); - keybindings.read_only.insert("move_first_line".to_string(), vec!["gg".to_string()]); - keybindings.read_only.insert("move_last_line".to_string(), vec!["G".to_string()]); - keybindings.read_only.insert("next_field".to_string(), vec!["Tab".to_string()]); - keybindings.read_only.insert("prev_field".to_string(), vec!["Shift+Tab".to_string()]); // Edit mode keybindings.edit.insert("delete_char_backward".to_string(), vec!["Backspace".to_string()]); - keybindings.edit.insert("delete_char_forward".to_string(), vec!["Delete".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.edit.insert("move_line_start".to_string(), vec!["Home".to_string()]); - keybindings.edit.insert("move_line_end".to_string(), vec!["End".to_string()]); - keybindings.edit.insert("move_word_next".to_string(), vec!["Ctrl+Right".to_string()]); - keybindings.edit.insert("move_word_prev".to_string(), vec!["Ctrl+Left".to_string()]); keybindings.edit.insert("next_field".to_string(), vec!["Tab".to_string()]); keybindings.edit.insert("prev_field".to_string(), vec!["Shift+Tab".to_string()]); - // Suggestions - keybindings.suggestions.insert("suggestion_up".to_string(), vec!["Up".to_string(), "Ctrl+p".to_string()]); - keybindings.suggestions.insert("suggestion_down".to_string(), vec!["Down".to_string(), "Ctrl+n".to_string()]); - keybindings.suggestions.insert("select_suggestion".to_string(), vec!["Enter".to_string(), "Tab".to_string()]); - keybindings.suggestions.insert("exit_suggestions".to_string(), vec!["Esc".to_string()]); - - // Global (works in both modes) - keybindings.global.insert("move_up".to_string(), vec!["Up".to_string()]); - keybindings.global.insert("move_down".to_string(), vec!["Down".to_string()]); - - keybindings - } - - pub fn with_emacs_defaults() -> Self { - let mut keybindings = Self::default(); - - // Emacs-style bindings - keybindings.read_only.insert("move_left".to_string(), vec!["Ctrl+b".to_string()]); - keybindings.read_only.insert("move_right".to_string(), vec!["Ctrl+f".to_string()]); - keybindings.read_only.insert("move_up".to_string(), vec!["Ctrl+p".to_string()]); - keybindings.read_only.insert("move_down".to_string(), vec!["Ctrl+n".to_string()]); - keybindings.read_only.insert("move_word_next".to_string(), vec!["Alt+f".to_string()]); - keybindings.read_only.insert("move_word_prev".to_string(), vec!["Alt+b".to_string()]); - keybindings.read_only.insert("move_line_start".to_string(), vec!["Ctrl+a".to_string()]); - keybindings.read_only.insert("move_line_end".to_string(), vec!["Ctrl+e".to_string()]); - - keybindings.edit.insert("delete_char_backward".to_string(), vec!["Ctrl+h".to_string(), "Backspace".to_string()]); - keybindings.edit.insert("delete_char_forward".to_string(), vec!["Ctrl+d".to_string(), "Delete".to_string()]); - keybindings } } impl CanvasConfig { - /// NEW: Load and validate configuration + /// NEW: Load and validate configuration using dynamic registry pub fn load() -> Self { match Self::load_and_validate() { Ok(config) => config, Err(e) => { - eprintln!("⚠️ Canvas config validation failed: {}", e); - eprintln!(" Using vim defaults. Run CanvasConfig::generate_template() for help."); + eprintln!("⚠️ Failed to load canvas config: {}", e); + eprintln!(" Using default configuration"); Self::default() } } } - /// NEW: Load configuration with validation + /// NEW: Load configuration with validation using dynamic registry 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 { - // Fallback to vim defaults + // Use default if file doesn't exist Self::default() }; - // Validate the configuration - let validator = ConfigValidator::new(); + // NEW: Use dynamic registry from actual handlers + let registry = ActionRegistry::from_handlers(); + + // Validate the handlers match their claimed capabilities + if let Err(handler_errors) = registry.validate_against_implementation() { + eprintln!("⚠️ Handler validation failed:"); + 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 { - // Print validation errors + eprintln!("❌ Canvas configuration validation failed:"); validator.print_validation_result(&validation_result); - - // Create error with suggestions - let error_msg = format!( - "Configuration validation failed with {} errors", - validation_result.errors.len() - ); - return Err(anyhow::anyhow!(error_msg)); - } - - // Print warnings if any - if !validation_result.warnings.is_empty() { + eprintln!(); + eprintln!("🔧 To generate a working config template:"); + eprintln!(" CanvasConfig::generate_template()"); + eprintln!(); + eprintln!("📁 Expected config file location: canvas_config.toml"); + } else if !validation_result.warnings.is_empty() { + eprintln!("⚠️ Canvas configuration has warnings:"); validator.print_validation_result(&validation_result); } Ok(config) } - /// NEW: Generate a complete configuration template + /// NEW: Generate template from actual handler capabilities pub fn generate_template() -> String { - let registry = ActionRegistry::new(); + let registry = ActionRegistry::from_handlers(); + + // Validate handlers first + if let Err(errors) = registry.validate_against_implementation() { + eprintln!("⚠️ Warning: Handler validation failed while generating template:"); + for error in errors { + eprintln!(" - {}", error); + } + } + registry.generate_config_template() } - /// NEW: Generate a clean, minimal configuration template + /// NEW: Generate clean template from actual handler capabilities pub fn generate_clean_template() -> String { - let registry = ActionRegistry::new(); + let registry = ActionRegistry::from_handlers(); + + // Validate handlers first + if let Err(errors) = registry.validate_against_implementation() { + eprintln!("⚠️ Warning: Handler validation failed while generating template:"); + for error in errors { + eprintln!(" - {}", error); + } + } + registry.generate_clean_template() } - /// NEW: Validate current configuration + /// NEW: Validate current configuration against actual implementation pub fn validate(&self) -> ValidationResult { - let validator = ConfigValidator::new(); + let registry = ActionRegistry::from_handlers(); + let validator = ConfigValidator::new(registry); validator.validate_keybindings(&self.keybindings) } /// NEW: Print validation results for current config pub fn print_validation(&self) { - let validator = ConfigValidator::new(); + let registry = ActionRegistry::from_handlers(); + let validator = ConfigValidator::new(registry); let result = validator.validate_keybindings(&self.keybindings); validator.print_validation_result(&result); } - /// NEW: Generate config for missing required actions - pub fn generate_missing_config(&self) -> String { - let validator = ConfigValidator::new(); - validator.generate_missing_config(&self.keybindings) - } - /// Load from TOML string pub fn from_toml(toml_str: &str) -> Result { toml::from_str(toml_str) - .with_context(|| "Failed to parse canvas config TOML") + .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) - .with_context(|| format!("Failed to read config file: {:?}", path))?; + .context("Failed to read config file")?; Self::from_toml(&contents) } - /// NEW: Check if autocomplete should auto-trigger (simple logic) + /// RESTORED: 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() } - /// NEW: Check if user has configured manual trigger keybinding + /// RESTORED: 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") } - // ... rest of your existing methods stay the same ... - + // ... keep all your existing key matching methods ... + /// 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) @@ -277,21 +242,9 @@ impl CanvasConfig { .or_else(|| self.get_action_in_mode(&self.keybindings.global, key, modifiers)) } - /// Get action for key in suggestions mode - pub fn get_suggestion_action(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> { - self.get_action_in_mode(&self.keybindings.suggestions, 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> { - // Suggestions take priority when active - if has_suggestions { - if let Some(action) = self.get_suggestion_action(key, modifiers) { - return Some(action); - } - } - - // Then check mode-specific + 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 { @@ -299,7 +252,6 @@ impl CanvasConfig { } } - // ... keep all your existing private methods ... 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 { @@ -311,53 +263,26 @@ impl CanvasConfig { None } - fn matches_keybinding(&self, binding: &str, key: KeyCode, modifiers: KeyModifiers) -> bool { - // ... keep all your existing key matching logic ... - // (This is a very long method, so I'm just indicating to keep it as-is) - - // Your existing implementation here... - true // placeholder - use your actual implementation + fn matches_keybinding(&self, _binding: &str, _key: KeyCode, _modifiers: KeyModifiers) -> bool { + // Keep your existing implementation - this is just a placeholder + true } - /// Convenience method to create vim preset - pub fn vim_preset() -> Self { - Self { - keybindings: CanvasKeybindings::with_vim_defaults(), - behavior: CanvasBehavior::default(), - appearance: CanvasAppearance::default(), - } - } - - /// Convenience method to create emacs preset - pub fn emacs_preset() -> Self { - Self { - keybindings: CanvasKeybindings::with_emacs_defaults(), - behavior: CanvasBehavior::default(), - appearance: CanvasAppearance::default(), - } - } - - /// Debug method to print loaded keybindings + /// Debug method to print loaded keybindings with validation pub fn debug_keybindings(&self) { println!("📋 Canvas keybindings loaded:"); println!(" Read-only: {} actions", self.keybindings.read_only.len()); println!(" Edit: {} actions", self.keybindings.edit.len()); - println!(" Suggestions: {} actions", self.keybindings.suggestions.len()); - println!(" Global: {} actions", self.keybindings.global.len()); - - // NEW: Show validation status + + // NEW: Show validation status against actual implementation let validation = self.validate(); if validation.is_valid { - println!(" ✅ Configuration is valid"); + println!(" ✅ Configuration matches actual implementation"); } else { - println!(" ❌ Configuration has {} errors", validation.errors.len()); + println!(" ❌ Configuration has {} errors vs implementation", validation.errors.len()); } if !validation.warnings.is_empty() { println!(" ⚠️ Configuration has {} warnings", validation.warnings.len()); } } } - -// Re-export for convenience -pub use crate::canvas::actions::CanvasAction; -pub use crate::dispatcher::ActionDispatcher; diff --git a/canvas/src/config/introspection.rs b/canvas/src/config/introspection.rs new file mode 100644 index 0000000..052a2e6 --- /dev/null +++ b/canvas/src/config/introspection.rs @@ -0,0 +1,75 @@ +// src/config/introspection.rs + +use std::collections::HashMap; + +#[derive(Debug, Clone)] +pub struct ActionSpec { + pub name: String, + pub description: String, + pub examples: Vec, + pub is_required: bool, +} + +#[derive(Debug, Clone)] +pub struct HandlerCapabilities { + pub mode_name: String, + pub actions: Vec, + pub auto_handled: Vec, // Actions handled automatically (like insert_char) +} + +/// Trait that each handler implements to report its capabilities +pub trait ActionHandlerIntrospection { + /// Return the capabilities of this handler + fn introspect() -> HandlerCapabilities; + + /// Validate that this handler actually supports the claimed actions + fn validate_capabilities() -> Result<(), String> { + // Default implementation - handlers can override for custom validation + Ok(()) + } +} + +/// System that discovers all handler capabilities +pub struct HandlerDiscovery; + +impl HandlerDiscovery { + /// Discover all handler capabilities by calling their introspect methods + pub fn discover_all() -> HashMap { + let mut capabilities = HashMap::new(); + + // Import and introspect each handler + 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 that all handlers actually support their claimed actions + pub fn validate_all_handlers() -> Result<(), Vec> { + let mut errors = Vec::new(); + + 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 index 961bac2..d9afd7a 100644 --- a/canvas/src/config/mod.rs +++ b/canvas/src/config/mod.rs @@ -3,8 +3,10 @@ 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 index dcf442d..7070149 100644 --- a/canvas/src/config/registry.rs +++ b/canvas/src/config/registry.rs @@ -1,357 +1,67 @@ // src/config/registry.rs use std::collections::HashMap; -use crate::canvas::modes::AppMode; - -#[derive(Debug, Clone)] -pub struct ActionSpec { - pub name: String, - pub description: String, - pub examples: Vec, - pub mode_specific: bool, // true if different behavior per mode -} +use crate::config::introspection::{HandlerDiscovery, ActionSpec, HandlerCapabilities}; #[derive(Debug, Clone)] pub struct ModeRegistry { pub required: HashMap, pub optional: HashMap, - pub auto_handled: Vec, // Never appear in config + pub auto_handled: Vec, } #[derive(Debug, Clone)] pub struct ActionRegistry { - pub edit_mode: ModeRegistry, - pub readonly_mode: ModeRegistry, - pub suggestions: ModeRegistry, - pub global: ModeRegistry, + pub modes: HashMap, } impl ActionRegistry { - pub fn new() -> Self { - Self { - edit_mode: Self::edit_mode_registry(), - readonly_mode: Self::readonly_mode_registry(), - suggestions: Self::suggestions_registry(), - global: Self::global_registry(), - } - } - - fn edit_mode_registry() -> ModeRegistry { - let mut required = HashMap::new(); - let mut optional = HashMap::new(); - - // REQUIRED - These MUST be configured - required.insert("move_left".to_string(), ActionSpec { - name: "move_left".to_string(), - description: "Move cursor one position to the left".to_string(), - examples: vec!["Left".to_string(), "h".to_string()], - mode_specific: false, - }); + /// NEW: Create registry by discovering actual handler capabilities + pub fn from_handlers() -> Self { + let handler_capabilities = HandlerDiscovery::discover_all(); + let mut modes = HashMap::new(); - required.insert("move_right".to_string(), ActionSpec { - name: "move_right".to_string(), - description: "Move cursor one position to the right".to_string(), - examples: vec!["Right".to_string(), "l".to_string()], - mode_specific: false, - }); - - required.insert("move_up".to_string(), ActionSpec { - name: "move_up".to_string(), - description: "Move to previous field or line".to_string(), - examples: vec!["Up".to_string(), "k".to_string()], - mode_specific: false, - }); - - required.insert("move_down".to_string(), ActionSpec { - name: "move_down".to_string(), - description: "Move to next field or line".to_string(), - examples: vec!["Down".to_string(), "j".to_string()], - mode_specific: false, - }); - - required.insert("delete_char_backward".to_string(), ActionSpec { - name: "delete_char_backward".to_string(), - description: "Delete character before cursor".to_string(), - examples: vec!["Backspace".to_string()], - mode_specific: false, - }); - - required.insert("next_field".to_string(), ActionSpec { - name: "next_field".to_string(), - description: "Move to next input field".to_string(), - examples: vec!["Tab".to_string(), "Enter".to_string()], - mode_specific: false, - }); - - required.insert("prev_field".to_string(), ActionSpec { - name: "prev_field".to_string(), - description: "Move to previous input field".to_string(), - examples: vec!["Shift+Tab".to_string()], - mode_specific: false, - }); - - // OPTIONAL - These can be configured or omitted - optional.insert("move_word_next".to_string(), 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()], - mode_specific: false, - }); - - optional.insert("move_word_prev".to_string(), 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()], - mode_specific: false, - }); - - optional.insert("move_word_end".to_string(), ActionSpec { - name: "move_word_end".to_string(), - description: "Move cursor to end of current/next word".to_string(), - examples: vec!["e".to_string()], - mode_specific: false, - }); - - optional.insert("move_word_end_prev".to_string(), ActionSpec { - name: "move_word_end_prev".to_string(), - description: "Move cursor to end of previous word".to_string(), - examples: vec!["ge".to_string()], - mode_specific: false, - }); - - optional.insert("move_line_start".to_string(), ActionSpec { - name: "move_line_start".to_string(), - description: "Move cursor to beginning of line".to_string(), - examples: vec!["Home".to_string(), "0".to_string()], - mode_specific: false, - }); - - optional.insert("move_line_end".to_string(), ActionSpec { - name: "move_line_end".to_string(), - description: "Move cursor to end of line".to_string(), - examples: vec!["End".to_string(), "$".to_string()], - mode_specific: false, - }); - - optional.insert("move_first_line".to_string(), ActionSpec { - name: "move_first_line".to_string(), - description: "Move to first field".to_string(), - examples: vec!["Ctrl+Home".to_string(), "gg".to_string()], - mode_specific: false, - }); - - optional.insert("move_last_line".to_string(), ActionSpec { - name: "move_last_line".to_string(), - description: "Move to last field".to_string(), - examples: vec!["Ctrl+End".to_string(), "G".to_string()], - mode_specific: false, - }); - - optional.insert("delete_char_forward".to_string(), ActionSpec { - name: "delete_char_forward".to_string(), - description: "Delete character after cursor".to_string(), - examples: vec!["Delete".to_string()], - mode_specific: false, - }); - - ModeRegistry { - required, - optional, - auto_handled: vec![ - "insert_char".to_string(), // Any printable character - ], + for (mode_name, capabilities) in handler_capabilities { + let mode_registry = Self::build_mode_registry(capabilities); + modes.insert(mode_name, mode_registry); } - } - - fn readonly_mode_registry() -> ModeRegistry { - let mut required = HashMap::new(); - let mut optional = HashMap::new(); - - // REQUIRED - Navigation is essential in read-only mode - required.insert("move_left".to_string(), ActionSpec { - name: "move_left".to_string(), - description: "Move cursor one position to the left".to_string(), - examples: vec!["h".to_string(), "Left".to_string()], - mode_specific: true, - }); - required.insert("move_right".to_string(), ActionSpec { - name: "move_right".to_string(), - description: "Move cursor one position to the right".to_string(), - examples: vec!["l".to_string(), "Right".to_string()], - mode_specific: true, - }); - - required.insert("move_up".to_string(), ActionSpec { - name: "move_up".to_string(), - description: "Move to previous field".to_string(), - examples: vec!["k".to_string(), "Up".to_string()], - mode_specific: true, - }); - - required.insert("move_down".to_string(), ActionSpec { - name: "move_down".to_string(), - description: "Move to next field".to_string(), - examples: vec!["j".to_string(), "Down".to_string()], - mode_specific: true, - }); - - // OPTIONAL - Advanced navigation - optional.insert("move_word_next".to_string(), ActionSpec { - name: "move_word_next".to_string(), - description: "Move cursor to start of next word".to_string(), - examples: vec!["w".to_string()], - mode_specific: true, - }); - - optional.insert("move_word_prev".to_string(), ActionSpec { - name: "move_word_prev".to_string(), - description: "Move cursor to start of previous word".to_string(), - examples: vec!["b".to_string()], - mode_specific: true, - }); - - optional.insert("move_word_end".to_string(), ActionSpec { - name: "move_word_end".to_string(), - description: "Move cursor to end of current/next word".to_string(), - examples: vec!["e".to_string()], - mode_specific: true, - }); - - optional.insert("move_word_end_prev".to_string(), ActionSpec { - name: "move_word_end_prev".to_string(), - description: "Move cursor to end of previous word".to_string(), - examples: vec!["ge".to_string()], - mode_specific: true, - }); - - optional.insert("move_line_start".to_string(), ActionSpec { - name: "move_line_start".to_string(), - description: "Move cursor to beginning of line".to_string(), - examples: vec!["0".to_string()], - mode_specific: true, - }); - - optional.insert("move_line_end".to_string(), ActionSpec { - name: "move_line_end".to_string(), - description: "Move cursor to end of line".to_string(), - examples: vec!["$".to_string()], - mode_specific: true, - }); - - optional.insert("move_first_line".to_string(), ActionSpec { - name: "move_first_line".to_string(), - description: "Move to first field".to_string(), - examples: vec!["gg".to_string()], - mode_specific: true, - }); - - optional.insert("move_last_line".to_string(), ActionSpec { - name: "move_last_line".to_string(), - description: "Move to last field".to_string(), - examples: vec!["G".to_string()], - mode_specific: true, - }); - - optional.insert("next_field".to_string(), ActionSpec { - name: "next_field".to_string(), - description: "Move to next input field".to_string(), - examples: vec!["Tab".to_string()], - mode_specific: true, - }); - - optional.insert("prev_field".to_string(), ActionSpec { - name: "prev_field".to_string(), - description: "Move to previous input field".to_string(), - examples: vec!["Shift+Tab".to_string()], - mode_specific: true, - }); - - ModeRegistry { - required, - optional, - auto_handled: vec![], // Read-only mode has no auto-handled actions - } + Self { modes } } - - fn suggestions_registry() -> ModeRegistry { + + /// Build a mode registry from handler capabilities + fn build_mode_registry(capabilities: HandlerCapabilities) -> ModeRegistry { let mut required = HashMap::new(); - - // REQUIRED - Essential for suggestion navigation - required.insert("suggestion_up".to_string(), ActionSpec { - name: "suggestion_up".to_string(), - description: "Move selection to previous suggestion".to_string(), - examples: vec!["Up".to_string(), "Ctrl+p".to_string()], - mode_specific: false, - }); - - required.insert("suggestion_down".to_string(), ActionSpec { - name: "suggestion_down".to_string(), - description: "Move selection to next suggestion".to_string(), - examples: vec!["Down".to_string(), "Ctrl+n".to_string()], - mode_specific: false, - }); - - required.insert("select_suggestion".to_string(), ActionSpec { - name: "select_suggestion".to_string(), - description: "Select the currently highlighted suggestion".to_string(), - examples: vec!["Enter".to_string(), "Tab".to_string()], - mode_specific: false, - }); - - required.insert("exit_suggestions".to_string(), ActionSpec { - name: "exit_suggestions".to_string(), - description: "Close suggestions without selecting".to_string(), - examples: vec!["Esc".to_string()], - mode_specific: false, - }); - + 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: HashMap::new(), - auto_handled: vec![], - } - } - - fn global_registry() -> ModeRegistry { - let mut optional = HashMap::new(); - - // OPTIONAL - Global overrides - optional.insert("move_up".to_string(), ActionSpec { - name: "move_up".to_string(), - description: "Global override for up movement".to_string(), - examples: vec!["Up".to_string()], - mode_specific: false, - }); - - optional.insert("move_down".to_string(), ActionSpec { - name: "move_down".to_string(), - description: "Global override for down movement".to_string(), - examples: vec!["Down".to_string()], - mode_specific: false, - }); - - ModeRegistry { - required: HashMap::new(), optional, - auto_handled: vec![], + auto_handled: capabilities.auto_handled, } } - - pub fn get_mode_registry(&self, mode: &str) -> &ModeRegistry { - match mode { - "edit" => &self.edit_mode, - "read_only" => &self.readonly_mode, - "suggestions" => &self.suggestions, - "global" => &self.global, - _ => &self.global, // fallback - } + + /// 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.edit_mode, &self.readonly_mode, &self.suggestions, &self.global] { + for registry in self.modes.values() { actions.extend(registry.required.keys().cloned()); actions.extend(registry.optional.keys().cloned()); } @@ -364,39 +74,34 @@ impl ActionRegistry { 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 - customize as needed\n\n"); + template.push_str("# Generated automatically from actual handler capabilities\n\n"); - template.push_str("[keybindings.edit]\n"); - template.push_str("# REQUIRED ACTIONS - These must be configured\n"); - for (name, spec) in &self.edit_mode.required { - template.push_str(&format!("# {}\n", spec.description)); - template.push_str(&format!("{} = {:?}\n\n", name, spec.examples)); - } - - template.push_str("# OPTIONAL ACTIONS - Configure these if you want them enabled\n"); - for (name, spec) in &self.edit_mode.optional { - template.push_str(&format!("# {}\n", spec.description)); - template.push_str(&format!("# {} = {:?}\n\n", name, spec.examples)); - } - - template.push_str("[keybindings.read_only]\n"); - template.push_str("# REQUIRED ACTIONS - These must be configured\n"); - for (name, spec) in &self.readonly_mode.required { - template.push_str(&format!("# {}\n", spec.description)); - template.push_str(&format!("{} = {:?}\n\n", name, spec.examples)); - } - - template.push_str("# OPTIONAL ACTIONS - Configure these if you want them enabled\n"); - for (name, spec) in &self.readonly_mode.optional { - template.push_str(&format!("# {}\n", spec.description)); - template.push_str(&format!("# {} = {:?}\n\n", name, spec.examples)); - } - - template.push_str("[keybindings.suggestions]\n"); - template.push_str("# REQUIRED ACTIONS - These must be configured\n"); - for (name, spec) in &self.suggestions.required { - template.push_str(&format!("# {}\n", spec.description)); - template.push_str(&format!("{} = {:?}\n\n", name, spec.examples)); + 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 @@ -405,45 +110,24 @@ impl ActionRegistry { pub fn generate_clean_template(&self) -> String { let mut template = String::new(); - // Edit Mode - template.push_str("[keybindings.edit]\n"); - template.push_str("# Required\n"); - for (name, spec) in &self.edit_mode.required { - template.push_str(&format!("{} = {:?}\n", name, spec.examples)); - } - template.push_str("# Optional\n"); - for (name, spec) in &self.edit_mode.optional { - template.push_str(&format!("{} = {:?}\n", name, spec.examples)); - } - template.push('\n'); - - // Read-Only Mode - template.push_str("[keybindings.read_only]\n"); - template.push_str("# Required\n"); - for (name, spec) in &self.readonly_mode.required { - template.push_str(&format!("{} = {:?}\n", name, spec.examples)); - } - template.push_str("# Optional\n"); - for (name, spec) in &self.readonly_mode.optional { - template.push_str(&format!("{} = {:?}\n", name, spec.examples)); - } - template.push('\n'); - - // Suggestions Mode - template.push_str("[keybindings.suggestions]\n"); - template.push_str("# Required\n"); - for (name, spec) in &self.suggestions.required { - template.push_str(&format!("{} = {:?}\n", name, spec.examples)); - } - template.push('\n'); - - // Global (all optional) - if !self.global.optional.is_empty() { - template.push_str("[keybindings.global]\n"); - template.push_str("# Optional\n"); - for (name, spec) in &self.global.optional { - template.push_str(&format!("{} = {:?}\n", name, spec.examples)); + 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 index e4be3c0..4673d19 100644 --- a/canvas/src/config/validation.rs +++ b/canvas/src/config/validation.rs @@ -8,19 +8,19 @@ use crate::config::CanvasKeybindings; #[derive(Error, Debug)] pub enum ValidationError { #[error("Missing required action '{action}' in {mode} mode")] - MissingRequired { - action: String, + MissingRequired { + action: String, mode: String, suggestion: String, }, - + #[error("Unknown action '{action}' in {mode} mode")] - UnknownAction { - action: String, + UnknownAction { + action: String, mode: String, similar: Vec, }, - + #[error("Multiple validation errors")] Multiple(Vec), } @@ -70,47 +70,46 @@ pub struct ConfigValidator { } impl ConfigValidator { - pub fn new() -> Self { + // FIXED: Accept registry parameter to match config.rs calls + pub fn new(registry: ActionRegistry) -> Self { Self { - registry: ActionRegistry::new(), + registry, } } pub fn validate_keybindings(&self, keybindings: &CanvasKeybindings) -> ValidationResult { let mut result = ValidationResult::new(); - // Validate each mode - result.merge(self.validate_mode_bindings( - "edit", - &keybindings.edit, - self.registry.get_mode_registry("edit") - )); + // 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 + )); + } - result.merge(self.validate_mode_bindings( - "read_only", - &keybindings.read_only, - self.registry.get_mode_registry("read_only") - )); + 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 + )); + } - result.merge(self.validate_mode_bindings( - "suggestions", - &keybindings.suggestions, - self.registry.get_mode_registry("suggestions") - )); + // Skip suggestions mode if not discovered by introspection + // (autocomplete is separate concern as requested) - result.merge(self.validate_mode_bindings( - "global", - &keybindings.global, - self.registry.get_mode_registry("global") - )); + // 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>, + &self, + mode_name: &str, + bindings: &HashMap>, registry: &ModeRegistry ) -> ValidationResult { let mut result = ValidationResult::new(); @@ -122,8 +121,8 @@ impl ConfigValidator { action: action_name.clone(), mode: mode_name.to_string(), suggestion: format!( - "Add to config: {} = {:?}", - action_name, + "Add to config: {} = {:?}", + action_name, spec.examples ), }); @@ -151,7 +150,7 @@ impl ConfigValidator { if key_list.is_empty() { result.add_warning(ValidationWarning { message: format!( - "Action '{}' in {} mode has empty keybinding list", + "Action '{}' in {} mode has empty keybinding list", action_name, mode_name ), suggestion: Some(format!( @@ -166,11 +165,11 @@ impl ConfigValidator { if bindings.contains_key(auto_action) { result.add_warning(ValidationWarning { message: format!( - "Action '{}' in {} mode is auto-handled and shouldn't be in config", + "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", + "Remove '{}' from config - it's handled automatically", auto_action )), }); @@ -182,7 +181,7 @@ impl ConfigValidator { 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()); @@ -198,7 +197,7 @@ impl ConfigValidator { // 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;