removed hardcoded values from the canvas library
This commit is contained in:
@@ -42,24 +42,7 @@ pub enum CanvasAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CanvasAction {
|
impl CanvasAction {
|
||||||
pub fn from_key(key: crossterm::event::KeyCode) -> Option<Self> {
|
/// Convert string action name to CanvasAction enum (config-driven)
|
||||||
match key {
|
|
||||||
crossterm::event::KeyCode::Char(c) => Some(Self::InsertChar(c)),
|
|
||||||
crossterm::event::KeyCode::Backspace => Some(Self::DeleteBackward),
|
|
||||||
crossterm::event::KeyCode::Delete => Some(Self::DeleteForward),
|
|
||||||
crossterm::event::KeyCode::Left => Some(Self::MoveLeft),
|
|
||||||
crossterm::event::KeyCode::Right => Some(Self::MoveRight),
|
|
||||||
crossterm::event::KeyCode::Up => Some(Self::MoveUp),
|
|
||||||
crossterm::event::KeyCode::Down => Some(Self::MoveDown),
|
|
||||||
crossterm::event::KeyCode::Home => Some(Self::MoveLineStart),
|
|
||||||
crossterm::event::KeyCode::End => Some(Self::MoveLineEnd),
|
|
||||||
crossterm::event::KeyCode::Tab => Some(Self::NextField),
|
|
||||||
crossterm::event::KeyCode::BackTab => Some(Self::PrevField),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backward compatibility method
|
|
||||||
pub fn from_string(action: &str) -> Self {
|
pub fn from_string(action: &str) -> Self {
|
||||||
match action {
|
match action {
|
||||||
"delete_char_backward" => Self::DeleteBackward,
|
"delete_char_backward" => Self::DeleteBackward,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
use crate::canvas::state::CanvasState;
|
use crate::canvas::state::CanvasState;
|
||||||
use crate::canvas::actions::{CanvasAction, ActionResult, execute_canvas_action};
|
use crate::canvas::actions::{CanvasAction, ActionResult, execute_canvas_action};
|
||||||
use crate::config::CanvasConfig;
|
use crate::config::CanvasConfig;
|
||||||
|
use crossterm::event::{KeyCode, KeyModifiers};
|
||||||
|
|
||||||
/// High-level action dispatcher that coordinates between different action types
|
/// High-level action dispatcher that coordinates between different action types
|
||||||
pub struct ActionDispatcher;
|
pub struct ActionDispatcher;
|
||||||
@@ -14,18 +15,23 @@ impl ActionDispatcher {
|
|||||||
state: &mut S,
|
state: &mut S,
|
||||||
ideal_cursor_column: &mut usize,
|
ideal_cursor_column: &mut usize,
|
||||||
) -> anyhow::Result<ActionResult> {
|
) -> anyhow::Result<ActionResult> {
|
||||||
|
|
||||||
// Load config once here instead of threading it everywhere
|
// Load config once here instead of threading it everywhere
|
||||||
execute_canvas_action(action, state, ideal_cursor_column, Some(&CanvasConfig::load())).await
|
execute_canvas_action(action, state, ideal_cursor_column, Some(&CanvasConfig::load())).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Quick action dispatch from KeyCode
|
/// Quick action dispatch from KeyCode using config
|
||||||
pub async fn dispatch_key<S: CanvasState>(
|
pub async fn dispatch_key<S: CanvasState>(
|
||||||
key: crossterm::event::KeyCode,
|
key: KeyCode,
|
||||||
|
modifiers: KeyModifiers,
|
||||||
state: &mut S,
|
state: &mut S,
|
||||||
ideal_cursor_column: &mut usize,
|
ideal_cursor_column: &mut usize,
|
||||||
|
is_edit_mode: bool,
|
||||||
|
has_suggestions: bool,
|
||||||
) -> anyhow::Result<Option<ActionResult>> {
|
) -> anyhow::Result<Option<ActionResult>> {
|
||||||
if let Some(action) = CanvasAction::from_key(key) {
|
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(action, state, ideal_cursor_column).await?;
|
let result = Self::dispatch(action, state, ideal_cursor_column).await?;
|
||||||
Ok(Some(result))
|
Ok(Some(result))
|
||||||
} else {
|
} else {
|
||||||
@@ -53,131 +59,3 @@ impl ActionDispatcher {
|
|||||||
Ok(results)
|
Ok(results)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::actions::CanvasAction;
|
|
||||||
|
|
||||||
// Simple test implementation
|
|
||||||
struct TestFormState {
|
|
||||||
current_field: usize,
|
|
||||||
cursor_pos: usize,
|
|
||||||
inputs: Vec<String>,
|
|
||||||
field_names: Vec<String>,
|
|
||||||
has_changes: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TestFormState {
|
|
||||||
fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
current_field: 0,
|
|
||||||
cursor_pos: 0,
|
|
||||||
inputs: vec!["".to_string(), "".to_string()],
|
|
||||||
field_names: vec!["username".to_string(), "password".to_string()],
|
|
||||||
has_changes: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CanvasState for TestFormState {
|
|
||||||
fn current_field(&self) -> usize { self.current_field }
|
|
||||||
fn current_cursor_pos(&self) -> usize { self.cursor_pos }
|
|
||||||
fn set_current_field(&mut self, index: usize) { self.current_field = index; }
|
|
||||||
fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; }
|
|
||||||
|
|
||||||
fn get_current_input(&self) -> &str { &self.inputs[self.current_field] }
|
|
||||||
fn get_current_input_mut(&mut self) -> &mut String { &mut self.inputs[self.current_field] }
|
|
||||||
fn inputs(&self) -> Vec<&String> { self.inputs.iter().collect() }
|
|
||||||
fn fields(&self) -> Vec<&str> { self.field_names.iter().map(|s| s.as_str()).collect() }
|
|
||||||
|
|
||||||
fn has_unsaved_changes(&self) -> bool { self.has_changes }
|
|
||||||
fn set_has_unsaved_changes(&mut self, changed: bool) { self.has_changes = changed; }
|
|
||||||
|
|
||||||
// Custom action handling for testing
|
|
||||||
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &crate::state::ActionContext) -> Option<String> {
|
|
||||||
match action {
|
|
||||||
CanvasAction::Custom(s) if s == "test_custom" => {
|
|
||||||
Some("Custom action handled".to_string())
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_typed_action_dispatch() {
|
|
||||||
let mut state = TestFormState::new();
|
|
||||||
let mut ideal_cursor = 0;
|
|
||||||
|
|
||||||
// Test character insertion
|
|
||||||
let result = ActionDispatcher::dispatch(
|
|
||||||
CanvasAction::InsertChar('a'),
|
|
||||||
&mut state,
|
|
||||||
&mut ideal_cursor,
|
|
||||||
).await.unwrap();
|
|
||||||
|
|
||||||
assert!(result.is_success());
|
|
||||||
assert_eq!(state.get_current_input(), "a");
|
|
||||||
assert_eq!(state.cursor_pos, 1);
|
|
||||||
assert!(state.has_changes);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_key_dispatch() {
|
|
||||||
let mut state = TestFormState::new();
|
|
||||||
let mut ideal_cursor = 0;
|
|
||||||
|
|
||||||
let result = ActionDispatcher::dispatch_key(
|
|
||||||
crossterm::event::KeyCode::Char('b'),
|
|
||||||
&mut state,
|
|
||||||
&mut ideal_cursor,
|
|
||||||
).await.unwrap();
|
|
||||||
|
|
||||||
assert!(result.is_some());
|
|
||||||
assert!(result.unwrap().is_success());
|
|
||||||
assert_eq!(state.get_current_input(), "b");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_custom_action() {
|
|
||||||
let mut state = TestFormState::new();
|
|
||||||
let mut ideal_cursor = 0;
|
|
||||||
|
|
||||||
let result = ActionDispatcher::dispatch(
|
|
||||||
CanvasAction::Custom("test_custom".to_string()),
|
|
||||||
&mut state,
|
|
||||||
&mut ideal_cursor,
|
|
||||||
).await.unwrap();
|
|
||||||
|
|
||||||
match result {
|
|
||||||
ActionResult::HandledByFeature(msg) => {
|
|
||||||
assert_eq!(msg, "Custom action handled");
|
|
||||||
}
|
|
||||||
_ => panic!("Expected HandledByFeature result"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_batch_dispatch() {
|
|
||||||
let mut state = TestFormState::new();
|
|
||||||
let mut ideal_cursor = 0;
|
|
||||||
|
|
||||||
let actions = vec![
|
|
||||||
CanvasAction::InsertChar('h'),
|
|
||||||
CanvasAction::InsertChar('i'),
|
|
||||||
CanvasAction::MoveLeft,
|
|
||||||
CanvasAction::InsertChar('e'),
|
|
||||||
];
|
|
||||||
|
|
||||||
let results = ActionDispatcher::dispatch_batch(
|
|
||||||
actions,
|
|
||||||
&mut state,
|
|
||||||
&mut ideal_cursor,
|
|
||||||
).await.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(results.len(), 4);
|
|
||||||
assert!(results.iter().all(|r| r.is_success()));
|
|
||||||
assert_eq!(state.get_current_input(), "hei");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -39,18 +39,6 @@ enter_edit_mode_after = ["a"]
|
|||||||
previous_entry = ["left","q"]
|
previous_entry = ["left","q"]
|
||||||
next_entry = ["right","1"]
|
next_entry = ["right","1"]
|
||||||
|
|
||||||
move_left = ["h"]
|
|
||||||
move_right = ["l"]
|
|
||||||
move_up = ["k"]
|
|
||||||
move_down = ["j"]
|
|
||||||
move_word_next = ["w"]
|
|
||||||
move_word_end = ["e"]
|
|
||||||
move_word_prev = ["b"]
|
|
||||||
move_word_end_prev = ["ge"]
|
|
||||||
move_line_start = ["0"]
|
|
||||||
move_line_end = ["$"]
|
|
||||||
move_first_line = ["gg"]
|
|
||||||
move_last_line = ["x"]
|
|
||||||
enter_highlight_mode = ["v"]
|
enter_highlight_mode = ["v"]
|
||||||
enter_highlight_mode_linewise = ["ctrl+v"]
|
enter_highlight_mode_linewise = ["ctrl+v"]
|
||||||
|
|
||||||
@@ -69,8 +57,6 @@ prev_field = ["shift+enter"]
|
|||||||
exit = ["esc", "ctrl+e"]
|
exit = ["esc", "ctrl+e"]
|
||||||
delete_char_forward = ["delete"]
|
delete_char_forward = ["delete"]
|
||||||
delete_char_backward = ["backspace"]
|
delete_char_backward = ["backspace"]
|
||||||
move_left = ["left"]
|
|
||||||
move_right = ["right"]
|
|
||||||
suggestion_down = ["ctrl+n", "tab"]
|
suggestion_down = ["ctrl+n", "tab"]
|
||||||
suggestion_up = ["ctrl+p", "shift+tab"]
|
suggestion_up = ["ctrl+p", "shift+tab"]
|
||||||
|
|
||||||
|
|||||||
@@ -79,7 +79,9 @@ pub async fn handle_form_edit_with_canvas(
|
|||||||
ideal_cursor_column: &mut usize,
|
ideal_cursor_column: &mut usize,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
// Try canvas action from key first
|
// Try canvas action from key first
|
||||||
if let Some(canvas_action) = CanvasAction::from_key(key_event.code) {
|
let canvas_config = canvas::config::CanvasConfig::load();
|
||||||
|
if let Some(action_name) = canvas_config.get_edit_action(key_event.code, key_event.modifiers) {
|
||||||
|
let canvas_action = CanvasAction::from_string(action_name);
|
||||||
match ActionDispatcher::dispatch(canvas_action, form_state, ideal_cursor_column).await {
|
match ActionDispatcher::dispatch(canvas_action, form_state, ideal_cursor_column).await {
|
||||||
Ok(ActionResult::Success(msg)) => {
|
Ok(ActionResult::Success(msg)) => {
|
||||||
return Ok(msg.unwrap_or_default());
|
return Ok(msg.unwrap_or_default());
|
||||||
@@ -150,7 +152,10 @@ async fn handle_canvas_state_edit<S: CanvasState>(
|
|||||||
ideal_cursor_column: &mut usize,
|
ideal_cursor_column: &mut usize,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
// Try direct key mapping first (same pattern as FormState)
|
// Try direct key mapping first (same pattern as FormState)
|
||||||
if let Some(canvas_action) = CanvasAction::from_key(key.code) {
|
let canvas_config = canvas::config::CanvasConfig::load();
|
||||||
|
if let Some(action_name) = canvas_config.get_edit_action(key.code, key.modifiers) {
|
||||||
|
let canvas_action = CanvasAction::from_string(action_name);
|
||||||
|
|
||||||
match ActionDispatcher::dispatch(canvas_action, state, ideal_cursor_column).await {
|
match ActionDispatcher::dispatch(canvas_action, state, ideal_cursor_column).await {
|
||||||
Ok(ActionResult::Success(msg)) => {
|
Ok(ActionResult::Success(msg)) => {
|
||||||
return Ok(msg.unwrap_or_default());
|
return Ok(msg.unwrap_or_default());
|
||||||
|
|||||||
@@ -91,7 +91,9 @@ pub async fn handle_form_readonly_with_canvas(
|
|||||||
ideal_cursor_column: &mut usize,
|
ideal_cursor_column: &mut usize,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
// Try canvas action from key first
|
// Try canvas action from key first
|
||||||
if let Some(canvas_action) = CanvasAction::from_key(key_event.code) {
|
let canvas_config = canvas::config::CanvasConfig::load();
|
||||||
|
if let Some(action_name) = canvas_config.get_read_only_action(key_event.code, key_event.modifiers) {
|
||||||
|
let canvas_action = CanvasAction::from_string(action_name);
|
||||||
match ActionDispatcher::dispatch(canvas_action, form_state, ideal_cursor_column).await {
|
match ActionDispatcher::dispatch(canvas_action, form_state, ideal_cursor_column).await {
|
||||||
Ok(ActionResult::Success(msg)) => {
|
Ok(ActionResult::Success(msg)) => {
|
||||||
return Ok(msg.unwrap_or_default());
|
return Ok(msg.unwrap_or_default());
|
||||||
|
|||||||
@@ -1108,58 +1108,7 @@ impl EventHandler {
|
|||||||
) -> Result<Option<String>> {
|
) -> Result<Option<String>> {
|
||||||
let canvas_config = canvas::config::CanvasConfig::load();
|
let canvas_config = canvas::config::CanvasConfig::load();
|
||||||
|
|
||||||
// Handle suggestion actions first if suggestions are active
|
// Get action from config - handles all modes (edit/read-only/suggestions)
|
||||||
if form_state.autocomplete_active {
|
|
||||||
if let Some(action_str) = canvas_config.get_suggestion_action(key_event.code, key_event.modifiers) {
|
|
||||||
let canvas_action = CanvasAction::from_string(&action_str);
|
|
||||||
match ActionDispatcher::dispatch(canvas_action, form_state, &mut self.ideal_cursor_column).await {
|
|
||||||
Ok(result) => return Ok(Some(result.message().unwrap_or("").to_string())),
|
|
||||||
Err(_) => return Ok(Some("Suggestion action failed".to_string())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback hardcoded suggestion handling
|
|
||||||
match key_event.code {
|
|
||||||
KeyCode::Up => {
|
|
||||||
if let Ok(result) = ActionDispatcher::dispatch(
|
|
||||||
CanvasAction::SuggestionUp,
|
|
||||||
form_state,
|
|
||||||
&mut self.ideal_cursor_column,
|
|
||||||
).await {
|
|
||||||
return Ok(Some(result.message().unwrap_or("").to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
KeyCode::Down => {
|
|
||||||
if let Ok(result) = ActionDispatcher::dispatch(
|
|
||||||
CanvasAction::SuggestionDown,
|
|
||||||
form_state,
|
|
||||||
&mut self.ideal_cursor_column,
|
|
||||||
).await {
|
|
||||||
return Ok(Some(result.message().unwrap_or("").to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
KeyCode::Enter => {
|
|
||||||
if let Ok(result) = ActionDispatcher::dispatch(
|
|
||||||
CanvasAction::SelectSuggestion,
|
|
||||||
form_state,
|
|
||||||
&mut self.ideal_cursor_column,
|
|
||||||
).await {
|
|
||||||
return Ok(Some(result.message().unwrap_or("").to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
KeyCode::Esc => {
|
|
||||||
if let Ok(result) = ActionDispatcher::dispatch(
|
|
||||||
CanvasAction::ExitSuggestions,
|
|
||||||
form_state,
|
|
||||||
&mut self.ideal_cursor_column,
|
|
||||||
).await {
|
|
||||||
return Ok(Some(result.message().unwrap_or("").to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let action_str = canvas_config.get_action_for_key(
|
let action_str = canvas_config.get_action_for_key(
|
||||||
key_event.code,
|
key_event.code,
|
||||||
key_event.modifiers,
|
key_event.modifiers,
|
||||||
@@ -1168,11 +1117,13 @@ impl EventHandler {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if let Some(action_str) = action_str {
|
if let Some(action_str) = action_str {
|
||||||
|
// Skip mode transition actions - let the main event handler deal with them
|
||||||
if Self::is_mode_transition_action(action_str) {
|
if Self::is_mode_transition_action(action_str) {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
let canvas_action = CanvasAction::from_string(&action_str);
|
// Execute the config-mapped action
|
||||||
|
let canvas_action = CanvasAction::from_string(action_str);
|
||||||
match ActionDispatcher::dispatch(
|
match ActionDispatcher::dispatch(
|
||||||
canvas_action,
|
canvas_action,
|
||||||
form_state,
|
form_state,
|
||||||
@@ -1187,9 +1138,10 @@ impl EventHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to automatic key handling for edit mode
|
// Handle character insertion for edit mode (not in config)
|
||||||
if is_edit_mode {
|
if is_edit_mode {
|
||||||
if let Some(canvas_action) = CanvasAction::from_key(key_event.code) {
|
if let KeyCode::Char(c) = key_event.code {
|
||||||
|
let canvas_action = CanvasAction::InsertChar(c);
|
||||||
match ActionDispatcher::dispatch(
|
match ActionDispatcher::dispatch(
|
||||||
canvas_action,
|
canvas_action,
|
||||||
form_state,
|
form_state,
|
||||||
@@ -1199,42 +1151,13 @@ impl EventHandler {
|
|||||||
return Ok(Some(result.message().unwrap_or("").to_string()));
|
return Ok(Some(result.message().unwrap_or("").to_string()));
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
return Ok(Some("Auto action failed".to_string()));
|
return Ok(Some("Character insertion failed".to_string()));
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// In read-only mode, only handle non-character keys
|
|
||||||
let canvas_action = match key_event.code {
|
|
||||||
KeyCode::Left => Some(CanvasAction::MoveLeft),
|
|
||||||
KeyCode::Right => Some(CanvasAction::MoveRight),
|
|
||||||
KeyCode::Up => Some(CanvasAction::MoveUp),
|
|
||||||
KeyCode::Down => Some(CanvasAction::MoveDown),
|
|
||||||
KeyCode::Home => Some(CanvasAction::MoveLineStart),
|
|
||||||
KeyCode::End => Some(CanvasAction::MoveLineEnd),
|
|
||||||
KeyCode::Tab => Some(CanvasAction::NextField),
|
|
||||||
KeyCode::BackTab => Some(CanvasAction::PrevField),
|
|
||||||
KeyCode::Delete => Some(CanvasAction::DeleteForward),
|
|
||||||
KeyCode::Backspace => Some(CanvasAction::DeleteBackward),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(canvas_action) = canvas_action {
|
|
||||||
match ActionDispatcher::dispatch(
|
|
||||||
canvas_action,
|
|
||||||
form_state,
|
|
||||||
&mut self.ideal_cursor_column,
|
|
||||||
).await {
|
|
||||||
Ok(result) => {
|
|
||||||
return Ok(Some(result.message().unwrap_or("").to_string()));
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
return Ok(Some("Action failed".to_string()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No action found
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user