add column names to completion candidates

pull/137/head
Takayuki Maeda 2 years ago
parent 36b1da0afa
commit 1874622172

@ -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() {

@ -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<String>, all: bool) -> Self {
pub fn new(
key_config: KeyConfig,
word: impl Into<String>,
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<String>) {
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<&String>>(),
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<&String>>(),
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<&String>>(),
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<&String>>(),
vec![&"NOT".to_string(), &"NULL".to_string()]

@ -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<dyn Pool>) -> 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<Database>) -> Result<()> {
self.tree = DatabaseTree::new(databases.as_slice(), &BTreeSet::new())?;
self.filterd_tree = None;
self.filter.reset();

@ -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::<String>()
.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::<Vec<Span>>();
spans.pop();
Spans::from(spans)
}
pub fn update(&mut self, databases: &Vec<Database>) {
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::<Vec<String>>()
})
.flatten()
.collect::<Vec<String>>(),
);
self.completion.add_candidates(
databases
.iter()
.map(|db| db.name.to_string())
.collect::<Vec<String>>(),
);
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::<Vec<String>>()
})
.flatten()
.collect::<Vec<String>>(),
);
}
pub fn selected_cells(&self) -> Option<String> {
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::<String>())
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);

@ -27,6 +27,7 @@ pub struct TableComponent {
selection_area_corner: Option<(usize, usize)>,
column_page_start: std::cell::Cell<usize>,
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,
);

@ -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),
}
}

@ -27,7 +27,7 @@ pub struct Config {
}
#[derive(Debug, Deserialize, Clone)]
enum DatabaseType {
pub enum DatabaseType {
#[serde(rename = "mysql")]
MySql,
#[serde(rename = "postgres")]

Loading…
Cancel
Save