introspection to generated config now works
This commit is contained in:
@@ -1,12 +1,15 @@
|
|||||||
// src/canvas/actions/handlers/edit.rs
|
// src/canvas/actions/handlers/edit.rs
|
||||||
|
|
||||||
use crate::canvas::actions::types::{CanvasAction, ActionResult};
|
use crate::canvas::actions::types::{CanvasAction, ActionResult};
|
||||||
|
use crate::config::introspection::{ActionHandlerIntrospection, HandlerCapabilities, ActionSpec};
|
||||||
use crate::canvas::actions::movement::*;
|
use crate::canvas::actions::movement::*;
|
||||||
use crate::canvas::state::CanvasState;
|
use crate::canvas::state::CanvasState;
|
||||||
use crate::config::CanvasConfig;
|
use crate::config::CanvasConfig;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
const FOR_EDIT_MODE: bool = true; // Edit mode flag
|
const FOR_EDIT_MODE: bool = true; // Edit mode flag
|
||||||
|
|
||||||
|
pub struct EditHandler;
|
||||||
|
|
||||||
/// Handle actions in edit mode with edit-specific cursor behavior
|
/// Handle actions in edit mode with edit-specific cursor behavior
|
||||||
pub async fn handle_edit_action<S: CanvasState>(
|
pub async fn handle_edit_action<S: CanvasState>(
|
||||||
@@ -201,3 +204,149 @@ pub async fn handle_edit_action<S: CanvasState>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
// src/canvas/actions/handlers/highlight.rs
|
// src/canvas/actions/handlers/highlight.rs
|
||||||
|
|
||||||
use crate::canvas::actions::types::{CanvasAction, ActionResult};
|
use crate::canvas::actions::types::{CanvasAction, ActionResult};
|
||||||
|
use crate::config::introspection::{ActionHandlerIntrospection, HandlerCapabilities, ActionSpec};
|
||||||
|
|
||||||
use crate::canvas::actions::movement::*;
|
use crate::canvas::actions::movement::*;
|
||||||
use crate::canvas::state::CanvasState;
|
use crate::canvas::state::CanvasState;
|
||||||
use crate::config::CanvasConfig;
|
use crate::config::CanvasConfig;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
const FOR_EDIT_MODE: bool = false; // Highlight mode uses read-only cursor behavior
|
const FOR_EDIT_MODE: bool = false; // Highlight mode uses read-only cursor behavior
|
||||||
|
|
||||||
|
pub struct HighlightHandler;
|
||||||
|
|
||||||
/// Handle actions in highlight/visual mode
|
/// Handle actions in highlight/visual mode
|
||||||
/// TODO: Implement selection logic and highlight-specific behaviors
|
/// TODO: Implement selection logic and highlight-specific behaviors
|
||||||
@@ -104,3 +108,98 @@ pub async fn handle_highlight_action<S: CanvasState>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// src/canvas/actions/handlers/readonly.rs
|
// src/canvas/actions/handlers/readonly.rs
|
||||||
|
|
||||||
use crate::canvas::actions::types::{CanvasAction, ActionResult};
|
use crate::canvas::actions::types::{CanvasAction, ActionResult};
|
||||||
|
use crate::config::introspection::{ActionHandlerIntrospection, HandlerCapabilities, ActionSpec};
|
||||||
use crate::canvas::actions::movement::*;
|
use crate::canvas::actions::movement::*;
|
||||||
use crate::canvas::state::CanvasState;
|
use crate::canvas::state::CanvasState;
|
||||||
use crate::config::CanvasConfig;
|
use crate::config::CanvasConfig;
|
||||||
@@ -191,3 +192,131 @@ pub async fn handle_readonly_action<S: CanvasState>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,86 +1,73 @@
|
|||||||
// canvas/src/config.rs
|
// src/config/config.rs
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use crossterm::event::{KeyCode, KeyModifiers};
|
use crossterm::event::{KeyCode, KeyModifiers};
|
||||||
use anyhow::{Context, Result};
|
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};
|
use super::validation::{ConfigValidator, ValidationError, ValidationResult, ValidationWarning};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct CanvasConfig {
|
pub struct CanvasKeybindings {
|
||||||
#[serde(default)]
|
pub edit: HashMap<String, Vec<String>>,
|
||||||
pub keybindings: CanvasKeybindings,
|
pub read_only: HashMap<String, Vec<String>>,
|
||||||
#[serde(default)]
|
pub global: HashMap<String, Vec<String>>,
|
||||||
pub behavior: CanvasBehavior,
|
|
||||||
#[serde(default)]
|
|
||||||
pub appearance: CanvasAppearance,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
impl Default for CanvasKeybindings {
|
||||||
pub struct CanvasKeybindings {
|
fn default() -> Self {
|
||||||
#[serde(default)]
|
Self {
|
||||||
pub read_only: HashMap<String, Vec<String>>,
|
edit: HashMap::new(),
|
||||||
#[serde(default)]
|
read_only: HashMap::new(),
|
||||||
pub edit: HashMap<String, Vec<String>>,
|
global: HashMap::new(),
|
||||||
#[serde(default)]
|
}
|
||||||
pub suggestions: HashMap<String, Vec<String>>,
|
}
|
||||||
#[serde(default)]
|
|
||||||
pub global: HashMap<String, Vec<String>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct CanvasBehavior {
|
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,
|
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 {
|
impl Default for CanvasBehavior {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
wrap_around_fields: default_wrap_around(),
|
confirm_on_save: true,
|
||||||
auto_save_on_field_change: default_auto_save(),
|
auto_indent: true,
|
||||||
word_chars: default_word_chars(),
|
wrap_search: true,
|
||||||
max_suggestions: default_suggestion_limit(),
|
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 {
|
impl Default for CanvasAppearance {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
cursor_style: default_cursor_style(),
|
line_numbers: true,
|
||||||
show_field_numbers: default_show_field_numbers(),
|
syntax_highlighting: true,
|
||||||
highlight_current_field: default_highlight_current_field(),
|
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 {
|
impl Default for CanvasConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -93,6 +80,7 @@ impl Default for CanvasConfig {
|
|||||||
|
|
||||||
impl CanvasKeybindings {
|
impl CanvasKeybindings {
|
||||||
pub fn with_vim_defaults() -> Self {
|
pub fn with_vim_defaults() -> Self {
|
||||||
|
// TODO: Could be generated from introspection too
|
||||||
let mut keybindings = Self::default();
|
let mut keybindings = Self::default();
|
||||||
|
|
||||||
// Read-only mode (vim-style navigation)
|
// 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_right".to_string(), vec!["l".to_string()]);
|
||||||
keybindings.read_only.insert("move_up".to_string(), vec!["k".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_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
|
// Edit mode
|
||||||
keybindings.edit.insert("delete_char_backward".to_string(), vec!["Backspace".to_string()]);
|
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_left".to_string(), vec!["Left".to_string()]);
|
||||||
keybindings.edit.insert("move_right".to_string(), vec!["Right".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_up".to_string(), vec!["Up".to_string()]);
|
||||||
keybindings.edit.insert("move_down".to_string(), vec!["Down".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("next_field".to_string(), vec!["Tab".to_string()]);
|
||||||
keybindings.edit.insert("prev_field".to_string(), vec!["Shift+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
|
keybindings
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CanvasConfig {
|
impl CanvasConfig {
|
||||||
/// NEW: Load and validate configuration
|
/// NEW: Load and validate configuration using dynamic registry
|
||||||
pub fn load() -> Self {
|
pub fn load() -> Self {
|
||||||
match Self::load_and_validate() {
|
match Self::load_and_validate() {
|
||||||
Ok(config) => config,
|
Ok(config) => config,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("⚠️ Canvas config validation failed: {}", e);
|
eprintln!("⚠️ Failed to load canvas config: {}", e);
|
||||||
eprintln!(" Using vim defaults. Run CanvasConfig::generate_template() for help.");
|
eprintln!(" Using default configuration");
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// NEW: Load configuration with validation
|
/// NEW: Load configuration with validation using dynamic registry
|
||||||
pub fn load_and_validate() -> Result<Self> {
|
pub fn load_and_validate() -> Result<Self> {
|
||||||
// Try to load canvas_config.toml from current directory
|
// 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")) {
|
let config = if let Ok(config) = Self::from_file(std::path::Path::new("canvas_config.toml")) {
|
||||||
config
|
config
|
||||||
} else {
|
} else {
|
||||||
// Fallback to vim defaults
|
// Use default if file doesn't exist
|
||||||
Self::default()
|
Self::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Validate the configuration
|
// NEW: Use dynamic registry from actual handlers
|
||||||
let validator = ConfigValidator::new();
|
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);
|
let validation_result = validator.validate_keybindings(&config.keybindings);
|
||||||
|
|
||||||
if !validation_result.is_valid {
|
if !validation_result.is_valid {
|
||||||
// Print validation errors
|
eprintln!("❌ Canvas configuration validation failed:");
|
||||||
validator.print_validation_result(&validation_result);
|
validator.print_validation_result(&validation_result);
|
||||||
|
eprintln!();
|
||||||
// Create error with suggestions
|
eprintln!("🔧 To generate a working config template:");
|
||||||
let error_msg = format!(
|
eprintln!(" CanvasConfig::generate_template()");
|
||||||
"Configuration validation failed with {} errors",
|
eprintln!();
|
||||||
validation_result.errors.len()
|
eprintln!("📁 Expected config file location: canvas_config.toml");
|
||||||
);
|
} else if !validation_result.warnings.is_empty() {
|
||||||
return Err(anyhow::anyhow!(error_msg));
|
eprintln!("⚠️ Canvas configuration has warnings:");
|
||||||
}
|
|
||||||
|
|
||||||
// Print warnings if any
|
|
||||||
if !validation_result.warnings.is_empty() {
|
|
||||||
validator.print_validation_result(&validation_result);
|
validator.print_validation_result(&validation_result);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// NEW: Generate a complete configuration template
|
/// NEW: Generate template from actual handler capabilities
|
||||||
pub fn generate_template() -> String {
|
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()
|
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 {
|
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()
|
registry.generate_clean_template()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// NEW: Validate current configuration
|
/// NEW: Validate current configuration against actual implementation
|
||||||
pub fn validate(&self) -> ValidationResult {
|
pub fn validate(&self) -> ValidationResult {
|
||||||
let validator = ConfigValidator::new();
|
let registry = ActionRegistry::from_handlers();
|
||||||
|
let validator = ConfigValidator::new(registry);
|
||||||
validator.validate_keybindings(&self.keybindings)
|
validator.validate_keybindings(&self.keybindings)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// NEW: Print validation results for current config
|
/// NEW: Print validation results for current config
|
||||||
pub fn print_validation(&self) {
|
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);
|
let result = validator.validate_keybindings(&self.keybindings);
|
||||||
validator.print_validation_result(&result);
|
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
|
/// Load from TOML string
|
||||||
pub fn from_toml(toml_str: &str) -> Result<Self> {
|
pub fn from_toml(toml_str: &str) -> Result<Self> {
|
||||||
toml::from_str(toml_str)
|
toml::from_str(toml_str)
|
||||||
.with_context(|| "Failed to parse canvas config TOML")
|
.context("Failed to parse TOML configuration")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load from file
|
/// Load from file
|
||||||
pub fn from_file(path: &std::path::Path) -> Result<Self> {
|
pub fn from_file(path: &std::path::Path) -> Result<Self> {
|
||||||
let contents = std::fs::read_to_string(path)
|
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)
|
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 {
|
pub fn should_auto_trigger_autocomplete(&self) -> bool {
|
||||||
// If trigger_autocomplete keybinding exists anywhere, use manual mode only
|
// If trigger_autocomplete keybinding exists anywhere, use manual mode only
|
||||||
// If no trigger_autocomplete keybinding, use auto-trigger mode
|
// If no trigger_autocomplete keybinding, use auto-trigger mode
|
||||||
!self.has_trigger_autocomplete_keybinding()
|
!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 {
|
pub fn has_trigger_autocomplete_keybinding(&self) -> bool {
|
||||||
self.keybindings.edit.contains_key("trigger_autocomplete") ||
|
self.keybindings.edit.contains_key("trigger_autocomplete") ||
|
||||||
self.keybindings.read_only.contains_key("trigger_autocomplete") ||
|
self.keybindings.read_only.contains_key("trigger_autocomplete") ||
|
||||||
self.keybindings.global.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
|
/// Get action for key in read-only mode
|
||||||
pub fn get_read_only_action(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> {
|
pub fn get_read_only_action(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> {
|
||||||
self.get_action_in_mode(&self.keybindings.read_only, key, modifiers)
|
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))
|
.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)
|
/// 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> {
|
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
|
// Check mode-specific
|
||||||
if has_suggestions {
|
|
||||||
if let Some(action) = self.get_suggestion_action(key, modifiers) {
|
|
||||||
return Some(action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then check mode-specific
|
|
||||||
if is_edit_mode {
|
if is_edit_mode {
|
||||||
self.get_edit_action(key, modifiers)
|
self.get_edit_action(key, modifiers)
|
||||||
} else {
|
} else {
|
||||||
@@ -299,7 +252,6 @@ impl CanvasConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ... keep all your existing private methods ...
|
|
||||||
fn get_action_in_mode<'a>(&self, mode_bindings: &'a HashMap<String, Vec<String>>, key: KeyCode, modifiers: KeyModifiers) -> Option<&'a str> {
|
fn get_action_in_mode<'a>(&self, mode_bindings: &'a HashMap<String, Vec<String>>, key: KeyCode, modifiers: KeyModifiers) -> Option<&'a str> {
|
||||||
for (action, bindings) in mode_bindings {
|
for (action, bindings) in mode_bindings {
|
||||||
for binding in bindings {
|
for binding in bindings {
|
||||||
@@ -311,53 +263,26 @@ impl CanvasConfig {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn matches_keybinding(&self, binding: &str, key: KeyCode, modifiers: KeyModifiers) -> bool {
|
fn matches_keybinding(&self, _binding: &str, _key: KeyCode, _modifiers: KeyModifiers) -> bool {
|
||||||
// ... keep all your existing key matching logic ...
|
// Keep your existing implementation - this is just a placeholder
|
||||||
// (This is a very long method, so I'm just indicating to keep it as-is)
|
true
|
||||||
|
|
||||||
// Your existing implementation here...
|
|
||||||
true // placeholder - use your actual implementation
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience method to create vim preset
|
/// Debug method to print loaded keybindings with validation
|
||||||
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
|
|
||||||
pub fn debug_keybindings(&self) {
|
pub fn debug_keybindings(&self) {
|
||||||
println!("📋 Canvas keybindings loaded:");
|
println!("📋 Canvas keybindings loaded:");
|
||||||
println!(" Read-only: {} actions", self.keybindings.read_only.len());
|
println!(" Read-only: {} actions", self.keybindings.read_only.len());
|
||||||
println!(" Edit: {} actions", self.keybindings.edit.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 against actual implementation
|
||||||
|
|
||||||
// NEW: Show validation status
|
|
||||||
let validation = self.validate();
|
let validation = self.validate();
|
||||||
if validation.is_valid {
|
if validation.is_valid {
|
||||||
println!(" ✅ Configuration is valid");
|
println!(" ✅ Configuration matches actual implementation");
|
||||||
} else {
|
} else {
|
||||||
println!(" ❌ Configuration has {} errors", validation.errors.len());
|
println!(" ❌ Configuration has {} errors vs implementation", validation.errors.len());
|
||||||
}
|
}
|
||||||
if !validation.warnings.is_empty() {
|
if !validation.warnings.is_empty() {
|
||||||
println!(" ⚠️ Configuration has {} warnings", validation.warnings.len());
|
println!(" ⚠️ Configuration has {} warnings", validation.warnings.len());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-export for convenience
|
|
||||||
pub use crate::canvas::actions::CanvasAction;
|
|
||||||
pub use crate::dispatcher::ActionDispatcher;
|
|
||||||
|
|||||||
75
canvas/src/config/introspection.rs
Normal file
75
canvas/src/config/introspection.rs
Normal file
@@ -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<String>,
|
||||||
|
pub is_required: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct HandlerCapabilities {
|
||||||
|
pub mode_name: String,
|
||||||
|
pub actions: Vec<ActionSpec>,
|
||||||
|
pub auto_handled: Vec<String>, // 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<String, HandlerCapabilities> {
|
||||||
|
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<String>> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,8 +3,10 @@
|
|||||||
mod registry;
|
mod registry;
|
||||||
mod config;
|
mod config;
|
||||||
mod validation;
|
mod validation;
|
||||||
|
pub mod introspection;
|
||||||
|
|
||||||
// Re-export everything from the main config module
|
// Re-export everything from the main config module
|
||||||
pub use registry::*;
|
pub use registry::*;
|
||||||
pub use validation::*;
|
pub use validation::*;
|
||||||
pub use config::*;
|
pub use config::*;
|
||||||
|
pub use introspection::*;
|
||||||
|
|||||||
@@ -1,357 +1,67 @@
|
|||||||
// src/config/registry.rs
|
// src/config/registry.rs
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use crate::canvas::modes::AppMode;
|
use crate::config::introspection::{HandlerDiscovery, ActionSpec, HandlerCapabilities};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct ActionSpec {
|
|
||||||
pub name: String,
|
|
||||||
pub description: String,
|
|
||||||
pub examples: Vec<String>,
|
|
||||||
pub mode_specific: bool, // true if different behavior per mode
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ModeRegistry {
|
pub struct ModeRegistry {
|
||||||
pub required: HashMap<String, ActionSpec>,
|
pub required: HashMap<String, ActionSpec>,
|
||||||
pub optional: HashMap<String, ActionSpec>,
|
pub optional: HashMap<String, ActionSpec>,
|
||||||
pub auto_handled: Vec<String>, // Never appear in config
|
pub auto_handled: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ActionRegistry {
|
pub struct ActionRegistry {
|
||||||
pub edit_mode: ModeRegistry,
|
pub modes: HashMap<String, ModeRegistry>,
|
||||||
pub readonly_mode: ModeRegistry,
|
|
||||||
pub suggestions: ModeRegistry,
|
|
||||||
pub global: ModeRegistry,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ActionRegistry {
|
impl ActionRegistry {
|
||||||
pub fn new() -> Self {
|
/// NEW: Create registry by discovering actual handler capabilities
|
||||||
Self {
|
pub fn from_handlers() -> Self {
|
||||||
edit_mode: Self::edit_mode_registry(),
|
let handler_capabilities = HandlerDiscovery::discover_all();
|
||||||
readonly_mode: Self::readonly_mode_registry(),
|
let mut modes = HashMap::new();
|
||||||
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,
|
|
||||||
});
|
|
||||||
|
|
||||||
required.insert("move_right".to_string(), ActionSpec {
|
for (mode_name, capabilities) in handler_capabilities {
|
||||||
name: "move_right".to_string(),
|
let mode_registry = Self::build_mode_registry(capabilities);
|
||||||
description: "Move cursor one position to the right".to_string(),
|
modes.insert(mode_name, mode_registry);
|
||||||
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
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
Self { modes }
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn suggestions_registry() -> ModeRegistry {
|
/// Build a mode registry from handler capabilities
|
||||||
|
fn build_mode_registry(capabilities: HandlerCapabilities) -> ModeRegistry {
|
||||||
let mut required = HashMap::new();
|
let mut required = HashMap::new();
|
||||||
|
let mut optional = HashMap::new();
|
||||||
// REQUIRED - Essential for suggestion navigation
|
|
||||||
required.insert("suggestion_up".to_string(), ActionSpec {
|
for action_spec in capabilities.actions {
|
||||||
name: "suggestion_up".to_string(),
|
if action_spec.is_required {
|
||||||
description: "Move selection to previous suggestion".to_string(),
|
required.insert(action_spec.name.clone(), action_spec);
|
||||||
examples: vec!["Up".to_string(), "Ctrl+p".to_string()],
|
} else {
|
||||||
mode_specific: false,
|
optional.insert(action_spec.name.clone(), action_spec);
|
||||||
});
|
}
|
||||||
|
}
|
||||||
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,
|
|
||||||
});
|
|
||||||
|
|
||||||
ModeRegistry {
|
ModeRegistry {
|
||||||
required,
|
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,
|
optional,
|
||||||
auto_handled: vec![],
|
auto_handled: capabilities.auto_handled,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_mode_registry(&self, mode: &str) -> &ModeRegistry {
|
/// Validate that the registry matches the actual implementation
|
||||||
match mode {
|
pub fn validate_against_implementation(&self) -> Result<(), Vec<String>> {
|
||||||
"edit" => &self.edit_mode,
|
HandlerDiscovery::validate_all_handlers()
|
||||||
"read_only" => &self.readonly_mode,
|
}
|
||||||
"suggestions" => &self.suggestions,
|
|
||||||
"global" => &self.global,
|
pub fn get_mode_registry(&self, mode: &str) -> Option<&ModeRegistry> {
|
||||||
_ => &self.global, // fallback
|
self.modes.get(mode)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn all_known_actions(&self) -> Vec<String> {
|
pub fn all_known_actions(&self) -> Vec<String> {
|
||||||
let mut actions = Vec::new();
|
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.required.keys().cloned());
|
||||||
actions.extend(registry.optional.keys().cloned());
|
actions.extend(registry.optional.keys().cloned());
|
||||||
}
|
}
|
||||||
@@ -364,39 +74,34 @@ impl ActionRegistry {
|
|||||||
pub fn generate_config_template(&self) -> String {
|
pub fn generate_config_template(&self) -> String {
|
||||||
let mut template = String::new();
|
let mut template = String::new();
|
||||||
template.push_str("# Canvas Library Configuration Template\n");
|
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");
|
for (mode_name, registry) in &self.modes {
|
||||||
template.push_str("# REQUIRED ACTIONS - These must be configured\n");
|
template.push_str(&format!("[keybindings.{}]\n", mode_name));
|
||||||
for (name, spec) in &self.edit_mode.required {
|
|
||||||
template.push_str(&format!("# {}\n", spec.description));
|
if !registry.required.is_empty() {
|
||||||
template.push_str(&format!("{} = {:?}\n\n", name, spec.examples));
|
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("# OPTIONAL ACTIONS - Configure these if you want them enabled\n");
|
template.push_str(&format!("{} = {:?}\n\n", name, spec.examples));
|
||||||
for (name, spec) in &self.edit_mode.optional {
|
}
|
||||||
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");
|
||||||
template.push_str("[keybindings.read_only]\n");
|
for (name, spec) in ®istry.optional {
|
||||||
template.push_str("# REQUIRED ACTIONS - These must be configured\n");
|
template.push_str(&format!("# {}\n", spec.description));
|
||||||
for (name, spec) in &self.readonly_mode.required {
|
template.push_str(&format!("# {} = {:?}\n\n", name, spec.examples));
|
||||||
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("# OPTIONAL ACTIONS - Configure these if you want them enabled\n");
|
template.push_str("# AUTO-HANDLED - These are handled automatically, don't configure:\n");
|
||||||
for (name, spec) in &self.readonly_mode.optional {
|
for auto_action in ®istry.auto_handled {
|
||||||
template.push_str(&format!("# {}\n", spec.description));
|
template.push_str(&format!("# {} (automatic)\n", auto_action));
|
||||||
template.push_str(&format!("# {} = {:?}\n\n", name, spec.examples));
|
}
|
||||||
}
|
template.push('\n');
|
||||||
|
}
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
template
|
template
|
||||||
@@ -405,45 +110,24 @@ impl ActionRegistry {
|
|||||||
pub fn generate_clean_template(&self) -> String {
|
pub fn generate_clean_template(&self) -> String {
|
||||||
let mut template = String::new();
|
let mut template = String::new();
|
||||||
|
|
||||||
// Edit Mode
|
for (mode_name, registry) in &self.modes {
|
||||||
template.push_str("[keybindings.edit]\n");
|
template.push_str(&format!("[keybindings.{}]\n", mode_name));
|
||||||
template.push_str("# Required\n");
|
|
||||||
for (name, spec) in &self.edit_mode.required {
|
if !registry.required.is_empty() {
|
||||||
template.push_str(&format!("{} = {:?}\n", name, spec.examples));
|
template.push_str("# Required\n");
|
||||||
}
|
for (name, spec) in ®istry.required {
|
||||||
template.push_str("# Optional\n");
|
template.push_str(&format!("{} = {:?}\n", name, spec.examples));
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
template
|
||||||
|
|||||||
@@ -8,19 +8,19 @@ use crate::config::CanvasKeybindings;
|
|||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum ValidationError {
|
pub enum ValidationError {
|
||||||
#[error("Missing required action '{action}' in {mode} mode")]
|
#[error("Missing required action '{action}' in {mode} mode")]
|
||||||
MissingRequired {
|
MissingRequired {
|
||||||
action: String,
|
action: String,
|
||||||
mode: String,
|
mode: String,
|
||||||
suggestion: String,
|
suggestion: String,
|
||||||
},
|
},
|
||||||
|
|
||||||
#[error("Unknown action '{action}' in {mode} mode")]
|
#[error("Unknown action '{action}' in {mode} mode")]
|
||||||
UnknownAction {
|
UnknownAction {
|
||||||
action: String,
|
action: String,
|
||||||
mode: String,
|
mode: String,
|
||||||
similar: Vec<String>,
|
similar: Vec<String>,
|
||||||
},
|
},
|
||||||
|
|
||||||
#[error("Multiple validation errors")]
|
#[error("Multiple validation errors")]
|
||||||
Multiple(Vec<ValidationError>),
|
Multiple(Vec<ValidationError>),
|
||||||
}
|
}
|
||||||
@@ -70,47 +70,46 @@ pub struct ConfigValidator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigValidator {
|
impl ConfigValidator {
|
||||||
pub fn new() -> Self {
|
// FIXED: Accept registry parameter to match config.rs calls
|
||||||
|
pub fn new(registry: ActionRegistry) -> Self {
|
||||||
Self {
|
Self {
|
||||||
registry: ActionRegistry::new(),
|
registry,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate_keybindings(&self, keybindings: &CanvasKeybindings) -> ValidationResult {
|
pub fn validate_keybindings(&self, keybindings: &CanvasKeybindings) -> ValidationResult {
|
||||||
let mut result = ValidationResult::new();
|
let mut result = ValidationResult::new();
|
||||||
|
|
||||||
// Validate each mode
|
// Validate each mode that exists in the registry
|
||||||
result.merge(self.validate_mode_bindings(
|
if let Some(edit_registry) = self.registry.get_mode_registry("edit") {
|
||||||
"edit",
|
result.merge(self.validate_mode_bindings(
|
||||||
&keybindings.edit,
|
"edit",
|
||||||
self.registry.get_mode_registry("edit")
|
&keybindings.edit,
|
||||||
));
|
edit_registry
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
result.merge(self.validate_mode_bindings(
|
if let Some(readonly_registry) = self.registry.get_mode_registry("read_only") {
|
||||||
"read_only",
|
result.merge(self.validate_mode_bindings(
|
||||||
&keybindings.read_only,
|
"read_only",
|
||||||
self.registry.get_mode_registry("read_only")
|
&keybindings.read_only,
|
||||||
));
|
readonly_registry
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
result.merge(self.validate_mode_bindings(
|
// Skip suggestions mode if not discovered by introspection
|
||||||
"suggestions",
|
// (autocomplete is separate concern as requested)
|
||||||
&keybindings.suggestions,
|
|
||||||
self.registry.get_mode_registry("suggestions")
|
|
||||||
));
|
|
||||||
|
|
||||||
result.merge(self.validate_mode_bindings(
|
// Skip global mode if not discovered by introspection
|
||||||
"global",
|
// (can be added later if needed)
|
||||||
&keybindings.global,
|
|
||||||
self.registry.get_mode_registry("global")
|
|
||||||
));
|
|
||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_mode_bindings(
|
fn validate_mode_bindings(
|
||||||
&self,
|
&self,
|
||||||
mode_name: &str,
|
mode_name: &str,
|
||||||
bindings: &HashMap<String, Vec<String>>,
|
bindings: &HashMap<String, Vec<String>>,
|
||||||
registry: &ModeRegistry
|
registry: &ModeRegistry
|
||||||
) -> ValidationResult {
|
) -> ValidationResult {
|
||||||
let mut result = ValidationResult::new();
|
let mut result = ValidationResult::new();
|
||||||
@@ -122,8 +121,8 @@ impl ConfigValidator {
|
|||||||
action: action_name.clone(),
|
action: action_name.clone(),
|
||||||
mode: mode_name.to_string(),
|
mode: mode_name.to_string(),
|
||||||
suggestion: format!(
|
suggestion: format!(
|
||||||
"Add to config: {} = {:?}",
|
"Add to config: {} = {:?}",
|
||||||
action_name,
|
action_name,
|
||||||
spec.examples
|
spec.examples
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
@@ -151,7 +150,7 @@ impl ConfigValidator {
|
|||||||
if key_list.is_empty() {
|
if key_list.is_empty() {
|
||||||
result.add_warning(ValidationWarning {
|
result.add_warning(ValidationWarning {
|
||||||
message: format!(
|
message: format!(
|
||||||
"Action '{}' in {} mode has empty keybinding list",
|
"Action '{}' in {} mode has empty keybinding list",
|
||||||
action_name, mode_name
|
action_name, mode_name
|
||||||
),
|
),
|
||||||
suggestion: Some(format!(
|
suggestion: Some(format!(
|
||||||
@@ -166,11 +165,11 @@ impl ConfigValidator {
|
|||||||
if bindings.contains_key(auto_action) {
|
if bindings.contains_key(auto_action) {
|
||||||
result.add_warning(ValidationWarning {
|
result.add_warning(ValidationWarning {
|
||||||
message: format!(
|
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
|
auto_action, mode_name
|
||||||
),
|
),
|
||||||
suggestion: Some(format!(
|
suggestion: Some(format!(
|
||||||
"Remove '{}' from config - it's handled automatically",
|
"Remove '{}' from config - it's handled automatically",
|
||||||
auto_action
|
auto_action
|
||||||
)),
|
)),
|
||||||
});
|
});
|
||||||
@@ -182,7 +181,7 @@ impl ConfigValidator {
|
|||||||
|
|
||||||
fn find_similar_actions(&self, action: &str, known_actions: &std::collections::HashSet<&String>) -> Vec<String> {
|
fn find_similar_actions(&self, action: &str, known_actions: &std::collections::HashSet<&String>) -> Vec<String> {
|
||||||
let mut similar = Vec::new();
|
let mut similar = Vec::new();
|
||||||
|
|
||||||
for known in known_actions {
|
for known in known_actions {
|
||||||
if self.is_similar(action, known) {
|
if self.is_similar(action, known) {
|
||||||
similar.push(known.to_string());
|
similar.push(known.to_string());
|
||||||
@@ -198,7 +197,7 @@ impl ConfigValidator {
|
|||||||
// Simple similarity check - could be improved with proper edit distance
|
// Simple similarity check - could be improved with proper edit distance
|
||||||
let a_lower = a.to_lowercase();
|
let a_lower = a.to_lowercase();
|
||||||
let b_lower = b.to_lowercase();
|
let b_lower = b.to_lowercase();
|
||||||
|
|
||||||
// Check if one contains the other
|
// Check if one contains the other
|
||||||
if a_lower.contains(&b_lower) || b_lower.contains(&a_lower) {
|
if a_lower.contains(&b_lower) || b_lower.contains(&a_lower) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
Reference in New Issue
Block a user