From a6262ed5c4bc8e390dcc7b199ce2f7a5c4a2e864 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Sat, 10 Jul 2021 18:00:06 +0900 Subject: [PATCH 1/7] filter databases --- Cargo.lock | 23 +++++ Cargo.toml | 1 + database-tree/src/databasetree.rs | 10 +++ database-tree/src/databasetreeitems.rs | 21 +++++ database-tree/src/item.rs | 24 ++++++ src/components/databases.rs | 115 ++++++++++++++++++++++--- src/handlers/database_list.rs | 12 ++- src/handlers/mod.rs | 18 ++-- 8 files changed, 198 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7739dc3..d1113cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,6 +47,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi 0.3.9", +] + [[package]] name = "autocfg" version = "0.1.7" @@ -197,6 +208,17 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "colored" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" +dependencies = [ + "atty", + "lazy_static", + "winapi 0.3.9", +] + [[package]] name = "copypasta" version = "0.7.1" @@ -515,6 +537,7 @@ version = "0.1.0-alpha.0" dependencies = [ "anyhow", "chrono", + "colored", "copypasta", "crossterm 0.19.0", "database-tree", diff --git a/Cargo.toml b/Cargo.toml index 14b6401..25342bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ strum_macros = "0.21" database-tree = { path = "./database-tree", version = "0.1" } easy-cast = "0.4" copypasta = { version = "0.7.0", default-features = false } +colored = "2" [target.'cfg(any(target_os = "macos", windows))'.dependencies] copypasta = { version = "0.7.0", default-features = false } diff --git a/database-tree/src/databasetree.rs b/database-tree/src/databasetree.rs index 7d126c6..47baf8a 100644 --- a/database-tree/src/databasetree.rs +++ b/database-tree/src/databasetree.rs @@ -44,6 +44,16 @@ impl DatabaseTree { Ok(new_self) } + pub fn filter(&self, filter_text: String) -> Self { + let mut new_self = Self { + items: self.items.filter(filter_text), + selection: Some(0), + visual_selection: None, + }; + new_self.visual_selection = new_self.calc_visual_selection(); + new_self + } + pub fn collapse_but_root(&mut self) { self.items.collapse(0, true); self.items.expand(0, false); diff --git a/database-tree/src/databasetreeitems.rs b/database-tree/src/databasetreeitems.rs index 69db1dd..1ff5fc6 100644 --- a/database-tree/src/databasetreeitems.rs +++ b/database-tree/src/databasetreeitems.rs @@ -19,6 +19,27 @@ impl DatabaseTreeItems { }) } + pub fn filter(&self, filter_text: String) -> Self { + Self { + tree_items: self + .tree_items + .iter() + .filter(|item| item.is_database() || item.is_match(&filter_text)) + .map(|item| { + if item.is_database() { + let mut item = item.clone(); + item.set_collapsed(false); + item.clone() + } else { + let mut item = item.clone(); + item.show(); + item.clone() + } + }) + .collect::>(), + } + } + fn create_items( list: &[Database], collapsed: &BTreeSet<&String>, diff --git a/database-tree/src/item.rs b/database-tree/src/item.rs index 27a237c..0b5e1a8 100644 --- a/database-tree/src/item.rs +++ b/database-tree/src/item.rs @@ -98,6 +98,15 @@ impl DatabaseTreeItem { }) } + pub fn set_collapsed(&mut self, collapsed: bool) { + if let DatabaseTreeItemKind::Database { name, .. } = self.kind() { + self.kind = DatabaseTreeItemKind::Database { + name: name.to_string(), + collapsed, + } + } + } + pub const fn info(&self) -> &TreeItemInfo { &self.info } @@ -128,9 +137,24 @@ impl DatabaseTreeItem { } } + pub fn show(&mut self) { + self.info.visible = true; + } + pub fn hide(&mut self) { self.info.visible = false; } + + pub fn is_match(&self, filter_text: &String) -> bool { + match self.kind.clone() { + DatabaseTreeItemKind::Database { name, .. } => name.contains(filter_text), + DatabaseTreeItemKind::Table { table, .. } => table.name.contains(filter_text), + } + } + + pub fn is_database(&self) -> bool { + self.kind.is_database() + } } impl Eq for DatabaseTreeItem {} diff --git a/src/components/databases.rs b/src/components/databases.rs index 1e38519..d6d6844 100644 --- a/src/components/databases.rs +++ b/src/components/databases.rs @@ -3,16 +3,19 @@ use crate::event::Key; use crate::ui::common_nav; use crate::ui::scrolllist::draw_list_block; use anyhow::Result; +use colored::Colorize; use database_tree::{DatabaseTree, DatabaseTreeItem}; use std::convert::From; use tui::{ backend::Backend, layout::{Constraint, Direction, Layout, Rect}, style::{Color, Style}, + symbols::line::HORIZONTAL, text::Span, widgets::{Block, Borders}, Frame, }; +use unicode_width::UnicodeWidthStr; // ▸ const FOLDER_ICON_COLLAPSED: &str = "\u{25b8}"; @@ -20,20 +23,35 @@ const FOLDER_ICON_COLLAPSED: &str = "\u{25b8}"; const FOLDER_ICON_EXPANDED: &str = "\u{25be}"; const EMPTY_STR: &str = ""; +pub enum FocusBlock { + Filter, + Tree, +} + pub struct DatabasesComponent { pub tree: DatabaseTree, + pub filterd_tree: Option, pub scroll: VerticalScroll, + pub input: String, + pub focus_block: FocusBlock, } impl DatabasesComponent { pub fn new() -> Self { Self { tree: DatabaseTree::default(), + filterd_tree: None, scroll: VerticalScroll::new(), + input: String::new(), + focus_block: FocusBlock::Tree, } } - fn tree_item_to_span(item: &DatabaseTreeItem, selected: bool, width: u16) -> Span<'_> { + pub fn tree(&self) -> &DatabaseTree { + self.filterd_tree.as_ref().unwrap_or(&self.tree) + } + + fn tree_item_to_span(item: DatabaseTreeItem, selected: bool, width: u16) -> Span<'static> { let name = item.kind().name(); let indent = item.info().indent(); @@ -72,21 +90,62 @@ impl DatabasesComponent { } fn draw_tree(&self, f: &mut Frame, area: Rect, focused: bool) { - let tree_height = usize::from(area.height.saturating_sub(2)); - self.tree.visual_selection().map_or_else( + let tree_height = usize::from(area.height.saturating_sub(4)); + let tree = if let Some(tree) = self.filterd_tree.as_ref() { + tree + } else { + &self.tree + }; + tree.visual_selection().map_or_else( || { self.scroll.reset(); }, |selection| { - self.scroll - .update(selection.index, selection.count, tree_height); + self.scroll.update( + selection.index, + selection.count.saturating_sub(2), + tree_height, + ); }, ); - let items = self - .tree + let mut items = tree .iterate(self.scroll.get_top(), tree_height) - .map(|(item, selected)| Self::tree_item_to_span(item, selected, area.width)); + .map(|(item, selected)| Self::tree_item_to_span(item.clone(), selected, area.width)) + .collect::>(); + + items.insert( + 0, + Span::styled( + format!( + "{}", + (0..area.width as usize) + .map(|_| HORIZONTAL) + .collect::>() + .join("") + ), + Style::default(), + ), + ); + items.insert( + 0, + Span::styled( + format!( + "{}{:w$}", + if self.input.is_empty() && matches!(self.focus_block, FocusBlock::Tree) { + "Press / to filter".to_string() + } else { + self.input.clone() + }, + w = area.width as usize + ), + if let FocusBlock::Filter = self.focus_block { + Style::default() + } else { + Style::default().fg(Color::DarkGray) + }, + ), + ); let title = "Databases"; draw_list_block( @@ -101,9 +160,12 @@ impl DatabasesComponent { }) .borders(Borders::ALL) .border_style(Style::default()), - items, + items.into_iter(), ); self.scroll.draw(f, area); + if let FocusBlock::Filter = self.focus_block { + f.set_cursor(area.x + self.input.width() as u16 + 1, area.y + 1) + } } } @@ -123,17 +185,42 @@ impl DrawableComponent for DatabasesComponent { impl Component for DatabasesComponent { fn event(&mut self, key: Key) -> Result<()> { - if tree_nav(&mut self.tree, key) { - return Ok(()); + match key { + Key::Char('/') if matches!(self.focus_block, FocusBlock::Tree) => { + self.focus_block = FocusBlock::Filter + } + Key::Char(c) if matches!(self.focus_block, FocusBlock::Filter) => { + self.input.push(c); + self.filterd_tree = Some(self.tree.filter(self.input.clone())) + } + Key::Delete | Key::Backspace if matches!(self.focus_block, FocusBlock::Filter) => { + self.input.pop(); + if self.input.is_empty() { + self.filterd_tree = None + } else { + self.filterd_tree = Some(self.tree.filter(self.input.clone())) + } + } + Key::Esc if matches!(self.focus_block, FocusBlock::Filter) => { + self.focus_block = FocusBlock::Tree + } + key => tree_nav( + if let Some(tree) = self.filterd_tree.as_mut() { + tree + } else { + &mut self.tree + }, + key, + ), } Ok(()) } } -fn tree_nav(tree: &mut DatabaseTree, key: Key) -> bool { +fn tree_nav(tree: &mut DatabaseTree, key: Key) { if let Some(common_nav) = common_nav(key) { - tree.move_selection(common_nav) + tree.move_selection(common_nav); } else { - false + false; } } diff --git a/src/handlers/database_list.rs b/src/handlers/database_list.rs index c8f4b9f..13a8b9b 100644 --- a/src/handlers/database_list.rs +++ b/src/handlers/database_list.rs @@ -6,11 +6,17 @@ use database_tree::Database; pub async fn handler(key: Key, app: &mut App) -> anyhow::Result<()> { match key { - Key::Esc => app.focus_block = FocusBlock::DabataseList, + Key::Char('c') + if matches!( + app.databases.focus_block, + crate::components::databases::FocusBlock::Tree + ) => + { + app.focus_block = FocusBlock::ConnectionList + } Key::Right => app.focus_block = FocusBlock::Table, - Key::Char('c') => app.focus_block = FocusBlock::ConnectionList, Key::Enter => { - if let Some((table, database)) = app.databases.tree.selected_table() { + if let Some((table, database)) = app.databases.tree().selected_table() { app.focus_block = FocusBlock::Table; let (headers, records) = get_records( &Database { diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index c2fb900..5d9cb8b 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -11,15 +11,15 @@ use crate::event::Key; pub async fn handle_app(key: Key, app: &mut App) -> anyhow::Result<()> { match key { - Key::Char('d') => match app.focus_block { - FocusBlock::Query => (), - _ => app.focus_block = FocusBlock::DabataseList, - }, - Key::Char('r') => match app.focus_block { - FocusBlock::Query => (), - _ => app.focus_block = FocusBlock::Table, - }, - Key::Char('e') => app.focus_block = FocusBlock::Query, + // Key::Char('d') => match app.focus_block { + // FocusBlock::Query => (), + // _ => app.focus_block = FocusBlock::DabataseList, + // }, + // Key::Char('r') => match app.focus_block { + // FocusBlock::Query => (), + // _ => app.focus_block = FocusBlock::Table, + // }, + // Key::Ctrl('e') => app.focus_block = FocusBlock::Query, Key::Esc if app.error.error.is_some() => { app.error.error = None; return Ok(()); From 4c767fe5dde1d6fafdd3035d07fcd98e16b01464 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Sat, 10 Jul 2021 22:39:48 +0900 Subject: [PATCH 2/7] implement utils --- Cargo.lock | 23 -------------- Cargo.toml | 1 - src/components/databases.rs | 54 +++++++++++++++++++++++++++++---- src/handlers/connection_list.rs | 16 +++++----- src/handlers/database_list.rs | 12 +++----- src/handlers/mod.rs | 2 +- 6 files changed, 61 insertions(+), 47 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d1113cf..7739dc3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,17 +47,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi", - "libc", - "winapi 0.3.9", -] - [[package]] name = "autocfg" version = "0.1.7" @@ -208,17 +197,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "colored" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" -dependencies = [ - "atty", - "lazy_static", - "winapi 0.3.9", -] - [[package]] name = "copypasta" version = "0.7.1" @@ -537,7 +515,6 @@ version = "0.1.0-alpha.0" dependencies = [ "anyhow", "chrono", - "colored", "copypasta", "crossterm 0.19.0", "database-tree", diff --git a/Cargo.toml b/Cargo.toml index 25342bf..14b6401 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,6 @@ strum_macros = "0.21" database-tree = { path = "./database-tree", version = "0.1" } easy-cast = "0.4" copypasta = { version = "0.7.0", default-features = false } -colored = "2" [target.'cfg(any(target_os = "macos", windows))'.dependencies] copypasta = { version = "0.7.0", default-features = false } diff --git a/src/components/databases.rs b/src/components/databases.rs index d6d6844..530beff 100644 --- a/src/components/databases.rs +++ b/src/components/databases.rs @@ -3,8 +3,8 @@ use crate::event::Key; use crate::ui::common_nav; use crate::ui::scrolllist::draw_list_block; use anyhow::Result; -use colored::Colorize; -use database_tree::{DatabaseTree, DatabaseTreeItem}; +use database_tree::{Database, DatabaseTree, DatabaseTreeItem}; +use std::collections::BTreeSet; use std::convert::From; use tui::{ backend::Backend, @@ -33,6 +33,7 @@ pub struct DatabasesComponent { pub filterd_tree: Option, pub scroll: VerticalScroll, pub input: String, + pub input_cursor_x: u16, pub focus_block: FocusBlock, } @@ -43,14 +44,39 @@ impl DatabasesComponent { filterd_tree: None, scroll: VerticalScroll::new(), input: String::new(), + input_cursor_x: 0, focus_block: FocusBlock::Tree, } } + pub fn update(&mut self, list: &[Database], collapsed: &BTreeSet<&String>) -> Result<()> { + self.tree = DatabaseTree::new(list, collapsed)?; + self.filterd_tree = None; + self.input = String::new(); + self.input_cursor_x = 0; + Ok(()) + } + + pub fn tree_focused(&self) -> bool { + matches!(self.focus_block, FocusBlock::Tree) + } + pub fn tree(&self) -> &DatabaseTree { self.filterd_tree.as_ref().unwrap_or(&self.tree) } + pub fn increment_input_cursor_x(&mut self) { + if self.input_cursor_x > 0 { + self.input_cursor_x -= 1; + } + } + + pub fn decrement_input_cursor_x(&mut self) { + if self.input_cursor_x < self.input.width() as u16 { + self.input_cursor_x += 1; + } + } + fn tree_item_to_span(item: DatabaseTreeItem, selected: bool, width: u16) -> Span<'static> { let name = item.kind().name(); let indent = item.info().indent(); @@ -164,7 +190,10 @@ impl DatabasesComponent { ); self.scroll.draw(f, area); if let FocusBlock::Filter = self.focus_block { - f.set_cursor(area.x + self.input.width() as u16 + 1, area.y + 1) + f.set_cursor( + area.x + self.input.width() as u16 + 1 - self.input_cursor_x, + area.y + 1, + ) } } } @@ -193,15 +222,28 @@ impl Component for DatabasesComponent { self.input.push(c); self.filterd_tree = Some(self.tree.filter(self.input.clone())) } - Key::Delete | Key::Backspace if matches!(self.focus_block, FocusBlock::Filter) => { - self.input.pop(); + Key::Delete | Key::Backspace => { if self.input.is_empty() { self.filterd_tree = None } else { + if self.input_cursor_x == 0 { + self.input.pop(); + return Ok(()); + } + if self.input.width() - self.input_cursor_x as usize > 0 { + self.input.remove( + self.input + .width() + .saturating_sub(self.input_cursor_x as usize) + .saturating_sub(1), + ); + } self.filterd_tree = Some(self.tree.filter(self.input.clone())) } } - Key::Esc if matches!(self.focus_block, FocusBlock::Filter) => { + Key::Left => self.decrement_input_cursor_x(), + Key::Right => self.increment_input_cursor_x(), + Key::Enter if matches!(self.focus_block, FocusBlock::Filter) => { self.focus_block = FocusBlock::Tree } key => tree_nav( diff --git a/src/handlers/connection_list.rs b/src/handlers/connection_list.rs index 822d5e6..ce18e00 100644 --- a/src/handlers/connection_list.rs +++ b/src/handlers/connection_list.rs @@ -21,23 +21,23 @@ pub async fn handler(key: Key, app: &mut App) -> anyhow::Result<()> { } if let Some(conn) = app.connections.selected_connection() { match &conn.database { - Some(database) => { - app.databases.tree = DatabaseTree::new( + Some(database) => app + .databases + .update( &[Database::new( database.clone(), get_tables(database.clone(), app.pool.as_ref().unwrap()).await?, )], &BTreeSet::new(), ) - .unwrap() - } - None => { - app.databases.tree = DatabaseTree::new( + .unwrap(), + None => app + .databases + .update( get_databases(app.pool.as_ref().unwrap()).await?.as_slice(), &BTreeSet::new(), ) - .unwrap() - } + .unwrap(), } }; } diff --git a/src/handlers/database_list.rs b/src/handlers/database_list.rs index 13a8b9b..72fe08e 100644 --- a/src/handlers/database_list.rs +++ b/src/handlers/database_list.rs @@ -1,4 +1,5 @@ use crate::app::{App, FocusBlock}; +use crate::components::databases::FocusBlock as DatabaseFocusBlock; use crate::components::Component as _; use crate::event::Key; use crate::utils::{get_columns, get_records}; @@ -6,16 +7,11 @@ use database_tree::Database; pub async fn handler(key: Key, app: &mut App) -> anyhow::Result<()> { match key { - Key::Char('c') - if matches!( - app.databases.focus_block, - crate::components::databases::FocusBlock::Tree - ) => - { + Key::Char('c') if app.databases.tree_focused() => { app.focus_block = FocusBlock::ConnectionList } - Key::Right => app.focus_block = FocusBlock::Table, - Key::Enter => { + Key::Right if app.databases.tree_focused() => app.focus_block = FocusBlock::Table, + Key::Enter if app.databases.tree_focused() => { if let Some((table, database)) = app.databases.tree().selected_table() { app.focus_block = FocusBlock::Table; let (headers, records) = get_records( diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 5d9cb8b..d090351 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -19,7 +19,7 @@ pub async fn handle_app(key: Key, app: &mut App) -> anyhow::Result<()> { // FocusBlock::Query => (), // _ => app.focus_block = FocusBlock::Table, // }, - // Key::Ctrl('e') => app.focus_block = FocusBlock::Query, + Key::Ctrl('e') => app.focus_block = FocusBlock::Query, Key::Esc if app.error.error.is_some() => { app.error.error = None; return Ok(()); From a3b9605f3dcac8cfd815b08e0e14eb140a8ecbfd Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Sat, 10 Jul 2021 23:17:52 +0900 Subject: [PATCH 3/7] remove filter when input is empty --- src/components/databases.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/databases.rs b/src/components/databases.rs index 530beff..2ed1dfe 100644 --- a/src/components/databases.rs +++ b/src/components/databases.rs @@ -223,14 +223,10 @@ impl Component for DatabasesComponent { self.filterd_tree = Some(self.tree.filter(self.input.clone())) } Key::Delete | Key::Backspace => { - if self.input.is_empty() { - self.filterd_tree = None - } else { + if !self.input.is_empty() { if self.input_cursor_x == 0 { self.input.pop(); - return Ok(()); - } - if self.input.width() - self.input_cursor_x as usize > 0 { + } else if self.input.width() - self.input_cursor_x as usize > 0 { self.input.remove( self.input .width() @@ -238,7 +234,11 @@ impl Component for DatabasesComponent { .saturating_sub(1), ); } - self.filterd_tree = Some(self.tree.filter(self.input.clone())) + self.filterd_tree = if self.input.is_empty() { + None + } else { + Some(self.tree.filter(self.input.clone())) + } } } Key::Left => self.decrement_input_cursor_x(), From 88e1113db4bb47929b8adc20b845707c14b15c38 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Sat, 10 Jul 2021 23:19:59 +0900 Subject: [PATCH 4/7] remove focus shortcut --- src/handlers/mod.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index d090351..bc7cbc1 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -11,14 +11,6 @@ use crate::event::Key; pub async fn handle_app(key: Key, app: &mut App) -> anyhow::Result<()> { match key { - // Key::Char('d') => match app.focus_block { - // FocusBlock::Query => (), - // _ => app.focus_block = FocusBlock::DabataseList, - // }, - // Key::Char('r') => match app.focus_block { - // FocusBlock::Query => (), - // _ => app.focus_block = FocusBlock::Table, - // }, Key::Ctrl('e') => app.focus_block = FocusBlock::Query, Key::Esc if app.error.error.is_some() => { app.error.error = None; From 7008af9e62b9342b0a4d340e262fb7566e764edc Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Sun, 11 Jul 2021 00:56:05 +0900 Subject: [PATCH 5/7] add tests for databasetree --- database-tree/src/databasetree.rs | 154 ++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) diff --git a/database-tree/src/databasetree.rs b/database-tree/src/databasetree.rs index 47baf8a..844691a 100644 --- a/database-tree/src/databasetree.rs +++ b/database-tree/src/databasetree.rs @@ -292,3 +292,157 @@ impl DatabaseTree { .unwrap_or_default() } } + +#[cfg(test)] +mod test { + use crate::{Database, DatabaseTree, MoveSelection, Table}; + // use pretty_assertions::assert_eq; + use std::collections::BTreeSet; + + impl Table { + fn new(name: String) -> Self { + Table { + name, + create_time: None, + update_time: None, + engine: None, + } + } + } + + #[test] + fn test_selection() { + let items = vec![Database::new( + "a".to_string(), + vec![Table::new("b".to_string())], + )]; + + // a + // b + + let mut tree = DatabaseTree::new(&items, &BTreeSet::new()).unwrap(); + + assert!(tree.move_selection(MoveSelection::Right)); + assert_eq!(tree.selection, Some(0)); + assert!(tree.move_selection(MoveSelection::Down)); + assert_eq!(tree.selection, Some(1)); + } + + #[test] + fn test_selection_skips_collapsed() { + let items = vec![ + Database::new( + "a".to_string(), + vec![Table::new("b".to_string()), Table::new("c".to_string())], + ), + Database::new("d".to_string(), vec![Table::new("e".to_string())]), + ]; + + // a + // b + // c + // d + // e + + let mut tree = DatabaseTree::new(&items, &BTreeSet::new()).unwrap(); + + tree.items.collapse(0, false); + tree.selection = Some(1); + + assert!(tree.move_selection(MoveSelection::Down)); + assert_eq!(tree.selection, Some(3)); + } + + #[test] + fn test_selection_left_collapse() { + let items = vec![Database::new( + "a".to_string(), + vec![Table::new("b".to_string()), Table::new("c".to_string())], + )]; + + // a + // b + // c + + let mut tree = DatabaseTree::new(&items, &BTreeSet::new()).unwrap(); + tree.selection = Some(0); + tree.items.expand(0, false); + + assert!(tree.move_selection(MoveSelection::Left)); + assert_eq!(tree.selection, Some(0)); + assert!(tree.items.tree_items[0].kind().is_database_collapsed()); + assert!(!tree.items.tree_items[1].info().is_visible()); + assert!(!tree.items.tree_items[2].info().is_visible()); + } + + #[test] + fn test_selection_left_parent() { + let items = vec![Database::new( + "a".to_string(), + vec![Table::new("b".to_string()), Table::new("c".to_string())], + )]; + + // a + // b + // c + + let mut tree = DatabaseTree::new(&items, &BTreeSet::new()).unwrap(); + tree.selection = Some(2); + tree.items.expand(0, false); + + assert!(tree.move_selection(MoveSelection::Left)); + assert_eq!(tree.selection, Some(0)); + } + + #[test] + fn test_selection_right_expand() { + let items = vec![Database::new( + "a".to_string(), + vec![Table::new("b".to_string()), Table::new("c".to_string())], + )]; + + // a + // b + // c + + let mut tree = DatabaseTree::new(&items, &BTreeSet::new()).unwrap(); + + tree.selection = Some(0); + + assert!(tree.move_selection(MoveSelection::Right)); + assert_eq!(tree.selection, Some(0)); + assert!(!tree.items.tree_items[0].kind().is_database_collapsed()); + + assert!(tree.move_selection(MoveSelection::Right)); + assert_eq!(tree.selection, Some(1)); + } + + #[test] + fn test_visible_selection() { + let items = vec![ + Database::new( + "a".to_string(), + vec![Table::new("b".to_string()), Table::new("c".to_string())], + ), + Database::new("d".to_string(), vec![Table::new("e".to_string())]), + ]; + + // a + // b + // c + // d + // e + + let mut tree = DatabaseTree::new(&items, &BTreeSet::new()).unwrap(); + tree.items.expand(0, false); + tree.items.expand(3, false); + + tree.selection = Some(0); + assert!(tree.move_selection(MoveSelection::Left)); + assert!(tree.move_selection(MoveSelection::Down)); + let s = tree.visual_selection().unwrap(); + + assert_eq!(s.count, 3); + assert_eq!(s.index, 1); + } +} From fa03b8371024df0982ddd82b4028442d553c7cf0 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Sun, 11 Jul 2021 00:56:15 +0900 Subject: [PATCH 6/7] fix clippy warnings --- database-tree/src/databasetreeitems.rs | 4 ++-- database-tree/src/item.rs | 2 +- src/components/databases.rs | 25 +++++++++---------------- src/components/error.rs | 2 +- src/handlers/connection_list.rs | 2 +- src/handlers/database_list.rs | 1 - 6 files changed, 14 insertions(+), 22 deletions(-) diff --git a/database-tree/src/databasetreeitems.rs b/database-tree/src/databasetreeitems.rs index 1ff5fc6..d0fedb9 100644 --- a/database-tree/src/databasetreeitems.rs +++ b/database-tree/src/databasetreeitems.rs @@ -29,11 +29,11 @@ impl DatabaseTreeItems { if item.is_database() { let mut item = item.clone(); item.set_collapsed(false); - item.clone() + item } else { let mut item = item.clone(); item.show(); - item.clone() + item } }) .collect::>(), diff --git a/database-tree/src/item.rs b/database-tree/src/item.rs index 0b5e1a8..a3cc268 100644 --- a/database-tree/src/item.rs +++ b/database-tree/src/item.rs @@ -145,7 +145,7 @@ impl DatabaseTreeItem { self.info.visible = false; } - pub fn is_match(&self, filter_text: &String) -> bool { + pub fn is_match(&self, filter_text: &str) -> bool { match self.kind.clone() { DatabaseTreeItemKind::Database { name, .. } => name.contains(filter_text), DatabaseTreeItemKind::Table { table, .. } => table.name.contains(filter_text), diff --git a/src/components/databases.rs b/src/components/databases.rs index 2ed1dfe..98c7393 100644 --- a/src/components/databases.rs +++ b/src/components/databases.rs @@ -143,13 +143,10 @@ impl DatabasesComponent { items.insert( 0, Span::styled( - format!( - "{}", - (0..area.width as usize) - .map(|_| HORIZONTAL) - .collect::>() - .join("") - ), + (0..area.width as usize) + .map(|_| HORIZONTAL) + .collect::>() + .join(""), Style::default(), ), ); @@ -200,14 +197,12 @@ impl DatabasesComponent { impl DrawableComponent for DatabasesComponent { fn draw(&mut self, f: &mut Frame, area: Rect, focused: bool) -> Result<()> { - if true { - let chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(100)].as_ref()) - .split(area); + let chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(100)].as_ref()) + .split(area); - self.draw_tree(f, chunks[0], focused); - } + self.draw_tree(f, chunks[0], focused); Ok(()) } } @@ -262,7 +257,5 @@ impl Component for DatabasesComponent { fn tree_nav(tree: &mut DatabaseTree, key: Key) { if let Some(common_nav) = common_nav(key) { tree.move_selection(common_nav); - } else { - false; } } diff --git a/src/components/error.rs b/src/components/error.rs index d83af84..0230849 100644 --- a/src/components/error.rs +++ b/src/components/error.rs @@ -49,7 +49,7 @@ impl DrawableComponent for ErrorComponent { } impl Component for ErrorComponent { - fn event(&mut self, key: Key) -> Result<()> { + fn event(&mut self, _key: Key) -> Result<()> { Ok(()) } } diff --git a/src/handlers/connection_list.rs b/src/handlers/connection_list.rs index ce18e00..ba4dd1a 100644 --- a/src/handlers/connection_list.rs +++ b/src/handlers/connection_list.rs @@ -2,7 +2,7 @@ use crate::app::{App, FocusBlock}; use crate::components::Component as _; use crate::event::Key; use crate::utils::{get_databases, get_tables}; -use database_tree::{Database, DatabaseTree}; +use database_tree::Database; use sqlx::mysql::MySqlPool; use std::collections::BTreeSet; diff --git a/src/handlers/database_list.rs b/src/handlers/database_list.rs index 72fe08e..88ca64c 100644 --- a/src/handlers/database_list.rs +++ b/src/handlers/database_list.rs @@ -1,5 +1,4 @@ use crate::app::{App, FocusBlock}; -use crate::components::databases::FocusBlock as DatabaseFocusBlock; use crate::components::Component as _; use crate::event::Key; use crate::utils::{get_columns, get_records}; From 7b71901c8bf81876109257d543c98e5f057085b6 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Sun, 11 Jul 2021 01:04:26 +0900 Subject: [PATCH 7/7] fix placeholder --- src/components/databases.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/databases.rs b/src/components/databases.rs index 98c7393..6065a4d 100644 --- a/src/components/databases.rs +++ b/src/components/databases.rs @@ -156,7 +156,7 @@ impl DatabasesComponent { format!( "{}{:w$}", if self.input.is_empty() && matches!(self.focus_block, FocusBlock::Tree) { - "Press / to filter".to_string() + " / to filter tables".to_string() } else { self.input.clone() },