Filter tables (#20)

* filter databases

* implement utils

* remove filter when input is empty

* remove focus shortcut

* add tests for databasetree

* fix clippy warnings

* fix placeholder
pull/22/head
Takayuki Maeda 3 years ago committed by GitHub
parent a9f918b53d
commit 4486fc707d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -44,6 +44,16 @@ impl DatabaseTree {
Ok(new_self) 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) { pub fn collapse_but_root(&mut self) {
self.items.collapse(0, true); self.items.collapse(0, true);
self.items.expand(0, false); self.items.expand(0, false);
@ -282,3 +292,157 @@ impl DatabaseTree {
.unwrap_or_default() .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);
}
}

@ -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
} else {
let mut item = item.clone();
item.show();
item
}
})
.collect::<Vec<DatabaseTreeItem>>(),
}
}
fn create_items( fn create_items(
list: &[Database], list: &[Database],
collapsed: &BTreeSet<&String>, collapsed: &BTreeSet<&String>,

@ -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 { pub const fn info(&self) -> &TreeItemInfo {
&self.info &self.info
} }
@ -128,9 +137,24 @@ impl DatabaseTreeItem {
} }
} }
pub fn show(&mut self) {
self.info.visible = true;
}
pub fn hide(&mut self) { pub fn hide(&mut self) {
self.info.visible = false; self.info.visible = false;
} }
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),
}
}
pub fn is_database(&self) -> bool {
self.kind.is_database()
}
} }
impl Eq for DatabaseTreeItem {} impl Eq for DatabaseTreeItem {}

@ -3,16 +3,19 @@ use crate::event::Key;
use crate::ui::common_nav; use crate::ui::common_nav;
use crate::ui::scrolllist::draw_list_block; use crate::ui::scrolllist::draw_list_block;
use anyhow::Result; use anyhow::Result;
use database_tree::{DatabaseTree, DatabaseTreeItem}; use database_tree::{Database, DatabaseTree, DatabaseTreeItem};
use std::collections::BTreeSet;
use std::convert::From; use std::convert::From;
use tui::{ use tui::{
backend::Backend, backend::Backend,
layout::{Constraint, Direction, Layout, Rect}, layout::{Constraint, Direction, Layout, Rect},
style::{Color, Style}, style::{Color, Style},
symbols::line::HORIZONTAL,
text::Span, text::Span,
widgets::{Block, Borders}, widgets::{Block, Borders},
Frame, Frame,
}; };
use unicode_width::UnicodeWidthStr;
// ▸ // ▸
const FOLDER_ICON_COLLAPSED: &str = "\u{25b8}"; const FOLDER_ICON_COLLAPSED: &str = "\u{25b8}";
@ -20,20 +23,61 @@ const FOLDER_ICON_COLLAPSED: &str = "\u{25b8}";
const FOLDER_ICON_EXPANDED: &str = "\u{25be}"; const FOLDER_ICON_EXPANDED: &str = "\u{25be}";
const EMPTY_STR: &str = ""; const EMPTY_STR: &str = "";
pub enum FocusBlock {
Filter,
Tree,
}
pub struct DatabasesComponent { pub struct DatabasesComponent {
pub tree: DatabaseTree, pub tree: DatabaseTree,
pub filterd_tree: Option<DatabaseTree>,
pub scroll: VerticalScroll, pub scroll: VerticalScroll,
pub input: String,
pub input_cursor_x: u16,
pub focus_block: FocusBlock,
} }
impl DatabasesComponent { impl DatabasesComponent {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
tree: DatabaseTree::default(), tree: DatabaseTree::default(),
filterd_tree: None,
scroll: VerticalScroll::new(), 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<'_> { fn tree_item_to_span(item: DatabaseTreeItem, selected: bool, width: u16) -> Span<'static> {
let name = item.kind().name(); let name = item.kind().name();
let indent = item.info().indent(); let indent = item.info().indent();
@ -72,21 +116,59 @@ impl DatabasesComponent {
} }
fn draw_tree<B: Backend>(&self, f: &mut Frame<B>, area: Rect, focused: bool) { fn draw_tree<B: Backend>(&self, f: &mut Frame<B>, area: Rect, focused: bool) {
let tree_height = usize::from(area.height.saturating_sub(2)); let tree_height = usize::from(area.height.saturating_sub(4));
self.tree.visual_selection().map_or_else( let tree = if let Some(tree) = self.filterd_tree.as_ref() {
tree
} else {
&self.tree
};
tree.visual_selection().map_or_else(
|| { || {
self.scroll.reset(); self.scroll.reset();
}, },
|selection| { |selection| {
self.scroll self.scroll.update(
.update(selection.index, selection.count, tree_height); selection.index,
selection.count.saturating_sub(2),
tree_height,
);
}, },
); );
let items = self let mut items = tree
.tree
.iterate(self.scroll.get_top(), tree_height) .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::<Vec<Span>>();
items.insert(
0,
Span::styled(
(0..area.width as usize)
.map(|_| HORIZONTAL)
.collect::<Vec<&str>>()
.join(""),
Style::default(),
),
);
items.insert(
0,
Span::styled(
format!(
"{}{:w$}",
if self.input.is_empty() && matches!(self.focus_block, FocusBlock::Tree) {
" / to filter tables".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"; let title = "Databases";
draw_list_block( draw_list_block(
@ -101,39 +183,79 @@ impl DatabasesComponent {
}) })
.borders(Borders::ALL) .borders(Borders::ALL)
.border_style(Style::default()), .border_style(Style::default()),
items, items.into_iter(),
); );
self.scroll.draw(f, area); self.scroll.draw(f, area);
if let FocusBlock::Filter = self.focus_block {
f.set_cursor(
area.x + self.input.width() as u16 + 1 - self.input_cursor_x,
area.y + 1,
)
}
} }
} }
impl DrawableComponent for DatabasesComponent { impl DrawableComponent for DatabasesComponent {
fn draw<B: Backend>(&mut self, f: &mut Frame<B>, area: Rect, focused: bool) -> Result<()> { fn draw<B: Backend>(&mut self, f: &mut Frame<B>, area: Rect, focused: bool) -> Result<()> {
if true { let chunks = Layout::default()
let chunks = Layout::default() .direction(Direction::Horizontal)
.direction(Direction::Horizontal) .constraints([Constraint::Percentage(100)].as_ref())
.constraints([Constraint::Percentage(100)].as_ref()) .split(area);
.split(area);
self.draw_tree(f, chunks[0], focused); self.draw_tree(f, chunks[0], focused);
}
Ok(()) Ok(())
} }
} }
impl Component for DatabasesComponent { impl Component for DatabasesComponent {
fn event(&mut self, key: Key) -> Result<()> { fn event(&mut self, key: Key) -> Result<()> {
if tree_nav(&mut self.tree, key) { match key {
return Ok(()); 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 !self.input.is_empty() {
if self.input_cursor_x == 0 {
self.input.pop();
} else 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 = if self.input.is_empty() {
None
} else {
Some(self.tree.filter(self.input.clone()))
}
}
}
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(
if let Some(tree) = self.filterd_tree.as_mut() {
tree
} else {
&mut self.tree
},
key,
),
} }
Ok(()) 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) { if let Some(common_nav) = common_nav(key) {
tree.move_selection(common_nav) tree.move_selection(common_nav);
} else {
false
} }
} }

@ -49,7 +49,7 @@ impl DrawableComponent for ErrorComponent {
} }
impl Component for ErrorComponent { impl Component for ErrorComponent {
fn event(&mut self, key: Key) -> Result<()> { fn event(&mut self, _key: Key) -> Result<()> {
Ok(()) Ok(())
} }
} }

