Switch to using unicode_segmentation's cursor as a basis on how we do cursor movement in search.

This commit is contained in:
ClementTsang 2020-02-27 16:02:53 -05:00
parent d96751b786
commit a755a5d41c
4 changed files with 162 additions and 128 deletions

View File

@ -38,6 +38,7 @@ backtrace = "0.3"
toml = "0.5.6" toml = "0.5.6"
serde = {version = "1.0", features = ["derive"] } serde = {version = "1.0", features = ["derive"] }
unicode-segmentation = "1.6.0" unicode-segmentation = "1.6.0"
unicode-width = "0.1.7"
[dev-dependencies] [dev-dependencies]
assert_cmd = "0.12" assert_cmd = "0.12"

View File

@ -6,9 +6,12 @@ pub mod data_farmer;
use data_farmer::*; use data_farmer::*;
use crate::{canvas, constants, utils::error::Result}; use crate::{canvas, constants, utils::error::Result};
mod process_killer; mod process_killer;
use unicode_segmentation::GraphemeCursor;
const MAX_SEARCH_LENGTH: usize = 200;
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum WidgetPosition { pub enum WidgetPosition {
Cpu, Cpu,
@ -60,9 +63,9 @@ pub struct AppSearchState {
pub is_enabled: bool, pub is_enabled: bool,
current_search_query: String, current_search_query: String,
current_regex: Option<std::result::Result<regex::Regex, regex::Error>>, current_regex: Option<std::result::Result<regex::Regex, regex::Error>>,
current_cursor_position: usize,
pub is_blank_search: bool, pub is_blank_search: bool,
pub is_invalid_search: bool, pub is_invalid_search: bool,
pub grapheme_cursor: GraphemeCursor,
} }
impl Default for AppSearchState { impl Default for AppSearchState {
@ -71,9 +74,9 @@ impl Default for AppSearchState {
is_enabled: false, is_enabled: false,
current_search_query: String::default(), current_search_query: String::default(),
current_regex: None, current_regex: None,
current_cursor_position: 0,
is_invalid_search: false, is_invalid_search: false,
is_blank_search: true, is_blank_search: true,
grapheme_cursor: GraphemeCursor::new(0, 0, true),
} }
} }
} }
@ -535,7 +538,8 @@ impl App {
pub fn get_cursor_position(&self) -> usize { pub fn get_cursor_position(&self) -> usize {
self.process_search_state self.process_search_state
.search_state .search_state
.current_cursor_position .grapheme_cursor
.cur_cursor()
} }
/// One of two functions allowed to run while in a dialog... /// One of two functions allowed to run while in a dialog...
@ -576,23 +580,26 @@ impl App {
WidgetPosition::Process => self.start_dd(), WidgetPosition::Process => self.start_dd(),
WidgetPosition::ProcessSearch => { WidgetPosition::ProcessSearch => {
if self.process_search_state.search_state.is_enabled if self.process_search_state.search_state.is_enabled
&& self && self.get_cursor_position()
.process_search_state < self
.search_state .process_search_state
.current_cursor_position < self .search_state
.process_search_state .current_search_query
.search_state .len()
.current_search_query
.len()
{ {
self.process_search_state self.process_search_state
.search_state .search_state
.current_search_query .current_search_query
.remove( .remove(self.get_cursor_position());
self.process_search_state
.search_state self.process_search_state.search_state.grapheme_cursor = GraphemeCursor::new(
.current_cursor_position, self.get_cursor_position(),
); self.process_search_state
.search_state
.current_search_query
.len(),
true,
);
self.update_regex(); self.update_regex();
self.update_process_gui = true; self.update_process_gui = true;
@ -619,9 +626,8 @@ impl App {
pub fn clear_search(&mut self) { pub fn clear_search(&mut self) {
if let WidgetPosition::ProcessSearch = self.current_widget_selected { if let WidgetPosition::ProcessSearch = self.current_widget_selected {
self.process_search_state self.process_search_state.search_state.grapheme_cursor =
.search_state GraphemeCursor::new(0, 0, true);
.current_cursor_position = 0;
self.process_search_state.search_state.current_search_query = String::default(); self.process_search_state.search_state.current_search_query = String::default();
self.process_search_state.search_state.is_blank_search = true; self.process_search_state.search_state.is_blank_search = true;
self.process_search_state.search_state.is_invalid_search = false; self.process_search_state.search_state.is_invalid_search = false;
@ -629,26 +635,46 @@ impl App {
} }
} }
pub fn search_walk_forward(&mut self, start_position: usize) {
self.process_search_state
.search_state
.grapheme_cursor
.next_boundary(
&self.process_search_state.search_state.current_search_query[start_position..],
start_position,
)
.unwrap();
}
pub fn search_walk_back(&mut self, start_position: usize) {
self.process_search_state
.search_state
.grapheme_cursor
.prev_boundary(
&self.process_search_state.search_state.current_search_query[..start_position],
0,
)
.unwrap();
}
pub fn on_backspace(&mut self) { pub fn on_backspace(&mut self) {
if let WidgetPosition::ProcessSearch = self.current_widget_selected { if let WidgetPosition::ProcessSearch = self.current_widget_selected {
if self.process_search_state.search_state.is_enabled if self.process_search_state.search_state.is_enabled && self.get_cursor_position() > 0 {
&& self self.search_walk_back(self.get_cursor_position());
.process_search_state
.search_state
.current_cursor_position
> 0
{
self.process_search_state
.search_state
.current_cursor_position -= 1;
self.process_search_state self.process_search_state
.search_state .search_state
.current_search_query .current_search_query
.remove( .remove(self.get_cursor_position());
self.process_search_state
.search_state self.process_search_state.search_state.grapheme_cursor = GraphemeCursor::new(
.current_cursor_position, self.get_cursor_position(),
); self.process_search_state
.search_state
.current_search_query
.len(),
true,
);
self.update_regex(); self.update_regex();
self.update_process_gui = true; self.update_process_gui = true;
@ -683,16 +709,7 @@ impl App {
pub fn on_left_key(&mut self) { pub fn on_left_key(&mut self) {
if !self.is_in_dialog() { if !self.is_in_dialog() {
if let WidgetPosition::ProcessSearch = self.current_widget_selected { if let WidgetPosition::ProcessSearch = self.current_widget_selected {
if self self.search_walk_back(self.get_cursor_position());
.process_search_state
.search_state
.current_cursor_position
> 0
{
self.process_search_state
.search_state
.current_cursor_position -= 1;
}
} }
} else if self.delete_dialog_state.is_showing_dd && !self.delete_dialog_state.is_on_yes { } else if self.delete_dialog_state.is_showing_dd && !self.delete_dialog_state.is_on_yes {
self.delete_dialog_state.is_on_yes = true; self.delete_dialog_state.is_on_yes = true;
@ -702,20 +719,7 @@ impl App {
pub fn on_right_key(&mut self) { pub fn on_right_key(&mut self) {
if !self.is_in_dialog() { if !self.is_in_dialog() {
if let WidgetPosition::ProcessSearch = self.current_widget_selected { if let WidgetPosition::ProcessSearch = self.current_widget_selected {
if self self.search_walk_forward(self.get_cursor_position());
.process_search_state
.search_state
.current_cursor_position
< self
.process_search_state
.search_state
.current_search_query
.len()
{
self.process_search_state
.search_state
.current_cursor_position += 1;
}
} }
} else if self.delete_dialog_state.is_showing_dd && self.delete_dialog_state.is_on_yes { } else if self.delete_dialog_state.is_showing_dd && self.delete_dialog_state.is_on_yes {
self.delete_dialog_state.is_on_yes = false; self.delete_dialog_state.is_on_yes = false;
@ -725,9 +729,14 @@ impl App {
pub fn skip_cursor_beginning(&mut self) { pub fn skip_cursor_beginning(&mut self) {
if !self.is_in_dialog() { if !self.is_in_dialog() {
if let WidgetPosition::ProcessSearch = self.current_widget_selected { if let WidgetPosition::ProcessSearch = self.current_widget_selected {
self.process_search_state self.process_search_state.search_state.grapheme_cursor = GraphemeCursor::new(
.search_state 0,
.current_cursor_position = 0; self.process_search_state
.search_state
.current_search_query
.len(),
true,
);
} }
} }
} }
@ -735,13 +744,17 @@ impl App {
pub fn skip_cursor_end(&mut self) { pub fn skip_cursor_end(&mut self) {
if !self.is_in_dialog() { if !self.is_in_dialog() {
if let WidgetPosition::ProcessSearch = self.current_widget_selected { if let WidgetPosition::ProcessSearch = self.current_widget_selected {
self.process_search_state self.process_search_state.search_state.grapheme_cursor = GraphemeCursor::new(
.search_state self.process_search_state
.current_cursor_position = self .search_state
.process_search_state .current_search_query
.search_state .len(),
.current_search_query self.process_search_state
.len(); .search_state
.current_search_query
.len(),
true,
);
} }
} }
} }
@ -788,13 +801,12 @@ impl App {
} }
pub fn on_char_key(&mut self, caught_char: char) { pub fn on_char_key(&mut self, caught_char: char) {
// Forbid any char key presses when showing a dialog box...
// Skip control code chars // Skip control code chars
if caught_char.is_control() { if caught_char.is_control() {
return; return;
} }
// Forbid any char key presses when showing a dialog box...
if !self.is_in_dialog() { if !self.is_in_dialog() {
let current_key_press_inst = Instant::now(); let current_key_press_inst = Instant::now();
if current_key_press_inst if current_key_press_inst
@ -806,21 +818,29 @@ impl App {
self.last_key_press = current_key_press_inst; self.last_key_press = current_key_press_inst;
if let WidgetPosition::ProcessSearch = self.current_widget_selected { if let WidgetPosition::ProcessSearch = self.current_widget_selected {
self.process_search_state if self
.process_search_state
.search_state .search_state
.current_search_query .current_search_query
.insert( .len() <= MAX_SEARCH_LENGTH
{
self.process_search_state
.search_state
.current_search_query
.insert(self.get_cursor_position(), caught_char);
self.process_search_state.search_state.grapheme_cursor = GraphemeCursor::new(
self.get_cursor_position(),
self.process_search_state self.process_search_state
.search_state .search_state
.current_cursor_position, .current_search_query
caught_char, .len(),
true,
); );
self.process_search_state self.search_walk_forward(self.get_cursor_position());
.search_state self.update_regex();
.current_cursor_position += 1; self.update_process_gui = true;
}
self.update_regex();
self.update_process_gui = true;
} else { } else {
match caught_char { match caught_char {
'/' => { '/' => {

View File

@ -4,7 +4,7 @@ use crate::{
data_conversion::{ConvertedCpuData, ConvertedProcessData}, data_conversion::{ConvertedCpuData, ConvertedProcessData},
utils::error, utils::error,
}; };
use std::cmp::max; use std::cmp::{max, min};
use std::collections::HashMap; use std::collections::HashMap;
use tui::{ use tui::{
backend, backend,
@ -14,6 +14,8 @@ use tui::{
widgets::{Axis, Block, Borders, Chart, Dataset, Marker, Paragraph, Row, Table, Text, Widget}, widgets::{Axis, Block, Borders, Chart, Dataset, Marker, Paragraph, Row, Table, Text, Widget},
Terminal, Terminal,
}; };
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
mod canvas_colours; mod canvas_colours;
use canvas_colours::*; use canvas_colours::*;
@ -1155,55 +1157,60 @@ impl Painter {
fn draw_search_field<B: backend::Backend>( fn draw_search_field<B: backend::Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, &self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect,
) { ) {
let width = max(0, draw_loc.width as i64 - 34) as u64; let width = max(0, draw_loc.width as i64 - 34) as u64; // TODO: [REFACTOR] Hard coding this is terrible.
let query = app_state.get_current_search_query(); let query = app_state.get_current_search_query().as_str();
let shrunk_query = if query.len() < width as usize { let grapheme_indices = UnicodeSegmentation::grapheme_indices(query, true)
query .rev()
} else { .enumerate(); // Reverse due to us wanting to draw from back -> front
&query[(query.len() - width as usize)..] let num_graphemes = min(UnicodeWidthStr::width_cjk(query), width as usize);
};
let cursor_position = app_state.get_cursor_position(); let cursor_position = app_state.get_cursor_position();
let query_with_cursor: Vec<Text<'_>> = let mut query_with_cursor: Vec<Text<'_>> = if let app::WidgetPosition::ProcessSearch =
if let app::WidgetPosition::ProcessSearch = app_state.current_widget_selected { app_state.current_widget_selected
if cursor_position >= query.len() { {
let mut q = vec![Text::styled( let mut res = Vec::new();
shrunk_query.to_string(), if cursor_position >= query.len() {
self.colours.text_style, res.push(Text::styled(
)]; " ",
self.colours.currently_selected_text_style,
))
}
q.push(Text::styled( res.extend(
" ".to_string(), grapheme_indices
self.colours.currently_selected_text_style, .filter_map(|(itx, grapheme)| {
)); if itx >= num_graphemes {
None
} else {
let styled = if grapheme.0 == cursor_position {
Text::styled(grapheme.1, self.colours.currently_selected_text_style)
} else {
Text::styled(grapheme.1, self.colours.text_style)
};
Some(styled)
}
})
.collect::<Vec<_>>(),
);
q res
} else { } else {
shrunk_query // This is easier - we just need to get a range of graphemes, rather than
.chars() // dealing with possibly inserting a cursor (as none is shown!)
.enumerate() grapheme_indices
.map(|(itx, c)| { .filter_map(|(itx, grapheme)| {
if let app::WidgetPosition::ProcessSearch = if itx >= num_graphemes {
app_state.current_widget_selected None
{ } else {
if itx == cursor_position { let styled = Text::styled(grapheme.1, self.colours.text_style);
return Text::styled( Some(styled)
c.to_string(), }
self.colours.currently_selected_text_style, })
); .collect::<Vec<_>>()
} };
}
Text::styled(c.to_string(), self.colours.text_style) // I feel like this is most definitely not the efficient way of doing this but eh
}) query_with_cursor.reverse();
.collect::<Vec<_>>()
}
} else {
vec![Text::styled(
shrunk_query.to_string(),
self.colours.text_style,
)]
};
let mut search_text = vec![if app_state.is_grouped() { let mut search_text = vec![if app_state.is_grouped() {
Text::styled("Search by Name: ", self.colours.table_header_style) Text::styled("Search by Name: ", self.colours.table_header_style)

View File

@ -315,12 +315,18 @@ fn handle_mouse_event(event: MouseEvent, app: &mut App) {
fn handle_key_event_or_break( fn handle_key_event_or_break(
event: KeyEvent, app: &mut App, rtx: &std::sync::mpsc::Sender<ResetEvent>, event: KeyEvent, app: &mut App, rtx: &std::sync::mpsc::Sender<ResetEvent>,
) -> bool { ) -> bool {
//debug!("KeyEvent: {:?}", event);
// TODO: [PASTE] Note that this does NOT support some emojis like flags. This is due to us
// catching PER CHARACTER right now WITH A forced throttle! This means multi-char will not work.
// We can solve this (when we do paste probably) while keeping the throttle (mainly meant for movement)
// by throttling after *bulk+singular* actions, not just singular ones.
if event.modifiers.is_empty() { if event.modifiers.is_empty() {
// Required catch for searching - otherwise you couldn't search with q. // Required catch for searching - otherwise you couldn't search with q.
if event.code == KeyCode::Char('q') && !app.is_in_search_widget() { if event.code == KeyCode::Char('q') && !app.is_in_search_widget() {
return true; return true;
} }
match event.code { match event.code {
KeyCode::End => app.skip_to_last(), KeyCode::End => app.skip_to_last(),
KeyCode::Home => app.skip_to_first(), KeyCode::Home => app.skip_to_first(),