diff --git a/canvas/src/canvas/actions/edit.rs b/canvas/src/canvas/actions/edit.rs index 8a0cc99..ab2682d 100644 --- a/canvas/src/canvas/actions/edit.rs +++ b/canvas/src/canvas/actions/edit.rs @@ -1,17 +1,37 @@ -// canvas/src/canvas/actions/edit.rs +// src/canvas/actions/edit.rs +// COMPATIBILITY LAYER - maintains old API while using new handler system use crate::canvas::state::{CanvasState, ActionContext}; use crate::canvas::actions::types::{CanvasAction, ActionResult}; use crate::config::CanvasConfig; use anyhow::Result; -/// Execute a typed canvas action on any CanvasState implementation +/// BACKWARD COMPATIBILITY: Execute a typed canvas action on any CanvasState implementation +/// This maintains the old API while routing to the new mode-aware system pub async fn execute_canvas_action( action: CanvasAction, state: &mut S, ideal_cursor_column: &mut usize, config: Option<&CanvasConfig>, ) -> Result { + // Route to new dispatcher system + crate::dispatcher::ActionDispatcher::dispatch_with_config( + action, + state, + ideal_cursor_column, + config, + ).await +} + +/// BACKWARD COMPATIBILITY: Handle core canvas actions with full type safety +/// This function is kept for backward compatibility with autocomplete and other modules +pub async fn handle_generic_canvas_action( + action: CanvasAction, + state: &mut S, + ideal_cursor_column: &mut usize, + config: Option<&CanvasConfig>, +) -> Result { + // Check for feature-specific handling first let context = ActionContext { key_code: None, ideal_cursor_column: *ideal_cursor_column, @@ -23,231 +43,20 @@ pub async fn execute_canvas_action( return Ok(ActionResult::HandledByFeature(result)); } - handle_generic_canvas_action(action, state, ideal_cursor_column, config).await -} - -/// Handle core canvas actions with full type safety -pub async fn handle_generic_canvas_action( - action: CanvasAction, - state: &mut S, - ideal_cursor_column: &mut usize, - config: Option<&CanvasConfig>, -) -> Result { - match action { - CanvasAction::InsertChar(c) => { - let cursor_pos = state.current_cursor_pos(); - let input = state.get_current_input_mut(); - input.insert(cursor_pos, c); - state.set_current_cursor_pos(cursor_pos + 1); - state.set_has_unsaved_changes(true); - *ideal_cursor_column = cursor_pos + 1; - Ok(ActionResult::success()) + // Route to appropriate mode handler based on current mode + match state.current_mode() { + crate::canvas::modes::AppMode::Edit => { + crate::canvas::actions::handlers::handle_edit_action(action, state, ideal_cursor_column, config).await } - - CanvasAction::NextField | CanvasAction::PrevField => { - let old_field = state.current_field(); - let total_fields = state.fields().len(); - - // Perform field navigation - let new_field = match action { - CanvasAction::NextField => { - if config.map_or(true, |c| c.behavior.wrap_around_fields) { - (old_field + 1) % total_fields - } else { - (old_field + 1).min(total_fields - 1) - } - } - CanvasAction::PrevField => { - if config.map_or(true, |c| c.behavior.wrap_around_fields) { - if old_field == 0 { total_fields - 1 } else { old_field - 1 } - } else { - old_field.saturating_sub(1) - } - } - _ => unreachable!(), - }; - - state.set_current_field(new_field); - *ideal_cursor_column = state.current_cursor_pos(); - Ok(ActionResult::success()) + crate::canvas::modes::AppMode::ReadOnly => { + crate::canvas::actions::handlers::handle_readonly_action(action, state, ideal_cursor_column, config).await } - - CanvasAction::DeleteBackward => { - let cursor_pos = state.current_cursor_pos(); - if cursor_pos > 0 { - let input = state.get_current_input_mut(); - input.remove(cursor_pos - 1); - state.set_current_cursor_pos(cursor_pos - 1); - state.set_has_unsaved_changes(true); - *ideal_cursor_column = cursor_pos - 1; - } - Ok(ActionResult::success()) + crate::canvas::modes::AppMode::Highlight => { + crate::canvas::actions::handlers::handle_highlight_action(action, state, ideal_cursor_column, config).await } - - CanvasAction::DeleteForward => { - let cursor_pos = state.current_cursor_pos(); - let input = state.get_current_input_mut(); - if cursor_pos < input.len() { - input.remove(cursor_pos); - state.set_has_unsaved_changes(true); - } - Ok(ActionResult::success()) + crate::canvas::modes::AppMode::General | crate::canvas::modes::AppMode::Command => { + // These modes might not handle canvas actions directly + Ok(ActionResult::success_with_message("Mode does not handle canvas actions")) } - - CanvasAction::MoveLeft => { - let cursor_pos = state.current_cursor_pos(); - if cursor_pos > 0 { - state.set_current_cursor_pos(cursor_pos - 1); - *ideal_cursor_column = cursor_pos - 1; - } - Ok(ActionResult::success()) - } - - CanvasAction::MoveRight => { - let cursor_pos = state.current_cursor_pos(); - let current_input = state.get_current_input(); - if cursor_pos < current_input.len() { - state.set_current_cursor_pos(cursor_pos + 1); - *ideal_cursor_column = cursor_pos + 1; - } - Ok(ActionResult::success()) - } - - CanvasAction::MoveLineStart => { - state.set_current_cursor_pos(0); - *ideal_cursor_column = 0; - Ok(ActionResult::success()) - } - - CanvasAction::MoveLineEnd => { - let end_pos = state.get_current_input().len(); - state.set_current_cursor_pos(end_pos); - *ideal_cursor_column = end_pos; - Ok(ActionResult::success()) - } - - CanvasAction::MoveUp => { - // For single-line fields, move to previous field - let current_field = state.current_field(); - if current_field > 0 { - state.set_current_field(current_field - 1); - *ideal_cursor_column = state.current_cursor_pos(); - } - Ok(ActionResult::success()) - } - - CanvasAction::MoveDown => { - // For single-line fields, move to next field - let current_field = state.current_field(); - let total_fields = state.fields().len(); - if current_field < total_fields - 1 { - state.set_current_field(current_field + 1); - *ideal_cursor_column = state.current_cursor_pos(); - } - Ok(ActionResult::success()) - } - - CanvasAction::MoveFirstLine => { - state.set_current_field(0); - state.set_current_cursor_pos(0); - *ideal_cursor_column = 0; - Ok(ActionResult::success()) - } - - CanvasAction::MoveLastLine => { - let last_field = state.fields().len() - 1; - state.set_current_field(last_field); - let end_pos = state.get_current_input().len(); - state.set_current_cursor_pos(end_pos); - *ideal_cursor_column = end_pos; - Ok(ActionResult::success()) - } - - CanvasAction::MoveWordNext => { - let current_input = state.get_current_input(); - if !current_input.is_empty() { - let new_pos = find_next_word_start(current_input, state.current_cursor_pos()); - state.set_current_cursor_pos(new_pos); - *ideal_cursor_column = new_pos; - } - Ok(ActionResult::success()) - } - - CanvasAction::MoveWordEnd => { - let current_input = state.get_current_input(); - if !current_input.is_empty() { - let new_pos = find_word_end(current_input, state.current_cursor_pos()); - state.set_current_cursor_pos(new_pos); - *ideal_cursor_column = new_pos; - } - Ok(ActionResult::success()) - } - - CanvasAction::MoveWordPrev => { - let current_input = state.get_current_input(); - if !current_input.is_empty() { - let new_pos = find_prev_word_start(current_input, state.current_cursor_pos()); - state.set_current_cursor_pos(new_pos); - *ideal_cursor_column = new_pos; - } - Ok(ActionResult::success()) - } - - CanvasAction::Custom(action_str) => { - Ok(ActionResult::success_with_message(&format!("Custom action: {}", action_str))) - } - - _ => Ok(ActionResult::success_with_message("Action not implemented")), } } - -// Helper functions for word navigation -fn find_next_word_start(text: &str, cursor_pos: usize) -> usize { - let chars: Vec = text.chars().collect(); - let mut pos = cursor_pos; - - // Skip current word - while pos < chars.len() && chars[pos].is_alphanumeric() { - pos += 1; - } - - // Skip whitespace - while pos < chars.len() && chars[pos].is_whitespace() { - pos += 1; - } - - pos -} - -fn find_word_end(text: &str, cursor_pos: usize) -> usize { - let chars: Vec = text.chars().collect(); - let mut pos = cursor_pos; - - // Move to end of current word - while pos < chars.len() && chars[pos].is_alphanumeric() { - pos += 1; - } - - pos -} - -fn find_prev_word_start(text: &str, cursor_pos: usize) -> usize { - if cursor_pos == 0 { - return 0; - } - - let chars: Vec = text.chars().collect(); - let mut pos = cursor_pos.saturating_sub(1); - - // Skip whitespace - while pos > 0 && chars[pos].is_whitespace() { - pos -= 1; - } - - // Skip to start of word - while pos > 0 && chars[pos - 1].is_alphanumeric() { - pos -= 1; - } - - pos -} diff --git a/canvas/src/canvas/actions/handlers/edit.rs b/canvas/src/canvas/actions/handlers/edit.rs new file mode 100644 index 0000000..cd65e7d --- /dev/null +++ b/canvas/src/canvas/actions/handlers/edit.rs @@ -0,0 +1,203 @@ +// src/canvas/actions/handlers/edit.rs + +use crate::canvas::actions::types::{CanvasAction, ActionResult}; +use crate::canvas::actions::movement::*; +use crate::canvas::state::CanvasState; +use crate::config::CanvasConfig; +use anyhow::Result; + +const FOR_EDIT_MODE: bool = true; // Edit mode flag + +/// Handle actions in edit mode with edit-specific cursor behavior +pub async fn handle_edit_action( + action: CanvasAction, + state: &mut S, + ideal_cursor_column: &mut usize, + config: Option<&CanvasConfig>, +) -> Result { + match action { + CanvasAction::InsertChar(c) => { + let cursor_pos = state.current_cursor_pos(); + let input = state.get_current_input_mut(); + input.insert(cursor_pos, c); + state.set_current_cursor_pos(cursor_pos + 1); + state.set_has_unsaved_changes(true); + *ideal_cursor_column = cursor_pos + 1; + Ok(ActionResult::success()) + } + + CanvasAction::DeleteBackward => { + let cursor_pos = state.current_cursor_pos(); + if cursor_pos > 0 { + let input = state.get_current_input_mut(); + input.remove(cursor_pos - 1); + state.set_current_cursor_pos(cursor_pos - 1); + state.set_has_unsaved_changes(true); + *ideal_cursor_column = cursor_pos - 1; + } + Ok(ActionResult::success()) + } + + CanvasAction::DeleteForward => { + let cursor_pos = state.current_cursor_pos(); + let input = state.get_current_input_mut(); + if cursor_pos < input.len() { + input.remove(cursor_pos); + state.set_has_unsaved_changes(true); + } + Ok(ActionResult::success()) + } + + CanvasAction::MoveLeft => { + let new_pos = move_left(state.current_cursor_pos()); + state.set_current_cursor_pos(new_pos); + *ideal_cursor_column = new_pos; + Ok(ActionResult::success()) + } + + CanvasAction::MoveRight => { + let current_input = state.get_current_input(); + let current_pos = state.current_cursor_pos(); + let new_pos = move_right(current_pos, current_input, FOR_EDIT_MODE); + state.set_current_cursor_pos(new_pos); + *ideal_cursor_column = new_pos; + Ok(ActionResult::success()) + } + + CanvasAction::MoveUp => { + // For single-line fields, move to previous field + let current_field = state.current_field(); + if current_field > 0 { + state.set_current_field(current_field - 1); + let current_input = state.get_current_input(); + let new_pos = safe_cursor_position(current_input, *ideal_cursor_column, FOR_EDIT_MODE); + state.set_current_cursor_pos(new_pos); + } + Ok(ActionResult::success()) + } + + CanvasAction::MoveDown => { + // For single-line fields, move to next field + let current_field = state.current_field(); + let total_fields = state.fields().len(); + if current_field < total_fields - 1 { + state.set_current_field(current_field + 1); + let current_input = state.get_current_input(); + let new_pos = safe_cursor_position(current_input, *ideal_cursor_column, FOR_EDIT_MODE); + state.set_current_cursor_pos(new_pos); + } + Ok(ActionResult::success()) + } + + CanvasAction::MoveLineStart => { + let new_pos = line_start_position(); + state.set_current_cursor_pos(new_pos); + *ideal_cursor_column = new_pos; + Ok(ActionResult::success()) + } + + CanvasAction::MoveLineEnd => { + let current_input = state.get_current_input(); + let new_pos = line_end_position(current_input, FOR_EDIT_MODE); + state.set_current_cursor_pos(new_pos); + *ideal_cursor_column = new_pos; + Ok(ActionResult::success()) + } + + CanvasAction::MoveFirstLine => { + state.set_current_field(0); + let current_input = state.get_current_input(); + let new_pos = safe_cursor_position(current_input, 0, FOR_EDIT_MODE); + state.set_current_cursor_pos(new_pos); + *ideal_cursor_column = new_pos; + Ok(ActionResult::success()) + } + + CanvasAction::MoveLastLine => { + let last_field = state.fields().len() - 1; + state.set_current_field(last_field); + let current_input = state.get_current_input(); + let new_pos = line_end_position(current_input, FOR_EDIT_MODE); + state.set_current_cursor_pos(new_pos); + *ideal_cursor_column = new_pos; + Ok(ActionResult::success()) + } + + CanvasAction::MoveWordNext => { + let current_input = state.get_current_input(); + if !current_input.is_empty() { + let new_pos = find_next_word_start(current_input, state.current_cursor_pos()); + state.set_current_cursor_pos(new_pos); + *ideal_cursor_column = new_pos; + } + Ok(ActionResult::success()) + } + + CanvasAction::MoveWordEnd => { + let current_input = state.get_current_input(); + if !current_input.is_empty() { + let new_pos = find_word_end(current_input, state.current_cursor_pos()); + state.set_current_cursor_pos(new_pos); + *ideal_cursor_column = new_pos; + } + Ok(ActionResult::success()) + } + + CanvasAction::MoveWordPrev => { + let current_input = state.get_current_input(); + if !current_input.is_empty() { + let new_pos = find_prev_word_start(current_input, state.current_cursor_pos()); + state.set_current_cursor_pos(new_pos); + *ideal_cursor_column = new_pos; + } + Ok(ActionResult::success()) + } + + CanvasAction::MoveWordEndPrev => { + let current_input = state.get_current_input(); + if !current_input.is_empty() { + let new_pos = find_prev_word_end(current_input, state.current_cursor_pos()); + state.set_current_cursor_pos(new_pos); + *ideal_cursor_column = new_pos; + } + Ok(ActionResult::success()) + } + + CanvasAction::NextField | CanvasAction::PrevField => { + let current_field = state.current_field(); + let total_fields = state.fields().len(); + + let new_field = match action { + CanvasAction::NextField => { + if config.map_or(true, |c| c.behavior.wrap_around_fields) { + (current_field + 1) % total_fields + } else { + (current_field + 1).min(total_fields - 1) + } + } + CanvasAction::PrevField => { + if config.map_or(true, |c| c.behavior.wrap_around_fields) { + if current_field == 0 { total_fields - 1 } else { current_field - 1 } + } else { + current_field.saturating_sub(1) + } + } + _ => unreachable!(), + }; + + state.set_current_field(new_field); + let current_input = state.get_current_input(); + let new_pos = safe_cursor_position(current_input, *ideal_cursor_column, FOR_EDIT_MODE); + state.set_current_cursor_pos(new_pos); + Ok(ActionResult::success()) + } + + CanvasAction::Custom(action_str) => { + Ok(ActionResult::success_with_message(&format!("Custom edit action: {}", action_str))) + } + + _ => { + Ok(ActionResult::success_with_message("Action not implemented for edit mode")) + } + } +} diff --git a/canvas/src/canvas/actions/handlers/highlight.rs b/canvas/src/canvas/actions/handlers/highlight.rs new file mode 100644 index 0000000..1c850e5 --- /dev/null +++ b/canvas/src/canvas/actions/handlers/highlight.rs @@ -0,0 +1,106 @@ +// src/canvas/actions/handlers/highlight.rs + +use crate::canvas::actions::types::{CanvasAction, ActionResult}; +use crate::canvas::actions::movement::*; +use crate::canvas::state::CanvasState; +use crate::config::CanvasConfig; +use anyhow::Result; + +const FOR_EDIT_MODE: bool = false; // Highlight mode uses read-only cursor behavior + +/// Handle actions in highlight/visual mode +/// TODO: Implement selection logic and highlight-specific behaviors +pub async fn handle_highlight_action( + action: CanvasAction, + state: &mut S, + ideal_cursor_column: &mut usize, + config: Option<&CanvasConfig>, +) -> Result { + match action { + // Movement actions work similar to read-only mode but with selection + CanvasAction::MoveLeft => { + let new_pos = move_left(state.current_cursor_pos()); + state.set_current_cursor_pos(new_pos); + *ideal_cursor_column = new_pos; + // TODO: Update selection range + Ok(ActionResult::success()) + } + + CanvasAction::MoveRight => { + let current_input = state.get_current_input(); + let current_pos = state.current_cursor_pos(); + let new_pos = move_right(current_pos, current_input, FOR_EDIT_MODE); + state.set_current_cursor_pos(new_pos); + *ideal_cursor_column = new_pos; + // TODO: Update selection range + Ok(ActionResult::success()) + } + + CanvasAction::MoveWordNext => { + let current_input = state.get_current_input(); + if !current_input.is_empty() { + let new_pos = find_next_word_start(current_input, state.current_cursor_pos()); + let final_pos = clamp_cursor_position(new_pos, current_input, FOR_EDIT_MODE); + state.set_current_cursor_pos(final_pos); + *ideal_cursor_column = final_pos; + // TODO: Update selection range + } + Ok(ActionResult::success()) + } + + CanvasAction::MoveWordEnd => { + let current_input = state.get_current_input(); + if !current_input.is_empty() { + let new_pos = find_word_end(current_input, state.current_cursor_pos()); + let final_pos = clamp_cursor_position(new_pos, current_input, FOR_EDIT_MODE); + state.set_current_cursor_pos(final_pos); + *ideal_cursor_column = final_pos; + // TODO: Update selection range + } + Ok(ActionResult::success()) + } + + CanvasAction::MoveWordPrev => { + let current_input = state.get_current_input(); + if !current_input.is_empty() { + let new_pos = find_prev_word_start(current_input, state.current_cursor_pos()); + state.set_current_cursor_pos(new_pos); + *ideal_cursor_column = new_pos; + // TODO: Update selection range + } + Ok(ActionResult::success()) + } + + CanvasAction::MoveLineStart => { + let new_pos = line_start_position(); + state.set_current_cursor_pos(new_pos); + *ideal_cursor_column = new_pos; + // TODO: Update selection range + Ok(ActionResult::success()) + } + + CanvasAction::MoveLineEnd => { + let current_input = state.get_current_input(); + let new_pos = line_end_position(current_input, FOR_EDIT_MODE); + state.set_current_cursor_pos(new_pos); + *ideal_cursor_column = new_pos; + // TODO: Update selection range + Ok(ActionResult::success()) + } + + // Highlight mode doesn't handle editing actions + CanvasAction::InsertChar(_) | + CanvasAction::DeleteBackward | + CanvasAction::DeleteForward => { + Ok(ActionResult::success_with_message("Action not available in highlight mode")) + } + + CanvasAction::Custom(action_str) => { + Ok(ActionResult::success_with_message(&format!("Custom highlight action: {}", action_str))) + } + + _ => { + Ok(ActionResult::success_with_message("Action not implemented for highlight mode")) + } + } +} diff --git a/canvas/src/canvas/actions/handlers/mod.rs b/canvas/src/canvas/actions/handlers/mod.rs new file mode 100644 index 0000000..91c0b75 --- /dev/null +++ b/canvas/src/canvas/actions/handlers/mod.rs @@ -0,0 +1,10 @@ +// src/canvas/actions/handlers/mod.rs + +pub mod edit; +pub mod readonly; +pub mod highlight; + +// Re-export handler functions +pub use edit::handle_edit_action; +pub use readonly::handle_readonly_action; +pub use highlight::handle_highlight_action; diff --git a/canvas/src/canvas/actions/handlers/readonly.rs b/canvas/src/canvas/actions/handlers/readonly.rs new file mode 100644 index 0000000..d7d6c4d --- /dev/null +++ b/canvas/src/canvas/actions/handlers/readonly.rs @@ -0,0 +1,193 @@ +// src/canvas/actions/handlers/readonly.rs + +use crate::canvas::actions::types::{CanvasAction, ActionResult}; +use crate::canvas::actions::movement::*; +use crate::canvas::state::CanvasState; +use crate::config::CanvasConfig; +use anyhow::Result; + +const FOR_EDIT_MODE: bool = false; // Read-only mode flag + +/// Handle actions in read-only mode with read-only specific cursor behavior +pub async fn handle_readonly_action( + action: CanvasAction, + state: &mut S, + ideal_cursor_column: &mut usize, + config: Option<&CanvasConfig>, +) -> Result { + match action { + CanvasAction::MoveLeft => { + let new_pos = move_left(state.current_cursor_pos()); + state.set_current_cursor_pos(new_pos); + *ideal_cursor_column = new_pos; + Ok(ActionResult::success()) + } + + CanvasAction::MoveRight => { + let current_input = state.get_current_input(); + let current_pos = state.current_cursor_pos(); + let new_pos = move_right(current_pos, current_input, FOR_EDIT_MODE); + state.set_current_cursor_pos(new_pos); + *ideal_cursor_column = new_pos; + Ok(ActionResult::success()) + } + + CanvasAction::MoveUp => { + let current_field = state.current_field(); + let new_field = current_field.saturating_sub(1); + state.set_current_field(new_field); + + // Apply ideal cursor column with read-only bounds + let current_input = state.get_current_input(); + let new_pos = safe_cursor_position(current_input, *ideal_cursor_column, FOR_EDIT_MODE); + state.set_current_cursor_pos(new_pos); + Ok(ActionResult::success()) + } + + CanvasAction::MoveDown => { + let current_field = state.current_field(); + let total_fields = state.fields().len(); + if total_fields == 0 { + return Ok(ActionResult::success_with_message("No fields to navigate")); + } + + let new_field = (current_field + 1).min(total_fields - 1); + state.set_current_field(new_field); + + // Apply ideal cursor column with read-only bounds + let current_input = state.get_current_input(); + let new_pos = safe_cursor_position(current_input, *ideal_cursor_column, FOR_EDIT_MODE); + state.set_current_cursor_pos(new_pos); + Ok(ActionResult::success()) + } + + CanvasAction::MoveFirstLine => { + let total_fields = state.fields().len(); + if total_fields == 0 { + return Ok(ActionResult::success_with_message("No fields to navigate")); + } + + state.set_current_field(0); + let current_input = state.get_current_input(); + let new_pos = safe_cursor_position(current_input, *ideal_cursor_column, FOR_EDIT_MODE); + state.set_current_cursor_pos(new_pos); + *ideal_cursor_column = new_pos; + Ok(ActionResult::success()) + } + + CanvasAction::MoveLastLine => { + let total_fields = state.fields().len(); + if total_fields == 0 { + return Ok(ActionResult::success_with_message("No fields to navigate")); + } + + let last_field = total_fields - 1; + state.set_current_field(last_field); + let current_input = state.get_current_input(); + let new_pos = safe_cursor_position(current_input, *ideal_cursor_column, FOR_EDIT_MODE); + state.set_current_cursor_pos(new_pos); + *ideal_cursor_column = new_pos; + Ok(ActionResult::success()) + } + + CanvasAction::MoveLineStart => { + let new_pos = line_start_position(); + state.set_current_cursor_pos(new_pos); + *ideal_cursor_column = new_pos; + Ok(ActionResult::success()) + } + + CanvasAction::MoveLineEnd => { + let current_input = state.get_current_input(); + let new_pos = line_end_position(current_input, FOR_EDIT_MODE); + state.set_current_cursor_pos(new_pos); + *ideal_cursor_column = new_pos; + Ok(ActionResult::success()) + } + + CanvasAction::MoveWordNext => { + let current_input = state.get_current_input(); + if !current_input.is_empty() { + let new_pos = find_next_word_start(current_input, state.current_cursor_pos()); + let final_pos = clamp_cursor_position(new_pos, current_input, FOR_EDIT_MODE); + state.set_current_cursor_pos(final_pos); + *ideal_cursor_column = final_pos; + } + Ok(ActionResult::success()) + } + + CanvasAction::MoveWordEnd => { + let current_input = state.get_current_input(); + if !current_input.is_empty() { + let current_pos = state.current_cursor_pos(); + let new_pos = find_word_end(current_input, current_pos); + let final_pos = clamp_cursor_position(new_pos, current_input, FOR_EDIT_MODE); + state.set_current_cursor_pos(final_pos); + *ideal_cursor_column = final_pos; + } + Ok(ActionResult::success()) + } + + CanvasAction::MoveWordPrev => { + let current_input = state.get_current_input(); + if !current_input.is_empty() { + let new_pos = find_prev_word_start(current_input, state.current_cursor_pos()); + state.set_current_cursor_pos(new_pos); + *ideal_cursor_column = new_pos; + } + Ok(ActionResult::success()) + } + + CanvasAction::MoveWordEndPrev => { + let current_input = state.get_current_input(); + if !current_input.is_empty() { + let new_pos = find_prev_word_end(current_input, state.current_cursor_pos()); + state.set_current_cursor_pos(new_pos); + *ideal_cursor_column = new_pos; + } + Ok(ActionResult::success()) + } + + CanvasAction::NextField | CanvasAction::PrevField => { + let current_field = state.current_field(); + let total_fields = state.fields().len(); + + let new_field = match action { + CanvasAction::NextField => { + if config.map_or(true, |c| c.behavior.wrap_around_fields) { + (current_field + 1) % total_fields + } else { + (current_field + 1).min(total_fields - 1) + } + } + CanvasAction::PrevField => { + if config.map_or(true, |c| c.behavior.wrap_around_fields) { + if current_field == 0 { total_fields - 1 } else { current_field - 1 } + } else { + current_field.saturating_sub(1) + } + } + _ => unreachable!(), + }; + + state.set_current_field(new_field); + *ideal_cursor_column = state.current_cursor_pos(); + Ok(ActionResult::success()) + } + + // Read-only mode doesn't handle editing actions + CanvasAction::InsertChar(_) | + CanvasAction::DeleteBackward | + CanvasAction::DeleteForward => { + Ok(ActionResult::success_with_message("Action not available in read-only mode")) + } + + CanvasAction::Custom(action_str) => { + Ok(ActionResult::success_with_message(&format!("Custom readonly action: {}", action_str))) + } + + _ => { + Ok(ActionResult::success_with_message("Action not implemented for read-only mode")) + } + } +} diff --git a/canvas/src/canvas/actions/mod.rs b/canvas/src/canvas/actions/mod.rs index 42bf355..9a97642 100644 --- a/canvas/src/canvas/actions/mod.rs +++ b/canvas/src/canvas/actions/mod.rs @@ -1,7 +1,12 @@ -// canvas/src/canvas/actions/mod.rs +// src/canvas/actions/mod.rs + pub mod types; -pub mod edit; +pub mod movement; +pub mod handlers; +pub mod edit; // Compatibility layer // Re-export the main types for convenience pub use types::{CanvasAction, ActionResult}; -pub use edit::execute_canvas_action; + +// Re-export from edit.rs for backward compatibility +pub use edit::{execute_canvas_action, handle_generic_canvas_action}; diff --git a/canvas/src/canvas/actions/movement/char.rs b/canvas/src/canvas/actions/movement/char.rs new file mode 100644 index 0000000..8898c01 --- /dev/null +++ b/canvas/src/canvas/actions/movement/char.rs @@ -0,0 +1,49 @@ +// src/canvas/actions/movement/char.rs + +/// Calculate new position when moving left +pub fn move_left(current_pos: usize) -> usize { + current_pos.saturating_sub(1) +} + +/// Calculate new position when moving right +pub fn move_right(current_pos: usize, text: &str, for_edit_mode: bool) -> usize { + if text.is_empty() { + return current_pos; + } + + if for_edit_mode { + // Edit mode: can move past end of text + (current_pos + 1).min(text.len()) + } else { + // Read-only/highlight mode: stays within text bounds + if current_pos < text.len().saturating_sub(1) { + current_pos + 1 + } else { + current_pos + } + } +} + +/// Check if cursor position is valid for the given mode +pub fn is_valid_cursor_position(pos: usize, text: &str, for_edit_mode: bool) -> bool { + if text.is_empty() { + return pos == 0; + } + + if for_edit_mode { + pos <= text.len() + } else { + pos < text.len() + } +} + +/// Clamp cursor position to valid bounds for the given mode +pub fn clamp_cursor_position(pos: usize, text: &str, for_edit_mode: bool) -> usize { + if text.is_empty() { + 0 + } else if for_edit_mode { + pos.min(text.len()) + } else { + pos.min(text.len().saturating_sub(1)) + } +} diff --git a/canvas/src/canvas/actions/movement/line.rs b/canvas/src/canvas/actions/movement/line.rs new file mode 100644 index 0000000..4aceaee --- /dev/null +++ b/canvas/src/canvas/actions/movement/line.rs @@ -0,0 +1,32 @@ +// src/canvas/actions/movement/line.rs + +/// Calculate cursor position for line start +pub fn line_start_position() -> usize { + 0 +} + +/// Calculate cursor position for line end +pub fn line_end_position(text: &str, for_edit_mode: bool) -> usize { + if text.is_empty() { + 0 + } else if for_edit_mode { + // Edit mode: cursor can go past end of text + text.len() + } else { + // Read-only/highlight mode: cursor stays on last character + text.len().saturating_sub(1) + } +} + +/// Calculate safe cursor position when switching fields +pub fn safe_cursor_position(text: &str, ideal_column: usize, for_edit_mode: bool) -> usize { + if text.is_empty() { + 0 + } else if for_edit_mode { + // Edit mode: cursor can go past end + ideal_column.min(text.len()) + } else { + // Read-only/highlight mode: cursor stays within text + ideal_column.min(text.len().saturating_sub(1)) + } +} diff --git a/canvas/src/canvas/actions/movement/mod.rs b/canvas/src/canvas/actions/movement/mod.rs new file mode 100644 index 0000000..c6f5600 --- /dev/null +++ b/canvas/src/canvas/actions/movement/mod.rs @@ -0,0 +1,10 @@ +// src/canvas/actions/movement/mod.rs + +pub mod word; +pub mod line; +pub mod char; + +// Re-export commonly used functions +pub use word::{find_next_word_start, find_word_end, find_prev_word_start, find_prev_word_end}; +pub use line::{line_start_position, line_end_position, safe_cursor_position}; +pub use char::{move_left, move_right, is_valid_cursor_position, clamp_cursor_position}; diff --git a/canvas/src/canvas/actions/movement/word.rs b/canvas/src/canvas/actions/movement/word.rs new file mode 100644 index 0000000..1b82210 --- /dev/null +++ b/canvas/src/canvas/actions/movement/word.rs @@ -0,0 +1,146 @@ +// src/canvas/actions/movement/word.rs + +#[derive(PartialEq)] +enum CharType { + Whitespace, + Alphanumeric, + Punctuation, +} + +fn get_char_type(c: char) -> CharType { + if c.is_whitespace() { + CharType::Whitespace + } else if c.is_alphanumeric() { + CharType::Alphanumeric + } else { + CharType::Punctuation + } +} + +/// Find the start of the next word from the current position +pub fn find_next_word_start(text: &str, current_pos: usize) -> usize { + let chars: Vec = text.chars().collect(); + if chars.is_empty() { + return 0; + } + let current_pos = current_pos.min(chars.len()); + + if current_pos == chars.len() { + return current_pos; + } + + let mut pos = current_pos; + let initial_type = get_char_type(chars[pos]); + + // Skip current word/token + while pos < chars.len() && get_char_type(chars[pos]) == initial_type { + pos += 1; + } + + // Skip whitespace + while pos < chars.len() && get_char_type(chars[pos]) == CharType::Whitespace { + pos += 1; + } + + pos +} + +/// Find the end of the current or next word +pub fn find_word_end(text: &str, current_pos: usize) -> usize { + let chars: Vec = text.chars().collect(); + let len = chars.len(); + if len == 0 { + return 0; + } + + let mut pos = current_pos.min(len - 1); + let current_type = get_char_type(chars[pos]); + + // If we're not on whitespace, move to end of current word + if current_type != CharType::Whitespace { + while pos < len && get_char_type(chars[pos]) == current_type { + pos += 1; + } + return pos.saturating_sub(1); + } + + // If we're on whitespace, find next word and go to its end + pos = find_next_word_start(text, pos); + if pos >= len { + return len.saturating_sub(1); + } + + let word_type = get_char_type(chars[pos]); + while pos < len && get_char_type(chars[pos]) == word_type { + pos += 1; + } + + pos.saturating_sub(1).min(len.saturating_sub(1)) +} + +/// Find the start of the previous word +pub fn find_prev_word_start(text: &str, current_pos: usize) -> usize { + let chars: Vec = text.chars().collect(); + if chars.is_empty() || current_pos == 0 { + return 0; + } + + let mut pos = current_pos.saturating_sub(1); + + // Skip whitespace backwards + while pos > 0 && get_char_type(chars[pos]) == CharType::Whitespace { + pos -= 1; + } + + // Move to start of word + if get_char_type(chars[pos]) != CharType::Whitespace { + let word_type = get_char_type(chars[pos]); + while pos > 0 && get_char_type(chars[pos - 1]) == word_type { + pos -= 1; + } + } + + if pos == 0 && get_char_type(chars[0]) == CharType::Whitespace { + 0 + } else { + pos + } +} + +/// Find the end of the previous word +pub fn find_prev_word_end(text: &str, current_pos: usize) -> usize { + let chars: Vec = text.chars().collect(); + if chars.is_empty() || current_pos == 0 { + return 0; + } + + let mut pos = current_pos.saturating_sub(1); + + // Skip whitespace backwards + while pos > 0 && get_char_type(chars[pos]) == CharType::Whitespace { + pos -= 1; + } + + if pos == 0 && get_char_type(chars[0]) == CharType::Whitespace { + return 0; + } + if pos == 0 && get_char_type(chars[0]) != CharType::Whitespace { + return 0; + } + + let word_type = get_char_type(chars[pos]); + while pos > 0 && get_char_type(chars[pos - 1]) == word_type { + pos -= 1; + } + + // Skip whitespace before this word + while pos > 0 && get_char_type(chars[pos - 1]) == CharType::Whitespace { + pos -= 1; + } + + if pos > 0 { + pos - 1 + } else { + 0 + } +} diff --git a/canvas/src/canvas/actions/types.rs b/canvas/src/canvas/actions/types.rs index 17878af..433a4d5 100644 --- a/canvas/src/canvas/actions/types.rs +++ b/canvas/src/canvas/actions/types.rs @@ -25,6 +25,7 @@ pub enum CanvasAction { MoveWordNext, MoveWordEnd, MoveWordPrev, + MoveWordEndPrev, // Field navigation NextField, @@ -58,6 +59,7 @@ impl CanvasAction { "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, diff --git a/canvas/src/canvas/state.rs b/canvas/src/canvas/state.rs index 29eb075..29915ba 100644 --- a/canvas/src/canvas/state.rs +++ b/canvas/src/canvas/state.rs @@ -1,6 +1,7 @@ -// canvas/src/state.rs +// src/canvas/state.rs use crate::canvas::actions::CanvasAction; +use crate::canvas::modes::AppMode; /// Context passed to feature-specific action handlers #[derive(Debug)] @@ -21,6 +22,9 @@ pub trait CanvasState { fn set_current_field(&mut self, index: usize); fn set_current_cursor_pos(&mut self, pos: usize); + // --- Mode Information --- + fn current_mode(&self) -> AppMode; + // --- Data Access --- fn get_current_input(&self) -> &str; fn get_current_input_mut(&mut self) -> &mut String; @@ -33,7 +37,7 @@ pub trait CanvasState { // --- Feature-specific action handling --- - /// Feature-specific action handling (NEW: Type-safe) + /// Feature-specific action handling (Type-safe) fn handle_feature_action(&mut self, _action: &CanvasAction, _context: &ActionContext) -> Option { None // Default: no feature-specific handling } diff --git a/canvas/src/dispatcher.rs b/canvas/src/dispatcher.rs index fab0622..1b7cbd5 100644 --- a/canvas/src/dispatcher.rs +++ b/canvas/src/dispatcher.rs @@ -1,22 +1,61 @@ -// canvas/src/dispatcher.rs +// src/dispatcher.rs -use crate::canvas::state::CanvasState; -use crate::canvas::actions::{CanvasAction, ActionResult, execute_canvas_action}; +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}; -/// High-level action dispatcher that coordinates between different action types +/// High-level action dispatcher that routes actions to mode-specific handlers pub struct ActionDispatcher; impl ActionDispatcher { - /// Dispatch any action to the appropriate handler + /// Dispatch any action to the appropriate mode handler pub async fn dispatch( action: CanvasAction, state: &mut S, ideal_cursor_column: &mut usize, ) -> anyhow::Result { - // Load config once here instead of threading it everywhere - execute_canvas_action(action, state, ideal_cursor_column, Some(&CanvasConfig::load())).await + 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( + action: CanvasAction, + state: &mut S, + ideal_cursor_column: &mut usize, + config: Option<&CanvasConfig>, + ) -> anyhow::Result { + // 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 @@ -29,10 +68,10 @@ impl ActionDispatcher { has_suggestions: bool, ) -> anyhow::Result> { 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_with_config(action, state, ideal_cursor_column, Some(&config)).await?; Ok(Some(result)) } else { Ok(None)