ui: add autocomplete for commands in execute bar

This commit is contained in:
Manos Pitsidianakis 2019-07-06 20:44:51 +03:00
parent a028aa9a44
commit 70e5949590
No known key found for this signature in database
GPG Key ID: 73627C2F690DF710
5 changed files with 224 additions and 97 deletions

View File

@ -37,6 +37,10 @@ pub trait Graphemes: UnicodeSegmentation + CodePointsIter {
count count
} }
fn grapheme_len(&self) -> usize {
self.split_graphemes().len()
}
} }
impl Graphemes for str {} impl Graphemes for str {}

View File

@ -205,6 +205,9 @@ impl Composer {
let book: &AddressBook = &c.accounts[account_cursor].address_book; let book: &AddressBook = &c.accounts[account_cursor].address_book;
let results: Vec<String> = book.search(term); let results: Vec<String> = book.search(term);
results results
.into_iter()
.map(|r| AutoCompleteEntry::from(r))
.collect::<Vec<AutoCompleteEntry>>()
}), }),
)); ));
} else { } else {

View File

@ -699,17 +699,24 @@ impl Component for StatusBar {
if self.ex_buffer.as_str().split_graphemes().len() <= 2 { if self.ex_buffer.as_str().split_graphemes().len() <= 2 {
return; return;
} }
let suggestions: Vec<String> = self let mut suggestions: Vec<AutoCompleteEntry> = self
.cmd_history .cmd_history
.iter() .iter()
.filter_map(|h| { .filter_map(|h| {
if h.starts_with(self.ex_buffer.as_str()) { if h.starts_with(self.ex_buffer.as_str()) {
Some(h.clone()) Some(h.clone().into())
} else { } else {
None None
} }
}) })
.collect(); .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() { if suggestions.is_empty() && !self.auto_complete.suggestions().is_empty() {
self.auto_complete.set_suggestions(suggestions); self.auto_complete.set_suggestions(suggestions);
/* redraw self.container because we have got ridden of an autocomplete /* redraw self.container because we have got ridden of an autocomplete
@ -803,7 +810,7 @@ impl Component for StatusBar {
.take(hist_height) .take(hist_height)
.enumerate() .enumerate()
{ {
write_string_to_grid( let (x, y) = write_string_to_grid(
s.as_str(), s.as_str(),
grid, grid,
Color::Byte(88), // DarkRed, Color::Byte(88), // DarkRed,
@ -817,6 +824,14 @@ impl Component for StatusBar {
), ),
true, 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() { if y_offset + offset == self.auto_complete.cursor() {
change_colors( change_colors(
grid, grid,

View File

@ -1,7 +1,7 @@
use super::*; use super::*;
use fnv::FnvHashMap; use fnv::FnvHashMap;
type AutoCompleteFn = Box<Fn(&Context, &str) -> Vec<String> + Send>; type AutoCompleteFn = Box<Fn(&Context, &str) -> Vec<AutoCompleteEntry> + Send>;
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
enum FormFocus { 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<String> 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)] #[derive(Debug, PartialEq, Clone)]
pub struct AutoComplete { pub struct AutoComplete {
entries: Vec<String>, entries: Vec<AutoCompleteEntry>,
content: CellBuffer, content: CellBuffer,
cursor: usize, cursor: usize,
@ -637,7 +678,7 @@ impl Component for AutoComplete {
} }
impl AutoComplete { impl AutoComplete {
pub fn new(entries: Vec<String>) -> Self { pub fn new(entries: Vec<AutoCompleteEntry>) -> Self {
let mut ret = AutoComplete { let mut ret = AutoComplete {
entries: Vec::new(), entries: Vec::new(),
content: CellBuffer::default(), content: CellBuffer::default(),
@ -649,26 +690,39 @@ impl AutoComplete {
ret ret
} }
pub fn set_suggestions(&mut self, entries: Vec<String>) -> bool { pub fn set_suggestions(&mut self, entries: Vec<AutoCompleteEntry>) -> bool {
if entries.len() == self.entries.len() && entries == self.entries { if entries.len() == self.entries.len() && entries == self.entries {
return false;; return false;;
} }
let mut content = CellBuffer::new( 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(), entries.len(),
Cell::with_style(Color::Byte(23), Color::Byte(7), Attr::Default), Cell::with_style(Color::Byte(23), Color::Byte(7), Attr::Default),
); );
let width = content.cols(); let width = content.cols();
for (i, e) in entries.iter().enumerate() { for (i, e) in entries.iter().enumerate() {
write_string_to_grid( let (x, _) = write_string_to_grid(
e, &e.entry,
&mut content, &mut content,
Color::Byte(23), Color::Byte(23),
Color::Byte(7), Color::Byte(7),
((0, i), (width - 1, i)), ((0, i), (width - 1, i)),
false, 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( write_string_to_grid(
"", "",
&mut content, &mut content,
@ -712,10 +766,10 @@ impl AutoComplete {
self.entries.clear(); self.entries.clear();
self.cursor = 0; self.cursor = 0;
self.content.empty(); self.content.empty();
Some(ret) Some(ret.entry)
} }
pub fn suggestions(&self) -> &Vec<String> { pub fn suggestions(&self) -> &Vec<AutoCompleteEntry> {
&self.entries &self.entries
} }
} }

View File

@ -30,6 +30,140 @@ pub use crate::actions::ListingAction::{self, *};
pub use crate::actions::MailingListAction::{self, *}; pub use crate::actions::MailingListAction::{self, *};
pub use crate::actions::TabAction::{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<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<Action>, map!(ws!(tag!("close")), |_| Tab(Close)));
)
},
{ tags: ["goto"],
desc: "goto [n], switch to nth mailbox in this account",
parser: (
named!(
goto<Action>,
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<Action>,
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<Action>,
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<Action>,
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<Action>,
map!(ws!(tag!("toggle_thread_snooze")), |_| ToggleThreadSnooze)
);
)
},
{ tags: ["filter"],
desc: "filter <TERM>, filters list with given term",
parser:(
named!(filter<Action>,
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<Action>,
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<Action>,
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<Action>,
do_parse!(
ws!(tag!("env"))
>> key: map_res!(call!(not_line_ending), std::str::from_utf8)
>> (PrintEnv(key.to_string()))
)
);
)
}
]);
named!( named!(
usize_c<usize>, usize_c<usize>,
map_res!( map_res!(
@ -54,23 +188,6 @@ named!(
) )
); );
named!(close<Action>, map!(ws!(tag!("close")), |_| Tab(Close)));
named!(
goto<Action>,
preceded!(tag!("go "), map!(call!(usize_c), Action::ViewMailbox))
);
named!(
subsort<Action>,
do_parse!(tag!("subsort ") >> p: pair!(sortfield, sortorder) >> (SubSort(p.0, p.1)))
);
named!(
sort<Action>,
do_parse!(
tag!("sort ") >> p: separated_pair!(sortfield, tag!(" "), sortorder) >> (Sort(p.0, p.1))
)
);
named!( named!(
threaded<Action>, threaded<Action>,
map!(ws!(tag!("threaded")), |_| Listing(SetThreaded)) map!(ws!(tag!("threaded")), |_| Listing(SetThreaded))
@ -85,76 +202,10 @@ named!(
compact<Action>, compact<Action>,
map!(ws!(tag!("compact")), |_| Listing(SetCompact)) map!(ws!(tag!("compact")), |_| Listing(SetCompact))
); );
named!(
toggle<Action>,
preceded!(tag!("set "), alt_complete!(threaded | plain | compact))
);
named!( named!(
listing_action<Action>, listing_action<Action>,
alt_complete!(toggle | envelope_action | filter | toggle_thread_snooze) alt_complete!(toggle | envelope_action | filter | toggle_thread_snooze)
); );
named!(
toggle_thread_snooze<Action>,
map!(ws!(tag!("toggle_thread_snooze")), |_| ToggleThreadSnooze)
);
named!(
filter<Action>,
do_parse!(
ws!(tag!("filter"))
>> string: map_res!(call!(not_line_ending), std::str::from_utf8)
>> (Listing(Filter(String::from(string))))
)
);
named!(
mailinglist<Action>,
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<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<Action>,
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<Action>,
do_parse!(
ws!(tag!("env"))
>> key: map_res!(call!(not_line_ending), std::str::from_utf8)
>> (PrintEnv(key.to_string()))
)
);
named!(pub parse_command<Action>, named!(pub parse_command<Action>,
alt_complete!( goto | listing_action | sort | subsort | close | mailinglist | setenv | printenv) alt_complete!( goto | listing_action | sort | subsort | close | mailinglist | setenv | printenv)
); );