397 lines
14 KiB
Rust
397 lines
14 KiB
Rust
// 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<String>,
|
|
dependents_map: HashMap<String, Vec<String>>,
|
|
root_tables: Vec<String>,
|
|
}
|
|
|
|
impl TableDependencyGraph {
|
|
pub fn from_profile_tree(profile_tree: &ProfileTreeResponse) -> Self {
|
|
let mut dependents_map: HashMap<String, Vec<String>> = HashMap::new();
|
|
let mut all_tables_set: HashSet<String> = HashSet::new();
|
|
let mut table_dependencies: HashMap<String, Vec<String>> = 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<String> = 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<String> {
|
|
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<usize>,
|
|
pub filtered_options: Vec<(usize, String)>,
|
|
pub navigation_type: NavigationType,
|
|
pub current_path: String,
|
|
pub graph: Option<TableDependencyGraph>,
|
|
pub all_options: Vec<String>,
|
|
}
|
|
|
|
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<String>) {
|
|
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<String> {
|
|
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<EventOutcome> {
|
|
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()))
|
|
}
|
|
}
|
|
}
|
|
}
|