diff --git a/client/src/modes/canvas/edit.rs b/client/src/modes/canvas/edit.rs index fab50db..498be68 100644 --- a/client/src/modes/canvas/edit.rs +++ b/client/src/modes/canvas/edit.rs @@ -3,7 +3,7 @@ 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::EventOutcome; +use crate::modes::handlers::event::EventHandler; use crate::services::grpc_client::GrpcClient; use crate::state::app::state::AppState; use crate::state::pages::admin::AdminState; @@ -14,8 +14,9 @@ use crate::state::pages::{ }; use anyhow::Result; use common::proto::multieko2::search::search_response::Hit; -use crossterm::event::KeyEvent; -use tracing::debug; +use crossterm::event::{KeyCode, KeyEvent}; +use tokio::sync::mpsc; +use tracing::{debug, info}; #[derive(Debug, Clone, PartialEq, Eq)] pub enum EditEventOutcome { @@ -23,6 +24,58 @@ pub enum EditEventOutcome { ExitEditMode, } +/// Helper function to spawn a non-blocking search task for autocomplete. +async fn trigger_form_autocomplete_search( + form_state: &mut FormState, + grpc_client: &mut GrpcClient, + sender: mpsc::UnboundedSender>, +) { + if let Some(field_def) = form_state.fields.get(form_state.current_field) { + if field_def.is_link { + if let Some(target_table) = &field_def.link_target_table { + // 1. Update state for immediate UI feedback + form_state.autocomplete_loading = true; + form_state.autocomplete_active = true; + form_state.autocomplete_suggestions.clear(); + form_state.selected_suggestion_index = None; + + // 2. Clone everything needed for the background task + let query = form_state.get_current_input().to_string(); + let table_to_search = target_table.clone(); + let mut grpc_client_clone = grpc_client.clone(); + + info!( + "[Autocomplete] Spawning search in '{}' for query: '{}'", + table_to_search, query + ); + + // 3. Spawn the non-blocking task + tokio::spawn(async move { + match grpc_client_clone + .search_table(table_to_search, query) + .await + { + Ok(response) => { + // Send results back through the channel + let _ = sender.send(response.hits); + } + Err(e) => { + tracing::error!( + "[Autocomplete] Search failed: {:?}", + e + ); + // Send an empty vec on error so the UI can stop loading + let _ = sender.send(vec![]); + } + } + }); + } + } + } +} + + +#[allow(clippy::too_many_arguments)] pub async fn handle_edit_event( key: KeyEvent, config: &Config, @@ -30,12 +83,12 @@ pub async fn handle_edit_event( login_state: &mut LoginState, register_state: &mut RegisterState, admin_state: &mut AdminState, - ideal_cursor_column: &mut usize, current_position: &mut u64, total_count: u64, - grpc_client: &mut GrpcClient, + event_handler: &mut EventHandler, app_state: &AppState, ) -> Result { + // --- AUTOCOMPLETE-SPECIFIC KEY HANDLING --- if app_state.ui.show_form && form_state.autocomplete_active { if let Some(action) = config.get_edit_action_for_key(key.code, key.modifiers) @@ -81,11 +134,11 @@ pub async fn handle_edit_event( { let current_input = form_state.get_current_input_mut(); - // Use the ID from the Hit struct for the field value *current_input = selection.id.to_string(); let new_cursor_pos = current_input.len(); form_state.set_current_cursor_pos(new_cursor_pos); - *ideal_cursor_column = new_cursor_pos; + // FIX: Access ideal_cursor_column through event_handler + event_handler.ideal_cursor_column = new_cursor_pos; form_state.deactivate_autocomplete(); form_state.set_has_unsaved_changes(true); return Ok(EditEventOutcome::Message( @@ -93,504 +146,180 @@ pub async fn handle_edit_event( )); } } - // If no selection, fall through to default behavior form_state.deactivate_autocomplete(); + // Fall through to default 'enter' behavior } - _ => {} // Other keys are not special, let them fall through + _ => {} // Let other keys fall through to the live search logic } } } - if let Some("enter_command_mode") = config.get_action_for_key_in_mode( - &config.keybindings.global, - key.code, - key.modifiers, - ) { - return Ok(EditEventOutcome::Message( - "Cannot enter command mode from edit mode here.".to_string(), - )); - } + // --- LIVE AUTOCOMPLETE TRIGGER LOGIC --- + let mut trigger_search = false; - if let Some(action) = config - .get_action_for_key_in_mode( - &config.keybindings.common, - key.code, - key.modifiers, - ) - .as_deref() - { - if matches!(action, "save" | "revert") { - let message_string: String = if app_state.ui.show_login { - auth_e::execute_common_action( - action, - login_state, - grpc_client, - current_position, - total_count, - ) - .await? - } else if app_state.ui.show_register { - auth_e::execute_common_action( - action, - register_state, - grpc_client, - current_position, - total_count, - ) - .await? - } else if app_state.ui.show_add_table { - format!( - "Action '{}' not implemented for Add Table in edit mode.", - action - ) - } else if app_state.ui.show_add_logic { - format!( - "Action '{}' not implemented for Add Logic in edit mode.", - action - ) - } else { - let outcome = - form_e::execute_common_action(action, form_state, grpc_client) - .await?; - match outcome { - EventOutcome::Ok(msg) | EventOutcome::DataSaved(_, msg) => msg, - _ => format!( - "Unexpected outcome from common action: {:?}", - outcome - ), - } - }; - return Ok(EditEventOutcome::Message(message_string)); - } - } - - if let Some(action_str) = - config.get_edit_action_for_key(key.code, key.modifiers).as_deref() - { - tracing::info!( - "[Handler] `handle_edit_event` received action: '{}'", - action_str - ); - - // --- MANUAL AUTOCOMPLETE TRIGGER --- - if action_str == "trigger_autocomplete" { - tracing::info!("[Handler] Action is 'trigger_autocomplete'. Checking conditions..."); - if app_state.ui.show_form { - if let Some(field_def) = form_state.fields.get(form_state.current_field) { - if field_def.is_link { - // Use our new field to get the table to search - if let Some(target_table) = &field_def.link_target_table { - tracing::info!( - "[Handler] Field '{}' is a link to table '{}'. Triggering search.", - field_def.display_name, - target_table - ); - - // Set loading state and activate autocomplete UI - form_state.autocomplete_loading = true; - form_state.autocomplete_active = true; - form_state.autocomplete_suggestions.clear(); - form_state.selected_suggestion_index = None; - - let query = form_state.get_current_input().to_string(); - let table_to_search = target_table.clone(); - - // Perform the gRPC call asynchronously - match grpc_client.search_table(table_to_search, query).await { - Ok(response) => { - form_state.autocomplete_suggestions = response.hits; - if !form_state.autocomplete_suggestions.is_empty() { - form_state.selected_suggestion_index = Some(0); - } - form_state.autocomplete_loading = false; // Turn off loading - return Ok(EditEventOutcome::Message(format!( - "Found {} suggestions.", - form_state.autocomplete_suggestions.len() - ))); - } - Err(e) => { - // Handle errors gracefully - form_state.autocomplete_loading = false; - form_state.deactivate_autocomplete(); // Close UI on error - let error_msg = format!("Search failed: {}", e); - tracing::error!("[Handler] {}", error_msg); - return Ok(EditEventOutcome::Message(error_msg)); - } - } - } else { - let msg = "Field is a link, but target table is not defined.".to_string(); - tracing::error!("[Handler] {}", msg); - return Ok(EditEventOutcome::Message(msg)); - } - } else { - let msg = format!("Field '{}' is not a linkable field.", field_def.display_name); - tracing::error!("[Handler] {}", msg); - return Ok(EditEventOutcome::Message(msg)); - } - } + if app_state.ui.show_form { + // Manual trigger + if let Some("trigger_autocomplete") = + config.get_edit_action_for_key(key.code, key.modifiers) + { + if !form_state.autocomplete_active { + trigger_search = true; } - // Fallback message if not in a form or something went wrong - return Ok(EditEventOutcome::Message("Autocomplete not available here.".to_string())); } - // --- END OF NEW LOGIC --- - - if action_str == "enter_decider" { - let effective_action = if app_state.ui.show_register - && register_state.in_suggestion_mode - && register_state.current_field() == 4 - { - "select_suggestion" - } else if app_state.ui.show_add_logic - && admin_state - .add_logic_state - .in_target_column_suggestion_mode - && admin_state.add_logic_state.current_field() == 1 - { - "select_suggestion" - } else { - "next_field" - }; - - let msg = if app_state.ui.show_login { - auth_e::execute_edit_action( - effective_action, - key, - login_state, - ideal_cursor_column, - ) - .await? - } else if app_state.ui.show_add_table { - add_table_e::execute_edit_action( - effective_action, - key, - &mut admin_state.add_table_state, - ideal_cursor_column, - ) - .await? - } else if app_state.ui.show_add_logic { - add_logic_e::execute_edit_action( - effective_action, - key, - &mut admin_state.add_logic_state, - ideal_cursor_column, - ) - .await? - } else if app_state.ui.show_register { - auth_e::execute_edit_action( - effective_action, - key, - register_state, - ideal_cursor_column, - ) - .await? - } else { + // Live search trigger while typing + else if form_state.autocomplete_active { + if let KeyCode::Char(_) | KeyCode::Backspace = key.code { + let action = if let KeyCode::Backspace = key.code { + "delete_char_backward" + } else { + "insert_char" + }; + // FIX: Pass &mut event_handler.ideal_cursor_column form_e::execute_edit_action( - effective_action, + action, key, form_state, - ideal_cursor_column, + &mut event_handler.ideal_cursor_column, ) - .await? - }; + .await?; + trigger_search = true; + } + } + } + + if trigger_search { + trigger_form_autocomplete_search( + form_state, + &mut event_handler.grpc_client, + event_handler.autocomplete_result_sender.clone(), + ) + .await; + return Ok(EditEventOutcome::Message("Searching...".to_string())); + } + + // --- GENERAL EDIT MODE EVENT HANDLING (IF NOT AUTOCOMPLETE) --- + + if let Some(action_str) = + config.get_edit_action_for_key(key.code, key.modifiers) + { + // 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( + "next_field", + key, + form_state, + &mut event_handler.ideal_cursor_column, + ) + .await?; return Ok(EditEventOutcome::Message(msg)); } + // Handle exiting edit mode if action_str == "exit" { - if app_state.ui.show_register && register_state.in_suggestion_mode { - let msg = auth_e::execute_edit_action( - "exit_suggestion_mode", - key, - register_state, - ideal_cursor_column, - ) - .await?; - return Ok(EditEventOutcome::Message(msg)); - } else if app_state.ui.show_add_logic - && admin_state - .add_logic_state - .in_target_column_suggestion_mode - { - admin_state - .add_logic_state - .in_target_column_suggestion_mode = false; - admin_state - .add_logic_state - .show_target_column_suggestions = false; - admin_state - .add_logic_state - .selected_target_column_suggestion_index = None; - return Ok(EditEventOutcome::Message( - "Exited column suggestions".to_string(), - )); - } else { - return Ok(EditEventOutcome::ExitEditMode); - } - } - - if app_state.ui.show_add_logic - && admin_state.add_logic_state.current_field() == 1 - { - if action_str == "suggestion_down" { - if !admin_state - .add_logic_state - .in_target_column_suggestion_mode - { - if let Some(profile_name) = - admin_state.add_logic_state.profile_name.clone().into() - { - if let Some(table_name) = admin_state - .add_logic_state - .selected_table_name - .clone() - { - debug!("Fetching table structure for autocomplete: Profile='{}', Table='{}'", profile_name, table_name); - match grpc_client - .get_table_structure(profile_name, table_name) - .await - { - Ok(ts_response) => { - admin_state - .add_logic_state - .table_columns_for_suggestions = - ts_response - .columns - .into_iter() - .map(|c| c.name) - .collect(); - admin_state - .add_logic_state - .update_target_column_suggestions(); - if !admin_state - .add_logic_state - .target_column_suggestions - .is_empty() - { - admin_state - .add_logic_state - .in_target_column_suggestion_mode = - true; - return Ok(EditEventOutcome::Message( - "Column suggestions shown" - .to_string(), - )); - } else { - return Ok(EditEventOutcome::Message( - "No column suggestions for current input" - .to_string(), - )); - } - } - Err(e) => { - debug!( - "Error fetching table structure: {}", - e - ); - admin_state - .add_logic_state - .table_columns_for_suggestions - .clear(); - admin_state - .add_logic_state - .update_target_column_suggestions(); - return Ok(EditEventOutcome::Message( - format!("Error fetching columns: {}", e), - )); - } - } - } else { - return Ok(EditEventOutcome::Message( - "No table selected for column suggestions" - .to_string(), - )); - } - } else { - return Ok(EditEventOutcome::Message( - "Profile name missing for column suggestions" - .to_string(), - )); - } - } else { - let msg = add_logic_e::execute_edit_action( - action_str, - key, - &mut admin_state.add_logic_state, - ideal_cursor_column, - ) - .await?; - return Ok(EditEventOutcome::Message(msg)); - } - } else if admin_state - .add_logic_state - .in_target_column_suggestion_mode - && action_str == "suggestion_up" - { - let msg = add_logic_e::execute_edit_action( - action_str, - key, - &mut admin_state.add_logic_state, - ideal_cursor_column, - ) - .await?; - return Ok(EditEventOutcome::Message(msg)); - } - } - - if app_state.ui.show_register && register_state.current_field() == 4 { - if !register_state.in_suggestion_mode - && action_str == "suggestion_down" - { - register_state.update_role_suggestions(); - if !register_state.role_suggestions.is_empty() { - register_state.in_suggestion_mode = true; - return Ok(EditEventOutcome::Message( - "Role suggestions shown".to_string(), - )); - } - } - if register_state.in_suggestion_mode - && matches!(action_str, "suggestion_down" | "suggestion_up") - { - let msg = auth_e::execute_edit_action( - action_str, - key, - register_state, - ideal_cursor_column, - ) - .await?; - return Ok(EditEventOutcome::Message(msg)); - } + return Ok(EditEventOutcome::ExitEditMode); } + // Handle all other edit actions let msg = if app_state.ui.show_login { + // FIX: Pass &mut event_handler.ideal_cursor_column auth_e::execute_edit_action( action_str, key, login_state, - ideal_cursor_column, + &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, key, &mut admin_state.add_table_state, - ideal_cursor_column, + &mut event_handler.ideal_cursor_column, ) .await? } else if app_state.ui.show_add_logic { - if !(admin_state - .add_logic_state - .in_target_column_suggestion_mode - && matches!(action_str, "suggestion_down" | "suggestion_up")) - { - add_logic_e::execute_edit_action( - action_str, - key, - &mut admin_state.add_logic_state, - ideal_cursor_column, - ) - .await? - } else { - String::new() - } + // FIX: Pass &mut event_handler.ideal_cursor_column + add_logic_e::execute_edit_action( + action_str, + key, + &mut admin_state.add_logic_state, + &mut event_handler.ideal_cursor_column, + ) + .await? } else if app_state.ui.show_register { - if !(register_state.in_suggestion_mode - && matches!(action_str, "suggestion_down" | "suggestion_up")) - { - auth_e::execute_edit_action( - action_str, - key, - register_state, - ideal_cursor_column, - ) - .await? - } else { - String::new() - } + // FIX: Pass &mut event_handler.ideal_cursor_column + auth_e::execute_edit_action( + action_str, + key, + register_state, + &mut event_handler.ideal_cursor_column, + ) + .await? } else { + // FIX: Pass &mut event_handler.ideal_cursor_column form_e::execute_edit_action( action_str, key, form_state, - ideal_cursor_column, + &mut event_handler.ideal_cursor_column, ) .await? }; return Ok(EditEventOutcome::Message(msg)); } - let mut exited_suggestion_mode_for_typing = false; - if app_state.ui.show_register && register_state.in_suggestion_mode { - register_state.in_suggestion_mode = false; - register_state.show_role_suggestions = false; - register_state.selected_suggestion_index = None; - exited_suggestion_mode_for_typing = true; - } - if app_state.ui.show_add_logic - && admin_state.add_logic_state.in_target_column_suggestion_mode - { - admin_state.add_logic_state.in_target_column_suggestion_mode = false; - admin_state.add_logic_state.show_target_column_suggestions = false; - admin_state - .add_logic_state - .selected_target_column_suggestion_index = None; - exited_suggestion_mode_for_typing = true; + // --- 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", + key, + 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", + key, + &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", + key, + &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", + key, + register_state, + &mut event_handler.ideal_cursor_column, + ) + .await? + } else { + // FIX: Pass &mut event_handler.ideal_cursor_column + form_e::execute_edit_action( + "insert_char", + key, + form_state, + &mut event_handler.ideal_cursor_column, + ) + .await? + }; + return Ok(EditEventOutcome::Message(msg)); } - let mut char_insert_msg = if app_state.ui.show_login { - auth_e::execute_edit_action( - "insert_char", - key, - login_state, - ideal_cursor_column, - ) - .await? - } else if app_state.ui.show_add_table { - add_table_e::execute_edit_action( - "insert_char", - key, - &mut admin_state.add_table_state, - ideal_cursor_column, - ) - .await? - } else if app_state.ui.show_add_logic { - add_logic_e::execute_edit_action( - "insert_char", - key, - &mut admin_state.add_logic_state, - ideal_cursor_column, - ) - .await? - } else if app_state.ui.show_register { - auth_e::execute_edit_action( - "insert_char", - key, - register_state, - ideal_cursor_column, - ) - .await? - } else { - form_e::execute_edit_action( - "insert_char", - key, - form_state, - ideal_cursor_column, - ) - .await? - }; - - if app_state.ui.show_register && register_state.current_field() == 4 { - register_state.update_role_suggestions(); - } - if app_state.ui.show_add_logic - && admin_state.add_logic_state.current_field() == 1 - { - admin_state.add_logic_state.update_target_column_suggestions(); - } - - if exited_suggestion_mode_for_typing && char_insert_msg.is_empty() { - char_insert_msg = "Suggestions hidden".to_string(); - } - - Ok(EditEventOutcome::Message(char_insert_msg)) + Ok(EditEventOutcome::Message(String::new())) // No action taken } diff --git a/client/src/modes/handlers/event.rs b/client/src/modes/handlers/event.rs index 3a10713..d3905fc 100644 --- a/client/src/modes/handlers/event.rs +++ b/client/src/modes/handlers/event.rs @@ -47,7 +47,7 @@ use crossterm::cursor::SetCursorStyle; use crossterm::event::{Event, KeyCode, KeyEvent}; use tokio::sync::mpsc; use tokio::sync::mpsc::unbounded_channel; -use tracing::{info, error}; +use tracing::{error, info}; #[derive(Debug, Clone, PartialEq, Eq)] pub enum EventOutcome { @@ -85,6 +85,9 @@ pub struct EventHandler { pub navigation_state: NavigationState, pub search_result_sender: mpsc::UnboundedSender>, pub search_result_receiver: mpsc::UnboundedReceiver>, + // --- ADDED FOR LIVE AUTOCOMPLETE --- + pub autocomplete_result_sender: mpsc::UnboundedSender>, + pub autocomplete_result_receiver: mpsc::UnboundedReceiver>, } impl EventHandler { @@ -96,6 +99,7 @@ impl EventHandler { grpc_client: GrpcClient, ) -> Result { let (search_tx, search_rx) = unbounded_channel(); + let (autocomplete_tx, autocomplete_rx) = unbounded_channel(); // ADDED Ok(EventHandler { command_mode: false, command_input: String::new(), @@ -114,6 +118,9 @@ 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, }) } @@ -143,19 +150,28 @@ impl EventHandler { outcome_message = "Search cancelled".to_string(); } KeyCode::Enter => { - if let Some(selected_hit) = search_state.results.get(search_state.selected_index) { - if let Ok(data) = serde_json::from_str::>(&selected_hit.content_json) { + if let Some(selected_hit) = + search_state.results.get(search_state.selected_index) + { + if let Ok(data) = serde_json::from_str::< + std::collections::HashMap, + >(&selected_hit.content_json) + { let detached_pos = form_state.total_count + 2; - form_state.update_from_response(&data, detached_pos); + form_state + .update_from_response(&data, detached_pos); } should_close = true; - outcome_message = format!("Loaded record ID {}", selected_hit.id); + outcome_message = + format!("Loaded record ID {}", selected_hit.id); } } KeyCode::Up => search_state.previous_result(), KeyCode::Down => search_state.next_result(), KeyCode::Char(c) => { - search_state.input.insert(search_state.cursor_position, c); + search_state + .input + .insert(search_state.cursor_position, c); search_state.cursor_position += 1; trigger_search = true; } @@ -167,10 +183,12 @@ impl EventHandler { } } KeyCode::Left => { - search_state.cursor_position = search_state.cursor_position.saturating_sub(1); + search_state.cursor_position = + search_state.cursor_position.saturating_sub(1); } KeyCode::Right => { - if search_state.cursor_position < search_state.input.len() { + if search_state.cursor_position < search_state.input.len() + { search_state.cursor_position += 1; } } @@ -188,13 +206,19 @@ impl EventHandler { let sender = self.search_result_sender.clone(); let mut grpc_client = self.grpc_client.clone(); - info!("--- 1. Spawning search task for query: '{}' ---", query); + info!( + "--- 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 { Ok(response) => { - info!("--- 3a. gRPC call successful. Found {} hits. ---", response.hits.len()); + info!( + "--- 3a. gRPC call successful. Found {} hits. ---", + response.hits.len() + ); let _ = sender.send(response.hits); } Err(e) => { @@ -237,22 +261,33 @@ impl EventHandler { 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, form_state, app_state).await; + return self + .handle_search_palette_event( + key_event, + form_state, + app_state, + ) + .await; } return Ok(EventOutcome::Ok(String::new())); } - let mut current_mode = ModeManager::derive_mode(app_state, self, admin_state); + let mut current_mode = + ModeManager::derive_mode(app_state, self, admin_state); if current_mode == AppMode::General && self.navigation_state.active { if let Event::Key(key_event) = event { - let outcome = - handle_command_navigation_event(&mut self.navigation_state, key_event, config) - .await?; + let outcome = handle_command_navigation_event( + &mut self.navigation_state, + key_event, + config, + ) + .await?; if !self.navigation_state.active { self.command_message = outcome.get_message_if_ok(); - current_mode = ModeManager::derive_mode(app_state, self, admin_state); + current_mode = + ModeManager::derive_mode(app_state, self, admin_state); } app_state.update_mode(current_mode); return Ok(outcome); @@ -265,23 +300,39 @@ impl EventHandler { let current_view = { let ui = &app_state.ui; - if ui.show_intro { AppView::Intro } - else if ui.show_login { AppView::Login } - else if ui.show_register { AppView::Register } - else if ui.show_admin { AppView::Admin } - else if ui.show_add_logic { AppView::AddLogic } - else if ui.show_add_table { AppView::AddTable } - else if ui.show_form { AppView::Form } - else { AppView::Scratch } + if ui.show_intro { + AppView::Intro + } else if ui.show_login { + AppView::Login + } else if ui.show_register { + AppView::Register + } else if ui.show_admin { + AppView::Admin + } else if ui.show_add_logic { + AppView::AddLogic + } else if ui.show_add_table { + AppView::AddTable + } else if ui.show_form { + AppView::Form + } else { + AppView::Scratch + } }; buffer_state.update_history(current_view); if app_state.ui.dialog.dialog_show { if let Event::Key(key_event) = event { if let Some(dialog_result) = dialog::handle_dialog_event( - &Event::Key(key_event), config, app_state, login_state, - register_state, buffer_state, admin_state, - ).await { + &Event::Key(key_event), + config, + app_state, + login_state, + register_state, + buffer_state, + admin_state, + ) + .await + { return dialog_result; } } else if let Event::Resize(_, _) = event { @@ -293,45 +344,88 @@ impl EventHandler { let key_code = key_event.code; let modifiers = key_event.modifiers; - if UiStateHandler::toggle_sidebar(&mut app_state.ui, config, key_code, modifiers) { - let message = format!("Sidebar {}", if app_state.ui.show_sidebar { "shown" } else { "hidden" }); + if UiStateHandler::toggle_sidebar( + &mut app_state.ui, + config, + key_code, + modifiers, + ) { + let message = format!( + "Sidebar {}", + if app_state.ui.show_sidebar { + "shown" + } else { + "hidden" + } + ); return Ok(EventOutcome::Ok(message)); } - if UiStateHandler::toggle_buffer_list(&mut app_state.ui, config, key_code, modifiers) { - let message = format!("Buffer {}", if app_state.ui.show_buffer_list { "shown" } else { "hidden" }); + if UiStateHandler::toggle_buffer_list( + &mut app_state.ui, + config, + key_code, + modifiers, + ) { + let message = format!( + "Buffer {}", + if app_state.ui.show_buffer_list { + "shown" + } else { + "hidden" + } + ); return Ok(EventOutcome::Ok(message)); } if !matches!(current_mode, AppMode::Edit | AppMode::Command) { - if let Some(action) = config.get_action_for_key_in_mode(&config.keybindings.global, key_code, modifiers) { + if let Some(action) = config.get_action_for_key_in_mode( + &config.keybindings.global, + key_code, + modifiers, + ) { match action { "next_buffer" => { if buffer::switch_buffer(buffer_state, true) { - return Ok(EventOutcome::Ok("Switched to next buffer".to_string())); + return Ok(EventOutcome::Ok( + "Switched to next buffer".to_string(), + )); } } "previous_buffer" => { if buffer::switch_buffer(buffer_state, false) { - return Ok(EventOutcome::Ok("Switched to previous buffer".to_string())); + return Ok(EventOutcome::Ok( + "Switched to previous buffer".to_string(), + )); } } "close_buffer" => { - let current_table_name = app_state.current_view_table_name.as_deref(); - let message = buffer_state.close_buffer_with_intro_fallback(current_table_name); + let current_table_name = + app_state.current_view_table_name.as_deref(); + let message = buffer_state + .close_buffer_with_intro_fallback( + current_table_name, + ); return Ok(EventOutcome::Ok(message)); } _ => {} } } - if let Some(action) = config.get_general_action(key_code, modifiers) { + if let Some(action) = + config.get_general_action(key_code, modifiers) + { if action == "open_search" { if app_state.ui.show_form { - if let Some(table_name) = app_state.current_view_table_name.clone() { + if let Some(table_name) = + app_state.current_view_table_name.clone() + { app_state.ui.show_search_palette = true; - app_state.search_state = Some(SearchState::new(table_name)); + app_state.search_state = + Some(SearchState::new(table_name)); app_state.ui.focus_outside_canvas = true; - return Ok(EventOutcome::Ok("Search palette opened".to_string())); + return Ok(EventOutcome::Ok( + "Search palette opened".to_string(), + )); } } } @@ -340,9 +434,20 @@ impl EventHandler { match current_mode { AppMode::General => { - if app_state.ui.show_admin && auth_state.role.as_deref() == Some("admin") { - if admin_nav::handle_admin_navigation(key_event, config, app_state, admin_state, buffer_state, &mut self.command_message) { - return Ok(EventOutcome::Ok(self.command_message.clone())); + if app_state.ui.show_admin + && auth_state.role.as_deref() == Some("admin") + { + if admin_nav::handle_admin_navigation( + key_event, + config, + app_state, + admin_state, + buffer_state, + &mut self.command_message, + ) { + return Ok(EventOutcome::Ok( + self.command_message.clone(), + )); } } @@ -350,10 +455,19 @@ impl EventHandler { let client_clone = self.grpc_client.clone(); let sender_clone = self.save_logic_result_sender.clone(); if add_logic_nav::handle_add_logic_navigation( - key_event, config, app_state, &mut admin_state.add_logic_state, - &mut self.is_edit_mode, buffer_state, client_clone, sender_clone, &mut self.command_message, + key_event, + config, + app_state, + &mut admin_state.add_logic_state, + &mut self.is_edit_mode, + buffer_state, + client_clone, + sender_clone, + &mut self.command_message, ) { - return Ok(EventOutcome::Ok(self.command_message.clone())); + return Ok(EventOutcome::Ok( + self.command_message.clone(), + )); } } @@ -361,44 +475,96 @@ impl EventHandler { let client_clone = self.grpc_client.clone(); let sender_clone = self.save_table_result_sender.clone(); if add_table_nav::handle_add_table_navigation( - key_event, config, app_state, &mut admin_state.add_table_state, - client_clone, sender_clone, &mut self.command_message, + key_event, + config, + app_state, + &mut admin_state.add_table_state, + client_clone, + sender_clone, + &mut self.command_message, ) { - return Ok(EventOutcome::Ok(self.command_message.clone())); + return Ok(EventOutcome::Ok( + self.command_message.clone(), + )); } } let nav_outcome = navigation::handle_navigation_event( - key_event, config, form_state, app_state, login_state, register_state, - intro_state, admin_state, &mut self.command_mode, &mut self.command_input, - &mut self.command_message, &mut self.navigation_state, - ).await; + key_event, + config, + form_state, + app_state, + login_state, + register_state, + intro_state, + admin_state, + &mut self.command_mode, + &mut self.command_input, + &mut self.command_message, + &mut self.navigation_state, + ) + .await; match nav_outcome { Ok(EventOutcome::ButtonSelected { context, index }) => { let message = match context { UiContext::Intro => { - intro::handle_intro_selection(app_state, buffer_state, index); - if app_state.ui.show_admin && !app_state.profile_tree.profiles.is_empty() { - admin_state.profile_list_state.select(Some(0)); + intro::handle_intro_selection( + app_state, + buffer_state, + index, + ); + if app_state.ui.show_admin + && !app_state + .profile_tree + .profiles + .is_empty() + { + admin_state + .profile_list_state + .select(Some(0)); } format!("Intro Option {} selected", index) } UiContext::Login => match index { - 0 => login::initiate_login(login_state, app_state, self.auth_client.clone(), self.login_result_sender.clone()), - 1 => login::back_to_main(login_state, app_state, buffer_state).await, + 0 => login::initiate_login( + login_state, + app_state, + self.auth_client.clone(), + self.login_result_sender.clone(), + ), + 1 => login::back_to_main( + login_state, + app_state, + buffer_state, + ) + .await, _ => "Invalid Login Option".to_string(), }, UiContext::Register => match index { - 0 => register::initiate_registration(register_state, app_state, self.auth_client.clone(), self.register_result_sender.clone()), - 1 => register::back_to_login(register_state, app_state, buffer_state).await, + 0 => register::initiate_registration( + register_state, + app_state, + self.auth_client.clone(), + self.register_result_sender.clone(), + ), + 1 => register::back_to_login( + register_state, + app_state, + buffer_state, + ) + .await, _ => "Invalid Login Option".to_string(), }, UiContext::Admin => { - admin::handle_admin_selection(app_state, admin_state); + admin::handle_admin_selection( + app_state, + admin_state, + ); format!("Admin Option {} selected", index) } - UiContext::Dialog => "Internal error: Unexpected dialog state".to_string(), + UiContext::Dialog => "Internal error: Unexpected dialog state" + .to_string(), }; return Ok(EventOutcome::Ok(message)); } @@ -450,34 +616,46 @@ impl EventHandler { return Ok(EventOutcome::Ok(String::new())); } - if let Some(action) = config.get_common_action(key_code, modifiers) { + if let Some(action) = + config.get_common_action(key_code, modifiers) + { match action { - "save" | "force_quit" | "save_and_quit" | "revert" => { + "save" | "force_quit" | "save_and_quit" + | "revert" => { return common_mode::handle_core_action( - action, form_state, auth_state, login_state, register_state, - &mut self.grpc_client, &mut self.auth_client, terminal, app_state, - ).await; + action, + form_state, + auth_state, + login_state, + register_state, + &mut self.grpc_client, + &mut self.auth_client, + terminal, + app_state, + ) + .await; } _ => {} } } - let (_should_exit, message) = read_only::handle_read_only_event( - app_state, - key_event, - config, - form_state, - login_state, - register_state, - &mut admin_state.add_table_state, - &mut admin_state.add_logic_state, - &mut self.key_sequence_tracker, - &mut self.grpc_client, // <-- FIX 1 - &mut self.command_message, - &mut self.edit_mode_cooldown, - &mut self.ideal_cursor_column, - ) - .await?; + let (_should_exit, message) = + read_only::handle_read_only_event( + app_state, + key_event, + config, + form_state, + login_state, + register_state, + &mut admin_state.add_table_state, + &mut admin_state.add_logic_state, + &mut self.key_sequence_tracker, + &mut self.grpc_client, // <-- FIX 1 + &mut self.command_message, + &mut self.edit_mode_cooldown, + &mut self.ideal_cursor_column, + ) + .await?; return Ok(EventOutcome::Ok(message)); } @@ -496,33 +674,45 @@ impl EventHandler { return Ok(EventOutcome::Ok("".to_string())); } - let (_should_exit, message) = read_only::handle_read_only_event( - app_state, - key_event, - config, - form_state, - login_state, - register_state, - &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.command_message, - &mut self.edit_mode_cooldown, - &mut self.ideal_cursor_column, - ) - .await?; + let (_should_exit, message) = + read_only::handle_read_only_event( + app_state, + key_event, + config, + form_state, + login_state, + register_state, + &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.command_message, + &mut self.edit_mode_cooldown, + &mut self.ideal_cursor_column, + ) + .await?; return Ok(EventOutcome::Ok(message)); } AppMode::Edit => { - if let Some(action) = config.get_common_action(key_code, modifiers) { + if let Some(action) = + config.get_common_action(key_code, modifiers) + { match action { - "save" | "force_quit" | "save_and_quit" | "revert" => { + "save" | "force_quit" | "save_and_quit" + | "revert" => { return common_mode::handle_core_action( - action, form_state, auth_state, login_state, register_state, - &mut self.grpc_client, &mut self.auth_client, terminal, app_state, // <-- FIX 3 - ).await; + action, + form_state, + auth_state, + login_state, + register_state, + &mut self.grpc_client, + &mut self.auth_client, + terminal, + app_state, + ) + .await; } _ => {} } @@ -530,11 +720,20 @@ impl EventHandler { let mut current_position = form_state.current_position; let total_count = form_state.total_count; + // --- MODIFIED: Pass `self` instead of `grpc_client` --- let edit_result = edit::handle_edit_event( - key_event, config, form_state, login_state, register_state, admin_state, - &mut self.ideal_cursor_column, &mut current_position, total_count, - &mut self.grpc_client, app_state, // <-- FIX 4 - ).await; + key_event, + config, + form_state, + login_state, + register_state, + admin_state, + &mut current_position, + total_count, + self, + app_state, + ) + .await; match edit_result { Ok(edit::EditEventOutcome::ExitEditMode) => { @@ -551,14 +750,22 @@ impl EventHandler { target_state.set_current_cursor_pos(new_pos); self.ideal_cursor_column = new_pos; } - return Ok(EventOutcome::Ok(self.command_message.clone())); + return Ok(EventOutcome::Ok( + self.command_message.clone(), + )); } Ok(edit::EditEventOutcome::Message(msg)) => { - if !msg.is_empty() { self.command_message = msg; } + if !msg.is_empty() { + self.command_message = msg; + } self.key_sequence_tracker.reset(); - return Ok(EventOutcome::Ok(self.command_message.clone())); + return Ok(EventOutcome::Ok( + self.command_message.clone(), + )); + } + Err(e) => { + return Err(e.into()); } - Err(e) => { return Err(e.into()); } } } @@ -568,21 +775,38 @@ impl EventHandler { self.command_message.clear(); self.command_mode = false; self.key_sequence_tracker.reset(); - return Ok(EventOutcome::Ok("Exited command mode".to_string())); + return Ok(EventOutcome::Ok( + "Exited command mode".to_string(), + )); } if config.is_command_execute(key_code, modifiers) { let mut current_position = form_state.current_position; let total_count = form_state.total_count; let outcome = command_mode::handle_command_event( - key_event, config, app_state, login_state, register_state, form_state, - &mut self.command_input, &mut self.command_message, &mut self.grpc_client, // <-- FIX 5 - command_handler, terminal, &mut current_position, total_count, - ).await?; + key_event, + config, + app_state, + login_state, + register_state, + form_state, + &mut self.command_input, + &mut self.command_message, + &mut self.grpc_client, // <-- FIX 5 + command_handler, + terminal, + &mut current_position, + total_count, + ) + .await?; form_state.current_position = current_position; self.command_mode = false; self.key_sequence_tracker.reset(); - let new_mode = ModeManager::derive_mode(app_state, self, admin_state); + let new_mode = ModeManager::derive_mode( + app_state, + self, + admin_state, + ); app_state.update_mode(new_mode); return Ok(outcome); } @@ -596,37 +820,59 @@ impl EventHandler { if let KeyCode::Char(c) = key_code { if c == 'f' { self.key_sequence_tracker.add_key(key_code); - let sequence = self.key_sequence_tracker.get_sequence(); + let sequence = + self.key_sequence_tracker.get_sequence(); - if config.matches_key_sequence_generalized(&sequence) == Some("find_file_palette_toggle") { - if app_state.ui.show_form || app_state.ui.show_intro { - let mut all_table_paths: Vec = app_state - .profile_tree - .profiles - .iter() - .flat_map(|profile| { - profile.tables.iter().map(move |table| { - format!("{}/{}", profile.name, table.name) + if config.matches_key_sequence_generalized( + &sequence, + ) == Some("find_file_palette_toggle") + { + if app_state.ui.show_form + || app_state.ui.show_intro + { + let mut all_table_paths: Vec = + app_state + .profile_tree + .profiles + .iter() + .flat_map(|profile| { + profile.tables.iter().map( + move |table| { + format!( + "{}/{}", + profile.name, + table.name + ) + }, + ) }) - }) - .collect(); + .collect(); all_table_paths.sort(); - self.navigation_state.activate_find_file(all_table_paths); + self.navigation_state + .activate_find_file(all_table_paths); self.command_mode = false; self.command_input.clear(); self.command_message.clear(); self.key_sequence_tracker.reset(); - return Ok(EventOutcome::Ok("Table selection palette activated".to_string())); + return Ok(EventOutcome::Ok( + "Table selection palette activated" + .to_string(), + )); } else { self.key_sequence_tracker.reset(); self.command_input.push('f'); - if sequence.len() > 1 && sequence[0] == KeyCode::Char('f') { + if sequence.len() > 1 + && sequence[0] == KeyCode::Char('f') + { self.command_input.push('f'); } - self.command_message = "Find File not available in this view.".to_string(); - return Ok(EventOutcome::Ok(self.command_message.clone())); + self.command_message = "Find File not available in this view." + .to_string(); + return Ok(EventOutcome::Ok( + self.command_message.clone(), + )); } } @@ -635,7 +881,9 @@ impl EventHandler { } } - if c != 'f' && !self.key_sequence_tracker.current_sequence.is_empty() { + if c != 'f' + && !self.key_sequence_tracker.current_sequence.is_empty() + { self.key_sequence_tracker.reset(); } diff --git a/client/src/ui/handlers/ui.rs b/client/src/ui/handlers/ui.rs index 6b320f1..770d393 100644 --- a/client/src/ui/handlers/ui.rs +++ b/client/src/ui/handlers/ui.rs @@ -140,6 +140,9 @@ pub async fn run_ui() -> Result<()> { let position_before_event = form_state.current_position; let mut event_processed = false; + // --- CHANNEL RECEIVERS --- + + // For main search palette match event_handler.search_result_receiver.try_recv() { Ok(hits) => { info!("--- 4. Main loop received message from channel. ---"); @@ -155,6 +158,29 @@ pub async fn run_ui() -> Result<()> { error!("Search result channel disconnected!"); } } + + // --- ADDED: For live form autocomplete --- + match event_handler.autocomplete_result_receiver.try_recv() { + Ok(hits) => { + if form_state.autocomplete_active { + form_state.autocomplete_suggestions = hits; + form_state.autocomplete_loading = false; + if !form_state.autocomplete_suggestions.is_empty() { + form_state.selected_suggestion_index = Some(0); + } else { + form_state.selected_suggestion_index = None; + } + event_handler.command_message = format!("Found {} suggestions.", form_state.autocomplete_suggestions.len()); + } + needs_redraw = true; + } + Err(mpsc::error::TryRecvError::Empty) => {} + Err(mpsc::error::TryRecvError::Disconnected) => { + error!("Autocomplete result channel disconnected!"); + } + } + + if app_state.ui.show_search_palette { needs_redraw = true; }