diff --git a/src/app.rs b/src/app.rs index db41011..30474a7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -173,10 +173,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?; @@ -224,7 +224,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 @@ -279,10 +279,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..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::{ @@ -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,22 @@ 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 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( @@ -168,10 +183,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( || { @@ -190,10 +204,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()) }, ) }); @@ -223,55 +237,51 @@ 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_str().is_empty() { - None - } else { - Some(self.tree.filter(self.filter.input_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::Esc => { + self.focus = Focus::Tree; 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, - ) { + 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(); + + self.filtered_tree = if filter_str.is_empty() { + None + } else { + Some(self.tree.filter(filter_str)) + }; + 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); + } + + if self.maybe_navigate_tree(key) { + return Ok(EventState::Consumed); + } + } } } Ok(EventState::NotConsumed) } } -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}; @@ -376,7 +386,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( 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..c1a8ab8 --- /dev/null +++ b/src/components/utils/input.rs @@ -0,0 +1,401 @@ +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 { + 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 value_width(&self) -> u16 { + 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; + self.cursor_position = 0; + } + + fn cannot_move_left(&self) -> bool { + self.value.is_empty() || self.cursor_index == 0 || self.value_width() == 0 + } + + fn cannot_move_right(&self) -> bool { + self.cursor_index == self.value.len() + } + + 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 range { + if is_char_of_kind(self.value[i]) { + result = Some(i); + break; + } + } + + 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(); + + self.cursor_index = new_cursor_index; + self.value.truncate(new_cursor_index); + self.cursor_position = self.value_width(); + 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) => { + 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 self.cannot_move_left() { + 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::Right | Key::Ctrl('f') => { + if self.cannot_move_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); + return (Some(key), true); + } + Key::Ctrl('e') => { + if self.cannot_move_right() { + return (Some(key), false); + } + + self.cursor_index = self.value.len(); + self.cursor_position = self.value_width(); + return (Some(key), true); + } + Key::Alt('f') => { + if self.cannot_move_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::Alt('d') => { + if self.cannot_move_right() { + return (Some(key), false); + } + + let index = self.cursor_index_forwards_until(&is_nonalphanumeric); + self.delete_right_until(index); + return (Some(key), true); + } + Key::Ctrl('d') => { + if self.cannot_move_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_move_left() { + 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::Ctrl('a') => { + if self.cannot_move_left() { + return (Some(key), false); + } + + self.cursor_index = 0; + self.cursor_position = 0; + return (Some(key), true); + } + Key::Alt('b') => { + if self.cannot_move_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.width_for(&self.value[0..new_cursor_index]); + return (Some(key), true); + } + Key::Ctrl('w') => { + if self.cannot_move_left() { + return (Some(key), false); + } + + let new_cursor_index = self.cursor_index_backwards_until(&is_whitespace); + self.delete_left_until(new_cursor_index); + + return (Some(key), true); + } + Key::AltBackspace => { + if self.cannot_move_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); + } + _ => (None, false), + } + } +} + +#[cfg(test)] + +mod test { + use super::Input; + use crate::components::compute_character_width; + use crate::event::Key; + + #[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_width(); + + 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_width(); + + 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_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); + } + + #[test] + fn test_deletes_backwards_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); + } + + #[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_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(); + 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); + } + + #[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); + } +} diff --git a/src/components/utils/mod.rs b/src/components/utils/mod.rs index 5860c6c..ed48b04 100644 --- a/src/components/utils/mod.rs +++ b/src/components/utils/mod.rs @@ -1 +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() +} 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, ..