From 37489944a7f44020c290462a7c6afd654630c0c6 Mon Sep 17 00:00:00 2001 From: Lutetium-Vanadium Date: Sat, 29 May 2021 14:52:37 +0530 Subject: [PATCH] added auto_complete for input --- Cargo.toml | 7 +- discourse-macros/src/question.rs | 35 ++++-- discourse-ui/src/char_input.rs | 2 +- discourse-ui/src/input.rs | 6 +- discourse-ui/src/list.rs | 20 +++- discourse-ui/src/string_input.rs | 24 +++- discourse-ui/src/widget.rs | 4 +- examples/file.rs | 63 +++++++++++ src/question/choice.rs | 59 ++++++++++ src/question/input.rs | 109 +++++++++++++++++-- src/question/mod.rs | 30 +++-- src/question/select.rs | 30 ++--- tests/macros.rs | 11 ++ tests/macros/checkbox/auto_complete.rs | 5 + tests/macros/checkbox/auto_complete.stderr | 5 + tests/macros/confirm/auto_complete.rs | 5 + tests/macros/confirm/auto_complete.stderr | 5 + tests/macros/duplicate/auto_complete.rs | 6 + tests/macros/duplicate/auto_complete.stderr | 5 + tests/macros/editor/auto_complete.rs | 5 + tests/macros/editor/auto_complete.stderr | 5 + tests/macros/expand/auto_complete.rs | 5 + tests/macros/expand/auto_complete.stderr | 5 + tests/macros/float/auto_complete.rs | 5 + tests/macros/float/auto_complete.stderr | 5 + tests/macros/input/valid.rs | 1 + tests/macros/int/auto_complete.rs | 5 + tests/macros/int/auto_complete.stderr | 5 + tests/macros/password/auto_complete.rs | 5 + tests/macros/password/auto_complete.stderr | 5 + tests/macros/plugin/auto_complete.rs | 5 + tests/macros/plugin/auto_complete.stderr | 5 + tests/macros/raw_select/auto_complete.rs | 5 + tests/macros/raw_select/auto_complete.stderr | 5 + tests/macros/select/auto_complete.rs | 5 + tests/macros/select/auto_complete.stderr | 5 + 36 files changed, 449 insertions(+), 63 deletions(-) create mode 100644 examples/file.rs create mode 100644 tests/macros/checkbox/auto_complete.rs create mode 100644 tests/macros/checkbox/auto_complete.stderr create mode 100644 tests/macros/confirm/auto_complete.rs create mode 100644 tests/macros/confirm/auto_complete.stderr create mode 100644 tests/macros/duplicate/auto_complete.rs create mode 100644 tests/macros/duplicate/auto_complete.stderr create mode 100644 tests/macros/editor/auto_complete.rs create mode 100644 tests/macros/editor/auto_complete.stderr create mode 100644 tests/macros/expand/auto_complete.rs create mode 100644 tests/macros/expand/auto_complete.stderr create mode 100644 tests/macros/float/auto_complete.rs create mode 100644 tests/macros/float/auto_complete.stderr create mode 100644 tests/macros/int/auto_complete.rs create mode 100644 tests/macros/int/auto_complete.stderr create mode 100644 tests/macros/password/auto_complete.rs create mode 100644 tests/macros/password/auto_complete.stderr create mode 100644 tests/macros/plugin/auto_complete.rs create mode 100644 tests/macros/plugin/auto_complete.stderr create mode 100644 tests/macros/raw_select/auto_complete.rs create mode 100644 tests/macros/raw_select/auto_complete.stderr create mode 100644 tests/macros/select/auto_complete.rs create mode 100644 tests/macros/select/auto_complete.stderr diff --git a/Cargo.toml b/Cargo.toml index c4bec07..69233ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] diff --git a/discourse-macros/src/question.rs b/discourse-macros/src/question.rs index eadc099..07e5f15 100644 --- a/discourse-macros/src/question.rs +++ b/discourse-macros/src/question.rs @@ -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, pub(crate) filter: Option, pub(crate) transform: Option, + pub(crate) auto_complete: Option, pub(crate) choices: Option, pub(crate) page_size: Option, @@ -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 => { diff --git a/discourse-ui/src/char_input.rs b/discourse-ui/src/char_input.rs index 0b059c7..08a0a50 100644 --- a/discourse-ui/src/char_input.rs +++ b/discourse-ui/src/char_input.rs @@ -83,7 +83,7 @@ where } fn height(&mut self, _: Layout) -> u16 { - 0 + 1 } fn cursor_pos(&mut self, layout: Layout) -> (u16, u16) { diff --git a/discourse-ui/src/input.rs b/discourse-ui/src/input.rs index 1e359f8..0eff872 100644 --- a/discourse-ui/src/input.rs +++ b/discourse-ui/src/input.rs @@ -86,7 +86,7 @@ impl Input { }; 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 Input { } 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 Input { } 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) } diff --git a/discourse-ui/src/list.rs b/discourse-ui/src/list.rs index 29d9311..818a9a1 100644 --- a/discourse-ui/src/list.rs +++ b/discourse-ui/src/list.rs @@ -1,3 +1,5 @@ +use std::ops::{Index, IndexMut}; + use crate::{ backend::{Backend, MoveDirection, Stylize}, error, @@ -58,6 +60,18 @@ pub struct ListPicker { pub list: L, } +impl> ListPicker { + pub fn selected(&self) -> &L::Output { + &self.list[self.at] + } +} + +impl> ListPicker { + pub fn selected_mut(&mut self) -> &mut L::Output { + &mut self.list[self.at] + } +} + impl ListPicker { /// Creates a new [`ListPicker`] pub fn new(list: L) -> Self { @@ -548,7 +562,8 @@ impl super::Widget for ListPicker { 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 super::Widget for ListPicker { .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, ) } } diff --git a/discourse-ui/src/string_input.rs b/discourse-ui/src/string_input.rs index 86ed0ec..6711c0b 100644 --- a/discourse-ui/src/string_input.rs +++ b/discourse-ui/src/string_input.rs @@ -58,6 +58,14 @@ impl StringInput { } } + 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 StringInput { self.value = value; } + /// Replaces the value with the result of the function + pub fn replace_with 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 } } diff --git a/discourse-ui/src/widget.rs b/discourse-ui/src/widget.rs index 19de39e..dcec5b9 100644 --- a/discourse-ui/src/widget.rs +++ b/discourse-ui/src/widget.rs @@ -28,7 +28,7 @@ pub trait Widget { } } -impl Widget for &str { +impl> Widget for T { /// Does not allow multi-line strings fn render( &mut self, @@ -69,6 +69,6 @@ impl Widget for &str { /// Does not allow multi-line strings fn height(&mut self, _: Layout) -> u16 { - 0 + 1 } } diff --git a/examples/file.rs b/examples/file.rs new file mode 100644 index 0000000..526cb98 --- /dev/null +++ b/examples/file.rs @@ -0,0 +1,63 @@ +use std::path::Path; + +use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; + +fn auto_complete(p: String) -> Vec { + 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)); +} diff --git a/src/question/choice.rs b/src/question/choice.rs index f0c4ac1..641d87c 100644 --- a/src/question/choice.rs +++ b/src/question/choice.rs @@ -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 IndexMut for ChoiceList { } } +impl std::iter::FromIterator for ChoiceList { + fn from_iter>(iter: I) -> Self { + Self { + choices: iter.into_iter().map(|c| Choice::Choice(c)).collect(), + ..Default::default() + } + } +} + impl Default for ChoiceList { fn default() -> Self { Self { @@ -79,6 +90,54 @@ impl Default for ChoiceList { } } +impl List for ChoiceList { + fn render_item( + &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 { Choice(T), diff --git a/src/question/input.rs b/src/question/input.rs index f9b63d6..e17b6f3 100644 --- a/src/question/input.rs +++ b/src/question/input.rs @@ -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, - 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>>, 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>( + input: &mut widgets::StringInput, + picker: &mut widgets::ListPicker>, + 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( &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 { + 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) -> ::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); diff --git a/src/question/mod.rs b/src/question/mod.rs index a286fc2..55596b7 100644 --- a/src/question/mod.rs +++ b/src/question/mod.rs @@ -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 $return + Send + Sync + 'a>), + Sync(Box $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 $return + Send + Sync + 'a>), + Sync(Box $return + 'a>), None, } @@ -210,6 +210,7 @@ macro_rules! handler { } handler!(Filter, FnOnce(T, &Answers) -> T); +handler!(AutoComplete, FnMut(T, &Answers) -> Vec); 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(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(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(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(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 = diff --git a/src/question/select.rs b/src/question/select.rs index 7fb2d6d..c858f21 100644 --- a/src/question/select.rs +++ b/src/question/select.rs @@ -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 { diff --git a/tests/macros.rs b/tests/macros.rs index c1e6e49..c0fc3a2 100644 --- a/tests/macros.rs +++ b/tests/macros.rs @@ -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"); diff --git a/tests/macros/checkbox/auto_complete.rs b/tests/macros/checkbox/auto_complete.rs new file mode 100644 index 0000000..1fc1eb0 --- /dev/null +++ b/tests/macros/checkbox/auto_complete.rs @@ -0,0 +1,5 @@ +fn main() { + discourse::questions![Checkbox { + auto_complete: todo!() + }]; +} diff --git a/tests/macros/checkbox/auto_complete.stderr b/tests/macros/checkbox/auto_complete.stderr new file mode 100644 index 0000000..0aac337 --- /dev/null +++ b/tests/macros/checkbox/auto_complete.stderr @@ -0,0 +1,5 @@ +error: option `auto_complete` does not exist for kind `checkbox` + --> $DIR/auto_complete.rs:3:9 + | +3 | auto_complete: todo!() + | ^^^^^^^^^^^^^ diff --git a/tests/macros/confirm/auto_complete.rs b/tests/macros/confirm/auto_complete.rs new file mode 100644 index 0000000..027ba73 --- /dev/null +++ b/tests/macros/confirm/auto_complete.rs @@ -0,0 +1,5 @@ +fn main() { + discourse::questions![Confirm { + auto_complete: todo!() + }]; +} diff --git a/tests/macros/confirm/auto_complete.stderr b/tests/macros/confirm/auto_complete.stderr new file mode 100644 index 0000000..d3f957a --- /dev/null +++ b/tests/macros/confirm/auto_complete.stderr @@ -0,0 +1,5 @@ +error: option `auto_complete` does not exist for kind `confirm` + --> $DIR/auto_complete.rs:3:9 + | +3 | auto_complete: todo!() + | ^^^^^^^^^^^^^ diff --git a/tests/macros/duplicate/auto_complete.rs b/tests/macros/duplicate/auto_complete.rs new file mode 100644 index 0000000..14dade2 --- /dev/null +++ b/tests/macros/duplicate/auto_complete.rs @@ -0,0 +1,6 @@ +fn main() { + discourse::questions![Input { + auto_complete: todo!(), + auto_complete: todo!() + }]; +} diff --git a/tests/macros/duplicate/auto_complete.stderr b/tests/macros/duplicate/auto_complete.stderr new file mode 100644 index 0000000..4f38798 --- /dev/null +++ b/tests/macros/duplicate/auto_complete.stderr @@ -0,0 +1,5 @@ +error: duplicate option `auto_complete` + --> $DIR/auto_complete.rs:4:9 + | +4 | auto_complete: todo!() + | ^^^^^^^^^^^^^ diff --git a/tests/macros/editor/auto_complete.rs b/tests/macros/editor/auto_complete.rs new file mode 100644 index 0000000..5a5cfc2 --- /dev/null +++ b/tests/macros/editor/auto_complete.rs @@ -0,0 +1,5 @@ +fn main() { + discourse::questions![Editor { + auto_complete: todo!() + }]; +} diff --git a/tests/macros/editor/auto_complete.stderr b/tests/macros/editor/auto_complete.stderr new file mode 100644 index 0000000..e70fd85 --- /dev/null +++ b/tests/macros/editor/auto_complete.stderr @@ -0,0 +1,5 @@ +error: option `auto_complete` does not exist for kind `editor` + --> $DIR/auto_complete.rs:3:9 + | +3 | auto_complete: todo!() + | ^^^^^^^^^^^^^ diff --git a/tests/macros/expand/auto_complete.rs b/tests/macros/expand/auto_complete.rs new file mode 100644 index 0000000..646aaec --- /dev/null +++ b/tests/macros/expand/auto_complete.rs @@ -0,0 +1,5 @@ +fn main() { + discourse::questions![Expand { + auto_complete: todo!() + }]; +} diff --git a/tests/macros/expand/auto_complete.stderr b/tests/macros/expand/auto_complete.stderr new file mode 100644 index 0000000..5f93fb3 --- /dev/null +++ b/tests/macros/expand/auto_complete.stderr @@ -0,0 +1,5 @@ +error: option `auto_complete` does not exist for kind `expand` + --> $DIR/auto_complete.rs:3:9 + | +3 | auto_complete: todo!() + | ^^^^^^^^^^^^^ diff --git a/tests/macros/float/auto_complete.rs b/tests/macros/float/auto_complete.rs new file mode 100644 index 0000000..f3f33e8 --- /dev/null +++ b/tests/macros/float/auto_complete.rs @@ -0,0 +1,5 @@ +fn main() { + discourse::questions![Float { + auto_complete: todo!() + }]; +} diff --git a/tests/macros/float/auto_complete.stderr b/tests/macros/float/auto_complete.stderr new file mode 100644 index 0000000..4ae4162 --- /dev/null +++ b/tests/macros/float/auto_complete.stderr @@ -0,0 +1,5 @@ +error: option `auto_complete` does not exist for kind `float` + --> $DIR/auto_complete.rs:3:9 + | +3 | auto_complete: todo!() + | ^^^^^^^^^^^^^ diff --git a/tests/macros/input/valid.rs b/tests/macros/input/valid.rs index 4d950d6..a41baad 100644 --- a/tests/macros/input/valid.rs +++ b/tests/macros/input/valid.rs @@ -5,5 +5,6 @@ fn main() { transform: |_, _, _| Ok(()), validate: |_, _| Ok(()), filter: |t, _| t, + auto_complete: |t, _| vec![t], }]; } diff --git a/tests/macros/int/auto_complete.rs b/tests/macros/int/auto_complete.rs new file mode 100644 index 0000000..efaf128 --- /dev/null +++ b/tests/macros/int/auto_complete.rs @@ -0,0 +1,5 @@ +fn main() { + discourse::questions![Int { + auto_complete: todo!() + }]; +} diff --git a/tests/macros/int/auto_complete.stderr b/tests/macros/int/auto_complete.stderr new file mode 100644 index 0000000..ce401b6 --- /dev/null +++ b/tests/macros/int/auto_complete.stderr @@ -0,0 +1,5 @@ +error: option `auto_complete` does not exist for kind `int` + --> $DIR/auto_complete.rs:3:9 + | +3 | auto_complete: todo!() + | ^^^^^^^^^^^^^ diff --git a/tests/macros/password/auto_complete.rs b/tests/macros/password/auto_complete.rs new file mode 100644 index 0000000..28c8146 --- /dev/null +++ b/tests/macros/password/auto_complete.rs @@ -0,0 +1,5 @@ +fn main() { + discourse::questions![Password { + auto_complete: todo!() + }]; +} diff --git a/tests/macros/password/auto_complete.stderr b/tests/macros/password/auto_complete.stderr new file mode 100644 index 0000000..267a467 --- /dev/null +++ b/tests/macros/password/auto_complete.stderr @@ -0,0 +1,5 @@ +error: option `auto_complete` does not exist for kind `password` + --> $DIR/auto_complete.rs:3:9 + | +3 | auto_complete: todo!() + | ^^^^^^^^^^^^^ diff --git a/tests/macros/plugin/auto_complete.rs b/tests/macros/plugin/auto_complete.rs new file mode 100644 index 0000000..cb6b3c7 --- /dev/null +++ b/tests/macros/plugin/auto_complete.rs @@ -0,0 +1,5 @@ +fn main() { + discourse::questions![Plugin { + auto_complete: todo!() + }]; +} diff --git a/tests/macros/plugin/auto_complete.stderr b/tests/macros/plugin/auto_complete.stderr new file mode 100644 index 0000000..8f666de --- /dev/null +++ b/tests/macros/plugin/auto_complete.stderr @@ -0,0 +1,5 @@ +error: option `auto_complete` does not exist for kind `plugin` + --> $DIR/auto_complete.rs:3:9 + | +3 | auto_complete: todo!() + | ^^^^^^^^^^^^^ diff --git a/tests/macros/raw_select/auto_complete.rs b/tests/macros/raw_select/auto_complete.rs new file mode 100644 index 0000000..494718a --- /dev/null +++ b/tests/macros/raw_select/auto_complete.rs @@ -0,0 +1,5 @@ +fn main() { + discourse::questions![RawSelect { + auto_complete: todo!() + }]; +} diff --git a/tests/macros/raw_select/auto_complete.stderr b/tests/macros/raw_select/auto_complete.stderr new file mode 100644 index 0000000..786912e --- /dev/null +++ b/tests/macros/raw_select/auto_complete.stderr @@ -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!() + | ^^^^^^^^^^^^^ diff --git a/tests/macros/select/auto_complete.rs b/tests/macros/select/auto_complete.rs new file mode 100644 index 0000000..2e00e2d --- /dev/null +++ b/tests/macros/select/auto_complete.rs @@ -0,0 +1,5 @@ +fn main() { + discourse::questions![Select { + auto_complete: todo!() + }]; +} diff --git a/tests/macros/select/auto_complete.stderr b/tests/macros/select/auto_complete.stderr new file mode 100644 index 0000000..c8f890a --- /dev/null +++ b/tests/macros/select/auto_complete.stderr @@ -0,0 +1,5 @@ +error: option `auto_complete` does not exist for kind `select` + --> $DIR/auto_complete.rs:3:9 + | +3 | auto_complete: todo!() + | ^^^^^^^^^^^^^