autotrigger vs manual trigger
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -482,6 +482,8 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tokio-test",
|
"tokio-test",
|
||||||
"toml",
|
"toml",
|
||||||
|
"tracing",
|
||||||
|
"tracing-subscriber",
|
||||||
"unicode-width 0.2.0",
|
"unicode-width 0.2.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ toml = { workspace = true }
|
|||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
unicode-width.workspace = true
|
unicode-width.workspace = true
|
||||||
|
|
||||||
|
tracing = "0.1.41"
|
||||||
|
tracing-subscriber = "0.3.19"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio-test = "0.4.4"
|
tokio-test = "0.4.4"
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
// src/autocomplete/actions.rs
|
// canvas/src/autocomplete/actions.rs
|
||||||
|
|
||||||
use crate::canvas::state::{CanvasState, ActionContext};
|
use crate::canvas::state::{CanvasState, ActionContext};
|
||||||
use crate::autocomplete::state::AutocompleteCanvasState;
|
use crate::autocomplete::state::AutocompleteCanvasState;
|
||||||
use crate::canvas::actions::types::{CanvasAction, ActionResult};
|
use crate::canvas::actions::types::{CanvasAction, ActionResult};
|
||||||
use crate::canvas::actions::edit::handle_generic_canvas_action; // Import the core function
|
use crate::canvas::actions::edit::handle_generic_canvas_action;
|
||||||
|
use crate::config::CanvasConfig;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
/// Version for states that implement rich autocomplete
|
/// Version for states that implement rich autocomplete
|
||||||
@@ -11,6 +12,7 @@ pub async fn execute_canvas_action_with_autocomplete<S: CanvasState + Autocomple
|
|||||||
action: CanvasAction,
|
action: CanvasAction,
|
||||||
state: &mut S,
|
state: &mut S,
|
||||||
ideal_cursor_column: &mut usize,
|
ideal_cursor_column: &mut usize,
|
||||||
|
config: Option<&CanvasConfig>,
|
||||||
) -> Result<ActionResult> {
|
) -> Result<ActionResult> {
|
||||||
// 1. Try feature-specific handler first
|
// 1. Try feature-specific handler first
|
||||||
let context = ActionContext {
|
let context = ActionContext {
|
||||||
@@ -20,31 +22,77 @@ pub async fn execute_canvas_action_with_autocomplete<S: CanvasState + Autocomple
|
|||||||
current_field: state.current_field(),
|
current_field: state.current_field(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(result) = state.handle_feature_action(&action, &context) {
|
if let Some(result) = handle_rich_autocomplete_action(action.clone(), state, &context) {
|
||||||
return Ok(ActionResult::HandledByFeature(result));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Handle rich autocomplete actions
|
|
||||||
if let Some(result) = handle_rich_autocomplete_action(&action, state)? {
|
|
||||||
return Ok(result);
|
return Ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Handle generic canvas actions
|
// 2. Handle generic actions and add auto-trigger logic
|
||||||
handle_generic_canvas_action(action, state, ideal_cursor_column).await
|
let result = handle_generic_canvas_action(action.clone(), state, ideal_cursor_column, config).await?;
|
||||||
|
|
||||||
|
// 3. AUTO-TRIGGER LOGIC: Check if we should activate/deactivate autocomplete
|
||||||
|
if let Some(cfg) = config {
|
||||||
|
println!("{:?}, {}", action, cfg.should_auto_trigger_autocomplete());
|
||||||
|
if cfg.should_auto_trigger_autocomplete() {
|
||||||
|
println!("AUTO-TRIGGER");
|
||||||
|
match action {
|
||||||
|
CanvasAction::InsertChar(_) => {
|
||||||
|
println!("AUTO-T on Ins");
|
||||||
|
let current_field = state.current_field();
|
||||||
|
let current_input = state.get_current_input();
|
||||||
|
|
||||||
|
if state.supports_autocomplete(current_field)
|
||||||
|
&& !state.is_autocomplete_active()
|
||||||
|
&& current_input.len() >= 1
|
||||||
|
{
|
||||||
|
println!("ACT AUTOC");
|
||||||
|
state.activate_autocomplete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CanvasAction::NextField | CanvasAction::PrevField => {
|
||||||
|
println!("AUTO-T on nav");
|
||||||
|
let current_field = state.current_field();
|
||||||
|
|
||||||
|
if state.supports_autocomplete(current_field) && !state.is_autocomplete_active() {
|
||||||
|
state.activate_autocomplete();
|
||||||
|
} else if !state.supports_autocomplete(current_field) && state.is_autocomplete_active() {
|
||||||
|
state.deactivate_autocomplete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {} // No auto-trigger for other actions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle rich autocomplete actions for AutocompleteCanvasState
|
/// Handle rich autocomplete actions for AutocompleteCanvasState
|
||||||
fn handle_rich_autocomplete_action<S: CanvasState + AutocompleteCanvasState>(
|
fn handle_rich_autocomplete_action<S: CanvasState + AutocompleteCanvasState>(
|
||||||
action: &CanvasAction,
|
action: CanvasAction,
|
||||||
state: &mut S,
|
state: &mut S,
|
||||||
) -> Result<Option<ActionResult>> {
|
_context: &ActionContext,
|
||||||
|
) -> Option<ActionResult> {
|
||||||
match action {
|
match action {
|
||||||
CanvasAction::TriggerAutocomplete => {
|
CanvasAction::TriggerAutocomplete => {
|
||||||
if state.supports_autocomplete(state.current_field()) {
|
let current_field = state.current_field();
|
||||||
|
if state.supports_autocomplete(current_field) {
|
||||||
state.activate_autocomplete();
|
state.activate_autocomplete();
|
||||||
Ok(Some(ActionResult::success_with_message("Autocomplete activated - fetching suggestions...")))
|
Some(ActionResult::success_with_message("Autocomplete activated"))
|
||||||
} else {
|
} else {
|
||||||
Ok(Some(ActionResult::error("Autocomplete not supported for this field")))
|
Some(ActionResult::success_with_message("Autocomplete not supported for this field"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CanvasAction::SuggestionUp => {
|
||||||
|
if state.is_autocomplete_ready() {
|
||||||
|
if let Some(autocomplete_state) = state.autocomplete_state_mut() {
|
||||||
|
autocomplete_state.select_previous();
|
||||||
|
}
|
||||||
|
Some(ActionResult::success())
|
||||||
|
} else {
|
||||||
|
Some(ActionResult::success_with_message("No suggestions available"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,42 +100,34 @@ fn handle_rich_autocomplete_action<S: CanvasState + AutocompleteCanvasState>(
|
|||||||
if state.is_autocomplete_ready() {
|
if state.is_autocomplete_ready() {
|
||||||
if let Some(autocomplete_state) = state.autocomplete_state_mut() {
|
if let Some(autocomplete_state) = state.autocomplete_state_mut() {
|
||||||
autocomplete_state.select_next();
|
autocomplete_state.select_next();
|
||||||
return Ok(Some(ActionResult::success()));
|
|
||||||
}
|
}
|
||||||
|
Some(ActionResult::success())
|
||||||
|
} else {
|
||||||
|
Some(ActionResult::success_with_message("No suggestions available"))
|
||||||
}
|
}
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
|
|
||||||
CanvasAction::SuggestionUp => {
|
|
||||||
if state.is_autocomplete_ready() {
|
|
||||||
if let Some(autocomplete_state) = state.autocomplete_state_mut() {
|
|
||||||
autocomplete_state.select_previous();
|
|
||||||
return Ok(Some(ActionResult::success()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(None)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CanvasAction::SelectSuggestion => {
|
CanvasAction::SelectSuggestion => {
|
||||||
if state.is_autocomplete_ready() {
|
if state.is_autocomplete_ready() {
|
||||||
if let Some(message) = state.apply_autocomplete_selection() {
|
if let Some(msg) = state.apply_autocomplete_selection() {
|
||||||
return Ok(Some(ActionResult::success_with_message(message)));
|
Some(ActionResult::success_with_message(&msg))
|
||||||
} else {
|
} else {
|
||||||
return Ok(Some(ActionResult::error("No suggestion selected")));
|
Some(ActionResult::success_with_message("No suggestion selected"))
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Some(ActionResult::success_with_message("No suggestions available"))
|
||||||
}
|
}
|
||||||
Ok(None)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CanvasAction::ExitSuggestions => {
|
CanvasAction::ExitSuggestions => {
|
||||||
if state.is_autocomplete_active() {
|
if state.is_autocomplete_active() {
|
||||||
state.deactivate_autocomplete();
|
state.deactivate_autocomplete();
|
||||||
Ok(Some(ActionResult::success_with_message("Autocomplete cancelled")))
|
Some(ActionResult::success_with_message("Exited autocomplete"))
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Some(ActionResult::success())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => Ok(None),
|
_ => None, // Not a rich autocomplete action
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
// canvas/src/actions/edit.rs
|
// canvas/src/canvas/actions/edit.rs
|
||||||
|
|
||||||
use crate::canvas::state::{CanvasState, ActionContext};
|
use crate::canvas::state::{CanvasState, ActionContext};
|
||||||
use crate::canvas::actions::types::{CanvasAction, ActionResult};
|
use crate::canvas::actions::types::{CanvasAction, ActionResult};
|
||||||
|
use crate::config::CanvasConfig;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
/// Execute a typed canvas action on any CanvasState implementation
|
/// Execute a typed canvas action on any CanvasState implementation
|
||||||
@@ -9,6 +10,7 @@ pub async fn execute_canvas_action<S: CanvasState>(
|
|||||||
action: CanvasAction,
|
action: CanvasAction,
|
||||||
state: &mut S,
|
state: &mut S,
|
||||||
ideal_cursor_column: &mut usize,
|
ideal_cursor_column: &mut usize,
|
||||||
|
config: Option<&CanvasConfig>,
|
||||||
) -> Result<ActionResult> {
|
) -> Result<ActionResult> {
|
||||||
let context = ActionContext {
|
let context = ActionContext {
|
||||||
key_code: None,
|
key_code: None,
|
||||||
@@ -21,7 +23,7 @@ pub async fn execute_canvas_action<S: CanvasState>(
|
|||||||
return Ok(ActionResult::HandledByFeature(result));
|
return Ok(ActionResult::HandledByFeature(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
handle_generic_canvas_action(action, state, ideal_cursor_column).await
|
handle_generic_canvas_action(action, state, ideal_cursor_column, config).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle core canvas actions with full type safety
|
/// Handle core canvas actions with full type safety
|
||||||
@@ -29,126 +31,84 @@ pub async fn handle_generic_canvas_action<S: CanvasState>(
|
|||||||
action: CanvasAction,
|
action: CanvasAction,
|
||||||
state: &mut S,
|
state: &mut S,
|
||||||
ideal_cursor_column: &mut usize,
|
ideal_cursor_column: &mut usize,
|
||||||
|
config: Option<&CanvasConfig>,
|
||||||
) -> Result<ActionResult> {
|
) -> Result<ActionResult> {
|
||||||
match action {
|
match action {
|
||||||
CanvasAction::InsertChar(c) => {
|
CanvasAction::InsertChar(c) => {
|
||||||
let cursor_pos = state.current_cursor_pos();
|
let cursor_pos = state.current_cursor_pos();
|
||||||
let field_value = state.get_current_input_mut();
|
let input = state.get_current_input_mut();
|
||||||
let mut chars: Vec<char> = field_value.chars().collect();
|
input.insert(cursor_pos, c);
|
||||||
|
|
||||||
if cursor_pos <= chars.len() {
|
|
||||||
chars.insert(cursor_pos, c);
|
|
||||||
*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 = cursor_pos + 1;
|
||||||
|
Ok(ActionResult::success())
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
*ideal_cursor_column = state.current_cursor_pos();
|
||||||
Ok(ActionResult::success())
|
Ok(ActionResult::success())
|
||||||
} else {
|
|
||||||
Ok(ActionResult::error("Invalid cursor position for character insertion"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CanvasAction::DeleteBackward => {
|
CanvasAction::DeleteBackward => {
|
||||||
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();
|
if cursor_pos > 0 {
|
||||||
let mut chars: Vec<char> = field_value.chars().collect();
|
let input = state.get_current_input_mut();
|
||||||
|
input.remove(cursor_pos - 1);
|
||||||
if cursor_pos <= chars.len() {
|
state.set_current_cursor_pos(cursor_pos - 1);
|
||||||
chars.remove(cursor_pos - 1);
|
|
||||||
*field_value = chars.into_iter().collect();
|
|
||||||
let new_pos = cursor_pos - 1;
|
|
||||||
state.set_current_cursor_pos(new_pos);
|
|
||||||
state.set_has_unsaved_changes(true);
|
state.set_has_unsaved_changes(true);
|
||||||
*ideal_cursor_column = new_pos;
|
*ideal_cursor_column = cursor_pos - 1;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Ok(ActionResult::success())
|
Ok(ActionResult::success())
|
||||||
}
|
}
|
||||||
|
|
||||||
CanvasAction::DeleteForward => {
|
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 input = state.get_current_input_mut();
|
||||||
let mut chars: Vec<char> = field_value.chars().collect();
|
if cursor_pos < input.len() {
|
||||||
|
input.remove(cursor_pos);
|
||||||
if cursor_pos < chars.len() {
|
|
||||||
chars.remove(cursor_pos);
|
|
||||||
*field_value = chars.into_iter().collect();
|
|
||||||
state.set_has_unsaved_changes(true);
|
state.set_has_unsaved_changes(true);
|
||||||
*ideal_cursor_column = cursor_pos;
|
|
||||||
}
|
|
||||||
Ok(ActionResult::success())
|
|
||||||
}
|
|
||||||
|
|
||||||
CanvasAction::NextField => {
|
|
||||||
let num_fields = state.fields().len();
|
|
||||||
if num_fields > 0 {
|
|
||||||
let current_field = state.current_field();
|
|
||||||
let new_field = (current_field + 1) % num_fields;
|
|
||||||
state.set_current_field(new_field);
|
|
||||||
let current_input = state.get_current_input();
|
|
||||||
let max_pos = current_input.len();
|
|
||||||
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
|
|
||||||
}
|
|
||||||
Ok(ActionResult::success())
|
|
||||||
}
|
|
||||||
|
|
||||||
CanvasAction::PrevField => {
|
|
||||||
let num_fields = state.fields().len();
|
|
||||||
if num_fields > 0 {
|
|
||||||
let current_field = state.current_field();
|
|
||||||
let new_field = if current_field == 0 {
|
|
||||||
num_fields - 1
|
|
||||||
} else {
|
|
||||||
current_field - 1
|
|
||||||
};
|
|
||||||
state.set_current_field(new_field);
|
|
||||||
let current_input = state.get_current_input();
|
|
||||||
let max_pos = current_input.len();
|
|
||||||
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
|
|
||||||
}
|
}
|
||||||
Ok(ActionResult::success())
|
Ok(ActionResult::success())
|
||||||
}
|
}
|
||||||
|
|
||||||
CanvasAction::MoveLeft => {
|
CanvasAction::MoveLeft => {
|
||||||
let new_pos = state.current_cursor_pos().saturating_sub(1);
|
let cursor_pos = state.current_cursor_pos();
|
||||||
state.set_current_cursor_pos(new_pos);
|
if cursor_pos > 0 {
|
||||||
*ideal_cursor_column = new_pos;
|
state.set_current_cursor_pos(cursor_pos - 1);
|
||||||
|
*ideal_cursor_column = cursor_pos - 1;
|
||||||
|
}
|
||||||
Ok(ActionResult::success())
|
Ok(ActionResult::success())
|
||||||
}
|
}
|
||||||
|
|
||||||
CanvasAction::MoveRight => {
|
CanvasAction::MoveRight => {
|
||||||
|
let cursor_pos = state.current_cursor_pos();
|
||||||
let current_input = state.get_current_input();
|
let current_input = state.get_current_input();
|
||||||
let current_pos = state.current_cursor_pos();
|
if cursor_pos < current_input.len() {
|
||||||
if current_pos < current_input.len() {
|
state.set_current_cursor_pos(cursor_pos + 1);
|
||||||
let new_pos = current_pos + 1;
|
*ideal_cursor_column = cursor_pos + 1;
|
||||||
state.set_current_cursor_pos(new_pos);
|
|
||||||
*ideal_cursor_column = new_pos;
|
|
||||||
}
|
|
||||||
Ok(ActionResult::success())
|
|
||||||
}
|
|
||||||
|
|
||||||
CanvasAction::MoveUp => {
|
|
||||||
let num_fields = state.fields().len();
|
|
||||||
if num_fields > 0 {
|
|
||||||
let current_field = state.current_field();
|
|
||||||
let new_field = current_field.saturating_sub(1);
|
|
||||||
state.set_current_field(new_field);
|
|
||||||
let current_input = state.get_current_input();
|
|
||||||
let max_pos = current_input.len();
|
|
||||||
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
|
|
||||||
}
|
|
||||||
Ok(ActionResult::success())
|
|
||||||
}
|
|
||||||
|
|
||||||
CanvasAction::MoveDown => {
|
|
||||||
let num_fields = state.fields().len();
|
|
||||||
if num_fields > 0 {
|
|
||||||
let new_field = (state.current_field() + 1).min(num_fields - 1);
|
|
||||||
state.set_current_field(new_field);
|
|
||||||
let current_input = state.get_current_input();
|
|
||||||
let max_pos = current_input.len();
|
|
||||||
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
|
|
||||||
}
|
}
|
||||||
Ok(ActionResult::success())
|
Ok(ActionResult::success())
|
||||||
}
|
}
|
||||||
@@ -160,43 +120,55 @@ pub async fn handle_generic_canvas_action<S: CanvasState>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
CanvasAction::MoveLineEnd => {
|
CanvasAction::MoveLineEnd => {
|
||||||
let current_input = state.get_current_input();
|
let end_pos = state.get_current_input().len();
|
||||||
let new_pos = current_input.len();
|
state.set_current_cursor_pos(end_pos);
|
||||||
state.set_current_cursor_pos(new_pos);
|
*ideal_cursor_column = end_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);
|
||||||
|
*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())
|
Ok(ActionResult::success())
|
||||||
}
|
}
|
||||||
|
|
||||||
CanvasAction::MoveFirstLine => {
|
CanvasAction::MoveFirstLine => {
|
||||||
let num_fields = state.fields().len();
|
|
||||||
if num_fields > 0 {
|
|
||||||
state.set_current_field(0);
|
state.set_current_field(0);
|
||||||
let current_input = state.get_current_input();
|
state.set_current_cursor_pos(0);
|
||||||
let max_pos = current_input.len();
|
*ideal_cursor_column = 0;
|
||||||
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
|
Ok(ActionResult::success())
|
||||||
}
|
|
||||||
Ok(ActionResult::success_with_message("Moved to first field"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CanvasAction::MoveLastLine => {
|
CanvasAction::MoveLastLine => {
|
||||||
let num_fields = state.fields().len();
|
let last_field = state.fields().len() - 1;
|
||||||
if num_fields > 0 {
|
state.set_current_field(last_field);
|
||||||
let new_field = num_fields - 1;
|
let end_pos = state.get_current_input().len();
|
||||||
state.set_current_field(new_field);
|
state.set_current_cursor_pos(end_pos);
|
||||||
let current_input = state.get_current_input();
|
*ideal_cursor_column = end_pos;
|
||||||
let max_pos = current_input.len();
|
Ok(ActionResult::success())
|
||||||
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
|
|
||||||
}
|
|
||||||
Ok(ActionResult::success_with_message("Moved to last field"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CanvasAction::MoveWordNext => {
|
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());
|
||||||
let final_pos = new_pos.min(current_input.len());
|
state.set_current_cursor_pos(new_pos);
|
||||||
state.set_current_cursor_pos(final_pos);
|
*ideal_cursor_column = new_pos;
|
||||||
*ideal_cursor_column = final_pos;
|
|
||||||
}
|
}
|
||||||
Ok(ActionResult::success())
|
Ok(ActionResult::success())
|
||||||
}
|
}
|
||||||
@@ -204,19 +176,9 @@ pub async fn handle_generic_canvas_action<S: CanvasState>(
|
|||||||
CanvasAction::MoveWordEnd => {
|
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 new_pos = find_word_end(current_input, state.current_cursor_pos());
|
||||||
let new_pos = find_word_end(current_input, current_pos);
|
state.set_current_cursor_pos(new_pos);
|
||||||
|
*ideal_cursor_column = new_pos;
|
||||||
let final_pos = if new_pos == current_pos {
|
|
||||||
find_word_end(current_input, new_pos + 1)
|
|
||||||
} else {
|
|
||||||
new_pos
|
|
||||||
};
|
|
||||||
|
|
||||||
let max_valid_index = current_input.len().saturating_sub(1);
|
|
||||||
let clamped_pos = final_pos.min(max_valid_index);
|
|
||||||
state.set_current_cursor_pos(clamped_pos);
|
|
||||||
*ideal_cursor_column = clamped_pos;
|
|
||||||
}
|
}
|
||||||
Ok(ActionResult::success())
|
Ok(ActionResult::success())
|
||||||
}
|
}
|
||||||
@@ -231,148 +193,61 @@ pub async fn handle_generic_canvas_action<S: CanvasState>(
|
|||||||
Ok(ActionResult::success())
|
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_with_message("Moved to previous word end"))
|
|
||||||
}
|
|
||||||
|
|
||||||
CanvasAction::Custom(action_str) => {
|
CanvasAction::Custom(action_str) => {
|
||||||
Ok(ActionResult::error(format!("Unknown or unhandled custom action: {}", action_str)))
|
Ok(ActionResult::success_with_message(&format!("Custom action: {}", action_str)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Autocomplete actions are handled by the autocomplete module
|
_ => Ok(ActionResult::success_with_message("Action not implemented")),
|
||||||
CanvasAction::TriggerAutocomplete | CanvasAction::SuggestionUp | CanvasAction::SuggestionDown |
|
|
||||||
CanvasAction::SelectSuggestion | CanvasAction::ExitSuggestions => {
|
|
||||||
Ok(ActionResult::error("Autocomplete actions should be handled by autocomplete module"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Word movement helper functions
|
// Helper functions for word navigation
|
||||||
#[derive(PartialEq)]
|
fn find_next_word_start(text: &str, cursor_pos: usize) -> usize {
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find_next_word_start(text: &str, current_pos: usize) -> usize {
|
|
||||||
let chars: Vec<char> = text.chars().collect();
|
let chars: Vec<char> = text.chars().collect();
|
||||||
let len = chars.len();
|
let mut pos = cursor_pos;
|
||||||
if len == 0 || current_pos >= len {
|
|
||||||
return len;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut pos = current_pos;
|
// Skip current word
|
||||||
let initial_type = get_char_type(chars[pos]);
|
while pos < chars.len() && chars[pos].is_alphanumeric() {
|
||||||
|
|
||||||
while pos < len && get_char_type(chars[pos]) == initial_type {
|
|
||||||
pos += 1;
|
pos += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
while pos < len && get_char_type(chars[pos]) == CharType::Whitespace {
|
// Skip whitespace
|
||||||
|
while pos < chars.len() && chars[pos].is_whitespace() {
|
||||||
pos += 1;
|
pos += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
pos
|
pos
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_word_end(text: &str, current_pos: usize) -> usize {
|
fn find_word_end(text: &str, cursor_pos: usize) -> usize {
|
||||||
let chars: Vec<char> = text.chars().collect();
|
let chars: Vec<char> = text.chars().collect();
|
||||||
let len = chars.len();
|
let mut pos = cursor_pos;
|
||||||
if len == 0 {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut pos = current_pos.min(len - 1);
|
// Move to end of current word
|
||||||
|
while pos < chars.len() && chars[pos].is_alphanumeric() {
|
||||||
if get_char_type(chars[pos]) == CharType::Whitespace {
|
|
||||||
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 += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
pos.saturating_sub(1).min(len.saturating_sub(1))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find_prev_word_start(text: &str, current_pos: usize) -> usize {
|
|
||||||
let chars: Vec<char> = text.chars().collect();
|
|
||||||
if chars.is_empty() || current_pos == 0 {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut pos = current_pos.saturating_sub(1);
|
|
||||||
|
|
||||||
while pos > 0 && get_char_type(chars[pos]) == CharType::Whitespace {
|
|
||||||
pos -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if pos == 0 && get_char_type(chars[pos]) == 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
pos
|
pos
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_prev_word_end(text: &str, current_pos: usize) -> usize {
|
fn find_prev_word_start(text: &str, cursor_pos: usize) -> usize {
|
||||||
|
if cursor_pos == 0 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
let chars: Vec<char> = text.chars().collect();
|
let chars: Vec<char> = text.chars().collect();
|
||||||
let len = chars.len();
|
let mut pos = cursor_pos.saturating_sub(1);
|
||||||
if len == 0 || current_pos == 0 {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut pos = current_pos.saturating_sub(1);
|
// Skip whitespace
|
||||||
|
while pos > 0 && chars[pos].is_whitespace() {
|
||||||
while pos > 0 && get_char_type(chars[pos]) == CharType::Whitespace {
|
|
||||||
pos -= 1;
|
pos -= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if pos == 0 && get_char_type(chars[pos]) == CharType::Whitespace {
|
// Skip to start of word
|
||||||
return 0;
|
while pos > 0 && chars[pos - 1].is_alphanumeric() {
|
||||||
}
|
|
||||||
if pos == 0 && get_char_type(chars[pos]) != 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;
|
pos -= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
while pos > 0 && get_char_type(chars[pos - 1]) == CharType::Whitespace {
|
pos
|
||||||
pos -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if pos > 0 {
|
|
||||||
pos - 1
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ pub mod edit;
|
|||||||
|
|
||||||
// Re-export the main types for convenience
|
// Re-export the main types for convenience
|
||||||
pub use types::{CanvasAction, ActionResult};
|
pub use types::{CanvasAction, ActionResult};
|
||||||
pub use edit::execute_canvas_action; // Remove execute_edit_action
|
pub use edit::execute_canvas_action;
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
// canvas/src/actions/types.rs
|
// src/canvas/actions/types.rs
|
||||||
|
|
||||||
use crossterm::event::KeyCode;
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
|
||||||
/// All possible canvas actions, type-safe and exhaustive
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub enum CanvasAction {
|
pub enum CanvasAction {
|
||||||
// Character input
|
// Character input
|
||||||
InsertChar(char),
|
InsertChar(char),
|
||||||
@@ -28,37 +25,43 @@ pub enum CanvasAction {
|
|||||||
MoveWordNext,
|
MoveWordNext,
|
||||||
MoveWordEnd,
|
MoveWordEnd,
|
||||||
MoveWordPrev,
|
MoveWordPrev,
|
||||||
MoveWordEndPrev,
|
|
||||||
|
|
||||||
// Field navigation
|
// Field navigation
|
||||||
NextField,
|
NextField,
|
||||||
PrevField,
|
PrevField,
|
||||||
|
|
||||||
// AUTOCOMPLETE ACTIONS (NEW)
|
// Autocomplete actions
|
||||||
/// Manually trigger autocomplete for current field
|
|
||||||
TriggerAutocomplete,
|
TriggerAutocomplete,
|
||||||
/// Move to next suggestion
|
|
||||||
SuggestionUp,
|
SuggestionUp,
|
||||||
/// Move to previous suggestion
|
|
||||||
SuggestionDown,
|
SuggestionDown,
|
||||||
/// Select the currently highlighted suggestion
|
|
||||||
SelectSuggestion,
|
SelectSuggestion,
|
||||||
/// Cancel/exit autocomplete mode
|
|
||||||
ExitSuggestions,
|
ExitSuggestions,
|
||||||
|
|
||||||
// Custom actions (escape hatch for feature-specific behavior)
|
// Custom actions
|
||||||
Custom(String),
|
Custom(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CanvasAction {
|
impl CanvasAction {
|
||||||
/// Convert a string action to typed action (for backwards compatibility during migration)
|
pub fn from_key(key: crossterm::event::KeyCode) -> Option<Self> {
|
||||||
|
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 {
|
||||||
"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_backward" => Self::DeleteBackward,
|
||||||
"delete_char_forward" => Self::DeleteForward,
|
"delete_char_forward" => Self::DeleteForward,
|
||||||
"move_left" => Self::MoveLeft,
|
"move_left" => Self::MoveLeft,
|
||||||
@@ -72,10 +75,8 @@ impl CanvasAction {
|
|||||||
"move_word_next" => Self::MoveWordNext,
|
"move_word_next" => Self::MoveWordNext,
|
||||||
"move_word_end" => Self::MoveWordEnd,
|
"move_word_end" => Self::MoveWordEnd,
|
||||||
"move_word_prev" => Self::MoveWordPrev,
|
"move_word_prev" => Self::MoveWordPrev,
|
||||||
"move_word_end_prev" => Self::MoveWordEndPrev,
|
|
||||||
"next_field" => Self::NextField,
|
"next_field" => Self::NextField,
|
||||||
"prev_field" => Self::PrevField,
|
"prev_field" => Self::PrevField,
|
||||||
// Autocomplete actions
|
|
||||||
"trigger_autocomplete" => Self::TriggerAutocomplete,
|
"trigger_autocomplete" => Self::TriggerAutocomplete,
|
||||||
"suggestion_up" => Self::SuggestionUp,
|
"suggestion_up" => Self::SuggestionUp,
|
||||||
"suggestion_down" => Self::SuggestionDown,
|
"suggestion_down" => Self::SuggestionDown,
|
||||||
@@ -84,94 +85,13 @@ impl CanvasAction {
|
|||||||
_ => Self::Custom(action.to_string()),
|
_ => 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",
|
|
||||||
// Autocomplete actions
|
|
||||||
Self::TriggerAutocomplete => "trigger_autocomplete",
|
|
||||||
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
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
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::TriggerAutocomplete | Self::SuggestionUp | Self::SuggestionDown |
|
|
||||||
Self::SelectSuggestion | Self::ExitSuggestions
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Result of executing a canvas action
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub enum ActionResult {
|
pub enum ActionResult {
|
||||||
/// Action completed successfully, optional message for user feedback
|
|
||||||
Success(Option<String>),
|
Success(Option<String>),
|
||||||
/// Action was handled by custom feature logic
|
|
||||||
HandledByFeature(String),
|
HandledByFeature(String),
|
||||||
/// Action requires additional context or cannot be performed
|
|
||||||
RequiresContext(String),
|
RequiresContext(String),
|
||||||
/// Action failed with error message
|
|
||||||
Error(String),
|
Error(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,11 +100,11 @@ impl ActionResult {
|
|||||||
Self::Success(None)
|
Self::Success(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn success_with_message(msg: impl Into<String>) -> Self {
|
pub fn success_with_message(msg: &str) -> Self {
|
||||||
Self::Success(Some(msg.into()))
|
Self::Success(Some(msg.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn error(msg: impl Into<String>) -> Self {
|
pub fn error(msg: &str) -> Self {
|
||||||
Self::Error(msg.into())
|
Self::Error(msg.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,37 +121,3 @@ impl ActionResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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("trigger_autocomplete"), CanvasAction::TriggerAutocomplete);
|
|
||||||
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::TriggerAutocomplete.is_suggestion());
|
|
||||||
assert!(!CanvasAction::MoveLeft.is_suggestion());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -180,6 +180,20 @@ impl CanvasConfig {
|
|||||||
Self::from_toml(&contents)
|
Self::from_toml(&contents)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// NEW: Check if autocomplete should auto-trigger (simple logic)
|
||||||
|
pub fn should_auto_trigger_autocomplete(&self) -> bool {
|
||||||
|
// If trigger_autocomplete keybinding exists anywhere, use manual mode only
|
||||||
|
// If no trigger_autocomplete keybinding, use auto-trigger mode
|
||||||
|
!self.has_trigger_autocomplete_keybinding()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NEW: Check if user has configured manual trigger keybinding
|
||||||
|
pub fn has_trigger_autocomplete_keybinding(&self) -> bool {
|
||||||
|
self.keybindings.edit.contains_key("trigger_autocomplete") ||
|
||||||
|
self.keybindings.read_only.contains_key("trigger_autocomplete") ||
|
||||||
|
self.keybindings.global.contains_key("trigger_autocomplete")
|
||||||
|
}
|
||||||
|
|
||||||
/// Get action for key in read-only mode
|
/// Get action for key in read-only mode
|
||||||
pub fn get_read_only_action(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> {
|
pub fn get_read_only_action(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> {
|
||||||
self.get_action_in_mode(&self.keybindings.read_only, key, modifiers)
|
self.get_action_in_mode(&self.keybindings.read_only, key, modifiers)
|
||||||
|
|||||||
@@ -2,6 +2,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;
|
||||||
|
|
||||||
/// 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;
|
||||||
@@ -13,7 +14,9 @@ impl ActionDispatcher {
|
|||||||
state: &mut S,
|
state: &mut S,
|
||||||
ideal_cursor_column: &mut usize,
|
ideal_cursor_column: &mut usize,
|
||||||
) -> anyhow::Result<ActionResult> {
|
) -> anyhow::Result<ActionResult> {
|
||||||
execute_canvas_action(action, state, ideal_cursor_column).await
|
|
||||||
|
// Load config once here instead of threading it everywhere
|
||||||
|
execute_canvas_action(action, state, ideal_cursor_column, Some(&CanvasConfig::load())).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Quick action dispatch from KeyCode
|
/// Quick action dispatch from KeyCode
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ move_word_next = ["Ctrl+Right"]
|
|||||||
move_word_prev = ["Ctrl+Left"]
|
move_word_prev = ["Ctrl+Left"]
|
||||||
next_field = ["Tab"]
|
next_field = ["Tab"]
|
||||||
prev_field = ["Shift+Tab"]
|
prev_field = ["Shift+Tab"]
|
||||||
|
trigger_autocomplete = ["Ctrl+p"]
|
||||||
|
|
||||||
# Suggestion/autocomplete keybindings
|
# Suggestion/autocomplete keybindings
|
||||||
[keybindings.suggestions]
|
[keybindings.suggestions]
|
||||||
|
|||||||
Reference in New Issue
Block a user