automcomplete running and working now

This commit is contained in:
filipriec
2025-05-25 19:26:30 +02:00
parent 5afb427bb4
commit 4e7213d1aa
4 changed files with 375 additions and 495 deletions

View File

@@ -12,16 +12,17 @@ use ratatui::{
Frame, Frame,
}; };
use crate::components::handlers::canvas::render_canvas; 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::config::binds::config::EditorKeybindingMode;
use crate::modes::handlers::mode_manager::AppMode; // For checking AppMode::Edit
pub fn render_add_logic( pub fn render_add_logic(
f: &mut Frame, f: &mut Frame,
area: Rect, area: Rect,
theme: &Theme, theme: &Theme,
app_state: &AppState, app_state: &AppState,
add_logic_state: &mut AddLogicState, add_logic_state: &mut AddLogicState, // Changed to &mut
is_edit_mode: bool, // Used for border/title hints in InsideScriptContent is_edit_mode: bool, // This is the general edit mode from EventHandler
highlight_state: &HighlightState, highlight_state: &HighlightState,
) { ) {
let main_block = Block::default() let main_block = Block::default()
@@ -41,20 +42,18 @@ pub fn render_add_logic(
let border_style = Style::default().fg(border_style_color); let border_style = Style::default().fg(border_style_color);
editor_ref.set_cursor_line_style(Style::default()); 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)); editor_ref.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED));
let script_title_hint = match add_logic_state.editor_keybinding_mode { let script_title_hint = match add_logic_state.editor_keybinding_mode {
EditorKeybindingMode::Vim => { EditorKeybindingMode::Vim => {
let vim_mode_status = crate::components::common::text_editor::TextEditor::get_vim_mode_status(&add_logic_state.vim_state); 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 // Vim mode status is relevant regardless of the general `is_edit_mode`
format!("Script {}", vim_mode_status) format!("Script {}", vim_mode_status)
} else {
format!("Script {}", vim_mode_status)
}
} }
EditorKeybindingMode::Emacs | EditorKeybindingMode::Default => { 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() "Script (Editing)".to_string()
} else { } else {
"Script".to_string() "Script".to_string()
@@ -79,7 +78,7 @@ pub fn render_add_logic(
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints([ .constraints([
Constraint::Length(3), // Top info 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::Min(5), // Script preview
Constraint::Length(3), // Buttons Constraint::Length(3), // Buttons
]) ])
@@ -123,10 +122,11 @@ pub fn render_add_logic(
| AddLogicFocus::InputTargetColumn | AddLogicFocus::InputTargetColumn
| AddLogicFocus::InputDescription | AddLogicFocus::InputDescription
); );
render_canvas( // Call render_canvas and get the active_field_rect
let active_field_rect = render_canvas(
f, f,
canvas_area, canvas_area,
add_logic_state, add_logic_state, // Pass the whole state as it impl CanvasState
&add_logic_state.fields(), &add_logic_state.fields(),
&add_logic_state.current_field(), &add_logic_state.current_field(),
&add_logic_state.inputs(), &add_logic_state.inputs(),
@@ -135,6 +135,26 @@ pub fn render_add_logic(
highlight_state, 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 // Script content preview
{ {
let mut editor_ref = add_logic_state.script_content_editor.borrow_mut(); 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; let is_script_preview_focused = add_logic_state.current_focus == AddLogicFocus::ScriptContentPreview;
if is_script_preview_focused { 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)); editor_ref.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED));
} else { } else {
// When script PREVIEW is NOT focused, use an underscore cursor.
let underscore_cursor_style = Style::default() let underscore_cursor_style = Style::default()
.add_modifier(Modifier::UNDERLINED) .add_modifier(Modifier::UNDERLINED)
.fg(theme.secondary); .fg(theme.secondary);
@@ -154,16 +172,12 @@ pub fn render_add_logic(
} }
let border_style_color = if is_script_preview_focused { let border_style_color = if is_script_preview_focused {
theme.highlight // Green highlight when focused and ready to select theme.highlight
} else { } else {
theme.secondary theme.secondary
}; };
let title_text = if is_script_preview_focused { let title_text = "Script Preview"; // Title doesn't need to change based on focus here
"Script Preview"
} else {
"Script Preview"
};
let title_style = if is_script_preview_focused { let title_style = if is_script_preview_focused {
Style::default().fg(theme.highlight).add_modifier(Modifier::BOLD) Style::default().fg(theme.highlight).add_modifier(Modifier::BOLD)
@@ -182,8 +196,8 @@ pub fn render_add_logic(
} }
// Buttons // Buttons
let get_button_style = |button_focus: AddLogicFocus, current_focus| { let get_button_style = |button_focus: AddLogicFocus, current_focus_state: AddLogicFocus| {
let is_focused = current_focus == button_focus; let is_focused = current_focus_state == button_focus;
let base_style = Style::default().fg(if is_focused { let base_style = Style::default().fg(if is_focused {
theme.highlight theme.highlight
} else { } 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 { if is_focused {
Style::default().fg(theme.highlight) Style::default().fg(current_theme.highlight)
} else { } else {
Style::default().fg(theme.secondary) Style::default().fg(current_theme.secondary)
} }
}; };

View File

@@ -1,277 +1,135 @@
// src/functions/modes/edit/add_logic_e.rs // 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 crate::state::pages::canvas_state::CanvasState;
use crossterm::event::{KeyCode, KeyEvent};
use anyhow::Result; 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<char> = 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<char> = 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<char> = 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<char> = 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( pub async fn execute_edit_action(
action: &str, action: &str,
key: KeyEvent, key: KeyEvent, // Keep key for insert_char
state: &mut AddLogicState, // Changed state: &mut AddLogicState,
ideal_cursor_column: &mut usize, ideal_cursor_column: &mut usize,
) -> Result<String> { ) -> Result<String> {
let mut message = String::new();
match action { 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<char> = 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<char> = 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<char> = 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" => { "next_field" => {
let num_fields = AddLogicState::INPUT_FIELD_COUNT; // Changed let current_field = state.current_field();
if num_fields > 0 { let next_field = (current_field + 1) % AddLogicState::INPUT_FIELD_COUNT;
let current_field = state.current_field(); state.set_current_field(next_field);
let last_field_index = num_fields - 1; *ideal_cursor_column = state.current_cursor_pos();
if current_field < last_field_index { // Prevent cycling message = format!("Focus on field {}", state.fields()[next_field]);
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())
} }
"prev_field" => { "prev_field" => {
let num_fields = AddLogicState::INPUT_FIELD_COUNT; // Changed let current_field = state.current_field();
if num_fields > 0 { let prev_field = if current_field == 0 {
let current_field = state.current_field(); AddLogicState::INPUT_FIELD_COUNT - 1
if current_field > 0 { // Prevent cycling } else {
state.set_current_field(current_field - 1); current_field - 1
} };
let current_input = state.get_current_input(); state.set_current_field(prev_field);
let max_pos = current_input.len(); *ideal_cursor_column = state.current_cursor_pos();
state.set_current_cursor_pos((*ideal_cursor_column).min(max_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" => { "move_left" => {
let new_pos = state.current_cursor_pos().saturating_sub(1); let current_pos = state.current_cursor_pos();
state.set_current_cursor_pos(new_pos); if current_pos > 0 {
*ideal_cursor_column = new_pos; let new_pos = current_pos - 1;
Ok("".to_string()) state.set_current_cursor_pos(new_pos);
*ideal_cursor_column = new_pos;
}
} }
"move_right" => { "move_right" => {
let current_input = state.get_current_input();
let current_pos = state.current_cursor_pos(); 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; let new_pos = current_pos + 1;
state.set_current_cursor_pos(new_pos); state.set_current_cursor_pos(new_pos);
*ideal_cursor_column = new_pos; *ideal_cursor_column = new_pos;
} }
Ok("".to_string())
} }
"move_up" => { // In edit mode, up/down usually means prev/next field "insert_char" => {
let current_field = state.current_field(); if let KeyCode::Char(c) = key.code {
if current_field > 0 { let current_pos = state.current_cursor_pos();
let new_field = current_field - 1; state.get_current_input_mut().insert(current_pos, c);
state.set_current_field(new_field); let new_pos = current_pos + 1;
let current_input = state.get_current_input(); state.set_current_cursor_pos(new_pos);
let max_pos = current_input.len(); *ideal_cursor_column = new_pos;
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos)); state.set_has_unsaved_changes(true);
} if state.current_field() == 1 {
Ok("".to_string()) state.update_target_column_suggestions();
}
"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));
} }
} }
Ok("".to_string())
} }
"move_line_start" => { "suggestion_down" => {
state.set_current_cursor_pos(0); if state.in_target_column_suggestion_mode && !state.target_column_suggestions.is_empty() {
*ideal_cursor_column = 0; let current_selection = state.selected_target_column_suggestion_index.unwrap_or(0);
Ok("".to_string()) let next_selection = (current_selection + 1) % state.target_column_suggestions.len();
} state.selected_target_column_suggestion_index = Some(next_selection);
"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));
} }
Ok("".to_string())
} }
"move_last_line" => { "suggestion_up" => {
let num_fields = AddLogicState::INPUT_FIELD_COUNT; // Changed if state.in_target_column_suggestion_mode && !state.target_column_suggestions.is_empty() {
if num_fields > 0 { let current_selection = state.selected_target_column_suggestion_index.unwrap_or(0);
let new_field = num_fields - 1; let prev_selection = if current_selection == 0 {
state.set_current_field(new_field); state.target_column_suggestions.len() - 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())
}
"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)
} else { } else {
new_pos current_selection - 1
}; };
let max_valid_index = current_input.len(); // Allow cursor at end state.selected_target_column_suggestion_index = Some(prev_selection);
let clamped_pos = final_pos.min(max_valid_index);
state.set_current_cursor_pos(clamped_pos);
*ideal_cursor_column = clamped_pos;
} }
Ok("".to_string())
} }
"move_word_prev" => { "select_suggestion" => {
let current_input = state.get_current_input(); if state.in_target_column_suggestion_mode {
if !current_input.is_empty() { let mut selected_suggestion_text: Option<String> = None;
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)),
}
}
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)
}

