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