@ -2,7 +2,7 @@ use crate::app::{App, FocusBlock};
use crate::components::Component as _; use crate::components::Component as _;
use crate::event::Key; use crate::event::Key;
use crate::utils::{get_databases, get_tables}; use crate::utils::{get_databases, get_tables};
use database_tree::{Database, DatabaseTree}; use database_tree::Database;
use sqlx::mysql::MySqlPool; use sqlx::mysql::MySqlPool;
use std::collections::BTreeSet; use std::collections::BTreeSet;
@ -21,23 +21,23 @@ pub async fn handler(key: Key, app: &mut App) -> anyhow::Result<()> {
} }
if let Some(conn) = app.connections.selected_connection() { if let Some(conn) = app.connections.selected_connection() {
match &conn.database { match &conn.database {
Some(database) => { Some(database) => app
app.databases.tree = DatabaseTree::new( .databases
.update(
&[Database::new( &[Database::new(
database.clone(), database.clone(),
get_tables(database.clone(), app.pool.as_ref().unwrap()).await?, get_tables(database.clone(), app.pool.as_ref().unwrap()).await?,
)], )],
&BTreeSet::new(), &BTreeSet::new(),
) )
.unwrap() .unwrap(),
} None => app
None => { .databases
app.databases.tree = DatabaseTree::new( .update(
get_databases(app.pool.as_ref().unwrap()).await?.as_slice(), get_databases(app.pool.as_ref().unwrap()).await?.as_slice(),
&BTreeSet::new(), &BTreeSet::new(),
) )
.unwrap() .unwrap(),
}
} }
}; };
} }

@ -6,11 +6,12 @@ use database_tree::Database;
pub async fn handler(key: Key, app: &mut App) -> anyhow::Result<()> { pub async fn handler(key: Key, app: &mut App) -> anyhow::Result<()> {
match key { match key {
Key::Esc => app.focus_block = FocusBlock::DabataseList, Key::Char('c') if app.databases.tree_focused() => {
Key::Right => app.focus_block = FocusBlock::Table, app.focus_block = FocusBlock::ConnectionList
Key::Char('c') => app.focus_block = FocusBlock::ConnectionList, }
Key::Enter => { Key::Right if app.databases.tree_focused() => app.focus_block = FocusBlock::Table,
if let Some((table, database)) = app.databases.tree.selected_table() { Key::Enter if app.databases.tree_focused() => {
if let Some((table, database)) = app.databases.tree().selected_table() {
app.focus_block = FocusBlock::Table; app.focus_block = FocusBlock::Table;
let (headers, records) = get_records( let (headers, records) = get_records(
&Database { &Database {

@ -11,15 +11,7 @@ use crate::event::Key;
pub async fn handle_app(key: Key, app: &mut App) -> anyhow::Result<()> { pub async fn handle_app(key: Key, app: &mut App) -> anyhow::Result<()> {
match key { match key {
Key::Char('d') => match app.focus_block { Key::Ctrl('e') => app.focus_block = FocusBlock::Query,
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::Esc if app.error.error.is_some() => { Key::Esc if app.error.error.is_some() => {
app.error.error = None; app.error.error = None;
return Ok(()); return Ok(());

Loading…
Cancel
Save