tabbing now adds / if there is nothing to tab to

This commit is contained in:
filipriec
2025-05-30 23:43:49 +02:00
parent 3df4baec92
commit ea88c2686d

View File

@@ -4,34 +4,31 @@ use crate::modes::handlers::event::EventOutcome;
use anyhow::Result; use anyhow::Result;
use common::proto::multieko2::table_definition::ProfileTreeResponse; use common::proto::multieko2::table_definition::ProfileTreeResponse;
use crossterm::event::{KeyCode, KeyEvent}; use crossterm::event::{KeyCode, KeyEvent};
use std::collections::{HashMap, HashSet}; // Added HashSet use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum NavigationType { pub enum NavigationType {
FindFile, FindFile,
TableTree, // Represents navigating the table dependency graph TableTree,
} }
// New structure for table dependencies
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TableDependencyGraph { pub struct TableDependencyGraph {
all_tables: HashSet<String>, all_tables: HashSet<String>,
dependents_map: HashMap<String, Vec<String>>, // Key: table_name, Value: list of tables that depend on key dependents_map: HashMap<String, Vec<String>>,
root_tables: Vec<String>, // Tables that don't depend on others root_tables: Vec<String>,
} }
impl TableDependencyGraph { impl TableDependencyGraph {
pub fn from_profile_tree(profile_tree: &ProfileTreeResponse) -> Self { pub fn from_profile_tree(profile_tree: &ProfileTreeResponse) -> Self {
let mut dependents_map: HashMap<String, Vec<String>> = HashMap::new(); let mut dependents_map: HashMap<String, Vec<String>> = HashMap::new();
let mut all_tables_set: HashSet<String> = HashSet::new(); let mut all_tables_set: HashSet<String> = HashSet::new();
let mut table_dependencies: HashMap<String, Vec<String>> = let mut table_dependencies: HashMap<String, Vec<String>> = HashMap::new();
HashMap::new();
for profile in &profile_tree.profiles { for profile in &profile_tree.profiles {
for table in &profile.tables { for table in &profile.tables {
all_tables_set.insert(table.name.clone()); all_tables_set.insert(table.name.clone());
table_dependencies table_dependencies.insert(table.name.clone(), table.depends_on.clone());
.insert(table.name.clone(), table.depends_on.clone());
for dependency_name in &table.depends_on { for dependency_name in &table.depends_on {
dependents_map dependents_map
@@ -50,11 +47,8 @@ impl TableDependencyGraph {
.map_or(true, |deps| deps.is_empty()) .map_or(true, |deps| deps.is_empty())
}) })
.cloned() .cloned()
.collect::<Vec<_>>()
.into_iter()
.collect(); .collect();
// Sort for consistent order
let mut sorted_root_tables = root_tables; let mut sorted_root_tables = root_tables;
sorted_root_tables.sort(); 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<String> { pub fn get_dependent_children(&self, path: &str) -> Vec<String> {
if path.is_empty() { if path.is_empty() {
return self.root_tables.clone(); return self.root_tables.clone();
} }
let path_segments: Vec<&str> = let path_segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
path.split('/').filter(|s| !s.is_empty()).collect();
if let Some(last_segment_name) = path_segments.last() { 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) { if self.all_tables.contains(*last_segment_name) {
return self return self
.dependents_map .dependents_map
@@ -93,13 +84,13 @@ impl TableDependencyGraph {
pub struct NavigationState { pub struct NavigationState {
pub active: bool, 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<usize>, pub selected_index: Option<usize>,
pub filtered_options: Vec<(usize, String)>, // (original_index_in_all_options, string_option) pub filtered_options: Vec<(usize, String)>,
pub navigation_type: NavigationType, pub navigation_type: NavigationType,
pub current_path: String, // Path built so far, e.g., "root_table/parent_table" pub current_path: String,
pub graph: Option<TableDependencyGraph>, // Changed from tree to graph pub graph: Option<TableDependencyGraph>,
pub all_options: Vec<String>, // Options currently available at this level of the path/input pub all_options: Vec<String>,
} }
impl NavigationState { impl NavigationState {
@@ -350,45 +341,63 @@ pub async fn handle_command_navigation_event(
KeyCode::Enter => { KeyCode::Enter => {
if let Some(selected_value) = navigation_state.get_selected_value() { if let Some(selected_value) = navigation_state.get_selected_value() {
let message = match navigation_state.navigation_type { let message = match navigation_state.navigation_type {
NavigationType::FindFile => { NavigationType::FindFile => format!("Selected file: {}", selected_value),
format!("Selected file: {}", selected_value) NavigationType::TableTree => format!("Selected table: {}", selected_value),
}
NavigationType::TableTree => {
format!("Selected table: {}", selected_value)
}
}; };
navigation_state.deactivate(); navigation_state.deactivate();
Ok(EventOutcome::Ok(message)) Ok(EventOutcome::Ok(message))
} else { } 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() { if navigation_state.navigation_type == NavigationType::TableTree && !navigation_state.input.is_empty() {
let current_input_clone = navigation_state.input.clone(); // Check if current input is a prefix of any option or a full option name
// Simulate pressing '/' to commit the current input as a path segment if let Some(selected_opt_str) = navigation_state.get_selected_option_str() {
// Store original options count to see if navigation happened if navigation_state.input == selected_opt_str {
let original_options_count = navigation_state.all_options.len(); // Input exactly matches the selected option, try to navigate
let original_path = navigation_state.current_path.clone(); let input_before_slash = navigation_state.input.clone();
navigation_state.add_char('/');
navigation_state.add_char('/');
if navigation_state.input.is_empty() {
// Check if path actually changed and new options are loaded return Ok(EventOutcome::Ok(format!("Navigated to: {}/", input_before_slash)));
if navigation_state.input.is_empty() && // Input cleared after '/' } else {
(navigation_state.current_path != original_path || // Path changed return Ok(EventOutcome::Ok(format!("Selected leaf: {}", input_before_slash)));
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)));
} }
} }
Ok(EventOutcome::Ok("No valid selection to confirm".to_string())) Ok(EventOutcome::Ok("No valid selection to confirm or navigate".to_string()))
} }
} }
KeyCode::Tab => { KeyCode::Tab => {
navigation_state.autocomplete_selected(); if let Some(selected_opt_str) = navigation_state.get_selected_option_str() {
Ok(EventOutcome::Ok(String::new())) // UI will refresh due to state change // 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 => { KeyCode::Up => {
navigation_state.move_up(); navigation_state.move_up();
@@ -407,9 +416,7 @@ pub async fn handle_command_navigation_event(
Ok(EventOutcome::Ok(String::new())) Ok(EventOutcome::Ok(String::new()))
} }
_ => { _ => {
if let Some(action) = if let Some(action) = config.get_general_action(key.code, key.modifiers) {
config.get_general_action(key.code, key.modifiers)
{
match action { match action {
"move_up" => { "move_up" => {
navigation_state.move_up(); navigation_state.move_up();
@@ -419,7 +426,7 @@ pub async fn handle_command_navigation_event(
navigation_state.move_down(); navigation_state.move_down();
Ok(EventOutcome::Ok(String::new())) Ok(EventOutcome::Ok(String::new()))
} }
"select" => { // This is equivalent to Enter "select" => {
if let Some(selected_value) = navigation_state.get_selected_value() { if let Some(selected_value) = navigation_state.get_selected_value() {
let message = match navigation_state.navigation_type { let message = match navigation_state.navigation_type {
NavigationType::FindFile => format!("Selected file: {}", selected_value), NavigationType::FindFile => format!("Selected file: {}", selected_value),