diff --git a/client/src/functions/modes/edit/form_e.rs b/client/src/functions/modes/edit/form_e.rs index f055d3a..2d74d69 100644 --- a/client/src/functions/modes/edit/form_e.rs +++ b/client/src/functions/modes/edit/form_e.rs @@ -1,159 +1,432 @@ // 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; -use anyhow::Result; +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 anyhow::Result; -pub async fn execute_common_action( +pub async fn execute_common_action( action: &str, - state: &mut FormState, + state: &mut S, grpc_client: &mut GrpcClient, ) -> Result { match 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)) + "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 + ))) + } } - "revert" => { - let message = form::revert(state, grpc_client).await?; - Ok(EventOutcome::Ok(message)) - } - _ => Ok(EventOutcome::Ok(format!( - "Unknown common action: {}", - action - ))), + _ => Ok(EventOutcome::Ok(format!("Common action '{}' not handled here.", action))), } } - pub async fn execute_edit_action( +pub async fn execute_edit_action( action: &str, key: KeyEvent, - state: &mut FormState, + state: &mut S, 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 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); + 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(String::new()) + Ok("".to_string()) } + "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); + 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(String::new()) + Ok("".to_string()) } + "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); + 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(); state.set_has_unsaved_changes(true); + *ideal_cursor_column = cursor_pos; } - Ok(String::new()) + 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 current_pos = state.current_cursor_pos(); - let new_pos = current_pos.saturating_sub(1); + let new_pos = state.current_cursor_pos().saturating_sub(1); state.set_current_cursor_pos(new_pos); *ideal_cursor_column = new_pos; - Ok(String::new()) + Ok("".to_string()) } + "move_right" => { + let current_input = state.get_current_input(); let current_pos = state.current_cursor_pos(); - let len = state.get_current_input().len(); - if current_pos < len { + 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(String::new()) + Ok("".to_string()) } - _ => Ok(format!("Unknown edit action: {}", action)), + + "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 } } diff --git a/client/src/modes/canvas/edit.rs b/client/src/modes/canvas/edit.rs index 96afbd8..3421b89 100644 --- a/client/src/modes/canvas/edit.rs +++ b/client/src/modes/canvas/edit.rs @@ -33,6 +33,54 @@ pub async fn handle_edit_event( grpc_client: &mut GrpcClient, app_state: &AppState, ) -> Result { +if app_state.ui.show_form && form_state.autocomplete_active { + if let Some(action) = config.get_edit_action_for_key(key.code, key.modifiers) { + match action { + "suggestion_down" => { + if !form_state.autocomplete_suggestions.is_empty() { + let current = form_state.selected_suggestion_index.unwrap_or(0); + let next = (current + 1) % form_state.autocomplete_suggestions.len(); + form_state.selected_suggestion_index = Some(next); + } + return Ok(EditEventOutcome::Message(String::new())); + } + "suggestion_up" => { + if !form_state.autocomplete_suggestions.is_empty() { + let current = form_state.selected_suggestion_index.unwrap_or(0); + let prev = if current == 0 { + form_state.autocomplete_suggestions.len() - 1 + } else { + current - 1 + }; + form_state.selected_suggestion_index = Some(prev); + } + return Ok(EditEventOutcome::Message(String::new())); + } + "exit" => { + form_state.deactivate_autocomplete(); + return Ok(EditEventOutcome::Message("Autocomplete cancelled".to_string())); + } + "enter_decider" => { + if let Some(selected_idx) = form_state.selected_suggestion_index { + if let Some(selection) = form_state.autocomplete_suggestions.get(selected_idx).cloned() { + let current_input = form_state.get_current_input_mut(); + *current_input = selection; + let new_cursor_pos = current_input.len(); + form_state.set_current_cursor_pos(new_cursor_pos); + *ideal_cursor_column = new_cursor_pos; + form_state.deactivate_autocomplete(); + form_state.set_has_unsaved_changes(true); + return Ok(EditEventOutcome::Message("Selection made".to_string())); + } + } + // If no selection, fall through to default behavior + form_state.deactivate_autocomplete(); + } + _ => {} // Other keys are not special, let them fall through + } + } + } + if let Some("enter_command_mode") = config.get_action_for_key_in_mode( &config.keybindings.global, key.code,