From 96680c2afb5809ab3c589725190ac312e1fdd4a7 Mon Sep 17 00:00:00 2001 From: sebashwa Date: Sun, 15 May 2022 21:03:04 +0200 Subject: [PATCH 01/15] Add new util input helper Most of the input logic was duplicated in three places as of now: * Database filter input * SQL Editor * Table filter This is done in preparation for adding more cursor movement functionality for inputs. --- src/app.rs | 9 +- src/components/database_filter.rs | 81 +++-------------- src/components/databases.rs | 8 +- src/components/sql_editor.rs | 123 ++++++++++++-------------- src/components/table_filter.rs | 139 +++++++++++------------------- src/components/utils/input.rs | 88 +++++++++++++++++++ src/components/utils/mod.rs | 1 + 7 files changed, 216 insertions(+), 233 deletions(-) create mode 100644 src/components/utils/input.rs 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; From ffe5ce2166b1a4fcd66aa3be21624433c4ebaa41 Mon Sep 17 00:00:00 2001 From: sebashwa Date: Sun, 15 May 2022 22:25:35 +0200 Subject: [PATCH 02/15] Add happy path tests for input key handling --- src/components/utils/input.rs | 96 +++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/src/components/utils/input.rs b/src/components/utils/input.rs index d03af21..31c0610 100644 --- a/src/components/utils/input.rs +++ b/src/components/utils/input.rs @@ -86,3 +86,99 @@ impl Input { } } } + +#[cfg(test)] + +mod test { + use super::Input; + use crate::components::compute_character_width; + use crate::event::Key; + use unicode_width::UnicodeWidthStr; + + #[test] + fn test_adds_new_chars_for_char_key() { + let mut input = Input::new(); + input.handle_key(Key::Char('a')); + + assert_eq!(input.value, vec!['a']); + assert_eq!(input.cursor_index, 1); + assert_eq!(input.cursor_position, compute_character_width('a')); + } + + #[test] + fn test_deletes_chars_for_backspace_and_delete_key() { + let mut input = Input::new(); + input.value = vec!['a', 'b']; + input.cursor_index = 2; + input.cursor_position = input.value_str().width() as u16; + + input.handle_key(Key::Delete); + input.handle_key(Key::Backspace); + + assert_eq!(input.value, Vec::::new()); + assert_eq!(input.cursor_index, 0); + assert_eq!(input.cursor_position, 0); + } + + #[test] + fn test_moves_cursor_left_for_left_key() { + let mut input = Input::new(); + input.value = vec!['a']; + input.cursor_index = 1; + input.cursor_position = compute_character_width('a'); + + let (matched_key, input_changed) = input.handle_key(Key::Left); + + assert_eq!(matched_key, Some(Key::Left)); + assert_eq!(input_changed, true); + assert_eq!(input.value, vec!['a']); + assert_eq!(input.cursor_index, 0); + assert_eq!(input.cursor_position, 0); + } + + #[test] + fn test_moves_cursor_right_for_right_key() { + let mut input = Input::new(); + input.value = vec!['a']; + + let (matched_key, input_changed) = input.handle_key(Key::Right); + + assert_eq!(matched_key, Some(Key::Right)); + assert_eq!(input_changed, true); + assert_eq!(input.value, vec!['a']); + assert_eq!(input.cursor_index, 1); + assert_eq!(input.cursor_position, compute_character_width('a')); + } + + #[test] + fn test_jumps_to_beginning_for_ctrl_a() { + let mut input = Input::new(); + input.value = vec!['a', 'b', 'c']; + input.cursor_index = 3; + input.cursor_position = input.value_str().width() as u16; + + let (matched_key, input_changed) = input.handle_key(Key::Ctrl('a')); + + assert_eq!(matched_key, Some(Key::Ctrl('a'))); + assert_eq!(input_changed, true); + assert_eq!(input.value, vec!['a', 'b', 'c']); + assert_eq!(input.cursor_index, 0); + assert_eq!(input.cursor_position, 0); + } + + #[test] + fn test_jumps_to_end_for_ctrl_e() { + let mut input = Input::new(); + input.value = vec!['a', 'b', 'c']; + input.cursor_index = 0; + input.cursor_position = 0; + + let (matched_key, input_changed) = input.handle_key(Key::Ctrl('e')); + + assert_eq!(matched_key, Some(Key::Ctrl('e'))); + assert_eq!(input_changed, true); + assert_eq!(input.value, vec!['a', 'b', 'c']); + assert_eq!(input.cursor_index, 3); + assert_eq!(input.cursor_position, input.value_str().width() as u16); + } +} From 1d0b79ec4811dc320e37a7fce3e333a2bb666e34 Mon Sep 17 00:00:00 2001 From: sebashwa Date: Sun, 15 May 2022 22:29:55 +0200 Subject: [PATCH 03/15] Use early returns in input util key matching --- src/components/utils/input.rs | 63 +++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 29 deletions(-) diff --git a/src/components/utils/input.rs b/src/components/utils/input.rs index 31c0610..efe4a5c 100644 --- a/src/components/utils/input.rs +++ b/src/components/utils/input.rs @@ -39,48 +39,53 @@ impl Input { 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); + if value_str.width() == 0 || self.value.is_empty() || self.cursor_index == 0 { + return (Some(key), false); } - return (Some(key), false); + + 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); } 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); + if self.value.is_empty() || self.cursor_index == 0 { + return (Some(key), false); } - return (Some(key), false); + + self.cursor_index -= 1; + self.cursor_position = self + .cursor_position + .saturating_sub(compute_character_width(self.value[self.cursor_index])); + return (Some(key), true); } 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); + if self.cursor_index == self.value.len() { + return (Some(key), false); } - return (Some(key), false); + + let next_c = self.value[self.cursor_index]; + self.cursor_index += 1; + self.cursor_position += compute_character_width(next_c); + return (Some(key), true); } Key::Ctrl('a') => { - if !self.value.is_empty() && self.cursor_index > 0 { - self.cursor_index = 0; - self.cursor_position = 0; - return (Some(key), true); + if self.value.is_empty() || self.cursor_index == 0 { + return (Some(key), false); } - return (Some(key), false); + + self.cursor_index = 0; + self.cursor_position = 0; + return (Some(key), true); } 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); + if self.cursor_index == self.value.len() { + return (Some(key), false); } - return (Some(key), false); + + self.cursor_index = self.value.len(); + self.cursor_position = self.value_str().width() as u16; + return (Some(key), true); } _ => (None, false), } From 898424209b47e723fc6ab3bf2b06b8ce0861e9c1 Mon Sep 17 00:00:00 2001 From: sebashwa Date: Mon, 16 May 2022 08:18:07 +0200 Subject: [PATCH 04/15] Add delete word for inputs deletes the input from the current cursor position up to the first occurence of a whitespace before the current position. --- src/components/utils/input.rs | 60 ++++++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 5 deletions(-) diff --git a/src/components/utils/input.rs b/src/components/utils/input.rs index efe4a5c..8767b8b 100644 --- a/src/components/utils/input.rs +++ b/src/components/utils/input.rs @@ -21,12 +21,29 @@ impl Input { self.value.iter().collect() } + pub fn value_width(&self) -> u16 { + self.value_str().width() as u16 + } + pub fn reset(&mut self) { self.value = Vec::new(); self.cursor_index = 0; self.cursor_position = 0; } + fn find_whitespace_backwards(&self) -> Option { + let mut result = None; + + for i in (0..self.cursor_index).rev() { + if (i < self.cursor_index - 1) && self.value[i].is_whitespace() { + result = Some(i); + break; + } + } + + return result; + } + pub fn handle_key(&mut self, key: Key) -> (Option, bool) { let value_str: String = self.value.iter().collect(); @@ -84,7 +101,26 @@ impl Input { } self.cursor_index = self.value.len(); - self.cursor_position = self.value_str().width() as u16; + self.cursor_position = self.value_width(); + return (Some(key), true); + } + Key::Ctrl('w') => { + if self.value.is_empty() || self.cursor_index == 0 { + return (Some(key), false); + } + + let new_cursor_index = match self.find_whitespace_backwards() { + Some(i) => i + 1, + None => 0, + }; + + let mut tail = self.value.to_vec().drain(self.cursor_index..).collect(); + + self.cursor_index = new_cursor_index; + self.value.truncate(new_cursor_index); + self.cursor_position = self.value_width(); + self.value.append(&mut tail); + return (Some(key), true); } _ => (None, false), @@ -98,7 +134,6 @@ mod test { use super::Input; use crate::components::compute_character_width; use crate::event::Key; - use unicode_width::UnicodeWidthStr; #[test] fn test_adds_new_chars_for_char_key() { @@ -115,7 +150,7 @@ mod test { let mut input = Input::new(); input.value = vec!['a', 'b']; input.cursor_index = 2; - input.cursor_position = input.value_str().width() as u16; + input.cursor_position = input.value_width(); input.handle_key(Key::Delete); input.handle_key(Key::Backspace); @@ -160,7 +195,7 @@ mod test { let mut input = Input::new(); input.value = vec!['a', 'b', 'c']; input.cursor_index = 3; - input.cursor_position = input.value_str().width() as u16; + input.cursor_position = input.value_width(); let (matched_key, input_changed) = input.handle_key(Key::Ctrl('a')); @@ -184,6 +219,21 @@ mod test { assert_eq!(input_changed, true); assert_eq!(input.value, vec!['a', 'b', 'c']); assert_eq!(input.cursor_index, 3); - assert_eq!(input.cursor_position, input.value_str().width() as u16); + assert_eq!(input.cursor_position, input.value_width()); + } + + #[test] + fn test_deletes_word_for_ctrl_w() { + let mut input = Input::new(); + input.value = vec!['a', ' ', 'c', 'd']; + input.cursor_index = 3; + input.cursor_position = input.value_width(); + + let (matched_key, input_changed) = input.handle_key(Key::Ctrl('w')); + + assert_eq!(matched_key, Some(Key::Ctrl('w'))); + assert_eq!(input_changed, true); + assert_eq!(input.value, vec!['a', ' ', 'd']); + assert_eq!(input.cursor_index, 2); } } From a55f0fe3e7382e79ec161962917f3c5d8609771d Mon Sep 17 00:00:00 2001 From: sebashwa Date: Mon, 16 May 2022 08:55:11 +0200 Subject: [PATCH 05/15] Change interface for moving cursor to whitespace --- src/components/utils/input.rs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/components/utils/input.rs b/src/components/utils/input.rs index 8767b8b..847dd5d 100644 --- a/src/components/utils/input.rs +++ b/src/components/utils/input.rs @@ -31,12 +31,13 @@ impl Input { self.cursor_position = 0; } - fn find_whitespace_backwards(&self) -> Option { - let mut result = None; - for i in (0..self.cursor_index).rev() { - if (i < self.cursor_index - 1) && self.value[i].is_whitespace() { - result = Some(i); + fn cursor_index_backwards_to_whitespace(&self) -> usize { + let mut result = 0; + + for i in (0..self.cursor_index - 1).rev() { + if self.value[i].is_whitespace() { + result = i + 1; break; } } @@ -109,11 +110,7 @@ impl Input { return (Some(key), false); } - let new_cursor_index = match self.find_whitespace_backwards() { - Some(i) => i + 1, - None => 0, - }; - + let new_cursor_index = self.cursor_index_backwards_to_whitespace(); let mut tail = self.value.to_vec().drain(self.cursor_index..).collect(); self.cursor_index = new_cursor_index; From 221d10ee91577ac365d8063d226ac852658ee037 Mon Sep 17 00:00:00 2001 From: sebashwa Date: Mon, 16 May 2022 08:55:56 +0200 Subject: [PATCH 06/15] Introduce helper methods for input key safeguards --- src/components/utils/input.rs | 51 +++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/src/components/utils/input.rs b/src/components/utils/input.rs index 847dd5d..d9fbe8b 100644 --- a/src/components/utils/input.rs +++ b/src/components/utils/input.rs @@ -31,6 +31,13 @@ impl Input { self.cursor_position = 0; } + fn cannot_go_left(&self) -> bool { + self.value.is_empty() || self.cursor_index == 0 || self.value_width() == 0 + } + + fn cannot_go_right(&self) -> bool { + self.cursor_index == self.value.len() + } fn cursor_index_backwards_to_whitespace(&self) -> usize { let mut result = 0; @@ -46,8 +53,6 @@ impl Input { } 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); @@ -57,7 +62,7 @@ impl Input { return (Some(key), true); } Key::Delete | Key::Backspace => { - if value_str.width() == 0 || self.value.is_empty() || self.cursor_index == 0 { + if self.cannot_go_left() { return (Some(key), false); } @@ -66,47 +71,47 @@ impl Input { self.cursor_position -= compute_character_width(last_c); return (Some(key), true); } - Key::Left => { - if self.value.is_empty() || self.cursor_index == 0 { + Key::Right => { + if self.cannot_go_right() { return (Some(key), false); } - self.cursor_index -= 1; - self.cursor_position = self - .cursor_position - .saturating_sub(compute_character_width(self.value[self.cursor_index])); + let next_c = self.value[self.cursor_index]; + self.cursor_index += 1; + self.cursor_position += compute_character_width(next_c); return (Some(key), true); } - Key::Right => { - if self.cursor_index == self.value.len() { + Key::Ctrl('e') => { + if self.cannot_go_right() { return (Some(key), false); } - let next_c = self.value[self.cursor_index]; - self.cursor_index += 1; - self.cursor_position += compute_character_width(next_c); + self.cursor_index = self.value.len(); + self.cursor_position = self.value_width(); return (Some(key), true); } - Key::Ctrl('a') => { - if self.value.is_empty() || self.cursor_index == 0 { + Key::Left => { + if self.cannot_go_left() { return (Some(key), false); } - self.cursor_index = 0; - self.cursor_position = 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); } - Key::Ctrl('e') => { - if self.cursor_index == self.value.len() { + Key::Ctrl('a') => { + if self.cannot_go_left() { return (Some(key), false); } - self.cursor_index = self.value.len(); - self.cursor_position = self.value_width(); + self.cursor_index = 0; + self.cursor_position = 0; return (Some(key), true); } Key::Ctrl('w') => { - if self.value.is_empty() || self.cursor_index == 0 { + if self.cannot_go_left() { return (Some(key), false); } From 8ab2d44d5393d2966562bacd7d642a4b4effaf14 Mon Sep 17 00:00:00 2001 From: sebashwa Date: Mon, 23 May 2022 07:40:42 +0200 Subject: [PATCH 07/15] Update database tree directly after filter change The database tree was updated for the filter string of the previous key press before. This became more apparent with the new movements like 'delete word' (Ctrl-w). --- src/components/databases.rs | 77 ++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 39 deletions(-) diff --git a/src/components/databases.rs b/src/components/databases.rs index 218ec48..9e357ef 100644 --- a/src/components/databases.rs +++ b/src/components/databases.rs @@ -36,7 +36,7 @@ pub enum Focus { pub struct DatabasesComponent { tree: DatabaseTree, filter: DatabaseFilterComponent, - filterd_tree: Option, + filtered_tree: Option, scroll: VerticalScroll, focus: Focus, key_config: KeyConfig, @@ -47,7 +47,7 @@ impl DatabasesComponent { Self { tree: DatabaseTree::default(), filter: DatabaseFilterComponent::new(), - filterd_tree: None, + filtered_tree: None, scroll: VerticalScroll::new(false, false), focus: Focus::Tree, key_config, @@ -63,7 +63,7 @@ impl DatabasesComponent { None => pool.get_databases().await?, }; self.tree = DatabaseTree::new(databases.as_slice(), &BTreeSet::new())?; - self.filterd_tree = None; + self.filtered_tree = None; self.filter.reset(); Ok(()) } @@ -73,7 +73,7 @@ impl DatabasesComponent { } pub fn tree(&self) -> &DatabaseTree { - self.filterd_tree.as_ref().unwrap_or(&self.tree) + self.filtered_tree.as_ref().unwrap_or(&self.tree) } fn tree_item_to_span( @@ -168,10 +168,9 @@ impl DatabasesComponent { .draw(f, chunks[0], matches!(self.focus, Focus::Filter))?; let tree_height = chunks[1].height as usize; - let tree = if let Some(tree) = self.filterd_tree.as_ref() { - tree - } else { - &self.tree + let tree = match self.filtered_tree.as_ref() { + Some(t) => t, + None => &self.tree, }; tree.visual_selection().map_or_else( || { @@ -223,40 +222,40 @@ impl Component for DatabasesComponent { } fn event(&mut self, key: Key) -> Result { - if key == self.key_config.filter && self.focus == Focus::Tree { - self.focus = Focus::Filter; - return Ok(EventState::Consumed); - } - if matches!(self.focus, Focus::Filter) { - self.filterd_tree = if self.filter.input.value_str().is_empty() { - None - } else { - Some(self.tree.filter(self.filter.input.value_str())) - }; - } - - match key { - Key::Enter if matches!(self.focus, Focus::Filter) => { - self.focus = Focus::Tree; - return Ok(EventState::Consumed); - } - key if matches!(self.focus, Focus::Filter) => { - if self.filter.event(key)?.is_consumed() { + match key { + Key::Enter => { + self.focus = Focus::Tree; return Ok(EventState::Consumed); } + key => { + if self.filter.event(key)?.is_consumed() { + let filter_str = self.filter.input.value_str(); + + self.filtered_tree = if filter_str.is_empty() { + None + } else { + Some(self.tree.filter(filter_str)) + }; + 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); + } else if matches!(self.focus, Focus::Tree) { + match key { + key => { + if key == self.key_config.filter { + self.focus = Focus::Filter; + return Ok(EventState::Consumed); + } + let tree_for_nav = match self.filtered_tree.as_mut() { + Some(tree) => tree, + None => &mut self.tree, + }; + + if tree_nav(tree_for_nav, key, &self.key_config) { + return Ok(EventState::Consumed); + } } } } @@ -376,7 +375,7 @@ mod test { } #[test] - fn test_filterd_tree_item_to_span() { + fn test_filtered_tree_item_to_span() { const WIDTH: u16 = 10; assert_eq!( DatabasesComponent::tree_item_to_span( From fff86b95d166d57547aed5a6db985713c89026d9 Mon Sep 17 00:00:00 2001 From: sebashwa Date: Mon, 23 May 2022 07:45:22 +0200 Subject: [PATCH 08/15] Add alt backspace keybinding for inputs This deletes in the left direction until a non alphanumeric character is found. --- src/components/utils/input.rs | 46 +++++++++++++++++++++++++++++------ src/components/utils/mod.rs | 8 ++++++ 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/src/components/utils/input.rs b/src/components/utils/input.rs index d9fbe8b..e3f89c1 100644 --- a/src/components/utils/input.rs +++ b/src/components/utils/input.rs @@ -1,3 +1,4 @@ +use super::{is_nonalphanumeric, is_whitespace}; use crate::components::compute_character_width; use crate::event::Key; use unicode_width::UnicodeWidthStr; @@ -39,11 +40,11 @@ impl Input { self.cursor_index == self.value.len() } - fn cursor_index_backwards_to_whitespace(&self) -> usize { + fn cursor_index_backwards_until(&self, char_fun: &dyn Fn(char) -> bool) -> usize { let mut result = 0; for i in (0..self.cursor_index - 1).rev() { - if self.value[i].is_whitespace() { + if char_fun(self.value[i]) { result = i + 1; break; } @@ -52,6 +53,15 @@ impl Input { return result; } + fn delete_left_until(&mut self, new_cursor_index: usize) { + let mut tail = self.value.to_vec().drain(self.cursor_index..).collect(); + + self.cursor_index = new_cursor_index; + self.value.truncate(new_cursor_index); + self.cursor_position = self.value_width(); + self.value.append(&mut tail); + } + pub fn handle_key(&mut self, key: Key) -> (Option, bool) { match key { Key::Char(c) => { @@ -115,13 +125,18 @@ impl Input { return (Some(key), false); } - let new_cursor_index = self.cursor_index_backwards_to_whitespace(); - let mut tail = self.value.to_vec().drain(self.cursor_index..).collect(); + let new_cursor_index = self.cursor_index_backwards_until(&is_whitespace); + self.delete_left_until(new_cursor_index); - self.cursor_index = new_cursor_index; - self.value.truncate(new_cursor_index); - self.cursor_position = self.value_width(); - self.value.append(&mut tail); + return (Some(key), true); + } + Key::AltBackspace => { + if self.cannot_go_left() { + return (Some(key), false); + } + + let new_cursor_index = self.cursor_index_backwards_until(&is_nonalphanumeric); + self.delete_left_until(new_cursor_index); return (Some(key), true); } @@ -238,4 +253,19 @@ mod test { assert_eq!(input.value, vec!['a', ' ', 'd']); assert_eq!(input.cursor_index, 2); } + + #[test] + fn test_deletes_til_nonalphanumeric_for_alt_backspace() { + let mut input = Input::new(); + input.value = vec!['a', '-', 'c', 'd']; + input.cursor_index = 3; + input.cursor_position = input.value_width(); + + let (matched_key, input_changed) = input.handle_key(Key::AltBackspace); + + assert_eq!(matched_key, Some(Key::AltBackspace)); + assert_eq!(input_changed, true); + assert_eq!(input.value, vec!['a', '-', 'd']); + assert_eq!(input.cursor_index, 2); + } } diff --git a/src/components/utils/mod.rs b/src/components/utils/mod.rs index 4f6d44f..ed48b04 100644 --- a/src/components/utils/mod.rs +++ b/src/components/utils/mod.rs @@ -1,2 +1,10 @@ pub mod input; pub mod scroll_vertical; + +pub fn is_whitespace(c: char) -> bool { + c.is_whitespace() +} + +pub fn is_nonalphanumeric(c: char) -> bool { + !c.is_alphanumeric() +} From dc9d7fd44ffc59c50aa2c3f0fe011d1b2a2d38bc Mon Sep 17 00:00:00 2001 From: sebashwa Date: Mon, 23 May 2022 09:37:15 +0200 Subject: [PATCH 09/15] Allow database tree navigation while filtering This makes UX much smoother IMHO. The filter focus is now moved back to the tree by pressing while in filtering mode. --- src/app.rs | 2 +- src/components/databases.rs | 41 +++++++++++++++++++++++-------------- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/app.rs b/src/app.rs index 55431af..f29646d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -223,7 +223,7 @@ impl App { return Ok(EventState::Consumed); } - if key == self.config.key_config.enter && self.databases.tree_focused() { + if key == self.config.key_config.enter { if let Some((database, table)) = self.databases.tree().selected_table() { self.record_table.reset(); let (headers, records) = self diff --git a/src/components/databases.rs b/src/components/databases.rs index 9e357ef..171e9e8 100644 --- a/src/components/databases.rs +++ b/src/components/databases.rs @@ -9,7 +9,7 @@ use crate::event::Key; use crate::ui::common_nav; use crate::ui::scrolllist::draw_list_block; use anyhow::Result; -use database_tree::{Database, DatabaseTree, DatabaseTreeItem}; +use database_tree::{Database, DatabaseTree, DatabaseTreeItem, MoveSelection}; use std::collections::BTreeSet; use std::convert::From; use tui::{ @@ -76,6 +76,21 @@ impl DatabasesComponent { self.filtered_tree.as_ref().unwrap_or(&self.tree) } + fn navigate_tree(&mut self, nav: MoveSelection) -> bool { + let tree = match self.filtered_tree.as_mut() { + Some(t) => t, + None => &mut self.tree, + }; + tree.move_selection(nav) + } + + fn maybe_navigate_tree(&mut self, key: Key) -> bool { + match common_nav(key, &self.key_config) { + Some(nav) => self.navigate_tree(nav), + None => false, + } + } + fn tree_item_to_span( item: DatabaseTreeItem, selected: bool, @@ -224,10 +239,18 @@ impl Component for DatabasesComponent { fn event(&mut self, key: Key) -> Result { if matches!(self.focus, Focus::Filter) { match key { - Key::Enter => { + Key::Esc => { self.focus = Focus::Tree; return Ok(EventState::Consumed); } + Key::Ctrl('j') | Key::Ctrl('n') => { + self.navigate_tree(MoveSelection::Down); + return Ok(EventState::Consumed); + } + Key::Ctrl('k') | Key::Ctrl('p') => { + self.navigate_tree(MoveSelection::Up); + return Ok(EventState::Consumed); + } key => { if self.filter.event(key)?.is_consumed() { let filter_str = self.filter.input.value_str(); @@ -248,12 +271,8 @@ impl Component for DatabasesComponent { self.focus = Focus::Filter; return Ok(EventState::Consumed); } - let tree_for_nav = match self.filtered_tree.as_mut() { - Some(tree) => tree, - None => &mut self.tree, - }; - if tree_nav(tree_for_nav, key, &self.key_config) { + if self.maybe_navigate_tree(key) { return Ok(EventState::Consumed); } } @@ -263,14 +282,6 @@ impl Component for DatabasesComponent { } } -fn tree_nav(tree: &mut DatabaseTree, key: Key, key_config: &KeyConfig) -> bool { - if let Some(common_nav) = common_nav(key, key_config) { - tree.move_selection(common_nav) - } else { - false - } -} - #[cfg(test)] mod test { use super::{Color, Database, DatabaseTreeItem, DatabasesComponent, Span, Spans, Style}; From f090dd07d51455594d3453e501a02a072976c12e Mon Sep 17 00:00:00 2001 From: sebashwa Date: Mon, 23 May 2022 09:39:20 +0200 Subject: [PATCH 10/15] Allow input navigation with and --- src/components/utils/input.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/utils/input.rs b/src/components/utils/input.rs index e3f89c1..8bcbacc 100644 --- a/src/components/utils/input.rs +++ b/src/components/utils/input.rs @@ -81,7 +81,7 @@ impl Input { self.cursor_position -= compute_character_width(last_c); return (Some(key), true); } - Key::Right => { + Key::Right | Key::Ctrl('f') => { if self.cannot_go_right() { return (Some(key), false); } @@ -100,7 +100,7 @@ impl Input { self.cursor_position = self.value_width(); return (Some(key), true); } - Key::Left => { + Key::Left | Key::Ctrl('b') => { if self.cannot_go_left() { return (Some(key), false); } From f108b28d5f813dcb25b2f901d5b350405c615890 Mon Sep 17 00:00:00 2001 From: sebashwa Date: Mon, 23 May 2022 09:39:42 +0200 Subject: [PATCH 11/15] Add for input navigation Move backwards until first non alphanumeric occurence. --- src/components/utils/input.rs | 28 ++++++++++++++++++++++++++++ src/event/key.rs | 5 +++++ 2 files changed, 33 insertions(+) diff --git a/src/components/utils/input.rs b/src/components/utils/input.rs index 8bcbacc..feb1d54 100644 --- a/src/components/utils/input.rs +++ b/src/components/utils/input.rs @@ -120,6 +120,19 @@ impl Input { self.cursor_position = 0; return (Some(key), true); } + Key::Alt('b') => { + if self.cannot_go_left() { + return (Some(key), false); + } + + let new_cursor_index = self.cursor_index_backwards_until(&is_nonalphanumeric); + self.cursor_index = new_cursor_index; + self.cursor_position = self.value[0..self.cursor_index] + .iter() + .collect::() + .width() as u16; + return (Some(key), true); + } Key::Ctrl('w') => { if self.cannot_go_left() { return (Some(key), false); @@ -268,4 +281,19 @@ mod test { assert_eq!(input.value, vec!['a', '-', 'd']); assert_eq!(input.cursor_index, 2); } + + #[test] + fn test_goes_til_nonalphanumeric_for_alt_b() { + let mut input = Input::new(); + input.value = vec!['a', '-', 'c', 'd']; + input.cursor_index = 3; + input.cursor_position = input.value_width(); + + let (matched_key, input_changed) = input.handle_key(Key::Alt('b')); + + assert_eq!(matched_key, Some(Key::Alt('b'))); + assert_eq!(input_changed, true); + assert_eq!(input.value, vec!['a', '-', 'c', 'd']); + assert_eq!(input.cursor_index, 2); + } } diff --git a/src/event/key.rs b/src/event/key.rs index c40240c..9e73d18 100644 --- a/src/event/key.rs +++ b/src/event/key.rs @@ -69,6 +69,7 @@ pub enum Key { Char(char), Ctrl(char), Alt(char), + AltBackspace, Unknown, } @@ -135,6 +136,10 @@ impl From for Key { code: event::KeyCode::Esc, .. } => Key::Esc, + event::KeyEvent { + code: event::KeyCode::Backspace, + modifiers: event::KeyModifiers::ALT, + } => Key::AltBackspace, event::KeyEvent { code: event::KeyCode::Backspace, .. From 195dc95a1bdacfd76b4a03483b315c5d3ef703d6 Mon Sep 17 00:00:00 2001 From: sebashwa Date: Mon, 23 May 2022 11:01:14 +0200 Subject: [PATCH 12/15] Implement to move forwards in inputs Until the next occurence of a nonalphanumeric char. --- src/components/utils/input.rs | 69 ++++++++++++++++++++++++++++++----- 1 file changed, 59 insertions(+), 10 deletions(-) diff --git a/src/components/utils/input.rs b/src/components/utils/input.rs index feb1d54..03db061 100644 --- a/src/components/utils/input.rs +++ b/src/components/utils/input.rs @@ -1,6 +1,7 @@ use super::{is_nonalphanumeric, is_whitespace}; use crate::components::compute_character_width; use crate::event::Key; +use std::ops::Range; use unicode_width::UnicodeWidthStr; pub struct Input { @@ -26,6 +27,10 @@ impl Input { self.value_str().width() as u16 } + fn width_for(&self, chars: &[char]) -> u16 { + chars.iter().collect::().width() as u16 + } + pub fn reset(&mut self) { self.value = Vec::new(); self.cursor_index = 0; @@ -40,12 +45,16 @@ impl Input { self.cursor_index == self.value.len() } - fn cursor_index_backwards_until(&self, char_fun: &dyn Fn(char) -> bool) -> usize { - let mut result = 0; + fn find_index_for_char_of_kind( + &self, + range: Range, + is_char_of_kind: &dyn Fn(char) -> bool, + ) -> Option { + let mut result = None; - for i in (0..self.cursor_index - 1).rev() { - if char_fun(self.value[i]) { - result = i + 1; + for i in range { + if is_char_of_kind(self.value[i]) { + result = Some(i); break; } } @@ -53,6 +62,24 @@ impl Input { return result; } + fn cursor_index_backwards_until(&self, is_char_of_kind: &dyn Fn(char) -> bool) -> usize { + let range = 0..self.cursor_index - 1; + + match self.find_index_for_char_of_kind(range, is_char_of_kind) { + Some(index) => index + 1, + None => 0, + } + } + + fn cursor_index_forwards_until(&self, is_char_of_kind: &dyn Fn(char) -> bool) -> usize { + let range = self.cursor_index + 1..self.value.len(); + + match self.find_index_for_char_of_kind(range, is_char_of_kind) { + Some(index) => index, + None => self.value.len(), + } + } + fn delete_left_until(&mut self, new_cursor_index: usize) { let mut tail = self.value.to_vec().drain(self.cursor_index..).collect(); @@ -100,6 +127,16 @@ impl Input { self.cursor_position = self.value_width(); return (Some(key), true); } + Key::Alt('f') => { + if self.cannot_go_right() { + return (Some(key), false); + } + + let new_cursor_index = self.cursor_index_forwards_until(&is_nonalphanumeric); + self.cursor_index = new_cursor_index; + self.cursor_position = self.width_for(&self.value[0..new_cursor_index]); + return (Some(key), true); + } Key::Left | Key::Ctrl('b') => { if self.cannot_go_left() { return (Some(key), false); @@ -127,10 +164,7 @@ impl Input { let new_cursor_index = self.cursor_index_backwards_until(&is_nonalphanumeric); self.cursor_index = new_cursor_index; - self.cursor_position = self.value[0..self.cursor_index] - .iter() - .collect::() - .width() as u16; + self.cursor_position = self.width_for(&self.value[0..new_cursor_index]); return (Some(key), true); } Key::Ctrl('w') => { @@ -283,7 +317,7 @@ mod test { } #[test] - fn test_goes_til_nonalphanumeric_for_alt_b() { + fn test_moves_backwards_til_nonalphanumeric_for_alt_b() { let mut input = Input::new(); input.value = vec!['a', '-', 'c', 'd']; input.cursor_index = 3; @@ -296,4 +330,19 @@ mod test { assert_eq!(input.value, vec!['a', '-', 'c', 'd']); assert_eq!(input.cursor_index, 2); } + + #[test] + fn test_moves_forwards_til_nonalphanumeric_for_alt_f() { + let mut input = Input::new(); + input.value = vec!['a', 'b', '-', 'c']; + input.cursor_index = 1; + input.cursor_position = input.value_width(); + + let (matched_key, input_changed) = input.handle_key(Key::Alt('f')); + + assert_eq!(matched_key, Some(Key::Alt('f'))); + assert_eq!(input_changed, true); + assert_eq!(input.value, vec!['a', 'b', '-', 'c']); + assert_eq!(input.cursor_index, 2); + } } From 2af554341b2775743e7d96515bfc4e863f905611 Mon Sep 17 00:00:00 2001 From: sebashwa Date: Mon, 23 May 2022 11:07:13 +0200 Subject: [PATCH 13/15] Implement to forward delete words in input Until a non alphanumeric char. --- src/components/utils/input.rs | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/components/utils/input.rs b/src/components/utils/input.rs index 03db061..974dd78 100644 --- a/src/components/utils/input.rs +++ b/src/components/utils/input.rs @@ -89,6 +89,13 @@ impl Input { self.value.append(&mut tail); } + fn delete_right_until(&mut self, index: usize) { + let mut tail = self.value.to_vec().drain(index..).collect(); + + self.value.truncate(self.cursor_index); + self.value.append(&mut tail); + } + pub fn handle_key(&mut self, key: Key) -> (Option, bool) { match key { Key::Char(c) => { @@ -137,6 +144,15 @@ impl Input { self.cursor_position = self.width_for(&self.value[0..new_cursor_index]); return (Some(key), true); } + Key::Alt('d') => { + if self.cannot_go_right() { + return (Some(key), false); + } + + let index = self.cursor_index_forwards_until(&is_nonalphanumeric); + self.delete_right_until(index); + return (Some(key), true); + } Key::Left | Key::Ctrl('b') => { if self.cannot_go_left() { return (Some(key), false); @@ -302,7 +318,7 @@ mod test { } #[test] - fn test_deletes_til_nonalphanumeric_for_alt_backspace() { + fn test_deletes_backwards_til_nonalphanumeric_for_alt_backspace() { let mut input = Input::new(); input.value = vec!['a', '-', 'c', 'd']; input.cursor_index = 3; @@ -316,6 +332,21 @@ mod test { assert_eq!(input.cursor_index, 2); } + #[test] + fn test_deletes_forwards_til_nonalphanumeric_for_alt_d() { + let mut input = Input::new(); + input.value = vec!['a', 'b', '-', 'd']; + input.cursor_index = 1; + input.cursor_position = input.value_width(); + + let (matched_key, input_changed) = input.handle_key(Key::Alt('d')); + + assert_eq!(matched_key, Some(Key::Alt('d'))); + assert_eq!(input_changed, true); + assert_eq!(input.value, vec!['a', '-', 'd']); + assert_eq!(input.cursor_index, 1); + } + #[test] fn test_moves_backwards_til_nonalphanumeric_for_alt_b() { let mut input = Input::new(); From 468611de705b6a1a0de7157bf6481eb748b2bb91 Mon Sep 17 00:00:00 2001 From: sebashwa Date: Mon, 23 May 2022 11:11:15 +0200 Subject: [PATCH 14/15] Implement for char deletion under cursor In inputs --- src/components/utils/input.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/components/utils/input.rs b/src/components/utils/input.rs index 974dd78..5f6dd29 100644 --- a/src/components/utils/input.rs +++ b/src/components/utils/input.rs @@ -153,6 +153,14 @@ impl Input { self.delete_right_until(index); return (Some(key), true); } + Key::Ctrl('d') => { + if self.cannot_go_right() { + return (Some(key), false); + } + + self.delete_right_until(self.cursor_index + 1); + return (Some(key), true); + } Key::Left | Key::Ctrl('b') => { if self.cannot_go_left() { return (Some(key), false); @@ -347,6 +355,20 @@ mod test { assert_eq!(input.cursor_index, 1); } + #[test] + fn test_deletes_char_under_current_cursor_for_ctrl_d() { + let mut input = Input::new(); + input.value = vec!['a', 'b', 'c', 'd']; + input.cursor_index = 1; + input.cursor_position = input.value_width(); + + let (matched_key, input_changed) = input.handle_key(Key::Ctrl('d')); + + assert_eq!(matched_key, Some(Key::Ctrl('d'))); + assert_eq!(input_changed, true); + assert_eq!(input.value, vec!['a', 'c', 'd']); + assert_eq!(input.cursor_index, 1); + } #[test] fn test_moves_backwards_til_nonalphanumeric_for_alt_b() { let mut input = Input::new(); From 25d032319e85c1cc24ad01c53fb1633d249f7431 Mon Sep 17 00:00:00 2001 From: sebashwa Date: Mon, 23 May 2022 11:12:14 +0200 Subject: [PATCH 15/15] Rename cannot_go_* functions to cannot_move_* To be more consistent with wording. --- src/components/utils/input.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/components/utils/input.rs b/src/components/utils/input.rs index 5f6dd29..c1a8ab8 100644 --- a/src/components/utils/input.rs +++ b/src/components/utils/input.rs @@ -37,11 +37,11 @@ impl Input { self.cursor_position = 0; } - fn cannot_go_left(&self) -> bool { + fn cannot_move_left(&self) -> bool { self.value.is_empty() || self.cursor_index == 0 || self.value_width() == 0 } - fn cannot_go_right(&self) -> bool { + fn cannot_move_right(&self) -> bool { self.cursor_index == self.value.len() } @@ -106,7 +106,7 @@ impl Input { return (Some(key), true); } Key::Delete | Key::Backspace => { - if self.cannot_go_left() { + if self.cannot_move_left() { return (Some(key), false); } @@ -116,7 +116,7 @@ impl Input { return (Some(key), true); } Key::Right | Key::Ctrl('f') => { - if self.cannot_go_right() { + if self.cannot_move_right() { return (Some(key), false); } @@ -126,7 +126,7 @@ impl Input { return (Some(key), true); } Key::Ctrl('e') => { - if self.cannot_go_right() { + if self.cannot_move_right() { return (Some(key), false); } @@ -135,7 +135,7 @@ impl Input { return (Some(key), true); } Key::Alt('f') => { - if self.cannot_go_right() { + if self.cannot_move_right() { return (Some(key), false); } @@ -145,7 +145,7 @@ impl Input { return (Some(key), true); } Key::Alt('d') => { - if self.cannot_go_right() { + if self.cannot_move_right() { return (Some(key), false); } @@ -154,7 +154,7 @@ impl Input { return (Some(key), true); } Key::Ctrl('d') => { - if self.cannot_go_right() { + if self.cannot_move_right() { return (Some(key), false); } @@ -162,7 +162,7 @@ impl Input { return (Some(key), true); } Key::Left | Key::Ctrl('b') => { - if self.cannot_go_left() { + if self.cannot_move_left() { return (Some(key), false); } @@ -173,7 +173,7 @@ impl Input { return (Some(key), true); } Key::Ctrl('a') => { - if self.cannot_go_left() { + if self.cannot_move_left() { return (Some(key), false); } @@ -182,7 +182,7 @@ impl Input { return (Some(key), true); } Key::Alt('b') => { - if self.cannot_go_left() { + if self.cannot_move_left() { return (Some(key), false); } @@ -192,7 +192,7 @@ impl Input { return (Some(key), true); } Key::Ctrl('w') => { - if self.cannot_go_left() { + if self.cannot_move_left() { return (Some(key), false); } @@ -202,7 +202,7 @@ impl Input { return (Some(key), true); } Key::AltBackspace => { - if self.cannot_go_left() { + if self.cannot_move_left() { return (Some(key), false); }