diff --git a/client/src/functions/modes/navigation/add_logic_nav.rs b/client/src/functions/modes/navigation/add_logic_nav.rs index aff4476..479e6a0 100644 --- a/client/src/functions/modes/navigation/add_logic_nav.rs +++ b/client/src/functions/modes/navigation/add_logic_nav.rs @@ -11,6 +11,7 @@ use crate::services::GrpcClient; use tokio::sync::mpsc; use anyhow::Result; use crate::components::common::text_editor::TextEditor; +use crate::services::ui_service::UiService; pub type SaveLogicResultSender = mpsc::Sender>; @@ -21,8 +22,8 @@ pub fn handle_add_logic_navigation( add_logic_state: &mut AddLogicState, is_edit_mode: &mut bool, buffer_state: &mut BufferState, - _grpc_client: GrpcClient, - _save_logic_sender: SaveLogicResultSender, + grpc_client: GrpcClient, + save_logic_sender: SaveLogicResultSender, command_message: &mut String, ) -> bool { // === FULLSCREEN SCRIPT EDITING - COMPLETE ISOLATION === @@ -134,11 +135,14 @@ pub fn handle_add_logic_navigation( let trigger_pos = add_logic_state.script_editor_trigger_position; let filter_len = add_logic_state.script_editor_filter_text.len(); - // Deactivate autocomplete first + // Check if this is a table name selection + let is_table_selection = add_logic_state.is_table_name_suggestion(&suggestion); + + // Deactivate current autocomplete first add_logic_state.deactivate_script_editor_autocomplete(); add_logic_state.has_unsaved_changes = true; - // Then replace text + // Replace text in editor if let Some(pos) = trigger_pos { let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut(); replace_autocomplete_text( @@ -147,9 +151,43 @@ pub fn handle_add_logic_navigation( filter_len, &suggestion, ); - } - *command_message = format!("Inserted: {}", suggestion); + // If it's a table selection, append "." and trigger column autocomplete + if is_table_selection { + editor_borrow.insert_str("."); + + // Get the new cursor position (after table name and dot) + let new_cursor = editor_borrow.cursor(); + drop(editor_borrow); // Release the borrow + + // Set up for column autocomplete + add_logic_state.script_editor_trigger_position = Some(new_cursor); + add_logic_state.script_editor_autocomplete_active = true; + add_logic_state.script_editor_filter_text.clear(); + add_logic_state.trigger_column_autocomplete_for_table(suggestion.clone()); + + // Initiate async column fetch + let profile_name = add_logic_state.profile_name.clone(); + let table_name = suggestion.clone(); + let mut client_clone = grpc_client.clone(); + + tokio::spawn(async move { + match UiService::fetch_columns_for_table(&mut client_clone, &profile_name, &table_name).await { + Ok(columns) => { + // Note: In a real implementation, you'd need to send this back to the main thread + // For now, we'll handle this synchronously in the main thread + } + Err(e) => { + tracing::error!("Failed to fetch columns for {}.{}: {}", profile_name, table_name, e); + } + } + }); + + *command_message = format!("Selected table '{}', fetching columns...", suggestion); + } else { + *command_message = format!("Inserted: {}", suggestion); + } + } return true; // Consume the key } } diff --git a/client/src/services/ui_service.rs b/client/src/services/ui_service.rs index 53f463e..3ed4a9d 100644 --- a/client/src/services/ui_service.rs +++ b/client/src/services/ui_service.rs @@ -18,15 +18,15 @@ impl UiService { let profile_name_clone_opt = Some(add_logic_state.profile_name.clone()); let table_name_opt_clone = add_logic_state.selected_table_name.clone(); - // Collect all table names from all profiles - let all_table_names: Vec = profile_tree.profiles + // Collect table names from SAME profile only + let same_profile_table_names: Vec = profile_tree.profiles .iter() - .flat_map(|profile| profile.tables.iter()) - .map(|table| table.name.clone()) - .collect(); + .find(|profile| profile.name == add_logic_state.profile_name) + .map(|profile| profile.tables.iter().map(|table| table.name.clone()).collect()) + .unwrap_or_default(); - // Set all table names for autocomplete - add_logic_state.set_all_table_names(all_table_names.clone()); + // Set same profile table names for autocomplete + add_logic_state.set_same_profile_table_names(same_profile_table_names.clone()); if let (Some(profile_name_clone), Some(table_name_clone)) = (profile_name_clone_opt, table_name_opt_clone) { match grpc_client.get_table_structure(profile_name_clone.clone(), table_name_clone.clone()).await { @@ -39,10 +39,11 @@ impl UiService { add_logic_state.set_table_columns(column_names.clone()); Ok(format!( - "Loaded {} columns for table '{}' and {} total tables for autocomplete", + "Loaded {} columns for table '{}' and {} tables from profile '{}'", column_names.len(), table_name_clone, - all_table_names.len() + same_profile_table_names.len(), + add_logic_state.profile_name )) } Err(e) => { @@ -53,20 +54,43 @@ impl UiService { e ); Ok(format!( - "Warning: Could not load table structure for '{}'. Autocomplete will use basic suggestions with {} tables.", + "Warning: Could not load table structure for '{}'. Autocomplete will use {} tables from profile '{}'.", table_name_clone, - all_table_names.len() + same_profile_table_names.len(), + add_logic_state.profile_name )) } } } else { Ok(format!( - "No table selected for Add Logic. Loaded {} tables for autocomplete.", - all_table_names.len() + "No table selected for Add Logic. Loaded {} tables from profile '{}' for autocomplete.", + same_profile_table_names.len(), + add_logic_state.profile_name )) } } + /// Fetches columns for a specific table (used for table.column autocomplete) + pub async fn fetch_columns_for_table( + grpc_client: &mut GrpcClient, + profile_name: &str, + table_name: &str, + ) -> Result> { + match grpc_client.get_table_structure(profile_name.to_string(), table_name.to_string()).await { + Ok(response) => { + let column_names: Vec = response.columns + .into_iter() + .map(|col| col.name) + .collect(); + Ok(column_names) + } + Err(e) => { + tracing::warn!("Failed to fetch columns for {}.{}: {}", profile_name, table_name, e); + Err(e.into()) + } + } + } + pub async fn initialize_app_state( grpc_client: &mut GrpcClient, app_state: &mut AppState, diff --git a/client/src/state/pages/add_logic.rs b/client/src/state/pages/add_logic.rs index 993a7f6..6e1b178 100644 --- a/client/src/state/pages/add_logic.rs +++ b/client/src/state/pages/add_logic.rs @@ -48,8 +48,12 @@ pub struct AddLogicState { pub script_editor_suggestions: Vec, pub script_editor_selected_suggestion_index: Option, pub script_editor_trigger_position: Option<(usize, usize)>, // (line, column) - pub all_table_names: Vec, + pub all_table_names: Vec, pub script_editor_filter_text: String, + + // New fields for same-profile table names and column autocomplete + pub same_profile_table_names: Vec, // Tables from same profile only + pub script_editor_awaiting_column_autocomplete: Option, // Table name waiting for column fetch } impl AddLogicState { @@ -78,13 +82,15 @@ impl AddLogicState { selected_target_column_suggestion_index: None, in_target_column_suggestion_mode: false, - // Script Editor Autocomplete initialization script_editor_autocomplete_active: false, script_editor_suggestions: Vec::new(), script_editor_selected_suggestion_index: None, 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, } } @@ -113,16 +119,10 @@ impl AddLogicState { self.show_target_column_suggestions = !self.target_column_suggestions.is_empty(); if self.show_target_column_suggestions { - // If suggestions are shown, ensure a selection (usually the first) - // or maintain current if it's still valid. if let Some(selected_idx) = self.selected_target_column_suggestion_index { if selected_idx >= self.target_column_suggestions.len() { self.selected_target_column_suggestion_index = Some(0); } - // If the previously selected item is no longer in the filtered list, reset. - // This is a bit more complex to check perfectly without iterating again. - // For now, just ensuring it's within bounds is a good start. - // A more robust way would be to check if the string at selected_idx still matches. } else { self.selected_target_column_suggestion_index = Some(0); } @@ -143,8 +143,8 @@ impl AddLogicState { // Add column names from the current table suggestions.extend(self.table_columns_for_suggestions.clone()); - // Add all table names from all profiles - suggestions.extend(self.all_table_names.clone()); + // Add table names from SAME profile only + suggestions.extend(self.same_profile_table_names.clone()); if self.script_editor_filter_text.is_empty() { self.script_editor_suggestions = suggestions; @@ -172,7 +172,6 @@ impl AddLogicState { /// Sets table columns for autocomplete suggestions pub fn set_table_columns(&mut self, columns: Vec) { self.table_columns_for_suggestions = columns.clone(); - // Also update target column suggestions for the input field if !columns.is_empty() { self.update_target_column_suggestions(); } @@ -183,6 +182,47 @@ impl AddLogicState { self.all_table_names = table_names; } + /// Sets table names from the same profile for autocomplete suggestions + pub fn set_same_profile_table_names(&mut self, table_names: Vec) { + self.same_profile_table_names = table_names; + } + + /// Checks if a suggestion is a table name (for triggering column autocomplete) + pub fn is_table_name_suggestion(&self, suggestion: &str) -> bool { + if suggestion == "sql" { + return false; + } + + if let Some(ref current_table) = self.selected_table_name { + if suggestion == current_table { + return false; + } + } + + if self.table_columns_for_suggestions.contains(&suggestion.to_string()) { + return false; + } + + self.same_profile_table_names.contains(&suggestion.to_string()) + } + + /// Triggers waiting for column autocomplete for a specific table + pub fn trigger_column_autocomplete_for_table(&mut self, table_name: String) { + self.script_editor_awaiting_column_autocomplete = Some(table_name); + } + + /// Updates autocomplete with columns for a specific table + pub fn set_columns_for_table_autocomplete(&mut self, columns: Vec) { + self.script_editor_suggestions = columns; + self.script_editor_selected_suggestion_index = if self.script_editor_suggestions.is_empty() { + None + } else { + Some(0) + }; + self.script_editor_autocomplete_active = !self.script_editor_suggestions.is_empty(); + self.script_editor_awaiting_column_autocomplete = None; + } + /// Deactivates script editor autocomplete and clears related state pub fn deactivate_script_editor_autocomplete(&mut self) { self.script_editor_autocomplete_active = false; @@ -257,10 +297,9 @@ impl CanvasState for AddLogicState { 0 => AddLogicFocus::InputLogicName, 1 => AddLogicFocus::InputTargetColumn, 2 => AddLogicFocus::InputDescription, - _ => return, // Or handle error/default + _ => return, }; if self.current_focus != new_focus { - // If changing field, exit suggestion mode for target column if self.current_focus == AddLogicFocus::InputTargetColumn { self.in_target_column_suggestion_mode = false; self.show_target_column_suggestions = false; @@ -276,12 +315,10 @@ impl CanvasState for AddLogicState { self.logic_name_cursor_pos = pos.min(self.logic_name_input.len()); } AddLogicFocus::InputTargetColumn => { - self.target_column_cursor_pos = - pos.min(self.target_column_input.len()); + self.target_column_cursor_pos = pos.min(self.target_column_input.len()); } AddLogicFocus::InputDescription => { - self.description_cursor_pos = - pos.min(self.description_input.len()); + self.description_cursor_pos = pos.min(self.description_input.len()); } _ => {} } @@ -292,7 +329,7 @@ impl CanvasState for AddLogicState { } fn get_suggestions(&self) -> Option<&[String]> { - if self.current_field() == 1 // Target Column field index + if self.current_field() == 1 && self.in_target_column_suggestion_mode && self.show_target_column_suggestions { @@ -303,7 +340,7 @@ impl CanvasState for AddLogicState { } fn get_selected_suggestion_index(&self) -> Option { - if self.current_field() == 1 // Target Column field index + if self.current_field() == 1 && self.in_target_column_suggestion_mode && self.show_target_column_suggestions { diff --git a/client/src/ui/handlers/ui.rs b/client/src/ui/handlers/ui.rs index 02aeed3..01f2906 100644 --- a/client/src/ui/handlers/ui.rs +++ b/client/src/ui/handlers/ui.rs @@ -224,6 +224,29 @@ pub async fn run_ui() -> Result<()> { needs_redraw = false; } + // --- Handle Pending Column Autocomplete for Table Selection --- + if let Some(table_name) = admin_state.add_logic_state.script_editor_awaiting_column_autocomplete.clone() { + if app_state.ui.show_add_logic { + let profile_name = admin_state.add_logic_state.profile_name.clone(); + + info!("Fetching columns for table selection: {}.{}", profile_name, table_name); + match UiService::fetch_columns_for_table(&mut grpc_client, &profile_name, &table_name).await { + Ok(columns) => { + admin_state.add_logic_state.set_columns_for_table_autocomplete(columns.clone()); + info!("Loaded {} columns for table '{}'", columns.len(), table_name); + event_handler.command_message = format!("Columns for '{}' loaded. Select a column.", table_name); + } + Err(e) => { + error!("Failed to fetch columns for {}.{}: {}", profile_name, table_name, e); + admin_state.add_logic_state.script_editor_awaiting_column_autocomplete = None; + admin_state.add_logic_state.deactivate_script_editor_autocomplete(); + event_handler.command_message = format!("Error loading columns for '{}': {}", table_name, e); + } + } + needs_redraw = true; + } + } + // --- Cursor Visibility Logic --- let current_mode = ModeManager::derive_mode(&app_state, &event_handler, &admin_state); match current_mode {