Highlight filter keywords (#34)

* highlight filter keywords

* remove src from gitignore

* move tree_nav

* remove unneeded line

* refactor databases component

* fix variable names

* update gobang.gif

* add scroll_up_down_multiple_lines command help

* add tests for tree_item_to_span
pull/36/head
Takayuki Maeda 3 years ago committed by GitHub
parent 07b2b5090f
commit 8f26ffed09
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -52,7 +52,7 @@ impl DatabaseTreeItems {
Self::push_databases(e, &mut items, &mut items_added, collapsed)?;
}
for table in &e.tables {
items.push(DatabaseTreeItem::new_table(e, table)?);
items.push(DatabaseTreeItem::new_table(e, table));
}
}
@ -84,7 +84,7 @@ impl DatabaseTreeItems {
*items_added.entry(database.name.clone()).or_insert(0) += 1;
let is_collapsed = collapsed.contains(&c);
nodes.push(DatabaseTreeItem::new_database(database, is_collapsed)?);
nodes.push(DatabaseTreeItem::new_database(database, is_collapsed));
}
// increase child count in parent node (the above ancenstor ignores the leaf component)

@ -1,6 +1,4 @@
use crate::error::Result;
use crate::{Database, Table};
use std::convert::TryFrom;
#[derive(Debug, Clone)]
pub struct TreeItemInfo {
@ -76,26 +74,24 @@ pub struct DatabaseTreeItem {
}
impl DatabaseTreeItem {
pub fn new_table(database: &Database, table: &Table) -> Result<Self> {
let indent = u8::try_from((3_usize).saturating_sub(2))?;
Ok(Self {
info: TreeItemInfo::new(indent, false),
pub fn new_table(database: &Database, table: &Table) -> Self {
Self {
info: TreeItemInfo::new(1, false),
kind: DatabaseTreeItemKind::Table {
database: database.clone(),
table: table.clone(),
},
})
}
}
pub fn new_database(database: &Database, _collapsed: bool) -> Result<Self> {
Ok(Self {
pub fn new_database(database: &Database, _collapsed: bool) -> Self {
Self {
info: TreeItemInfo::new(0, true),
kind: DatabaseTreeItemKind::Database {
name: database.name.to_string(),
collapsed: true,
},
})
}
}
pub fn set_collapsed(&mut self, collapsed: bool) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 MiB

After

Width:  |  Height:  |  Size: 8.8 MiB

@ -111,6 +111,9 @@ impl App {
let mut res = vec![
CommandInfo::new(command::scroll(&self.config.key_config)),
CommandInfo::new(command::scroll_to_top_bottom(&self.config.key_config)),
CommandInfo::new(command::scroll_up_down_multiple_lines(
&self.config.key_config,
)),
CommandInfo::new(command::move_focus(&self.config.key_config)),
CommandInfo::new(command::filter(&self.config.key_config)),
CommandInfo::new(command::help(&self.config.key_config)),
@ -123,6 +126,81 @@ impl App {
res
}
async fn update_databases(&mut self) -> anyhow::Result<()> {
if let Some(conn) = self.connections.selected_connection() {
if let Some(pool) = self.pool.as_ref() {
pool.close().await;
}
self.pool = Some(Box::new(
MySqlPool::new(conn.database_url().as_str()).await?,
));
let databases = match &conn.database {
Some(database) => vec![Database::new(
database.clone(),
self.pool
.as_ref()
.unwrap()
.get_tables(database.clone())
.await?,
)],
None => self.pool.as_ref().unwrap().get_databases().await?,
};
self.databases.update(databases.as_slice()).unwrap();
self.focus = Focus::DabataseList;
self.record_table.reset();
}
Ok(())
}
async fn update_table(&mut self) -> anyhow::Result<()> {
if let Some((database, table)) = self.databases.tree().selected_table() {
self.focus = Focus::Table;
let (headers, records) = self
.pool
.as_ref()
.unwrap()
.get_records(&database.name, &table.name, 0, None)
.await?;
self.record_table
.update(records, headers, database.clone(), table.clone());
let (headers, records) = self
.pool
.as_ref()
.unwrap()
.get_columns(&database.name, &table.name)
.await?;
self.structure_table
.update(records, headers, database.clone(), table.clone());
self.table_status
.update(self.record_table.len() as u64, table);
}
Ok(())
}
async fn update_record_table(&mut self) -> anyhow::Result<()> {
if let Some((database, table)) = self.databases.tree().selected_table() {
let (headers, records) = self
.pool
.as_ref()
.unwrap()
.get_records(
&database.name,
&table.name,
0,
if self.record_table.filter.input.is_empty() {
None
} else {
Some(self.record_table.filter.input_str())
},
)
.await?;
self.record_table
.update(records, headers, database.clone(), table.clone());
}
Ok(())
}
pub async fn event(&mut self, key: Key) -> anyhow::Result<EventState> {
self.update_commands();
@ -152,28 +230,7 @@ impl App {
}
if key == self.config.key_config.enter {
if let Some(conn) = self.connections.selected_connection() {
if let Some(pool) = self.pool.as_ref() {
pool.close().await;
}
self.pool = Some(Box::new(
MySqlPool::new(conn.database_url().as_str()).await?,
));
let databases = match &conn.database {
Some(database) => vec![Database::new(
database.clone(),
self.pool
.as_ref()
.unwrap()
.get_tables(database.clone())
.await?,
)],
None => self.pool.as_ref().unwrap().get_databases().await?,
};
self.databases.update(databases.as_slice()).unwrap();
self.focus = Focus::DabataseList;
self.record_table.reset();
}
self.update_databases().await?;
return Ok(EventState::Consumed);
}
}
@ -183,32 +240,7 @@ impl App {
}
if key == self.config.key_config.enter && self.databases.tree_focused() {
if let Some((database, table)) = self.databases.tree().selected_table() {
self.focus = Focus::Table;
let (headers, records) = self
.pool
.as_ref()
.unwrap()
.get_records(&database.name, &table.name, 0, None)
.await?;
self.record_table
.update(records, headers, database.clone(), table.clone());
let (headers, records) = self
.pool
.as_ref()
.unwrap()
.get_columns(&database.name, &table.name)
.await?;
self.structure_table.update(
records,
headers,
database.clone(),
table.clone(),
);
self.table_status
.update(self.record_table.len() as u64, table);
}
self.update_table().await?;
return Ok(EventState::Consumed);
}
}
@ -228,25 +260,7 @@ impl App {
if key == self.config.key_config.enter && self.record_table.filter_focused()
{
self.record_table.focus = crate::components::record_table::Focus::Table;
if let Some((database, table)) = self.databases.tree().selected_table()
{
let (headers, records) = self
.pool
.as_ref()
.unwrap()
.get_records(
&database.name.clone(),
&table.name,
0,
if self.record_table.filter.input.is_empty() {
None
} else {
Some(self.record_table.filter.input_str())
},
)
.await?;
self.record_table.update(records, headers, database, table);
}
self.update_record_table().await?;
}
if self.record_table.table.eod {

@ -35,10 +35,17 @@ pub fn scroll(key: &KeyConfig) -> CommandText {
CommandText::new(
format!(
"Scroll up/down/left/right [{},{},{},{}]",
key.scroll_up.to_string(),
key.scroll_down.to_string(),
key.scroll_left.to_string(),
key.scroll_right.to_string()
key.scroll_up, key.scroll_down, key.scroll_left, key.scroll_right
),
CMD_GROUP_GENERAL,
)
}
pub fn scroll_up_down_multiple_lines(key: &KeyConfig) -> CommandText {
CommandText::new(
format!(
"Scroll up/down multiple lines [{},{}]",
key.scroll_up_multiple_lines, key.scroll_down_multiple_lines,
),
CMD_GROUP_GENERAL,
)
@ -48,8 +55,7 @@ pub fn scroll_to_top_bottom(key: &KeyConfig) -> CommandText {
CommandText::new(
format!(
"Scroll to top/bottom [{},{}]",
key.scroll_to_top.to_string(),
key.scroll_to_bottom.to_string(),
key.scroll_to_top, key.scroll_to_bottom,
),
CMD_GROUP_GENERAL,
)
@ -57,20 +63,13 @@ pub fn scroll_to_top_bottom(key: &KeyConfig) -> CommandText {
pub fn expand_collapse(key: &KeyConfig) -> CommandText {
CommandText::new(
format!(
"Expand/Collapse [{},{}]",
key.scroll_right.to_string(),
key.scroll_left.to_string(),
),
format!("Expand/Collapse [{},{}]", key.scroll_right, key.scroll_left,),
CMD_GROUP_DATABASES,
)
}
pub fn filter(key: &KeyConfig) -> CommandText {
CommandText::new(
format!("Filter [{}]", key.filter.to_string()),
CMD_GROUP_GENERAL,
)
CommandText::new(format!("Filter [{}]", key.filter), CMD_GROUP_GENERAL)
}
pub fn move_focus(key: &KeyConfig) -> CommandText {

@ -15,7 +15,7 @@ use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Style},
text::Span,
text::{Span, Spans},
widgets::{Block, Borders, Paragraph},
Frame,
};
@ -28,7 +28,7 @@ const FOLDER_ICON_EXPANDED: &str = "\u{25be}";
const EMPTY_STR: &str = "";
#[derive(PartialEq)]
pub enum FocusBlock {
pub enum Focus {
Filter,
Tree,
}
@ -40,7 +40,7 @@ pub struct DatabasesComponent {
input: Vec<char>,
input_idx: usize,
input_cursor_position: u16,
focus_block: FocusBlock,
focus: Focus,
key_config: KeyConfig,
}
@ -53,7 +53,7 @@ impl DatabasesComponent {
input: Vec::new(),
input_idx: 0,
input_cursor_position: 0,
focus_block: FocusBlock::Tree,
focus: Focus::Tree,
key_config,
}
}
@ -67,18 +67,24 @@ impl DatabasesComponent {
self.filterd_tree = None;
self.input = Vec::new();
self.input_idx = 0;
self.input_cursor_position = 0;
Ok(())
}
pub fn tree_focused(&self) -> bool {
matches!(self.focus_block, FocusBlock::Tree)
matches!(self.focus, Focus::Tree)
}
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> {
fn tree_item_to_span(
item: DatabaseTreeItem,
selected: bool,
width: u16,
filter: Option<String>,
) -> Spans<'static> {
let name = item.kind().name();
let indent = item.info().indent();
@ -88,8 +94,7 @@ impl DatabasesComponent {
format!("{:w$}", " ", w = (indent as usize) * 2)
};
let is_database = item.kind().is_database();
let path_arrow = if is_database {
let arrow = if item.kind().is_database() {
if item.kind().is_database_collapsed() {
FOLDER_ICON_COLLAPSED
} else {
@ -99,21 +104,47 @@ impl DatabasesComponent {
EMPTY_STR
};
let name = format!(
"{}{}{:w$}",
indent_str,
path_arrow,
name,
w = width as usize
);
Span::styled(
name,
if let Some(filter) = filter {
if item.kind().is_table() && name.contains(&filter) {
let (first, rest) = &name.split_at(name.find(filter.as_str()).unwrap_or(0));
let (middle, last) = &rest.split_at(filter.len().clamp(0, rest.len()));
return Spans::from(vec![
Span::styled(
format!("{}{}{}", indent_str, arrow, first),
if selected {
Style::default().bg(Color::Blue)
} else {
Style::default()
},
),
Span::styled(
middle.to_string(),
if selected {
Style::default().bg(Color::Blue).fg(Color::Blue)
} else {
Style::default().fg(Color::Blue)
},
),
Span::styled(
format!("{:w$}", last.to_string(), w = width as usize),
if selected {
Style::default().bg(Color::Blue)
} else {
Style::default()
},
),
]);
}
}
Spans::from(Span::styled(
format!("{}{}{:w$}", indent_str, arrow, name, w = width as usize),
if selected {
Style::default().bg(Color::Blue)
} else {
Style::default()
},
)
))
}
fn draw_tree<B: Backend>(&self, f: &mut Frame<B>, area: Rect, focused: bool) {
@ -139,14 +170,14 @@ impl DatabasesComponent {
let filter = Paragraph::new(Span::styled(
format!(
"{}{:w$}",
if self.input.is_empty() && matches!(self.focus_block, FocusBlock::Tree) {
if self.input.is_empty() && matches!(self.focus, Focus::Tree) {
"Filter tables".to_string()
} else {
self.input_str()
},
w = area.width as usize
),
if let FocusBlock::Filter = self.focus_block {
if let Focus::Filter = self.focus {
Style::default()
} else {
Style::default().fg(Color::DarkGray)
@ -173,11 +204,22 @@ impl DatabasesComponent {
let items = tree
.iterate(self.scroll.get_top(), tree_height)
.map(|(item, selected)| Self::tree_item_to_span(item.clone(), selected, area.width));
.map(|(item, selected)| {
Self::tree_item_to_span(
item.clone(),
selected,
area.width,
if self.input.is_empty() {
None
} else {
Some(self.input_str())
},
)
});
draw_list_block(f, chunks[1], Block::default().borders(Borders::NONE), items);
self.scroll.draw(f, area);
if let FocusBlock::Filter = self.focus_block {
if let Focus::Filter = self.focus {
f.set_cursor(area.x + self.input_cursor_position + 1, area.y + 1)
}
}
@ -202,31 +244,19 @@ impl Component for DatabasesComponent {
fn event(&mut self, key: Key) -> Result<EventState> {
let input_str: String = self.input.iter().collect();
if tree_nav(
if let Some(tree) = self.filterd_tree.as_mut() {
tree
} else {
&mut self.tree
},
key,
&self.key_config,
) {
return Ok(EventState::Consumed);
}
if key == self.key_config.filter && self.focus_block == FocusBlock::Tree {
self.focus_block = FocusBlock::Filter;
if key == self.key_config.filter && self.focus == Focus::Tree {
self.focus = Focus::Filter;
return Ok(EventState::Consumed);
}
match key {
Key::Char(c) if self.focus_block == FocusBlock::Filter => {
Key::Char(c) if self.focus == Focus::Filter => {
self.input.insert(self.input_idx, c);
self.input_idx += 1;
self.input_cursor_position += compute_character_width(c);
self.filterd_tree = Some(self.tree.filter(self.input_str()));
return Ok(EventState::Consumed);
}
Key::Delete | Key::Backspace if matches!(self.focus_block, FocusBlock::Filter) => {
Key::Delete | Key::Backspace if matches!(self.focus, Focus::Filter) => {
if input_str.width() > 0 {
if !self.input.is_empty() && self.input_idx > 0 {
let last_c = self.input.remove(self.input_idx - 1);
@ -242,7 +272,7 @@ impl Component for DatabasesComponent {
return Ok(EventState::Consumed);
}
}
Key::Left if matches!(self.focus_block, FocusBlock::Filter) => {
Key::Left if matches!(self.focus, Focus::Filter) => {
if !self.input.is_empty() && self.input_idx > 0 {
self.input_idx -= 1;
self.input_cursor_position = self
@ -258,7 +288,7 @@ impl Component for DatabasesComponent {
}
return Ok(EventState::Consumed);
}
Key::Right if matches!(self.focus_block, FocusBlock::Filter) => {
Key::Right if matches!(self.focus, Focus::Filter) => {
if self.input_idx < self.input.len() {
let next_c = self.input[self.input_idx];
self.input_idx += 1;
@ -273,11 +303,23 @@ impl Component for DatabasesComponent {
}
return Ok(EventState::Consumed);
}
Key::Enter if matches!(self.focus_block, FocusBlock::Filter) => {
self.focus_block = FocusBlock::Tree;
Key::Enter if matches!(self.focus, Focus::Filter) => {
self.focus = Focus::Tree;
return Ok(EventState::Consumed);
}
_ => (),
key => {
if tree_nav(
if let Some(tree) = self.filterd_tree.as_mut() {
tree
} else {
&mut self.tree
},
key,
&self.key_config,
) {
return Ok(EventState::Consumed);
}
}
}
Ok(EventState::NotConsumed)
}
@ -290,3 +332,162 @@ fn tree_nav(tree: &mut DatabaseTree, key: Key, key_config: &KeyConfig) -> bool {
false
}
}
#[cfg(test)]
mod test {
use super::{Color, Database, DatabaseTreeItem, DatabasesComponent, Span, Spans, Style};
use database_tree::Table;
#[test]
fn test_tree_database_tree_item_to_span() {
const WIDTH: u16 = 10;
assert_eq!(
DatabasesComponent::tree_item_to_span(
DatabaseTreeItem::new_database(
&Database {
name: "foo".to_string(),
tables: Vec::new(),
},
false,
),
false,
WIDTH,
None,
),
Spans::from(vec![Span::raw(format!(
"\u{25b8}{:w$}",
"foo",
w = WIDTH as usize
))])
);
assert_eq!(
DatabasesComponent::tree_item_to_span(
DatabaseTreeItem::new_database(
&Database {
name: "foo".to_string(),
tables: Vec::new(),
},
false,
),
true,
WIDTH,
None,
),
Spans::from(vec![Span::styled(
format!("\u{25b8}{:w$}", "foo", w = WIDTH as usize),
Style::default().bg(Color::Blue)
)])
);
}
#[test]
fn test_tree_table_tree_item_to_span() {
const WIDTH: u16 = 10;
assert_eq!(
DatabasesComponent::tree_item_to_span(
DatabaseTreeItem::new_table(
&Database {
name: "foo".to_string(),
tables: Vec::new(),
},
&Table {
name: "bar".to_string(),
create_time: None,
update_time: None,
engine: None,
},
),
false,
WIDTH,
None,
),
Spans::from(vec![Span::raw(format!(
" {:w$}",
"bar",
w = WIDTH as usize
))])
);
assert_eq!(
DatabasesComponent::tree_item_to_span(
DatabaseTreeItem::new_table(
&Database {
name: "foo".to_string(),
tables: Vec::new(),
},
&Table {
name: "bar".to_string(),
create_time: None,
update_time: None,
engine: None,
},
),
true,
WIDTH,
None,
),
Spans::from(Span::styled(
format!(" {:w$}", "bar", w = WIDTH as usize),
Style::default().bg(Color::Blue),
))
);
}
#[test]
fn test_filterd_tree_item_to_span() {
const WIDTH: u16 = 10;
assert_eq!(
DatabasesComponent::tree_item_to_span(
DatabaseTreeItem::new_table(
&Database {
name: "foo".to_string(),
tables: Vec::new(),
},
&Table {
name: "barbaz".to_string(),
create_time: None,
update_time: None,
engine: None,
},
),
false,
WIDTH,
Some("rb".to_string()),
),
Spans::from(vec![
Span::raw(format!(" {}", "ba")),
Span::styled("rb", Style::default().fg(Color::Blue)),
Span::raw(format!("{:w$}", "az", w = WIDTH as usize))
])
);
assert_eq!(
DatabasesComponent::tree_item_to_span(
DatabaseTreeItem::new_table(
&Database {
name: "foo".to_string(),
tables: Vec::new(),
},
&Table {
name: "barbaz".to_string(),
create_time: None,
update_time: None,
engine: None,
},
),
true,
WIDTH,
Some("rb".to_string()),
),
Spans::from(vec![
Span::styled(format!(" {}", "ba"), Style::default().bg(Color::Blue)),
Span::styled("rb", Style::default().bg(Color::Blue).fg(Color::Blue)),
Span::styled(
format!("{:w$}", "az", w = WIDTH as usize),
Style::default().bg(Color::Blue)
)
])
);
}
}

@ -4,26 +4,23 @@ use tui::{
buffer::Buffer,
layout::Rect,
style::Style,
text::Span,
text::Spans,
widgets::{Block, List, ListItem, Widget},
Frame,
};
///
struct ScrollableList<'b, L>
where
L: Iterator<Item = Span<'b>>,
L: Iterator<Item = Spans<'b>>,
{
block: Option<Block<'b>>,
/// Items to be displayed
items: L,
/// Base style of the widget
style: Style,
}
impl<'b, L> ScrollableList<'b, L>
where
L: Iterator<Item = Span<'b>>,
L: Iterator<Item = Spans<'b>>,
{
fn new(items: L) -> Self {
Self {
@ -41,10 +38,9 @@ where
impl<'b, L> Widget for ScrollableList<'b, L>
where
L: Iterator<Item = Span<'b>>,
L: Iterator<Item = Spans<'b>>,
{
fn render(self, area: Rect, buf: &mut Buffer) {
// Render items
List::new(self.items.map(ListItem::new).collect::<Vec<ListItem>>())
.block(self.block.unwrap_or_default())
.style(self.style)
@ -54,7 +50,7 @@ where
pub fn draw_list_block<'b, B: Backend, L>(f: &mut Frame<B>, r: Rect, block: Block<'b>, items: L)
where
L: Iterator<Item = Span<'b>>,
L: Iterator<Item = Spans<'b>>,
{
let list = ScrollableList::new(items).block(block);
f.render_widget(list, r);

Loading…
Cancel
Save