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 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),
|
||||||
|
|||||||
Reference in New Issue
Block a user