From 70e594959054b7955ddef0854e02fe588392e0db Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Sat, 6 Jul 2019 20:44:51 +0300 Subject: [PATCH] ui: add autocomplete for commands in execute bar --- melib/src/grapheme_clusters.rs | 4 + ui/src/components/mail/compose.rs | 3 + ui/src/components/utilities.rs | 21 ++- ui/src/components/utilities/widgets.rs | 72 +++++++- ui/src/execute.rs | 221 +++++++++++++++---------- 5 files changed, 224 insertions(+), 97 deletions(-) diff --git a/melib/src/grapheme_clusters.rs b/melib/src/grapheme_clusters.rs index 8553c9e4e..db6b54279 100644 --- a/melib/src/grapheme_clusters.rs +++ b/melib/src/grapheme_clusters.rs @@ -37,6 +37,10 @@ pub trait Graphemes: UnicodeSegmentation + CodePointsIter { count } + + fn grapheme_len(&self) -> usize { + self.split_graphemes().len() + } } impl Graphemes for str {} diff --git a/ui/src/components/mail/compose.rs b/ui/src/components/mail/compose.rs index 1b7eab6c2..f81b210f5 100644 --- a/ui/src/components/mail/compose.rs +++ b/ui/src/components/mail/compose.rs @@ -205,6 +205,9 @@ impl Composer { let book: &AddressBook = &c.accounts[account_cursor].address_book; let results: Vec = book.search(term); results + .into_iter() + .map(|r| AutoCompleteEntry::from(r)) + .collect::>() }), )); } else { diff --git a/ui/src/components/utilities.rs b/ui/src/components/utilities.rs index 6837d5f52..d538a3a37 100644 --- a/ui/src/components/utilities.rs +++ b/ui/src/components/utilities.rs @@ -699,17 +699,24 @@ impl Component for StatusBar { if self.ex_buffer.as_str().split_graphemes().len() <= 2 { return; } - let suggestions: Vec = self + let mut suggestions: Vec = self .cmd_history .iter() .filter_map(|h| { if h.starts_with(self.ex_buffer.as_str()) { - Some(h.clone()) + Some(h.clone().into()) } else { None } }) .collect(); + suggestions.extend(crate::execute::COMMAND_COMPLETION.iter().filter_map(|e| { + if e.0.starts_with(self.ex_buffer.as_str()) { + Some(e.into()) + } else { + None + } + })); if suggestions.is_empty() && !self.auto_complete.suggestions().is_empty() { self.auto_complete.set_suggestions(suggestions); /* redraw self.container because we have got ridden of an autocomplete @@ -803,7 +810,7 @@ impl Component for StatusBar { .take(hist_height) .enumerate() { - write_string_to_grid( + let (x, y) = write_string_to_grid( s.as_str(), grid, Color::Byte(88), // DarkRed, @@ -817,6 +824,14 @@ impl Component for StatusBar { ), true, ); + write_string_to_grid( + &s.description, + grid, + Color::White, + Color::Byte(174), + ((x + 2, y), bottom_right!(hist_area)), + false, + ); if y_offset + offset == self.auto_complete.cursor() { change_colors( grid, diff --git a/ui/src/components/utilities/widgets.rs b/ui/src/components/utilities/widgets.rs index a3ad1e181..1582668fe 100644 --- a/ui/src/components/utilities/widgets.rs +++ b/ui/src/components/utilities/widgets.rs @@ -1,7 +1,7 @@ use super::*; use fnv::FnvHashMap; -type AutoCompleteFn = Box Vec + Send>; +type AutoCompleteFn = Box Vec + Send>; #[derive(Debug, PartialEq)] enum FormFocus { @@ -575,9 +575,50 @@ where } } +#[derive(Debug, PartialEq, Clone)] +pub struct AutoCompleteEntry { + pub entry: String, + pub description: String, +} + +impl AutoCompleteEntry { + pub fn as_str(&self) -> &str { + self.entry.as_str() + } +} + +impl From for AutoCompleteEntry { + fn from(val: String) -> Self { + AutoCompleteEntry { + entry: val, + description: String::new(), + } + } +} + +impl From<&(&str, &str)> for AutoCompleteEntry { + fn from(val: &(&str, &str)) -> Self { + let (a, b) = val; + AutoCompleteEntry { + entry: a.to_string(), + description: b.to_string(), + } + } +} + +impl From<(String, String)> for AutoCompleteEntry { + fn from(val: (String, String)) -> Self { + let (a, b) = val; + AutoCompleteEntry { + entry: a, + description: b, + } + } +} + #[derive(Debug, PartialEq, Clone)] pub struct AutoComplete { - entries: Vec, + entries: Vec, content: CellBuffer, cursor: usize, @@ -637,7 +678,7 @@ impl Component for AutoComplete { } impl AutoComplete { - pub fn new(entries: Vec) -> Self { + pub fn new(entries: Vec) -> Self { let mut ret = AutoComplete { entries: Vec::new(), content: CellBuffer::default(), @@ -649,26 +690,39 @@ impl AutoComplete { ret } - pub fn set_suggestions(&mut self, entries: Vec) -> bool { + pub fn set_suggestions(&mut self, entries: Vec) -> bool { if entries.len() == self.entries.len() && entries == self.entries { return false;; } let mut content = CellBuffer::new( - entries.iter().map(String::len).max().unwrap_or(0) + 1, + entries + .iter() + .map(|a| a.entry.grapheme_len() + a.description.grapheme_len() + 2) + .max() + .unwrap_or(0) + + 1, entries.len(), Cell::with_style(Color::Byte(23), Color::Byte(7), Attr::Default), ); let width = content.cols(); for (i, e) in entries.iter().enumerate() { - write_string_to_grid( - e, + let (x, _) = write_string_to_grid( + &e.entry, &mut content, Color::Byte(23), Color::Byte(7), ((0, i), (width - 1, i)), false, ); + write_string_to_grid( + &e.description, + &mut content, + Color::Byte(23), + Color::Byte(7), + ((x + 2, i), (width - 1, i)), + false, + ); write_string_to_grid( "▒", &mut content, @@ -712,10 +766,10 @@ impl AutoComplete { self.entries.clear(); self.cursor = 0; self.content.empty(); - Some(ret) + Some(ret.entry) } - pub fn suggestions(&self) -> &Vec { + pub fn suggestions(&self) -> &Vec { &self.entries } } diff --git a/ui/src/execute.rs b/ui/src/execute.rs index e19b50516..7008064ad 100644 --- a/ui/src/execute.rs +++ b/ui/src/execute.rs @@ -30,6 +30,140 @@ pub use crate::actions::ListingAction::{self, *}; pub use crate::actions::MailingListAction::{self, *}; pub use crate::actions::TabAction::{self, *}; +/* Create a const table with every command part that can be auto-completed and its description */ +macro_rules! define_commands { + ( [$({ tags: [$( $tags:literal),*], desc: $desc:literal, parser: ($parser:item)}),*]) => { + pub const COMMAND_COMPLETION: &[(&str, &str)] = &[$($( ($tags, $desc ) ),*),* ]; + $( $parser )* + }; +} + +define_commands!([ + { tags: ["set"], + desc: "set [seen/unseen], toggles message's Seen flag.", + parser: + ( named!( + envelope_action, + alt_complete!( + preceded!( + ws!(tag!("set")), + alt_complete!( + map!(ws!(tag!("read")), |_| Listing(SetRead)) + | map!(ws!(tag!("unread")), |_| Listing(SetUnread)) + ) + ) | map!(ws!(tag!("delete")), |_| Listing(Delete)) + ) + ); ) + }, + { tags: ["close"], + desc: "close non-sticky tabs", + parser: ( + named!(close, map!(ws!(tag!("close")), |_| Tab(Close))); + ) + }, + { tags: ["goto"], + desc: "goto [n], switch to nth mailbox in this account", + parser: ( + named!( + goto, + preceded!(tag!("go "), map!(call!(usize_c), Action::ViewMailbox)) + ); + ) + }, + { tags: ["subsort"], + desc: "subsort [date/subject] [asc/desc], sorts first level replies in threads.", + parser: ( + named!( + subsort, + do_parse!(tag!("subsort ") >> p: pair!(sortfield, sortorder) >> (SubSort(p.0, p.1))) + ); + ) + }, + { tags: ["sort"], + desc: "sort [date/subject] [asc/desc], sorts threads.", + parser: ( + named!( + sort, + do_parse!( + tag!("sort ") >> p: separated_pair!(sortfield, tag!(" "), sortorder) >> (Sort(p.0, p.1)) + ) + ); + ) + }, + { tags: ["set", "set plain", "set threaded", "set compact"], + desc: "set [plain/threaded/compact], changes the mail listing view", + parser: ( + named!( + toggle, + preceded!(tag!("set "), alt_complete!(threaded | plain | compact)) + ); + ) + }, + { tags: ["toggle_thread_snooze"], + desc: "turn off new notifications for this thread", + parser: ( + named!(toggle_thread_snooze, + map!(ws!(tag!("toggle_thread_snooze")), |_| ToggleThreadSnooze) + ); + ) + }, + { tags: ["filter"], + desc: "filter , filters list with given term", + parser:( + named!(filter, + do_parse!( + ws!(tag!("filter")) + >> string: map_res!(call!(not_line_ending), std::str::from_utf8) + >> (Listing(Filter(String::from(string)))) + ) + ); + ) + }, + { tags: ["list-archive", "list-post", "list-unsubscribe", "list-"], + desc: "list-[unsubscribe/post/archive]", + parser: ( + named!( + mailinglist, + alt_complete!( + map!(ws!(tag!("list-post")), |_| MailingListAction(ListPost)) + | map!(ws!(tag!("list-unsubscribe")), |_| MailingListAction( + ListUnsubscribe + )) + | map!(ws!(tag!("list-archive")), |_| MailingListAction( + ListArchive + )) + ) + ); + ) + }, + { tags: ["setenv "], + desc:"setenv VAR=VALUE", + parser: ( + named!( setenv, + do_parse!( + ws!(tag!("setenv")) + >> key: map_res!(take_until1!("="), std::str::from_utf8) + >> ws!(tag!("=")) + >> val: map_res!(call!(not_line_ending), std::str::from_utf8) + >> (SetEnv(key.to_string(), val.to_string())) + ) + ); + ) + }, + { tags: ["printenv "], + desc: "printenv VAR", + parser:( + named!( printenv, + do_parse!( + ws!(tag!("env")) + >> key: map_res!(call!(not_line_ending), std::str::from_utf8) + >> (PrintEnv(key.to_string())) + ) + ); + ) + } +]); + named!( usize_c, map_res!( @@ -54,23 +188,6 @@ named!( ) ); -named!(close, map!(ws!(tag!("close")), |_| Tab(Close))); -named!( - goto, - preceded!(tag!("go "), map!(call!(usize_c), Action::ViewMailbox)) -); - -named!( - subsort, - do_parse!(tag!("subsort ") >> p: pair!(sortfield, sortorder) >> (SubSort(p.0, p.1))) -); -named!( - sort, - do_parse!( - tag!("sort ") >> p: separated_pair!(sortfield, tag!(" "), sortorder) >> (Sort(p.0, p.1)) - ) -); - named!( threaded, map!(ws!(tag!("threaded")), |_| Listing(SetThreaded)) @@ -85,76 +202,10 @@ named!( compact, map!(ws!(tag!("compact")), |_| Listing(SetCompact)) ); - -named!( - toggle, - preceded!(tag!("set "), alt_complete!(threaded | plain | compact)) -); named!( listing_action, alt_complete!(toggle | envelope_action | filter | toggle_thread_snooze) ); - -named!( - toggle_thread_snooze, - map!(ws!(tag!("toggle_thread_snooze")), |_| ToggleThreadSnooze) -); - -named!( - filter, - do_parse!( - ws!(tag!("filter")) - >> string: map_res!(call!(not_line_ending), std::str::from_utf8) - >> (Listing(Filter(String::from(string)))) - ) -); - -named!( - mailinglist, - alt_complete!( - map!(ws!(tag!("list-post")), |_| MailingListAction(ListPost)) - | map!(ws!(tag!("list-unsubscribe")), |_| MailingListAction( - ListUnsubscribe - )) - | map!(ws!(tag!("list-archive")), |_| MailingListAction( - ListArchive - )) - ) -); - -named!( - envelope_action, - alt_complete!( - preceded!( - ws!(tag!("set")), - alt_complete!( - map!(ws!(tag!("read")), |_| Listing(SetRead)) - | map!(ws!(tag!("unread")), |_| Listing(SetUnread)) - ) - ) | map!(ws!(tag!("delete")), |_| Listing(Delete)) - ) -); - -named!( - setenv, - do_parse!( - ws!(tag!("setenv")) - >> key: map_res!(take_until1!("="), std::str::from_utf8) - >> ws!(tag!("=")) - >> val: map_res!(call!(not_line_ending), std::str::from_utf8) - >> (SetEnv(key.to_string(), val.to_string())) - ) -); - -named!( - printenv, - do_parse!( - ws!(tag!("env")) - >> key: map_res!(call!(not_line_ending), std::str::from_utf8) - >> (PrintEnv(key.to_string())) - ) -); - named!(pub parse_command, - alt_complete!( goto | listing_action | sort | subsort | close | mailinglist | setenv | printenv) - ); + alt_complete!( goto | listing_action | sort | subsort | close | mailinglist | setenv | printenv) +);