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,
..