autocomplete is now powerful

This commit is contained in:
filipriec
2025-05-26 20:22:47 +02:00
parent bf2726c151
commit 43b064673b
4 changed files with 160 additions and 38 deletions

View File

@@ -11,6 +11,7 @@ use crate::services::GrpcClient;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use anyhow::Result; use anyhow::Result;
use crate::components::common::text_editor::TextEditor; use crate::components::common::text_editor::TextEditor;
use crate::services::ui_service::UiService;
pub type SaveLogicResultSender = mpsc::Sender<Result<String>>; pub type SaveLogicResultSender = mpsc::Sender<Result<String>>;
@@ -21,8 +22,8 @@ pub fn handle_add_logic_navigation(
add_logic_state: &mut AddLogicState, add_logic_state: &mut AddLogicState,
is_edit_mode: &mut bool, is_edit_mode: &mut bool,
buffer_state: &mut BufferState, buffer_state: &mut BufferState,
_grpc_client: GrpcClient, grpc_client: GrpcClient,
_save_logic_sender: SaveLogicResultSender, save_logic_sender: SaveLogicResultSender,
command_message: &mut String, command_message: &mut String,
) -> bool { ) -> bool {
// === FULLSCREEN SCRIPT EDITING - COMPLETE ISOLATION === // === 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 trigger_pos = add_logic_state.script_editor_trigger_position;
let filter_len = add_logic_state.script_editor_filter_text.len(); 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.deactivate_script_editor_autocomplete();
add_logic_state.has_unsaved_changes = true; add_logic_state.has_unsaved_changes = true;
// Then replace text // Replace text in editor
if let Some(pos) = trigger_pos { if let Some(pos) = trigger_pos {
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut(); let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
replace_autocomplete_text( replace_autocomplete_text(
@@ -147,9 +151,43 @@ pub fn handle_add_logic_navigation(
filter_len, filter_len,
&suggestion, &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); *command_message = format!("Inserted: {}", suggestion);
}
}
return true; // Consume the key return true; // Consume the key
} }
} }

View File

