diff --git a/src/app.rs b/src/app.rs index abfc7b1..55431af 100644 --- a/src/app.rs +++ b/src/app.rs @@ -172,10 +172,10 @@ impl App { &database, &table, 0, - if self.record_table.filter.input_str().is_empty() { + if self.record_table.filter.input.value_str().is_empty() { None } else { - Some(self.record_table.filter.input_str()) + Some(self.record_table.filter.input.value_str()) }, ) .await?; @@ -278,10 +278,11 @@ impl App { &database, &table, index as u16, - if self.record_table.filter.input_str().is_empty() { + if self.record_table.filter.input.value_str().is_empty() + { None } else { - Some(self.record_table.filter.input_str()) + Some(self.record_table.filter.input.value_str()) }, ) .await?; diff --git a/src/components/database_filter.rs b/src/components/database_filter.rs index 6aeea78..3106859 100644 --- a/src/components/database_filter.rs +++ b/src/components/database_filter.rs @@ -1,5 +1,6 @@ -use super::{compute_character_width, Component, DrawableComponent, EventState}; +use super::{Component, DrawableComponent, EventState}; use crate::components::command::CommandInfo; +use crate::components::utils::input::Input; use crate::event::Key; use anyhow::Result; use database_tree::Table; @@ -11,34 +12,23 @@ use tui::{ widgets::{Block, Borders, Paragraph}, Frame, }; -use unicode_width::UnicodeWidthStr; pub struct DatabaseFilterComponent { pub table: Option, - input: Vec, - input_idx: usize, - input_cursor_position: u16, + pub input: Input, } impl DatabaseFilterComponent { pub fn new() -> Self { Self { table: None, - input: Vec::new(), - input_idx: 0, - input_cursor_position: 0, + input: Input::new(), } } - pub fn input_str(&self) -> String { - self.input.iter().collect() - } - pub fn reset(&mut self) { self.table = None; - self.input = Vec::new(); - self.input_idx = 0; - self.input_cursor_position = 0; + self.input.reset(); } } @@ -46,10 +36,10 @@ impl DrawableComponent for DatabaseFilterComponent { fn draw(&self, f: &mut Frame, area: Rect, focused: bool) -> Result<()> { let query = Paragraph::new(Spans::from(format!( "{:w$}", - if self.input.is_empty() && !focused { + if self.input.value.is_empty() && !focused { "Filter tables".to_string() } else { - self.input_str() + self.input.value_str() }, w = area.width as usize ))) @@ -63,7 +53,7 @@ impl DrawableComponent for DatabaseFilterComponent { if focused { f.set_cursor( - (area.x + self.input_cursor_position).min(area.right().saturating_sub(1)), + (area.x + self.input.cursor_position).min(area.right().saturating_sub(1)), area.y, ) } @@ -75,58 +65,9 @@ impl Component for DatabaseFilterComponent { fn commands(&self, _out: &mut Vec) {} fn event(&mut self, key: Key) -> Result { - let input_str: String = self.input.iter().collect(); - - match key { - Key::Char(c) => { - self.input.insert(self.input_idx, c); - self.input_idx += 1; - self.input_cursor_position += compute_character_width(c); - - return Ok(EventState::Consumed); - } - Key::Delete | Key::Backspace => { - 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 -= compute_character_width(last_c); - } - return Ok(EventState::Consumed); - } - Key::Left => { - if !self.input.is_empty() && self.input_idx > 0 { - self.input_idx -= 1; - self.input_cursor_position = self - .input_cursor_position - .saturating_sub(compute_character_width(self.input[self.input_idx])); - } - return Ok(EventState::Consumed); - } - Key::Ctrl('a') => { - if !self.input.is_empty() && self.input_idx > 0 { - self.input_idx = 0; - self.input_cursor_position = 0 - } - return Ok(EventState::Consumed); - } - Key::Right => { - if self.input_idx < self.input.len() { - let next_c = self.input[self.input_idx]; - self.input_idx += 1; - self.input_cursor_position += compute_character_width(next_c); - } - return Ok(EventState::Consumed); - } - Key::Ctrl('e') => { - if self.input_idx < self.input.len() { - self.input_idx = self.input.len(); - self.input_cursor_position = self.input_str().width() as u16; - } - return Ok(EventState::Consumed); - } - _ => (), + match self.input.handle_key(key) { + (Some(_), _) => Ok(EventState::Consumed), + _ => Ok(EventState::NotConsumed), } - - Ok(EventState::NotConsumed) } } diff --git a/src/components/databases.rs b/src/components/databases.rs index e4af306..218ec48 100644 --- a/src/components/databases.rs +++ b/src/components/databases.rs @@ -190,10 +190,10 @@ impl DatabasesComponent { item.clone(), selected, area.width, - if self.filter.input_str().is_empty() { + if self.filter.input.value_str().is_empty() { None } else { - Some(self.filter.input_str()) + Some(self.filter.input.value_str()) }, ) }); @@ -229,10 +229,10 @@ impl Component for DatabasesComponent { } if matches!(self.focus, Focus::Filter) { - self.filterd_tree = if self.filter.input_str().is_empty() { + self.filterd_tree = if self.filter.input.value_str().is_empty() { None } else { - Some(self.tree.filter(self.filter.input_str())) + Some(self.tree.filter(self.filter.input.value_str())) }; } diff --git a/src/components/sql_editor.rs b/src/components/sql_editor.rs index 50e53cf..c1423d4 100644 --- a/src/components/sql_editor.rs +++ b/src/components/sql_editor.rs @@ -3,6 +3,7 @@ use super::{ StatefulDrawableComponent, TableComponent, }; use crate::components::command::CommandInfo; +use crate::components::utils::input::Input; use crate::config::KeyConfig; use crate::database::{ExecuteResult, Pool}; use crate::event::Key; @@ -34,9 +35,7 @@ pub enum Focus { } pub struct SqlEditorComponent { - input: Vec, - input_cursor_position_x: u16, - input_idx: usize, + input: Input, table: TableComponent, query_result: Option, completion: CompletionComponent, @@ -48,9 +47,7 @@ pub struct SqlEditorComponent { impl SqlEditorComponent { pub fn new(key_config: KeyConfig) -> Self { Self { - input: Vec::new(), - input_idx: 0, - input_cursor_position_x: 0, + input: Input::new(), table: TableComponent::new(key_config.clone()), completion: CompletionComponent::new(key_config.clone(), "", true), focus: Focus::Editor, @@ -63,9 +60,10 @@ impl SqlEditorComponent { fn update_completion(&mut self) { let input = &self .input + .value .iter() .enumerate() - .filter(|(i, _)| i < &self.input_idx) + .filter(|(i, _)| i < &self.input.cursor_index) .map(|(_, i)| i) .collect::() .split(' ') @@ -80,16 +78,23 @@ impl SqlEditorComponent { let mut input = Vec::new(); let first = self .input + .value .iter() .enumerate() - .filter(|(i, _)| i < &self.input_idx.saturating_sub(self.completion.word().len())) + .filter(|(i, _)| { + i < &self + .input + .cursor_index + .saturating_sub(self.completion.word().len()) + }) .map(|(_, c)| c.to_string()) .collect::>(); let last = self .input + .value .iter() .enumerate() - .filter(|(i, _)| i >= &self.input_idx) + .filter(|(i, _)| i >= &self.input.cursor_index) .map(|(_, c)| c.to_string()) .collect::>(); @@ -113,21 +118,21 @@ impl SqlEditorComponent { input.extend(middle.clone()); input.extend(last); - self.input = input.join("").chars().collect(); - self.input_idx += &middle.len(); + self.input.value = input.join("").chars().collect(); + self.input.cursor_index += &middle.len(); if is_last_word { - self.input_idx += 1; + self.input.cursor_index += 1; } - self.input_idx -= self.completion.word().len(); - self.input_cursor_position_x += middle + self.input.cursor_index -= self.completion.word().len(); + self.input.cursor_position += 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 += " ".to_string().width() as u16 } - self.input_cursor_position_x -= self + self.input.cursor_position -= self .completion .word() .chars() @@ -151,7 +156,7 @@ impl StatefulDrawableComponent for SqlEditorComponent { }) .split(area); - let editor = StatefulParagraph::new(self.input.iter().collect::()) + let editor = StatefulParagraph::new(self.input.value.iter().collect::()) .wrap(Wrap { trim: true }) .block(Block::default().borders(Borders::ALL)); @@ -176,14 +181,10 @@ impl StatefulDrawableComponent for SqlEditorComponent { 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), - ) + .saturating_add(self.input.cursor_position % 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()), + (layout[0].y + 1 + self.input.cursor_position / layout[0].width.saturating_sub(2)) + .min(layout[0].bottom()), ) } @@ -192,8 +193,8 @@ impl StatefulDrawableComponent for SqlEditorComponent { 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), + self.input.cursor_position % layout[0].width.saturating_sub(2) + 1, + self.input.cursor_position / layout[0].width.saturating_sub(2), )?; }; Ok(()) @@ -205,62 +206,48 @@ 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.insert(self.input_idx, c); - self.input_idx += 1; - self.input_cursor_position_x += compute_character_width(c); - self.update_completion(); + if matches!(self.focus, Focus::Table) { + return self.table.event(key); + } - 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); - self.completion.update(""); - } + if !matches!(self.focus, Focus::Editor) { + return Ok(EventState::NotConsumed); + } - 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])); - self.completion.update(""); - } - 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); - self.completion.update(""); - } - return Ok(EventState::Consumed); + if key == Key::Esc { + self.focus = Focus::Table; + return Ok(EventState::Consumed); + } else { + match self.input.handle_key(key) { + (Some(matched_key), input_updated) => match matched_key { + Key::Char(_) => { + self.update_completion(); + return Ok(EventState::Consumed); + } + Key::Ctrl(_) => { + return Ok(EventState::Consumed); + } + _ => { + if input_updated { + self.completion.update(""); + } + return Ok(EventState::Consumed); + } + }, + _ => return Ok(EventState::NotConsumed), } - key if matches!(self.focus, Focus::Table) => return self.table.event(key), - _ => (), } - return Ok(EventState::NotConsumed); } async fn async_event(&mut self, key: Key, pool: &Box) -> Result { if key == self.key_config.enter && matches!(self.focus, Focus::Editor) { - let query = self.input.iter().collect(); + let query = self.input.value.iter().collect(); let result = pool.execute(&query).await?; match result { ExecuteResult::Read { diff --git a/src/components/table_filter.rs b/src/components/table_filter.rs index 9875f24..8f3126a 100644 --- a/src/components/table_filter.rs +++ b/src/components/table_filter.rs @@ -3,6 +3,7 @@ use super::{ StatefulDrawableComponent, }; use crate::components::command::CommandInfo; +use crate::components::utils::input::Input; use crate::config::KeyConfig; use crate::event::Key; use anyhow::Result; @@ -20,9 +21,7 @@ use unicode_width::UnicodeWidthStr; pub struct TableFilterComponent { key_config: KeyConfig, pub table: Option
, - input: Vec, - input_idx: usize, - input_cursor_position: u16, + pub input: Input, completion: CompletionComponent, } @@ -31,30 +30,23 @@ impl TableFilterComponent { Self { key_config: key_config.clone(), table: None, - input: Vec::new(), - input_idx: 0, - input_cursor_position: 0, + input: Input::new(), completion: CompletionComponent::new(key_config, "", false), } } - pub fn input_str(&self) -> String { - self.input.iter().collect() - } - pub fn reset(&mut self) { + self.input.reset(); self.table = None; - self.input = Vec::new(); - self.input_idx = 0; - self.input_cursor_position = 0; } fn update_completion(&mut self) { let input = &self .input + .value .iter() .enumerate() - .filter(|(i, _)| i < &self.input_idx) + .filter(|(i, _)| i < &self.input.cursor_index) .map(|(_, i)| i) .collect::() .split(' ') @@ -69,16 +61,23 @@ impl TableFilterComponent { let mut input = Vec::new(); let first = self .input + .value .iter() .enumerate() - .filter(|(i, _)| i < &self.input_idx.saturating_sub(self.completion.word().len())) + .filter(|(i, _)| { + i < &self + .input + .cursor_index + .saturating_sub(self.completion.word().len()) + }) .map(|(_, c)| c.to_string()) .collect::>(); let last = self .input + .value .iter() .enumerate() - .filter(|(i, _)| i >= &self.input_idx) + .filter(|(i, _)| i >= &self.input.cursor_index) .map(|(_, c)| c.to_string()) .collect::>(); @@ -102,21 +101,21 @@ impl TableFilterComponent { input.extend(middle.clone()); input.extend(last); - self.input = input.join("").chars().collect(); - self.input_idx += &middle.len(); + self.input.value = input.join("").chars().collect(); + self.input.cursor_index += &middle.len(); if is_last_word { - self.input_idx += 1; + self.input.cursor_index += 1; } - self.input_idx -= self.completion.word().len(); - self.input_cursor_position += middle + self.input.cursor_index -= self.completion.word().len(); + self.input.cursor_position += middle .join("") .chars() .map(compute_character_width) .sum::(); if is_last_word { - self.input_cursor_position += " ".to_string().width() as u16 + self.input.cursor_position += " ".to_string().width() as u16 } - self.input_cursor_position -= self + self.input.cursor_position -= self .completion .word() .chars() @@ -140,8 +139,8 @@ impl StatefulDrawableComponent for TableFilterComponent { ), Span::from(format!( " {}", - if focused || !self.input.is_empty() { - self.input.iter().collect::() + if focused || !self.input.value.is_empty() { + self.input.value.iter().collect::() } else { "Enter a SQL expression in WHERE clause to filter records".to_string() } @@ -167,7 +166,7 @@ impl StatefulDrawableComponent for TableFilterComponent { format!("{} ", table.name.to_string()) }) .width() as u16) - .saturating_add(self.input_cursor_position), + .saturating_add(self.input.cursor_position), 0, )?; }; @@ -181,7 +180,7 @@ impl StatefulDrawableComponent for TableFilterComponent { .map_or(String::new(), |table| table.name.to_string()) .width() + 1) as u16) - .saturating_add(self.input_cursor_position) + .saturating_add(self.input.cursor_position) .min(area.right().saturating_sub(2)), area.y + 1, ) @@ -194,8 +193,6 @@ impl Component for TableFilterComponent { fn commands(&self, _out: &mut Vec) {} fn event(&mut self, key: Key) -> Result { - let input_str: String = self.input.iter().collect(); - // apply comletion candidates if key == self.key_config.enter { return self.complete(); @@ -203,58 +200,23 @@ impl Component for TableFilterComponent { self.completion.selected_candidate(); - match key { - Key::Char(c) => { - self.input.insert(self.input_idx, c); - self.input_idx += 1; - self.input_cursor_position += compute_character_width(c); - self.update_completion(); - - Ok(EventState::Consumed) - } - Key::Delete | Key::Backspace => { - 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 -= compute_character_width(last_c); - self.completion.update(""); - } - Ok(EventState::Consumed) - } - Key::Left => { - if !self.input.is_empty() && self.input_idx > 0 { - self.input_idx -= 1; - self.input_cursor_position = self - .input_cursor_position - .saturating_sub(compute_character_width(self.input[self.input_idx])); - self.completion.update(""); + match self.input.handle_key(key) { + (Some(matched_key), input_updated) => match matched_key { + Key::Char(_) => { + self.update_completion(); + return Ok(EventState::Consumed); } - Ok(EventState::Consumed) - } - Key::Ctrl('a') => { - if !self.input.is_empty() && self.input_idx > 0 { - self.input_idx = 0; - self.input_cursor_position = 0 - } - Ok(EventState::Consumed) - } - Key::Right => { - if self.input_idx < self.input.len() { - let next_c = self.input[self.input_idx]; - self.input_idx += 1; - self.input_cursor_position += compute_character_width(next_c); - self.completion.update(""); + Key::Ctrl(_) => { + return Ok(EventState::Consumed); } - Ok(EventState::Consumed) - } - Key::Ctrl('e') => { - if self.input_idx < self.input.len() { - self.input_idx = self.input.len(); - self.input_cursor_position = self.input_str().width() as u16; + _ => { + if input_updated { + self.completion.update(""); + } + return Ok(EventState::Consumed); } - Ok(EventState::Consumed) - } - key => self.completion.event(key), + }, + _ => self.completion.event(key), } } } @@ -266,12 +228,12 @@ mod test { #[test] fn test_complete() { let mut filter = TableFilterComponent::new(KeyConfig::default()); - filter.input_idx = 2; - filter.input = vec!['a', 'n', ' ', 'c', 'd', 'e', 'f', 'g']; + filter.input.cursor_index = 2; + filter.input.value = vec!['a', 'n', ' ', 'c', 'd', 'e', 'f', 'g']; filter.completion.update("an"); assert!(filter.complete().is_ok()); assert_eq!( - filter.input, + filter.input.value, vec!['A', 'N', 'D', ' ', 'c', 'd', 'e', 'f', 'g'] ); } @@ -279,12 +241,12 @@ mod test { #[test] fn test_complete_end() { let mut filter = TableFilterComponent::new(KeyConfig::default()); - filter.input_idx = 9; - filter.input = vec!['a', 'b', ' ', 'c', 'd', 'e', 'f', ' ', 'i']; + filter.input.cursor_index = 9; + filter.input.value = vec!['a', 'b', ' ', 'c', 'd', 'e', 'f', ' ', 'i']; filter.completion.update('i'); assert!(filter.complete().is_ok()); assert_eq!( - filter.input, + filter.input.value, vec!['a', 'b', ' ', 'c', 'd', 'e', 'f', ' ', 'I', 'N', ' '] ); } @@ -292,10 +254,13 @@ mod test { #[test] fn test_complete_no_candidates() { let mut filter = TableFilterComponent::new(KeyConfig::default()); - filter.input_idx = 2; - filter.input = vec!['a', 'n', ' ', 'c', 'd', 'e', 'f', 'g']; + filter.input.cursor_index = 2; + filter.input.value = vec!['a', 'n', ' ', 'c', 'd', 'e', 'f', 'g']; filter.completion.update("foo"); assert!(filter.complete().is_ok()); - assert_eq!(filter.input, vec!['a', 'n', ' ', 'c', 'd', 'e', 'f', 'g']); + assert_eq!( + filter.input.value, + vec!['a', 'n', ' ', 'c', 'd', 'e', 'f', 'g'] + ); } } diff --git a/src/components/utils/input.rs b/src/components/utils/input.rs new file mode 100644 index 0000000..d03af21 --- /dev/null +++ b/src/components/utils/input.rs @@ -0,0 +1,88 @@ +use crate::components::compute_character_width; +use crate::event::Key; +use unicode_width::UnicodeWidthStr; + +pub struct Input { + pub value: Vec, + pub cursor_position: u16, + pub cursor_index: usize, +} + +impl Input { + pub fn new() -> Self { + Self { + value: Vec::new(), + cursor_index: 0, + cursor_position: 0, + } + } + + pub fn value_str(&self) -> String { + self.value.iter().collect() + } + + pub fn reset(&mut self) { + self.value = Vec::new(); + self.cursor_index = 0; + self.cursor_position = 0; + } + + pub fn handle_key(&mut self, key: Key) -> (Option, bool) { + let value_str: String = self.value.iter().collect(); + + match key { + Key::Char(c) => { + self.value.insert(self.cursor_index, c); + self.cursor_index += 1; + self.cursor_position += compute_character_width(c); + + return (Some(key), true); + } + Key::Delete | Key::Backspace => { + if value_str.width() > 0 && !self.value.is_empty() && self.cursor_index > 0 { + let last_c = self.value.remove(self.cursor_index - 1); + self.cursor_index -= 1; + self.cursor_position -= compute_character_width(last_c); + return (Some(key), true); + } + return (Some(key), false); + } + Key::Left => { + if !self.value.is_empty() && self.cursor_index > 0 { + self.cursor_index -= 1; + self.cursor_position = self + .cursor_position + .saturating_sub(compute_character_width(self.value[self.cursor_index])); + return (Some(key), true); + } + return (Some(key), false); + } + Key::Right => { + if self.cursor_index < self.value.len() { + let next_c = self.value[self.cursor_index]; + self.cursor_index += 1; + self.cursor_position += compute_character_width(next_c); + return (Some(key), true); + } + return (Some(key), false); + } + Key::Ctrl('a') => { + if !self.value.is_empty() && self.cursor_index > 0 { + self.cursor_index = 0; + self.cursor_position = 0; + return (Some(key), true); + } + return (Some(key), false); + } + Key::Ctrl('e') => { + if self.cursor_index < self.value.len() { + self.cursor_index = self.value.len(); + self.cursor_position = self.value_str().width() as u16; + return (Some(key), true); + } + return (Some(key), false); + } + _ => (None, false), + } + } +} diff --git a/src/components/utils/mod.rs b/src/components/utils/mod.rs index 5860c6c..4f6d44f 100644 --- a/src/components/utils/mod.rs +++ b/src/components/utils/mod.rs @@ -1 +1,2 @@ +pub mod input; pub mod scroll_vertical;