tabbing now adds / if there is nothing to tab to
This commit is contained in:
@@ -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<String>,
|
||||
dependents_map: HashMap<String, Vec<String>>, // Key: table_name, Value: list of tables that depend on key
|
||||
root_tables: Vec<String>, // Tables that don't depend on others
|
||||
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();
|
||||
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());
|
||||
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::<Vec<_>>()
|
||||
.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<String> {
|
||||
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<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 current_path: String, // Path built so far, e.g., "root_table/parent_table"
|
||||
pub graph: Option<TableDependencyGraph>, // Changed from tree to graph
|
||||
pub all_options: Vec<String>, // Options currently available at this level of the path/input
|
||||
pub current_path: String,
|
||||
pub graph: Option<TableDependencyGraph>,
|
||||
pub all_options: Vec<String>,
|
||||
}
|
||||
|
||||
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),
|
||||
|
||||
Reference in New Issue
Block a user