strings to enum, eased state.rs
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -478,6 +478,8 @@ dependencies = [
|
|||||||
"common",
|
"common",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
|
"tokio",
|
||||||
|
"tokio-test",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -14,3 +14,7 @@ common = { path = "../common" }
|
|||||||
ratatui = { workspace = true }
|
ratatui = { workspace = true }
|
||||||
crossterm = { workspace = true }
|
crossterm = { workspace = true }
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
|
tokio = { workspace = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio-test = "0.4.4"
|
||||||
|
|||||||
@@ -1,106 +1,135 @@
|
|||||||
// canvas/src/actions/edit.rs
|
// canvas/src/actions/edit.rs
|
||||||
|
|
||||||
use crate::state::CanvasState;
|
use crate::state::{CanvasState, ActionContext};
|
||||||
|
use crate::actions::types::{CanvasAction, ActionResult};
|
||||||
use crossterm::event::{KeyCode, KeyEvent};
|
use crossterm::event::{KeyCode, KeyEvent};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
/// Execute a generic edit action on any CanvasState implementation.
|
/// Execute a typed canvas action on any CanvasState implementation
|
||||||
/// This is the core function that makes the mode system work across all features.
|
pub async fn execute_canvas_action<S: CanvasState>(
|
||||||
|
action: CanvasAction,
|
||||||
|
state: &mut S,
|
||||||
|
ideal_cursor_column: &mut usize,
|
||||||
|
) -> Result<ActionResult> {
|
||||||
|
// 1. Try feature-specific handler first
|
||||||
|
let context = ActionContext {
|
||||||
|
key_code: None, // We don't need KeyCode anymore since action is typed
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Handle suggestion actions
|
||||||
|
if let Some(result) = handle_suggestion_action(&action, state)? {
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Handle generic canvas actions
|
||||||
|
handle_generic_canvas_action(action, state, ideal_cursor_column).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Legacy function for string-based actions (backwards compatibility)
|
||||||
pub async fn execute_edit_action<S: CanvasState>(
|
pub async fn execute_edit_action<S: CanvasState>(
|
||||||
action: &str,
|
action: &str,
|
||||||
key: KeyEvent,
|
key: KeyEvent,
|
||||||
state: &mut S,
|
state: &mut S,
|
||||||
ideal_cursor_column: &mut usize,
|
ideal_cursor_column: &mut usize,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
// 1. Try feature-specific handler first (for autocomplete, field-specific logic, etc.)
|
let typed_action = match action {
|
||||||
let context = crate::state::ActionContext {
|
"insert_char" => {
|
||||||
key_code: Some(key.code),
|
if let KeyCode::Char(c) = key.code {
|
||||||
ideal_cursor_column: *ideal_cursor_column,
|
CanvasAction::InsertChar(c)
|
||||||
current_input: state.get_current_input().to_string(),
|
} else {
|
||||||
current_field: state.current_field(),
|
return Ok("Error: insert_char called without a char key.".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => CanvasAction::from_string(action),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(result) = state.handle_feature_action(action, &context) {
|
let result = execute_canvas_action(typed_action, state, ideal_cursor_column).await?;
|
||||||
return Ok(result);
|
|
||||||
|
// Convert ActionResult back to string for backwards compatibility
|
||||||
|
Ok(result.message().unwrap_or("").to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Handle suggestion-related actions generically
|
/// Handle suggestion-related actions
|
||||||
if handle_suggestion_actions(action, state)? {
|
fn handle_suggestion_action<S: CanvasState>(
|
||||||
return Ok("".to_string()); // Suggestion action handled
|
action: &CanvasAction,
|
||||||
}
|
state: &mut S,
|
||||||
|
) -> Result<Option<ActionResult>> {
|
||||||
// 3. Fall back to generic canvas actions (handles 95% of all actions)
|
|
||||||
handle_generic_action(action, key, state, ideal_cursor_column).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle suggestion/autocomplete actions generically
|
|
||||||
fn handle_suggestion_actions<S: CanvasState>(action: &str, state: &mut S) -> Result<bool> {
|
|
||||||
match action {
|
match action {
|
||||||
"suggestion_down" => {
|
CanvasAction::SuggestionDown => {
|
||||||
if let Some(suggestions) = state.get_suggestions() {
|
if let Some(suggestions) = state.get_suggestions() {
|
||||||
if !suggestions.is_empty() {
|
if !suggestions.is_empty() {
|
||||||
let current = state.get_selected_suggestion_index().unwrap_or(0);
|
let current = state.get_selected_suggestion_index().unwrap_or(0);
|
||||||
let next = (current + 1) % suggestions.len();
|
let next = (current + 1) % suggestions.len();
|
||||||
state.set_selected_suggestion_index(Some(next));
|
state.set_selected_suggestion_index(Some(next));
|
||||||
return Ok(true);
|
return Ok(Some(ActionResult::success()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(false)
|
Ok(None)
|
||||||
}
|
}
|
||||||
"suggestion_up" => {
|
|
||||||
|
CanvasAction::SuggestionUp => {
|
||||||
if let Some(suggestions) = state.get_suggestions() {
|
if let Some(suggestions) = state.get_suggestions() {
|
||||||
if !suggestions.is_empty() {
|
if !suggestions.is_empty() {
|
||||||
let current = state.get_selected_suggestion_index().unwrap_or(0);
|
let current = state.get_selected_suggestion_index().unwrap_or(0);
|
||||||
let prev = if current == 0 { suggestions.len() - 1 } else { current - 1 };
|
let prev = if current == 0 { suggestions.len() - 1 } else { current - 1 };
|
||||||
state.set_selected_suggestion_index(Some(prev));
|
state.set_selected_suggestion_index(Some(prev));
|
||||||
return Ok(true);
|
return Ok(Some(ActionResult::success()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(false)
|
Ok(None)
|
||||||
}
|
}
|
||||||
"select_suggestion" => {
|
|
||||||
|
CanvasAction::SelectSuggestion => {
|
||||||
// Let feature handle this via handle_feature_action since it's feature-specific
|
// Let feature handle this via handle_feature_action since it's feature-specific
|
||||||
Ok(false)
|
Ok(None)
|
||||||
}
|
}
|
||||||
"exit_suggestions" => {
|
|
||||||
|
CanvasAction::ExitSuggestions => {
|
||||||
state.deactivate_suggestions();
|
state.deactivate_suggestions();
|
||||||
Ok(true)
|
Ok(Some(ActionResult::success()))
|
||||||
}
|
}
|
||||||
_ => Ok(false)
|
|
||||||
|
_ => Ok(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle generic canvas actions (movement, editing, etc.)
|
/// Handle core canvas actions with full type safety
|
||||||
async fn handle_generic_action<S: CanvasState>(
|
async fn handle_generic_canvas_action<S: CanvasState>(
|
||||||
action: &str,
|
action: CanvasAction,
|
||||||
key: KeyEvent,
|
|
||||||
state: &mut S,
|
state: &mut S,
|
||||||
ideal_cursor_column: &mut usize,
|
ideal_cursor_column: &mut usize,
|
||||||
) -> Result<String> {
|
) -> Result<ActionResult> {
|
||||||
match action {
|
match action {
|
||||||
"insert_char" => {
|
CanvasAction::InsertChar(c) => {
|
||||||
if let KeyCode::Char(c) = key.code {
|
|
||||||
let cursor_pos = state.current_cursor_pos();
|
let cursor_pos = state.current_cursor_pos();
|
||||||
let field_value = state.get_current_input_mut();
|
let field_value = state.get_current_input_mut();
|
||||||
let mut chars: Vec<char> = field_value.chars().collect();
|
let mut chars: Vec<char> = field_value.chars().collect();
|
||||||
|
|
||||||
if cursor_pos <= chars.len() {
|
if cursor_pos <= chars.len() {
|
||||||
chars.insert(cursor_pos, c);
|
chars.insert(cursor_pos, c);
|
||||||
*field_value = chars.into_iter().collect();
|
*field_value = chars.into_iter().collect();
|
||||||
state.set_current_cursor_pos(cursor_pos + 1);
|
state.set_current_cursor_pos(cursor_pos + 1);
|
||||||
state.set_has_unsaved_changes(true);
|
state.set_has_unsaved_changes(true);
|
||||||
*ideal_cursor_column = state.current_cursor_pos();
|
*ideal_cursor_column = state.current_cursor_pos();
|
||||||
}
|
Ok(ActionResult::success())
|
||||||
} else {
|
} else {
|
||||||
return Ok("Error: insert_char called without a char key.".to_string());
|
Ok(ActionResult::error("Invalid cursor position for character insertion"))
|
||||||
}
|
}
|
||||||
Ok("".to_string())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
"delete_char_backward" => {
|
CanvasAction::DeleteBackward => {
|
||||||
if state.current_cursor_pos() > 0 {
|
if state.current_cursor_pos() > 0 {
|
||||||
let cursor_pos = state.current_cursor_pos();
|
let cursor_pos = state.current_cursor_pos();
|
||||||
let field_value = state.get_current_input_mut();
|
let field_value = state.get_current_input_mut();
|
||||||
let mut chars: Vec<char> = field_value.chars().collect();
|
let mut chars: Vec<char> = field_value.chars().collect();
|
||||||
|
|
||||||
if cursor_pos <= chars.len() {
|
if cursor_pos <= chars.len() {
|
||||||
chars.remove(cursor_pos - 1);
|
chars.remove(cursor_pos - 1);
|
||||||
*field_value = chars.into_iter().collect();
|
*field_value = chars.into_iter().collect();
|
||||||
@@ -110,23 +139,24 @@ async fn handle_generic_action<S: CanvasState>(
|
|||||||
*ideal_cursor_column = new_pos;
|
*ideal_cursor_column = new_pos;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok("".to_string())
|
Ok(ActionResult::success())
|
||||||
}
|
}
|
||||||
|
|
||||||
"delete_char_forward" => {
|
CanvasAction::DeleteForward => {
|
||||||
let cursor_pos = state.current_cursor_pos();
|
let cursor_pos = state.current_cursor_pos();
|
||||||
let field_value = state.get_current_input_mut();
|
let field_value = state.get_current_input_mut();
|
||||||
let mut chars: Vec<char> = field_value.chars().collect();
|
let mut chars: Vec<char> = field_value.chars().collect();
|
||||||
|
|
||||||
if cursor_pos < chars.len() {
|
if cursor_pos < chars.len() {
|
||||||
chars.remove(cursor_pos);
|
chars.remove(cursor_pos);
|
||||||
*field_value = chars.into_iter().collect();
|
*field_value = chars.into_iter().collect();
|
||||||
state.set_has_unsaved_changes(true);
|
state.set_has_unsaved_changes(true);
|
||||||
*ideal_cursor_column = cursor_pos;
|
*ideal_cursor_column = cursor_pos;
|
||||||
}
|
}
|
||||||
Ok("".to_string())
|
Ok(ActionResult::success())
|
||||||
}
|
}
|
||||||
|
|
||||||
"next_field" => {
|
CanvasAction::NextField => {
|
||||||
let num_fields = state.fields().len();
|
let num_fields = state.fields().len();
|
||||||
if num_fields > 0 {
|
if num_fields > 0 {
|
||||||
let current_field = state.current_field();
|
let current_field = state.current_field();
|
||||||
@@ -136,10 +166,10 @@ async fn handle_generic_action<S: CanvasState>(
|
|||||||
let max_pos = current_input.len();
|
let max_pos = current_input.len();
|
||||||
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
|
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
|
||||||
}
|
}
|
||||||
Ok("".to_string())
|
Ok(ActionResult::success())
|
||||||
}
|
}
|
||||||
|
|
||||||
"prev_field" => {
|
CanvasAction::PrevField => {
|
||||||
let num_fields = state.fields().len();
|
let num_fields = state.fields().len();
|
||||||
if num_fields > 0 {
|
if num_fields > 0 {
|
||||||
let current_field = state.current_field();
|
let current_field = state.current_field();
|
||||||
@@ -153,17 +183,17 @@ async fn handle_generic_action<S: CanvasState>(
|
|||||||
let max_pos = current_input.len();
|
let max_pos = current_input.len();
|
||||||
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
|
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
|
||||||
}
|
}
|
||||||
Ok("".to_string())
|
Ok(ActionResult::success())
|
||||||
}
|
}
|
||||||
|
|
||||||
"move_left" => {
|
CanvasAction::MoveLeft => {
|
||||||
let new_pos = state.current_cursor_pos().saturating_sub(1);
|
let new_pos = state.current_cursor_pos().saturating_sub(1);
|
||||||
state.set_current_cursor_pos(new_pos);
|
state.set_current_cursor_pos(new_pos);
|
||||||
*ideal_cursor_column = new_pos;
|
*ideal_cursor_column = new_pos;
|
||||||
Ok("".to_string())
|
Ok(ActionResult::success())
|
||||||
}
|
}
|
||||||
|
|
||||||
"move_right" => {
|
CanvasAction::MoveRight => {
|
||||||
let current_input = state.get_current_input();
|
let current_input = state.get_current_input();
|
||||||
let current_pos = state.current_cursor_pos();
|
let current_pos = state.current_cursor_pos();
|
||||||
if current_pos < current_input.len() {
|
if current_pos < current_input.len() {
|
||||||
@@ -171,10 +201,10 @@ async fn handle_generic_action<S: CanvasState>(
|
|||||||
state.set_current_cursor_pos(new_pos);
|
state.set_current_cursor_pos(new_pos);
|
||||||
*ideal_cursor_column = new_pos;
|
*ideal_cursor_column = new_pos;
|
||||||
}
|
}
|
||||||
Ok("".to_string())
|
Ok(ActionResult::success())
|
||||||
}
|
}
|
||||||
|
|
||||||
"move_up" => {
|
CanvasAction::MoveUp => {
|
||||||
let num_fields = state.fields().len();
|
let num_fields = state.fields().len();
|
||||||
if num_fields > 0 {
|
if num_fields > 0 {
|
||||||
let current_field = state.current_field();
|
let current_field = state.current_field();
|
||||||
@@ -184,10 +214,10 @@ async fn handle_generic_action<S: CanvasState>(
|
|||||||
let max_pos = current_input.len();
|
let max_pos = current_input.len();
|
||||||
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
|
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
|
||||||
}
|
}
|
||||||
Ok("".to_string())
|
Ok(ActionResult::success())
|
||||||
}
|
}
|
||||||
|
|
||||||
"move_down" => {
|
CanvasAction::MoveDown => {
|
||||||
let num_fields = state.fields().len();
|
let num_fields = state.fields().len();
|
||||||
if num_fields > 0 {
|
if num_fields > 0 {
|
||||||
let new_field = (state.current_field() + 1).min(num_fields - 1);
|
let new_field = (state.current_field() + 1).min(num_fields - 1);
|
||||||
@@ -196,24 +226,24 @@ async fn handle_generic_action<S: CanvasState>(
|
|||||||
let max_pos = current_input.len();
|
let max_pos = current_input.len();
|
||||||
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
|
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
|
||||||
}
|
}
|
||||||
Ok("".to_string())
|
Ok(ActionResult::success())
|
||||||
}
|
}
|
||||||
|
|
||||||
"move_line_start" => {
|
CanvasAction::MoveLineStart => {
|
||||||
state.set_current_cursor_pos(0);
|
state.set_current_cursor_pos(0);
|
||||||
*ideal_cursor_column = 0;
|
*ideal_cursor_column = 0;
|
||||||
Ok("".to_string())
|
Ok(ActionResult::success())
|
||||||
}
|
}
|
||||||
|
|
||||||
"move_line_end" => {
|
CanvasAction::MoveLineEnd => {
|
||||||
let current_input = state.get_current_input();
|
let current_input = state.get_current_input();
|
||||||
let new_pos = current_input.len();
|
let new_pos = current_input.len();
|
||||||
state.set_current_cursor_pos(new_pos);
|
state.set_current_cursor_pos(new_pos);
|
||||||
*ideal_cursor_column = new_pos;
|
*ideal_cursor_column = new_pos;
|
||||||
Ok("".to_string())
|
Ok(ActionResult::success())
|
||||||
}
|
}
|
||||||
|
|
||||||
"move_first_line" => {
|
CanvasAction::MoveFirstLine => {
|
||||||
let num_fields = state.fields().len();
|
let num_fields = state.fields().len();
|
||||||
if num_fields > 0 {
|
if num_fields > 0 {
|
||||||
state.set_current_field(0);
|
state.set_current_field(0);
|
||||||
@@ -221,10 +251,10 @@ async fn handle_generic_action<S: CanvasState>(
|
|||||||
let max_pos = current_input.len();
|
let max_pos = current_input.len();
|
||||||
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
|
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
|
||||||
}
|
}
|
||||||
Ok("Moved to first field".to_string())
|
Ok(ActionResult::success_with_message("Moved to first field"))
|
||||||
}
|
}
|
||||||
|
|
||||||
"move_last_line" => {
|
CanvasAction::MoveLastLine => {
|
||||||
let num_fields = state.fields().len();
|
let num_fields = state.fields().len();
|
||||||
if num_fields > 0 {
|
if num_fields > 0 {
|
||||||
let new_field = num_fields - 1;
|
let new_field = num_fields - 1;
|
||||||
@@ -233,10 +263,10 @@ async fn handle_generic_action<S: CanvasState>(
|
|||||||
let max_pos = current_input.len();
|
let max_pos = current_input.len();
|
||||||
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
|
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
|
||||||
}
|
}
|
||||||
Ok("Moved to last field".to_string())
|
Ok(ActionResult::success_with_message("Moved to last field"))
|
||||||
}
|
}
|
||||||
|
|
||||||
"move_word_next" => {
|
CanvasAction::MoveWordNext => {
|
||||||
let current_input = state.get_current_input();
|
let current_input = state.get_current_input();
|
||||||
if !current_input.is_empty() {
|
if !current_input.is_empty() {
|
||||||
let new_pos = find_next_word_start(current_input, state.current_cursor_pos());
|
let new_pos = find_next_word_start(current_input, state.current_cursor_pos());
|
||||||
@@ -244,10 +274,10 @@ async fn handle_generic_action<S: CanvasState>(
|
|||||||
state.set_current_cursor_pos(final_pos);
|
state.set_current_cursor_pos(final_pos);
|
||||||
*ideal_cursor_column = final_pos;
|
*ideal_cursor_column = final_pos;
|
||||||
}
|
}
|
||||||
Ok("".to_string())
|
Ok(ActionResult::success())
|
||||||
}
|
}
|
||||||
|
|
||||||
"move_word_end" => {
|
CanvasAction::MoveWordEnd => {
|
||||||
let current_input = state.get_current_input();
|
let current_input = state.get_current_input();
|
||||||
if !current_input.is_empty() {
|
if !current_input.is_empty() {
|
||||||
let current_pos = state.current_cursor_pos();
|
let current_pos = state.current_cursor_pos();
|
||||||
@@ -264,34 +294,43 @@ async fn handle_generic_action<S: CanvasState>(
|
|||||||
state.set_current_cursor_pos(clamped_pos);
|
state.set_current_cursor_pos(clamped_pos);
|
||||||
*ideal_cursor_column = clamped_pos;
|
*ideal_cursor_column = clamped_pos;
|
||||||
}
|
}
|
||||||
Ok("".to_string())
|
Ok(ActionResult::success())
|
||||||
}
|
}
|
||||||
|
|
||||||
"move_word_prev" => {
|
CanvasAction::MoveWordPrev => {
|
||||||
let current_input = state.get_current_input();
|
let current_input = state.get_current_input();
|
||||||
if !current_input.is_empty() {
|
if !current_input.is_empty() {
|
||||||
let new_pos = find_prev_word_start(current_input, state.current_cursor_pos());
|
let new_pos = find_prev_word_start(current_input, state.current_cursor_pos());
|
||||||
state.set_current_cursor_pos(new_pos);
|
state.set_current_cursor_pos(new_pos);
|
||||||
*ideal_cursor_column = new_pos;
|
*ideal_cursor_column = new_pos;
|
||||||
}
|
}
|
||||||
Ok("".to_string())
|
Ok(ActionResult::success())
|
||||||
}
|
}
|
||||||
|
|
||||||
"move_word_end_prev" => {
|
CanvasAction::MoveWordEndPrev => {
|
||||||
let current_input = state.get_current_input();
|
let current_input = state.get_current_input();
|
||||||
if !current_input.is_empty() {
|
if !current_input.is_empty() {
|
||||||
let new_pos = find_prev_word_end(current_input, state.current_cursor_pos());
|
let new_pos = find_prev_word_end(current_input, state.current_cursor_pos());
|
||||||
state.set_current_cursor_pos(new_pos);
|
state.set_current_cursor_pos(new_pos);
|
||||||
*ideal_cursor_column = new_pos;
|
*ideal_cursor_column = new_pos;
|
||||||
}
|
}
|
||||||
Ok("Moved to previous word end".to_string())
|
Ok(ActionResult::success_with_message("Moved to previous word end"))
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => Ok(format!("Unknown or unhandled edit action: {}", action)),
|
CanvasAction::Custom(action_str) => {
|
||||||
|
Ok(ActionResult::error(format!("Unknown or unhandled custom action: {}", action_str)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suggestion actions should have been handled above
|
||||||
|
CanvasAction::SuggestionUp | CanvasAction::SuggestionDown |
|
||||||
|
CanvasAction::SelectSuggestion | CanvasAction::ExitSuggestions => {
|
||||||
|
Ok(ActionResult::error("Suggestion action not handled properly"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Word movement helper functions
|
// Word movement helper functions
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
enum CharType {
|
enum CharType {
|
||||||
Whitespace,
|
Whitespace,
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
// canvas/src/actions/mod.rs
|
// canvas/src/actions/mod.rs
|
||||||
|
|
||||||
|
pub mod types;
|
||||||
pub mod edit;
|
pub mod edit;
|
||||||
|
|
||||||
|
// Re-export the main types for convenience
|
||||||
|
pub use types::{CanvasAction, ActionResult};
|
||||||
|
pub use edit::{execute_canvas_action, execute_edit_action};
|
||||||
|
|||||||
225
canvas/src/actions/types.rs
Normal file
225
canvas/src/actions/types.rs
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
// canvas/src/actions/types.rs
|
||||||
|
|
||||||
|
use crossterm::event::KeyCode;
|
||||||
|
|
||||||
|
/// All possible canvas actions, type-safe and exhaustive
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum CanvasAction {
|
||||||
|
// Character input
|
||||||
|
InsertChar(char),
|
||||||
|
|
||||||
|
// Deletion
|
||||||
|
DeleteBackward,
|
||||||
|
DeleteForward,
|
||||||
|
|
||||||
|
// Basic cursor movement
|
||||||
|
MoveLeft,
|
||||||
|
MoveRight,
|
||||||
|
MoveUp,
|
||||||
|
MoveDown,
|
||||||
|
|
||||||
|
// Line movement
|
||||||
|
MoveLineStart,
|
||||||
|
MoveLineEnd,
|
||||||
|
MoveFirstLine,
|
||||||
|
MoveLastLine,
|
||||||
|
|
||||||
|
// Word movement
|
||||||
|
MoveWordNext,
|
||||||
|
MoveWordEnd,
|
||||||
|
MoveWordPrev,
|
||||||
|
MoveWordEndPrev,
|
||||||
|
|
||||||
|
// Field navigation
|
||||||
|
NextField,
|
||||||
|
PrevField,
|
||||||
|
|
||||||
|
// Suggestions
|
||||||
|
SuggestionUp,
|
||||||
|
SuggestionDown,
|
||||||
|
SelectSuggestion,
|
||||||
|
ExitSuggestions,
|
||||||
|
|
||||||
|
// Custom actions (escape hatch for feature-specific behavior)
|
||||||
|
Custom(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CanvasAction {
|
||||||
|
/// Convert a string action to typed action (for backwards compatibility during migration)
|
||||||
|
pub fn from_string(action: &str) -> Self {
|
||||||
|
match action {
|
||||||
|
"insert_char" => {
|
||||||
|
// This is a bit tricky - we need the char from context
|
||||||
|
// For now, we'll use Custom until we refactor the call sites
|
||||||
|
Self::Custom(action.to_string())
|
||||||
|
}
|
||||||
|
"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,
|
||||||
|
"suggestion_up" => Self::SuggestionUp,
|
||||||
|
"suggestion_down" => Self::SuggestionDown,
|
||||||
|
"select_suggestion" => Self::SelectSuggestion,
|
||||||
|
"exit_suggestions" => Self::ExitSuggestions,
|
||||||
|
_ => Self::Custom(action.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get string representation (for logging, debugging)
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Self::InsertChar(_) => "insert_char",
|
||||||
|
Self::DeleteBackward => "delete_char_backward",
|
||||||
|
Self::DeleteForward => "delete_char_forward",
|
||||||
|
Self::MoveLeft => "move_left",
|
||||||
|
Self::MoveRight => "move_right",
|
||||||
|
Self::MoveUp => "move_up",
|
||||||
|
Self::MoveDown => "move_down",
|
||||||
|
Self::MoveLineStart => "move_line_start",
|
||||||
|
Self::MoveLineEnd => "move_line_end",
|
||||||
|
Self::MoveFirstLine => "move_first_line",
|
||||||
|
Self::MoveLastLine => "move_last_line",
|
||||||
|
Self::MoveWordNext => "move_word_next",
|
||||||
|
Self::MoveWordEnd => "move_word_end",
|
||||||
|
Self::MoveWordPrev => "move_word_prev",
|
||||||
|
Self::MoveWordEndPrev => "move_word_end_prev",
|
||||||
|
Self::NextField => "next_field",
|
||||||
|
Self::PrevField => "prev_field",
|
||||||
|
Self::SuggestionUp => "suggestion_up",
|
||||||
|
Self::SuggestionDown => "suggestion_down",
|
||||||
|
Self::SelectSuggestion => "select_suggestion",
|
||||||
|
Self::ExitSuggestions => "exit_suggestions",
|
||||||
|
Self::Custom(s) => s,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create action from KeyCode for common cases
|
||||||
|
pub fn from_key(key: KeyCode) -> Option<Self> {
|
||||||
|
match key {
|
||||||
|
KeyCode::Char(c) => Some(Self::InsertChar(c)),
|
||||||
|
KeyCode::Backspace => Some(Self::DeleteBackward),
|
||||||
|
KeyCode::Delete => Some(Self::DeleteForward),
|
||||||
|
KeyCode::Left => Some(Self::MoveLeft),
|
||||||
|
KeyCode::Right => Some(Self::MoveRight),
|
||||||
|
KeyCode::Up => Some(Self::MoveUp),
|
||||||
|
KeyCode::Down => Some(Self::MoveDown),
|
||||||
|
KeyCode::Home => Some(Self::MoveLineStart),
|
||||||
|
KeyCode::End => Some(Self::MoveLineEnd),
|
||||||
|
KeyCode::Tab => Some(Self::NextField),
|
||||||
|
KeyCode::BackTab => Some(Self::PrevField),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if this action modifies content
|
||||||
|
pub fn is_modifying(&self) -> bool {
|
||||||
|
matches!(self,
|
||||||
|
Self::InsertChar(_) |
|
||||||
|
Self::DeleteBackward |
|
||||||
|
Self::DeleteForward |
|
||||||
|
Self::SelectSuggestion
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if this action moves the cursor
|
||||||
|
pub fn is_movement(&self) -> bool {
|
||||||
|
matches!(self,
|
||||||
|
Self::MoveLeft | Self::MoveRight | Self::MoveUp | Self::MoveDown |
|
||||||
|
Self::MoveLineStart | Self::MoveLineEnd | Self::MoveFirstLine | Self::MoveLastLine |
|
||||||
|
Self::MoveWordNext | Self::MoveWordEnd | Self::MoveWordPrev | Self::MoveWordEndPrev |
|
||||||
|
Self::NextField | Self::PrevField
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if this is a suggestion-related action
|
||||||
|
pub fn is_suggestion(&self) -> bool {
|
||||||
|
matches!(self,
|
||||||
|
Self::SuggestionUp | Self::SuggestionDown |
|
||||||
|
Self::SelectSuggestion | Self::ExitSuggestions
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result of executing a canvas action
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum ActionResult {
|
||||||
|
/// Action completed successfully, optional message for user feedback
|
||||||
|
Success(Option<String>),
|
||||||
|
/// Action was handled by custom feature logic
|
||||||
|
HandledByFeature(String),
|
||||||
|
/// Action requires additional context or cannot be performed
|
||||||
|
RequiresContext(String),
|
||||||
|
/// Action failed with error message
|
||||||
|
Error(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActionResult {
|
||||||
|
pub fn success() -> Self {
|
||||||
|
Self::Success(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn success_with_message(msg: impl Into<String>) -> Self {
|
||||||
|
Self::Success(Some(msg.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn error(msg: impl Into<String>) -> Self {
|
||||||
|
Self::Error(msg.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_success(&self) -> bool {
|
||||||
|
matches!(self, Self::Success(_) | Self::HandledByFeature(_))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn message(&self) -> Option<&str> {
|
||||||
|
match self {
|
||||||
|
Self::Success(msg) => msg.as_deref(),
|
||||||
|
Self::HandledByFeature(msg) => Some(msg),
|
||||||
|
Self::RequiresContext(msg) => Some(msg),
|
||||||
|
Self::Error(msg) => Some(msg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_action_from_string() {
|
||||||
|
assert_eq!(CanvasAction::from_string("move_left"), CanvasAction::MoveLeft);
|
||||||
|
assert_eq!(CanvasAction::from_string("delete_char_backward"), CanvasAction::DeleteBackward);
|
||||||
|
assert_eq!(CanvasAction::from_string("unknown"), CanvasAction::Custom("unknown".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_action_from_key() {
|
||||||
|
assert_eq!(CanvasAction::from_key(KeyCode::Char('a')), Some(CanvasAction::InsertChar('a')));
|
||||||
|
assert_eq!(CanvasAction::from_key(KeyCode::Left), Some(CanvasAction::MoveLeft));
|
||||||
|
assert_eq!(CanvasAction::from_key(KeyCode::Backspace), Some(CanvasAction::DeleteBackward));
|
||||||
|
assert_eq!(CanvasAction::from_key(KeyCode::F(1)), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_action_properties() {
|
||||||
|
assert!(CanvasAction::InsertChar('a').is_modifying());
|
||||||
|
assert!(!CanvasAction::MoveLeft.is_modifying());
|
||||||
|
|
||||||
|
assert!(CanvasAction::MoveLeft.is_movement());
|
||||||
|
assert!(!CanvasAction::InsertChar('a').is_movement());
|
||||||
|
|
||||||
|
assert!(CanvasAction::SuggestionUp.is_suggestion());
|
||||||
|
assert!(!CanvasAction::MoveLeft.is_suggestion());
|
||||||
|
}
|
||||||
|
}
|
||||||
180
canvas/src/dispatcher.rs
Normal file
180
canvas/src/dispatcher.rs
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
// canvas/src/dispatcher.rs
|
||||||
|
|
||||||
|
use crate::state::CanvasState;
|
||||||
|
use crate::actions::{CanvasAction, ActionResult, execute_canvas_action};
|
||||||
|
|
||||||
|
/// High-level action dispatcher that coordinates between different action types
|
||||||
|
pub struct ActionDispatcher;
|
||||||
|
|
||||||
|
impl ActionDispatcher {
|
||||||
|
/// Dispatch any action to the appropriate handler
|
||||||
|
pub async fn dispatch<S: CanvasState>(
|
||||||
|
action: CanvasAction,
|
||||||
|
state: &mut S,
|
||||||
|
ideal_cursor_column: &mut usize,
|
||||||
|
) -> anyhow::Result<ActionResult> {
|
||||||
|
execute_canvas_action(action, state, ideal_cursor_column).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Quick action dispatch from KeyCode
|
||||||
|
pub async fn dispatch_key<S: CanvasState>(
|
||||||
|
key: crossterm::event::KeyCode,
|
||||||
|
state: &mut S,
|
||||||
|
ideal_cursor_column: &mut usize,
|
||||||
|
) -> anyhow::Result<Option<ActionResult>> {
|
||||||
|
if let Some(action) = CanvasAction::from_key(key) {
|
||||||
|
let result = Self::dispatch(action, state, ideal_cursor_column).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(); // Check success before moving
|
||||||
|
results.push(result);
|
||||||
|
|
||||||
|
// Stop on first error
|
||||||
|
if !is_success {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
// canvas/src/lib.rs
|
// canvas/src/lib.rs
|
||||||
|
|
||||||
//! Canvas - A reusable text editing and form canvas system
|
//! Canvas - A reusable text editing and form canvas system
|
||||||
//!
|
//!
|
||||||
//! This crate provides a generic canvas abstraction for building text-based interfaces
|
//! This crate provides a generic canvas abstraction for building text-based interfaces
|
||||||
@@ -9,19 +8,25 @@ pub mod state;
|
|||||||
pub mod actions;
|
pub mod actions;
|
||||||
pub mod modes;
|
pub mod modes;
|
||||||
pub mod suggestions;
|
pub mod suggestions;
|
||||||
|
pub mod dispatcher;
|
||||||
|
|
||||||
// Re-export the main types for easy use
|
// Re-export the main types for easy use
|
||||||
pub use state::{CanvasState, ActionContext};
|
pub use state::{CanvasState, ActionContext};
|
||||||
pub use actions::edit::execute_edit_action;
|
pub use actions::{CanvasAction, ActionResult, execute_edit_action, execute_canvas_action};
|
||||||
pub use modes::{AppMode, ModeManager, HighlightState};
|
pub use modes::{AppMode, ModeManager, HighlightState};
|
||||||
pub use suggestions::SuggestionState;
|
pub use suggestions::SuggestionState;
|
||||||
|
pub use dispatcher::ActionDispatcher;
|
||||||
|
|
||||||
// High-level convenience API
|
// High-level convenience API
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
pub use crate::{
|
pub use crate::{
|
||||||
CanvasState,
|
CanvasState,
|
||||||
ActionContext,
|
ActionContext,
|
||||||
|
CanvasAction,
|
||||||
|
ActionResult,
|
||||||
execute_edit_action,
|
execute_edit_action,
|
||||||
|
execute_canvas_action,
|
||||||
|
ActionDispatcher,
|
||||||
AppMode,
|
AppMode,
|
||||||
ModeManager,
|
ModeManager,
|
||||||
HighlightState,
|
HighlightState,
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
// canvas/src/state.rs
|
// canvas/src/state.rs
|
||||||
|
|
||||||
|
use crate::actions::CanvasAction;
|
||||||
|
|
||||||
/// Context passed to feature-specific action handlers
|
/// Context passed to feature-specific action handlers
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ActionContext {
|
pub struct ActionContext {
|
||||||
pub key_code: Option<crossterm::event::KeyCode>,
|
pub key_code: Option<crossterm::event::KeyCode>, // Kept for backwards compatibility
|
||||||
pub ideal_cursor_column: usize,
|
pub ideal_cursor_column: usize,
|
||||||
pub current_input: String,
|
pub current_input: String,
|
||||||
pub current_field: usize,
|
pub current_field: usize,
|
||||||
@@ -46,11 +48,28 @@ pub trait CanvasState {
|
|||||||
// Default: no-op (override if you support suggestions)
|
// Default: no-op (override if you support suggestions)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Feature-specific action handling ---
|
// --- Feature-specific action handling (NEW: Type-safe) ---
|
||||||
fn handle_feature_action(&mut self, action: &str, context: &ActionContext) -> Option<String> {
|
fn handle_feature_action(&mut self, _action: &CanvasAction, _context: &ActionContext) -> Option<String> {
|
||||||
None // Default: no feature-specific handling
|
None // Default: no feature-specific handling
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Legacy string-based action handling (for backwards compatibility) ---
|
||||||
|
fn handle_feature_action_legacy(&mut self, action: &str, context: &ActionContext) -> Option<String> {
|
||||||
|
// Convert string to typed action and delegate
|
||||||
|
let typed_action = match action {
|
||||||
|
"insert_char" => {
|
||||||
|
// This is tricky - we need the char from the KeyCode in context
|
||||||
|
if let Some(crossterm::event::KeyCode::Char(c)) = context.key_code {
|
||||||
|
CanvasAction::InsertChar(c)
|
||||||
|
} else {
|
||||||
|
CanvasAction::Custom(action.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => CanvasAction::from_string(action),
|
||||||
|
};
|
||||||
|
self.handle_feature_action(&typed_action, context)
|
||||||
|
}
|
||||||
|
|
||||||
// --- Display Overrides (for links, computed values, etc.) ---
|
// --- Display Overrides (for links, computed values, etc.) ---
|
||||||
fn get_display_value_for_field(&self, index: usize) -> &str {
|
fn get_display_value_for_field(&self, index: usize) -> &str {
|
||||||
self.inputs()
|
self.inputs()
|
||||||
|
|||||||
Reference in New Issue
Block a user