View File

@@ -5,26 +5,27 @@ use crate::state::pages::{
auth::{LoginState, RegisterState}, auth::{LoginState, RegisterState},
canvas_state::CanvasState, canvas_state::CanvasState,
}; };
use crate::state::pages::add_logic::AddLogicState; use crate::state::pages::form::FormState; // <<< ADD THIS LINE
use crate::state::pages::form::FormState; // AddLogicState is already imported
use crate::state::pages::add_table::AddTableState; // AddTableState is already imported
use crate::state::pages::admin::AdminState; use crate::state::pages::admin::AdminState;
use crate::modes::handlers::event::EventOutcome; use crate::modes::handlers::event::EventOutcome;
use crate::functions::modes::edit::{add_logic_e, auth_e, form_e, add_table_e}; use crate::functions::modes::edit::{add_logic_e, auth_e, form_e, add_table_e};
use crate::state::app::state::AppState; use crate::state::app::state::AppState;
use anyhow::Result; 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)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum EditEventOutcome { pub enum EditEventOutcome {
Message(String), // Return a message, stay in Edit mode Message(String),
ExitEditMode, // Signal to exit Edit mode ExitEditMode,
} }
pub async fn handle_edit_event( pub async fn handle_edit_event(
key: KeyEvent, key: KeyEvent,
config: &Config, config: &Config,
form_state: &mut FormState, form_state: &mut FormState, // Now FormState is in scope
login_state: &mut LoginState, login_state: &mut LoginState,
register_state: &mut RegisterState, register_state: &mut RegisterState,
admin_state: &mut AdminState, admin_state: &mut AdminState,
@@ -34,17 +35,20 @@ pub async fn handle_edit_event(
grpc_client: &mut GrpcClient, grpc_client: &mut GrpcClient,
app_state: &AppState, app_state: &AppState,
) -> Result<EditEventOutcome> { ) -> Result<EditEventOutcome> {
// 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( 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.code,
key.modifiers, 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( 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( if let Some(action) = config.get_action_for_key_in_mode(
&config.keybindings.common, &config.keybindings.common,
key.code, key.code,
@@ -52,261 +56,197 @@ pub async fn handle_edit_event(
).as_deref() { ).as_deref() {
if matches!(action, "save" | "revert") { if matches!(action, "save" | "revert") {
let message_string: String = if app_state.ui.show_login { let message_string: String = if app_state.ui.show_login {
auth_e::execute_common_action( auth_e::execute_common_action(action, login_state, grpc_client, current_position, total_count).await?
action,
login_state,
grpc_client,
current_position,
total_count,
)
.await?
} else if app_state.ui.show_register { } else if app_state.ui.show_register {
auth_e::execute_common_action( auth_e::execute_common_action(action, register_state, grpc_client, current_position, total_count).await?
action,
register_state,
grpc_client,
current_position,
total_count,
)
.await?
} else if app_state.ui.show_add_table { } else if app_state.ui.show_add_table {
format!( // TODO: Implement common actions for AddTable if needed
"Action '{}' not fully implemented for Add Table view here.", format!("Action '{}' not implemented for Add Table in edit mode.", action)
action
)
} else if app_state.ui.show_add_logic { } else if app_state.ui.show_add_logic {
format!( // TODO: Implement common actions for AddLogic if needed
"Action '{}' not fully implemented for Add Logic view here.", format!("Action '{}' not implemented for Add Logic in edit mode.", action)
action } else { // Assuming Form view
) let outcome = form_e::execute_common_action(action, form_state, grpc_client, current_position, total_count).await?;
} else {
let outcome = form_e::execute_common_action(
action,
form_state,
grpc_client,
current_position,
total_count,
)
.await?;
match outcome { match outcome {
EventOutcome::Ok(msg) => msg, EventOutcome::Ok(msg) | EventOutcome::DataSaved(_, msg) => msg,
EventOutcome::DataSaved(_, msg) => msg, _ => format!("Unexpected outcome from common action: {:?}", outcome),
_ => format!(
"Unexpected outcome from common action: {:?}",
outcome
),
} }
}; };
return Ok(EditEventOutcome::Message(message_string)); return Ok(EditEventOutcome::Message(message_string));
} }
} }
// Edit-specific actions // --- Edit-specific actions ---
if let Some(action) = if let Some(action_str) = config.get_edit_action_for_key(key.code, key.modifiers).as_deref() {
config.get_edit_action_for_key(key.code, key.modifiers) // --- Handle "enter_decider" (Enter key) ---
.as_deref() { if action_str == "enter_decider" {
// Handle enter_decider first
if action == "enter_decider" {
let effective_action = if app_state.ui.show_register let effective_action = if app_state.ui.show_register
&& register_state.in_suggestion_mode && 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" "select_suggestion"
} else { } else {
"next_field" "next_field" // Default action for Enter
}; };
let msg = if app_state.ui.show_login { let msg = if app_state.ui.show_login {
auth_e::execute_edit_action( auth_e::execute_edit_action(effective_action, key, login_state, ideal_cursor_column).await?
effective_action,
key,
login_state,
ideal_cursor_column,
)
.await?
} else if app_state.ui.show_add_table { } else if app_state.ui.show_add_table {
add_table_e::execute_edit_action( add_table_e::execute_edit_action(effective_action, key, &mut admin_state.add_table_state, ideal_cursor_column).await?
effective_action,
key,
&mut admin_state.add_table_state,
ideal_cursor_column,
)
.await?
} else if app_state.ui.show_add_logic { } else if app_state.ui.show_add_logic {
add_logic_e::execute_edit_action( add_logic_e::execute_edit_action(effective_action, key, &mut admin_state.add_logic_state, ideal_cursor_column).await?
effective_action,
key,
&mut admin_state.add_logic_state,
ideal_cursor_column,
)
.await?
} else if app_state.ui.show_register { } else if app_state.ui.show_register {
auth_e::execute_edit_action( auth_e::execute_edit_action(effective_action, key, register_state, ideal_cursor_column).await?
effective_action, } else { // Form view
key, form_e::execute_edit_action(effective_action, key, form_state, ideal_cursor_column).await?
register_state,
ideal_cursor_column,
)
.await?
} else {
form_e::execute_edit_action(
effective_action,
key,
form_state,
ideal_cursor_column,
)
.await?
}; };
return Ok(EditEventOutcome::Message(msg)); 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 { if app_state.ui.show_register && register_state.in_suggestion_mode {
let msg = auth_e::execute_edit_action( let msg = auth_e::execute_edit_action("exit_suggestion_mode", key, register_state, ideal_cursor_column).await?;
"exit_suggestion_mode",
key,
register_state,
ideal_cursor_column,
)
.await?;
return Ok(EditEventOutcome::Message(msg)); 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 { } else {
return Ok(EditEventOutcome::ExitEditMode); return Ok(EditEventOutcome::ExitEditMode);
} }
} }
// Special handling for role field suggestions (Register view only) // --- Autocomplete for AddLogicState Target Column ---
if app_state.ui.show_register && register_state.current_field() == 4 { if app_state.ui.show_add_logic && admin_state.add_logic_state.current_field() == 1 { // Target Column field
if !register_state.in_suggestion_mode if action_str == "suggestion_down" { // "Tab" is mapped to suggestion_down
&& key.code == KeyCode::Tab if !admin_state.add_logic_state.in_target_column_suggestion_mode {
&& key.modifiers == KeyModifiers::NONE // Attempt to open suggestions
{ if let Some(profile_name) = admin_state.add_logic_state.profile_name.clone().into() {
register_state.update_role_suggestions(); if let Some(table_name) = admin_state.add_logic_state.selected_table_name.clone() {
if !register_state.role_suggestions.is_empty() { debug!("Fetching table structure for autocomplete: Profile='{}', Table='{}'", profile_name, table_name);
register_state.in_suggestion_mode = true; match grpc_client.get_table_structure(profile_name, table_name).await {
register_state.selected_suggestion_index = Some(0); Ok(ts_response) => {
return Ok(EditEventOutcome::Message( admin_state.add_logic_state.table_columns_for_suggestions =
"Suggestions shown".to_string(), ts_response.columns.into_iter().map(|c| c.name).collect();
)); admin_state.add_logic_state.update_target_column_suggestions();
} else { if !admin_state.add_logic_state.target_column_suggestions.is_empty() {
return Ok(EditEventOutcome::Message( admin_state.add_logic_state.in_target_column_suggestion_mode = true;
"No suggestions available".to_string(), // 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));
} }
} } else if admin_state.add_logic_state.in_target_column_suggestion_mode && action_str == "suggestion_up" {
if register_state.in_suggestion_mode let msg = add_logic_e::execute_edit_action(action_str, key, &mut admin_state.add_logic_state, ideal_cursor_column).await?;
&& matches!(
action,
"suggestion_down" | "suggestion_up"
)
{
let msg = auth_e::execute_edit_action(
action,
key,
register_state,
ideal_cursor_column,
)
.await?;
return Ok(EditEventOutcome::Message(msg)); 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 { let msg = if app_state.ui.show_login {
auth_e::execute_edit_action( auth_e::execute_edit_action(action_str, key, login_state, ideal_cursor_column).await?
action,
key,
login_state,
ideal_cursor_column,
)
.await?
} else if app_state.ui.show_add_table { } else if app_state.ui.show_add_table {
add_table_e::execute_edit_action( add_table_e::execute_edit_action(action_str, key, &mut admin_state.add_table_state, ideal_cursor_column).await?
action,
key,
&mut admin_state.add_table_state,
ideal_cursor_column,
)
.await?
} else if app_state.ui.show_add_logic { } else if app_state.ui.show_add_logic {
add_logic_e::execute_edit_action( // If not a suggestion action handled above for AddLogic
action, if !(admin_state.add_logic_state.in_target_column_suggestion_mode && matches!(action_str, "suggestion_down" | "suggestion_up")) {
key, add_logic_e::execute_edit_action(action_str, key, &mut admin_state.add_logic_state, ideal_cursor_column).await?
&mut admin_state.add_logic_state, } else { String::new() /* Already handled */ }
ideal_cursor_column,
)
.await?
} else if app_state.ui.show_register { } else if app_state.ui.show_register {
auth_e::execute_edit_action( if !(register_state.in_suggestion_mode && matches!(action_str, "suggestion_down" | "suggestion_up")) {
action, auth_e::execute_edit_action(action_str, key, register_state, ideal_cursor_column).await?
key, } else { String::new() /* Already handled */ }
register_state, } else { // Form view
ideal_cursor_column, form_e::execute_edit_action(action_str, key, form_state, ideal_cursor_column).await?
)
.await?
} else {
form_e::execute_edit_action(
action,
key,
form_state,
ideal_cursor_column,
)
.await?
}; };
return Ok(EditEventOutcome::Message(msg)); return Ok(EditEventOutcome::Message(msg));
} }
// --- Character insertion --- // --- 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 { if app_state.ui.show_register && register_state.in_suggestion_mode {
register_state.in_suggestion_mode = false; register_state.in_suggestion_mode = false;
register_state.show_role_suggestions = false; register_state.show_role_suggestions = false;
register_state.selected_suggestion_index = None; 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 { let mut char_insert_msg = if app_state.ui.show_login {
auth_e::execute_edit_action( auth_e::execute_edit_action("insert_char", key, login_state, ideal_cursor_column).await?
"insert_char",
key,
login_state,
ideal_cursor_column,
)
.await?
} else if app_state.ui.show_add_table { } else if app_state.ui.show_add_table {
add_table_e::execute_edit_action( add_table_e::execute_edit_action("insert_char", key, &mut admin_state.add_table_state, ideal_cursor_column).await?
"insert_char",
key,
&mut admin_state.add_table_state,
ideal_cursor_column,
)
.await?
} else if app_state.ui.show_add_logic { } else if app_state.ui.show_add_logic {
add_logic_e::execute_edit_action( add_logic_e::execute_edit_action("insert_char", key, &mut admin_state.add_logic_state, ideal_cursor_column).await?
"insert_char",
key,
&mut admin_state.add_logic_state,
ideal_cursor_column,
)
.await?
} else if app_state.ui.show_register { } else if app_state.ui.show_register {
auth_e::execute_edit_action( auth_e::execute_edit_action("insert_char", key, register_state, ideal_cursor_column).await?
"insert_char", } else { // Form view
key, form_e::execute_edit_action("insert_char", key, form_state, ideal_cursor_column).await?
register_state,
ideal_cursor_column,
)
.await?
} else {
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 { if app_state.ui.show_register && register_state.current_field() == 4 {
register_state.update_role_suggestions(); 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))
} }

View File

@@ -12,8 +12,8 @@ pub enum AddLogicFocus {
InputLogicName, InputLogicName,
InputTargetColumn, InputTargetColumn,
InputDescription, InputDescription,
ScriptContentPreview, // Like ColumnsTable - can be highlighted/selected ScriptContentPreview,
InsideScriptContent, // Like InsideColumnsTable - full editing mode InsideScriptContent,
SaveButton, SaveButton,
CancelButton, CancelButton,
} }
@@ -35,6 +35,13 @@ pub struct AddLogicState {
pub has_unsaved_changes: bool, pub has_unsaved_changes: bool,
pub editor_keybinding_mode: EditorKeybindingMode, pub editor_keybinding_mode: EditorKeybindingMode,
pub vim_state: VimState, pub vim_state: VimState,
// New fields for Target Column Autocomplete
pub table_columns_for_suggestions: Vec<String>, // All columns for the table
pub target_column_suggestions: Vec<String>, // Filtered suggestions
pub show_target_column_suggestions: bool,
pub selected_target_column_suggestion_index: Option<usize>,
pub in_target_column_suggestion_mode: bool,
} }
impl AddLogicState { impl AddLogicState {
@@ -56,10 +63,57 @@ impl AddLogicState {
has_unsaved_changes: false, has_unsaved_changes: false,
editor_keybinding_mode: editor_config.keybinding_mode.clone(), editor_keybinding_mode: editor_config.keybinding_mode.clone(),
vim_state: VimState::default(), 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; 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(&current_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 { impl Default for AddLogicState {
@@ -122,21 +176,21 @@ impl CanvasState for AddLogicState {
} }
fn set_current_field(&mut self, index: usize) { fn set_current_field(&mut self, index: usize) {
self.current_focus = match index { let new_focus = match index {
0 => { 0 => AddLogicFocus::InputLogicName,
self.last_canvas_field = 0; 1 => AddLogicFocus::InputTargetColumn,
AddLogicFocus::InputLogicName 2 => AddLogicFocus::InputDescription,
}, _ => return, // Or handle error/default
1 => {
self.last_canvas_field = 1;
AddLogicFocus::InputTargetColumn
},
2 => {
self.last_canvas_field = 2;
AddLogicFocus::InputDescription
},
_ => self.current_focus,
}; };
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) { fn set_current_cursor_pos(&mut self, pos: usize) {
@@ -161,10 +215,24 @@ impl CanvasState for AddLogicState {
} }
fn get_suggestions(&self) -> Option<&[String]> { 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<usize> { fn get_selected_suggestion_index(&self) -> Option<usize> {
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
}
} }
} }