finally works as i wanted it to

This commit is contained in:
filipriec
2025-06-15 14:23:19 +02:00
parent 49ed1dfe33
commit eb5532c200
2 changed files with 436 additions and 115 deletions

View File

@@ -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<S: CanvasState + Any>(
action: &str,
state: &mut FormState,
state: &mut S,
grpc_client: &mut GrpcClient,
) -> Result<EventOutcome> {
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::<FormState>()
{
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<S: CanvasState>(
action: &str,
key: KeyEvent,
state: &mut FormState,
state: &mut S,
ideal_cursor_column: &mut usize,
) -> Result<String> {
// --- 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<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(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<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(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<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(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<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
}
}

View File

@@ -33,6 +33,54 @@ pub async fn handle_edit_event(
grpc_client: &mut GrpcClient,
app_state: &AppState,
) -> Result<EditEventOutcome> {
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,