// src/modes/general/command_navigation.rs use crate::config::binds::config::Config; use crate::modes::handlers::event::EventOutcome; use anyhow::Result; use common::proto::komp_ac::table_definition::ProfileTreeResponse; use crossterm::event::{KeyCode, KeyEvent}; use std::collections::{HashMap, HashSet}; #[derive(Debug, Clone, PartialEq)] pub enum NavigationType { FindFile, TableTree, } #[derive(Debug, Clone)] pub struct TableDependencyGraph { all_tables: HashSet, 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(); 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()); for dependency_name in &table.depends_on { dependents_map .entry(dependency_name.clone()) .or_default() .push(table.name.clone()); } } } let root_tables: Vec = all_tables_set .iter() .filter(|name| { table_dependencies .get(*name) .map_or(true, |deps| deps.is_empty()) }) .cloned() .collect(); let mut sorted_root_tables = root_tables; sorted_root_tables.sort(); for dependents_list in dependents_map.values_mut() { dependents_list.sort(); } Self { all_tables: all_tables_set, dependents_map, root_tables: sorted_root_tables, } } 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(); if let Some(last_segment_name) = path_segments.last() { if self.all_tables.contains(*last_segment_name) { return self .dependents_map .get(*last_segment_name) .cloned() .unwrap_or_default(); } } Vec::new() } } // ... (NavigationState struct and its new(), activate_*, deactivate(), add_char(), remove_char(), move_*, autocomplete_selected(), get_display_input() methods are unchanged) ... pub struct NavigationState { pub active: bool, pub input: String, pub selected_index: Option, pub filtered_options: Vec<(usize, String)>, pub navigation_type: NavigationType, pub current_path: String, pub graph: Option, pub all_options: Vec, } impl NavigationState { pub fn new() -> Self { Self { active: false, input: String::new(), selected_index: None, filtered_options: Vec::new(), navigation_type: NavigationType::FindFile, current_path: String::new(), graph: None, all_options: Vec::new(), } } pub fn activate_find_file(&mut self, options: Vec) { self.active = true; self.navigation_type = NavigationType::FindFile; self.all_options = options; self.input.clear(); self.current_path.clear(); self.graph = None; self.update_filtered_options(); } pub fn activate_table_tree(&mut self, graph: TableDependencyGraph) { self.active = true; self.navigation_type = NavigationType::TableTree; self.graph = Some(graph); self.input.clear(); self.current_path.clear(); self.update_options_for_path(); } pub fn deactivate(&mut self) { self.active = false; self.input.clear(); self.all_options.clear(); self.filtered_options.clear(); self.selected_index = None; self.current_path.clear(); self.graph = None; } pub fn add_char(&mut self, c: char) { match self.navigation_type { NavigationType::FindFile => { self.input.push(c); self.update_filtered_options(); } NavigationType::TableTree => { if c == '/' { if !self.input.is_empty() { if self.current_path.is_empty() { self.current_path = self.input.clone(); } else { self.current_path.push('/'); self.current_path.push_str(&self.input); } self.input.clear(); self.update_options_for_path(); } } else { self.input.push(c); self.update_filtered_options(); } } } } pub fn remove_char(&mut self) { match self.navigation_type { NavigationType::FindFile => { self.input.pop(); self.update_filtered_options(); } NavigationType::TableTree => { if self.input.is_empty() { if !self.current_path.is_empty() { if let Some(last_slash_idx) = self.current_path.rfind('/') { self.input = self.current_path[last_slash_idx + 1..].to_string(); self.current_path = self.current_path[..last_slash_idx].to_string(); } else { self.input = self.current_path.clone(); self.current_path.clear(); } self.update_options_for_path(); self.update_filtered_options(); } } else { self.input.pop(); self.update_filtered_options(); } } } } pub fn move_up(&mut self) { if self.filtered_options.is_empty() { self.selected_index = None; return; } self.selected_index = match self.selected_index { Some(0) => Some(self.filtered_options.len() - 1), Some(current) => Some(current - 1), None => Some(self.filtered_options.len() - 1), }; } pub fn move_down(&mut self) { if self.filtered_options.is_empty() { self.selected_index = None; return; } self.selected_index = match self.selected_index { Some(current) if current >= self.filtered_options.len() - 1 => Some(0), Some(current) => Some(current + 1), None => Some(0), }; } pub fn get_selected_option_str(&self) -> Option<&str> { self.selected_index .and_then(|idx| self.filtered_options.get(idx)) .map(|(_, option_str)| option_str.as_str()) } pub fn autocomplete_selected(&mut self) { if let Some(selected_option_str) = self.get_selected_option_str() { self.input = selected_option_str.to_string(); self.update_filtered_options(); } } pub fn get_display_input(&self) -> String { match self.navigation_type { NavigationType::FindFile => self.input.clone(), NavigationType::TableTree => { if self.current_path.is_empty() { self.input.clone() } else { format!("{}/{}", self.current_path, self.input) } } } } // --- START FIX --- pub fn get_selected_value(&self) -> Option { match self.navigation_type { NavigationType::FindFile => { // Return the highlighted option, not the raw input buffer. self.get_selected_option_str().map(|s| s.to_string()) } NavigationType::TableTree => { self.get_selected_option_str().map(|selected_name| { if self.current_path.is_empty() { selected_name.to_string() } else { format!("{}/{}", self.current_path, selected_name) } }) } } } // --- END FIX --- fn update_options_for_path(&mut self) { if let NavigationType::TableTree = self.navigation_type { if let Some(graph) = &self.graph { self.all_options = graph.get_dependent_children(&self.current_path); } else { self.all_options.clear(); } } self.update_filtered_options(); } fn update_filtered_options(&mut self) { let filter_text = match self.navigation_type { NavigationType::FindFile => &self.input, NavigationType::TableTree => &self.input, } .to_lowercase(); if filter_text.is_empty() { self.filtered_options = self .all_options .iter() .enumerate() .map(|(i, opt)| (i, opt.clone())) .collect(); } else { self.filtered_options = self .all_options .iter() .enumerate() .filter(|(_, opt)| opt.to_lowercase().contains(&filter_text)) .map(|(i, opt)| (i, opt.clone())) .collect(); } if self.filtered_options.is_empty() { self.selected_index = None; } else { self.selected_index = Some(0); } } } pub async fn handle_command_navigation_event( navigation_state: &mut NavigationState, key: KeyEvent, config: &Config, ) -> Result { if !navigation_state.active { return Ok(EventOutcome::Ok(String::new())); } match key.code { KeyCode::Esc => { navigation_state.deactivate(); Ok(EventOutcome::Ok("Navigation cancelled".to_string())) } KeyCode::Tab => { if let Some(selected_opt_str) = navigation_state.get_selected_option_str() { if navigation_state.input == selected_opt_str { 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())) { 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 { navigation_state.autocomplete_selected(); } } Ok(EventOutcome::Ok(String::new())) } KeyCode::Backspace => { navigation_state.remove_char(); Ok(EventOutcome::Ok(String::new())) } KeyCode::Char(c) => { navigation_state.add_char(c); Ok(EventOutcome::Ok(String::new())) } _ => { if let Some(action) = config.get_general_action(key.code, key.modifiers) { match action { "move_up" => { navigation_state.move_up(); Ok(EventOutcome::Ok(String::new())) } "move_down" => { navigation_state.move_down(); Ok(EventOutcome::Ok(String::new())) } "select" => { if let Some(selected_value) = navigation_state.get_selected_value() { let outcome = match navigation_state.navigation_type { // --- START FIX --- NavigationType::FindFile => { // The purpose of this palette is to select a table. // Emit a TableSelected event instead of a generic Ok message. EventOutcome::TableSelected { path: selected_value, } } // --- END FIX --- NavigationType::TableTree => { EventOutcome::TableSelected { path: selected_value, } } }; navigation_state.deactivate(); Ok(outcome) } else { Ok(EventOutcome::Ok("No selection".to_string())) } } _ => Ok(EventOutcome::Ok(String::new())), } } else { Ok(EventOutcome::Ok(String::new())) } } } }