Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
baa4295059 | ||
|
|
6cbfac9d6e | ||
|
|
13d28f19ea | ||
|
|
8fa86965b8 | ||
|
|
72c38f613f | ||
|
|
e4982f871f | ||
|
|
4e0338276f | ||
|
|
fe193f4f91 | ||
|
|
0011ba0c04 | ||
|
|
3c2eef9596 | ||
|
|
dac788351f |
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -482,6 +482,8 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-test",
|
||||
"toml",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"unicode-width 0.2.0",
|
||||
]
|
||||
|
||||
|
||||
@@ -19,6 +19,9 @@ toml = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
unicode-width.workspace = true
|
||||
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = "0.3.19"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-test = "0.4.4"
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ highlight_current_field = true
|
||||
move_left = ["h"]
|
||||
move_right = ["l"]
|
||||
move_up = ["k"]
|
||||
move_down = ["p"]
|
||||
move_down = ["j"]
|
||||
move_word_next = ["w"]
|
||||
move_word_end = ["e"]
|
||||
move_word_prev = ["b"]
|
||||
@@ -42,6 +42,8 @@ move_word_next = ["Ctrl+Right"]
|
||||
move_word_prev = ["Ctrl+Left"]
|
||||
next_field = ["Tab"]
|
||||
prev_field = ["Shift+Tab"]
|
||||
trigger_autocomplete = ["Ctrl+p"]
|
||||
|
||||
|
||||
# Suggestion/autocomplete keybindings
|
||||
[keybindings.suggestions]
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// src/autocomplete/actions.rs
|
||||
// canvas/src/autocomplete/actions.rs
|
||||
|
||||
use crate::canvas::state::{CanvasState, ActionContext};
|
||||
use crate::autocomplete::state::AutocompleteCanvasState;
|
||||
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;
|
||||
|
||||
/// Version for states that implement rich autocomplete
|
||||
@@ -11,6 +12,7 @@ pub async fn execute_canvas_action_with_autocomplete<S: CanvasState + Autocomple
|
||||
action: CanvasAction,
|
||||
state: &mut S,
|
||||
ideal_cursor_column: &mut usize,
|
||||
config: Option<&CanvasConfig>,
|
||||
) -> Result<ActionResult> {
|
||||
// 1. Try feature-specific handler first
|
||||
let context = ActionContext {
|
||||
@@ -20,31 +22,77 @@ pub async fn execute_canvas_action_with_autocomplete<S: CanvasState + Autocomple
|
||||
current_field: state.current_field(),
|
||||
};
|
||||
|
||||
if let Some(result) = state.handle_feature_action(&action, &context) {
|
||||
return Ok(ActionResult::HandledByFeature(result));
|
||||
}
|
||||
|
||||
// 2. Handle rich autocomplete actions
|
||||
if let Some(result) = handle_rich_autocomplete_action(&action, state)? {
|
||||
if let Some(result) = handle_rich_autocomplete_action(action.clone(), state, &context) {
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
// 3. Handle generic canvas actions
|
||||
handle_generic_canvas_action(action, state, ideal_cursor_column).await
|
||||
// 2. Handle generic actions and add auto-trigger logic
|
||||
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
|
||||
fn handle_rich_autocomplete_action<S: CanvasState + AutocompleteCanvasState>(
|
||||
action: &CanvasAction,
|
||||
action: CanvasAction,
|
||||
state: &mut S,
|
||||
) -> Result<Option<ActionResult>> {
|
||||
_context: &ActionContext,
|
||||
) -> Option<ActionResult> {
|
||||
match action {
|
||||
CanvasAction::TriggerAutocomplete => {
|
||||
if state.supports_autocomplete(state.current_field()) {
|
||||
let current_field = state.current_field();
|
||||
if state.supports_autocomplete(current_field) {
|
||||
state.activate_autocomplete();
|
||||
Ok(Some(ActionResult::success_with_message("Autocomplete activated - fetching suggestions...")))
|
||||
Some(ActionResult::success_with_message("Autocomplete activated"))
|
||||
} 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 let Some(autocomplete_state) = state.autocomplete_state_mut() {
|
||||
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 => {
|
||||
if state.is_autocomplete_ready() {
|
||||
if let Some(message) = state.apply_autocomplete_selection() {
|
||||
return Ok(Some(ActionResult::success_with_message(message)));
|
||||
if let Some(msg) = state.apply_autocomplete_selection() {
|
||||
Some(ActionResult::success_with_message(&msg))
|
||||
} 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 => {
|
||||
if state.is_autocomplete_active() {
|
||||
state.deactivate_autocomplete();
|
||||
Ok(Some(ActionResult::success_with_message("Autocomplete cancelled")))
|
||||
Some(ActionResult::success_with_message("Exited autocomplete"))
|
||||
} else {
|
||||
Ok(None)
|
||||
Some(ActionResult::success())
|
||||
}
|
||||
}
|
||||
|
||||
_ => Ok(None),
|
||||
_ => None, // Not a rich autocomplete action
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,8 +56,6 @@ fn render_loading_indicator<T: CanvasTheme>(
|
||||
);
|
||||
|
||||
let loading_block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(theme.accent()))
|
||||
.style(Style::default().bg(theme.bg()));
|
||||
|
||||
let loading_paragraph = Paragraph::new(loading_text)
|
||||
@@ -92,8 +90,6 @@ fn render_suggestions_dropdown<T: CanvasTheme>(
|
||||
|
||||
// Background
|
||||
let dropdown_block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(theme.accent()))
|
||||
.style(Style::default().bg(theme.bg()));
|
||||
|
||||
// List items
|
||||
@@ -111,7 +107,7 @@ fn render_suggestions_dropdown<T: CanvasTheme>(
|
||||
f.render_stateful_widget(list, dropdown_area, &mut list_state);
|
||||
}
|
||||
|
||||
/// Calculate dropdown size based on suggestions
|
||||
/// Calculate dropdown size based on suggestions - updated to match client dimensions
|
||||
#[cfg(feature = "gui")]
|
||||
fn calculate_dropdown_dimensions(display_texts: &[&str]) -> DropdownDimensions {
|
||||
let max_width = display_texts
|
||||
@@ -120,9 +116,9 @@ fn calculate_dropdown_dimensions(display_texts: &[&str]) -> DropdownDimensions {
|
||||
.max()
|
||||
.unwrap_or(0) as u16;
|
||||
|
||||
let horizontal_padding = 4; // borders + padding
|
||||
let width = (max_width + horizontal_padding).max(12);
|
||||
let height = (display_texts.len() as u16).min(8) + 2; // max 8 visible items + borders
|
||||
let horizontal_padding = 2; // Changed from 4 to 2 to match client
|
||||
let width = (max_width + horizontal_padding).max(10); // Changed from 12 to 10 to match client
|
||||
let height = (display_texts.len() as u16).min(5); // Removed +2 since no borders
|
||||
|
||||
DropdownDimensions { width, height }
|
||||
}
|
||||
@@ -155,7 +151,7 @@ fn calculate_dropdown_position(
|
||||
dropdown_area
|
||||
}
|
||||
|
||||
/// Create styled list items
|
||||
/// Create styled list items - updated to match client spacing
|
||||
#[cfg(feature = "gui")]
|
||||
fn create_suggestion_list_items<'a, T: CanvasTheme>(
|
||||
display_texts: &'a [&'a str],
|
||||
@@ -163,8 +159,8 @@ fn create_suggestion_list_items<'a, T: CanvasTheme>(
|
||||
dropdown_width: u16,
|
||||
theme: &T,
|
||||
) -> Vec<ListItem<'a>> {
|
||||
let horizontal_padding = 4;
|
||||
let available_width = dropdown_width.saturating_sub(horizontal_padding);
|
||||
let horizontal_padding = 2; // Changed from 4 to 2 to match client
|
||||
let available_width = dropdown_width; // No border padding needed
|
||||
|
||||
display_texts
|
||||
.iter()
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// canvas/src/actions/edit.rs
|
||||
// canvas/src/canvas/actions/edit.rs
|
||||
|
||||
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
|
||||
@@ -9,6 +10,7 @@ pub async fn execute_canvas_action<S: CanvasState>(
|
||||
action: CanvasAction,
|
||||
state: &mut S,
|
||||
ideal_cursor_column: &mut usize,
|
||||
config: Option<&CanvasConfig>,
|
||||
) -> Result<ActionResult> {
|
||||
let context = ActionContext {
|
||||
key_code: None,
|
||||
@@ -21,7 +23,7 @@ pub async fn execute_canvas_action<S: CanvasState>(
|
||||
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
|
||||
@@ -29,126 +31,84 @@ pub async fn handle_generic_canvas_action<S: CanvasState>(
|
||||
action: CanvasAction,
|
||||
state: &mut S,
|
||||
ideal_cursor_column: &mut usize,
|
||||
config: Option<&CanvasConfig>,
|
||||
) -> Result<ActionResult> {
|
||||
match action {
|
||||
CanvasAction::InsertChar(c) => {
|
||||
let cursor_pos = state.current_cursor_pos();
|
||||
let field_value = state.get_current_input_mut();
|
||||
let mut chars: Vec<char> = field_value.chars().collect();
|
||||
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())
|
||||
}
|
||||
|
||||
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_has_unsaved_changes(true);
|
||||
*ideal_cursor_column = state.current_cursor_pos();
|
||||
Ok(ActionResult::success())
|
||||
} else {
|
||||
Ok(ActionResult::error("Invalid cursor position for character insertion"))
|
||||
}
|
||||
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())
|
||||
}
|
||||
|
||||
CanvasAction::DeleteBackward => {
|
||||
if state.current_cursor_pos() > 0 {
|
||||
let cursor_pos = state.current_cursor_pos();
|
||||
let field_value = state.get_current_input_mut();
|
||||
let mut chars: Vec<char> = field_value.chars().collect();
|
||||
|
||||
if cursor_pos <= chars.len() {
|
||||
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);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
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 field_value = state.get_current_input_mut();
|
||||
let mut chars: Vec<char> = field_value.chars().collect();
|
||||
|
||||
if cursor_pos < chars.len() {
|
||||
chars.remove(cursor_pos);
|
||||
*field_value = chars.into_iter().collect();
|
||||
let input = state.get_current_input_mut();
|
||||
if cursor_pos < input.len() {
|
||||
input.remove(cursor_pos);
|
||||
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())
|
||||
}
|
||||
|
||||
CanvasAction::MoveLeft => {
|
||||
let new_pos = state.current_cursor_pos().saturating_sub(1);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
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();
|
||||
let current_pos = state.current_cursor_pos();
|
||||
if current_pos < current_input.len() {
|
||||
let new_pos = current_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));
|
||||
if cursor_pos < current_input.len() {
|
||||
state.set_current_cursor_pos(cursor_pos + 1);
|
||||
*ideal_cursor_column = cursor_pos + 1;
|
||||
}
|
||||
Ok(ActionResult::success())
|
||||
}
|
||||
@@ -160,43 +120,55 @@ pub async fn handle_generic_canvas_action<S: CanvasState>(
|
||||
}
|
||||
|
||||
CanvasAction::MoveLineEnd => {
|
||||
let current_input = state.get_current_input();
|
||||
let new_pos = current_input.len();
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
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 => {
|
||||
let num_fields = state.fields().len();
|
||||
if num_fields > 0 {
|
||||
state.set_current_field(0);
|
||||
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_with_message("Moved to first field"))
|
||||
state.set_current_field(0);
|
||||
state.set_current_cursor_pos(0);
|
||||
*ideal_cursor_column = 0;
|
||||
Ok(ActionResult::success())
|
||||
}
|
||||
|
||||
CanvasAction::MoveLastLine => {
|
||||
let num_fields = state.fields().len();
|
||||
if num_fields > 0 {
|
||||
let new_field = 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_with_message("Moved to last field"))
|
||||
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());
|
||||
let final_pos = new_pos.min(current_input.len());
|
||||
state.set_current_cursor_pos(final_pos);
|
||||
*ideal_cursor_column = final_pos;
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
Ok(ActionResult::success())
|
||||
}
|
||||
@@ -204,19 +176,9 @@ pub async fn handle_generic_canvas_action<S: CanvasState>(
|
||||
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 = 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;
|
||||
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())
|
||||
}
|
||||
@@ -231,148 +193,61 @@ pub async fn handle_generic_canvas_action<S: CanvasState>(
|
||||
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) => {
|
||||
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
|
||||
CanvasAction::TriggerAutocomplete | CanvasAction::SuggestionUp | CanvasAction::SuggestionDown |
|
||||
CanvasAction::SelectSuggestion | CanvasAction::ExitSuggestions => {
|
||||
Ok(ActionResult::error("Autocomplete actions should be handled by autocomplete module"))
|
||||
}
|
||||
_ => Ok(ActionResult::success_with_message("Action not implemented")),
|
||||
}
|
||||
}
|
||||
|
||||
// Word movement helper functions
|
||||
#[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
|
||||
}
|
||||
}
|
||||
|
||||
fn find_next_word_start(text: &str, current_pos: usize) -> usize {
|
||||
// Helper functions for word navigation
|
||||
fn find_next_word_start(text: &str, cursor_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let len = chars.len();
|
||||
if len == 0 || current_pos >= len {
|
||||
return len;
|
||||
}
|
||||
|
||||
let mut pos = current_pos;
|
||||
let initial_type = get_char_type(chars[pos]);
|
||||
|
||||
while pos < len && get_char_type(chars[pos]) == initial_type {
|
||||
let mut pos = cursor_pos;
|
||||
|
||||
// Skip current word
|
||||
while pos < chars.len() && chars[pos].is_alphanumeric() {
|
||||
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
|
||||
}
|
||||
|
||||
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 len = chars.len();
|
||||
if len == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let mut pos = current_pos.min(len - 1);
|
||||
|
||||
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 {
|
||||
let mut pos = cursor_pos;
|
||||
|
||||
// Move to end of current word
|
||||
while pos < chars.len() && chars[pos].is_alphanumeric() {
|
||||
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
|
||||
}
|
||||
|
||||
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 len = chars.len();
|
||||
if len == 0 || current_pos == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let mut pos = current_pos.saturating_sub(1);
|
||||
|
||||
while pos > 0 && get_char_type(chars[pos]) == CharType::Whitespace {
|
||||
let mut pos = cursor_pos.saturating_sub(1);
|
||||
|
||||
// Skip whitespace
|
||||
while pos > 0 && chars[pos].is_whitespace() {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
if pos == 0 && get_char_type(chars[pos]) == CharType::Whitespace {
|
||||
return 0;
|
||||
}
|
||||
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 {
|
||||
|
||||
// Skip to start of word
|
||||
while pos > 0 && chars[pos - 1].is_alphanumeric() {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
while pos > 0 && get_char_type(chars[pos - 1]) == CharType::Whitespace {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
if pos > 0 {
|
||||
pos - 1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
pos
|
||||
}
|
||||
|
||||
@@ -4,4 +4,4 @@ pub mod edit;
|
||||
|
||||
// Re-export the main types for convenience
|
||||
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;
|
||||
|
||||
/// All possible canvas actions, type-safe and exhaustive
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum CanvasAction {
|
||||
// Character input
|
||||
InsertChar(char),
|
||||
@@ -28,37 +25,43 @@ pub enum CanvasAction {
|
||||
MoveWordNext,
|
||||
MoveWordEnd,
|
||||
MoveWordPrev,
|
||||
MoveWordEndPrev,
|
||||
|
||||
// Field navigation
|
||||
NextField,
|
||||
PrevField,
|
||||
|
||||
// AUTOCOMPLETE ACTIONS (NEW)
|
||||
/// Manually trigger autocomplete for current field
|
||||
// Autocomplete actions
|
||||
TriggerAutocomplete,
|
||||
/// Move to next suggestion
|
||||
SuggestionUp,
|
||||
/// Move to previous suggestion
|
||||
SuggestionDown,
|
||||
/// Select the currently highlighted suggestion
|
||||
SelectSuggestion,
|
||||
/// Cancel/exit autocomplete mode
|
||||
ExitSuggestions,
|
||||
|
||||
// Custom actions (escape hatch for feature-specific behavior)
|
||||
// Custom actions
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
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 {
|
||||
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,
|
||||
@@ -72,10 +75,8 @@ 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,
|
||||
// Autocomplete actions
|
||||
"trigger_autocomplete" => Self::TriggerAutocomplete,
|
||||
"suggestion_up" => Self::SuggestionUp,
|
||||
"suggestion_down" => Self::SuggestionDown,
|
||||
@@ -84,94 +85,13 @@ impl CanvasAction {
|
||||
_ => 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
|
||||
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)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
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),
|
||||
}
|
||||
|
||||
@@ -180,11 +100,11 @@ impl ActionResult {
|
||||
Self::Success(None)
|
||||
}
|
||||
|
||||
pub fn success_with_message(msg: impl Into<String>) -> Self {
|
||||
Self::Success(Some(msg.into()))
|
||||
pub fn success_with_message(msg: &str) -> Self {
|
||||
Self::Success(Some(msg.to_string()))
|
||||
}
|
||||
|
||||
pub fn error(msg: impl Into<String>) -> Self {
|
||||
pub fn error(msg: &str) -> Self {
|
||||
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)
|
||||
}
|
||||
|
||||
/// 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
|
||||
pub fn get_read_only_action(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> {
|
||||
self.get_action_in_mode(&self.keybindings.read_only, key, modifiers)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use crate::canvas::state::CanvasState;
|
||||
use crate::canvas::actions::{CanvasAction, ActionResult, execute_canvas_action};
|
||||
use crate::config::CanvasConfig;
|
||||
|
||||
/// High-level action dispatcher that coordinates between different action types
|
||||
pub struct ActionDispatcher;
|
||||
@@ -13,7 +14,9 @@ impl ActionDispatcher {
|
||||
state: &mut S,
|
||||
ideal_cursor_column: &mut usize,
|
||||
) -> 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
|
||||
|
||||
@@ -42,6 +42,7 @@ move_word_next = ["Ctrl+Right"]
|
||||
move_word_prev = ["Ctrl+Left"]
|
||||
next_field = ["Tab"]
|
||||
prev_field = ["Shift+Tab"]
|
||||
trigger_autocomplete = ["Ctrl+p"]
|
||||
|
||||
# Suggestion/autocomplete keybindings
|
||||
[keybindings.suggestions]
|
||||
|
||||
124
client/docs/canvas_add_functionality.md
Normal file
124
client/docs/canvas_add_functionality.md
Normal file
@@ -0,0 +1,124 @@
|
||||
## How Canvas Library Custom Functionality Works
|
||||
|
||||
### 1. **The Canvas Library Calls YOUR Custom Code First**
|
||||
|
||||
When you call `ActionDispatcher::dispatch()`, here's what happens:
|
||||
|
||||
```rust
|
||||
// Inside canvas library (canvas/src/actions/edit.rs):
|
||||
pub async fn execute_canvas_action<S: CanvasState>(
|
||||
action: CanvasAction,
|
||||
state: &mut S,
|
||||
ideal_cursor_column: &mut usize,
|
||||
) -> Result<ActionResult> {
|
||||
// 1. FIRST: Canvas library calls YOUR custom handler
|
||||
if let Some(result) = state.handle_feature_action(&action, &context) {
|
||||
return Ok(ActionResult::HandledByFeature(result)); // YOUR code handled it
|
||||
}
|
||||
|
||||
// 2. ONLY IF your code returns None: Canvas handles generic actions
|
||||
handle_generic_canvas_action(action, state, ideal_cursor_column).await
|
||||
}
|
||||
```
|
||||
|
||||
### 2. **Your Extension Point: `handle_feature_action`**
|
||||
|
||||
You add custom functionality by implementing `handle_feature_action` in your states:
|
||||
|
||||
```rust
|
||||
// In src/state/pages/auth.rs
|
||||
impl CanvasState for LoginState {
|
||||
// ... other methods ...
|
||||
|
||||
fn handle_feature_action(&mut self, action: &CanvasAction, context: &ActionContext) -> Option<String> {
|
||||
match action {
|
||||
// Custom login-specific actions
|
||||
CanvasAction::Custom(action_str) if action_str == "submit_login" => {
|
||||
if self.username.is_empty() || self.password.is_empty() {
|
||||
Some("Please fill in all required fields".to_string())
|
||||
} else {
|
||||
// Trigger login process
|
||||
Some(format!("Logging in user: {}", self.username))
|
||||
}
|
||||
}
|
||||
|
||||
CanvasAction::Custom(action_str) if action_str == "clear_form" => {
|
||||
self.username.clear();
|
||||
self.password.clear();
|
||||
self.set_has_unsaved_changes(false);
|
||||
Some("Login form cleared".to_string())
|
||||
}
|
||||
|
||||
// Custom behavior for standard actions
|
||||
CanvasAction::NextField => {
|
||||
// Custom validation when moving between fields
|
||||
if self.current_field == 0 && self.username.is_empty() {
|
||||
Some("Username cannot be empty".to_string())
|
||||
} else {
|
||||
None // Let canvas library handle the normal field movement
|
||||
}
|
||||
}
|
||||
|
||||
// Let canvas library handle everything else
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. **Multiple Ways to Add Custom Functionality**
|
||||
|
||||
#### A) **Custom Actions via Config**
|
||||
```toml
|
||||
# In config.toml
|
||||
[keybindings.edit]
|
||||
submit_login = ["ctrl+enter"]
|
||||
clear_form = ["ctrl+r"]
|
||||
```
|
||||
|
||||
#### B) **Override Standard Actions**
|
||||
```rust
|
||||
fn handle_feature_action(&mut self, action: &CanvasAction, context: &ActionContext) -> Option<String> {
|
||||
match action {
|
||||
CanvasAction::InsertChar('p') if self.current_field == 1 => {
|
||||
// Custom behavior when typing 'p' in password field
|
||||
Some("Password field - use secure input".to_string())
|
||||
}
|
||||
_ => None, // Let canvas handle normally
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### C) **Context-Aware Logic**
|
||||
```rust
|
||||
fn handle_feature_action(&mut self, action: &CanvasAction, context: &ActionContext) -> Option<String> {
|
||||
match action {
|
||||
CanvasAction::MoveDown => {
|
||||
// Custom logic based on current state
|
||||
if context.current_field == 1 && context.current_input.len() < 8 {
|
||||
Some("Password should be at least 8 characters".to_string())
|
||||
} else {
|
||||
None // Normal field movement
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## The Canvas Library Philosophy
|
||||
|
||||
**Canvas Library = Generic behavior + Your extension points**
|
||||
|
||||
- ✅ **Canvas handles**: Character insertion, cursor movement, field navigation, etc.
|
||||
- ✅ **You handle**: Validation, submission, clearing, app-specific logic
|
||||
- ✅ **You decide**: Return `Some(message)` to override, `None` to use canvas default
|
||||
|
||||
## Summary
|
||||
|
||||
You **don't communicate with the library elsewhere**. Instead:
|
||||
|
||||
1. **Canvas library calls your code first** via `handle_feature_action`
|
||||
2. **Your code decides** whether to handle the action or let canvas handle it
|
||||
3. **Canvas library handles** generic form behavior when you return `None`
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::config::colors::themes::Theme;
|
||||
use crate::state::app::highlight::HighlightState;
|
||||
use crate::state::app::state::AppState;
|
||||
use crate::state::pages::add_logic::{AddLogicFocus, AddLogicState};
|
||||
use crate::state::pages::canvas_state::CanvasState;
|
||||
use canvas::canvas::{render_canvas, CanvasState, HighlightState as CanvasHighlightState}; // Use canvas library
|
||||
use ratatui::{
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||
style::{Modifier, Style},
|
||||
@@ -11,10 +11,18 @@ use ratatui::{
|
||||
widgets::{Block, BorderType, Borders, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
use crate::components::handlers::canvas::render_canvas;
|
||||
use crate::components::common::{dialog, autocomplete}; // Added autocomplete
|
||||
use crate::config::binds::config::EditorKeybindingMode;
|
||||
|
||||
// Helper function to convert between HighlightState types
|
||||
fn convert_highlight_state(local: &HighlightState) -> CanvasHighlightState {
|
||||
match local {
|
||||
HighlightState::Off => CanvasHighlightState::Off,
|
||||
HighlightState::Characterwise { anchor } => CanvasHighlightState::Characterwise { anchor: *anchor },
|
||||
HighlightState::Linewise { anchor_line } => CanvasHighlightState::Linewise { anchor_line: *anchor_line },
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_add_logic(
|
||||
f: &mut Frame,
|
||||
area: Rect,
|
||||
@@ -77,18 +85,18 @@ pub fn render_add_logic(
|
||||
let editor_borrow = add_logic_state.script_content_editor.borrow();
|
||||
editor_borrow.cursor() // Returns (row, col) as (usize, usize)
|
||||
};
|
||||
|
||||
|
||||
let (cursor_line, cursor_col) = current_cursor;
|
||||
|
||||
|
||||
// Account for TextArea's block borders (1 for each side)
|
||||
let block_offset_x = 1;
|
||||
let block_offset_y = 1;
|
||||
|
||||
|
||||
// Position autocomplete at current cursor position
|
||||
// Add 1 to column to position dropdown right after the cursor
|
||||
let autocomplete_x = cursor_col + 1;
|
||||
let autocomplete_y = cursor_line;
|
||||
|
||||
|
||||
let input_rect = Rect {
|
||||
x: (inner_area.x + block_offset_x + autocomplete_x as u16).min(inner_area.right().saturating_sub(20)),
|
||||
y: (inner_area.y + block_offset_y + autocomplete_y as u16).min(inner_area.bottom().saturating_sub(5)),
|
||||
@@ -152,40 +160,37 @@ pub fn render_add_logic(
|
||||
);
|
||||
f.render_widget(profile_text, top_info_area);
|
||||
|
||||
// Canvas
|
||||
// Canvas - USING CANVAS LIBRARY
|
||||
let focus_on_canvas_inputs = matches!(
|
||||
add_logic_state.current_focus,
|
||||
AddLogicFocus::InputLogicName
|
||||
| AddLogicFocus::InputTargetColumn
|
||||
| AddLogicFocus::InputDescription
|
||||
);
|
||||
// Call render_canvas and get the active_field_rect
|
||||
|
||||
let canvas_highlight_state = convert_highlight_state(highlight_state);
|
||||
let active_field_rect = render_canvas(
|
||||
f,
|
||||
canvas_area,
|
||||
add_logic_state, // Pass the whole state as it impl CanvasState
|
||||
&add_logic_state.fields(),
|
||||
&add_logic_state.current_field(),
|
||||
&add_logic_state.inputs(),
|
||||
theme,
|
||||
is_edit_mode && focus_on_canvas_inputs, // is_edit_mode for canvas fields
|
||||
highlight_state,
|
||||
add_logic_state, // AddLogicState implements CanvasState
|
||||
theme, // Theme implements CanvasTheme
|
||||
is_edit_mode && focus_on_canvas_inputs,
|
||||
&canvas_highlight_state,
|
||||
);
|
||||
|
||||
// --- Render Autocomplete for Target Column ---
|
||||
// `is_edit_mode` here refers to the general edit mode of the EventHandler
|
||||
if is_edit_mode && add_logic_state.current_field() == 1 { // Target Column field
|
||||
if let Some(suggestions) = add_logic_state.get_suggestions() { // Uses CanvasState impl
|
||||
let selected = add_logic_state.get_selected_suggestion_index();
|
||||
if !suggestions.is_empty() { // Only render if there are suggestions to show
|
||||
if add_logic_state.in_target_column_suggestion_mode && add_logic_state.show_target_column_suggestions {
|
||||
if !add_logic_state.target_column_suggestions.is_empty() {
|
||||
if let Some(input_rect) = active_field_rect {
|
||||
autocomplete::render_autocomplete_dropdown(
|
||||
f,
|
||||
input_rect,
|
||||
f.area(), // Full frame area for clamping
|
||||
theme,
|
||||
suggestions,
|
||||
selected,
|
||||
&add_logic_state.target_column_suggestions,
|
||||
add_logic_state.selected_target_column_suggestion_index,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,7 @@ use crate::config::colors::themes::Theme;
|
||||
use crate::state::app::highlight::HighlightState;
|
||||
use crate::state::app::state::AppState;
|
||||
use crate::state::pages::add_table::{AddTableFocus, AddTableState};
|
||||
use crate::state::pages::canvas_state::CanvasState;
|
||||
// use crate::state::pages::add_table::{ColumnDefinition, LinkDefinition}; // Not directly used here
|
||||
use canvas::canvas::{render_canvas, CanvasState, HighlightState as CanvasHighlightState};
|
||||
use ratatui::{
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||
style::{Modifier, Style},
|
||||
@@ -12,16 +11,24 @@ use ratatui::{
|
||||
widgets::{Block, BorderType, Borders, Cell, Paragraph, Row, Table},
|
||||
Frame,
|
||||
};
|
||||
use crate::components::handlers::canvas::render_canvas;
|
||||
use crate::components::common::dialog;
|
||||
|
||||
// Helper function to convert between HighlightState types
|
||||
fn convert_highlight_state(local: &HighlightState) -> CanvasHighlightState {
|
||||
match local {
|
||||
HighlightState::Off => CanvasHighlightState::Off,
|
||||
HighlightState::Characterwise { anchor } => CanvasHighlightState::Characterwise { anchor: *anchor },
|
||||
HighlightState::Linewise { anchor_line } => CanvasHighlightState::Linewise { anchor_line: *anchor_line },
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders the Add New Table page layout, structuring the display of table information,
|
||||
/// input fields, and action buttons. Adapts layout based on terminal width.
|
||||
pub fn render_add_table(
|
||||
f: &mut Frame,
|
||||
area: Rect,
|
||||
theme: &Theme,
|
||||
app_state: &AppState, // Currently unused, might be needed later
|
||||
app_state: &AppState,
|
||||
add_table_state: &mut AddTableState,
|
||||
is_edit_mode: bool, // Determines if canvas inputs are in edit mode
|
||||
highlight_state: &HighlightState, // For text highlighting in canvas
|
||||
@@ -349,17 +356,15 @@ pub fn render_add_table(
|
||||
&mut add_table_state.column_table_state,
|
||||
);
|
||||
|
||||
// --- Canvas Rendering (Column Definition Input) ---
|
||||
// --- Canvas Rendering (Column Definition Input) - USING CANVAS LIBRARY ---
|
||||
let canvas_highlight_state = convert_highlight_state(highlight_state);
|
||||
let _active_field_rect = render_canvas(
|
||||
f,
|
||||
canvas_area,
|
||||
add_table_state,
|
||||
&add_table_state.fields(),
|
||||
&add_table_state.current_field(),
|
||||
&add_table_state.inputs(),
|
||||
theme,
|
||||
add_table_state, // AddTableState implements CanvasState
|
||||
theme, // Theme implements CanvasTheme
|
||||
is_edit_mode && focus_on_canvas_inputs,
|
||||
highlight_state,
|
||||
&canvas_highlight_state,
|
||||
);
|
||||
|
||||
// --- Button Style Helpers ---
|
||||
@@ -557,7 +562,7 @@ pub fn render_add_table(
|
||||
|
||||
// --- DIALOG ---
|
||||
// Render the dialog overlay if it's active
|
||||
if app_state.ui.dialog.dialog_show { // Use the passed-in app_state
|
||||
if app_state.ui.dialog.dialog_show {
|
||||
dialog::render_dialog(
|
||||
f,
|
||||
f.area(), // Render over the whole frame area
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// src/components/handlers.rs
|
||||
pub mod canvas;
|
||||
pub mod sidebar;
|
||||
pub mod buffer_list;
|
||||
|
||||
pub use canvas::*;
|
||||
pub use sidebar::*;
|
||||
pub use buffer_list::*;
|
||||
|
||||
@@ -1,255 +0,0 @@
|
||||
// src/components/handlers/canvas.rs
|
||||
|
||||
use ratatui::{
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||
style::{Modifier, Style},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
use crate::config::colors::themes::Theme;
|
||||
use crate::state::app::highlight::HighlightState;
|
||||
use crate::state::pages::canvas_state::CanvasState as LegacyCanvasState;
|
||||
use canvas::canvas::CanvasState as LibraryCanvasState;
|
||||
use std::cmp::{max, min};
|
||||
|
||||
/// Render canvas for legacy CanvasState (AddTableState, LoginState, RegisterState, AddLogicState)
|
||||
pub fn render_canvas(
|
||||
f: &mut Frame,
|
||||
area: Rect,
|
||||
form_state: &impl LegacyCanvasState,
|
||||
fields: &[&str],
|
||||
current_field_idx: &usize,
|
||||
inputs: &[&String],
|
||||
theme: &Theme,
|
||||
is_edit_mode: bool,
|
||||
highlight_state: &HighlightState,
|
||||
) -> Option<Rect> {
|
||||
render_canvas_impl(
|
||||
f,
|
||||
area,
|
||||
fields,
|
||||
current_field_idx,
|
||||
inputs,
|
||||
theme,
|
||||
is_edit_mode,
|
||||
highlight_state,
|
||||
form_state.current_cursor_pos(),
|
||||
form_state.has_unsaved_changes(),
|
||||
|i| form_state.get_display_value_for_field(i).to_string(),
|
||||
|i| form_state.has_display_override(i),
|
||||
)
|
||||
}
|
||||
|
||||
/// Render canvas for library CanvasState (FormState)
|
||||
pub fn render_canvas_library(
|
||||
f: &mut Frame,
|
||||
area: Rect,
|
||||
form_state: &impl LibraryCanvasState,
|
||||
fields: &[&str],
|
||||
current_field_idx: &usize,
|
||||
inputs: &[&String],
|
||||
theme: &Theme,
|
||||
is_edit_mode: bool,
|
||||
highlight_state: &HighlightState,
|
||||
) -> Option<Rect> {
|
||||
render_canvas_impl(
|
||||
f,
|
||||
area,
|
||||
fields,
|
||||
current_field_idx,
|
||||
inputs,
|
||||
theme,
|
||||
is_edit_mode,
|
||||
highlight_state,
|
||||
form_state.current_cursor_pos(),
|
||||
form_state.has_unsaved_changes(),
|
||||
|i| form_state.get_display_value_for_field(i).to_string(),
|
||||
|i| form_state.has_display_override(i),
|
||||
)
|
||||
}
|
||||
|
||||
/// Internal implementation shared by both render functions
|
||||
fn render_canvas_impl<F1, F2>(
|
||||
f: &mut Frame,
|
||||
area: Rect,
|
||||
fields: &[&str],
|
||||
current_field_idx: &usize,
|
||||
inputs: &[&String],
|
||||
theme: &Theme,
|
||||
is_edit_mode: bool,
|
||||
highlight_state: &HighlightState,
|
||||
current_cursor_pos: usize,
|
||||
has_unsaved_changes: bool,
|
||||
get_display_value: F1,
|
||||
has_display_override: F2,
|
||||
) -> Option<Rect>
|
||||
where
|
||||
F1: Fn(usize) -> String,
|
||||
F2: Fn(usize) -> bool,
|
||||
{
|
||||
let columns = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)])
|
||||
.split(area);
|
||||
|
||||
let border_style = if has_unsaved_changes {
|
||||
Style::default().fg(theme.warning)
|
||||
} else if is_edit_mode {
|
||||
Style::default().fg(theme.accent)
|
||||
} else {
|
||||
Style::default().fg(theme.secondary)
|
||||
};
|
||||
let input_container = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(border_style)
|
||||
.style(Style::default().bg(theme.bg));
|
||||
|
||||
let input_block = Rect {
|
||||
x: columns[1].x,
|
||||
y: columns[1].y,
|
||||
width: columns[1].width,
|
||||
height: fields.len() as u16 + 2,
|
||||
};
|
||||
|
||||
f.render_widget(&input_container, input_block);
|
||||
|
||||
let input_area = input_container.inner(input_block);
|
||||
let input_rows = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(vec![Constraint::Length(1); fields.len()])
|
||||
.split(input_area);
|
||||
|
||||
let mut active_field_input_rect = None;
|
||||
|
||||
for (i, field) in fields.iter().enumerate() {
|
||||
let label = Paragraph::new(Line::from(Span::styled(
|
||||
format!("{}:", field),
|
||||
Style::default().fg(theme.fg),
|
||||
)));
|
||||
f.render_widget(
|
||||
label,
|
||||
Rect {
|
||||
x: columns[0].x,
|
||||
y: input_block.y + 1 + i as u16,
|
||||
width: columns[0].width,
|
||||
height: 1,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
for (i, _input) in inputs.iter().enumerate() {
|
||||
let is_active = i == *current_field_idx;
|
||||
|
||||
// Use the provided closure to get display value
|
||||
let text = get_display_value(i);
|
||||
let text_len = text.chars().count();
|
||||
let line: Line;
|
||||
|
||||
match highlight_state {
|
||||
HighlightState::Off => {
|
||||
line = Line::from(Span::styled(
|
||||
&text,
|
||||
if is_active {
|
||||
Style::default().fg(theme.highlight)
|
||||
} else {
|
||||
Style::default().fg(theme.fg)
|
||||
},
|
||||
));
|
||||
}
|
||||
HighlightState::Characterwise { anchor } => {
|
||||
let (anchor_field, anchor_char) = *anchor;
|
||||
let start_field = min(anchor_field, *current_field_idx);
|
||||
let end_field = max(anchor_field, *current_field_idx);
|
||||
|
||||
let (start_char, end_char) = if anchor_field == *current_field_idx {
|
||||
(min(anchor_char, current_cursor_pos), max(anchor_char, current_cursor_pos))
|
||||
} else if anchor_field < *current_field_idx {
|
||||
(anchor_char, current_cursor_pos)
|
||||
} else {
|
||||
(current_cursor_pos, anchor_char)
|
||||
};
|
||||
|
||||
let highlight_style = Style::default().fg(theme.highlight).bg(theme.highlight_bg).add_modifier(Modifier::BOLD);
|
||||
let normal_style_in_highlight = Style::default().fg(theme.highlight);
|
||||
let normal_style_outside = Style::default().fg(theme.fg);
|
||||
|
||||
if i >= start_field && i <= end_field {
|
||||
if start_field == end_field {
|
||||
let clamped_start = start_char.min(text_len);
|
||||
let clamped_end = end_char.min(text_len);
|
||||
|
||||
let before: String = text.chars().take(clamped_start).collect();
|
||||
let highlighted: String = text.chars().skip(clamped_start).take(clamped_end.saturating_sub(clamped_start) + 1).collect();
|
||||
let after: String = text.chars().skip(clamped_end + 1).collect();
|
||||
|
||||
line = Line::from(vec![
|
||||
Span::styled(before, normal_style_in_highlight),
|
||||
Span::styled(highlighted, highlight_style),
|
||||
Span::styled(after, normal_style_in_highlight),
|
||||
]);
|
||||
} else if i == start_field {
|
||||
let safe_start = start_char.min(text_len);
|
||||
let before: String = text.chars().take(safe_start).collect();
|
||||
let highlighted: String = text.chars().skip(safe_start).collect();
|
||||
line = Line::from(vec![
|
||||
Span::styled(before, normal_style_in_highlight),
|
||||
Span::styled(highlighted, highlight_style),
|
||||
]);
|
||||
} else if i == end_field {
|
||||
let safe_end_inclusive = if text_len > 0 { end_char.min(text_len - 1) } else { 0 };
|
||||
let highlighted: String = text.chars().take(safe_end_inclusive + 1).collect();
|
||||
let after: String = text.chars().skip(safe_end_inclusive + 1).collect();
|
||||
line = Line::from(vec![
|
||||
Span::styled(highlighted, highlight_style),
|
||||
Span::styled(after, normal_style_in_highlight),
|
||||
]);
|
||||
} else {
|
||||
line = Line::from(Span::styled(&text, highlight_style));
|
||||
}
|
||||
} else {
|
||||
line = Line::from(Span::styled(
|
||||
&text,
|
||||
if is_active { normal_style_in_highlight } else { normal_style_outside }
|
||||
));
|
||||
}
|
||||
}
|
||||
HighlightState::Linewise { anchor_line } => {
|
||||
let start_field = min(*anchor_line, *current_field_idx);
|
||||
let end_field = max(*anchor_line, *current_field_idx);
|
||||
let highlight_style = Style::default().fg(theme.highlight).bg(theme.highlight_bg).add_modifier(Modifier::BOLD);
|
||||
let normal_style_in_highlight = Style::default().fg(theme.highlight);
|
||||
let normal_style_outside = Style::default().fg(theme.fg);
|
||||
|
||||
if i >= start_field && i <= end_field {
|
||||
line = Line::from(Span::styled(&text, highlight_style));
|
||||
} else {
|
||||
line = Line::from(Span::styled(
|
||||
&text,
|
||||
if is_active { normal_style_in_highlight } else { normal_style_outside }
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let input_display = Paragraph::new(line).alignment(Alignment::Left);
|
||||
f.render_widget(input_display, input_rows[i]);
|
||||
|
||||
if is_active {
|
||||
active_field_input_rect = Some(input_rows[i]);
|
||||
|
||||
// Use the provided closure to check for display override
|
||||
let cursor_x = if has_display_override(i) {
|
||||
// If an override exists, place the cursor at the end.
|
||||
input_rows[i].x + text.chars().count() as u16
|
||||
} else {
|
||||
// Otherwise, use the real cursor position.
|
||||
input_rows[i].x + current_cursor_pos as u16
|
||||
};
|
||||
let cursor_y = input_rows[i].y;
|
||||
f.set_cursor_position((cursor_x, cursor_y));
|
||||
}
|
||||
}
|
||||
|
||||
active_field_input_rect
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
// src/functions/modes.rs
|
||||
|
||||
pub mod read_only;
|
||||
pub mod edit;
|
||||
pub mod navigation;
|
||||
|
||||
pub use read_only::*;
|
||||
pub use edit::*;
|
||||
pub use navigation::*;
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
// src/functions/modes/edit.rs
|
||||
|
||||
pub mod form_e;
|
||||
pub mod auth_e;
|
||||
pub mod add_table_e;
|
||||
pub mod add_logic_e;
|
||||
@@ -1,135 +0,0 @@
|
||||
// src/functions/modes/edit/add_logic_e.rs
|
||||
use crate::state::pages::add_logic::AddLogicState;
|
||||
use crate::state::pages::canvas_state::CanvasState;
|
||||
use anyhow::Result;
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
|
||||
pub async fn execute_edit_action(
|
||||
action: &str,
|
||||
key: KeyEvent, // Keep key for insert_char
|
||||
state: &mut AddLogicState,
|
||||
ideal_cursor_column: &mut usize,
|
||||
) -> Result<String> {
|
||||
let mut message = String::new();
|
||||
|
||||
match action {
|
||||
"next_field" => {
|
||||
let current_field = state.current_field();
|
||||
let next_field = (current_field + 1) % AddLogicState::INPUT_FIELD_COUNT;
|
||||
state.set_current_field(next_field);
|
||||
*ideal_cursor_column = state.current_cursor_pos();
|
||||
message = format!("Focus on field {}", state.fields()[next_field]);
|
||||
}
|
||||
"prev_field" => {
|
||||
let current_field = state.current_field();
|
||||
let prev_field = if current_field == 0 {
|
||||
AddLogicState::INPUT_FIELD_COUNT - 1
|
||||
} else {
|
||||
current_field - 1
|
||||
};
|
||||
state.set_current_field(prev_field);
|
||||
*ideal_cursor_column = state.current_cursor_pos();
|
||||
message = format!("Focus on field {}", state.fields()[prev_field]);
|
||||
}
|
||||
"delete_char_forward" => {
|
||||
let current_pos = state.current_cursor_pos();
|
||||
let current_input_mut = state.get_current_input_mut();
|
||||
if current_pos < current_input_mut.len() {
|
||||
current_input_mut.remove(current_pos);
|
||||
state.set_has_unsaved_changes(true);
|
||||
if state.current_field() == 1 { state.update_target_column_suggestions(); }
|
||||
}
|
||||
}
|
||||
"delete_char_backward" => {
|
||||
let current_pos = state.current_cursor_pos();
|
||||
if current_pos > 0 {
|
||||
let new_pos = current_pos - 1;
|
||||
state.get_current_input_mut().remove(new_pos);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
state.set_has_unsaved_changes(true);
|
||||
if state.current_field() == 1 { state.update_target_column_suggestions(); }
|
||||
}
|
||||
}
|
||||
"move_left" => {
|
||||
let current_pos = state.current_cursor_pos();
|
||||
if current_pos > 0 {
|
||||
let new_pos = current_pos - 1;
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
}
|
||||
"move_right" => {
|
||||
let current_pos = state.current_cursor_pos();
|
||||
let input_len = state.get_current_input().len();
|
||||
if current_pos < input_len {
|
||||
let new_pos = current_pos + 1;
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
}
|
||||
"insert_char" => {
|
||||
if let KeyCode::Char(c) = key.code {
|
||||
let current_pos = state.current_cursor_pos();
|
||||
state.get_current_input_mut().insert(current_pos, c);
|
||||
let new_pos = current_pos + 1;
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
state.set_has_unsaved_changes(true);
|
||||
if state.current_field() == 1 {
|
||||
state.update_target_column_suggestions();
|
||||
}
|
||||
}
|
||||
}
|
||||
"suggestion_down" => {
|
||||
if state.in_target_column_suggestion_mode && !state.target_column_suggestions.is_empty() {
|
||||
let current_selection = state.selected_target_column_suggestion_index.unwrap_or(0);
|
||||
let next_selection = (current_selection + 1) % state.target_column_suggestions.len();
|
||||
state.selected_target_column_suggestion_index = Some(next_selection);
|
||||
}
|
||||
}
|
||||
"suggestion_up" => {
|
||||
if state.in_target_column_suggestion_mode && !state.target_column_suggestions.is_empty() {
|
||||
let current_selection = state.selected_target_column_suggestion_index.unwrap_or(0);
|
||||
let prev_selection = if current_selection == 0 {
|
||||
state.target_column_suggestions.len() - 1
|
||||
} else {
|
||||
current_selection - 1
|
||||
};
|
||||
state.selected_target_column_suggestion_index = Some(prev_selection);
|
||||
}
|
||||
}
|
||||
"select_suggestion" => {
|
||||
if state.in_target_column_suggestion_mode {
|
||||
let mut selected_suggestion_text: Option<String> = None;
|
||||
|
||||
if let Some(selected_idx) = state.selected_target_column_suggestion_index {
|
||||
if let Some(suggestion) = state.target_column_suggestions.get(selected_idx) {
|
||||
selected_suggestion_text = Some(suggestion.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(suggestion_text) = selected_suggestion_text {
|
||||
state.target_column_input = suggestion_text.clone();
|
||||
state.target_column_cursor_pos = state.target_column_input.len();
|
||||
*ideal_cursor_column = state.target_column_cursor_pos;
|
||||
state.set_has_unsaved_changes(true);
|
||||
message = format!("Selected column: '{}'", suggestion_text);
|
||||
}
|
||||
|
||||
state.in_target_column_suggestion_mode = false;
|
||||
state.show_target_column_suggestions = false;
|
||||
state.selected_target_column_suggestion_index = None;
|
||||
state.update_target_column_suggestions();
|
||||
} else {
|
||||
let current_field = state.current_field();
|
||||
let next_field = (current_field + 1) % AddLogicState::INPUT_FIELD_COUNT;
|
||||
state.set_current_field(next_field);
|
||||
*ideal_cursor_column = state.current_cursor_pos();
|
||||
message = format!("Focus on field {}", state.fields()[next_field]);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(message)
|
||||
}
|
||||
@@ -1,341 +0,0 @@
|
||||
// src/functions/modes/edit/add_table_e.rs
|
||||
use crate::state::pages::add_table::AddTableState;
|
||||
use crate::state::pages::canvas_state::CanvasState; // Use trait
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use anyhow::Result;
|
||||
|
||||
#[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
|
||||
}
|
||||
}
|
||||
|
||||
fn find_next_word_start(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let len = chars.len();
|
||||
if len == 0 || current_pos >= len {
|
||||
return len;
|
||||
}
|
||||
|
||||
let mut pos = current_pos;
|
||||
let initial_type = get_char_type(chars[pos]);
|
||||
|
||||
while pos < len && get_char_type(chars[pos]) == initial_type {
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
while pos < len && get_char_type(chars[pos]) == CharType::Whitespace {
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
pos
|
||||
}
|
||||
|
||||
fn find_word_end(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let len = chars.len();
|
||||
if len == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let mut pos = current_pos.min(len - 1);
|
||||
|
||||
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.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
|
||||
}
|
||||
|
||||
fn find_prev_word_end(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let len = chars.len();
|
||||
if len == 0 || 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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
while pos > 0 && get_char_type(chars[pos - 1]) == CharType::Whitespace {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
if pos > 0 {
|
||||
pos - 1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes edit actions for the AddTable view canvas.
|
||||
pub async fn execute_edit_action(
|
||||
action: &str,
|
||||
key: KeyEvent, // Needed for insert_char
|
||||
state: &mut AddTableState,
|
||||
ideal_cursor_column: &mut usize,
|
||||
// Add other params like grpc_client if needed for future actions (e.g., validation)
|
||||
) -> Result<String> {
|
||||
// Use the CanvasState trait methods implemented for AddTableState
|
||||
match action {
|
||||
"insert_char" => {
|
||||
if let KeyCode::Char(c) = key.code {
|
||||
let cursor_pos = state.current_cursor_pos();
|
||||
let field_value = state.get_current_input_mut();
|
||||
let mut chars: Vec<char> = field_value.chars().collect();
|
||||
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_has_unsaved_changes(true);
|
||||
*ideal_cursor_column = state.current_cursor_pos();
|
||||
}
|
||||
} else {
|
||||
return Ok("Error: insert_char called without a char key.".to_string());
|
||||
}
|
||||
Ok("".to_string()) // No message needed for char insertion
|
||||
}
|
||||
"delete_char_backward" => {
|
||||
if state.current_cursor_pos() > 0 {
|
||||
let cursor_pos = state.current_cursor_pos();
|
||||
let field_value = state.get_current_input_mut();
|
||||
let mut chars: Vec<char> = field_value.chars().collect();
|
||||
if cursor_pos <= chars.len() {
|
||||
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);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
"delete_char_forward" => {
|
||||
let cursor_pos = state.current_cursor_pos();
|
||||
let field_value = state.get_current_input_mut();
|
||||
let mut chars: Vec<char> = field_value.chars().collect();
|
||||
if cursor_pos < chars.len() {
|
||||
chars.remove(cursor_pos);
|
||||
*field_value = chars.into_iter().collect();
|
||||
state.set_has_unsaved_changes(true);
|
||||
*ideal_cursor_column = cursor_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
"next_field" => {
|
||||
let num_fields = AddTableState::INPUT_FIELD_COUNT;
|
||||
if num_fields > 0 {
|
||||
let current_field = state.current_field();
|
||||
let last_field_index = num_fields - 1;
|
||||
// Prevent cycling forward
|
||||
if current_field < last_field_index {
|
||||
state.set_current_field(current_field + 1);
|
||||
}
|
||||
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("".to_string())
|
||||
}
|
||||
"prev_field" => {
|
||||
let num_fields = AddTableState::INPUT_FIELD_COUNT;
|
||||
if num_fields > 0 {
|
||||
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 max_pos = current_input.len();
|
||||
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_left" => {
|
||||
let new_pos = state.current_cursor_pos().saturating_sub(1);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_right" => {
|
||||
let current_input = state.get_current_input();
|
||||
let current_pos = state.current_cursor_pos();
|
||||
if current_pos < current_input.len() {
|
||||
let new_pos = current_pos + 1;
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_up" => {
|
||||
let current_field = state.current_field();
|
||||
// Prevent moving up from the first field
|
||||
if current_field > 0 {
|
||||
let new_field = 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("ahoj".to_string())
|
||||
}
|
||||
"move_down" => {
|
||||
let num_fields = AddTableState::INPUT_FIELD_COUNT;
|
||||
if num_fields > 0 {
|
||||
let current_field = state.current_field();
|
||||
let last_field_index = num_fields - 1;
|
||||
if current_field < last_field_index {
|
||||
let new_field = 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("".to_string())
|
||||
}
|
||||
"move_line_start" => {
|
||||
state.set_current_cursor_pos(0);
|
||||
*ideal_cursor_column = 0;
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_line_end" => {
|
||||
let current_input = state.get_current_input();
|
||||
let new_pos = current_input.len();
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_first_line" => {
|
||||
if AddTableState::INPUT_FIELD_COUNT > 0 {
|
||||
state.set_current_field(0);
|
||||
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("".to_string())
|
||||
}
|
||||
"move_last_line" => {
|
||||
let num_fields = AddTableState::INPUT_FIELD_COUNT;
|
||||
if num_fields > 0 {
|
||||
let new_field = 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("".to_string())
|
||||
}
|
||||
"move_word_next" => {
|
||||
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 = new_pos.min(current_input.len());
|
||||
state.set_current_cursor_pos(final_pos);
|
||||
*ideal_cursor_column = final_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_word_end" => {
|
||||
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 = 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("".to_string())
|
||||
}
|
||||
"move_word_prev" => {
|
||||
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("".to_string())
|
||||
}
|
||||
"move_word_end_prev" => {
|
||||
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("".to_string())
|
||||
}
|
||||
// Actions handled by main event loop (mode changes, save, revert)
|
||||
"exit_edit_mode" | "save" | "revert" => {
|
||||
Ok("Action handled by main loop".to_string())
|
||||
}
|
||||
_ => Ok(format!("Unknown or unhandled edit action: {}", action)),
|
||||
}
|
||||
}
|
||||
@@ -1,466 +0,0 @@
|
||||
// src/functions/modes/edit/auth_e.rs
|
||||
|
||||
use crate::services::grpc_client::GrpcClient;
|
||||
use crate::state::pages::form::FormState;
|
||||
use crate::state::pages::auth::RegisterState;
|
||||
use crate::state::app::state::AppState;
|
||||
use crate::tui::functions::common::form::{revert, save};
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use canvas::autocomplete::AutocompleteCanvasState;
|
||||
use canvas::canvas::CanvasState;
|
||||
use std::any::Any;
|
||||
use anyhow::Result;
|
||||
|
||||
pub async fn execute_common_action<S: CanvasState + Any>(
|
||||
action: &str,
|
||||
state: &mut S,
|
||||
grpc_client: &mut GrpcClient,
|
||||
app_state: &AppState,
|
||||
current_position: &mut u64,
|
||||
total_count: u64,
|
||||
) -> Result<String> {
|
||||
match action {
|
||||
"save" | "revert" => {
|
||||
if !state.has_unsaved_changes() {
|
||||
return Ok("No changes to save or revert.".to_string());
|
||||
}
|
||||
if let Some(form_state) =
|
||||
(state as &mut dyn Any).downcast_mut::<FormState>()
|
||||
{
|
||||
match action {
|
||||
"save" => {
|
||||
let outcome = save(
|
||||
app_state,
|
||||
form_state,
|
||||
grpc_client,
|
||||
)
|
||||
.await?;
|
||||
let message = format!("Save successful: {:?}", outcome); // Simple message for now
|
||||
Ok(message)
|
||||
}
|
||||
"revert" => {
|
||||
revert(
|
||||
form_state,
|
||||
grpc_client,
|
||||
)
|
||||
.await
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
} else {
|
||||
Ok(format!(
|
||||
"Action '{}' not implemented for this state type.",
|
||||
action
|
||||
))
|
||||
}
|
||||
}
|
||||
_ => Ok(format!("Common action '{}' not handled here.", action)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn execute_edit_action<S: CanvasState + Any + Send>(
|
||||
action: &str,
|
||||
key: KeyEvent,
|
||||
state: &mut S,
|
||||
ideal_cursor_column: &mut usize,
|
||||
) -> Result<String> {
|
||||
match action {
|
||||
"insert_char" => {
|
||||
if let KeyCode::Char(c) = key.code {
|
||||
let cursor_pos = state.current_cursor_pos();
|
||||
let field_value = state.get_current_input_mut();
|
||||
let mut chars: Vec<char> = field_value.chars().collect();
|
||||
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_has_unsaved_changes(true);
|
||||
*ideal_cursor_column = state.current_cursor_pos();
|
||||
}
|
||||
} else {
|
||||
return Ok("Error: insert_char called without a char key."
|
||||
.to_string());
|
||||
}
|
||||
Ok("working?".to_string())
|
||||
}
|
||||
|
||||
"delete_char_backward" => {
|
||||
if state.current_cursor_pos() > 0 {
|
||||
let cursor_pos = state.current_cursor_pos();
|
||||
let field_value = state.get_current_input_mut();
|
||||
let mut chars: Vec<char> = field_value.chars().collect();
|
||||
if cursor_pos <= chars.len() {
|
||||
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);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"delete_char_forward" => {
|
||||
let cursor_pos = state.current_cursor_pos();
|
||||
let field_value = state.get_current_input_mut();
|
||||
let mut chars: Vec<char> = field_value.chars().collect();
|
||||
if cursor_pos < chars.len() {
|
||||
chars.remove(cursor_pos);
|
||||
*field_value = chars.into_iter().collect();
|
||||
state.set_has_unsaved_changes(true);
|
||||
*ideal_cursor_column = cursor_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"next_field" => {
|
||||
let num_fields = state.fields().len();
|
||||
if num_fields > 0 {
|
||||
let current_field = state.current_field();
|
||||
let new_field = (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("".to_string())
|
||||
}
|
||||
|
||||
"prev_field" => {
|
||||
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("".to_string())
|
||||
}
|
||||
|
||||
"move_left" => {
|
||||
let new_pos = state.current_cursor_pos().saturating_sub(1);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"move_right" => {
|
||||
let current_input = state.get_current_input();
|
||||
let current_pos = state.current_cursor_pos();
|
||||
if current_pos < current_input.len() {
|
||||
let new_pos = current_pos + 1;
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"move_up" => {
|
||||
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("".to_string())
|
||||
}
|
||||
|
||||
"move_down" => {
|
||||
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("".to_string())
|
||||
}
|
||||
|
||||
"move_line_start" => {
|
||||
state.set_current_cursor_pos(0);
|
||||
*ideal_cursor_column = 0;
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"move_line_end" => {
|
||||
let current_input = state.get_current_input();
|
||||
let new_pos = current_input.len();
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"move_first_line" => {
|
||||
let num_fields = state.fields().len();
|
||||
if num_fields > 0 {
|
||||
state.set_current_field(0);
|
||||
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("Moved to first field".to_string())
|
||||
}
|
||||
|
||||
"move_last_line" => {
|
||||
let num_fields = state.fields().len();
|
||||
if num_fields > 0 {
|
||||
let new_field = 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("Moved to last field".to_string())
|
||||
}
|
||||
|
||||
"move_word_next" => {
|
||||
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 = new_pos.min(current_input.len());
|
||||
state.set_current_cursor_pos(final_pos);
|
||||
*ideal_cursor_column = final_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"move_word_end" => {
|
||||
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 = 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("".to_string())
|
||||
}
|
||||
|
||||
"move_word_prev" => {
|
||||
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("".to_string())
|
||||
}
|
||||
|
||||
"move_word_end_prev" => {
|
||||
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("Moved to previous word end".to_string())
|
||||
}
|
||||
|
||||
// --- Autocomplete Actions ---
|
||||
"suggestion_down" | "suggestion_up" | "select_suggestion" | "exit_suggestion_mode" => {
|
||||
// Attempt to downcast to RegisterState to handle suggestion logic here
|
||||
if let Some(register_state) = (state as &mut dyn Any).downcast_mut::<RegisterState>() {
|
||||
// Only handle if it's the role field (index 4) and autocomplete is active
|
||||
if register_state.current_field() == 4 && register_state.is_autocomplete_active() {
|
||||
match action {
|
||||
"suggestion_down" => {
|
||||
if let Some(autocomplete_state) = register_state.autocomplete_state_mut() {
|
||||
autocomplete_state.select_next();
|
||||
Ok("Suggestion changed down".to_string())
|
||||
} else {
|
||||
Ok("No autocomplete state".to_string())
|
||||
}
|
||||
}
|
||||
"suggestion_up" => {
|
||||
if let Some(autocomplete_state) = register_state.autocomplete_state_mut() {
|
||||
autocomplete_state.select_previous();
|
||||
Ok("Suggestion changed up".to_string())
|
||||
} else {
|
||||
Ok("No autocomplete state".to_string())
|
||||
}
|
||||
}
|
||||
"select_suggestion" => {
|
||||
if let Some(message) = register_state.apply_autocomplete_selection() {
|
||||
Ok(message)
|
||||
} else {
|
||||
Ok("No suggestion selected".to_string())
|
||||
}
|
||||
}
|
||||
"exit_suggestion_mode" => {
|
||||
register_state.deactivate_autocomplete();
|
||||
Ok("Suggestions hidden".to_string())
|
||||
}
|
||||
_ => Ok("Suggestion action ignored: State mismatch.".to_string())
|
||||
}
|
||||
} else {
|
||||
Ok("Suggestion action ignored: Not on role field or autocomplete not active.".to_string())
|
||||
}
|
||||
} else {
|
||||
Ok(format!("Action '{}' not applicable for this state type.", action))
|
||||
}
|
||||
}
|
||||
// --- End Autocomplete Actions ---
|
||||
|
||||
|
||||
_ => Ok(format!("Unknown or unhandled edit action: {}", action)),
|
||||
}
|
||||
}
|
||||
|
||||
#[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
|
||||
}
|
||||
}
|
||||
|
||||
fn find_next_word_start(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let len = chars.len();
|
||||
if len == 0 || current_pos >= len {
|
||||
return len;
|
||||
}
|
||||
|
||||
let mut pos = current_pos;
|
||||
let initial_type = get_char_type(chars[pos]);
|
||||
|
||||
while pos < len && get_char_type(chars[pos]) == initial_type {
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
while pos < len && get_char_type(chars[pos]) == CharType::Whitespace {
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
pos
|
||||
}
|
||||
|
||||
fn find_word_end(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let len = chars.len();
|
||||
if len == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let mut pos = current_pos.min(len - 1);
|
||||
|
||||
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.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
|
||||
}
|
||||
|
||||
fn find_prev_word_end(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let len = chars.len();
|
||||
if len == 0 || 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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
while pos > 0 && get_char_type(chars[pos - 1]) == CharType::Whitespace {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
if pos > 0 {
|
||||
pos - 1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
@@ -1,435 +0,0 @@
|
||||
// src/functions/modes/edit/form_e.rs
|
||||
|
||||
use crate::services::grpc_client::GrpcClient;
|
||||
use crate::state::pages::form::FormState;
|
||||
use crate::state::app::state::AppState;
|
||||
use crate::tui::functions::common::form::{revert, save};
|
||||
use crate::tui::functions::common::form::SaveOutcome;
|
||||
use crate::modes::handlers::event::EventOutcome;
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use canvas::canvas::CanvasState;
|
||||
use std::any::Any;
|
||||
use anyhow::Result;
|
||||
|
||||
pub async fn execute_common_action<S: CanvasState + Any>(
|
||||
action: &str,
|
||||
state: &mut S,
|
||||
grpc_client: &mut GrpcClient,
|
||||
app_state: &AppState,
|
||||
) -> Result<EventOutcome> {
|
||||
match action {
|
||||
"save" | "revert" => {
|
||||
if !state.has_unsaved_changes() {
|
||||
return Ok(EventOutcome::Ok("No changes to save or revert.".to_string()));
|
||||
}
|
||||
if let Some(form_state) =
|
||||
(state as &mut dyn Any).downcast_mut::<FormState>()
|
||||
{
|
||||
match action {
|
||||
"save" => {
|
||||
let save_result = save(
|
||||
app_state,
|
||||
form_state,
|
||||
grpc_client,
|
||||
).await;
|
||||
|
||||
match save_result {
|
||||
Ok(save_outcome) => {
|
||||
let message = match save_outcome {
|
||||
SaveOutcome::NoChange => "No changes to save.".to_string(),
|
||||
SaveOutcome::UpdatedExisting => "Entry updated.".to_string(),
|
||||
SaveOutcome::CreatedNew(_) => "New entry created.".to_string(),
|
||||
};
|
||||
Ok(EventOutcome::DataSaved(save_outcome, message))
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
"revert" => {
|
||||
let revert_result = revert(
|
||||
form_state,
|
||||
grpc_client,
|
||||
).await;
|
||||
|
||||
match revert_result {
|
||||
Ok(message) => Ok(EventOutcome::Ok(message)),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
} else {
|
||||
Ok(EventOutcome::Ok(format!(
|
||||
"Action '{}' not implemented for this state type.",
|
||||
action
|
||||
)))
|
||||
}
|
||||
}
|
||||
_ => Ok(EventOutcome::Ok(format!("Common action '{}' not handled here.", action))),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn execute_edit_action<S: CanvasState>(
|
||||
action: &str,
|
||||
key: KeyEvent,
|
||||
state: &mut S,
|
||||
ideal_cursor_column: &mut usize,
|
||||
) -> Result<String> {
|
||||
match action {
|
||||
"insert_char" => {
|
||||
if let KeyCode::Char(c) = key.code {
|
||||
let cursor_pos = state.current_cursor_pos();
|
||||
let field_value = state.get_current_input_mut();
|
||||
let mut chars: Vec<char> = field_value.chars().collect();
|
||||
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_has_unsaved_changes(true);
|
||||
*ideal_cursor_column = state.current_cursor_pos();
|
||||
}
|
||||
} else {
|
||||
return Ok("Error: insert_char called without a char key."
|
||||
.to_string());
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"delete_char_backward" => {
|
||||
if state.current_cursor_pos() > 0 {
|
||||
let cursor_pos = state.current_cursor_pos();
|
||||
let field_value = state.get_current_input_mut();
|
||||
let mut chars: Vec<char> = field_value.chars().collect();
|
||||
if cursor_pos <= chars.len() {
|
||||
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);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"delete_char_forward" => {
|
||||
let cursor_pos = state.current_cursor_pos();
|
||||
let field_value = state.get_current_input_mut();
|
||||
let mut chars: Vec<char> = field_value.chars().collect();
|
||||
if cursor_pos < chars.len() {
|
||||
chars.remove(cursor_pos);
|
||||
*field_value = chars.into_iter().collect();
|
||||
state.set_has_unsaved_changes(true);
|
||||
*ideal_cursor_column = cursor_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"next_field" => {
|
||||
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("".to_string())
|
||||
}
|
||||
|
||||
"prev_field" => {
|
||||
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("".to_string())
|
||||
}
|
||||
|
||||
"move_left" => {
|
||||
let new_pos = state.current_cursor_pos().saturating_sub(1);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"move_right" => {
|
||||
let current_input = state.get_current_input();
|
||||
let current_pos = state.current_cursor_pos();
|
||||
if current_pos < current_input.len() {
|
||||
let new_pos = current_pos + 1;
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"move_up" => {
|
||||
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("".to_string())
|
||||
}
|
||||
|
||||
"move_down" => {
|
||||
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("".to_string())
|
||||
}
|
||||
|
||||
"move_line_start" => {
|
||||
state.set_current_cursor_pos(0);
|
||||
*ideal_cursor_column = 0;
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"move_line_end" => {
|
||||
let current_input = state.get_current_input();
|
||||
let new_pos = current_input.len();
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"move_first_line" => {
|
||||
let num_fields = state.fields().len();
|
||||
if num_fields > 0 {
|
||||
state.set_current_field(0);
|
||||
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("Moved to first field".to_string())
|
||||
}
|
||||
|
||||
"move_last_line" => {
|
||||
let num_fields = state.fields().len();
|
||||
if num_fields > 0 {
|
||||
let new_field = 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("Moved to last field".to_string())
|
||||
}
|
||||
|
||||
"move_word_next" => {
|
||||
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 = new_pos.min(current_input.len());
|
||||
state.set_current_cursor_pos(final_pos);
|
||||
*ideal_cursor_column = final_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
"move_word_end" => {
|
||||
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 = 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("".to_string())
|
||||
}
|
||||
|
||||
"move_word_prev" => {
|
||||
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("".to_string())
|
||||
}
|
||||
|
||||
"move_word_end_prev" => {
|
||||
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("Moved to previous word end".to_string())
|
||||
}
|
||||
|
||||
_ => Ok(format!("Unknown or unhandled edit action: {}", action)),
|
||||
}
|
||||
}
|
||||
|
||||
#[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
|
||||
}
|
||||
}
|
||||
|
||||
fn find_next_word_start(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let len = chars.len();
|
||||
if len == 0 || current_pos >= len {
|
||||
return len;
|
||||
}
|
||||
|
||||
let mut pos = current_pos;
|
||||
let initial_type = get_char_type(chars[pos]);
|
||||
|
||||
while pos < len && get_char_type(chars[pos]) == initial_type {
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
while pos < len && get_char_type(chars[pos]) == CharType::Whitespace {
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
pos
|
||||
}
|
||||
|
||||
fn find_word_end(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let len = chars.len();
|
||||
if len == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let mut pos = current_pos.min(len - 1);
|
||||
|
||||
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.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
|
||||
}
|
||||
|
||||
fn find_prev_word_end(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let len = chars.len();
|
||||
if len == 0 || 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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
while pos > 0 && get_char_type(chars[pos - 1]) == CharType::Whitespace {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
if pos > 0 {
|
||||
pos - 1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// src/functions/modes/read_only.rs
|
||||
|
||||
pub mod auth_ro;
|
||||
pub mod form_ro;
|
||||
pub mod add_table_ro;
|
||||
pub mod add_logic_ro;
|
||||
@@ -1,235 +0,0 @@
|
||||
// src/functions/modes/read_only/add_logic_ro.rs
|
||||
use crate::config::binds::key_sequences::KeySequenceTracker;
|
||||
use crate::state::pages::add_logic::AddLogicState; // Changed
|
||||
use crate::state::pages::canvas_state::CanvasState;
|
||||
use crate::state::app::state::AppState;
|
||||
use anyhow::Result;
|
||||
|
||||
// Word navigation helpers (get_char_type, find_next_word_start, etc.)
|
||||
// can be kept as they are generic.
|
||||
#[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 }
|
||||
}
|
||||
|
||||
fn find_next_word_start(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let len = chars.len();
|
||||
if len == 0 || current_pos >= len { return len; }
|
||||
let mut pos = current_pos;
|
||||
let initial_type = get_char_type(chars[pos]);
|
||||
while pos < len && get_char_type(chars[pos]) == initial_type { pos += 1; }
|
||||
while pos < len && get_char_type(chars[pos]) == CharType::Whitespace { pos += 1; }
|
||||
pos
|
||||
}
|
||||
|
||||
fn find_word_end(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let len = chars.len();
|
||||
if len == 0 { return 0; }
|
||||
let mut pos = current_pos.min(len - 1);
|
||||
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.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
|
||||
}
|
||||
|
||||
fn find_prev_word_end(text: &str, current_pos: usize) -> usize {
|
||||
let prev_start = find_prev_word_start(text, current_pos);
|
||||
if prev_start == 0 { return 0; }
|
||||
find_word_end(text, prev_start.saturating_sub(1))
|
||||
}
|
||||
|
||||
|
||||
/// Executes read-only actions for the AddLogic view canvas.
|
||||
pub async fn execute_action(
|
||||
action: &str,
|
||||
app_state: &mut AppState,
|
||||
state: &mut AddLogicState,
|
||||
ideal_cursor_column: &mut usize,
|
||||
key_sequence_tracker: &mut KeySequenceTracker,
|
||||
command_message: &mut String,
|
||||
) -> Result<String> {
|
||||
match action {
|
||||
"move_up" => {
|
||||
key_sequence_tracker.reset();
|
||||
let num_fields = AddLogicState::INPUT_FIELD_COUNT;
|
||||
if num_fields == 0 { return Ok("No fields.".to_string()); }
|
||||
let current_field = state.current_field();
|
||||
|
||||
if current_field > 0 {
|
||||
let new_field = current_field - 1;
|
||||
state.set_current_field(new_field);
|
||||
let current_input = state.get_current_input();
|
||||
let max_cursor_pos = if current_input.is_empty() { 0 } else { current_input.len().saturating_sub(1) };
|
||||
let new_pos = (*ideal_cursor_column).min(max_cursor_pos);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
} else {
|
||||
*command_message = "At top of form.".to_string();
|
||||
}
|
||||
Ok(command_message.clone())
|
||||
}
|
||||
"move_down" => {
|
||||
key_sequence_tracker.reset();
|
||||
let num_fields = AddLogicState::INPUT_FIELD_COUNT;
|
||||
if num_fields == 0 { return Ok("No fields.".to_string()); }
|
||||
let current_field = state.current_field();
|
||||
let last_field_index = num_fields - 1;
|
||||
|
||||
if current_field < last_field_index {
|
||||
let new_field = current_field + 1;
|
||||
state.set_current_field(new_field);
|
||||
let current_input = state.get_current_input();
|
||||
let max_cursor_pos = if current_input.is_empty() { 0 } else { current_input.len().saturating_sub(1) };
|
||||
let new_pos = (*ideal_cursor_column).min(max_cursor_pos);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
} else {
|
||||
// Move focus outside canvas when moving down from the last field
|
||||
// FIX: Go to ScriptContentPreview instead of SaveButton
|
||||
app_state.ui.focus_outside_canvas = true;
|
||||
state.last_canvas_field = 2;
|
||||
state.current_focus = crate::state::pages::add_logic::AddLogicFocus::ScriptContentPreview; // FIXED!
|
||||
*command_message = "Focus moved to script preview".to_string();
|
||||
}
|
||||
Ok(command_message.clone())
|
||||
}
|
||||
// ... (rest of the actions remain the same) ...
|
||||
"move_first_line" => {
|
||||
key_sequence_tracker.reset();
|
||||
if AddLogicState::INPUT_FIELD_COUNT > 0 {
|
||||
state.set_current_field(0);
|
||||
let current_input = state.get_current_input();
|
||||
let max_cursor_pos = if current_input.is_empty() { 0 } else { current_input.len().saturating_sub(1) };
|
||||
let new_pos = (*ideal_cursor_column).min(max_cursor_pos);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_last_line" => {
|
||||
key_sequence_tracker.reset();
|
||||
let num_fields = AddLogicState::INPUT_FIELD_COUNT;
|
||||
if num_fields > 0 {
|
||||
let last_field_index = num_fields - 1;
|
||||
state.set_current_field(last_field_index);
|
||||
let current_input = state.get_current_input();
|
||||
let max_cursor_pos = if current_input.is_empty() { 0 } else { current_input.len().saturating_sub(1) };
|
||||
let new_pos = (*ideal_cursor_column).min(max_cursor_pos);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_left" => {
|
||||
let current_pos = state.current_cursor_pos();
|
||||
let new_pos = current_pos.saturating_sub(1);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_right" => {
|
||||
let current_input = state.get_current_input();
|
||||
let current_pos = state.current_cursor_pos();
|
||||
if !current_input.is_empty() && current_pos < current_input.len().saturating_sub(1) {
|
||||
let new_pos = current_pos + 1;
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_word_next" => {
|
||||
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 = new_pos.min(current_input.len().saturating_sub(1));
|
||||
state.set_current_cursor_pos(final_pos);
|
||||
*ideal_cursor_column = final_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_word_end" => {
|
||||
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 = if new_pos == current_pos && current_pos < current_input.len().saturating_sub(1) {
|
||||
find_word_end(current_input, current_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("".to_string())
|
||||
}
|
||||
"move_word_prev" => {
|
||||
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("".to_string())
|
||||
}
|
||||
"move_word_end_prev" => {
|
||||
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("".to_string())
|
||||
}
|
||||
"move_line_start" => {
|
||||
state.set_current_cursor_pos(0);
|
||||
*ideal_cursor_column = 0;
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_line_end" => {
|
||||
let current_input = state.get_current_input();
|
||||
if !current_input.is_empty() {
|
||||
let new_pos = current_input.len().saturating_sub(1);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
} else {
|
||||
state.set_current_cursor_pos(0);
|
||||
*ideal_cursor_column = 0;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
"enter_edit_mode_before" | "enter_edit_mode_after" | "enter_command_mode" | "exit_highlight_mode" => {
|
||||
key_sequence_tracker.reset();
|
||||
Ok("Mode change handled by main loop".to_string())
|
||||
}
|
||||
_ => {
|
||||
key_sequence_tracker.reset();
|
||||
command_message.clear();
|
||||
Ok(format!("Unknown read-only action: {}", action))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,267 +0,0 @@
|
||||
// src/functions/modes/read_only/add_table_ro.rs
|
||||
use crate::config::binds::key_sequences::KeySequenceTracker;
|
||||
use crate::state::pages::add_table::AddTableState;
|
||||
use crate::state::pages::canvas_state::CanvasState;
|
||||
use crate::state::app::state::AppState;
|
||||
use anyhow::Result;
|
||||
|
||||
// Re-use word navigation helpers if they are public or move them to a common module
|
||||
// For now, duplicating them here for simplicity. Consider refactoring later.
|
||||
#[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 }
|
||||
}
|
||||
|
||||
fn find_next_word_start(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let len = chars.len();
|
||||
if len == 0 || current_pos >= len { return len; }
|
||||
let mut pos = current_pos;
|
||||
let initial_type = get_char_type(chars[pos]);
|
||||
while pos < len && get_char_type(chars[pos]) == initial_type { pos += 1; }
|
||||
while pos < len && get_char_type(chars[pos]) == CharType::Whitespace { pos += 1; }
|
||||
pos
|
||||
}
|
||||
|
||||
fn find_word_end(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let len = chars.len();
|
||||
if len == 0 { return 0; }
|
||||
let mut pos = current_pos.min(len - 1);
|
||||
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.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
|
||||
}
|
||||
|
||||
// Note: find_prev_word_end might need adjustments based on desired behavior.
|
||||
// This version finds the end of the word *before* the previous word start.
|
||||
fn find_prev_word_end(text: &str, current_pos: usize) -> usize {
|
||||
let prev_start = find_prev_word_start(text, current_pos);
|
||||
if prev_start == 0 { return 0; }
|
||||
// Find the end of the word that starts at prev_start - 1
|
||||
find_word_end(text, prev_start.saturating_sub(1))
|
||||
}
|
||||
|
||||
|
||||
/// Executes read-only actions for the AddTable view canvas.
|
||||
pub async fn execute_action(
|
||||
action: &str,
|
||||
app_state: &mut AppState, // Needed for focus_outside_canvas
|
||||
state: &mut AddTableState,
|
||||
ideal_cursor_column: &mut usize,
|
||||
key_sequence_tracker: &mut KeySequenceTracker,
|
||||
command_message: &mut String, // Keep for potential messages
|
||||
) -> Result<String> {
|
||||
// Use the CanvasState trait methods implemented for AddTableState
|
||||
match action {
|
||||
"move_up" => {
|
||||
key_sequence_tracker.reset();
|
||||
let num_fields = AddTableState::INPUT_FIELD_COUNT;
|
||||
if num_fields == 0 {
|
||||
*command_message = "No fields.".to_string();
|
||||
return Ok(command_message.clone());
|
||||
}
|
||||
let current_field = state.current_field(); // Gets the index (0, 1, or 2)
|
||||
|
||||
if current_field > 0 {
|
||||
// This handles moving from field 2 -> 1, or 1 -> 0
|
||||
let new_field = current_field - 1;
|
||||
state.set_current_field(new_field);
|
||||
let current_input = state.get_current_input();
|
||||
let max_cursor_pos = current_input.len(); // Allow cursor at end
|
||||
let new_pos = (*ideal_cursor_column).min(max_cursor_pos);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos; // Update ideal column as cursor moved
|
||||
*command_message = "".to_string(); // Clear message for successful internal navigation
|
||||
} else {
|
||||
// current_field is 0 (InputTableName), and user pressed Up.
|
||||
// Forbid moving up. Do not change focus or cursor.
|
||||
*command_message = "At top of form.".to_string();
|
||||
}
|
||||
Ok(command_message.clone())
|
||||
}
|
||||
"move_down" => {
|
||||
key_sequence_tracker.reset();
|
||||
let num_fields = AddTableState::INPUT_FIELD_COUNT;
|
||||
if num_fields == 0 {
|
||||
*command_message = "No fields.".to_string();
|
||||
return Ok(command_message.clone());
|
||||
}
|
||||
let current_field = state.current_field();
|
||||
let last_field_index = num_fields - 1;
|
||||
|
||||
if current_field < last_field_index {
|
||||
let new_field = current_field + 1;
|
||||
state.set_current_field(new_field);
|
||||
let current_input = state.get_current_input();
|
||||
let max_cursor_pos = current_input.len(); // Allow cursor at end
|
||||
let new_pos = (*ideal_cursor_column).min(max_cursor_pos);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos; // Update ideal column
|
||||
*command_message = "".to_string();
|
||||
} else {
|
||||
// Move focus outside canvas when moving down from the last field
|
||||
app_state.ui.focus_outside_canvas = true;
|
||||
// Set focus to the first element outside canvas (AddColumnButton)
|
||||
state.current_focus =
|
||||
crate::state::pages::add_table::AddTableFocus::AddColumnButton;
|
||||
*command_message = "Focus moved below canvas".to_string();
|
||||
}
|
||||
Ok(command_message.clone())
|
||||
}
|
||||
// ... (other actions like "move_first_line", "move_left", etc. remain the same) ...
|
||||
"move_first_line" => {
|
||||
key_sequence_tracker.reset();
|
||||
if AddTableState::INPUT_FIELD_COUNT > 0 {
|
||||
state.set_current_field(0);
|
||||
let current_input = state.get_current_input();
|
||||
let max_cursor_pos = current_input.len();
|
||||
let new_pos = (*ideal_cursor_column).min(max_cursor_pos);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos; // Update ideal column
|
||||
}
|
||||
*command_message = "".to_string();
|
||||
Ok(command_message.clone())
|
||||
}
|
||||
"move_last_line" => {
|
||||
key_sequence_tracker.reset();
|
||||
let num_fields = AddTableState::INPUT_FIELD_COUNT;
|
||||
if num_fields > 0 {
|
||||
let last_field_index = num_fields - 1;
|
||||
state.set_current_field(last_field_index);
|
||||
let current_input = state.get_current_input();
|
||||
let max_cursor_pos = current_input.len();
|
||||
let new_pos = (*ideal_cursor_column).min(max_cursor_pos);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos; // Update ideal column
|
||||
}
|
||||
*command_message = "".to_string();
|
||||
Ok(command_message.clone())
|
||||
}
|
||||
"move_left" => {
|
||||
let current_pos = state.current_cursor_pos();
|
||||
let new_pos = current_pos.saturating_sub(1);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
*command_message = "".to_string();
|
||||
Ok(command_message.clone())
|
||||
}
|
||||
"move_right" => {
|
||||
let current_input = state.get_current_input();
|
||||
let current_pos = state.current_cursor_pos();
|
||||
// Allow moving cursor one position past the end
|
||||
if current_pos < current_input.len() {
|
||||
let new_pos = current_pos + 1;
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
*command_message = "".to_string();
|
||||
Ok(command_message.clone())
|
||||
}
|
||||
"move_word_next" => {
|
||||
let current_input = state.get_current_input();
|
||||
let new_pos = find_next_word_start(
|
||||
current_input,
|
||||
state.current_cursor_pos(),
|
||||
);
|
||||
let final_pos = new_pos.min(current_input.len()); // Allow cursor at end
|
||||
state.set_current_cursor_pos(final_pos);
|
||||
*ideal_cursor_column = final_pos;
|
||||
*command_message = "".to_string();
|
||||
Ok(command_message.clone())
|
||||
}
|
||||
"move_word_end" => {
|
||||
let current_input = state.get_current_input();
|
||||
let current_pos = state.current_cursor_pos();
|
||||
let new_pos = find_word_end(current_input, current_pos);
|
||||
// If find_word_end returns current_pos, try starting search from next char
|
||||
let final_pos =
|
||||
if new_pos == current_pos && current_pos < current_input.len() {
|
||||
find_word_end(current_input, current_pos + 1)
|
||||
} else {
|
||||
new_pos
|
||||
};
|
||||
let max_valid_index = current_input.len(); // Allow cursor at end
|
||||
let clamped_pos = final_pos.min(max_valid_index);
|
||||
state.set_current_cursor_pos(clamped_pos);
|
||||
*ideal_cursor_column = clamped_pos;
|
||||
*command_message = "".to_string();
|
||||
Ok(command_message.clone())
|
||||
}
|
||||
"move_word_prev" => {
|
||||
let current_input = state.get_current_input();
|
||||
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;
|
||||
*command_message = "".to_string();
|
||||
Ok(command_message.clone())
|
||||
}
|
||||
"move_word_end_prev" => {
|
||||
let current_input = state.get_current_input();
|
||||
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;
|
||||
*command_message = "".to_string();
|
||||
Ok(command_message.clone())
|
||||
}
|
||||
"move_line_start" => {
|
||||
state.set_current_cursor_pos(0);
|
||||
*ideal_cursor_column = 0;
|
||||
*command_message = "".to_string();
|
||||
Ok(command_message.clone())
|
||||
}
|
||||
"move_line_end" => {
|
||||
let current_input = state.get_current_input();
|
||||
let new_pos = current_input.len(); // Allow cursor at end
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
*command_message = "".to_string();
|
||||
Ok(command_message.clone())
|
||||
}
|
||||
// Actions handled by main event loop (mode changes)
|
||||
"enter_edit_mode_before" | "enter_edit_mode_after"
|
||||
| "enter_command_mode" | "exit_highlight_mode" => {
|
||||
key_sequence_tracker.reset();
|
||||
// These actions are primarily mode changes handled by the main event loop.
|
||||
// The message here might be overridden by the main loop's message for mode change.
|
||||
*command_message = "Mode change initiated".to_string();
|
||||
Ok(command_message.clone())
|
||||
}
|
||||
_ => {
|
||||
key_sequence_tracker.reset();
|
||||
*command_message =
|
||||
format!("Unknown read-only action: {}", action);
|
||||
Ok(command_message.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,343 +0,0 @@
|
||||
// src/functions/modes/read_only/auth_ro.rs
|
||||
|
||||
use crate::config::binds::key_sequences::KeySequenceTracker;
|
||||
use crate::state::app::state::AppState;
|
||||
use canvas::canvas::CanvasState;
|
||||
use anyhow::Result;
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum CharType {
|
||||
Whitespace,
|
||||
Alphanumeric,
|
||||
Punctuation,
|
||||
}
|
||||
|
||||
pub async fn execute_action<S: CanvasState>(
|
||||
action: &str,
|
||||
app_state: &mut AppState,
|
||||
state: &mut S,
|
||||
ideal_cursor_column: &mut usize,
|
||||
key_sequence_tracker: &mut KeySequenceTracker,
|
||||
command_message: &mut String,
|
||||
) -> Result<String> {
|
||||
match action {
|
||||
"previous_entry" | "next_entry" => {
|
||||
key_sequence_tracker.reset();
|
||||
Ok(format!(
|
||||
"Action '{}' should be handled by context-specific logic",
|
||||
action
|
||||
))
|
||||
}
|
||||
"move_up" => {
|
||||
key_sequence_tracker.reset();
|
||||
let num_fields = state.fields().len();
|
||||
if num_fields == 0 {
|
||||
return Ok("No fields to navigate.".to_string());
|
||||
}
|
||||
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_cursor_pos = if current_input.is_empty() {
|
||||
0
|
||||
} else {
|
||||
current_input.len().saturating_sub(1)
|
||||
};
|
||||
let new_pos = (*ideal_cursor_column).min(max_cursor_pos);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
|
||||
Ok("move up from functions/modes/read_only/auth_ro.rs".to_string())
|
||||
}
|
||||
"move_down" => {
|
||||
key_sequence_tracker.reset();
|
||||
let num_fields = state.fields().len();
|
||||
if num_fields == 0 {
|
||||
return Ok("No fields to navigate.".to_string());
|
||||
}
|
||||
let current_field = state.current_field();
|
||||
let last_field_index = num_fields - 1;
|
||||
|
||||
if current_field == last_field_index {
|
||||
// Already on the last field, move focus outside
|
||||
app_state.ui.focus_outside_canvas = true;
|
||||
app_state.focused_button_index= 0;
|
||||
key_sequence_tracker.reset();
|
||||
Ok("Focus moved below canvas".to_string())
|
||||
} else {
|
||||
// Move to the next field within the canvas
|
||||
let new_field = (current_field + 1).min(last_field_index);
|
||||
state.set_current_field(new_field);
|
||||
let current_input = state.get_current_input();
|
||||
let max_cursor_pos = if current_input.is_empty() {
|
||||
0
|
||||
} else {
|
||||
current_input.len().saturating_sub(1)
|
||||
};
|
||||
let new_pos = (*ideal_cursor_column).min(max_cursor_pos);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
Ok("".to_string()) // Clear previous debug message
|
||||
}
|
||||
}
|
||||
"move_first_line" => {
|
||||
key_sequence_tracker.reset();
|
||||
let num_fields = state.fields().len();
|
||||
if num_fields == 0 {
|
||||
return Ok("No fields to navigate to.".to_string());
|
||||
}
|
||||
state.set_current_field(0);
|
||||
let current_input = state.get_current_input();
|
||||
let max_cursor_pos = if current_input.is_empty() {
|
||||
0
|
||||
} else {
|
||||
current_input.len().saturating_sub(1)
|
||||
};
|
||||
let new_pos = (*ideal_cursor_column).min(max_cursor_pos);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_last_line" => {
|
||||
key_sequence_tracker.reset();
|
||||
let num_fields = state.fields().len();
|
||||
if num_fields == 0 {
|
||||
return Ok("No fields to navigate to.".to_string());
|
||||
}
|
||||
let last_field_index = num_fields - 1;
|
||||
state.set_current_field(last_field_index);
|
||||
let current_input = state.get_current_input();
|
||||
let max_cursor_pos = if current_input.is_empty() {
|
||||
0
|
||||
} else {
|
||||
current_input.len().saturating_sub(1)
|
||||
};
|
||||
let new_pos = (*ideal_cursor_column).min(max_cursor_pos);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
Ok("".to_string())
|
||||
}
|
||||
"exit_edit_mode" => {
|
||||
key_sequence_tracker.reset();
|
||||
command_message.clear();
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_left" => {
|
||||
let current_pos = state.current_cursor_pos();
|
||||
let new_pos = current_pos.saturating_sub(1);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_right" => {
|
||||
let current_input = state.get_current_input();
|
||||
let current_pos = state.current_cursor_pos();
|
||||
if !current_input.is_empty()
|
||||
&& current_pos < current_input.len().saturating_sub(1)
|
||||
{
|
||||
let new_pos = current_pos + 1;
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_word_next" => {
|
||||
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 = new_pos.min(current_input.len().saturating_sub(1));
|
||||
state.set_current_cursor_pos(final_pos);
|
||||
*ideal_cursor_column = final_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_word_end" => {
|
||||
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 = if new_pos != current_pos {
|
||||
new_pos
|
||||
} else {
|
||||
find_word_end(current_input, new_pos + 1)
|
||||
};
|
||||
|
||||
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("".to_string())
|
||||
}
|
||||
"move_word_prev" => {
|
||||
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("".to_string())
|
||||
}
|
||||
"move_word_end_prev" => {
|
||||
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("Moved to previous word end".to_string())
|
||||
}
|
||||
"move_line_start" => {
|
||||
state.set_current_cursor_pos(0);
|
||||
*ideal_cursor_column = 0;
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_line_end" => {
|
||||
let current_input = state.get_current_input();
|
||||
if !current_input.is_empty() {
|
||||
let new_pos = current_input.len().saturating_sub(1);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
} else {
|
||||
state.set_current_cursor_pos(0);
|
||||
*ideal_cursor_column = 0;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
_ => {
|
||||
key_sequence_tracker.reset();
|
||||
Ok(format!("Unknown read-only action: {}", action))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
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]);
|
||||
|
||||
while pos < chars.len() && get_char_type(chars[pos]) == initial_type {
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
while pos < chars.len() && get_char_type(chars[pos]) == CharType::Whitespace {
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
pos
|
||||
}
|
||||
|
||||
fn find_word_end(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = 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 current_type != CharType::Whitespace {
|
||||
while pos < len && get_char_type(chars[pos]) == current_type {
|
||||
pos += 1;
|
||||
}
|
||||
return pos.saturating_sub(1);
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
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 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
|
||||
}
|
||||
}
|
||||
|
||||
fn find_prev_word_end(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[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;
|
||||
}
|
||||
|
||||
while pos > 0 && get_char_type(chars[pos - 1]) == CharType::Whitespace {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
if pos > 0 {
|
||||
pos - 1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
@@ -1,329 +0,0 @@
|
||||
// src/functions/modes/read_only/form_ro.rs
|
||||
|
||||
use crate::config::binds::key_sequences::KeySequenceTracker;
|
||||
use canvas::canvas::CanvasState;
|
||||
use anyhow::Result;
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum CharType {
|
||||
Whitespace,
|
||||
Alphanumeric,
|
||||
Punctuation,
|
||||
}
|
||||
|
||||
pub async fn execute_action<S: CanvasState>(
|
||||
action: &str,
|
||||
state: &mut S,
|
||||
ideal_cursor_column: &mut usize,
|
||||
key_sequence_tracker: &mut KeySequenceTracker,
|
||||
command_message: &mut String,
|
||||
) -> Result<String> {
|
||||
match action {
|
||||
"previous_entry" | "next_entry" => {
|
||||
key_sequence_tracker.reset();
|
||||
Ok(format!(
|
||||
"Action '{}' should be handled by context-specific logic",
|
||||
action
|
||||
))
|
||||
}
|
||||
"move_up" => {
|
||||
key_sequence_tracker.reset();
|
||||
let num_fields = state.fields().len();
|
||||
if num_fields == 0 {
|
||||
return Ok("No fields to navigate.".to_string());
|
||||
}
|
||||
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_cursor_pos = if current_input.is_empty() {
|
||||
0
|
||||
} else {
|
||||
current_input.len().saturating_sub(1)
|
||||
};
|
||||
let new_pos = (*ideal_cursor_column).min(max_cursor_pos);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_down" => {
|
||||
key_sequence_tracker.reset();
|
||||
let num_fields = state.fields().len();
|
||||
if num_fields == 0 {
|
||||
return Ok("No fields to navigate.".to_string());
|
||||
}
|
||||
let current_field = state.current_field();
|
||||
let new_field = (current_field + 1).min(num_fields - 1);
|
||||
state.set_current_field(new_field);
|
||||
let current_input = state.get_current_input();
|
||||
let max_cursor_pos = if current_input.is_empty() {
|
||||
0
|
||||
} else {
|
||||
current_input.len().saturating_sub(1)
|
||||
};
|
||||
let new_pos = (*ideal_cursor_column).min(max_cursor_pos);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_first_line" => {
|
||||
key_sequence_tracker.reset();
|
||||
let num_fields = state.fields().len();
|
||||
if num_fields == 0 {
|
||||
return Ok("No fields to navigate to.".to_string());
|
||||
}
|
||||
state.set_current_field(0);
|
||||
let current_input = state.get_current_input();
|
||||
let max_cursor_pos = if current_input.is_empty() {
|
||||
0
|
||||
} else {
|
||||
current_input.len().saturating_sub(1)
|
||||
};
|
||||
let new_pos = (*ideal_cursor_column).min(max_cursor_pos);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_last_line" => {
|
||||
key_sequence_tracker.reset();
|
||||
let num_fields = state.fields().len();
|
||||
if num_fields == 0 {
|
||||
return Ok("No fields to navigate to.".to_string());
|
||||
}
|
||||
let last_field_index = num_fields - 1;
|
||||
state.set_current_field(last_field_index);
|
||||
let current_input = state.get_current_input();
|
||||
let max_cursor_pos = if current_input.is_empty() {
|
||||
0
|
||||
} else {
|
||||
current_input.len().saturating_sub(1)
|
||||
};
|
||||
let new_pos = (*ideal_cursor_column).min(max_cursor_pos);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
Ok("".to_string())
|
||||
}
|
||||
"exit_edit_mode" => {
|
||||
key_sequence_tracker.reset();
|
||||
command_message.clear();
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_left" => {
|
||||
let current_pos = state.current_cursor_pos();
|
||||
let new_pos = current_pos.saturating_sub(1);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_right" => {
|
||||
let current_input = state.get_current_input();
|
||||
let current_pos = state.current_cursor_pos();
|
||||
if !current_input.is_empty()
|
||||
&& current_pos < current_input.len().saturating_sub(1)
|
||||
{
|
||||
let new_pos = current_pos + 1;
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_word_next" => {
|
||||
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 = new_pos.min(current_input.len().saturating_sub(1));
|
||||
state.set_current_cursor_pos(final_pos);
|
||||
*ideal_cursor_column = final_pos;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_word_end" => {
|
||||
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 = if new_pos != current_pos {
|
||||
new_pos
|
||||
} else {
|
||||
find_word_end(current_input, new_pos + 1)
|
||||
};
|
||||
|
||||
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("".to_string())
|
||||
}
|
||||
"move_word_prev" => {
|
||||
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("".to_string())
|
||||
}
|
||||
"move_word_end_prev" => {
|
||||
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("Moved to previous word end".to_string())
|
||||
}
|
||||
"move_line_start" => {
|
||||
state.set_current_cursor_pos(0);
|
||||
*ideal_cursor_column = 0;
|
||||
Ok("".to_string())
|
||||
}
|
||||
"move_line_end" => {
|
||||
let current_input = state.get_current_input();
|
||||
if !current_input.is_empty() {
|
||||
let new_pos = current_input.len().saturating_sub(1);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
} else {
|
||||
state.set_current_cursor_pos(0);
|
||||
*ideal_cursor_column = 0;
|
||||
}
|
||||
Ok("".to_string())
|
||||
}
|
||||
_ => {
|
||||
key_sequence_tracker.reset();
|
||||
Ok(format!("Unknown read-only action: {}", action))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
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]);
|
||||
|
||||
while pos < chars.len() && get_char_type(chars[pos]) == initial_type {
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
while pos < chars.len() && get_char_type(chars[pos]) == CharType::Whitespace {
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
pos
|
||||
}
|
||||
|
||||
fn find_word_end(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = 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 current_type != CharType::Whitespace {
|
||||
while pos < len && get_char_type(chars[pos]) == current_type {
|
||||
pos += 1;
|
||||
}
|
||||
return pos.saturating_sub(1);
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
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 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
|
||||
}
|
||||
}
|
||||
|
||||
fn find_prev_word_end(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[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;
|
||||
}
|
||||
|
||||
while pos > 0 && get_char_type(chars[pos - 1]) == CharType::Whitespace {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
if pos > 0 {
|
||||
pos - 1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
// src/modes/canvas/edit.rs
|
||||
use crate::config::binds::config::Config;
|
||||
use crate::functions::modes::edit::{
|
||||
add_logic_e, add_table_e, auth_e, form_e,
|
||||
};
|
||||
use crate::modes::handlers::event::EventHandler;
|
||||
use crate::services::grpc_client::GrpcClient;
|
||||
use crate::state::app::state::AppState;
|
||||
@@ -127,6 +124,77 @@ pub async fn handle_form_edit_with_canvas(
|
||||
Ok(String::new())
|
||||
}
|
||||
|
||||
/// Helper function to execute a specific action using canvas library
|
||||
async fn execute_canvas_action(
|
||||
action: &str,
|
||||
key: KeyEvent,
|
||||
form_state: &mut FormState,
|
||||
ideal_cursor_column: &mut usize,
|
||||
) -> Result<String> {
|
||||
let canvas_action = CanvasAction::from_string(action);
|
||||
match ActionDispatcher::dispatch(canvas_action, form_state, ideal_cursor_column).await {
|
||||
Ok(ActionResult::Success(msg)) => Ok(msg.unwrap_or_default()),
|
||||
Ok(ActionResult::HandledByFeature(msg)) => Ok(msg),
|
||||
Ok(ActionResult::Error(msg)) => Ok(format!("Error: {}", msg)),
|
||||
Ok(ActionResult::RequiresContext(msg)) => Ok(format!("Context needed: {}", msg)),
|
||||
Err(e) => Ok(format!("Action failed: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
/// NEW: Unified canvas action handler for any CanvasState (LoginState, RegisterState, etc.)
|
||||
/// This replaces the old auth_e::execute_edit_action calls with the new canvas library
|
||||
async fn handle_canvas_state_edit<S: CanvasState>(
|
||||
key: KeyEvent,
|
||||
config: &Config,
|
||||
state: &mut S,
|
||||
ideal_cursor_column: &mut usize,
|
||||
) -> Result<String> {
|
||||
// Try direct key mapping first (same pattern as FormState)
|
||||
if let Some(canvas_action) = CanvasAction::from_key(key.code) {
|
||||
match ActionDispatcher::dispatch(canvas_action, state, ideal_cursor_column).await {
|
||||
Ok(ActionResult::Success(msg)) => {
|
||||
return Ok(msg.unwrap_or_default());
|
||||
}
|
||||
Ok(ActionResult::HandledByFeature(msg)) => {
|
||||
return Ok(msg);
|
||||
}
|
||||
Ok(ActionResult::Error(msg)) => {
|
||||
return Ok(format!("Error: {}", msg));
|
||||
}
|
||||
Ok(ActionResult::RequiresContext(msg)) => {
|
||||
return Ok(format!("Context needed: {}", msg));
|
||||
}
|
||||
Err(_) => {
|
||||
// Fall through to try config mapping
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try config-mapped action (same pattern as FormState)
|
||||
if let Some(action_str) = config.get_edit_action_for_key(key.code, key.modifiers) {
|
||||
let canvas_action = CanvasAction::from_string(&action_str);
|
||||
match ActionDispatcher::dispatch(canvas_action, state, ideal_cursor_column).await {
|
||||
Ok(ActionResult::Success(msg)) => {
|
||||
return Ok(msg.unwrap_or_default());
|
||||
}
|
||||
Ok(ActionResult::HandledByFeature(msg)) => {
|
||||
return Ok(msg);
|
||||
}
|
||||
Ok(ActionResult::Error(msg)) => {
|
||||
return Ok(format!("Error: {}", msg));
|
||||
}
|
||||
Ok(ActionResult::RequiresContext(msg)) => {
|
||||
return Ok(format!("Context needed: {}", msg));
|
||||
}
|
||||
Err(e) => {
|
||||
return Ok(format!("Action failed: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(String::new())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn handle_edit_event(
|
||||
key: KeyEvent,
|
||||
@@ -237,8 +305,8 @@ pub async fn handle_edit_event(
|
||||
} else {
|
||||
"insert_char"
|
||||
};
|
||||
// FIX: Pass &mut event_handler.ideal_cursor_column
|
||||
form_e::execute_edit_action(
|
||||
// FIXED: Use canvas library instead of form_e::execute_edit_action
|
||||
execute_canvas_action(
|
||||
action,
|
||||
key,
|
||||
form_state,
|
||||
@@ -267,8 +335,8 @@ pub async fn handle_edit_event(
|
||||
{
|
||||
// Handle Enter key (next field)
|
||||
if action_str == "enter_decider" {
|
||||
// FIX: Pass &mut event_handler.ideal_cursor_column
|
||||
let msg = form_e::execute_edit_action(
|
||||
// FIXED: Use canvas library instead of form_e::execute_edit_action
|
||||
let msg = execute_canvas_action(
|
||||
"next_field",
|
||||
key,
|
||||
form_state,
|
||||
@@ -283,46 +351,46 @@ pub async fn handle_edit_event(
|
||||
return Ok(EditEventOutcome::ExitEditMode);
|
||||
}
|
||||
|
||||
// Handle all other edit actions
|
||||
// Handle all other edit actions - NOW USING CANVAS LIBRARY
|
||||
let msg = if app_state.ui.show_login {
|
||||
// FIX: Pass &mut event_handler.ideal_cursor_column
|
||||
auth_e::execute_edit_action(
|
||||
action_str,
|
||||
// NEW: Use unified canvas handler instead of auth_e::execute_edit_action
|
||||
handle_canvas_state_edit(
|
||||
key,
|
||||
config,
|
||||
login_state,
|
||||
&mut event_handler.ideal_cursor_column,
|
||||
)
|
||||
.await?
|
||||
} else if app_state.ui.show_add_table {
|
||||
// FIX: Pass &mut event_handler.ideal_cursor_column
|
||||
add_table_e::execute_edit_action(
|
||||
action_str,
|
||||
// NEW: Use unified canvas handler instead of add_table_e::execute_edit_action
|
||||
handle_canvas_state_edit(
|
||||
key,
|
||||
config,
|
||||
&mut admin_state.add_table_state,
|
||||
&mut event_handler.ideal_cursor_column,
|
||||
)
|
||||
.await?
|
||||
} else if app_state.ui.show_add_logic {
|
||||
// FIX: Pass &mut event_handler.ideal_cursor_column
|
||||
add_logic_e::execute_edit_action(
|
||||
action_str,
|
||||
// NEW: Use unified canvas handler instead of add_logic_e::execute_edit_action
|
||||
handle_canvas_state_edit(
|
||||
key,
|
||||
config,
|
||||
&mut admin_state.add_logic_state,
|
||||
&mut event_handler.ideal_cursor_column,
|
||||
)
|
||||
.await?
|
||||
} else if app_state.ui.show_register {
|
||||
// FIX: Pass &mut event_handler.ideal_cursor_column
|
||||
auth_e::execute_edit_action(
|
||||
action_str,
|
||||
// NEW: Use unified canvas handler instead of auth_e::execute_edit_action
|
||||
handle_canvas_state_edit(
|
||||
key,
|
||||
config,
|
||||
register_state,
|
||||
&mut event_handler.ideal_cursor_column,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
// FIX: Pass &mut event_handler.ideal_cursor_column
|
||||
form_e::execute_edit_action(
|
||||
// FIXED: Use canvas library instead of form_e::execute_edit_action
|
||||
execute_canvas_action(
|
||||
action_str,
|
||||
key,
|
||||
form_state,
|
||||
@@ -336,44 +404,44 @@ pub async fn handle_edit_event(
|
||||
// --- FALLBACK FOR CHARACTER INSERTION (IF NO OTHER BINDING MATCHED) ---
|
||||
if let KeyCode::Char(_) = key.code {
|
||||
let msg = if app_state.ui.show_login {
|
||||
// FIX: Pass &mut event_handler.ideal_cursor_column
|
||||
auth_e::execute_edit_action(
|
||||
"insert_char",
|
||||
// NEW: Use unified canvas handler instead of auth_e::execute_edit_action
|
||||
handle_canvas_state_edit(
|
||||
key,
|
||||
config,
|
||||
login_state,
|
||||
&mut event_handler.ideal_cursor_column,
|
||||
)
|
||||
.await?
|
||||
} else if app_state.ui.show_add_table {
|
||||
// FIX: Pass &mut event_handler.ideal_cursor_column
|
||||
add_table_e::execute_edit_action(
|
||||
"insert_char",
|
||||
// NEW: Use unified canvas handler instead of add_table_e::execute_edit_action
|
||||
handle_canvas_state_edit(
|
||||
key,
|
||||
config,
|
||||
&mut admin_state.add_table_state,
|
||||
&mut event_handler.ideal_cursor_column,
|
||||
)
|
||||
.await?
|
||||
} else if app_state.ui.show_add_logic {
|
||||
// FIX: Pass &mut event_handler.ideal_cursor_column
|
||||
add_logic_e::execute_edit_action(
|
||||
"insert_char",
|
||||
// NEW: Use unified canvas handler instead of add_logic_e::execute_edit_action
|
||||
handle_canvas_state_edit(
|
||||
key,
|
||||
config,
|
||||
&mut admin_state.add_logic_state,
|
||||
&mut event_handler.ideal_cursor_column,
|
||||
)
|
||||
.await?
|
||||
} else if app_state.ui.show_register {
|
||||
// FIX: Pass &mut event_handler.ideal_cursor_column
|
||||
auth_e::execute_edit_action(
|
||||
"insert_char",
|
||||
// NEW: Use unified canvas handler instead of auth_e::execute_edit_action
|
||||
handle_canvas_state_edit(
|
||||
key,
|
||||
config,
|
||||
register_state,
|
||||
&mut event_handler.ideal_cursor_column,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
// FIX: Pass &mut event_handler.ideal_cursor_column
|
||||
form_e::execute_edit_action(
|
||||
// FIXED: Use canvas library instead of form_e::execute_edit_action
|
||||
execute_canvas_action(
|
||||
"insert_char",
|
||||
key,
|
||||
form_state,
|
||||
|
||||
@@ -5,16 +5,85 @@ use crate::config::binds::key_sequences::KeySequenceTracker;
|
||||
use crate::services::grpc_client::GrpcClient;
|
||||
use crate::state::pages::auth::LoginState;
|
||||
use crate::state::pages::auth::RegisterState;
|
||||
use crate::state::pages::canvas_state::CanvasState as LocalCanvasState;
|
||||
use crate::state::pages::form::FormState;
|
||||
use crate::state::pages::add_logic::AddLogicState;
|
||||
use crate::state::pages::add_table::AddTableState;
|
||||
use crate::state::app::state::AppState;
|
||||
use crate::functions::modes::read_only::{add_logic_ro, auth_ro, form_ro, add_table_ro};
|
||||
use canvas::{canvas::{CanvasAction, CanvasState, ActionResult}, dispatcher::ActionDispatcher};
|
||||
use crossterm::event::KeyEvent;
|
||||
use anyhow::Result;
|
||||
|
||||
/// Helper function to dispatch canvas action for any CanvasState
|
||||
async fn dispatch_canvas_action<S: CanvasState>(
|
||||
action: &str,
|
||||
state: &mut S,
|
||||
ideal_cursor_column: &mut usize,
|
||||
) -> String {
|
||||
let canvas_action = CanvasAction::from_string(action);
|
||||
match ActionDispatcher::dispatch(canvas_action, state, ideal_cursor_column).await {
|
||||
Ok(ActionResult::Success(msg)) => msg.unwrap_or_default(),
|
||||
Ok(ActionResult::HandledByFeature(msg)) => msg,
|
||||
Ok(ActionResult::Error(msg)) => format!("Error: {}", msg),
|
||||
Ok(ActionResult::RequiresContext(msg)) => format!("Context needed: {}", msg),
|
||||
Err(e) => format!("Action failed: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to dispatch canvas action to the appropriate state based on UI
|
||||
async fn dispatch_to_active_state(
|
||||
action: &str,
|
||||
app_state: &AppState,
|
||||
form_state: &mut FormState,
|
||||
login_state: &mut LoginState,
|
||||
register_state: &mut RegisterState,
|
||||
add_table_state: &mut AddTableState,
|
||||
add_logic_state: &mut AddLogicState,
|
||||
ideal_cursor_column: &mut usize,
|
||||
) -> String {
|
||||
if app_state.ui.show_add_table {
|
||||
dispatch_canvas_action(action, add_table_state, ideal_cursor_column).await
|
||||
} else if app_state.ui.show_add_logic {
|
||||
dispatch_canvas_action(action, add_logic_state, ideal_cursor_column).await
|
||||
} else if app_state.ui.show_register {
|
||||
dispatch_canvas_action(action, register_state, ideal_cursor_column).await
|
||||
} else if app_state.ui.show_login {
|
||||
dispatch_canvas_action(action, login_state, ideal_cursor_column).await
|
||||
} else {
|
||||
dispatch_canvas_action(action, form_state, ideal_cursor_column).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to handle context-specific actions that need special treatment
|
||||
async fn handle_context_action(
|
||||
action: &str,
|
||||
app_state: &AppState,
|
||||
form_state: &mut FormState,
|
||||
grpc_client: &mut GrpcClient,
|
||||
ideal_cursor_column: &mut usize,
|
||||
) -> Result<Option<String>> {
|
||||
const CONTEXT_ACTIONS_FORM: &[&str] = &[
|
||||
"previous_entry",
|
||||
"next_entry",
|
||||
];
|
||||
const CONTEXT_ACTIONS_LOGIN: &[&str] = &[
|
||||
"previous_entry",
|
||||
"next_entry",
|
||||
];
|
||||
|
||||
if app_state.ui.show_form && CONTEXT_ACTIONS_FORM.contains(&action) {
|
||||
Ok(Some(crate::tui::functions::form::handle_action(
|
||||
action,
|
||||
form_state,
|
||||
grpc_client,
|
||||
ideal_cursor_column,
|
||||
).await?))
|
||||
} else if app_state.ui.show_login && CONTEXT_ACTIONS_LOGIN.contains(&action) {
|
||||
Ok(Some(crate::tui::functions::login::handle_action(action).await?))
|
||||
} else {
|
||||
Ok(None) // Not a context action, use regular canvas dispatch
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_form_readonly_with_canvas(
|
||||
key_event: KeyEvent,
|
||||
config: &Config,
|
||||
@@ -89,8 +158,7 @@ pub async fn handle_read_only_event(
|
||||
}
|
||||
|
||||
if config.is_enter_edit_mode_after(key.code, key.modifiers) {
|
||||
// Determine target state to adjust cursor
|
||||
|
||||
// Determine target state to adjust cursor - all states now use CanvasState trait
|
||||
if app_state.ui.show_login {
|
||||
let current_input = login_state.get_current_input();
|
||||
let current_pos = login_state.current_cursor_pos();
|
||||
@@ -120,8 +188,7 @@ pub async fn handle_read_only_event(
|
||||
*ideal_cursor_column = add_table_state.current_cursor_pos();
|
||||
}
|
||||
} else {
|
||||
// Handle FormState (uses library CanvasState)
|
||||
use canvas::canvas::CanvasState as LibraryCanvasState; // Import at the top of the function
|
||||
// Handle FormState
|
||||
let current_input = form_state.get_current_input();
|
||||
let current_pos = form_state.current_cursor_pos();
|
||||
if !current_input.is_empty() && current_pos < current_input.len() {
|
||||
@@ -135,76 +202,31 @@ pub async fn handle_read_only_event(
|
||||
return Ok((false, command_message.clone()));
|
||||
}
|
||||
|
||||
const CONTEXT_ACTIONS_FORM: &[&str] = &[
|
||||
"previous_entry",
|
||||
"next_entry",
|
||||
];
|
||||
const CONTEXT_ACTIONS_LOGIN: &[&str] = &[
|
||||
"previous_entry",
|
||||
"next_entry",
|
||||
];
|
||||
|
||||
if key.modifiers.is_empty() {
|
||||
key_sequence_tracker.add_key(key.code);
|
||||
let sequence = key_sequence_tracker.get_sequence();
|
||||
|
||||
if let Some(action) = config.matches_key_sequence_generalized(&sequence).as_deref() {
|
||||
let result = if app_state.ui.show_form && CONTEXT_ACTIONS_FORM.contains(&action) {
|
||||
crate::tui::functions::form::handle_action(
|
||||
// Try context-specific actions first, otherwise use canvas dispatch
|
||||
let result = if let Some(context_result) = handle_context_action(
|
||||
action,
|
||||
app_state,
|
||||
form_state,
|
||||
grpc_client,
|
||||
ideal_cursor_column,
|
||||
).await? {
|
||||
context_result
|
||||
} else {
|
||||
dispatch_to_active_state(
|
||||
action,
|
||||
app_state,
|
||||
form_state,
|
||||
grpc_client,
|
||||
ideal_cursor_column,
|
||||
)
|
||||
.await?
|
||||
} else if app_state.ui.show_login && CONTEXT_ACTIONS_LOGIN.contains(&action) {
|
||||
crate::tui::functions::login::handle_action(action).await?
|
||||
} else if app_state.ui.show_add_table {
|
||||
add_table_ro::execute_action(
|
||||
action,
|
||||
app_state,
|
||||
login_state,
|
||||
register_state,
|
||||
add_table_state,
|
||||
ideal_cursor_column,
|
||||
key_sequence_tracker,
|
||||
command_message,
|
||||
).await?
|
||||
} else if app_state.ui.show_add_logic {
|
||||
add_logic_ro::execute_action(
|
||||
action,
|
||||
app_state,
|
||||
add_logic_state,
|
||||
ideal_cursor_column,
|
||||
key_sequence_tracker,
|
||||
command_message,
|
||||
).await?
|
||||
} else if app_state.ui.show_register{
|
||||
auth_ro::execute_action(
|
||||
action,
|
||||
app_state,
|
||||
register_state,
|
||||
ideal_cursor_column,
|
||||
key_sequence_tracker,
|
||||
command_message,
|
||||
).await?
|
||||
} else if app_state.ui.show_login {
|
||||
auth_ro::execute_action(
|
||||
action,
|
||||
app_state,
|
||||
login_state,
|
||||
ideal_cursor_column,
|
||||
key_sequence_tracker,
|
||||
command_message,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
form_ro::execute_action(
|
||||
action,
|
||||
form_state,
|
||||
ideal_cursor_column,
|
||||
key_sequence_tracker,
|
||||
command_message,
|
||||
)
|
||||
.await?
|
||||
).await
|
||||
};
|
||||
key_sequence_tracker.reset();
|
||||
return Ok((false, result));
|
||||
@@ -216,62 +238,26 @@ pub async fn handle_read_only_event(
|
||||
|
||||
if sequence.len() == 1 && !config.is_key_sequence_prefix(&sequence) {
|
||||
if let Some(action) = config.get_read_only_action_for_key(key.code, key.modifiers).as_deref() {
|
||||
let result = if app_state.ui.show_form && CONTEXT_ACTIONS_FORM.contains(&action) {
|
||||
crate::tui::functions::form::handle_action(
|
||||
// Try context-specific actions first, otherwise use canvas dispatch
|
||||
let result = if let Some(context_result) = handle_context_action(
|
||||
action,
|
||||
app_state,
|
||||
form_state,
|
||||
grpc_client,
|
||||
ideal_cursor_column,
|
||||
).await? {
|
||||
context_result
|
||||
} else {
|
||||
dispatch_to_active_state(
|
||||
action,
|
||||
app_state,
|
||||
form_state,
|
||||
grpc_client,
|
||||
ideal_cursor_column,
|
||||
)
|
||||
.await?
|
||||
} else if app_state.ui.show_login && CONTEXT_ACTIONS_LOGIN.contains(&action) {
|
||||
crate::tui::functions::login::handle_action(action).await?
|
||||
} else if app_state.ui.show_add_table {
|
||||
add_table_ro::execute_action(
|
||||
action,
|
||||
app_state,
|
||||
login_state,
|
||||
register_state,
|
||||
add_table_state,
|
||||
ideal_cursor_column,
|
||||
key_sequence_tracker,
|
||||
command_message,
|
||||
).await?
|
||||
} else if app_state.ui.show_add_logic {
|
||||
add_logic_ro::execute_action(
|
||||
action,
|
||||
app_state,
|
||||
add_logic_state,
|
||||
ideal_cursor_column,
|
||||
key_sequence_tracker,
|
||||
command_message,
|
||||
).await?
|
||||
} else if app_state.ui.show_register {
|
||||
auth_ro::execute_action(
|
||||
action,
|
||||
app_state,
|
||||
register_state,
|
||||
ideal_cursor_column,
|
||||
key_sequence_tracker,
|
||||
command_message,
|
||||
).await?
|
||||
} else if app_state.ui.show_login {
|
||||
auth_ro::execute_action(
|
||||
action,
|
||||
app_state,
|
||||
login_state,
|
||||
ideal_cursor_column,
|
||||
key_sequence_tracker,
|
||||
command_message,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
form_ro::execute_action(
|
||||
action,
|
||||
form_state,
|
||||
ideal_cursor_column,
|
||||
key_sequence_tracker,
|
||||
command_message,
|
||||
)
|
||||
.await?
|
||||
).await
|
||||
};
|
||||
key_sequence_tracker.reset();
|
||||
return Ok((false, result));
|
||||
@@ -282,62 +268,26 @@ pub async fn handle_read_only_event(
|
||||
key_sequence_tracker.reset();
|
||||
|
||||
if let Some(action) = config.get_read_only_action_for_key(key.code, key.modifiers).as_deref() {
|
||||
let result = if app_state.ui.show_form && CONTEXT_ACTIONS_FORM.contains(&action) {
|
||||
crate::tui::functions::form::handle_action(
|
||||
// Try context-specific actions first, otherwise use canvas dispatch
|
||||
let result = if let Some(context_result) = handle_context_action(
|
||||
action,
|
||||
app_state,
|
||||
form_state,
|
||||
grpc_client,
|
||||
ideal_cursor_column,
|
||||
).await? {
|
||||
context_result
|
||||
} else {
|
||||
dispatch_to_active_state(
|
||||
action,
|
||||
app_state,
|
||||
form_state,
|
||||
grpc_client,
|
||||
ideal_cursor_column,
|
||||
)
|
||||
.await?
|
||||
} else if app_state.ui.show_login && CONTEXT_ACTIONS_LOGIN.contains(&action) {
|
||||
crate::tui::functions::login::handle_action(action).await?
|
||||
} else if app_state.ui.show_add_table {
|
||||
add_table_ro::execute_action(
|
||||
action,
|
||||
app_state,
|
||||
login_state,
|
||||
register_state,
|
||||
add_table_state,
|
||||
ideal_cursor_column,
|
||||
key_sequence_tracker,
|
||||
command_message,
|
||||
).await?
|
||||
} else if app_state.ui.show_add_logic {
|
||||
add_logic_ro::execute_action(
|
||||
action,
|
||||
app_state,
|
||||
add_logic_state,
|
||||
ideal_cursor_column,
|
||||
key_sequence_tracker,
|
||||
command_message,
|
||||
).await?
|
||||
} else if app_state.ui.show_register {
|
||||
auth_ro::execute_action(
|
||||
action,
|
||||
app_state,
|
||||
register_state,
|
||||
ideal_cursor_column,
|
||||
key_sequence_tracker,
|
||||
command_message,
|
||||
).await?
|
||||
} else if app_state.ui.show_login {
|
||||
auth_ro::execute_action(
|
||||
action,
|
||||
app_state,
|
||||
login_state,
|
||||
ideal_cursor_column,
|
||||
key_sequence_tracker,
|
||||
command_message,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
form_ro::execute_action(
|
||||
action,
|
||||
form_state,
|
||||
ideal_cursor_column,
|
||||
key_sequence_tracker,
|
||||
command_message,
|
||||
)
|
||||
.await?
|
||||
).await
|
||||
};
|
||||
return Ok((false, result));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// src/modes/handlers.rs
|
||||
pub mod event;
|
||||
pub mod event_helper;
|
||||
pub mod mode_manager;
|
||||
|
||||
@@ -15,23 +15,20 @@ use crate::modes::{
|
||||
general::{dialog, navigation},
|
||||
handlers::mode_manager::{AppMode, ModeManager},
|
||||
};
|
||||
use crate::state::pages::canvas_state::CanvasState as LegacyCanvasState;
|
||||
use crate::services::auth::AuthClient;
|
||||
use crate::services::grpc_client::GrpcClient;
|
||||
use canvas::{canvas::CanvasAction, dispatcher::ActionDispatcher};
|
||||
use canvas::canvas::CanvasState as LibraryCanvasState;
|
||||
use super::event_helper::*;
|
||||
use canvas::canvas::CanvasState; // Only need this import now
|
||||
use crate::state::{
|
||||
app::{
|
||||
buffer::{AppView, BufferState},
|
||||
highlight::HighlightState,
|
||||
search::SearchState, // Correctly imported
|
||||
search::SearchState,
|
||||
state::AppState,
|
||||
},
|
||||
pages::{
|
||||
admin::AdminState,
|
||||
auth::{AuthState, LoginState, RegisterState},
|
||||
canvas_state::CanvasState,
|
||||
form::FormState,
|
||||
intro::IntroState,
|
||||
},
|
||||
@@ -89,7 +86,6 @@ pub struct EventHandler {
|
||||
pub navigation_state: NavigationState,
|
||||
pub search_result_sender: mpsc::UnboundedSender<Vec<Hit>>,
|
||||
pub search_result_receiver: mpsc::UnboundedReceiver<Vec<Hit>>,
|
||||
// --- ADDED FOR LIVE AUTOCOMPLETE ---
|
||||
pub autocomplete_result_sender: mpsc::UnboundedSender<Vec<Hit>>,
|
||||
pub autocomplete_result_receiver: mpsc::UnboundedReceiver<Vec<Hit>>,
|
||||
}
|
||||
@@ -103,7 +99,7 @@ impl EventHandler {
|
||||
grpc_client: GrpcClient,
|
||||
) -> Result<Self> {
|
||||
let (search_tx, search_rx) = unbounded_channel();
|
||||
let (autocomplete_tx, autocomplete_rx) = unbounded_channel(); // ADDED
|
||||
let (autocomplete_tx, autocomplete_rx) = unbounded_channel();
|
||||
Ok(EventHandler {
|
||||
command_mode: false,
|
||||
command_input: String::new(),
|
||||
@@ -122,7 +118,6 @@ impl EventHandler {
|
||||
navigation_state: NavigationState::new(),
|
||||
search_result_sender: search_tx,
|
||||
search_result_receiver: search_rx,
|
||||
// --- ADDED ---
|
||||
autocomplete_result_sender: autocomplete_tx,
|
||||
autocomplete_result_receiver: autocomplete_rx,
|
||||
})
|
||||
@@ -136,6 +131,95 @@ impl EventHandler {
|
||||
self.navigation_state.activate_find_file(options);
|
||||
}
|
||||
|
||||
// Helper functions - replace the removed event_helper functions
|
||||
fn get_current_field_for_state(
|
||||
app_state: &AppState,
|
||||
login_state: &LoginState,
|
||||
register_state: &RegisterState,
|
||||
form_state: &FormState,
|
||||
) -> usize {
|
||||
if app_state.ui.show_login {
|
||||
login_state.current_field()
|
||||
} else if app_state.ui.show_register {
|
||||
register_state.current_field()
|
||||
} else {
|
||||
form_state.current_field()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_cursor_pos_for_state(
|
||||
app_state: &AppState,
|
||||
login_state: &LoginState,
|
||||
register_state: &RegisterState,
|
||||
form_state: &FormState,
|
||||
) -> usize {
|
||||
if app_state.ui.show_login {
|
||||
login_state.current_cursor_pos()
|
||||
} else if app_state.ui.show_register {
|
||||
register_state.current_cursor_pos()
|
||||
} else {
|
||||
form_state.current_cursor_pos()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_has_unsaved_changes_for_state(
|
||||
app_state: &AppState,
|
||||
login_state: &LoginState,
|
||||
register_state: &RegisterState,
|
||||
form_state: &FormState,
|
||||
) -> bool {
|
||||
if app_state.ui.show_login {
|
||||
login_state.has_unsaved_changes()
|
||||
} else if app_state.ui.show_register {
|
||||
register_state.has_unsaved_changes()
|
||||
} else {
|
||||
form_state.has_unsaved_changes()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_input_for_state<'a>(
|
||||
app_state: &AppState,
|
||||
login_state: &'a LoginState,
|
||||
register_state: &'a RegisterState,
|
||||
form_state: &'a FormState,
|
||||
) -> &'a str {
|
||||
if app_state.ui.show_login {
|
||||
login_state.get_current_input()
|
||||
} else if app_state.ui.show_register {
|
||||
register_state.get_current_input()
|
||||
} else {
|
||||
form_state.get_current_input()
|
||||
}
|
||||
}
|
||||
|
||||
fn set_current_cursor_pos_for_state(
|
||||
app_state: &AppState,
|
||||
login_state: &mut LoginState,
|
||||
register_state: &mut RegisterState,
|
||||
form_state: &mut FormState,
|
||||
pos: usize,
|
||||
) {
|
||||
if app_state.ui.show_login {
|
||||
login_state.set_current_cursor_pos(pos);
|
||||
} else if app_state.ui.show_register {
|
||||
register_state.set_current_cursor_pos(pos);
|
||||
} else {
|
||||
form_state.set_current_cursor_pos(pos);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_cursor_pos_for_mixed_state(
|
||||
app_state: &AppState,
|
||||
login_state: &LoginState,
|
||||
form_state: &FormState,
|
||||
) -> usize {
|
||||
if app_state.ui.show_login || app_state.ui.show_register {
|
||||
login_state.current_cursor_pos()
|
||||
} else {
|
||||
form_state.current_cursor_pos()
|
||||
}
|
||||
}
|
||||
|
||||
// This function handles state changes.
|
||||
async fn handle_search_palette_event(
|
||||
&mut self,
|
||||
@@ -199,7 +283,6 @@ impl EventHandler {
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// --- START CORRECTED LOGIC ---
|
||||
if trigger_search {
|
||||
search_state.is_loading = true;
|
||||
search_state.results.clear();
|
||||
@@ -214,7 +297,6 @@ impl EventHandler {
|
||||
"--- 1. Spawning search task for query: '{}' ---",
|
||||
query
|
||||
);
|
||||
// We now move the grpc_client into the task, just like with login.
|
||||
tokio::spawn(async move {
|
||||
info!("--- 2. Background task started. ---");
|
||||
match grpc_client.search_table(table_name, query).await {
|
||||
@@ -226,7 +308,6 @@ impl EventHandler {
|
||||
let _ = sender.send(response.hits);
|
||||
}
|
||||
Err(e) => {
|
||||
// THE FIX: Use the debug formatter `{:?}` to print the full error chain.
|
||||
error!("--- 3b. gRPC call failed: {:?} ---", e);
|
||||
let _ = sender.send(vec![]);
|
||||
}
|
||||
@@ -235,8 +316,6 @@ impl EventHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// The borrow on `app_state.search_state` ends here.
|
||||
// Now we can safely modify the Option itself.
|
||||
if should_close {
|
||||
app_state.search_state = None;
|
||||
app_state.ui.show_search_palette = false;
|
||||
@@ -264,7 +343,6 @@ impl EventHandler {
|
||||
) -> Result<EventOutcome> {
|
||||
if app_state.ui.show_search_palette {
|
||||
if let Event::Key(key_event) = event {
|
||||
// The call no longer passes grpc_client
|
||||
return self
|
||||
.handle_search_palette_event(
|
||||
key_event,
|
||||
@@ -581,7 +659,7 @@ impl EventHandler {
|
||||
if config.get_read_only_action_for_key(key_code, modifiers) == Some("enter_highlight_mode_linewise")
|
||||
&& ModeManager::can_enter_highlight_mode(current_mode)
|
||||
{
|
||||
let current_field_index = get_current_field_for_state(
|
||||
let current_field_index = Self::get_current_field_for_state(
|
||||
app_state,
|
||||
login_state,
|
||||
register_state,
|
||||
@@ -596,13 +674,13 @@ impl EventHandler {
|
||||
else if config.get_read_only_action_for_key(key_code, modifiers) == Some("enter_highlight_mode")
|
||||
&& ModeManager::can_enter_highlight_mode(current_mode)
|
||||
{
|
||||
let current_field_index = get_current_field_for_state(
|
||||
let current_field_index = Self::get_current_field_for_state(
|
||||
app_state,
|
||||
login_state,
|
||||
register_state,
|
||||
form_state
|
||||
);
|
||||
let current_cursor_pos = get_current_cursor_pos_for_state(
|
||||
let current_cursor_pos = Self::get_current_cursor_pos_for_state(
|
||||
app_state,
|
||||
login_state,
|
||||
register_state,
|
||||
@@ -627,13 +705,13 @@ impl EventHandler {
|
||||
else if config.get_read_only_action_for_key(key_code, modifiers).as_deref() == Some("enter_edit_mode_after")
|
||||
&& ModeManager::can_enter_edit_mode(current_mode)
|
||||
{
|
||||
let current_input = get_current_input_for_state(
|
||||
let current_input = Self::get_current_input_for_state(
|
||||
app_state,
|
||||
login_state,
|
||||
register_state,
|
||||
form_state
|
||||
);
|
||||
let current_cursor_pos = get_cursor_pos_for_mixed_state(
|
||||
let current_cursor_pos = Self::get_cursor_pos_for_mixed_state(
|
||||
app_state,
|
||||
login_state,
|
||||
form_state
|
||||
@@ -642,14 +720,14 @@ impl EventHandler {
|
||||
// Move cursor forward if possible
|
||||
if !current_input.is_empty() && current_cursor_pos < current_input.len() {
|
||||
let new_cursor_pos = current_cursor_pos + 1;
|
||||
set_current_cursor_pos_for_state(
|
||||
Self::set_current_cursor_pos_for_state(
|
||||
app_state,
|
||||
login_state,
|
||||
register_state,
|
||||
form_state,
|
||||
new_cursor_pos
|
||||
);
|
||||
self.ideal_cursor_column = get_current_cursor_pos_for_state(
|
||||
self.ideal_cursor_column = Self::get_current_cursor_pos_for_state(
|
||||
app_state,
|
||||
login_state,
|
||||
register_state,
|
||||
@@ -694,13 +772,13 @@ impl EventHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// Try canvas action for form first (NEW: Canvas library integration)
|
||||
// Try canvas action for form first
|
||||
if app_state.ui.show_form {
|
||||
if let Ok(Some(canvas_message)) = self.handle_form_canvas_action(
|
||||
key_event,
|
||||
config,
|
||||
form_state,
|
||||
false, // not edit mode
|
||||
false,
|
||||
).await {
|
||||
return Ok(EventOutcome::Ok(canvas_message));
|
||||
}
|
||||
@@ -753,7 +831,7 @@ impl EventHandler {
|
||||
&mut admin_state.add_table_state,
|
||||
&mut admin_state.add_logic_state,
|
||||
&mut self.key_sequence_tracker,
|
||||
&mut self.grpc_client, // <-- FIX 2
|
||||
&mut self.grpc_client,
|
||||
&mut self.command_message,
|
||||
&mut self.edit_mode_cooldown,
|
||||
&mut self.ideal_cursor_column,
|
||||
@@ -784,13 +862,13 @@ impl EventHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// Try canvas action for form first (NEW: Canvas library integration)
|
||||
// Try canvas action for form first
|
||||
if app_state.ui.show_form {
|
||||
if let Ok(Some(canvas_message)) = self.handle_form_canvas_action(
|
||||
key_event,
|
||||
config,
|
||||
form_state,
|
||||
true, // edit mode
|
||||
true,
|
||||
).await {
|
||||
if !canvas_message.is_empty() {
|
||||
self.command_message = canvas_message.clone();
|
||||
@@ -823,7 +901,7 @@ impl EventHandler {
|
||||
self.edit_mode_cooldown = true;
|
||||
|
||||
// Check for unsaved changes across all states
|
||||
let has_changes = get_has_unsaved_changes_for_state(
|
||||
let has_changes = Self::get_has_unsaved_changes_for_state(
|
||||
app_state,
|
||||
login_state,
|
||||
register_state,
|
||||
@@ -840,13 +918,13 @@ impl EventHandler {
|
||||
terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?;
|
||||
|
||||
// Get current input and cursor position
|
||||
let current_input = get_current_input_for_state(
|
||||
let current_input = Self::get_current_input_for_state(
|
||||
app_state,
|
||||
login_state,
|
||||
register_state,
|
||||
form_state
|
||||
);
|
||||
let current_cursor_pos = get_current_cursor_pos_for_state(
|
||||
let current_cursor_pos = Self::get_current_cursor_pos_for_state(
|
||||
app_state,
|
||||
login_state,
|
||||
register_state,
|
||||
@@ -856,7 +934,7 @@ impl EventHandler {
|
||||
// Adjust cursor if it's beyond the input length
|
||||
if !current_input.is_empty() && current_cursor_pos >= current_input.len() {
|
||||
let new_pos = current_input.len() - 1;
|
||||
set_current_cursor_pos_for_state(
|
||||
Self::set_current_cursor_pos_for_state(
|
||||
app_state,
|
||||
login_state,
|
||||
register_state,
|
||||
@@ -906,7 +984,7 @@ impl EventHandler {
|
||||
form_state,
|
||||
&mut self.command_input,
|
||||
&mut self.command_message,
|
||||
&mut self.grpc_client, // <-- FIX 5
|
||||
&mut self.grpc_client,
|
||||
command_handler,
|
||||
terminal,
|
||||
&mut current_position,
|
||||
@@ -1024,11 +1102,10 @@ impl EventHandler {
|
||||
async fn handle_form_canvas_action(
|
||||
&mut self,
|
||||
key_event: KeyEvent,
|
||||
_config: &Config, // Not used anymore - canvas has its own config
|
||||
_config: &Config,
|
||||
form_state: &mut FormState,
|
||||
is_edit_mode: bool,
|
||||
) -> Result<Option<String>> {
|
||||
// Load canvas config (canvas_config.toml or vim defaults)
|
||||
let canvas_config = canvas::config::CanvasConfig::load();
|
||||
|
||||
// Handle suggestion actions first if suggestions are active
|
||||
@@ -1083,7 +1160,6 @@ impl EventHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// FIXED: Use canvas config instead of client config
|
||||
let action_str = canvas_config.get_action_for_key(
|
||||
key_event.code,
|
||||
key_event.modifiers,
|
||||
@@ -1092,9 +1168,8 @@ impl EventHandler {
|
||||
);
|
||||
|
||||
if let Some(action_str) = action_str {
|
||||
// Filter out mode transition actions - let legacy handlers deal with these
|
||||
if Self::is_mode_transition_action(action_str) {
|
||||
return Ok(None); // Let legacy handler handle mode transitions
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let canvas_action = CanvasAction::from_string(&action_str);
|
||||
@@ -1131,7 +1206,6 @@ impl EventHandler {
|
||||
} else {
|
||||
// In read-only mode, only handle non-character keys
|
||||
let canvas_action = match key_event.code {
|
||||
// Only handle special keys that don't conflict with vim bindings
|
||||
KeyCode::Left => Some(CanvasAction::MoveLeft),
|
||||
KeyCode::Right => Some(CanvasAction::MoveRight),
|
||||
KeyCode::Up => Some(CanvasAction::MoveUp),
|
||||
@@ -1164,7 +1238,6 @@ impl EventHandler {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
// ADDED: Helper function to identify mode transition actions
|
||||
fn is_mode_transition_action(action: &str) -> bool {
|
||||
matches!(action,
|
||||
"exit" |
|
||||
@@ -1181,11 +1254,11 @@ impl EventHandler {
|
||||
"force_quit" |
|
||||
"save_and_quit" |
|
||||
"revert" |
|
||||
"enter_decider" | // This is also handled specially by legacy system
|
||||
"trigger_autocomplete" | // This is handled specially by legacy system
|
||||
"suggestion_up" | // These are handled above in suggestion logic
|
||||
"enter_decider" |
|
||||
"trigger_autocomplete" |
|
||||
"suggestion_up" |
|
||||
"suggestion_down" |
|
||||
"previous_entry" | // Navigation between records
|
||||
"previous_entry" |
|
||||
"next_entry" |
|
||||
"toggle_sidebar" |
|
||||
"toggle_buffer_list" |
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
|
||||
// src/modes/handlers/event_helper.rs
|
||||
//! Helper functions to handle the differences between legacy and library CanvasState traits
|
||||
|
||||
use crate::state::app::state::AppState;
|
||||
use crate::state::pages::{
|
||||
form::FormState,
|
||||
auth::{LoginState, RegisterState},
|
||||
};
|
||||
use crate::state::pages::canvas_state::CanvasState as LegacyCanvasState;
|
||||
use canvas::canvas::CanvasState as LibraryCanvasState;
|
||||
|
||||
/// Get the current field index from the appropriate state based on which UI is active
|
||||
pub fn get_current_field_for_state(
|
||||
app_state: &AppState,
|
||||
login_state: &LoginState,
|
||||
register_state: &RegisterState,
|
||||
form_state: &FormState,
|
||||
) -> usize {
|
||||
if app_state.ui.show_login {
|
||||
login_state.current_field() // Uses LegacyCanvasState
|
||||
} else if app_state.ui.show_register {
|
||||
register_state.current_field() // Uses LegacyCanvasState
|
||||
} else {
|
||||
form_state.current_field() // Uses LibraryCanvasState
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current cursor position from the appropriate state based on which UI is active
|
||||
pub fn get_current_cursor_pos_for_state(
|
||||
app_state: &AppState,
|
||||
login_state: &LoginState,
|
||||
register_state: &RegisterState,
|
||||
form_state: &FormState,
|
||||
) -> usize {
|
||||
if app_state.ui.show_login {
|
||||
login_state.current_cursor_pos() // Uses LegacyCanvasState
|
||||
} else if app_state.ui.show_register {
|
||||
register_state.current_cursor_pos() // Uses LegacyCanvasState
|
||||
} else {
|
||||
form_state.current_cursor_pos() // Uses LibraryCanvasState
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the appropriate state has unsaved changes based on which UI is active
|
||||
pub fn get_has_unsaved_changes_for_state(
|
||||
app_state: &AppState,
|
||||
login_state: &LoginState,
|
||||
register_state: &RegisterState,
|
||||
form_state: &FormState,
|
||||
) -> bool {
|
||||
if app_state.ui.show_login {
|
||||
login_state.has_unsaved_changes() // Uses LegacyCanvasState
|
||||
} else if app_state.ui.show_register {
|
||||
register_state.has_unsaved_changes() // Uses LegacyCanvasState
|
||||
} else {
|
||||
form_state.has_unsaved_changes() // Uses LibraryCanvasState
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current input from the appropriate state based on which UI is active
|
||||
pub fn get_current_input_for_state<'a>(
|
||||
app_state: &AppState,
|
||||
login_state: &'a LoginState,
|
||||
register_state: &'a RegisterState,
|
||||
form_state: &'a FormState,
|
||||
) -> &'a str {
|
||||
if app_state.ui.show_login {
|
||||
login_state.get_current_input() // Uses LegacyCanvasState
|
||||
} else if app_state.ui.show_register {
|
||||
register_state.get_current_input() // Uses LegacyCanvasState
|
||||
} else {
|
||||
form_state.get_current_input() // Uses LibraryCanvasState
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the cursor position for the appropriate state based on which UI is active
|
||||
pub fn set_current_cursor_pos_for_state(
|
||||
app_state: &AppState,
|
||||
login_state: &mut LoginState,
|
||||
register_state: &mut RegisterState,
|
||||
form_state: &mut FormState,
|
||||
pos: usize,
|
||||
) {
|
||||
if app_state.ui.show_login {
|
||||
login_state.set_current_cursor_pos(pos); // Uses LegacyCanvasState
|
||||
} else if app_state.ui.show_register {
|
||||
register_state.set_current_cursor_pos(pos); // Uses LegacyCanvasState
|
||||
} else {
|
||||
form_state.set_current_cursor_pos(pos); // Uses LibraryCanvasState
|
||||
}
|
||||
}
|
||||
|
||||
/// Get cursor position for mixed login/register vs form logic
|
||||
pub fn get_cursor_pos_for_mixed_state(
|
||||
app_state: &AppState,
|
||||
login_state: &LoginState,
|
||||
form_state: &FormState,
|
||||
) -> usize {
|
||||
if app_state.ui.show_login || app_state.ui.show_register {
|
||||
login_state.current_cursor_pos() // Uses LegacyCanvasState
|
||||
} else {
|
||||
form_state.current_cursor_pos() // Uses LibraryCanvasState
|
||||
}
|
||||
}
|
||||
@@ -6,4 +6,3 @@ pub mod admin;
|
||||
pub mod intro;
|
||||
pub mod add_table;
|
||||
pub mod add_logic;
|
||||
pub mod canvas_state;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// src/state/pages/add_logic.rs
|
||||
use crate::config::binds::config::{EditorConfig, EditorKeybindingMode};
|
||||
use crate::state::pages::canvas_state::CanvasState;
|
||||
use canvas::canvas::{CanvasState, ActionContext, CanvasAction}; // External library
|
||||
use crate::components::common::text_editor::{TextEditor, VimState};
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
@@ -50,7 +50,7 @@ pub struct AddLogicState {
|
||||
pub script_editor_trigger_position: Option<(usize, usize)>, // (line, column)
|
||||
pub all_table_names: Vec<String>,
|
||||
pub script_editor_filter_text: String,
|
||||
|
||||
|
||||
// New fields for same-profile table names and column autocomplete
|
||||
pub same_profile_table_names: Vec<String>, // Tables from same profile only
|
||||
pub script_editor_awaiting_column_autocomplete: Option<String>, // Table name waiting for column fetch
|
||||
@@ -88,7 +88,7 @@ impl AddLogicState {
|
||||
script_editor_trigger_position: None,
|
||||
all_table_names: Vec::new(),
|
||||
script_editor_filter_text: String::new(),
|
||||
|
||||
|
||||
same_profile_table_names: Vec::new(),
|
||||
script_editor_awaiting_column_autocomplete: None,
|
||||
}
|
||||
@@ -181,7 +181,7 @@ impl AddLogicState {
|
||||
}
|
||||
self.same_profile_table_names.contains(&suggestion.to_string())
|
||||
}
|
||||
|
||||
|
||||
/// Sets table columns for autocomplete suggestions
|
||||
pub fn set_table_columns(&mut self, columns: Vec<String>) {
|
||||
self.table_columns_for_suggestions = columns.clone();
|
||||
@@ -225,6 +225,46 @@ impl AddLogicState {
|
||||
self.script_editor_trigger_position = None;
|
||||
self.script_editor_filter_text.clear();
|
||||
}
|
||||
|
||||
/// Helper method to validate and save logic
|
||||
pub fn save_logic(&mut self) -> Option<String> {
|
||||
if self.logic_name_input.trim().is_empty() {
|
||||
return Some("Logic name is required".to_string());
|
||||
}
|
||||
|
||||
if self.target_column_input.trim().is_empty() {
|
||||
return Some("Target column is required".to_string());
|
||||
}
|
||||
|
||||
let script_content = {
|
||||
let editor_borrow = self.script_content_editor.borrow();
|
||||
editor_borrow.lines().join("\n")
|
||||
};
|
||||
|
||||
if script_content.trim().is_empty() {
|
||||
return Some("Script content is required".to_string());
|
||||
}
|
||||
|
||||
// Here you would typically save to database/storage
|
||||
// For now, just clear the form and mark as saved
|
||||
self.has_unsaved_changes = false;
|
||||
Some(format!("Logic '{}' saved successfully", self.logic_name_input.trim()))
|
||||
}
|
||||
|
||||
/// Helper method to clear the form
|
||||
pub fn clear_form(&mut self) -> Option<String> {
|
||||
let profile = self.profile_name.clone();
|
||||
let table_id = self.selected_table_id;
|
||||
let table_name = self.selected_table_name.clone();
|
||||
let editor_config = EditorConfig::default(); // You might want to preserve the actual config
|
||||
|
||||
*self = Self::new(&editor_config);
|
||||
self.profile_name = profile;
|
||||
self.selected_table_id = table_id;
|
||||
self.selected_table_name = table_name;
|
||||
|
||||
Some("Form cleared".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AddLogicState {
|
||||
@@ -233,59 +273,18 @@ impl Default for AddLogicState {
|
||||
}
|
||||
}
|
||||
|
||||
// Implement external library's CanvasState for AddLogicState
|
||||
impl CanvasState for AddLogicState {
|
||||
fn current_field(&self) -> usize {
|
||||
match self.current_focus {
|
||||
AddLogicFocus::InputLogicName => 0,
|
||||
AddLogicFocus::InputTargetColumn => 1,
|
||||
AddLogicFocus::InputDescription => 2,
|
||||
// If focus is elsewhere, return the last canvas field used
|
||||
_ => self.last_canvas_field,
|
||||
}
|
||||
}
|
||||
|
||||
fn current_cursor_pos(&self) -> usize {
|
||||
match self.current_focus {
|
||||
AddLogicFocus::InputLogicName => self.logic_name_cursor_pos,
|
||||
AddLogicFocus::InputTargetColumn => self.target_column_cursor_pos,
|
||||
AddLogicFocus::InputDescription => self.description_cursor_pos,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn has_unsaved_changes(&self) -> bool {
|
||||
self.has_unsaved_changes
|
||||
}
|
||||
|
||||
fn inputs(&self) -> Vec<&String> {
|
||||
vec![
|
||||
&self.logic_name_input,
|
||||
&self.target_column_input,
|
||||
&self.description_input,
|
||||
]
|
||||
}
|
||||
|
||||
fn get_current_input(&self) -> &str {
|
||||
match self.current_focus {
|
||||
AddLogicFocus::InputLogicName => &self.logic_name_input,
|
||||
AddLogicFocus::InputTargetColumn => &self.target_column_input,
|
||||
AddLogicFocus::InputDescription => &self.description_input,
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_input_mut(&mut self) -> &mut String {
|
||||
match self.current_focus {
|
||||
AddLogicFocus::InputLogicName => &mut self.logic_name_input,
|
||||
AddLogicFocus::InputTargetColumn => &mut self.target_column_input,
|
||||
AddLogicFocus::InputDescription => &mut self.description_input,
|
||||
_ => &mut self.logic_name_input,
|
||||
}
|
||||
}
|
||||
|
||||
fn fields(&self) -> Vec<&str> {
|
||||
vec!["Logic Name", "Target Column", "Description"]
|
||||
}
|
||||
|
||||
fn set_current_field(&mut self, index: usize) {
|
||||
let new_focus = match index {
|
||||
0 => AddLogicFocus::InputLogicName,
|
||||
@@ -303,6 +302,15 @@ impl CanvasState for AddLogicState {
|
||||
}
|
||||
}
|
||||
|
||||
fn current_cursor_pos(&self) -> usize {
|
||||
match self.current_focus {
|
||||
AddLogicFocus::InputLogicName => self.logic_name_cursor_pos,
|
||||
AddLogicFocus::InputTargetColumn => self.target_column_cursor_pos,
|
||||
AddLogicFocus::InputDescription => self.description_cursor_pos,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_current_cursor_pos(&mut self, pos: usize) {
|
||||
match self.current_focus {
|
||||
AddLogicFocus::InputLogicName => {
|
||||
@@ -318,29 +326,117 @@ impl CanvasState for AddLogicState {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_input(&self) -> &str {
|
||||
match self.current_focus {
|
||||
AddLogicFocus::InputLogicName => &self.logic_name_input,
|
||||
AddLogicFocus::InputTargetColumn => &self.target_column_input,
|
||||
AddLogicFocus::InputDescription => &self.description_input,
|
||||
_ => "", // Should not happen if called correctly
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_input_mut(&mut self) -> &mut String {
|
||||
match self.current_focus {
|
||||
AddLogicFocus::InputLogicName => &mut self.logic_name_input,
|
||||
AddLogicFocus::InputTargetColumn => &mut self.target_column_input,
|
||||
AddLogicFocus::InputDescription => &mut self.description_input,
|
||||
_ => &mut self.logic_name_input, // Fallback
|
||||
}
|
||||
}
|
||||
|
||||
fn inputs(&self) -> Vec<&String> {
|
||||
vec![
|
||||
&self.logic_name_input,
|
||||
&self.target_column_input,
|
||||
&self.description_input,
|
||||
]
|
||||
}
|
||||
|
||||
fn fields(&self) -> Vec<&str> {
|
||||
vec!["Logic Name", "Target Column", "Description"]
|
||||
}
|
||||
|
||||
fn has_unsaved_changes(&self) -> bool {
|
||||
self.has_unsaved_changes
|
||||
}
|
||||
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) {
|
||||
self.has_unsaved_changes = changed;
|
||||
}
|
||||
|
||||
fn get_suggestions(&self) -> Option<&[String]> {
|
||||
if self.current_field() == 1
|
||||
&& self.in_target_column_suggestion_mode
|
||||
&& self.show_target_column_suggestions
|
||||
{
|
||||
Some(&self.target_column_suggestions)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
|
||||
match action {
|
||||
// Handle saving logic script
|
||||
CanvasAction::Custom(action_str) if action_str == "save_logic" => {
|
||||
self.save_logic()
|
||||
}
|
||||
|
||||
fn get_selected_suggestion_index(&self) -> Option<usize> {
|
||||
if self.current_field() == 1
|
||||
&& self.in_target_column_suggestion_mode
|
||||
&& self.show_target_column_suggestions
|
||||
{
|
||||
self.selected_target_column_suggestion_index
|
||||
} else {
|
||||
None
|
||||
// Handle clearing the form
|
||||
CanvasAction::Custom(action_str) if action_str == "clear_form" => {
|
||||
self.clear_form()
|
||||
}
|
||||
|
||||
// Handle target column autocomplete activation
|
||||
CanvasAction::Custom(action_str) if action_str == "activate_autocomplete" => {
|
||||
if self.current_field() == 1 { // Target Column field
|
||||
self.in_target_column_suggestion_mode = true;
|
||||
self.update_target_column_suggestions();
|
||||
Some("Autocomplete activated".to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// Handle target column suggestion selection
|
||||
CanvasAction::Custom(action_str) if action_str == "select_suggestion" => {
|
||||
if self.current_field() == 1 && self.in_target_column_suggestion_mode {
|
||||
if let Some(selected_idx) = self.selected_target_column_suggestion_index {
|
||||
if let Some(suggestion) = self.target_column_suggestions.get(selected_idx) {
|
||||
self.target_column_input = suggestion.clone();
|
||||
self.target_column_cursor_pos = suggestion.len();
|
||||
self.in_target_column_suggestion_mode = false;
|
||||
self.show_target_column_suggestions = false;
|
||||
self.has_unsaved_changes = true;
|
||||
return Some(format!("Selected: {}", suggestion));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
// Custom validation when moving between fields
|
||||
CanvasAction::NextField => {
|
||||
match self.current_field() {
|
||||
0 => { // Logic Name field
|
||||
if self.logic_name_input.trim().is_empty() {
|
||||
Some("Logic name cannot be empty".to_string())
|
||||
} else {
|
||||
None // Let canvas library handle the normal field movement
|
||||
}
|
||||
}
|
||||
1 => { // Target Column field
|
||||
// Update suggestions when entering target column field
|
||||
self.update_target_column_suggestions();
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle character insertion with validation
|
||||
CanvasAction::InsertChar(c) => {
|
||||
if self.current_field() == 1 { // Target Column field
|
||||
// Update suggestions after character insertion
|
||||
// Note: Canvas library will handle the actual insertion
|
||||
// This is just for triggering suggestion updates
|
||||
None // Let canvas handle insertion, then we'll update suggestions
|
||||
} else {
|
||||
None // Let canvas handle normally
|
||||
}
|
||||
}
|
||||
|
||||
// Let canvas library handle everything else
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// src/state/pages/add_table.rs
|
||||
use crate::state::pages::canvas_state::CanvasState;
|
||||
use canvas::canvas::{CanvasState, ActionContext, CanvasAction}; // External library
|
||||
use ratatui::widgets::TableState;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -67,7 +67,6 @@ pub struct AddTableState {
|
||||
|
||||
impl Default for AddTableState {
|
||||
fn default() -> Self {
|
||||
// Initialize with some dummy data for demonstration
|
||||
AddTableState {
|
||||
profile_name: "default".to_string(),
|
||||
table_name: String::new(),
|
||||
@@ -92,16 +91,91 @@ impl Default for AddTableState {
|
||||
|
||||
impl AddTableState {
|
||||
pub const INPUT_FIELD_COUNT: usize = 3;
|
||||
|
||||
/// Helper method to add a column from current inputs
|
||||
pub fn add_column_from_inputs(&mut self) -> Option<String> {
|
||||
if self.column_name_input.trim().is_empty() || self.column_type_input.trim().is_empty() {
|
||||
return Some("Both column name and type are required".to_string());
|
||||
}
|
||||
|
||||
// Check for duplicate column names
|
||||
if self.columns.iter().any(|col| col.name == self.column_name_input.trim()) {
|
||||
return Some("Column name already exists".to_string());
|
||||
}
|
||||
|
||||
// Add the column
|
||||
self.columns.push(ColumnDefinition {
|
||||
name: self.column_name_input.trim().to_string(),
|
||||
data_type: self.column_type_input.trim().to_string(),
|
||||
selected: false,
|
||||
});
|
||||
|
||||
// Clear inputs and reset focus to column name for next entry
|
||||
self.column_name_input.clear();
|
||||
self.column_type_input.clear();
|
||||
self.column_name_cursor_pos = 0;
|
||||
self.column_type_cursor_pos = 0;
|
||||
self.current_focus = AddTableFocus::InputColumnName;
|
||||
self.last_canvas_field = 1;
|
||||
self.has_unsaved_changes = true;
|
||||
|
||||
Some(format!("Column '{}' added successfully", self.columns.last().unwrap().name))
|
||||
}
|
||||
|
||||
/// Helper method to delete selected items
|
||||
pub fn delete_selected_items(&mut self) -> Option<String> {
|
||||
let mut deleted_items = Vec::new();
|
||||
|
||||
// Remove selected columns
|
||||
let initial_column_count = self.columns.len();
|
||||
self.columns.retain(|col| {
|
||||
if col.selected {
|
||||
deleted_items.push(format!("column '{}'", col.name));
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
// Remove selected indexes
|
||||
let initial_index_count = self.indexes.len();
|
||||
self.indexes.retain(|idx| {
|
||||
if idx.selected {
|
||||
deleted_items.push(format!("index '{}'", idx.name));
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
// Remove selected links
|
||||
let initial_link_count = self.links.len();
|
||||
self.links.retain(|link| {
|
||||
if link.selected {
|
||||
deleted_items.push(format!("link to '{}'", link.linked_table_name));
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
if deleted_items.is_empty() {
|
||||
Some("No items selected for deletion".to_string())
|
||||
} else {
|
||||
self.has_unsaved_changes = true;
|
||||
Some(format!("Deleted: {}", deleted_items.join(", ")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Implement CanvasState for the input fields
|
||||
// Implement external library's CanvasState for AddTableState
|
||||
impl CanvasState for AddTableState {
|
||||
fn current_field(&self) -> usize {
|
||||
match self.current_focus {
|
||||
AddTableFocus::InputTableName => 0,
|
||||
AddTableFocus::InputColumnName => 1,
|
||||
AddTableFocus::InputColumnType => 2,
|
||||
// If focus is elsewhere, default to the first field for canvas rendering logic
|
||||
// If focus is elsewhere, return the last canvas field used
|
||||
_ => self.last_canvas_field,
|
||||
}
|
||||
}
|
||||
@@ -115,37 +189,6 @@ impl CanvasState for AddTableState {
|
||||
}
|
||||
}
|
||||
|
||||
fn has_unsaved_changes(&self) -> bool {
|
||||
self.has_unsaved_changes
|
||||
}
|
||||
|
||||
fn inputs(&self) -> Vec<&String> {
|
||||
vec![&self.table_name_input, &self.column_name_input, &self.column_type_input]
|
||||
}
|
||||
|
||||
fn get_current_input(&self) -> &str {
|
||||
match self.current_focus {
|
||||
AddTableFocus::InputTableName => &self.table_name_input,
|
||||
AddTableFocus::InputColumnName => &self.column_name_input,
|
||||
AddTableFocus::InputColumnType => &self.column_type_input,
|
||||
_ => "", // Should not happen if called correctly
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_input_mut(&mut self) -> &mut String {
|
||||
match self.current_focus {
|
||||
AddTableFocus::InputTableName => &mut self.table_name_input,
|
||||
AddTableFocus::InputColumnName => &mut self.column_name_input,
|
||||
AddTableFocus::InputColumnType => &mut self.column_type_input,
|
||||
_ => &mut self.table_name_input,
|
||||
}
|
||||
}
|
||||
|
||||
fn fields(&self) -> Vec<&str> {
|
||||
// These must match the order used in render_add_table
|
||||
vec!["Table name", "Name", "Type"]
|
||||
}
|
||||
|
||||
fn set_current_field(&mut self, index: usize) {
|
||||
// Update both current focus and last canvas field
|
||||
self.current_focus = match index {
|
||||
@@ -174,17 +217,84 @@ impl CanvasState for AddTableState {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_input(&self) -> &str {
|
||||
match self.current_focus {
|
||||
AddTableFocus::InputTableName => &self.table_name_input,
|
||||
AddTableFocus::InputColumnName => &self.column_name_input,
|
||||
AddTableFocus::InputColumnType => &self.column_type_input,
|
||||
_ => "", // Should not happen if called correctly
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_input_mut(&mut self) -> &mut String {
|
||||
match self.current_focus {
|
||||
AddTableFocus::InputTableName => &mut self.table_name_input,
|
||||
AddTableFocus::InputColumnName => &mut self.column_name_input,
|
||||
AddTableFocus::InputColumnType => &mut self.column_type_input,
|
||||
_ => &mut self.table_name_input, // Fallback
|
||||
}
|
||||
}
|
||||
|
||||
fn inputs(&self) -> Vec<&String> {
|
||||
vec![&self.table_name_input, &self.column_name_input, &self.column_type_input]
|
||||
}
|
||||
|
||||
fn fields(&self) -> Vec<&str> {
|
||||
// These must match the order used in render_add_table
|
||||
vec!["Table name", "Name", "Type"]
|
||||
}
|
||||
|
||||
fn has_unsaved_changes(&self) -> bool {
|
||||
self.has_unsaved_changes
|
||||
}
|
||||
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) {
|
||||
self.has_unsaved_changes = changed;
|
||||
}
|
||||
|
||||
// --- Autocomplete Support (Not needed for this form yet) ---
|
||||
fn get_suggestions(&self) -> Option<&[String]> {
|
||||
None
|
||||
}
|
||||
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
|
||||
match action {
|
||||
// Handle adding column when user presses Enter on the Add button or uses specific action
|
||||
CanvasAction::Custom(action_str) if action_str == "add_column" => {
|
||||
self.add_column_from_inputs()
|
||||
}
|
||||
|
||||
fn get_selected_suggestion_index(&self) -> Option<usize> {
|
||||
None
|
||||
// Handle table saving
|
||||
CanvasAction::Custom(action_str) if action_str == "save_table" => {
|
||||
if self.table_name_input.trim().is_empty() {
|
||||
Some("Table name is required".to_string())
|
||||
} else if self.columns.is_empty() {
|
||||
Some("At least one column is required".to_string())
|
||||
} else {
|
||||
Some(format!("Saving table: {}", self.table_name_input))
|
||||
}
|
||||
}
|
||||
|
||||
// Handle deleting selected items
|
||||
CanvasAction::Custom(action_str) if action_str == "delete_selected" => {
|
||||
self.delete_selected_items()
|
||||
}
|
||||
|
||||
// Handle canceling (clear form)
|
||||
CanvasAction::Custom(action_str) if action_str == "cancel" => {
|
||||
// Reset to defaults but keep profile_name
|
||||
let profile = self.profile_name.clone();
|
||||
*self = Self::default();
|
||||
self.profile_name = profile;
|
||||
Some("Form cleared".to_string())
|
||||
}
|
||||
|
||||
// Custom validation when moving between fields
|
||||
CanvasAction::NextField => {
|
||||
// When leaving table name field, update the table_name for display
|
||||
if self.current_field() == 0 && !self.table_name_input.trim().is_empty() {
|
||||
self.table_name = self.table_name_input.trim().to_string();
|
||||
}
|
||||
None // Let canvas library handle the normal field movement
|
||||
}
|
||||
|
||||
// Let canvas library handle everything else
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
// src/state/pages/canvas_state.rs
|
||||
|
||||
use common::proto::komp_ac::search::search_response::Hit;
|
||||
|
||||
pub trait CanvasState {
|
||||
// --- Existing methods (unchanged) ---
|
||||
fn current_field(&self) -> usize;
|
||||
fn current_cursor_pos(&self) -> usize;
|
||||
fn has_unsaved_changes(&self) -> bool;
|
||||
fn inputs(&self) -> Vec<&String>;
|
||||
fn get_current_input(&self) -> &str;
|
||||
fn get_current_input_mut(&mut self) -> &mut String;
|
||||
fn fields(&self) -> Vec<&str>;
|
||||
fn set_current_field(&mut self, index: usize);
|
||||
fn set_current_cursor_pos(&mut self, pos: usize);
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool);
|
||||
fn get_suggestions(&self) -> Option<&[String]>;
|
||||
fn get_selected_suggestion_index(&self) -> Option<usize>;
|
||||
fn get_rich_suggestions(&self) -> Option<&[Hit]> {
|
||||
None
|
||||
}
|
||||
|
||||
fn get_display_value_for_field(&self, index: usize) -> &str {
|
||||
self.inputs()
|
||||
.get(index)
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or("")
|
||||
}
|
||||
fn has_display_override(&self, _index: usize) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -16,11 +16,10 @@ use crate::components::{
|
||||
};
|
||||
use crate::config::colors::themes::Theme;
|
||||
use crate::modes::general::command_navigation::NavigationState;
|
||||
use crate::state::pages::canvas_state::CanvasState as LocalCanvasState; // Keep local one with alias
|
||||
use canvas::canvas::CanvasState; // Import external library's CanvasState trait
|
||||
use canvas::canvas::CanvasState;
|
||||
use crate::state::app::buffer::BufferState;
|
||||
use crate::state::app::highlight::HighlightState as LocalHighlightState; // CHANGED: Alias local version
|
||||
use canvas::canvas::HighlightState as CanvasHighlightState; // CHANGED: Import canvas version with alias
|
||||
use crate::state::app::highlight::HighlightState as LocalHighlightState;
|
||||
use canvas::canvas::HighlightState as CanvasHighlightState;
|
||||
use crate::state::app::state::AppState;
|
||||
use crate::state::pages::admin::AdminState;
|
||||
use crate::state::pages::auth::AuthState;
|
||||
|
||||
@@ -8,9 +8,8 @@ use crate::config::storage::storage::load_auth_data;
|
||||
use crate::modes::common::commands::CommandHandler;
|
||||
use crate::modes::handlers::event::{EventHandler, EventOutcome};
|
||||
use crate::modes::handlers::mode_manager::{AppMode, ModeManager};
|
||||
use crate::state::pages::canvas_state::CanvasState as LocalCanvasState; // Keep local one with alias
|
||||
use canvas::canvas::CanvasState; // Import external library's CanvasState trait
|
||||
use crate::state::pages::form::{FormState, FieldDefinition}; // Import FieldDefinition
|
||||
use canvas::canvas::CanvasState; // Only external library import
|
||||
use crate::state::pages::form::{FormState, FieldDefinition};
|
||||
use crate::state::pages::auth::AuthState;
|
||||
use crate::state::pages::auth::LoginState;
|
||||
use crate::state::pages::auth::RegisterState;
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
use rstest::{fixture, rstest};
|
||||
use std::collections::HashMap;
|
||||
use client::state::pages::form::{FormState, FieldDefinition};
|
||||
use canvas::state::CanvasState
|
||||
use client::state::pages::canvas_state::CanvasState;
|
||||
use canvas::canvas::CanvasState;
|
||||
|
||||
#[fixture]
|
||||
fn test_form_state() -> FormState {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
pub use rstest::{fixture, rstest};
|
||||
pub use client::services::grpc_client::GrpcClient;
|
||||
pub use client::state::pages::form::FormState;
|
||||
pub use client::state::pages::canvas_state::CanvasState;
|
||||
pub use canvas::canvas::CanvasState;
|
||||
pub use prost_types::Value;
|
||||
pub use prost_types::value::Kind;
|
||||
pub use std::collections::HashMap;
|
||||
|
||||
@@ -5,6 +5,7 @@ use sqlx::{PgPool, Transaction, Postgres};
|
||||
use serde_json::json;
|
||||
use common::proto::komp_ac::table_definition::{PostTableDefinitionRequest, TableDefinitionResponse};
|
||||
|
||||
// TODO CRITICAL add decimal with optional precision"
|
||||
const PREDEFINED_FIELD_TYPES: &[(&str, &str)] = &[
|
||||
("text", "TEXT"),
|
||||
("string", "TEXT"),
|
||||
|
||||
Reference in New Issue
Block a user