diff --git a/client/config.toml b/client/config.toml index cf791d2..3edbfb7 100644 --- a/client/config.toml +++ b/client/config.toml @@ -70,7 +70,7 @@ prev_field = ["shift+enter"] exit = ["esc", "ctrl+e"] delete_char_forward = ["delete"] delete_char_backward = ["backspace"] -move_left = [""] +move_left = ["left"] move_right = ["right"] suggestion_down = ["ctrl+n", "tab"] suggestion_up = ["ctrl+p", "shift+tab"] diff --git a/client/src/functions/modes/edit/form_e.rs b/client/src/functions/modes/edit/form_e.rs index ab95b5d..f055d3a 100644 --- a/client/src/functions/modes/edit/form_e.rs +++ b/client/src/functions/modes/edit/form_e.rs @@ -1,432 +1,159 @@ // src/functions/modes/edit/form_e.rs +use crate::modes::handlers::event::EventOutcome; use crate::services::grpc_client::GrpcClient; use crate::state::pages::canvas_state::CanvasState; use crate::state::pages::form::FormState; -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 std::any::Any; +use crate::tui::functions::common::form; use anyhow::Result; +use crossterm::event::{KeyCode, KeyEvent}; -pub async fn execute_common_action( +pub async fn execute_common_action( action: &str, - state: &mut S, + state: &mut FormState, grpc_client: &mut GrpcClient, ) -> Result { 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::() - { - match action { - "save" => { - let save_result = save( - 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 - ))) - } + "save" => { + let outcome = form::save(state, grpc_client).await?; + let message = match outcome { + form::SaveOutcome::NoChange => "No changes to save".to_string(), + form::SaveOutcome::UpdatedExisting => "Entry updated successfully".to_string(), + form::SaveOutcome::CreatedNew(id) => format!("New entry {} created", id), + }; + Ok(EventOutcome::DataSaved(outcome, message)) } - _ => Ok(EventOutcome::Ok(format!("Common action '{}' not handled here.", action))), + "revert" => { + let message = form::revert(state, grpc_client).await?; + Ok(EventOutcome::Ok(message)) + } + _ => Ok(EventOutcome::Ok(format!( + "Unknown common action: {}", + action + ))), } } -pub async fn execute_edit_action( + pub async fn execute_edit_action( action: &str, key: KeyEvent, - state: &mut S, + state: &mut FormState, ideal_cursor_column: &mut usize, ) -> Result { + // --- Autocomplete Interaction Logic --- + if state.autocomplete_active { + match action { + "suggestion_down" => { + // ... logic is correct ... + if !state.autocomplete_suggestions.is_empty() { + let current = state.selected_suggestion_index.unwrap_or(0); + let next = (current + 1) % state.autocomplete_suggestions.len(); + state.selected_suggestion_index = Some(next); + } + return Ok(String::new()); + } + "suggestion_up" => { + // ... logic is correct ... + if !state.autocomplete_suggestions.is_empty() { + let current = state.selected_suggestion_index.unwrap_or(0); + let prev = if current == 0 { + state.autocomplete_suggestions.len() - 1 + } else { + current - 1 + }; + state.selected_suggestion_index = Some(prev); + } + return Ok(String::new()); + } + + // --- THIS IS THE FIX --- + // We only handle "exit" if autocomplete is active. + "exit" => { + state.deactivate_autocomplete(); + return Ok("Autocomplete cancelled".to_string()); + } + // --- END OF FIX --- + + "enter_decider" => { + // ... logic is correct ... + if let Some(selected_idx) = state.selected_suggestion_index { + if let Some(selection) = state.autocomplete_suggestions.get(selected_idx).cloned() { + *state.get_current_input_mut() = selection; + state.deactivate_autocomplete(); + state.set_has_unsaved_changes(true); + return Ok("Selection made".to_string()); + } + } + } + _ => {} + } + } + + // --- Default Edit Actions --- match action { + "next_field" => { + let next_field = (state.current_field() + 1) % state.fields.len(); + state.set_current_field(next_field); + let len = state.get_current_input().len(); + state.set_current_cursor_pos(len); + *ideal_cursor_column = len; + Ok(String::new()) + } + "prev_field" => { + let prev_field = if state.current_field() == 0 { + state.fields().len() - 1 + } else { + state.current_field() - 1 + }; + state.set_current_field(prev_field); + let len = state.get_current_input().len(); + state.set_current_cursor_pos(len); + *ideal_cursor_column = len; + Ok(String::new()) + } "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 = 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 = 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 = field_value.chars().collect(); - if cursor_pos < chars.len() { - chars.remove(cursor_pos); - *field_value = chars.into_iter().collect(); + let current_pos = state.current_cursor_pos(); + state.get_current_input_mut().insert(current_pos, c); + state.set_current_cursor_pos(current_pos + 1); state.set_has_unsaved_changes(true); - *ideal_cursor_column = cursor_pos; } - Ok("".to_string()) + Ok(String::new()) } - - "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), - ); + "delete_char_backward" => { + let current_pos = state.current_cursor_pos(); + if current_pos > 0 { + state.get_current_input_mut().remove(current_pos - 1); + state.set_current_cursor_pos(current_pos - 1); + state.set_has_unsaved_changes(true); } - Ok("".to_string()) + Ok(String::new()) } - - "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), - ); + "delete_char_forward" => { + let current_pos = state.current_cursor_pos(); + if current_pos < state.get_current_input().len() { + state.get_current_input_mut().remove(current_pos); + state.set_has_unsaved_changes(true); } - Ok("".to_string()) + Ok(String::new()) } - "move_left" => { - let new_pos = state.current_cursor_pos().saturating_sub(1); + 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()) + Ok(String::new()) } - "move_right" => { - let current_input = state.get_current_input(); let current_pos = state.current_cursor_pos(); - if current_pos < current_input.len() { + let len = state.get_current_input().len(); + if current_pos < len { let new_pos = current_pos + 1; state.set_current_cursor_pos(new_pos); *ideal_cursor_column = new_pos; } - Ok("".to_string()) + Ok(String::new()) } - - "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 = 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 = 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 = 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 = 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 + _ => Ok(format!("Unknown edit action: {}", action)), } }