From 4e7213d1aa37ed1087c97bf3e26d645508851e25 Mon Sep 17 00:00:00 2001 From: filipriec Date: Sun, 25 May 2025 19:26:30 +0200 Subject: [PATCH] automcomplete running and working now --- client/src/components/admin/add_logic.rs | 66 ++-- .../src/functions/modes/edit/add_logic_e.rs | 346 +++++------------ client/src/modes/canvas/edit.rs | 354 ++++++++---------- client/src/state/pages/add_logic.rs | 104 ++++- 4 files changed, 375 insertions(+), 495 deletions(-) diff --git a/client/src/components/admin/add_logic.rs b/client/src/components/admin/add_logic.rs index 1d1773a..d8afe9d 100644 --- a/client/src/components/admin/add_logic.rs +++ b/client/src/components/admin/add_logic.rs @@ -12,16 +12,17 @@ use ratatui::{ Frame, }; use crate::components::handlers::canvas::render_canvas; -use crate::components::common::dialog; +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, - is_edit_mode: bool, // Used for border/title hints in InsideScriptContent + add_logic_state: &mut AddLogicState, // Changed to &mut + is_edit_mode: bool, // This is the general edit mode from EventHandler highlight_state: &HighlightState, ) { let main_block = Block::default() @@ -41,20 +42,18 @@ pub fn render_add_logic( let border_style = Style::default().fg(border_style_color); editor_ref.set_cursor_line_style(Style::default()); - // Explicitly set to tui-textarea's default "active" editing cursor style editor_ref.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED)); 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); - if is_edit_mode { // is_edit_mode here refers to Vim's Insert mode - format!("Script {}", vim_mode_status) - } else { - format!("Script {}", vim_mode_status) - } + // Vim mode status is relevant regardless of the general `is_edit_mode` + format!("Script {}", vim_mode_status) } EditorKeybindingMode::Emacs | EditorKeybindingMode::Default => { - if is_edit_mode { + // 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. "Script (Editing)".to_string() } else { "Script".to_string() @@ -79,7 +78,7 @@ pub fn render_add_logic( .direction(Direction::Vertical) .constraints([ Constraint::Length(3), // Top info - Constraint::Length(9), // Canvas + Constraint::Length(9), // Canvas for 3 inputs (each 1 line + 1 padding = 2 lines * 3 + 2 border = 8, +1 for good measure) Constraint::Min(5), // Script preview Constraint::Length(3), // Buttons ]) @@ -123,10 +122,11 @@ pub fn render_add_logic( | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription ); - render_canvas( + // Call render_canvas and get the active_field_rect + let active_field_rect = render_canvas( f, canvas_area, - add_logic_state, + add_logic_state, // Pass the whole state as it impl CanvasState &add_logic_state.fields(), &add_logic_state.current_field(), &add_logic_state.inputs(), @@ -135,6 +135,26 @@ pub fn render_add_logic( highlight_state, ); + // --- Render Autocomplete for Target Column --- + // `is_edit_mode` here refers to the general edit mode of the EventHandler + if is_edit_mode && add_logic_state.current_field() == 1 { // Target Column field + if let Some(suggestions) = add_logic_state.get_suggestions() { // Uses CanvasState impl + let selected = add_logic_state.get_selected_suggestion_index(); + if !suggestions.is_empty() { // Only render if there are suggestions to show + if let Some(input_rect) = active_field_rect { + autocomplete::render_autocomplete_dropdown( + f, + input_rect, + f.area(), // Full frame area for clamping + theme, + suggestions, + selected, + ); + } + } + } + } + // Script content preview { let mut editor_ref = add_logic_state.script_content_editor.borrow_mut(); @@ -143,10 +163,8 @@ pub fn render_add_logic( let is_script_preview_focused = add_logic_state.current_focus == AddLogicFocus::ScriptContentPreview; if is_script_preview_focused { - // When script PREVIEW is focused, use tui-textarea's default "active" cursor (block-like). editor_ref.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED)); } else { - // When script PREVIEW is NOT focused, use an underscore cursor. let underscore_cursor_style = Style::default() .add_modifier(Modifier::UNDERLINED) .fg(theme.secondary); @@ -154,16 +172,12 @@ pub fn render_add_logic( } let border_style_color = if is_script_preview_focused { - theme.highlight // Green highlight when focused and ready to select + theme.highlight } else { theme.secondary }; - let title_text = if is_script_preview_focused { - "Script Preview" - } else { - "Script Preview" - }; + let title_text = "Script Preview"; // Title doesn't need to change based on focus here let title_style = if is_script_preview_focused { Style::default().fg(theme.highlight).add_modifier(Modifier::BOLD) @@ -182,8 +196,8 @@ pub fn render_add_logic( } // Buttons - let get_button_style = |button_focus: AddLogicFocus, current_focus| { - let is_focused = current_focus == button_focus; + let get_button_style = |button_focus: AddLogicFocus, current_focus_state: AddLogicFocus| { + let is_focused = current_focus_state == button_focus; let base_style = Style::default().fg(if is_focused { theme.highlight } else { @@ -196,11 +210,11 @@ pub fn render_add_logic( } }; - let get_button_border_style = |is_focused: bool, theme: &Theme| { + let get_button_border_style = |is_focused: bool, current_theme: &Theme| { if is_focused { - Style::default().fg(theme.highlight) + Style::default().fg(current_theme.highlight) } else { - Style::default().fg(theme.secondary) + Style::default().fg(current_theme.secondary) } }; diff --git a/client/src/functions/modes/edit/add_logic_e.rs b/client/src/functions/modes/edit/add_logic_e.rs index f16a6ba..90ffd9d 100644 --- a/client/src/functions/modes/edit/add_logic_e.rs +++ b/client/src/functions/modes/edit/add_logic_e.rs @@ -1,277 +1,135 @@ // src/functions/modes/edit/add_logic_e.rs -use crate::state::pages::add_logic::AddLogicState; // Changed +use crate::state::pages::add_logic::AddLogicState; use crate::state::pages::canvas_state::CanvasState; -use crossterm::event::{KeyCode, KeyEvent}; use anyhow::Result; +use crossterm::event::{KeyCode, KeyEvent}; -// Word navigation helpers (get_char_type, find_next_word_start, etc.) -// can be kept as they are generic. -#[derive(PartialEq)] -enum CharType { - Whitespace, - Alphanumeric, - Punctuation, -} - -fn get_char_type(c: char) -> CharType { - if c.is_whitespace() { CharType::Whitespace } - else if c.is_alphanumeric() { CharType::Alphanumeric } - else { CharType::Punctuation } -} - -fn find_next_word_start(text: &str, current_pos: usize) -> usize { - let chars: Vec = text.chars().collect(); - let len = chars.len(); - if len == 0 || current_pos >= len { return len; } - let mut pos = current_pos; - let initial_type = get_char_type(chars[pos]); - while pos < len && get_char_type(chars[pos]) == initial_type { pos += 1; } - while pos < len && get_char_type(chars[pos]) == CharType::Whitespace { pos += 1; } - pos -} - -fn find_word_end(text: &str, current_pos: usize) -> usize { - let chars: Vec = text.chars().collect(); - let len = chars.len(); - if len == 0 { return 0; } - let mut pos = current_pos.min(len - 1); - if get_char_type(chars[pos]) == CharType::Whitespace { - pos = find_next_word_start(text, pos); - } - if pos >= len { return len.saturating_sub(1); } - let word_type = get_char_type(chars[pos]); - while pos < len && get_char_type(chars[pos]) == word_type { pos += 1; } - pos.saturating_sub(1).min(len.saturating_sub(1)) -} - -fn find_prev_word_start(text: &str, current_pos: usize) -> usize { - let chars: Vec = text.chars().collect(); - if chars.is_empty() || current_pos == 0 { return 0; } - let mut pos = current_pos.saturating_sub(1); - while pos > 0 && get_char_type(chars[pos]) == CharType::Whitespace { pos -= 1; } - if pos == 0 && get_char_type(chars[pos]) == CharType::Whitespace { return 0; } - let word_type = get_char_type(chars[pos]); - while pos > 0 && get_char_type(chars[pos - 1]) == word_type { pos -= 1; } - pos -} - -fn find_prev_word_end(text: &str, current_pos: usize) -> usize { - let chars: Vec = text.chars().collect(); - let len = chars.len(); - if len == 0 || current_pos == 0 { return 0; } - let mut pos = current_pos.saturating_sub(1); - while pos > 0 && get_char_type(chars[pos]) == CharType::Whitespace { pos -= 1; } - if pos == 0 && get_char_type(chars[pos]) == CharType::Whitespace { return 0; } - if pos == 0 && get_char_type(chars[pos]) != CharType::Whitespace { return 0; } - let word_type = get_char_type(chars[pos]); - while pos > 0 && get_char_type(chars[pos - 1]) == word_type { pos -= 1; } - while pos > 0 && get_char_type(chars[pos - 1]) == CharType::Whitespace { pos -= 1; } - if pos > 0 { pos - 1 } else { 0 } -} - -/// Executes edit actions for the AddLogic view canvas. pub async fn execute_edit_action( action: &str, - key: KeyEvent, - state: &mut AddLogicState, // Changed + key: KeyEvent, // Keep key for insert_char + state: &mut AddLogicState, ideal_cursor_column: &mut usize, ) -> Result { + let mut message = String::new(); + match action { - "insert_char" => { - if let KeyCode::Char(c) = key.code { - let cursor_pos = state.current_cursor_pos(); - let field_value = state.get_current_input_mut(); - let mut chars: Vec = field_value.chars().collect(); - if cursor_pos <= chars.len() { - chars.insert(cursor_pos, c); - *field_value = chars.into_iter().collect(); - state.set_current_cursor_pos(cursor_pos + 1); - state.set_has_unsaved_changes(true); - *ideal_cursor_column = state.current_cursor_pos(); - } - } else { - return Ok("Error: insert_char called without a char key.".to_string()); - } - Ok("".to_string()) - } - "delete_char_backward" => { - if state.current_cursor_pos() > 0 { - let cursor_pos = state.current_cursor_pos(); - let field_value = state.get_current_input_mut(); - let mut chars: Vec = field_value.chars().collect(); - if cursor_pos <= chars.len() { - chars.remove(cursor_pos - 1); - *field_value = chars.into_iter().collect(); - let new_pos = cursor_pos - 1; - state.set_current_cursor_pos(new_pos); - state.set_has_unsaved_changes(true); - *ideal_cursor_column = new_pos; - } - } - Ok("".to_string()) - } - "delete_char_forward" => { - let cursor_pos = state.current_cursor_pos(); - let field_value = state.get_current_input_mut(); - let mut chars: Vec = field_value.chars().collect(); - if cursor_pos < chars.len() { - chars.remove(cursor_pos); - *field_value = chars.into_iter().collect(); - state.set_has_unsaved_changes(true); - *ideal_cursor_column = cursor_pos; - } - Ok("".to_string()) - } "next_field" => { - let num_fields = AddLogicState::INPUT_FIELD_COUNT; // Changed - if num_fields > 0 { - let current_field = state.current_field(); - let last_field_index = num_fields - 1; - if current_field < last_field_index { // Prevent cycling - state.set_current_field(current_field + 1); - } - let current_input = state.get_current_input(); - let max_pos = current_input.len(); - state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos)); - } - Ok("".to_string()) + let current_field = state.current_field(); + let next_field = (current_field + 1) % AddLogicState::INPUT_FIELD_COUNT; + state.set_current_field(next_field); + *ideal_cursor_column = state.current_cursor_pos(); + message = format!("Focus on field {}", state.fields()[next_field]); } "prev_field" => { - let num_fields = AddLogicState::INPUT_FIELD_COUNT; // Changed - if num_fields > 0 { - let current_field = state.current_field(); - if current_field > 0 { // Prevent cycling - state.set_current_field(current_field - 1); - } - let current_input = state.get_current_input(); - let max_pos = current_input.len(); - state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos)); + let current_field = state.current_field(); + let prev_field = if current_field == 0 { + AddLogicState::INPUT_FIELD_COUNT - 1 + } else { + current_field - 1 + }; + state.set_current_field(prev_field); + *ideal_cursor_column = state.current_cursor_pos(); + message = format!("Focus on field {}", state.fields()[prev_field]); + } + "delete_char_forward" => { + let current_pos = state.current_cursor_pos(); + let current_input_mut = state.get_current_input_mut(); + if current_pos < current_input_mut.len() { + current_input_mut.remove(current_pos); + state.set_has_unsaved_changes(true); + if state.current_field() == 1 { state.update_target_column_suggestions(); } + } + } + "delete_char_backward" => { + let current_pos = state.current_cursor_pos(); + if current_pos > 0 { + let new_pos = current_pos - 1; + state.get_current_input_mut().remove(new_pos); + state.set_current_cursor_pos(new_pos); + *ideal_cursor_column = new_pos; + state.set_has_unsaved_changes(true); + if state.current_field() == 1 { state.update_target_column_suggestions(); } } - Ok("".to_string()) } "move_left" => { - let new_pos = state.current_cursor_pos().saturating_sub(1); - state.set_current_cursor_pos(new_pos); - *ideal_cursor_column = new_pos; - Ok("".to_string()) + let current_pos = state.current_cursor_pos(); + if current_pos > 0 { + let new_pos = current_pos - 1; + state.set_current_cursor_pos(new_pos); + *ideal_cursor_column = new_pos; + } } "move_right" => { - let current_input = state.get_current_input(); let current_pos = state.current_cursor_pos(); - if current_pos < current_input.len() { + let input_len = state.get_current_input().len(); + if current_pos < input_len { let new_pos = current_pos + 1; state.set_current_cursor_pos(new_pos); *ideal_cursor_column = new_pos; } - Ok("".to_string()) } - "move_up" => { // In edit mode, up/down usually means prev/next field - let current_field = state.current_field(); - if current_field > 0 { - let new_field = current_field - 1; - state.set_current_field(new_field); - let current_input = state.get_current_input(); - let max_pos = current_input.len(); - state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos)); - } - Ok("".to_string()) - } - "move_down" => { // In edit mode, up/down usually means prev/next field - let num_fields = AddLogicState::INPUT_FIELD_COUNT; // Changed - if num_fields > 0 { - let current_field = state.current_field(); - let last_field_index = num_fields - 1; - if current_field < last_field_index { - let new_field = current_field + 1; - state.set_current_field(new_field); - let current_input = state.get_current_input(); - let max_pos = current_input.len(); - state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos)); + "insert_char" => { + if let KeyCode::Char(c) = key.code { + let current_pos = state.current_cursor_pos(); + state.get_current_input_mut().insert(current_pos, c); + let new_pos = current_pos + 1; + state.set_current_cursor_pos(new_pos); + *ideal_cursor_column = new_pos; + state.set_has_unsaved_changes(true); + if state.current_field() == 1 { + state.update_target_column_suggestions(); } } - Ok("".to_string()) } - "move_line_start" => { - state.set_current_cursor_pos(0); - *ideal_cursor_column = 0; - Ok("".to_string()) - } - "move_line_end" => { - let current_input = state.get_current_input(); - let new_pos = current_input.len(); - state.set_current_cursor_pos(new_pos); - *ideal_cursor_column = new_pos; - Ok("".to_string()) - } - "move_first_line" => { - if AddLogicState::INPUT_FIELD_COUNT > 0 { // Changed - state.set_current_field(0); - let current_input = state.get_current_input(); - let max_pos = current_input.len(); - state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos)); + "suggestion_down" => { + if state.in_target_column_suggestion_mode && !state.target_column_suggestions.is_empty() { + let current_selection = state.selected_target_column_suggestion_index.unwrap_or(0); + let next_selection = (current_selection + 1) % state.target_column_suggestions.len(); + state.selected_target_column_suggestion_index = Some(next_selection); } - Ok("".to_string()) } - "move_last_line" => { - let num_fields = AddLogicState::INPUT_FIELD_COUNT; // Changed - if num_fields > 0 { - let new_field = num_fields - 1; - state.set_current_field(new_field); - let current_input = state.get_current_input(); - let max_pos = current_input.len(); - state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos)); - } - Ok("".to_string()) - } - "move_word_next" => { - let current_input = state.get_current_input(); - if !current_input.is_empty() { - let new_pos = find_next_word_start(current_input, state.current_cursor_pos()); - let final_pos = new_pos.min(current_input.len()); - state.set_current_cursor_pos(final_pos); - *ideal_cursor_column = final_pos; - } - Ok("".to_string()) - } - "move_word_end" => { - let current_input = state.get_current_input(); - if !current_input.is_empty() { - let current_pos = state.current_cursor_pos(); - let new_pos = find_word_end(current_input, current_pos); - let final_pos = if new_pos == current_pos && current_pos < current_input.len() { // Ensure not to go past end - find_word_end(current_input, current_pos + 1) + "suggestion_up" => { + if state.in_target_column_suggestion_mode && !state.target_column_suggestions.is_empty() { + let current_selection = state.selected_target_column_suggestion_index.unwrap_or(0); + let prev_selection = if current_selection == 0 { + state.target_column_suggestions.len() - 1 } else { - new_pos + current_selection - 1 }; - let max_valid_index = current_input.len(); // Allow cursor at end - let clamped_pos = final_pos.min(max_valid_index); - state.set_current_cursor_pos(clamped_pos); - *ideal_cursor_column = clamped_pos; + state.selected_target_column_suggestion_index = Some(prev_selection); } - Ok("".to_string()) } - "move_word_prev" => { - let current_input = state.get_current_input(); - if !current_input.is_empty() { - let new_pos = find_prev_word_start(current_input, state.current_cursor_pos()); - state.set_current_cursor_pos(new_pos); - *ideal_cursor_column = new_pos; - } - Ok("".to_string()) - } - "move_word_end_prev" => { - let current_input = state.get_current_input(); - if !current_input.is_empty() { - let new_pos = find_prev_word_end(current_input, state.current_cursor_pos()); - state.set_current_cursor_pos(new_pos); - *ideal_cursor_column = new_pos; - } - Ok("".to_string()) - } - "exit_edit_mode" | "save" | "revert" => { - Ok("Action handled by main loop".to_string()) - } - _ => Ok(format!("Unknown or unhandled edit action: {}", action)), - } -} + "select_suggestion" => { + if state.in_target_column_suggestion_mode { + let mut selected_suggestion_text: Option = None; + if let Some(selected_idx) = state.selected_target_column_suggestion_index { + if let Some(suggestion) = state.target_column_suggestions.get(selected_idx) { + selected_suggestion_text = Some(suggestion.clone()); + } + } + + if let Some(suggestion_text) = selected_suggestion_text { + state.target_column_input = suggestion_text.clone(); + state.target_column_cursor_pos = state.target_column_input.len(); + *ideal_cursor_column = state.target_column_cursor_pos; + state.set_has_unsaved_changes(true); + message = format!("Selected column: '{}'", suggestion_text); + } + + state.in_target_column_suggestion_mode = false; + state.show_target_column_suggestions = false; + state.selected_target_column_suggestion_index = None; + state.update_target_column_suggestions(); + } else { + let current_field = state.current_field(); + let next_field = (current_field + 1) % AddLogicState::INPUT_FIELD_COUNT; + state.set_current_field(next_field); + *ideal_cursor_column = state.current_cursor_pos(); + message = format!("Focus on field {}", state.fields()[next_field]); + } + } + _ => {} + } + Ok(message) +} diff --git a/client/src/modes/canvas/edit.rs b/client/src/modes/canvas/edit.rs index 6959dee..2774d4b 100644 --- a/client/src/modes/canvas/edit.rs +++ b/client/src/modes/canvas/edit.rs @@ -5,26 +5,27 @@ use crate::state::pages::{ auth::{LoginState, RegisterState}, canvas_state::CanvasState, }; -use crate::state::pages::add_logic::AddLogicState; -use crate::state::pages::form::FormState; -use crate::state::pages::add_table::AddTableState; +use crate::state::pages::form::FormState; // <<< ADD THIS LINE +// AddLogicState is already imported +// AddTableState is already imported use crate::state::pages::admin::AdminState; use crate::modes::handlers::event::EventOutcome; use crate::functions::modes::edit::{add_logic_e, auth_e, form_e, add_table_e}; use crate::state::app::state::AppState; use anyhow::Result; -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use crossterm::event::KeyEvent; // Removed KeyCode, KeyModifiers as they were unused +use tracing::debug; #[derive(Debug, Clone, PartialEq, Eq)] pub enum EditEventOutcome { - Message(String), // Return a message, stay in Edit mode - ExitEditMode, // Signal to exit Edit mode + Message(String), + ExitEditMode, } pub async fn handle_edit_event( key: KeyEvent, config: &Config, - form_state: &mut FormState, + form_state: &mut FormState, // Now FormState is in scope login_state: &mut LoginState, register_state: &mut RegisterState, admin_state: &mut AdminState, @@ -34,17 +35,20 @@ pub async fn handle_edit_event( grpc_client: &mut GrpcClient, app_state: &AppState, ) -> Result { - // Global command mode check (should ideally be handled before calling this function) + // --- Global command mode check --- if let Some("enter_command_mode") = config.get_action_for_key_in_mode( - &config.keybindings.global, + &config.keybindings.global, // Assuming command mode can be entered globally key.code, key.modifiers, ) { + // This check might be redundant if EventHandler already prevents entering Edit mode + // when command_mode is true. However, it's a safeguard. return Ok(EditEventOutcome::Message( - "Command mode entry handled globally.".to_string(), + "Cannot enter command mode from edit mode here.".to_string(), )); } + // --- Common actions (save, revert) --- if let Some(action) = config.get_action_for_key_in_mode( &config.keybindings.common, key.code, @@ -52,261 +56,197 @@ pub async fn handle_edit_event( ).as_deref() { if matches!(action, "save" | "revert") { let message_string: String = if app_state.ui.show_login { - auth_e::execute_common_action( - action, - login_state, - grpc_client, - current_position, - total_count, - ) - .await? + auth_e::execute_common_action(action, login_state, grpc_client, current_position, total_count).await? } else if app_state.ui.show_register { - auth_e::execute_common_action( - action, - register_state, - grpc_client, - current_position, - total_count, - ) - .await? + auth_e::execute_common_action(action, register_state, grpc_client, current_position, total_count).await? } else if app_state.ui.show_add_table { - format!( - "Action '{}' not fully implemented for Add Table view here.", - action - ) + // TODO: Implement common actions for AddTable if needed + format!("Action '{}' not implemented for Add Table in edit mode.", action) } else if app_state.ui.show_add_logic { - format!( - "Action '{}' not fully implemented for Add Logic view here.", - action - ) - } else { - let outcome = form_e::execute_common_action( - action, - form_state, - grpc_client, - current_position, - total_count, - ) - .await?; + // TODO: Implement common actions for AddLogic if needed + format!("Action '{}' not implemented for Add Logic in edit mode.", action) + } else { // Assuming Form view + let outcome = form_e::execute_common_action(action, form_state, grpc_client, current_position, total_count).await?; match outcome { - EventOutcome::Ok(msg) => msg, - EventOutcome::DataSaved(_, msg) => msg, - _ => format!( - "Unexpected outcome from common action: {:?}", - outcome - ), + EventOutcome::Ok(msg) | EventOutcome::DataSaved(_, msg) => msg, + _ => format!("Unexpected outcome from common action: {:?}", outcome), } }; return Ok(EditEventOutcome::Message(message_string)); } } - // Edit-specific actions - if let Some(action) = - config.get_edit_action_for_key(key.code, key.modifiers) - .as_deref() { - // Handle enter_decider first - if action == "enter_decider" { + // --- Edit-specific actions --- + if let Some(action_str) = config.get_edit_action_for_key(key.code, key.modifiers).as_deref() { + // --- Handle "enter_decider" (Enter key) --- + if action_str == "enter_decider" { let effective_action = if app_state.ui.show_register && register_state.in_suggestion_mode - && register_state.current_field() == 4 { + && register_state.current_field() == 4 { // Role field + "select_suggestion" + } else if app_state.ui.show_add_logic + && admin_state.add_logic_state.in_target_column_suggestion_mode + && admin_state.add_logic_state.current_field() == 1 { // Target Column field "select_suggestion" } else { - "next_field" + "next_field" // Default action for Enter }; let msg = if app_state.ui.show_login { - auth_e::execute_edit_action( - effective_action, - key, - login_state, - ideal_cursor_column, - ) - .await? + auth_e::execute_edit_action(effective_action, key, login_state, ideal_cursor_column).await? } else if app_state.ui.show_add_table { - add_table_e::execute_edit_action( - effective_action, - key, - &mut admin_state.add_table_state, - ideal_cursor_column, - ) - .await? + add_table_e::execute_edit_action(effective_action, key, &mut admin_state.add_table_state, ideal_cursor_column).await? } else if app_state.ui.show_add_logic { - add_logic_e::execute_edit_action( - effective_action, - key, - &mut admin_state.add_logic_state, - ideal_cursor_column, - ) - .await? + add_logic_e::execute_edit_action(effective_action, key, &mut admin_state.add_logic_state, ideal_cursor_column).await? } else if app_state.ui.show_register { - auth_e::execute_edit_action( - effective_action, - key, - register_state, - ideal_cursor_column, - ) - .await? - } else { - form_e::execute_edit_action( - effective_action, - key, - form_state, - ideal_cursor_column, - ) - .await? + auth_e::execute_edit_action(effective_action, key, register_state, ideal_cursor_column).await? + } else { // Form view + form_e::execute_edit_action(effective_action, key, form_state, ideal_cursor_column).await? }; return Ok(EditEventOutcome::Message(msg)); } - if action == "exit" { + // --- Handle "exit" (Escape key) --- + if action_str == "exit" { if app_state.ui.show_register && register_state.in_suggestion_mode { - let msg = auth_e::execute_edit_action( - "exit_suggestion_mode", - key, - register_state, - ideal_cursor_column, - ) - .await?; + let msg = auth_e::execute_edit_action("exit_suggestion_mode", key, register_state, ideal_cursor_column).await?; return Ok(EditEventOutcome::Message(msg)); + } else if app_state.ui.show_add_logic && admin_state.add_logic_state.in_target_column_suggestion_mode { + admin_state.add_logic_state.in_target_column_suggestion_mode = false; + admin_state.add_logic_state.show_target_column_suggestions = false; + admin_state.add_logic_state.selected_target_column_suggestion_index = None; + return Ok(EditEventOutcome::Message("Exited column suggestions".to_string())); } else { return Ok(EditEventOutcome::ExitEditMode); } } - // Special handling for role field suggestions (Register view only) - if app_state.ui.show_register && register_state.current_field() == 4 { - if !register_state.in_suggestion_mode - && key.code == KeyCode::Tab - && key.modifiers == KeyModifiers::NONE - { - register_state.update_role_suggestions(); - if !register_state.role_suggestions.is_empty() { - register_state.in_suggestion_mode = true; - register_state.selected_suggestion_index = Some(0); - return Ok(EditEventOutcome::Message( - "Suggestions shown".to_string(), - )); - } else { - return Ok(EditEventOutcome::Message( - "No suggestions available".to_string(), - )); + // --- Autocomplete for AddLogicState Target Column --- + if app_state.ui.show_add_logic && admin_state.add_logic_state.current_field() == 1 { // Target Column field + if action_str == "suggestion_down" { // "Tab" is mapped to suggestion_down + if !admin_state.add_logic_state.in_target_column_suggestion_mode { + // Attempt to open suggestions + if let Some(profile_name) = admin_state.add_logic_state.profile_name.clone().into() { + if let Some(table_name) = admin_state.add_logic_state.selected_table_name.clone() { + debug!("Fetching table structure for autocomplete: Profile='{}', Table='{}'", profile_name, table_name); + match grpc_client.get_table_structure(profile_name, table_name).await { + Ok(ts_response) => { + admin_state.add_logic_state.table_columns_for_suggestions = + ts_response.columns.into_iter().map(|c| c.name).collect(); + admin_state.add_logic_state.update_target_column_suggestions(); + if !admin_state.add_logic_state.target_column_suggestions.is_empty() { + admin_state.add_logic_state.in_target_column_suggestion_mode = true; + // update_target_column_suggestions handles initial selection + return Ok(EditEventOutcome::Message("Column suggestions shown".to_string())); + } else { + return Ok(EditEventOutcome::Message("No column suggestions for current input".to_string())); + } + } + Err(e) => { + debug!("Error fetching table structure: {}", e); + admin_state.add_logic_state.table_columns_for_suggestions.clear(); // Clear old data on error + admin_state.add_logic_state.update_target_column_suggestions(); + return Ok(EditEventOutcome::Message(format!("Error fetching columns: {}", e))); + } + } + } else { + return Ok(EditEventOutcome::Message("No table selected for column suggestions".to_string())); + } + } else { // Should not happen if AddLogic is properly initialized + return Ok(EditEventOutcome::Message("Profile name missing for column suggestions".to_string())); + } + } else { // Already in suggestion mode, navigate down + let msg = add_logic_e::execute_edit_action(action_str, key, &mut admin_state.add_logic_state, ideal_cursor_column).await?; + return Ok(EditEventOutcome::Message(msg)); } - } - if register_state.in_suggestion_mode - && matches!( - action, - "suggestion_down" | "suggestion_up" - ) - { - let msg = auth_e::execute_edit_action( - action, - key, - register_state, - ideal_cursor_column, - ) - .await?; + } else if admin_state.add_logic_state.in_target_column_suggestion_mode && action_str == "suggestion_up" { + let msg = add_logic_e::execute_edit_action(action_str, key, &mut admin_state.add_logic_state, ideal_cursor_column).await?; return Ok(EditEventOutcome::Message(msg)); } } - // Execute other edit actions based on the current view + // --- Autocomplete for RegisterState Role Field --- + if app_state.ui.show_register && register_state.current_field() == 4 { // Role field + if !register_state.in_suggestion_mode && action_str == "suggestion_down" { // Tab + register_state.update_role_suggestions(); + if !register_state.role_suggestions.is_empty() { + register_state.in_suggestion_mode = true; + // update_role_suggestions should handle initial selection + return Ok(EditEventOutcome::Message("Role suggestions shown".to_string())); + } else { + // If Tab doesn't open suggestions, it might fall through to "next_field" + // or you might want specific behavior. For now, let it fall through. + } + } + if register_state.in_suggestion_mode && matches!(action_str, "suggestion_down" | "suggestion_up") { + let msg = auth_e::execute_edit_action(action_str, key, register_state, ideal_cursor_column).await?; + return Ok(EditEventOutcome::Message(msg)); + } + } + + // --- Dispatch other edit actions --- let msg = if app_state.ui.show_login { - auth_e::execute_edit_action( - action, - key, - login_state, - ideal_cursor_column, - ) - .await? + auth_e::execute_edit_action(action_str, key, login_state, ideal_cursor_column).await? } else if app_state.ui.show_add_table { - add_table_e::execute_edit_action( - action, - key, - &mut admin_state.add_table_state, - ideal_cursor_column, - ) - .await? + add_table_e::execute_edit_action(action_str, key, &mut admin_state.add_table_state, ideal_cursor_column).await? } else if app_state.ui.show_add_logic { - add_logic_e::execute_edit_action( - action, - key, - &mut admin_state.add_logic_state, - ideal_cursor_column, - ) - .await? + // If not a suggestion action handled above for AddLogic + if !(admin_state.add_logic_state.in_target_column_suggestion_mode && matches!(action_str, "suggestion_down" | "suggestion_up")) { + add_logic_e::execute_edit_action(action_str, key, &mut admin_state.add_logic_state, ideal_cursor_column).await? + } else { String::new() /* Already handled */ } } else if app_state.ui.show_register { - auth_e::execute_edit_action( - action, - key, - register_state, - ideal_cursor_column, - ) - .await? - } else { - form_e::execute_edit_action( - action, - key, - form_state, - ideal_cursor_column, - ) - .await? + if !(register_state.in_suggestion_mode && matches!(action_str, "suggestion_down" | "suggestion_up")) { + auth_e::execute_edit_action(action_str, key, register_state, ideal_cursor_column).await? + } else { String::new() /* Already handled */ } + } else { // Form view + form_e::execute_edit_action(action_str, key, form_state, ideal_cursor_column).await? }; return Ok(EditEventOutcome::Message(msg)); } // --- Character insertion --- + // If character insertion happens while in suggestion mode, exit suggestion mode first. + let mut exited_suggestion_mode_for_typing = false; if app_state.ui.show_register && register_state.in_suggestion_mode { register_state.in_suggestion_mode = false; register_state.show_role_suggestions = false; register_state.selected_suggestion_index = None; + exited_suggestion_mode_for_typing = true; + } + if app_state.ui.show_add_logic && admin_state.add_logic_state.in_target_column_suggestion_mode { + admin_state.add_logic_state.in_target_column_suggestion_mode = false; + admin_state.add_logic_state.show_target_column_suggestions = false; + admin_state.add_logic_state.selected_target_column_suggestion_index = None; + exited_suggestion_mode_for_typing = true; } - let msg = if app_state.ui.show_login { - auth_e::execute_edit_action( - "insert_char", - key, - login_state, - ideal_cursor_column, - ) - .await? + let mut char_insert_msg = if app_state.ui.show_login { + auth_e::execute_edit_action("insert_char", key, login_state, ideal_cursor_column).await? } else if app_state.ui.show_add_table { - add_table_e::execute_edit_action( - "insert_char", - key, - &mut admin_state.add_table_state, - ideal_cursor_column, - ) - .await? + add_table_e::execute_edit_action("insert_char", key, &mut admin_state.add_table_state, ideal_cursor_column).await? } else if app_state.ui.show_add_logic { - add_logic_e::execute_edit_action( - "insert_char", - key, - &mut admin_state.add_logic_state, - ideal_cursor_column, - ) - .await? + add_logic_e::execute_edit_action("insert_char", key, &mut admin_state.add_logic_state, ideal_cursor_column).await? } else if app_state.ui.show_register { - auth_e::execute_edit_action( - "insert_char", - key, - register_state, - ideal_cursor_column, - ) - .await? - } else { - form_e::execute_edit_action( - "insert_char", - key, - form_state, - ideal_cursor_column, - ) - .await? + auth_e::execute_edit_action("insert_char", key, register_state, ideal_cursor_column).await? + } else { // Form view + form_e::execute_edit_action("insert_char", key, form_state, ideal_cursor_column).await? }; + // After character insertion, update suggestions if applicable if app_state.ui.show_register && register_state.current_field() == 4 { register_state.update_role_suggestions(); + // If we just exited suggestion mode by typing, don't immediately show them again unless Tab is pressed. + // However, update_role_suggestions will set show_role_suggestions if matches are found. + // This is fine, as the render logic checks in_suggestion_mode. + } + if app_state.ui.show_add_logic && admin_state.add_logic_state.current_field() == 1 { + admin_state.add_logic_state.update_target_column_suggestions(); } - return Ok(EditEventOutcome::Message(msg)); + if exited_suggestion_mode_for_typing && char_insert_msg.is_empty() { + char_insert_msg = "Suggestions hidden".to_string(); + } + + + Ok(EditEventOutcome::Message(char_insert_msg)) } diff --git a/client/src/state/pages/add_logic.rs b/client/src/state/pages/add_logic.rs index 025bf85..d167c8a 100644 --- a/client/src/state/pages/add_logic.rs +++ b/client/src/state/pages/add_logic.rs @@ -12,8 +12,8 @@ pub enum AddLogicFocus { InputLogicName, InputTargetColumn, InputDescription, - ScriptContentPreview, // Like ColumnsTable - can be highlighted/selected - InsideScriptContent, // Like InsideColumnsTable - full editing mode + ScriptContentPreview, + InsideScriptContent, SaveButton, CancelButton, } @@ -35,6 +35,13 @@ pub struct AddLogicState { pub has_unsaved_changes: bool, pub editor_keybinding_mode: EditorKeybindingMode, pub vim_state: VimState, + + // New fields for Target Column Autocomplete + pub table_columns_for_suggestions: Vec, // All columns for the table + pub target_column_suggestions: Vec, // Filtered suggestions + pub show_target_column_suggestions: bool, + pub selected_target_column_suggestion_index: Option, + pub in_target_column_suggestion_mode: bool, } impl AddLogicState { @@ -56,10 +63,57 @@ impl AddLogicState { has_unsaved_changes: false, editor_keybinding_mode: editor_config.keybinding_mode.clone(), vim_state: VimState::default(), + + table_columns_for_suggestions: Vec::new(), + target_column_suggestions: Vec::new(), + show_target_column_suggestions: false, + selected_target_column_suggestion_index: None, + in_target_column_suggestion_mode: false, } } pub const INPUT_FIELD_COUNT: usize = 3; + + /// Updates the target_column_suggestions based on current input. + pub fn update_target_column_suggestions(&mut self) { + let current_input = self.target_column_input.to_lowercase(); + if self.table_columns_for_suggestions.is_empty() { + self.target_column_suggestions.clear(); + self.show_target_column_suggestions = false; + self.selected_target_column_suggestion_index = None; + return; + } + + if current_input.is_empty() { + self.target_column_suggestions = self.table_columns_for_suggestions.clone(); + } else { + self.target_column_suggestions = self + .table_columns_for_suggestions + .iter() + .filter(|name| name.to_lowercase().contains(¤t_input)) + .cloned() + .collect(); + } + + 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); + } + } else { + self.selected_target_column_suggestion_index = None; + } + } } impl Default for AddLogicState { @@ -122,21 +176,21 @@ impl CanvasState for AddLogicState { } fn set_current_field(&mut self, index: usize) { - self.current_focus = match index { - 0 => { - self.last_canvas_field = 0; - AddLogicFocus::InputLogicName - }, - 1 => { - self.last_canvas_field = 1; - AddLogicFocus::InputTargetColumn - }, - 2 => { - self.last_canvas_field = 2; - AddLogicFocus::InputDescription - }, - _ => self.current_focus, + let new_focus = match index { + 0 => AddLogicFocus::InputLogicName, + 1 => AddLogicFocus::InputTargetColumn, + 2 => AddLogicFocus::InputDescription, + _ => return, // Or handle error/default }; + 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; + } + self.current_focus = new_focus; + self.last_canvas_field = index; + } } fn set_current_cursor_pos(&mut self, pos: usize) { @@ -161,10 +215,24 @@ impl CanvasState for AddLogicState { } fn get_suggestions(&self) -> Option<&[String]> { - None + if self.current_field() == 1 // Target Column field index + && self.in_target_column_suggestion_mode + && self.show_target_column_suggestions + { + Some(&self.target_column_suggestions) + } else { + None + } } fn get_selected_suggestion_index(&self) -> Option { - None + if self.current_field() == 1 // Target Column field index + && self.in_target_column_suggestion_mode + && self.show_target_column_suggestions + { + self.selected_target_column_suggestion_index + } else { + None + } } }