diff --git a/client/src/components/admin/add_logic.rs b/client/src/components/admin/add_logic.rs index d8afe9d..9499203 100644 --- a/client/src/components/admin/add_logic.rs +++ b/client/src/components/admin/add_logic.rs @@ -14,15 +14,14 @@ use ratatui::{ use crate::components::handlers::canvas::render_canvas; use crate::components::common::{dialog, autocomplete}; // Added autocomplete use crate::config::binds::config::EditorKeybindingMode; -use crate::modes::handlers::mode_manager::AppMode; // For checking AppMode::Edit pub fn render_add_logic( f: &mut Frame, area: Rect, theme: &Theme, app_state: &AppState, - add_logic_state: &mut AddLogicState, // Changed to &mut - is_edit_mode: bool, // This is the general edit mode from EventHandler + add_logic_state: &mut AddLogicState, + is_edit_mode: bool, highlight_state: &HighlightState, ) { let main_block = Block::default() @@ -47,13 +46,10 @@ pub fn render_add_logic( let script_title_hint = match add_logic_state.editor_keybinding_mode { EditorKeybindingMode::Vim => { let vim_mode_status = crate::components::common::text_editor::TextEditor::get_vim_mode_status(&add_logic_state.vim_state); - // Vim mode status is relevant regardless of the general `is_edit_mode` format!("Script {}", vim_mode_status) } EditorKeybindingMode::Emacs | EditorKeybindingMode::Default => { - // For default/emacs, the general `is_edit_mode` (passed to this function) - // indicates if the text area itself is in an "editing" state. - if is_edit_mode { // This `is_edit_mode` refers to the text area's active editing. + if is_edit_mode { "Script (Editing)".to_string() } else { "Script".to_string() @@ -70,7 +66,46 @@ pub fn render_add_logic( .border_style(border_style), ); f.render_widget(&*editor_ref, inner_area); - return; + + // Drop the editor borrow before accessing autocomplete state + drop(editor_ref); + + // === SCRIPT EDITOR AUTOCOMPLETE RENDERING === + if add_logic_state.script_editor_autocomplete_active && !add_logic_state.script_editor_suggestions.is_empty() { + if let Some((trigger_line, trigger_col)) = add_logic_state.script_editor_trigger_position { + // For now, we'll position the autocomplete at a simple offset from the trigger + // Since we can't easily get viewport info, we'll position it relatively + // This is a simplified approach - in a real implementation you'd want proper viewport tracking + + // Account for TextArea's block borders (1 for each side) + let block_offset_x = 1; + let block_offset_y = 1; + + // Simple positioning: assume trigger is visible and use direct coordinates + // This works for small scripts but may need improvement for larger ones + let visible_line = trigger_line; + let visible_col = trigger_col + 1 + add_logic_state.script_editor_filter_text.len(); + + let input_rect = Rect { + x: (inner_area.x + block_offset_x + visible_col as u16).min(inner_area.right().saturating_sub(20)), + y: (inner_area.y + block_offset_y + visible_line as u16 + 1).min(inner_area.bottom().saturating_sub(5)), + width: 1, // Minimum width for positioning + height: 1, + }; + + // Render autocomplete dropdown + autocomplete::render_autocomplete_dropdown( + f, + input_rect, + f.area(), // Full frame area for clamping + theme, + &add_logic_state.script_editor_suggestions, + add_logic_state.script_editor_selected_suggestion_index, + ); + } + } + + return; // Exit early for fullscreen mode } // Regular layout with preview diff --git a/client/src/functions/modes/navigation/add_logic_nav.rs b/client/src/functions/modes/navigation/add_logic_nav.rs index 8406f3f..aff4476 100644 --- a/client/src/functions/modes/navigation/add_logic_nav.rs +++ b/client/src/functions/modes/navigation/add_logic_nav.rs @@ -21,13 +21,217 @@ 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 === if add_logic_state.current_focus == AddLogicFocus::InsideScriptContent { - let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut(); + + // === AUTOCOMPLETE HANDLING === + if add_logic_state.script_editor_autocomplete_active { + match key_event.code { + 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( + &mut editor_borrow, + key_event, + &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( + &mut editor_borrow, + key_event, + &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(); + editor_borrow.cursor() + }; + current_cursor.0 == trigger_line && current_cursor.1 == trigger_col + 1 + } 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( + &mut editor_borrow, + key_event, + &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 + } + 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 { + add_logic_state.script_editor_suggestions.len() - 1 + } else { + current - 1 + }; + 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 + } + 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(); + + // Deactivate autocomplete first + add_logic_state.deactivate_script_editor_autocomplete(); + add_logic_state.has_unsaved_changes = true; + + // Then replace text + 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, + ); + } + + *command_message = format!("Inserted: {}", suggestion); + return true; // Consume the key + } + } + + // If 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(); + TextEditor::handle_input( + &mut editor_borrow, + key_event, + &add_logic_state.editor_keybinding_mode, + &mut add_logic_state.vim_state, + ); + } + return true; + } + 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 + } + _ => { + // 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( + &mut editor_borrow, + key_event, + &add_logic_state.editor_keybinding_mode, + &mut add_logic_state.vim_state, + ); + } + return true; + } + } + } + + // === AUTOCOMPLETE TRIGGER === + 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 + }; + + 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( + &mut editor_borrow, + key_event, + &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.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 { @@ -35,12 +239,15 @@ pub fn handle_add_logic_navigation( EditorKeybindingMode::Vim => { if *is_edit_mode { // First escape: try to go to Vim Normal mode - TextEditor::handle_input( - &mut editor_borrow, - key_event, - &add_logic_state.editor_keybinding_mode, - &mut add_logic_state.vim_state, - ); + { + let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut(); + TextEditor::handle_input( + &mut editor_borrow, + key_event, + &add_logic_state.editor_keybinding_mode, + &mut add_logic_state.vim_state, + ); + } if TextEditor::is_vim_normal_mode(&add_logic_state.vim_state) { *is_edit_mode = false; *command_message = "VIM: Normal Mode. Esc again to exit script.".to_string(); @@ -70,14 +277,17 @@ pub fn handle_add_logic_navigation( } // ALL OTHER KEYS: Pass directly to textarea without any interference - let changed = TextEditor::handle_input( - &mut editor_borrow, - key_event, - &add_logic_state.editor_keybinding_mode, - &mut add_logic_state.vim_state, - ); - if changed { - add_logic_state.has_unsaved_changes = true; + let changed = { + let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut(); + TextEditor::handle_input( + &mut editor_borrow, + key_event, + &add_logic_state.editor_keybinding_mode, + &mut add_logic_state.vim_state, + ) + }; + if changed { + add_logic_state.has_unsaved_changes = true; } // Update edit mode status for Vim @@ -236,3 +446,25 @@ pub fn handle_add_logic_navigation( handled } + +// Helper function for text replacement +fn replace_autocomplete_text( + editor: &mut tui_textarea::TextArea, + trigger_pos: (usize, usize), + filter_len: usize, + 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); +} diff --git a/client/src/state/pages/add_logic.rs b/client/src/state/pages/add_logic.rs index d167c8a..8bd4539 100644 --- a/client/src/state/pages/add_logic.rs +++ b/client/src/state/pages/add_logic.rs @@ -42,6 +42,13 @@ pub struct AddLogicState { pub show_target_column_suggestions: bool, pub selected_target_column_suggestion_index: Option, pub in_target_column_suggestion_mode: bool, + + // Script Editor Autocomplete + pub script_editor_autocomplete_active: bool, + pub script_editor_suggestions: Vec, + pub script_editor_selected_suggestion_index: Option, + pub script_editor_trigger_position: Option<(usize, usize)>, // (line, column) + pub script_editor_filter_text: String, } impl AddLogicState { @@ -69,6 +76,13 @@ impl AddLogicState { show_target_column_suggestions: false, 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, + script_editor_filter_text: String::new(), } } @@ -114,6 +128,46 @@ impl AddLogicState { self.selected_target_column_suggestion_index = None; } } + + /// Updates script editor suggestions based on current filter text + pub fn update_script_editor_suggestions(&mut self) { + let hardcoded_suggestions = vec![ + "sql".to_string(), + "tablename".to_string(), + "table column".to_string() + ]; + + if self.script_editor_filter_text.is_empty() { + self.script_editor_suggestions = hardcoded_suggestions; + } else { + let filter_lower = self.script_editor_filter_text.to_lowercase(); + self.script_editor_suggestions = hardcoded_suggestions + .into_iter() + .filter(|suggestion| suggestion.to_lowercase().contains(&filter_lower)) + .collect(); + } + + // Update selection index + if self.script_editor_suggestions.is_empty() { + self.script_editor_selected_suggestion_index = None; + self.script_editor_autocomplete_active = false; + } else if let Some(selected_idx) = self.script_editor_selected_suggestion_index { + if selected_idx >= self.script_editor_suggestions.len() { + self.script_editor_selected_suggestion_index = Some(0); + } + } else { + self.script_editor_selected_suggestion_index = Some(0); + } + } + + /// Deactivates script editor autocomplete and clears related state + pub fn deactivate_script_editor_autocomplete(&mut self) { + self.script_editor_autocomplete_active = false; + self.script_editor_suggestions.clear(); + self.script_editor_selected_suggestion_index = None; + self.script_editor_trigger_position = None; + self.script_editor_filter_text.clear(); + } } impl Default for AddLogicState {