diff --git a/client/src/modes/general/command_navigation.rs b/client/src/modes/general/command_navigation.rs index a48eb89..25ad577 100644 --- a/client/src/modes/general/command_navigation.rs +++ b/client/src/modes/general/command_navigation.rs @@ -4,34 +4,31 @@ use crate::modes::handlers::event::EventOutcome; use anyhow::Result; use common::proto::multieko2::table_definition::ProfileTreeResponse; use crossterm::event::{KeyCode, KeyEvent}; -use std::collections::{HashMap, HashSet}; // Added HashSet +use std::collections::{HashMap, HashSet}; #[derive(Debug, Clone, PartialEq)] pub enum NavigationType { FindFile, - TableTree, // Represents navigating the table dependency graph + TableTree, } -// New structure for table dependencies #[derive(Debug, Clone)] pub struct TableDependencyGraph { all_tables: HashSet, - dependents_map: HashMap>, // Key: table_name, Value: list of tables that depend on key - root_tables: Vec, // Tables that don't depend on others + dependents_map: HashMap>, + root_tables: Vec, } impl TableDependencyGraph { pub fn from_profile_tree(profile_tree: &ProfileTreeResponse) -> Self { let mut dependents_map: HashMap> = HashMap::new(); let mut all_tables_set: HashSet = HashSet::new(); - let mut table_dependencies: HashMap> = - HashMap::new(); + let mut table_dependencies: HashMap> = HashMap::new(); for profile in &profile_tree.profiles { for table in &profile.tables { all_tables_set.insert(table.name.clone()); - table_dependencies - .insert(table.name.clone(), table.depends_on.clone()); + table_dependencies.insert(table.name.clone(), table.depends_on.clone()); for dependency_name in &table.depends_on { dependents_map @@ -50,11 +47,8 @@ impl TableDependencyGraph { .map_or(true, |deps| deps.is_empty()) }) .cloned() - .collect::>() - .into_iter() .collect(); - // Sort for consistent order let mut sorted_root_tables = root_tables; sorted_root_tables.sort(); @@ -69,16 +63,13 @@ impl TableDependencyGraph { } } - // Gets tables that depend on the last element of the path pub fn get_dependent_children(&self, path: &str) -> Vec { if path.is_empty() { return self.root_tables.clone(); } - let path_segments: Vec<&str> = - path.split('/').filter(|s| !s.is_empty()).collect(); + let path_segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect(); if let Some(last_segment_name) = path_segments.last() { - // Ensure the last segment is a valid table name before querying dependents if self.all_tables.contains(*last_segment_name) { return self .dependents_map @@ -93,13 +84,13 @@ impl TableDependencyGraph { pub struct NavigationState { pub active: bool, - pub input: String, // Current text in the input field (e.g., "my_tab" or "child_of_root") + pub input: String, pub selected_index: Option, - pub filtered_options: Vec<(usize, String)>, // (original_index_in_all_options, string_option) + pub filtered_options: Vec<(usize, String)>, pub navigation_type: NavigationType, - pub current_path: String, // Path built so far, e.g., "root_table/parent_table" - pub graph: Option, // Changed from tree to graph - pub all_options: Vec, // Options currently available at this level of the path/input + pub current_path: String, + pub graph: Option, + pub all_options: Vec, } impl NavigationState { @@ -350,45 +341,63 @@ pub async fn handle_command_navigation_event( KeyCode::Enter => { if let Some(selected_value) = navigation_state.get_selected_value() { let message = match navigation_state.navigation_type { - NavigationType::FindFile => { - format!("Selected file: {}", selected_value) - } - NavigationType::TableTree => { - format!("Selected table: {}", selected_value) - } + NavigationType::FindFile => format!("Selected file: {}", selected_value), + NavigationType::TableTree => format!("Selected table: {}", selected_value), }; navigation_state.deactivate(); Ok(EventOutcome::Ok(message)) } else { + // Enhanced Enter behavior for TableTree: if input is a valid partial path, try to navigate if navigation_state.navigation_type == NavigationType::TableTree && !navigation_state.input.is_empty() { - let current_input_clone = navigation_state.input.clone(); - // Simulate pressing '/' to commit the current input as a path segment - // Store original options count to see if navigation happened - let original_options_count = navigation_state.all_options.len(); - let original_path = navigation_state.current_path.clone(); - - navigation_state.add_char('/'); - - // Check if path actually changed and new options are loaded - if navigation_state.input.is_empty() && // Input cleared after '/' - (navigation_state.current_path != original_path || // Path changed - navigation_state.all_options.len() != original_options_count || // Options changed - !navigation_state.all_options.is_empty()) // Or new options appeared - { - return Ok(EventOutcome::Ok(format!("Navigated to: {}/", current_input_clone))); - } else { - // Navigation didn't happen, revert the add_char('/') effect if necessary - // This part is tricky, as add_char('/') modifies state. - // For simplicity, we'll just say "no valid selection" if it didn't navigate. - return Ok(EventOutcome::Ok(format!("Cannot navigate: '{}' is not a valid path segment or has no children.", current_input_clone))); + // Check if current input is a prefix of any option or a full option name + if let Some(selected_opt_str) = navigation_state.get_selected_option_str() { + if navigation_state.input == selected_opt_str { + // Input exactly matches the selected option, try to navigate + let input_before_slash = navigation_state.input.clone(); + navigation_state.add_char('/'); + + if navigation_state.input.is_empty() { + return Ok(EventOutcome::Ok(format!("Navigated to: {}/", input_before_slash))); + } else { + return Ok(EventOutcome::Ok(format!("Selected leaf: {}", input_before_slash))); + } + } } } - Ok(EventOutcome::Ok("No valid selection to confirm".to_string())) + Ok(EventOutcome::Ok("No valid selection to confirm or navigate".to_string())) } } KeyCode::Tab => { - navigation_state.autocomplete_selected(); - Ok(EventOutcome::Ok(String::new())) // UI will refresh due to state change + if let Some(selected_opt_str) = navigation_state.get_selected_option_str() { + // Scenario 1: Input already exactly matches the selected option + if navigation_state.input == selected_opt_str { + // Only attempt to navigate deeper for TableTree mode + if navigation_state.navigation_type == NavigationType::TableTree { + let path_before_nav = navigation_state.current_path.clone(); + let input_before_nav = navigation_state.input.clone(); + + navigation_state.add_char('/'); + + if navigation_state.input.is_empty() && + (navigation_state.current_path != path_before_nav || !navigation_state.all_options.is_empty()) { + // Navigation successful + } else { + // Revert if navigation didn't happen + if !navigation_state.input.is_empty() && navigation_state.input != input_before_nav { + navigation_state.input = input_before_nav; + if navigation_state.current_path != path_before_nav { + navigation_state.current_path = path_before_nav; + } + navigation_state.update_options_for_path(); + } + } + } + } else { + // Scenario 2: Input is a partial match - autocomplete + navigation_state.autocomplete_selected(); + } + } + Ok(EventOutcome::Ok(String::new())) } KeyCode::Up => { navigation_state.move_up(); @@ -407,9 +416,7 @@ pub async fn handle_command_navigation_event( Ok(EventOutcome::Ok(String::new())) } _ => { - if let Some(action) = - config.get_general_action(key.code, key.modifiers) - { + if let Some(action) = config.get_general_action(key.code, key.modifiers) { match action { "move_up" => { navigation_state.move_up(); @@ -419,7 +426,7 @@ pub async fn handle_command_navigation_event( navigation_state.move_down(); Ok(EventOutcome::Ok(String::new())) } - "select" => { // This is equivalent to Enter + "select" => { if let Some(selected_value) = navigation_state.get_selected_value() { let message = match navigation_state.navigation_type { NavigationType::FindFile => format!("Selected file: {}", selected_value),