diff --git a/src/components/sql_editor.rs b/src/components/sql_editor.rs index f2f3932..b6050bb 100644 --- a/src/components/sql_editor.rs +++ b/src/components/sql_editor.rs @@ -1,5 +1,6 @@ use super::{ - compute_character_width, Component, EventState, StatefulDrawableComponent, TableComponent, + compute_character_width, CompletionComponent, Component, EventState, MovableComponent, + StatefulDrawableComponent, TableComponent, }; use crate::components::command::CommandInfo; use crate::config::KeyConfig; @@ -14,12 +15,19 @@ use tui::{ widgets::{Block, Borders, Paragraph, Wrap}, Frame, }; +use unicode_width::UnicodeWidthStr; struct QueryResult { updated_rows: u64, query: String, } +impl QueryResult { + fn result_str(&self) -> String { + format!("Query OK, {} row affected", self.updated_rows) + } +} + pub enum Focus { Editor, Table, @@ -27,8 +35,11 @@ pub enum Focus { pub struct SqlEditorComponent { input: Vec, + input_cursor_position_x: u16, + input_idx: usize, table: TableComponent, query_result: Option, + completion: CompletionComponent, key_config: KeyConfig, focus: Focus, } @@ -37,12 +48,94 @@ impl SqlEditorComponent { pub fn new(key_config: KeyConfig) -> Self { Self { input: Vec::new(), + input_idx: 0, + input_cursor_position_x: 0, table: TableComponent::new(key_config.clone()), + completion: CompletionComponent::new(key_config.clone(), ""), focus: Focus::Editor, query_result: None, key_config, } } + + fn update_completion(&mut self) { + let input = &self + .input + .iter() + .enumerate() + .filter(|(i, _)| i < &self.input_idx) + .map(|(_, i)| i) + .collect::() + .split(' ') + .map(|i| i.to_string()) + .collect::>(); + self.completion + .update(input.last().unwrap_or(&String::new())); + } + + fn complete(&mut self) -> anyhow::Result { + if let Some(candidate) = self.completion.selected_candidate() { + let mut input = Vec::new(); + let first = self + .input + .iter() + .enumerate() + .filter(|(i, _)| i < &self.input_idx.saturating_sub(self.completion.word().len())) + .map(|(_, c)| c.to_string()) + .collect::>(); + let last = self + .input + .iter() + .enumerate() + .filter(|(i, _)| i >= &self.input_idx) + .map(|(_, c)| c.to_string()) + .collect::>(); + + let is_last_word = last.first().map_or(false, |c| c == &" ".to_string()); + + let middle = if is_last_word { + candidate + .chars() + .map(|c| c.to_string()) + .collect::>() + } else { + let mut c = candidate + .chars() + .map(|c| c.to_string()) + .collect::>(); + c.push(" ".to_string()); + c + }; + + input.extend(first); + input.extend(middle.clone()); + input.extend(last); + + self.input = input.join("").chars().collect(); + self.input_idx += &middle.len(); + if is_last_word { + self.input_idx += 1; + } + self.input_idx -= self.completion.word().len(); + self.input_cursor_position_x += middle + .join("") + .chars() + .map(compute_character_width) + .sum::(); + if is_last_word { + self.input_cursor_position_x += " ".to_string().width() as u16 + } + self.input_cursor_position_x -= self + .completion + .word() + .chars() + .map(compute_character_width) + .sum::(); + self.update_completion(); + return Ok(EventState::Consumed); + } + Ok(EventState::NotConsumed) + } } impl StatefulDrawableComponent for SqlEditorComponent { @@ -70,8 +163,45 @@ impl StatefulDrawableComponent for SqlEditorComponent { .wrap(Wrap { trim: true }); f.render_widget(editor, layout[0]); - self.table - .draw(f, layout[1], matches!(self.focus, Focus::Table))?; + if let Some(result) = self.query_result.as_ref() { + let result = Paragraph::new(result.result_str()) + .block(Block::default().borders(Borders::ALL).style( + if focused && matches!(self.focus, Focus::Editor) { + Style::default() + } else { + Style::default().fg(Color::DarkGray) + }, + )) + .wrap(Wrap { trim: true }); + f.render_widget(result, layout[1]); + } else { + self.table + .draw(f, layout[1], focused && matches!(self.focus, Focus::Table))?; + } + + if focused && matches!(self.focus, Focus::Editor) { + f.set_cursor( + (layout[0].x + 1) + .saturating_add( + self.input_cursor_position_x % layout[0].width.saturating_sub(2), + ) + .min(area.right().saturating_sub(2)), + (layout[0].y + + 1 + + self.input_cursor_position_x / layout[0].width.saturating_sub(2)) + .min(layout[0].bottom()), + ) + } + + if focused { + self.completion.draw( + f, + area, + false, + self.input_cursor_position_x % layout[0].width.saturating_sub(2) + 1, + self.input_cursor_position_x / layout[0].width.saturating_sub(2), + )?; + }; Ok(()) } } @@ -81,15 +211,50 @@ impl Component for SqlEditorComponent { fn commands(&self, _out: &mut Vec) {} fn event(&mut self, key: Key) -> Result { + 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(); } + match key { Key::Char(c) if matches!(self.focus, Focus::Editor) => { - self.input.push(c); + self.input.insert(self.input_idx, c); + self.input_idx += 1; + self.input_cursor_position_x += compute_character_width(c); + self.update_completion(); + + return Ok(EventState::Consumed); + } + Key::Esc if matches!(self.focus, Focus::Editor) => self.focus = Focus::Table, + Key::Delete | Key::Backspace if matches!(self.focus, Focus::Editor) => { + if input_str.width() > 0 && !self.input.is_empty() && self.input_idx > 0 { + let last_c = self.input.remove(self.input_idx - 1); + self.input_idx -= 1; + self.input_cursor_position_x -= compute_character_width(last_c); + } return Ok(EventState::Consumed); } + Key::Left if matches!(self.focus, Focus::Editor) => { + if !self.input.is_empty() && self.input_idx > 0 { + self.input_idx -= 1; + self.input_cursor_position_x = self + .input_cursor_position_x + .saturating_sub(compute_character_width(self.input[self.input_idx])); + } + return Ok(EventState::Consumed); + } + Key::Right if matches!(self.focus, Focus::Editor) => { + if self.input_idx < self.input.len() { + let next_c = self.input[self.input_idx]; + self.input_idx += 1; + self.input_cursor_position_x += compute_character_width(next_c); + } + return Ok(EventState::Consumed); + } key if matches!(self.focus, Focus::Table) => return self.table.event(key), _ => (), } @@ -97,7 +262,7 @@ impl Component for SqlEditorComponent { } async fn async_event(&mut self, key: Key, pool: &Box) -> Result { - if key == self.key_config.enter { + if key == self.key_config.run && matches!(self.focus, Focus::Editor) { let query = self.input.iter().collect(); let result = pool.execute(&query).await?; match result { @@ -108,7 +273,8 @@ impl Component for SqlEditorComponent { table, } => { self.table.update(rows, headers, database, table); - self.focus = Focus::Table + self.focus = Focus::Table; + self.query_result = None; } ExecuteResult::Write { updated_rows } => { self.query_result = Some(QueryResult {