From 187462217249db3714054c078194b95d763ffe85 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Tue, 15 Feb 2022 12:44:45 +0900 Subject: [PATCH] add column names to completion candidates --- src/app.rs | 25 ++++- src/components/completion.rs | 175 +++++++++++++++++++++++++++++---- src/components/databases.rs | 12 +-- src/components/sql_editor.rs | 96 ++++++++++++++++-- src/components/table.rs | 44 +++++++-- src/components/table_filter.rs | 4 +- src/config.rs | 2 +- 7 files changed, 309 insertions(+), 49 deletions(-) diff --git a/src/app.rs b/src/app.rs index abfc7b1..0bce3e7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -12,6 +12,7 @@ use crate::{ }, config::Config, }; +use database_tree::Database; use tui::{ backend::Backend, layout::{Constraint, Direction, Layout, Rect}, @@ -152,9 +153,19 @@ impl App { SqlitePool::new(conn.database_url()?.as_str()).await?, )) }; - self.databases - .update(conn, self.pool.as_ref().unwrap()) - .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.sql_editor.update(&databases); + self.databases.update(databases).await?; self.focus = Focus::DabataseList; self.record_table.reset(); self.tab.reset(); @@ -303,7 +314,13 @@ impl App { .is_consumed() { return Ok(EventState::Consumed); - }; + } + + if key == self.config.key_config.copy { + if let Some(text) = self.sql_editor.selected_cells() { + copy_to_clipboard(text.as_str())? + } + } } Tab::Properties => { if self.properties.event(key)?.is_consumed() { diff --git a/src/components/completion.rs b/src/components/completion.rs index 848c355..1a9565e 100644 --- a/src/components/completion.rs +++ b/src/components/completion.rs @@ -1,5 +1,6 @@ use super::{Component, EventState, MovableComponent}; use crate::components::command::CommandInfo; +use crate::config::DatabaseType; use crate::config::KeyConfig; use crate::event::Key; use anyhow::Result; @@ -12,8 +13,142 @@ use tui::{ }; const RESERVED_WORDS_IN_WHERE_CLAUSE: &[&str] = &["IN", "AND", "OR", "NOT", "NULL", "IS"]; -const ALL_RESERVED_WORDS: &[&str] = &[ - "IN", "AND", "OR", "NOT", "NULL", "IS", "SELECT", "UPDATE", "DELETE", "FROM", "LIMIT", "WHERE", + +pub const MYSQL_KEYWORDS: &[&str] = &[ + "ACCESS", + "ADD", + "ALL", + "ALTER TABLE", + "AND", + "ANY", + "AS", + "ASC", + "AUTO_INCREMENT", + "BEFORE", + "BEGIN", + "BETWEEN", + "BIGINT", + "BINARY", + "BY", + "CASE", + "CHANGE MASTER TO", + "CHAR", + "CHARACTER SET", + "CHECK", + "COLLATE", + "COLUMN", + "COMMENT", + "COMMIT", + "CONSTRAINT", + "CREATE", + "CURRENT", + "CURRENT_TIMESTAMP", + "DATABASE", + "DATE", + "DECIMAL", + "DEFAULT", + "DELETE FROM", + "DESC", + "DESCRIBE", + "DROP", + "ELSE", + "END", + "ENGINE", + "ESCAPE", + "EXISTS", + "FILE", + "FLOAT", + "FOR", + "FOREIGN KEY", + "FORMAT", + "FROM", + "FULL", + "FUNCTION", + "GRANT", + "GROUP BY", + "HAVING", + "HOST", + "IDENTIFIED", + "IN", + "INCREMENT", + "INDEX", + "INSERT INTO", + "INT", + "INTEGER", + "INTERVAL", + "INTO", + "IS", + "JOIN", + "KEY", + "LEFT", + "LEVEL", + "LIKE", + "LIMIT", + "LOCK", + "LOGS", + "LONG", + "MASTER", + "MEDIUMINT", + "MODE", + "MODIFY", + "NOT", + "NULL", + "NUMBER", + "OFFSET", + "ON", + "OPTION", + "OR", + "ORDER BY", + "OUTER", + "OWNER", + "PASSWORD", + "PORT", + "PRIMARY", + "PRIVILEGES", + "PROCESSLIST", + "PURGE", + "REFERENCES", + "REGEXP", + "RENAME", + "REPAIR", + "RESET", + "REVOKE", + "RIGHT", + "ROLLBACK", + "ROW", + "ROWS", + "ROW_FORMAT", + "SAVEPOINT", + "SELECT", + "SESSION", + "SET", + "SHARE", + "SHOW", + "SLAVE", + "SMALLINT", + "SMALLINT", + "START", + "STOP", + "TABLE", + "THEN", + "TINYINT", + "TO", + "TRANSACTION", + "TRIGGER", + "TRUNCATE", + "UNION", + "UNIQUE", + "UNSIGNED", + "UPDATE", + "USE", + "USER", + "USING", + "VALUES", + "VARCHAR", + "VIEW", + "WHEN", + "WHERE", + "WITH", ]; pub struct CompletionComponent { @@ -24,18 +159,18 @@ pub struct CompletionComponent { } impl CompletionComponent { - pub fn new(key_config: KeyConfig, word: impl Into, all: bool) -> Self { + pub fn new( + key_config: KeyConfig, + word: impl Into, + database_type: DatabaseType, + ) -> Self { Self { key_config, state: ListState::default(), word: word.into(), - candidates: if all { - ALL_RESERVED_WORDS.iter().map(|w| w.to_string()).collect() - } else { - RESERVED_WORDS_IN_WHERE_CLAUSE - .iter() - .map(|w| w.to_string()) - .collect() + candidates: match database_type { + DatabaseType::MySql => MYSQL_KEYWORDS.iter().map(|w| w.to_string()).collect(), + _ => MYSQL_KEYWORDS.iter().map(|w| w.to_string()).collect(), }, } } @@ -46,10 +181,14 @@ impl CompletionComponent { self.state.select(Some(0)) } + pub fn add_candidates(&mut self, candidates: Vec) { + self.candidates.extend(candidates) + } + fn next(&mut self) { let i = match self.state.selected() { Some(i) => { - if i >= self.filterd_candidates().count() - 1 { + if i >= self.filterd_candidates().count().saturating_sub(1) { 0 } else { i + 1 @@ -64,9 +203,9 @@ impl CompletionComponent { let i = match self.state.selected() { Some(i) => { if i == 0 { - self.filterd_candidates().count() - 1 + self.filterd_candidates().count().saturating_sub(1) } else { - i - 1 + i.saturating_sub(1) } } None => 0, @@ -150,12 +289,12 @@ impl Component for CompletionComponent { #[cfg(test)] mod test { - use super::{CompletionComponent, KeyConfig}; + use super::{CompletionComponent, DatabaseType, KeyConfig}; #[test] fn test_filterd_candidates_lowercase() { assert_eq!( - CompletionComponent::new(KeyConfig::default(), "an", false) + CompletionComponent::new(KeyConfig::default(), "an", DatabaseType::MySql) .filterd_candidates() .collect::>(), vec![&"AND".to_string()] @@ -165,7 +304,7 @@ mod test { #[test] fn test_filterd_candidates_uppercase() { assert_eq!( - CompletionComponent::new(KeyConfig::default(), "AN", false) + CompletionComponent::new(KeyConfig::default(), "AN", DatabaseType::MySql) .filterd_candidates() .collect::>(), vec![&"AND".to_string()] @@ -175,14 +314,14 @@ mod test { #[test] fn test_filterd_candidates_multiple_candidates() { assert_eq!( - CompletionComponent::new(KeyConfig::default(), "n", false) + CompletionComponent::new(KeyConfig::default(), "n", DatabaseType::MySql) .filterd_candidates() .collect::>(), vec![&"NOT".to_string(), &"NULL".to_string()] ); assert_eq!( - CompletionComponent::new(KeyConfig::default(), "N", false) + CompletionComponent::new(KeyConfig::default(), "N", DatabaseType::MySql) .filterd_candidates() .collect::>(), vec![&"NOT".to_string(), &"NULL".to_string()] diff --git a/src/components/databases.rs b/src/components/databases.rs index e4af306..36b8e0b 100644 --- a/src/components/databases.rs +++ b/src/components/databases.rs @@ -3,8 +3,7 @@ use super::{ EventState, }; use crate::components::command::{self, CommandInfo}; -use crate::config::{Connection, KeyConfig}; -use crate::database::Pool; +use crate::config::KeyConfig; use crate::event::Key; use crate::ui::common_nav; use crate::ui::scrolllist::draw_list_block; @@ -54,14 +53,7 @@ impl DatabasesComponent { } } - pub async fn update(&mut self, connection: &Connection, pool: &Box) -> Result<()> { - let databases = match &connection.database { - Some(database) => vec![Database::new( - database.clone(), - pool.get_tables(database.clone()).await?, - )], - None => pool.get_databases().await?, - }; + pub async fn update(&mut self, databases: Vec) -> Result<()> { self.tree = DatabaseTree::new(databases.as_slice(), &BTreeSet::new())?; self.filterd_tree = None; self.filter.reset(); diff --git a/src/components/sql_editor.rs b/src/components/sql_editor.rs index 50e53cf..cc3eeec 100644 --- a/src/components/sql_editor.rs +++ b/src/components/sql_editor.rs @@ -1,18 +1,22 @@ +use super::completion::MYSQL_KEYWORDS; use super::{ compute_character_width, CompletionComponent, Component, EventState, MovableComponent, StatefulDrawableComponent, TableComponent, }; use crate::components::command::CommandInfo; +use crate::config::DatabaseType; use crate::config::KeyConfig; use crate::database::{ExecuteResult, Pool}; use crate::event::Key; use crate::ui::stateful_paragraph::{ParagraphState, StatefulParagraph}; use anyhow::Result; use async_trait::async_trait; +use database_tree::{Child, Database}; use tui::{ backend::Backend, layout::{Constraint, Direction, Layout, Rect}, style::{Color, Style}, + text::{Span, Spans}, widgets::{Block, Borders, Paragraph, Wrap}, Frame, }; @@ -51,8 +55,8 @@ impl SqlEditorComponent { input: Vec::new(), input_idx: 0, input_cursor_position_x: 0, - table: TableComponent::new(key_config.clone()), - completion: CompletionComponent::new(key_config.clone(), "", true), + table: TableComponent::new_without_title(key_config.clone()), + completion: CompletionComponent::new(key_config.clone(), "", DatabaseType::MySql), focus: Focus::Editor, paragraph_state: ParagraphState::default(), query_result: None, @@ -138,6 +142,83 @@ impl SqlEditorComponent { } Ok(EventState::NotConsumed) } + + fn input_to_span(&self) -> Spans<'static> { + let mut spans = self + .input + .iter() + .collect::() + .clone() + .split(' ') + .map(|i| { + if MYSQL_KEYWORDS.contains(&i) { + vec![ + Span::styled(i.to_string(), Style::default().fg(Color::Blue)), + Span::from(" "), + ] + } else { + vec![Span::from(i.to_string()), Span::from(" ")] + } + }) + .flatten() + .collect::>(); + spans.pop(); + Spans::from(spans) + } + + pub fn update(&mut self, databases: &Vec) { + self.completion.add_candidates( + databases + .iter() + .map(|db| { + db.children + .iter() + .map(|c| match c { + Child::Table(table) => format!("{}.{}", db.name, table.name), + Child::Schema(schema) => schema + .tables + .iter() + .map(|table| format!("{}.{}.{}", db.name, schema.name, table.name)) + .collect(), + }) + .collect::>() + }) + .flatten() + .collect::>(), + ); + self.completion.add_candidates( + databases + .iter() + .map(|db| db.name.to_string()) + .collect::>(), + ); + self.completion.add_candidates( + databases + .iter() + .map(|db| { + db.children + .iter() + .map(|c| match c { + Child::Table(table) => table.name.to_string(), + Child::Schema(schema) => schema + .tables + .iter() + .map(|table| table.name.to_string()) + .collect(), + }) + .collect::>() + }) + .flatten() + .collect::>(), + ); + } + + pub fn selected_cells(&self) -> Option { + if !matches!(self.focus, Focus::Table) { + return None; + } + self.table.selected_cells() + } } impl StatefulDrawableComponent for SqlEditorComponent { @@ -151,7 +232,7 @@ impl StatefulDrawableComponent for SqlEditorComponent { }) .split(area); - let editor = StatefulParagraph::new(self.input.iter().collect::()) + let editor = StatefulParagraph::new(self.input_to_span()) .wrap(Wrap { trim: true }) .block(Block::default().borders(Borders::ALL)); @@ -208,9 +289,11 @@ impl Component for SqlEditorComponent { let input_str: String = self.input.iter().collect(); if key == self.key_config.focus_above && matches!(self.focus, Focus::Table) { - self.focus = Focus::Editor - } else if key == self.key_config.enter { - return self.complete(); + self.focus = Focus::Editor; + return Ok(EventState::Consumed); + } else if key == self.key_config.enter && self.completion.selected_candidate().is_some() { + self.complete()?; + return Ok(EventState::Consumed); } match key { @@ -253,6 +336,7 @@ impl Component for SqlEditorComponent { return Ok(EventState::Consumed); } key if matches!(self.focus, Focus::Table) => return self.table.event(key), + key if matches!(self.focus, Focus::Editor) => return self.completion.event(key), _ => (), } return Ok(EventState::NotConsumed); diff --git a/src/components/table.rs b/src/components/table.rs index 0c5da62..b4c7876 100644 --- a/src/components/table.rs +++ b/src/components/table.rs @@ -27,6 +27,7 @@ pub struct TableComponent { selection_area_corner: Option<(usize, usize)>, column_page_start: std::cell::Cell, scroll: VerticalScroll, + with_title: bool, key_config: KeyConfig, } @@ -42,6 +43,23 @@ impl TableComponent { column_page_start: std::cell::Cell::new(0), scroll: VerticalScroll::new(false, false), eod: false, + with_title: true, + key_config, + } + } + + pub fn new_without_title(key_config: KeyConfig) -> Self { + Self { + selected_row: TableState::default(), + headers: vec![], + rows: vec![], + table: None, + selected_column: 0, + selection_area_corner: None, + column_page_start: std::cell::Cell::new(0), + scroll: VerticalScroll::new(false, false), + eod: false, + with_title: false, key_config, } } @@ -417,14 +435,24 @@ impl StatefulDrawableComponent for TableComponent { .split(area); f.render_widget( - Block::default() - .title(self.title()) - .borders(Borders::ALL) - .style(if focused { - Style::default() - } else { - Style::default().fg(Color::DarkGray) - }), + if self.with_title { + Block::default() + .borders(Borders::ALL) + .style(if focused { + Style::default() + } else { + Style::default().fg(Color::DarkGray) + }) + .title(self.title()) + } else { + Block::default() + .borders(Borders::ALL) + .style(if focused { + Style::default() + } else { + Style::default().fg(Color::DarkGray) + }) + }, area, ); diff --git a/src/components/table_filter.rs b/src/components/table_filter.rs index 9875f24..d0f40df 100644 --- a/src/components/table_filter.rs +++ b/src/components/table_filter.rs @@ -3,7 +3,7 @@ use super::{ StatefulDrawableComponent, }; use crate::components::command::CommandInfo; -use crate::config::KeyConfig; +use crate::config::{DatabaseType, KeyConfig}; use crate::event::Key; use anyhow::Result; use database_tree::Table; @@ -34,7 +34,7 @@ impl TableFilterComponent { input: Vec::new(), input_idx: 0, input_cursor_position: 0, - completion: CompletionComponent::new(key_config, "", false), + completion: CompletionComponent::new(key_config, "", DatabaseType::MySql), } } diff --git a/src/config.rs b/src/config.rs index a7f116a..31cb145 100644 --- a/src/config.rs +++ b/src/config.rs @@ -27,7 +27,7 @@ pub struct Config { } #[derive(Debug, Deserialize, Clone)] -enum DatabaseType { +pub enum DatabaseType { #[serde(rename = "mysql")] MySql, #[serde(rename = "postgres")]