// src/functions/modes/navigation/add_logic_nav.rs use crate::config::binds::config::{Config, EditorKeybindingMode}; use crate::state::{ app::state::AppState, pages::add_logic::{AddLogicFocus, AddLogicState}, }; use crate::buffer::{AppView, BufferState}; use crossterm::event::{KeyEvent, KeyCode, KeyModifiers}; use crate::services::GrpcClient; use tokio::sync::mpsc; use anyhow::Result; use crate::components::common::text_editor::TextEditor; use crate::services::ui_service::UiService; use tui_textarea::CursorMove; use crate::pages::admin::AdminState; use crate::pages::routing::{Router, Page}; pub type SaveLogicResultSender = mpsc::Sender>; pub fn handle_add_logic_navigation( key_event: KeyEvent, config: &Config, app_state: &mut AppState, buffer_state: &mut BufferState, grpc_client: GrpcClient, save_logic_sender: SaveLogicResultSender, command_message: &mut String, router: &mut Router, ) -> bool { if let Page::AddLogic(add_logic_state) = &mut router.current { // === FULLSCREEN SCRIPT EDITING === if add_logic_state.current_focus == AddLogicFocus::InsideScriptContent { // === AUTOCOMPLETE HANDLING === if add_logic_state.script_editor_autocomplete_active { match key_event.code { KeyCode::Char(c) if c.is_alphanumeric() || c == '_' => { add_logic_state.script_editor_filter_text.push(c); add_logic_state.update_script_editor_suggestions(); { 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, ); } *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() { add_logic_state.script_editor_filter_text.pop(); add_logic_state.update_script_editor_suggestions(); { 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, ); } *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 { 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(); } { 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::Tab | KeyCode::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; } KeyCode::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; } KeyCode::Enter => { 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() { let trigger_pos = add_logic_state.script_editor_trigger_position; let filter_len = add_logic_state.script_editor_filter_text.len(); add_logic_state.deactivate_script_editor_autocomplete(); add_logic_state.has_unsaved_changes = true; if let Some(pos) = trigger_pos { let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut(); if suggestion == "sql" { replace_autocomplete_text( &mut editor_borrow, pos, filter_len, "sql", ); editor_borrow.insert_str("('')"); editor_borrow.move_cursor(CursorMove::Back); editor_borrow.move_cursor(CursorMove::Back); *command_message = "Inserted: @sql('')".to_string(); } else { 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); 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(), ); let profile_name = add_logic_state.profile_name.clone(); let table_name_for_fetch = suggestion.clone(); let mut client_clone = grpc_client.clone(); tokio::spawn(async move { if let Err(e) = UiService::fetch_columns_for_table( &mut client_clone, &profile_name, &table_name_for_fetch, ) .await { 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; } } 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 => { add_logic_state.deactivate_script_editor_autocomplete(); *command_message = "Autocomplete cancelled".to_string(); } _ => { add_logic_state.deactivate_script_editor_autocomplete(); *command_message = "Autocomplete cancelled".to_string(); { 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; } } } // Trigger autocomplete with '@' if key_event.code == KeyCode::Char('@') && key_event.modifiers == KeyModifiers::NONE { let should_trigger = match add_logic_state.editor_keybinding_mode { EditorKeybindingMode::Vim => { TextEditor::is_vim_insert_mode(&add_logic_state.vim_state) } _ => true, }; if should_trigger { let cursor_before = { let editor_borrow = add_logic_state.script_content_editor.borrow(); editor_borrow.cursor() }; { 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, ); } 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; } } // Esc handling if key_event.code == KeyCode::Esc && key_event.modifiers == KeyModifiers::NONE { match add_logic_state.editor_keybinding_mode { EditorKeybindingMode::Vim => { let was_insert = TextEditor::is_vim_insert_mode(&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 was_insert { *command_message = "VIM: Normal Mode. Esc again to exit script.".to_string(); } else { add_logic_state.current_focus = AddLogicFocus::ScriptContentPreview; app_state.ui.focus_outside_canvas = true; *command_message = "Exited script editing.".to_string(); } } _ => { add_logic_state.current_focus = AddLogicFocus::ScriptContentPreview; app_state.ui.focus_outside_canvas = true; *command_message = "Exited script editing.".to_string(); } } return true; } // Normal text input 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; } return true; } // === NON-FULLSCREEN NAVIGATION === let action = config.get_general_action(key_event.code, key_event.modifiers); let current_focus = add_logic_state.current_focus; let mut handled = true; let mut new_focus = current_focus; match action.as_deref() { Some("exit_table_scroll") => { handled = false; } Some("move_up") => { match current_focus { AddLogicFocus::InputLogicName => {} AddLogicFocus::InputTargetColumn => new_focus = AddLogicFocus::InputLogicName, AddLogicFocus::InputDescription => { new_focus = AddLogicFocus::InputTargetColumn } AddLogicFocus::ScriptContentPreview => { new_focus = AddLogicFocus::InputDescription } AddLogicFocus::SaveButton => new_focus = AddLogicFocus::ScriptContentPreview, AddLogicFocus::CancelButton => new_focus = AddLogicFocus::SaveButton, _ => handled = false, } } Some("move_down") => { match current_focus { AddLogicFocus::InputLogicName => { new_focus = AddLogicFocus::InputTargetColumn } AddLogicFocus::InputTargetColumn => { new_focus = AddLogicFocus::InputDescription } AddLogicFocus::InputDescription => { add_logic_state.last_canvas_field = 2; new_focus = AddLogicFocus::ScriptContentPreview; } AddLogicFocus::ScriptContentPreview => { new_focus = AddLogicFocus::SaveButton } AddLogicFocus::SaveButton => new_focus = AddLogicFocus::CancelButton, AddLogicFocus::CancelButton => {} _ => handled = false, } } Some("next_option") => { match current_focus { AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription => { new_focus = AddLogicFocus::ScriptContentPreview } AddLogicFocus::ScriptContentPreview => { new_focus = AddLogicFocus::SaveButton } AddLogicFocus::SaveButton => new_focus = AddLogicFocus::CancelButton, AddLogicFocus::CancelButton => {} _ => handled = false, } } Some("previous_option") => { match current_focus { AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription => {} AddLogicFocus::ScriptContentPreview => { new_focus = AddLogicFocus::InputDescription } AddLogicFocus::SaveButton => { new_focus = AddLogicFocus::ScriptContentPreview } AddLogicFocus::CancelButton => new_focus = AddLogicFocus::SaveButton, _ => handled = false, } } Some("next_field") => { new_focus = match current_focus { AddLogicFocus::InputLogicName => AddLogicFocus::InputTargetColumn, AddLogicFocus::InputTargetColumn => AddLogicFocus::InputDescription, AddLogicFocus::InputDescription => AddLogicFocus::ScriptContentPreview, AddLogicFocus::ScriptContentPreview => AddLogicFocus::SaveButton, AddLogicFocus::SaveButton => AddLogicFocus::CancelButton, AddLogicFocus::CancelButton => AddLogicFocus::InputLogicName, _ => current_focus, }; } Some("prev_field") => { new_focus = match current_focus { AddLogicFocus::InputLogicName => AddLogicFocus::CancelButton, AddLogicFocus::InputTargetColumn => AddLogicFocus::InputLogicName, AddLogicFocus::InputDescription => AddLogicFocus::InputTargetColumn, AddLogicFocus::ScriptContentPreview => AddLogicFocus::InputDescription, AddLogicFocus::SaveButton => AddLogicFocus::ScriptContentPreview, AddLogicFocus::CancelButton => AddLogicFocus::SaveButton, _ => current_focus, }; } Some("select") => { match current_focus { AddLogicFocus::ScriptContentPreview => { new_focus = AddLogicFocus::InsideScriptContent; 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", }; *command_message = format!( "Fullscreen script editing. {} or Esc to exit.", mode_hint ); handled = true; } AddLogicFocus::SaveButton => { *command_message = "Save logic action".to_string(); handled = true; } AddLogicFocus::CancelButton => { buffer_state.update_history(AppView::Admin); *command_message = "Cancelled Add Logic".to_string(); handled = true; } AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription => { // Focus canvas inputs; let canvas keymap handle editing app_state.ui.focus_outside_canvas = false; handled = false; // forward to canvas } _ => handled = false, } } Some("toggle_edit_mode") => { match current_focus { AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription => { app_state.ui.focus_outside_canvas = false; *command_message = "Focus moved to input. Use i/a (Vim) or type to edit.".to_string(); handled = true; } _ => { *command_message = "Cannot toggle edit mode here.".to_string(); } } } _ => handled = false, } if handled && current_focus != new_focus { add_logic_state.current_focus = new_focus; let new_is_canvas_input_focus = matches!( new_focus, AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription ); if new_is_canvas_input_focus { app_state.ui.focus_outside_canvas = false; } else { app_state.ui.focus_outside_canvas = true; } } handled } else { false } } fn replace_autocomplete_text( editor: &mut tui_textarea::TextArea, trigger_pos: (usize, usize), filter_len: usize, replacement: &str, ) { 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)); for _ in 0..filter_len { editor.delete_next_char(); } editor.insert_str(replacement); }