better autocomplete

This commit is contained in:
filipriec
2025-05-26 20:43:58 +02:00
parent 43b064673b
commit b770240f0d
2 changed files with 84 additions and 152 deletions

View File

@@ -12,6 +12,7 @@ use tokio::sync::mpsc;
use anyhow::Result;
use crate::components::common::text_editor::TextEditor;
use crate::services::ui_service::UiService;
use tui_textarea::CursorMove;
pub type SaveLogicResultSender = mpsc::Sender<Result<String>>;
@@ -23,21 +24,18 @@ pub fn handle_add_logic_navigation(
is_edit_mode: &mut bool,
buffer_state: &mut BufferState,
grpc_client: GrpcClient,
save_logic_sender: SaveLogicResultSender,
_save_logic_sender: SaveLogicResultSender, // Marked as unused
command_message: &mut String,
) -> bool {
// === FULLSCREEN SCRIPT EDITING - COMPLETE ISOLATION ===
if add_logic_state.current_focus == AddLogicFocus::InsideScriptContent {
// === AUTOCOMPLETE HANDLING ===
if add_logic_state.script_editor_autocomplete_active {
match key_event.code {
// ... (Char, Backspace, Tab, Down, Up cases remain the same) ...
KeyCode::Char(c) if c.is_alphanumeric() || c == '_' => {
// Update filter text first
add_logic_state.script_editor_filter_text.push(c);
add_logic_state.update_script_editor_suggestions();
// Then handle editor input
{
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
TextEditor::handle_input(
@@ -46,18 +44,14 @@ pub fn handle_add_logic_navigation(
&add_logic_state.editor_keybinding_mode,
&mut add_logic_state.vim_state,
);
} // Drop editor borrow
}
*command_message = format!("Filtering: @{}", add_logic_state.script_editor_filter_text);
return true;
}
KeyCode::Backspace => {
if !add_logic_state.script_editor_filter_text.is_empty() {
// Remove last character from filter
add_logic_state.script_editor_filter_text.pop();
add_logic_state.update_script_editor_suggestions();
// Then handle editor input
{
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
TextEditor::handle_input(
@@ -66,15 +60,13 @@ pub fn handle_add_logic_navigation(
&add_logic_state.editor_keybinding_mode,
&mut add_logic_state.vim_state,
);
} // Drop editor borrow
}
*command_message = if add_logic_state.script_editor_filter_text.is_empty() {
"Autocomplete: @".to_string()
} else {
format!("Filtering: @{}", add_logic_state.script_editor_filter_text)
};
} else {
// Check if we're deleting the @ trigger
let should_deactivate = if let Some((trigger_line, trigger_col)) = add_logic_state.script_editor_trigger_position {
let current_cursor = {
let editor_borrow = add_logic_state.script_content_editor.borrow();
@@ -84,13 +76,10 @@ pub fn handle_add_logic_navigation(
} else {
false
};
if should_deactivate {
add_logic_state.deactivate_script_editor_autocomplete();
*command_message = "Autocomplete cancelled".to_string();
}
// Handle editor input
{
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
TextEditor::handle_input(
@@ -99,22 +88,20 @@ pub fn handle_add_logic_navigation(
&add_logic_state.editor_keybinding_mode,
&mut add_logic_state.vim_state,
);
} // Drop editor borrow
}
}
return true;
}
KeyCode::Tab | KeyCode::Down => {
// Navigate suggestions down
if !add_logic_state.script_editor_suggestions.is_empty() {
let current = add_logic_state.script_editor_selected_suggestion_index.unwrap_or(0);
let next = (current + 1) % add_logic_state.script_editor_suggestions.len();
add_logic_state.script_editor_selected_suggestion_index = Some(next);
*command_message = format!("Selected: {}", add_logic_state.script_editor_suggestions[next]);
}
return true; // Consume the key
return true;
}
KeyCode::Up => {
// Navigate suggestions up
if !add_logic_state.script_editor_suggestions.is_empty() {
let current = add_logic_state.script_editor_selected_suggestion_index.unwrap_or(0);
let prev = if current == 0 {
@@ -125,74 +112,63 @@ pub fn handle_add_logic_navigation(
add_logic_state.script_editor_selected_suggestion_index = Some(prev);
*command_message = format!("Selected: {}", add_logic_state.script_editor_suggestions[prev]);
}
return true; // Consume the key
return true;
}
KeyCode::Enter => {
// Select current suggestion
if let Some(selected_idx) = add_logic_state.script_editor_selected_suggestion_index {
if let Some(suggestion) = add_logic_state.script_editor_suggestions.get(selected_idx).cloned() {
// Get trigger position and filter length
let trigger_pos = add_logic_state.script_editor_trigger_position;
let filter_len = add_logic_state.script_editor_filter_text.len();
// 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;
// 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(
&mut editor_borrow,
pos,
filter_len,
&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);
if suggestion == "sql" {
replace_autocomplete_text(&mut editor_borrow, pos, filter_len, "sql");
editor_borrow.insert_str("('')");
editor_borrow.move_cursor(CursorMove::Back); // Move cursor between ''
*command_message = "Inserted: @sql('')".to_string();
} else {
*command_message = format!("Inserted: {}", suggestion);
let is_table_selection = add_logic_state.is_table_name_suggestion(&suggestion);
replace_autocomplete_text(&mut editor_borrow, pos, filter_len, &suggestion);
if is_table_selection {
editor_borrow.insert_str(".");
let new_cursor = editor_borrow.cursor();
drop(editor_borrow); // Release borrow before calling add_logic_state methods
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());
// Asynchronous column fetch (remains the same)
let profile_name = add_logic_state.profile_name.clone();
let table_name_for_fetch = suggestion.clone(); // Use a new variable for clarity
let mut client_clone = grpc_client.clone();
tokio::spawn(async move {
match UiService::fetch_columns_for_table(&mut client_clone, &profile_name, &table_name_for_fetch).await {
Ok(_columns) => {
// Result handled by main UI loop checking script_editor_awaiting_column_autocomplete
}
Err(e) => {
tracing::error!("Failed to fetch columns for {}.{}: {}", profile_name, table_name_for_fetch, e);
}
}
});
*command_message = format!("Selected table '{}', fetching columns...", suggestion);
} else {
*command_message = format!("Inserted: {}", suggestion);
}
}
}
return true; // Consume the key
return true;
}
}
// If no suggestion selected, pass Enter to editor
// Fallthrough: No suggestion selected, pass Enter to editor
add_logic_state.deactivate_script_editor_autocomplete();
{
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
@@ -205,20 +181,15 @@ pub fn handle_add_logic_navigation(
}
return true;
}
// ... (Esc and _ cases remain the same) ...
KeyCode::Esc => {
// Cancel autocomplete first
add_logic_state.deactivate_script_editor_autocomplete();
*command_message = "Autocomplete cancelled".to_string();
// Then handle normal Esc behavior (vim mode, exit script, etc.)
// Fall through to normal Esc handling below
// Fall through to normal Esc handling
}
_ => {
// Other keys deactivate autocomplete and pass through
add_logic_state.deactivate_script_editor_autocomplete();
*command_message = "Autocomplete cancelled".to_string();
// Pass key to editor
{
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
TextEditor::handle_input(
@@ -233,22 +204,17 @@ pub fn handle_add_logic_navigation(
}
}
// === AUTOCOMPLETE TRIGGER ===
// ... (AUTOCOMPLETE TRIGGER, Escape to exit fullscreen, ALL OTHER KEYS remain the same) ...
if key_event.code == KeyCode::Char('@') && key_event.modifiers == KeyModifiers::NONE {
// Only trigger in insert mode for Vim, or always for other modes
let should_trigger = match add_logic_state.editor_keybinding_mode {
EditorKeybindingMode::Vim => *is_edit_mode, // Only in Vim insert mode
_ => true, // Always for non-Vim modes when editing
EditorKeybindingMode::Vim => *is_edit_mode,
_ => true,
};
if should_trigger {
// Get cursor position before inserting @
let cursor_before = {
let editor_borrow = add_logic_state.script_content_editor.borrow();
editor_borrow.cursor()
};
// Handle editor input first
{
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
TextEditor::handle_input(
@@ -257,26 +223,21 @@ pub fn handle_add_logic_navigation(
&add_logic_state.editor_keybinding_mode,
&mut add_logic_state.vim_state,
);
} // Drop editor borrow
// Activate autocomplete at the @ position
}
add_logic_state.script_editor_trigger_position = Some(cursor_before);
add_logic_state.script_editor_autocomplete_active = true;
add_logic_state.script_editor_filter_text.clear();
add_logic_state.update_script_editor_suggestions();
add_logic_state.update_script_editor_suggestions(); // This will now use the new logic
add_logic_state.has_unsaved_changes = true;
*command_message = "Autocomplete: @ (Tab/↑↓ to navigate, Enter to select, Esc to cancel)".to_string();
return true;
}
}
// Handle ONLY Escape to exit fullscreen mode
if key_event.code == KeyCode::Esc && key_event.modifiers == KeyModifiers::NONE {
match add_logic_state.editor_keybinding_mode {
EditorKeybindingMode::Vim => {
if *is_edit_mode {
// First escape: try to go to Vim Normal mode
{
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
TextEditor::handle_input(
@@ -291,7 +252,6 @@ pub fn handle_add_logic_navigation(
*command_message = "VIM: Normal Mode. Esc again to exit script.".to_string();
}
} else {
// Second escape: exit fullscreen
add_logic_state.current_focus = AddLogicFocus::ScriptContentPreview;
app_state.ui.focus_outside_canvas = true;
*is_edit_mode = false;
@@ -303,7 +263,6 @@ pub fn handle_add_logic_navigation(
*is_edit_mode = false;
*command_message = "Exited script edit. Esc again to exit script.".to_string();
} else {
// Exit fullscreen
add_logic_state.current_focus = AddLogicFocus::ScriptContentPreview;
app_state.ui.focus_outside_canvas = true;
*is_edit_mode = false;
@@ -314,7 +273,6 @@ pub fn handle_add_logic_navigation(
return true;
}
// ALL OTHER KEYS: Pass directly to textarea without any interference
let changed = {
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
TextEditor::handle_input(
@@ -327,17 +285,14 @@ pub fn handle_add_logic_navigation(
if changed {
add_logic_state.has_unsaved_changes = true;
}
// Update edit mode status for Vim
if add_logic_state.editor_keybinding_mode == EditorKeybindingMode::Vim {
*is_edit_mode = !TextEditor::is_vim_normal_mode(&add_logic_state.vim_state);
}
return true; // Always consume the event in fullscreen mode
return true;
}
// === END FULLSCREEN ISOLATION ===
// Regular navigation logic for non-fullscreen elements
// ... (Regular navigation logic for non-fullscreen elements remains the same) ...
let action = config.get_general_action(key_event.code, key_event.modifiers);
let current_focus = add_logic_state.current_focus;
let mut handled = true;
@@ -345,14 +300,11 @@ pub fn handle_add_logic_navigation(
match action.as_deref() {
Some("exit_table_scroll") => {
// This shouldn't happen since we handle InsideScriptContent above
handled = false;
}
Some("move_up") => {
match current_focus {
AddLogicFocus::InputLogicName => {
// Stay at top
}
AddLogicFocus::InputLogicName => {}
AddLogicFocus::InputTargetColumn => new_focus = AddLogicFocus::InputLogicName,
AddLogicFocus::InputDescription => new_focus = AddLogicFocus::InputTargetColumn,
AddLogicFocus::ScriptContentPreview => new_focus = AddLogicFocus::InputDescription,
@@ -371,9 +323,7 @@ pub fn handle_add_logic_navigation(
},
AddLogicFocus::ScriptContentPreview => new_focus = AddLogicFocus::SaveButton,
AddLogicFocus::SaveButton => new_focus = AddLogicFocus::CancelButton,
AddLogicFocus::CancelButton => {
// Stay at bottom
}
AddLogicFocus::CancelButton => {}
_ => handled = false,
}
}
@@ -383,14 +333,14 @@ pub fn handle_add_logic_navigation(
{ new_focus = AddLogicFocus::ScriptContentPreview; }
AddLogicFocus::ScriptContentPreview => new_focus = AddLogicFocus::SaveButton,
AddLogicFocus::SaveButton => new_focus = AddLogicFocus::CancelButton,
AddLogicFocus::CancelButton => { /* Stay at last */ }
AddLogicFocus::CancelButton => { }
_ => handled = false,
}
}
Some("previous_option") => {
match current_focus {
AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription =>
{ /* Stay at first */ }
{ }
AddLogicFocus::ScriptContentPreview => new_focus = AddLogicFocus::InputDescription,
AddLogicFocus::SaveButton => new_focus = AddLogicFocus::ScriptContentPreview,
AddLogicFocus::CancelButton => new_focus = AddLogicFocus::SaveButton,
@@ -423,8 +373,8 @@ pub fn handle_add_logic_navigation(
match current_focus {
AddLogicFocus::ScriptContentPreview => {
new_focus = AddLogicFocus::InsideScriptContent;
*is_edit_mode = false; // Start in preview mode
app_state.ui.focus_outside_canvas = false; // Script is like canvas
*is_edit_mode = false;
app_state.ui.focus_outside_canvas = false;
let mode_hint = match add_logic_state.editor_keybinding_mode {
EditorKeybindingMode::Vim => "VIM mode - 'i'/'a'/'o' to edit",
_ => "Enter/Ctrl+E to edit",
@@ -463,29 +413,23 @@ pub fn handle_add_logic_navigation(
if handled && current_focus != new_focus {
add_logic_state.current_focus = new_focus;
// Set edit mode and canvas focus based on new focus
let new_is_canvas_input_focus = matches!(new_focus,
AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription
);
if new_is_canvas_input_focus {
// Entering canvas - start in readonly mode
*is_edit_mode = false;
app_state.ui.focus_outside_canvas = false;
} else {
// Outside canvas
app_state.ui.focus_outside_canvas = true;
if matches!(new_focus, AddLogicFocus::ScriptContentPreview) {
*is_edit_mode = false;
}
}
}
handled
}
// Helper function for text replacement
// ... (replace_autocomplete_text helper function remains the same) ...
fn replace_autocomplete_text(
editor: &mut tui_textarea::TextArea,
trigger_pos: (usize, usize),
@@ -493,16 +437,10 @@ fn replace_autocomplete_text(
replacement: &str,
) {
use tui_textarea::CursorMove;
// Move cursor to the position right after the @ symbol (where filter text starts)
let filter_start_pos = (trigger_pos.0, trigger_pos.1 + 1);
editor.move_cursor(CursorMove::Jump(filter_start_pos.0 as u16, filter_start_pos.1 as u16));
// Delete only the filter text (not the @ symbol)
for _ in 0..filter_len {
editor.delete_next_char();
}
// Insert replacement text (this will be appended to the @ symbol)
editor.insert_str(replacement);
}

View File

@@ -135,16 +135,17 @@ impl AddLogicState {
pub fn update_script_editor_suggestions(&mut self) {
let mut suggestions = vec!["sql".to_string()];
// Add actual table name if available (current table)
if let Some(ref table_name) = self.selected_table_name {
suggestions.push(table_name.clone());
if self.selected_table_name.is_some() {
suggestions.extend(self.table_columns_for_suggestions.clone());
}
// Add column names from the current table
suggestions.extend(self.table_columns_for_suggestions.clone());
// Add table names from SAME profile only
suggestions.extend(self.same_profile_table_names.clone());
let current_selected_table_name = self.selected_table_name.as_deref();
suggestions.extend(
self.same_profile_table_names
.iter()
.filter(|tn| Some(tn.as_str()) != current_selected_table_name)
.cloned()
);
if self.script_editor_filter_text.is_empty() {
self.script_editor_suggestions = suggestions;
@@ -169,6 +170,18 @@ impl AddLogicState {
}
}
/// Checks if a suggestion is a table name (for triggering column autocomplete)
pub fn is_table_name_suggestion(&self, suggestion: &str) -> bool {
// Not "sql"
if suggestion == "sql" {
return false;
}
if self.table_columns_for_suggestions.contains(&suggestion.to_string()) {
return false;
}
self.same_profile_table_names.contains(&suggestion.to_string())
}
/// Sets table columns for autocomplete suggestions
pub fn set_table_columns(&mut self, columns: Vec<String>) {
self.table_columns_for_suggestions = columns.clone();
@@ -187,25 +200,6 @@ impl AddLogicState {
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);