it works amazingly well now, we can select the table name via command line
This commit is contained in:
@@ -1,22 +1,108 @@
|
||||
// src/modes/general/command_navigation.rs
|
||||
use crossterm::event::{KeyEvent, KeyCode};
|
||||
use crate::config::binds::config::Config;
|
||||
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
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum NavigationType {
|
||||
FindFile,
|
||||
// Future: CommandPalette, BufferList, etc.
|
||||
TableTree, // Represents navigating the table dependency graph
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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::<Vec<_>>()
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
// Sort for consistent order
|
||||
// dependents_map.values_mut().for_each(|deps| deps.sort());
|
||||
// root_tables.sort(); // This was causing an error if root_tables was not mutable. Let's fix it.
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
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
|
||||
.get(*last_segment_name)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
}
|
||||
}
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NavigationState {
|
||||
pub active: bool,
|
||||
pub input: String,
|
||||
pub options: Vec<String>,
|
||||
pub input: String, // Current text in the input field (e.g., "my_tab" or "child_of_root")
|
||||
pub selected_index: Option<usize>,
|
||||
pub filtered_options: Vec<(usize, String)>, // (original_index, filtered_string)
|
||||
pub filtered_options: Vec<(usize, String)>, // (original_index_in_all_options, string_option)
|
||||
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
|
||||
}
|
||||
|
||||
impl NavigationState {
|
||||
@@ -24,37 +110,106 @@ impl NavigationState {
|
||||
Self {
|
||||
active: false,
|
||||
input: String::new(),
|
||||
options: Vec::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.options = options;
|
||||
self.all_options = options;
|
||||
self.input.clear();
|
||||
self.update_filtered_options();
|
||||
self.current_path.clear();
|
||||
self.graph = None;
|
||||
self.update_filtered_options(); // Initial filter with empty input
|
||||
}
|
||||
|
||||
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(); // Initial options are root tables
|
||||
}
|
||||
|
||||
pub fn deactivate(&mut self) {
|
||||
self.active = false;
|
||||
self.input.clear();
|
||||
self.options.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) {
|
||||
self.input.push(c);
|
||||
self.update_filtered_options();
|
||||
match self.navigation_type {
|
||||
NavigationType::FindFile => {
|
||||
self.input.push(c);
|
||||
self.update_filtered_options();
|
||||
}
|
||||
NavigationType::TableTree => {
|
||||
if c == '/' {
|
||||
if !self.input.is_empty() {
|
||||
// Append current input to path
|
||||
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();
|
||||
}
|
||||
// If input is empty and char is '/', do nothing or define behavior
|
||||
} else {
|
||||
self.input.push(c);
|
||||
self.update_filtered_options(); // Filter current level options based on input
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_char(&mut self) {
|
||||
self.input.pop();
|
||||
self.update_filtered_options();
|
||||
match self.navigation_type {
|
||||
NavigationType::FindFile => {
|
||||
self.input.pop();
|
||||
self.update_filtered_options();
|
||||
}
|
||||
NavigationType::TableTree => {
|
||||
if self.input.is_empty() {
|
||||
// If input is empty, try to go up in path
|
||||
if !self.current_path.is_empty() {
|
||||
if let Some(last_slash_idx) =
|
||||
self.current_path.rfind('/')
|
||||
{
|
||||
// Set input to the segment being removed from path
|
||||
self.input = self.current_path
|
||||
[last_slash_idx + 1..]
|
||||
.to_string();
|
||||
self.current_path =
|
||||
self.current_path[..last_slash_idx].to_string();
|
||||
} else {
|
||||
// Path was a single segment
|
||||
self.input = self.current_path.clone();
|
||||
self.current_path.clear();
|
||||
}
|
||||
self.update_options_for_path();
|
||||
// After path change, current input might match some options, so filter
|
||||
self.update_filtered_options();
|
||||
}
|
||||
} else {
|
||||
self.input.pop();
|
||||
self.update_filtered_options();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move_up(&mut self) {
|
||||
@@ -62,19 +217,11 @@ impl NavigationState {
|
||||
self.selected_index = None;
|
||||
return;
|
||||
}
|
||||
|
||||
match self.selected_index {
|
||||
Some(current) => {
|
||||
if current == 0 {
|
||||
self.selected_index = Some(self.filtered_options.len() - 1);
|
||||
} else {
|
||||
self.selected_index = Some(current - 1);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
self.selected_index = Some(self.filtered_options.len() - 1);
|
||||
}
|
||||
}
|
||||
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) {
|
||||
@@ -82,54 +229,101 @@ impl NavigationState {
|
||||
self.selected_index = None;
|
||||
return;
|
||||
}
|
||||
|
||||
match self.selected_index {
|
||||
Some(current) => {
|
||||
if current >= self.filtered_options.len() - 1 {
|
||||
self.selected_index = Some(0);
|
||||
} else {
|
||||
self.selected_index = Some(current + 1);
|
||||
}
|
||||
self.selected_index = match self.selected_index {
|
||||
Some(current) if current >= self.filtered_options.len() - 1 => {
|
||||
Some(0)
|
||||
}
|
||||
None => {
|
||||
self.selected_index = 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())
|
||||
}
|
||||
|
||||
// Returns the string to display in the input line of the palette
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_selected_option(&self) -> Option<&str> {
|
||||
self.selected_index
|
||||
.and_then(|idx| self.filtered_options.get(idx))
|
||||
.map(|(_, option)| option.as_str())
|
||||
// Gets the full path of the currently selected item for TableTree, or input for FindFile
|
||||
pub fn get_selected_value(&self) -> Option<String> {
|
||||
match self.navigation_type {
|
||||
NavigationType::FindFile => {
|
||||
if self.input.is_empty() { None } else { Some(self.input.clone()) }
|
||||
}
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Update self.all_options based on current_path (for TableTree)
|
||||
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();
|
||||
}
|
||||
}
|
||||
// For FindFile, all_options is set once at activation.
|
||||
self.update_filtered_options();
|
||||
}
|
||||
|
||||
// Update self.filtered_options based on self.all_options and self.input
|
||||
fn update_filtered_options(&mut self) {
|
||||
if self.input.is_empty() {
|
||||
self.filtered_options = self.options
|
||||
let filter_text = match self.navigation_type {
|
||||
NavigationType::FindFile => &self.input,
|
||||
NavigationType::TableTree => &self.input, // For TableTree, input is the current segment being typed
|
||||
}
|
||||
.to_lowercase();
|
||||
|
||||
if filter_text.is_empty() {
|
||||
self.filtered_options = self
|
||||
.all_options
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, opt)| (i, opt.clone()))
|
||||
.collect();
|
||||
} else {
|
||||
let input_lower = self.input.to_lowercase();
|
||||
self.filtered_options = self.options
|
||||
self.filtered_options = self
|
||||
.all_options
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, opt)| opt.to_lowercase().contains(&input_lower))
|
||||
.filter(|(_, opt)| opt.to_lowercase().contains(&filter_text))
|
||||
.map(|(i, opt)| (i, opt.clone()))
|
||||
.collect();
|
||||
}
|
||||
|
||||
// Reset selection to first item if current selection is invalid
|
||||
if self.filtered_options.is_empty() {
|
||||
self.selected_index = None;
|
||||
} else if self.selected_index.map_or(true, |idx| idx >= self.filtered_options.len()) {
|
||||
self.selected_index = Some(0);
|
||||
} else {
|
||||
self.selected_index = Some(0); // Default to selecting the first item
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle navigation events within General mode
|
||||
pub async fn handle_command_navigation_event(
|
||||
navigation_state: &mut NavigationState,
|
||||
key: KeyEvent,
|
||||
@@ -142,19 +336,33 @@ pub async fn handle_command_navigation_event(
|
||||
match key.code {
|
||||
KeyCode::Esc => {
|
||||
navigation_state.deactivate();
|
||||
Ok(EventOutcome::Ok("Find File cancelled".to_string()))
|
||||
Ok(EventOutcome::Ok("Navigation cancelled".to_string()))
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
if let Some(selected) = navigation_state.get_selected_option() {
|
||||
let selected = selected.to_string();
|
||||
navigation_state.deactivate();
|
||||
match navigation_state.navigation_type {
|
||||
if let Some(selected_value) = navigation_state.get_selected_value() {
|
||||
let message = match navigation_state.navigation_type {
|
||||
NavigationType::FindFile => {
|
||||
Ok(EventOutcome::Ok(format!("Selected file: {}", selected)))
|
||||
format!("Selected file: {}", selected_value)
|
||||
}
|
||||
NavigationType::TableTree => {
|
||||
format!("Selected table: {}", selected_value)
|
||||
}
|
||||
};
|
||||
navigation_state.deactivate();
|
||||
Ok(EventOutcome::Ok(message))
|
||||
} else {
|
||||
// If nothing is selected but enter is pressed, maybe try to navigate if input is a valid path part?
|
||||
// For now, just indicate no selection or clear.
|
||||
if navigation_state.navigation_type == NavigationType::TableTree && !navigation_state.input.is_empty() {
|
||||
// Try to commit current input as a path segment
|
||||
let current_input_clone = navigation_state.input.clone();
|
||||
navigation_state.add_char('/'); // This will commit the input if valid
|
||||
// Check if path actually changed or options updated
|
||||
if navigation_state.input.is_empty() && !navigation_state.all_options.is_empty() {
|
||||
return Ok(EventOutcome::Ok(format!("Navigated to: {}/", current_input_clone)));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Ok(EventOutcome::Ok("No file selected".to_string()))
|
||||
Ok(EventOutcome::Ok("No valid selection to confirm".to_string()))
|
||||
}
|
||||
}
|
||||
KeyCode::Up => {
|
||||
@@ -174,8 +382,9 @@ pub async fn handle_command_navigation_event(
|
||||
Ok(EventOutcome::Ok(String::new()))
|
||||
}
|
||||
_ => {
|
||||
// Check for general keybindings that might apply to navigation
|
||||
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();
|
||||
@@ -185,13 +394,16 @@ pub async fn handle_command_navigation_event(
|
||||
navigation_state.move_down();
|
||||
Ok(EventOutcome::Ok(String::new()))
|
||||
}
|
||||
"select" => {
|
||||
if let Some(selected) = navigation_state.get_selected_option() {
|
||||
let selected = selected.to_string();
|
||||
"select" => { // This is equivalent to 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),
|
||||
};
|
||||
navigation_state.deactivate();
|
||||
Ok(EventOutcome::Ok(format!("Selected file: {}", selected)))
|
||||
Ok(EventOutcome::Ok(message))
|
||||
} else {
|
||||
Ok(EventOutcome::Ok("No file selected".to_string()))
|
||||
Ok(EventOutcome::Ok("No selection".to_string()))
|
||||
}
|
||||
}
|
||||
_ => Ok(EventOutcome::Ok(String::new())),
|
||||
|
||||
@@ -35,7 +35,9 @@ use crate::modes::{
|
||||
canvas::{edit, read_only, common_mode},
|
||||
general::{navigation, dialog},
|
||||
};
|
||||
use crate::modes::general::command_navigation::{NavigationState, handle_command_navigation_event};
|
||||
use crate::modes::general::command_navigation::{
|
||||
handle_command_navigation_event, NavigationState, TableDependencyGraph,
|
||||
};
|
||||
use crate::functions::modes::navigation::{admin_nav, add_table_nav};
|
||||
use crate::config::binds::key_sequences::KeySequenceTracker;
|
||||
use tokio::sync::mpsc;
|
||||
@@ -580,30 +582,26 @@ impl EventHandler {
|
||||
}
|
||||
|
||||
if let KeyCode::Char(c) = key_code {
|
||||
if c == 'f' {
|
||||
if c == 'f' { // Assuming 'f' is part of the sequence, e.g. ":f" or " f"
|
||||
self.key_sequence_tracker.add_key(key_code);
|
||||
let sequence = self.key_sequence_tracker.get_sequence();
|
||||
|
||||
if config.matches_key_sequence_generalized(&sequence) == Some("find_file_palette_toggle") {
|
||||
if app_state.ui.show_form || app_state.ui.show_intro {
|
||||
let options = vec![
|
||||
"src/main.rs".to_string(),
|
||||
"src/lib.rs".to_string(),
|
||||
"Cargo.toml".to_string(),
|
||||
"README.md".to_string(),
|
||||
"config.toml".to_string(),
|
||||
"src/ui/handlers/ui.rs".to_string(),
|
||||
"src/modes/handlers/event.rs".to_string(),
|
||||
"another_file.txt".to_string(),
|
||||
"yet_another_one.md".to_string(),
|
||||
];
|
||||
self.activate_find_file(options);
|
||||
self.command_mode = false;
|
||||
// Build table graph from profile data
|
||||
let graph = TableDependencyGraph::from_profile_tree(&app_state.profile_tree);
|
||||
|
||||
// Activate navigation with graph
|
||||
self.navigation_state.activate_table_tree(graph);
|
||||
|
||||
self.command_mode = false; // Exit command mode
|
||||
self.command_input.clear();
|
||||
self.command_message = "Find File:".to_string();
|
||||
// Message is set by render_find_file_palette's prompt_prefix
|
||||
self.command_message.clear(); // Clear old command message
|
||||
self.key_sequence_tracker.reset();
|
||||
app_state.update_mode(AppMode::General);
|
||||
return Ok(EventOutcome::Ok("Find File palette activated".to_string()));
|
||||
// ModeManager will derive AppMode::General due to navigation_state.active
|
||||
// app_state.update_mode(AppMode::General); // This will be handled by ModeManager
|
||||
return Ok(EventOutcome::Ok("Table tree palette activated".to_string()));
|
||||
} else {
|
||||
self.key_sequence_tracker.reset();
|
||||
self.command_input.push('f');
|
||||
|
||||
Reference in New Issue
Block a user