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.
pull/154/head
sebashwa 2 years ago
parent b13e4bb255
commit 96680c2afb

@ -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?;

@ -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<Table>,
input: Vec<char>,
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<B: Backend>(&self, f: &mut Frame<B>, 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<CommandInfo>) {}
fn event(&mut self, key: Key) -> Result<EventState> {
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)
}
}

@ -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()))
};
}

@ -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<char>,
input_cursor_position_x: u16,
input_idx: usize,
input: Input,
table: TableComponent,
query_result: Option<QueryResult>,
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::<String>()
.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::<Vec<String>>();
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::<Vec<String>>();
@ -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::<u16>();
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::<String>())
let editor = StatefulParagraph::new(self.input.value.iter().collect::<String>())
.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<CommandInfo>) {}
fn event(&mut self, key: Key) -> Result<EventState> {
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<dyn Pool>) -> Result<EventState> {
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 {

@ -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<Table>,
input: Vec<char>,
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::<String>()
.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::<Vec<String>>();
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::<Vec<String>>();
@ -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::<u16>();
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::<String>()
if focused || !self.input.value.is_empty() {
self.input.value.iter().collect::<String>()
} 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<CommandInfo>) {}
fn event(&mut self, key: Key) -> Result<EventState> {
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']
);
}
}

@ -0,0 +1,88 @@
use crate::components::compute_character_width;
use crate::event::Key;
use unicode_width::UnicodeWidthStr;
pub struct Input {
pub value: Vec<char>,
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<Key>, 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),
}
}
}

@ -1 +1,2 @@
pub mod input;
pub mod scroll_vertical;

Loading…
Cancel
Save