added auto_complete for input
This commit is contained in:
parent
20b2c31632
commit
37489944a7
|
@ -20,9 +20,10 @@ macros = { package = "discourse-macros", path = "./discourse-macros" }
|
|||
ahash = { version = "0.7", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
trybuild = "1.0.42"
|
||||
csscolorparser = "0.4" # examples/input.rs
|
||||
regex = "1.5" # examples/prompt_module.rs
|
||||
trybuild = { version = "1.0.42", features = ["diff"] }
|
||||
csscolorparser = "0.4" # examples/input.rs
|
||||
regex = "1.5" # examples/prompt_module.rs
|
||||
fuzzy-matcher = "0.3" # examples/file.rs
|
||||
|
||||
[features]
|
||||
default = ["crossterm", "ahash"]
|
||||
|
|
|
@ -8,13 +8,14 @@ use crate::helpers::*;
|
|||
|
||||
bitflags::bitflags! {
|
||||
pub struct BuilderMethods: u8 {
|
||||
const DEFAULT = 0b000_0001;
|
||||
const TRANSFORM = 0b000_0010;
|
||||
const VAL_FIL = 0b000_0100;
|
||||
const LIST = 0b000_1000;
|
||||
const MASK = 0b001_0000;
|
||||
const EXTENSION = 0b010_0000;
|
||||
const PLUGIN = 0b100_0000;
|
||||
const DEFAULT = 0b0000_0001;
|
||||
const TRANSFORM = 0b0000_0010;
|
||||
const VAL_FIL = 0b0000_0100;
|
||||
const AUTO_COMPLETE = 0b0000_1000;
|
||||
const LIST = 0b0001_0000;
|
||||
const MASK = 0b0010_0000;
|
||||
const EXTENSION = 0b0100_0000;
|
||||
const PLUGIN = 0b1000_0000;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -52,7 +53,13 @@ impl QuestionKind {
|
|||
|
||||
fn get_builder_methods(&self) -> BuilderMethods {
|
||||
match *self {
|
||||
QuestionKind::Input | QuestionKind::Int | QuestionKind::Float => {
|
||||
QuestionKind::Input => {
|
||||
BuilderMethods::DEFAULT
|
||||
| BuilderMethods::TRANSFORM
|
||||
| BuilderMethods::VAL_FIL
|
||||
| BuilderMethods::AUTO_COMPLETE
|
||||
}
|
||||
QuestionKind::Int | QuestionKind::Float => {
|
||||
BuilderMethods::DEFAULT
|
||||
| BuilderMethods::TRANSFORM
|
||||
| BuilderMethods::VAL_FIL
|
||||
|
@ -141,6 +148,7 @@ pub(crate) struct QuestionOpts {
|
|||
pub(crate) validate: Option<syn::Expr>,
|
||||
pub(crate) filter: Option<syn::Expr>,
|
||||
pub(crate) transform: Option<syn::Expr>,
|
||||
pub(crate) auto_complete: Option<syn::Expr>,
|
||||
|
||||
pub(crate) choices: Option<Choices>,
|
||||
pub(crate) page_size: Option<syn::Expr>,
|
||||
|
@ -164,6 +172,7 @@ impl Default for QuestionOpts {
|
|||
validate: None,
|
||||
filter: None,
|
||||
transform: None,
|
||||
auto_complete: None,
|
||||
|
||||
choices: None,
|
||||
page_size: None,
|
||||
|
@ -195,6 +204,9 @@ fn check_disallowed(
|
|||
ident == "filter") &&
|
||||
!allowed.contains(BuilderMethods::VAL_FIL)) ||
|
||||
|
||||
((ident == "auto_complete") &&
|
||||
!allowed.contains(BuilderMethods::AUTO_COMPLETE)) ||
|
||||
|
||||
((ident == "choices" ||
|
||||
ident == "page_size" ||
|
||||
ident == "should_loop") &&
|
||||
|
@ -269,6 +281,8 @@ impl Parse for Question {
|
|||
insert_non_dup(ident, &mut opts.filter, &content)?;
|
||||
} else if ident == "transform" {
|
||||
insert_non_dup(ident, &mut opts.transform, &content)?;
|
||||
} else if ident == "auto_complete" {
|
||||
insert_non_dup(ident, &mut opts.auto_complete, &content)?;
|
||||
} else if ident == "choices" {
|
||||
let parser = match kind {
|
||||
QuestionKind::Checkbox => Choices::parse_checkbox_choice,
|
||||
|
@ -382,6 +396,11 @@ impl quote::ToTokens for Question {
|
|||
quote_spanned! { transform.span() => .transform(#transform) },
|
||||
);
|
||||
}
|
||||
if let Some(ref auto_complete) = self.opts.auto_complete {
|
||||
tokens.extend(
|
||||
quote_spanned! { auto_complete.span() => .auto_complete(#auto_complete) },
|
||||
);
|
||||
}
|
||||
if let Some(ref choices) = self.opts.choices {
|
||||
tokens.extend(match self.kind {
|
||||
QuestionKind::Checkbox => {
|
||||
|
|
|
@ -83,7 +83,7 @@ where
|
|||
}
|
||||
|
||||
fn height(&mut self, _: Layout) -> u16 {
|
||||
0
|
||||
1
|
||||
}
|
||||
|
||||
fn cursor_pos(&mut self, layout: Layout) -> (u16, u16) {
|
||||
|
|
|
@ -86,7 +86,7 @@ impl<P: Prompt, B: Backend> Input<P, B> {
|
|||
};
|
||||
|
||||
self.base_row = self.backend.get_cursor()?.1;
|
||||
let height = self.prompt.height(self.layout());
|
||||
let height = self.prompt.height(self.layout()).saturating_sub(1);
|
||||
self.base_row = self.adjust_scrollback(height)?;
|
||||
self.base_col = prompt_len + hint_len;
|
||||
|
||||
|
@ -119,7 +119,7 @@ impl<P: Prompt, B: Backend> Input<P, B> {
|
|||
}
|
||||
|
||||
pub(super) fn render(&mut self) -> error::Result<()> {
|
||||
let height = self.prompt.height(self.layout());
|
||||
let height = self.prompt.height(self.layout()).saturating_sub(1);
|
||||
self.base_row = self.adjust_scrollback(height)?;
|
||||
self.clear(self.base_col)?;
|
||||
self.backend.set_cursor(self.base_col, self.base_row)?;
|
||||
|
@ -140,7 +140,7 @@ impl<P: Prompt, B: Backend> Input<P, B> {
|
|||
}
|
||||
|
||||
pub(super) fn goto_last_line(&mut self) -> error::Result<()> {
|
||||
let height = self.prompt.height(self.layout()) + 1;
|
||||
let height = self.prompt.height(self.layout());
|
||||
self.base_row = self.adjust_scrollback(height)?;
|
||||
self.backend.set_cursor(0, self.base_row + height as u16)
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use std::ops::{Index, IndexMut};
|
||||
|
||||
use crate::{
|
||||
backend::{Backend, MoveDirection, Stylize},
|
||||
error,
|
||||
|
@ -58,6 +60,18 @@ pub struct ListPicker<L> {
|
|||
pub list: L,
|
||||
}
|
||||
|
||||
impl<L: Index<usize>> ListPicker<L> {
|
||||
pub fn selected(&self) -> &L::Output {
|
||||
&self.list[self.at]
|
||||
}
|
||||
}
|
||||
|
||||
impl<L: IndexMut<usize>> ListPicker<L> {
|
||||
pub fn selected_mut(&mut self) -> &mut L::Output {
|
||||
&mut self.list[self.at]
|
||||
}
|
||||
}
|
||||
|
||||
impl<L: List> ListPicker<L> {
|
||||
/// Creates a new [`ListPicker`]
|
||||
pub fn new(list: L) -> Self {
|
||||
|
@ -548,7 +562,8 @@ impl<L: List> super::Widget for ListPicker<L> {
|
|||
self.update_heights(layout);
|
||||
|
||||
// Try to show everything
|
||||
self.height
|
||||
1 + self // Add one since we go to the next line
|
||||
.height
|
||||
// otherwise show whatever is possible
|
||||
.min(self.page_size())
|
||||
// but do not show less than a single element
|
||||
|
@ -559,7 +574,8 @@ impl<L: List> super::Widget for ListPicker<L> {
|
|||
.heights
|
||||
.get(self.at)
|
||||
.unwrap_or(&0)
|
||||
+ 1, // +1 since the message at the end takes one line
|
||||
// +1 if paginating since the message at the end takes one line
|
||||
+ self.is_paginating() as u16,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,6 +58,14 @@ impl<F> StringInput<F> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn get_at(&self) -> usize {
|
||||
self.at
|
||||
}
|
||||
|
||||
pub fn set_at(&mut self, at: usize) {
|
||||
self.at = at.min(self.value_len);
|
||||
}
|
||||
|
||||
/// The currently inputted value
|
||||
pub fn value(&self) -> &str {
|
||||
&self.value
|
||||
|
@ -70,6 +78,18 @@ impl<F> StringInput<F> {
|
|||
self.value = value;
|
||||
}
|
||||
|
||||
/// Replaces the value with the result of the function
|
||||
pub fn replace_with<W: FnOnce(String) -> String>(&mut self, with: W) {
|
||||
self.value = with(std::mem::take(&mut self.value));
|
||||
let old_len = self.value_len;
|
||||
self.value_len = self.value.chars().count();
|
||||
if self.at == old_len {
|
||||
self.at = self.value_len;
|
||||
} else {
|
||||
self.set_at(self.at);
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether any character has come to the input
|
||||
pub fn has_value(&self) -> bool {
|
||||
self.value.capacity() > 0
|
||||
|
@ -320,9 +340,9 @@ where
|
|||
|
||||
fn height(&mut self, layout: Layout) -> u16 {
|
||||
if self.value_len as u16 > layout.line_width() {
|
||||
1 + (self.value_len as u16 - layout.line_width()) / layout.width
|
||||
2 + (self.value_len as u16 - layout.line_width()) / layout.width
|
||||
} else {
|
||||
0
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ pub trait Widget {
|
|||
}
|
||||
}
|
||||
|
||||
impl Widget for &str {
|
||||
impl<T: std::ops::Deref<Target = str>> Widget for T {
|
||||
/// Does not allow multi-line strings
|
||||
fn render<B: Backend>(
|
||||
&mut self,
|
||||
|
@ -69,6 +69,6 @@ impl Widget for &str {
|
|||
|
||||
/// Does not allow multi-line strings
|
||||
fn height(&mut self, _: Layout) -> u16 {
|
||||
0
|
||||
1
|
||||
}
|
||||
}
|
||||
|
|
63
examples/file.rs
Normal file
63
examples/file.rs
Normal file
|
@ -0,0 +1,63 @@
|
|||
use std::path::Path;
|
||||
|
||||
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
|
||||
|
||||
fn auto_complete(p: String) -> Vec<String> {
|
||||
let current: &Path = p.as_ref();
|
||||
let (mut dir, last) = if p.ends_with('/') {
|
||||
(current, "")
|
||||
} else {
|
||||
let dir = current.parent().unwrap_or_else(|| "/".as_ref());
|
||||
let last = current
|
||||
.file_name()
|
||||
.and_then(std::ffi::OsStr::to_str)
|
||||
.unwrap_or("");
|
||||
(dir, last)
|
||||
};
|
||||
|
||||
if dir.to_str().unwrap().is_empty() {
|
||||
dir = ".".as_ref();
|
||||
}
|
||||
|
||||
let mut files: Vec<_> = match dir.read_dir() {
|
||||
Ok(files) => files
|
||||
.flatten()
|
||||
.flat_map(|file| {
|
||||
let path = file.path();
|
||||
let is_dir = path.is_dir();
|
||||
match path.into_os_string().into_string() {
|
||||
Ok(s) if is_dir => Some(s + "/"),
|
||||
Ok(s) => Some(s),
|
||||
Err(_) => None,
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
Err(_) => return vec![p],
|
||||
};
|
||||
|
||||
if files.is_empty() {
|
||||
return vec![p];
|
||||
} else {
|
||||
let fuzzer = SkimMatcherV2::default();
|
||||
files.sort_by_cached_key(|file| {
|
||||
fuzzer.fuzzy_match(file, last).unwrap_or(i64::MAX)
|
||||
});
|
||||
files
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let question = discourse::Question::input("a")
|
||||
.message("Enter a file")
|
||||
.auto_complete(|p, _| auto_complete(p))
|
||||
.validate(|p, _| {
|
||||
if (p.as_ref() as &Path).exists() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("file `{}` doesn't exist", p))
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
println!("{:#?}", discourse::prompt_one(question));
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
use std::ops::{Index, IndexMut};
|
||||
|
||||
use ui::{backend::Color, widgets::List, Widget};
|
||||
|
||||
use crate::ExpandItem;
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -67,6 +69,15 @@ impl<T> IndexMut<usize> for ChoiceList<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T> std::iter::FromIterator<T> for ChoiceList<T> {
|
||||
fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
|
||||
Self {
|
||||
choices: iter.into_iter().map(|c| Choice::Choice(c)).collect(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for ChoiceList<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
|
@ -79,6 +90,54 @@ impl<T> Default for ChoiceList<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T: Widget> List for ChoiceList<T> {
|
||||
fn render_item<B: ui::backend::Backend>(
|
||||
&mut self,
|
||||
index: usize,
|
||||
hovered: bool,
|
||||
mut layout: ui::Layout,
|
||||
b: &mut B,
|
||||
) -> ui::error::Result<()> {
|
||||
if hovered {
|
||||
b.set_fg(Color::Cyan)?;
|
||||
b.write_all("❯ ".as_bytes())?;
|
||||
} else {
|
||||
b.write_all(b" ")?;
|
||||
|
||||
if !self.is_selectable(index) {
|
||||
b.set_fg(Color::DarkGrey)?;
|
||||
}
|
||||
}
|
||||
|
||||
layout.offset_x += 2;
|
||||
self.choices[index].render(layout, b)?;
|
||||
|
||||
b.set_fg(Color::Reset)
|
||||
}
|
||||
|
||||
fn is_selectable(&self, index: usize) -> bool {
|
||||
matches!(self.choices[index], Choice::Choice(_))
|
||||
}
|
||||
|
||||
fn page_size(&self) -> usize {
|
||||
self.page_size
|
||||
}
|
||||
|
||||
fn should_loop(&self) -> bool {
|
||||
self.should_loop
|
||||
}
|
||||
|
||||
fn height_at(&mut self, index: usize, mut layout: ui::Layout) -> u16 {
|
||||
layout.offset_x += 2;
|
||||
|
||||
self[index].height(layout)
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.choices.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Choice<T> {
|
||||
Choice(T),
|
||||
|
|
|
@ -1,43 +1,122 @@
|
|||
use ui::{
|
||||
backend::{Backend, Stylize},
|
||||
error,
|
||||
events::KeyEvent,
|
||||
events::{KeyCode, KeyEvent},
|
||||
widgets, Prompt, Validation, Widget,
|
||||
};
|
||||
|
||||
use super::{Filter, Options, Transform, Validate};
|
||||
use super::{AutoComplete, ChoiceList, Filter, Options, Transform, Validate};
|
||||
use crate::{Answer, Answers};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Input<'i> {
|
||||
pub struct Input<'a> {
|
||||
default: Option<String>,
|
||||
filter: Filter<'i, String>,
|
||||
validate: Validate<'i, str>,
|
||||
transform: Transform<'i, str>,
|
||||
filter: Filter<'a, String>,
|
||||
validate: Validate<'a, str>,
|
||||
transform: Transform<'a, str>,
|
||||
auto_complete: AutoComplete<'a, String>,
|
||||
}
|
||||
|
||||
struct InputPrompt<'i, 'a> {
|
||||
message: String,
|
||||
input_opts: Input<'i>,
|
||||
input: widgets::StringInput,
|
||||
/// When the picker is Some, then currently the user is selecting from the
|
||||
/// auto complete options. The picker must not be used directly, and instead by used
|
||||
/// through `picker_op`. See `picker_op`s documentation for more.
|
||||
picker: Option<widgets::ListPicker<ChoiceList<String>>>,
|
||||
answers: &'a Answers,
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Calls a function with the given picker. Anytime the picker is used, it must be used
|
||||
/// through this function. This is is because the selected element of the picker doesn't
|
||||
/// actually contain the element, it is contained by the input. This function
|
||||
/// temporarily swaps the picker's selected item and the input, performs the function,
|
||||
/// and swaps back.
|
||||
fn picker_op<T, F: FnOnce(&mut widgets::ListPicker<ChoiceList<String>>) -> T>(
|
||||
input: &mut widgets::StringInput,
|
||||
picker: &mut widgets::ListPicker<ChoiceList<String>>,
|
||||
op: F,
|
||||
) -> T {
|
||||
let mut res = None;
|
||||
|
||||
input.replace_with(|mut s| {
|
||||
std::mem::swap(&mut s, picker.selected_mut().as_mut().unwrap_choice());
|
||||
res = Some(op(picker));
|
||||
std::mem::swap(&mut s, picker.selected_mut().as_mut().unwrap_choice());
|
||||
s
|
||||
});
|
||||
|
||||
res.unwrap()
|
||||
}
|
||||
|
||||
impl Widget for InputPrompt<'_, '_> {
|
||||
fn render<B: Backend>(
|
||||
&mut self,
|
||||
layout: ui::Layout,
|
||||
b: &mut B,
|
||||
) -> error::Result<()> {
|
||||
self.input.render(layout, b)
|
||||
self.input.render(layout, b)?;
|
||||
if let Some(ref mut picker) = self.picker {
|
||||
picker_op(&mut self.input, picker, |picker| picker.render(layout, b))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn height(&mut self, layout: ui::Layout) -> u16 {
|
||||
self.input.height(layout)
|
||||
let mut height = self.input.height(layout);
|
||||
if let Some(ref mut picker) = self.picker {
|
||||
height +=
|
||||
picker_op(&mut self.input, picker, |picker| picker.height(layout))
|
||||
- 1;
|
||||
}
|
||||
height
|
||||
}
|
||||
|
||||
fn handle_key(&mut self, key: KeyEvent) -> bool {
|
||||
self.input.handle_key(key)
|
||||
fn handle_key(&mut self, mut key: KeyEvent) -> bool {
|
||||
let Self {
|
||||
answers,
|
||||
input_opts,
|
||||
input,
|
||||
picker,
|
||||
..
|
||||
} = self;
|
||||
|
||||
match input_opts.auto_complete {
|
||||
AutoComplete::Sync(ref mut ac) if key.code == KeyCode::Tab => {
|
||||
if picker.is_some() {
|
||||
key.code = KeyCode::Down;
|
||||
} else {
|
||||
input.replace_with(|s| {
|
||||
let mut completions = ac(s, answers);
|
||||
assert!(!completions.is_empty());
|
||||
if completions.len() == 1 {
|
||||
completions.pop().unwrap()
|
||||
} else {
|
||||
let res = std::mem::take(&mut completions[0]);
|
||||
|
||||
*picker = Some(widgets::ListPicker::new(
|
||||
completions.into_iter().collect(),
|
||||
));
|
||||
|
||||
res
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if input.handle_key(key) {
|
||||
*picker = None;
|
||||
true
|
||||
} else if let Some(picker) = picker {
|
||||
picker_op(input, picker, |picker| picker.handle_key(key))
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn cursor_pos(&mut self, layout: ui::Layout) -> (u16, u16) {
|
||||
|
@ -70,7 +149,13 @@ impl Prompt for InputPrompt<'_, '_> {
|
|||
|
||||
ans
|
||||
}
|
||||
|
||||
fn validate(&mut self) -> Result<Validation, Self::ValidateErr> {
|
||||
if self.picker.is_some() {
|
||||
self.picker = None;
|
||||
return Ok(Validation::Continue);
|
||||
}
|
||||
|
||||
if !self.input.has_value() {
|
||||
if self.has_default() {
|
||||
return Ok(Validation::Finish);
|
||||
|
@ -85,9 +170,11 @@ impl Prompt for InputPrompt<'_, '_> {
|
|||
|
||||
Ok(Validation::Finish)
|
||||
}
|
||||
|
||||
fn has_default(&self) -> bool {
|
||||
self.input_opts.default.is_some()
|
||||
}
|
||||
|
||||
fn finish_default(self) -> <Self as ui::Prompt>::Output {
|
||||
remove_brackets(self.input_opts.default.unwrap())
|
||||
}
|
||||
|
@ -113,6 +200,7 @@ impl Input<'_> {
|
|||
message,
|
||||
input_opts: self,
|
||||
input: widgets::StringInput::default(),
|
||||
picker: None,
|
||||
answers,
|
||||
},
|
||||
b,
|
||||
|
@ -151,6 +239,7 @@ impl<'a> InputBuilder<'a> {
|
|||
}
|
||||
|
||||
crate::impl_options_builder!();
|
||||
crate::impl_auto_complete_builder!(String; input);
|
||||
crate::impl_filter_builder!(String; input);
|
||||
crate::impl_validate_builder!(str; input);
|
||||
crate::impl_transform_builder!(str; input);
|
||||
|
|
|
@ -151,7 +151,7 @@ impl Question<'_> {
|
|||
macro_rules! handler {
|
||||
($name:ident, $fn_trait:ident ( $($type:ty),* ) -> $return:ty) => {
|
||||
pub(crate) enum $name<'a, T> {
|
||||
Sync(Box<dyn $fn_trait( $($type),* ) -> $return + Send + Sync + 'a>),
|
||||
Sync(Box<dyn $fn_trait( $($type),* ) -> $return + 'a>),
|
||||
None,
|
||||
}
|
||||
|
||||
|
@ -181,7 +181,7 @@ macro_rules! handler {
|
|||
// The type signature of the function must only contain &T
|
||||
($name:ident, ?Sized $fn_trait:ident ( $($type:ty),* ) -> $return:ty) => {
|
||||
pub(crate) enum $name<'a, T: ?Sized> {
|
||||
Sync(Box<dyn $fn_trait( $($type),* ) -> $return + Send + Sync + 'a>),
|
||||
Sync(Box<dyn $fn_trait( $($type),* ) -> $return + 'a>),
|
||||
None,
|
||||
}
|
||||
|
||||
|
@ -210,6 +210,7 @@ macro_rules! handler {
|
|||
}
|
||||
|
||||
handler!(Filter, FnOnce(T, &Answers) -> T);
|
||||
handler!(AutoComplete, FnMut(T, &Answers) -> Vec<T>);
|
||||
handler!(Validate, ?Sized FnMut(&T, &Answers) -> Result<(), String>);
|
||||
handler!(ValidateByVal, FnMut(T, &Answers) -> Result<(), String>);
|
||||
handler!(Transform, ?Sized FnOnce(&T, &Answers, &mut dyn Backend) -> error::Result<()>);
|
||||
|
@ -224,7 +225,7 @@ macro_rules! impl_filter_builder {
|
|||
($t:ty; $inner:ident) => {
|
||||
pub fn filter<F>(mut self, filter: F) -> Self
|
||||
where
|
||||
F: FnOnce($t, &crate::Answers) -> $t + Send + Sync + 'a,
|
||||
F: FnOnce($t, &crate::Answers) -> $t + 'a,
|
||||
{
|
||||
self.$inner.filter = crate::question::Filter::Sync(Box::new(filter));
|
||||
self
|
||||
|
@ -232,13 +233,28 @@ macro_rules! impl_filter_builder {
|
|||
};
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[macro_export]
|
||||
macro_rules! impl_auto_complete_builder {
|
||||
($t:ty; $inner:ident) => {
|
||||
pub fn auto_complete<F>(mut self, auto_complete: F) -> Self
|
||||
where
|
||||
F: FnMut($t, &crate::Answers) -> Vec<$t> + 'a,
|
||||
{
|
||||
self.$inner.auto_complete =
|
||||
crate::question::AutoComplete::Sync(Box::new(auto_complete));
|
||||
self
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[macro_export]
|
||||
macro_rules! impl_validate_builder {
|
||||
($t:ty; $inner:ident) => {
|
||||
pub fn validate<F>(mut self, filter: F) -> Self
|
||||
where
|
||||
F: FnMut(&$t, &crate::Answers) -> Result<(), String> + Send + Sync + 'a,
|
||||
F: FnMut(&$t, &crate::Answers) -> Result<(), String> + 'a,
|
||||
{
|
||||
self.$inner.validate = crate::question::Validate::Sync(Box::new(filter));
|
||||
self
|
||||
|
@ -248,7 +264,7 @@ macro_rules! impl_validate_builder {
|
|||
(by val $t:ty; $inner:ident) => {
|
||||
pub fn validate<F>(mut self, filter: F) -> Self
|
||||
where
|
||||
F: FnMut($t, &crate::Answers) -> Result<(), String> + Send + Sync + 'a,
|
||||
F: FnMut($t, &crate::Answers) -> Result<(), String> + 'a,
|
||||
{
|
||||
self.$inner.validate =
|
||||
crate::question::ValidateByVal::Sync(Box::new(filter));
|
||||
|
@ -268,8 +284,6 @@ macro_rules! impl_transform_builder {
|
|||
&crate::Answers,
|
||||
&mut dyn Backend,
|
||||
) -> ui::error::Result<()>
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'a,
|
||||
{
|
||||
self.$inner.transform =
|
||||
|
@ -286,8 +300,6 @@ macro_rules! impl_transform_builder {
|
|||
&crate::Answers,
|
||||
&mut dyn Backend,
|
||||
) -> ui::error::Result<()>
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'a,
|
||||
{
|
||||
self.$inner.transform =
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use ui::{
|
||||
backend::{Backend, Color, Stylize},
|
||||
backend::{Backend, Stylize},
|
||||
error,
|
||||
events::KeyEvent,
|
||||
widgets::{self, Text},
|
||||
|
@ -85,34 +85,18 @@ impl widgets::List for Select<'_> {
|
|||
&mut self,
|
||||
index: usize,
|
||||
hovered: bool,
|
||||
mut layout: ui::Layout,
|
||||
b: &mut B,
|
||||
layout: ui::Layout,
|
||||
backend: &mut B,
|
||||
) -> error::Result<()> {
|
||||
if hovered {
|
||||
b.set_fg(Color::Cyan)?;
|
||||
b.write_all("❯ ".as_bytes())?;
|
||||
} else {
|
||||
b.write_all(b" ")?;
|
||||
|
||||
if !self.is_selectable(index) {
|
||||
b.set_fg(Color::DarkGrey)?;
|
||||
}
|
||||
}
|
||||
|
||||
layout.offset_x += 2;
|
||||
self.choices[index].render(layout, b)?;
|
||||
|
||||
b.set_fg(Color::Reset)
|
||||
self.choices.render_item(index, hovered, layout, backend)
|
||||
}
|
||||
|
||||
fn is_selectable(&self, index: usize) -> bool {
|
||||
!self.choices[index].is_separator()
|
||||
self.choices.is_selectable(index)
|
||||
}
|
||||
|
||||
fn height_at(&mut self, index: usize, mut layout: ui::Layout) -> u16 {
|
||||
layout.offset_x += 2;
|
||||
|
||||
self.choices[index].height(layout)
|
||||
fn height_at(&mut self, index: usize, layout: ui::Layout) -> u16 {
|
||||
self.choices.height_at(index, layout)
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
|
|
|
@ -33,6 +33,7 @@ fn duplicate() {
|
|||
t.compile_fail("validate");
|
||||
t.compile_fail("filter");
|
||||
t.compile_fail("transform");
|
||||
t.compile_fail("auto_complete");
|
||||
t.compile_fail("choices");
|
||||
t.compile_fail("page_size");
|
||||
t.compile_fail("should_loop");
|
||||
|
@ -62,6 +63,7 @@ fn checkbox() {
|
|||
t.pass("valid");
|
||||
t.compile_fail("default");
|
||||
t.compile_fail("default_with_sep");
|
||||
t.compile_fail("auto_complete");
|
||||
t.compile_fail("mask");
|
||||
t.compile_fail("extension");
|
||||
t.compile_fail("plugin");
|
||||
|
@ -74,6 +76,7 @@ fn confirm() {
|
|||
t.pass("valid");
|
||||
t.compile_fail("filter");
|
||||
t.compile_fail("validate");
|
||||
t.compile_fail("auto_complete");
|
||||
t.compile_fail("choices");
|
||||
t.compile_fail("should_loop");
|
||||
t.compile_fail("page_size");
|
||||
|
@ -87,6 +90,7 @@ fn editor() {
|
|||
let t = Runner::new("editor");
|
||||
|
||||
t.pass("valid");
|
||||
t.compile_fail("auto_complete");
|
||||
t.compile_fail("choices");
|
||||
t.compile_fail("should_loop");
|
||||
t.compile_fail("page_size");
|
||||
|
@ -101,6 +105,7 @@ fn expand() {
|
|||
t.pass("valid");
|
||||
t.compile_fail("filter");
|
||||
t.compile_fail("validate");
|
||||
t.compile_fail("auto_complete");
|
||||
t.compile_fail("mask");
|
||||
t.compile_fail("extension");
|
||||
t.compile_fail("plugin");
|
||||
|
@ -111,6 +116,7 @@ fn float() {
|
|||
let t = Runner::new("float");
|
||||
|
||||
t.pass("valid");
|
||||
t.compile_fail("auto_complete");
|
||||
t.compile_fail("choices");
|
||||
t.compile_fail("should_loop");
|
||||
t.compile_fail("page_size");
|
||||
|
@ -137,6 +143,7 @@ fn int() {
|
|||
let t = Runner::new("int");
|
||||
|
||||
t.pass("valid");
|
||||
t.compile_fail("auto_complete");
|
||||
t.compile_fail("choices");
|
||||
t.compile_fail("should_loop");
|
||||
t.compile_fail("page_size");
|
||||
|
@ -152,6 +159,7 @@ fn select() {
|
|||
t.pass("valid");
|
||||
t.compile_fail("filter");
|
||||
t.compile_fail("validate");
|
||||
t.compile_fail("auto_complete");
|
||||
t.compile_fail("mask");
|
||||
t.compile_fail("extension");
|
||||
t.compile_fail("plugin");
|
||||
|
@ -163,6 +171,7 @@ fn password() {
|
|||
|
||||
t.pass("valid");
|
||||
t.compile_fail("default");
|
||||
t.compile_fail("auto_complete");
|
||||
t.compile_fail("choices");
|
||||
t.compile_fail("should_loop");
|
||||
t.compile_fail("page_size");
|
||||
|
@ -179,6 +188,7 @@ fn plugin() {
|
|||
t.compile_fail("transform");
|
||||
t.compile_fail("filter");
|
||||
t.compile_fail("validate");
|
||||
t.compile_fail("auto_complete");
|
||||
t.compile_fail("choices");
|
||||
t.compile_fail("should_loop");
|
||||
t.compile_fail("page_size");
|
||||
|
@ -193,6 +203,7 @@ fn raw_select() {
|
|||
t.pass("valid");
|
||||
t.compile_fail("filter");
|
||||
t.compile_fail("validate");
|
||||
t.compile_fail("auto_complete");
|
||||
t.compile_fail("mask");
|
||||
t.compile_fail("extension");
|
||||
t.compile_fail("plugin");
|
||||
|
|
5
tests/macros/checkbox/auto_complete.rs
Normal file
5
tests/macros/checkbox/auto_complete.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
fn main() {
|
||||
discourse::questions![Checkbox {
|
||||
auto_complete: todo!()
|
||||
}];
|
||||
}
|
5
tests/macros/checkbox/auto_complete.stderr
Normal file
5
tests/macros/checkbox/auto_complete.stderr
Normal file
|
@ -0,0 +1,5 @@
|
|||
error: option `auto_complete` does not exist for kind `checkbox`
|
||||
--> $DIR/auto_complete.rs:3:9
|
||||
|
|
||||
3 | auto_complete: todo!()
|
||||
| ^^^^^^^^^^^^^
|
5
tests/macros/confirm/auto_complete.rs
Normal file
5
tests/macros/confirm/auto_complete.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
fn main() {
|
||||
discourse::questions![Confirm {
|
||||
auto_complete: todo!()
|
||||
}];
|
||||
}
|
5
tests/macros/confirm/auto_complete.stderr
Normal file
5
tests/macros/confirm/auto_complete.stderr
Normal file
|
@ -0,0 +1,5 @@
|
|||
error: option `auto_complete` does not exist for kind `confirm`
|
||||
--> $DIR/auto_complete.rs:3:9
|
||||
|
|
||||
3 | auto_complete: todo!()
|
||||
| ^^^^^^^^^^^^^
|
6
tests/macros/duplicate/auto_complete.rs
Normal file
6
tests/macros/duplicate/auto_complete.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
fn main() {
|
||||
discourse::questions![Input {
|
||||
auto_complete: todo!(),
|
||||
auto_complete: todo!()
|
||||
}];
|
||||
}
|
5
tests/macros/duplicate/auto_complete.stderr
Normal file
5
tests/macros/duplicate/auto_complete.stderr
Normal file
|
@ -0,0 +1,5 @@
|
|||
error: duplicate option `auto_complete`
|
||||
--> $DIR/auto_complete.rs:4:9
|
||||
|
|
||||
4 | auto_complete: todo!()
|
||||
| ^^^^^^^^^^^^^
|
5
tests/macros/editor/auto_complete.rs
Normal file
5
tests/macros/editor/auto_complete.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
fn main() {
|
||||
discourse::questions![Editor {
|
||||
auto_complete: todo!()
|
||||
}];
|
||||
}
|
5
tests/macros/editor/auto_complete.stderr
Normal file
5
tests/macros/editor/auto_complete.stderr
Normal file
|
@ -0,0 +1,5 @@
|
|||
error: option `auto_complete` does not exist for kind `editor`
|
||||
--> $DIR/auto_complete.rs:3:9
|
||||
|
|
||||
3 | auto_complete: todo!()
|
||||
| ^^^^^^^^^^^^^
|
5
tests/macros/expand/auto_complete.rs
Normal file
5
tests/macros/expand/auto_complete.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
fn main() {
|
||||
discourse::questions![Expand {
|
||||
auto_complete: todo!()
|
||||
}];
|
||||
}
|
5
tests/macros/expand/auto_complete.stderr
Normal file
5
tests/macros/expand/auto_complete.stderr
Normal file
|
@ -0,0 +1,5 @@
|
|||
error: option `auto_complete` does not exist for kind `expand`
|
||||
--> $DIR/auto_complete.rs:3:9
|
||||
|
|
||||
3 | auto_complete: todo!()
|
||||
| ^^^^^^^^^^^^^
|
5
tests/macros/float/auto_complete.rs
Normal file
5
tests/macros/float/auto_complete.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
fn main() {
|
||||
discourse::questions![Float {
|
||||
auto_complete: todo!()
|
||||
}];
|
||||
}
|
5
tests/macros/float/auto_complete.stderr
Normal file
5
tests/macros/float/auto_complete.stderr
Normal file
|
@ -0,0 +1,5 @@
|
|||
error: option `auto_complete` does not exist for kind `float`
|
||||
--> $DIR/auto_complete.rs:3:9
|
||||
|
|
||||
3 | auto_complete: todo!()
|
||||
| ^^^^^^^^^^^^^
|
|
@ -5,5 +5,6 @@ fn main() {
|
|||
transform: |_, _, _| Ok(()),
|
||||
validate: |_, _| Ok(()),
|
||||
filter: |t, _| t,
|
||||
auto_complete: |t, _| vec![t],
|
||||
}];
|
||||
}
|
||||
|
|
5
tests/macros/int/auto_complete.rs
Normal file
5
tests/macros/int/auto_complete.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
fn main() {
|
||||
discourse::questions![Int {
|
||||
auto_complete: todo!()
|
||||
}];
|
||||
}
|
5
tests/macros/int/auto_complete.stderr
Normal file
5
tests/macros/int/auto_complete.stderr
Normal file
|
@ -0,0 +1,5 @@
|
|||
error: option `auto_complete` does not exist for kind `int`
|
||||
--> $DIR/auto_complete.rs:3:9
|
||||
|
|
||||
3 | auto_complete: todo!()
|
||||
| ^^^^^^^^^^^^^
|
5
tests/macros/password/auto_complete.rs
Normal file
5
tests/macros/password/auto_complete.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
fn main() {
|
||||
discourse::questions![Password {
|
||||
auto_complete: todo!()
|
||||
}];
|
||||
}
|
5
tests/macros/password/auto_complete.stderr
Normal file
5
tests/macros/password/auto_complete.stderr
Normal file
|
@ -0,0 +1,5 @@
|
|||
error: option `auto_complete` does not exist for kind `password`
|
||||
--> $DIR/auto_complete.rs:3:9
|
||||
|
|
||||
3 | auto_complete: todo!()
|
||||
| ^^^^^^^^^^^^^
|
5
tests/macros/plugin/auto_complete.rs
Normal file
5
tests/macros/plugin/auto_complete.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
fn main() {
|
||||
discourse::questions![Plugin {
|
||||
auto_complete: todo!()
|
||||
}];
|
||||
}
|
5
tests/macros/plugin/auto_complete.stderr
Normal file
5
tests/macros/plugin/auto_complete.stderr
Normal file
|
@ -0,0 +1,5 @@
|
|||
error: option `auto_complete` does not exist for kind `plugin`
|
||||
--> $DIR/auto_complete.rs:3:9
|
||||
|
|
||||
3 | auto_complete: todo!()
|
||||
| ^^^^^^^^^^^^^
|
5
tests/macros/raw_select/auto_complete.rs
Normal file
5
tests/macros/raw_select/auto_complete.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
fn main() {
|
||||
discourse::questions![RawSelect {
|
||||
auto_complete: todo!()
|
||||
}];
|
||||
}
|
5
tests/macros/raw_select/auto_complete.stderr
Normal file
5
tests/macros/raw_select/auto_complete.stderr
Normal file
|
@ -0,0 +1,5 @@
|
|||
error: option `auto_complete` does not exist for kind `raw_select`
|
||||
--> $DIR/auto_complete.rs:3:9
|
||||
|
|
||||
3 | auto_complete: todo!()
|
||||
| ^^^^^^^^^^^^^
|
5
tests/macros/select/auto_complete.rs
Normal file
5
tests/macros/select/auto_complete.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
fn main() {
|
||||
discourse::questions![Select {
|
||||
auto_complete: todo!()
|
||||
}];
|
||||
}
|
5
tests/macros/select/auto_complete.stderr
Normal file
5
tests/macros/select/auto_complete.stderr
Normal file
|
@ -0,0 +1,5 @@
|
|||
error: option `auto_complete` does not exist for kind `select`
|
||||
--> $DIR/auto_complete.rs:3:9
|
||||
|
|
||||
3 | auto_complete: todo!()
|
||||
| ^^^^^^^^^^^^^
|
Loading…
Reference in New Issue
Block a user