@@ -18,15 +18,15 @@ impl UiService {
let profile_name_clone_opt = Some(add_logic_state.profile_name.clone()); let profile_name_clone_opt = Some(add_logic_state.profile_name.clone());
let table_name_opt_clone = add_logic_state.selected_table_name.clone(); let table_name_opt_clone = add_logic_state.selected_table_name.clone();
// Collect all table names from all profiles // Collect table names from SAME profile only
let all_table_names: Vec<String> = profile_tree.profiles let same_profile_table_names: Vec<String> = profile_tree.profiles
.iter() .iter()
.flat_map(|profile| profile.tables.iter()) .find(|profile| profile.name == add_logic_state.profile_name)
.map(|table| table.name.clone()) .map(|profile| profile.tables.iter().map(|table| table.name.clone()).collect())
.collect(); .unwrap_or_default();
// Set all table names for autocomplete // Set same profile table names for autocomplete
add_logic_state.set_all_table_names(all_table_names.clone()); 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) { 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 { 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()); add_logic_state.set_table_columns(column_names.clone());
Ok(format!( Ok(format!(
"Loaded {} columns for table '{}' and {} total tables for autocomplete", "Loaded {} columns for table '{}' and {} tables from profile '{}'",
column_names.len(), column_names.len(),
table_name_clone, table_name_clone,
all_table_names.len() same_profile_table_names.len(),
add_logic_state.profile_name
)) ))
} }
Err(e) => { Err(e) => {
@@ -53,20 +54,43 @@ impl UiService {
e e
); );
Ok(format!( 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, table_name_clone,
all_table_names.len() same_profile_table_names.len(),
add_logic_state.profile_name
)) ))
} }
} }
} else { } else {
Ok(format!( Ok(format!(
"No table selected for Add Logic. Loaded {} tables for autocomplete.", "No table selected for Add Logic. Loaded {} tables from profile '{}' for autocomplete.",
all_table_names.len() 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<Vec<String>> {
match grpc_client.get_table_structure(profile_name.to_string(), table_name.to_string()).await {
Ok(response) => {
let column_names: Vec<String> = 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( pub async fn initialize_app_state(
grpc_client: &mut GrpcClient, grpc_client: &mut GrpcClient,
app_state: &mut AppState, app_state: &mut AppState,

View File

@@ -50,6 +50,10 @@ pub struct AddLogicState {
pub script_editor_trigger_position: Option<(usize, usize)>, // (line, column) pub script_editor_trigger_position: Option<(usize, usize)>, // (line, column)
pub all_table_names: Vec<String>, pub all_table_names: Vec<String>,
pub script_editor_filter_text: String, pub script_editor_filter_text: String,
// New fields for same-profile table names and column autocomplete
pub same_profile_table_names: Vec<String>, // Tables from same profile only
pub script_editor_awaiting_column_autocomplete: Option<String>, // Table name waiting for column fetch
} }
impl AddLogicState { impl AddLogicState {
@@ -78,13 +82,15 @@ impl AddLogicState {
selected_target_column_suggestion_index: None, selected_target_column_suggestion_index: None,
in_target_column_suggestion_mode: false, in_target_column_suggestion_mode: false,
// Script Editor Autocomplete initialization
script_editor_autocomplete_active: false, script_editor_autocomplete_active: false,
script_editor_suggestions: Vec::new(), script_editor_suggestions: Vec::new(),
script_editor_selected_suggestion_index: None, script_editor_selected_suggestion_index: None,
script_editor_trigger_position: None, script_editor_trigger_position: None,
all_table_names: Vec::new(), all_table_names: Vec::new(),
script_editor_filter_text: String::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(); self.show_target_column_suggestions = !self.target_column_suggestions.is_empty();
if self.show_target_column_suggestions { 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 let Some(selected_idx) = self.selected_target_column_suggestion_index {
if selected_idx >= self.target_column_suggestions.len() { if selected_idx >= self.target_column_suggestions.len() {
self.selected_target_column_suggestion_index = Some(0); 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 { } else {
self.selected_target_column_suggestion_index = Some(0); self.selected_target_column_suggestion_index = Some(0);
} }
@@ -143,8 +143,8 @@ impl AddLogicState {
// Add column names from the current table // Add column names from the current table
suggestions.extend(self.table_columns_for_suggestions.clone()); suggestions.extend(self.table_columns_for_suggestions.clone());
// Add all table names from all profiles // Add table names from SAME profile only
suggestions.extend(self.all_table_names.clone()); suggestions.extend(self.same_profile_table_names.clone());
if self.script_editor_filter_text.is_empty() { if self.script_editor_filter_text.is_empty() {
self.script_editor_suggestions = suggestions; self.script_editor_suggestions = suggestions;
@@ -172,7 +172,6 @@ impl AddLogicState {
/// Sets table columns for autocomplete suggestions /// Sets table columns for autocomplete suggestions
pub fn set_table_columns(&mut self, columns: Vec<String>) { pub fn set_table_columns(&mut self, columns: Vec<String>) {
self.table_columns_for_suggestions = columns.clone(); self.table_columns_for_suggestions = columns.clone();
// Also update target column suggestions for the input field
if !columns.is_empty() { if !columns.is_empty() {
self.update_target_column_suggestions(); self.update_target_column_suggestions();
} }
@@ -183,6 +182,47 @@ impl AddLogicState {
self.all_table_names = table_names; 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<String>) {
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<String>) {
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 /// Deactivates script editor autocomplete and clears related state
pub fn deactivate_script_editor_autocomplete(&mut self) { pub fn deactivate_script_editor_autocomplete(&mut self) {
self.script_editor_autocomplete_active = false; self.script_editor_autocomplete_active = false;
@@ -257,10 +297,9 @@ impl CanvasState for AddLogicState {
0 => AddLogicFocus::InputLogicName, 0 => AddLogicFocus::InputLogicName,
1 => AddLogicFocus::InputTargetColumn, 1 => AddLogicFocus::InputTargetColumn,
2 => AddLogicFocus::InputDescription, 2 => AddLogicFocus::InputDescription,
_ => return, // Or handle error/default _ => return,
}; };
if self.current_focus != new_focus { if self.current_focus != new_focus {
// If changing field, exit suggestion mode for target column
if self.current_focus == AddLogicFocus::InputTargetColumn { if self.current_focus == AddLogicFocus::InputTargetColumn {
self.in_target_column_suggestion_mode = false; self.in_target_column_suggestion_mode = false;
self.show_target_column_suggestions = 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()); self.logic_name_cursor_pos = pos.min(self.logic_name_input.len());
} }
AddLogicFocus::InputTargetColumn => { AddLogicFocus::InputTargetColumn => {
self.target_column_cursor_pos = self.target_column_cursor_pos = pos.min(self.target_column_input.len());
pos.min(self.target_column_input.len());
} }
AddLogicFocus::InputDescription => { AddLogicFocus::InputDescription => {
self.description_cursor_pos = self.description_cursor_pos = pos.min(self.description_input.len());
pos.min(self.description_input.len());
} }
_ => {} _ => {}
} }
@@ -292,7 +329,7 @@ impl CanvasState for AddLogicState {
} }
fn get_suggestions(&self) -> Option<&[String]> { 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.in_target_column_suggestion_mode
&& self.show_target_column_suggestions && self.show_target_column_suggestions
{ {
@@ -303,7 +340,7 @@ impl CanvasState for AddLogicState {
} }
fn get_selected_suggestion_index(&self) -> Option<usize> { fn get_selected_suggestion_index(&self) -> Option<usize> {
if self.current_field() == 1 // Target Column field index if self.current_field() == 1
&& self.in_target_column_suggestion_mode && self.in_target_column_suggestion_mode
&& self.show_target_column_suggestions && self.show_target_column_suggestions
{ {

View File

@@ -224,6 +224,29 @@ pub async fn run_ui() -> Result<()> {
needs_redraw = false; 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 --- // --- Cursor Visibility Logic ---
let current_mode = ModeManager::derive_mode(&app_state, &event_handler, &admin_state); let current_mode = ModeManager::derive_mode(&app_state, &event_handler, &admin_state);
match current_mode { match current_mode {