Skip to content

Commit

Permalink
New completer poc
Browse files Browse the repository at this point in the history
  • Loading branch information
mikaelmello committed Sep 26, 2022
1 parent 63715e5 commit a36cf14
Show file tree
Hide file tree
Showing 10 changed files with 196 additions and 140 deletions.
159 changes: 101 additions & 58 deletions examples/autocomplete_path.rs
@@ -1,9 +1,13 @@
use inquire::{CustomUserError, Text};
use std::fs::DirEntry;

use inquire::{
autocompletion::{AutoComplete, Completion},
CustomUserError, Text,
};

fn main() {
let ans = Text::new("Profile picture:")
.with_suggester(&suggest_file_paths)
.with_completer(&complete_file_path)
.with_auto_completion(FilePathCompleter::default())
.prompt();

match ans {
Expand All @@ -12,73 +16,112 @@ fn main() {
}
}

fn suggest_file_paths(input: &str) -> Result<Vec<String>, CustomUserError> {
Ok(list_paths(input)?)
#[derive(Clone, Default)]
pub struct FilePathCompleter {
paths: Vec<String>,
}

fn complete_file_path(input: &str) -> Result<Option<String>, CustomUserError> {
// Implementation from https://rosettacode.org/wiki/Longest_common_prefix#Rust
fn longest_common_prefix<T: AsRef<[u8]>>(list: &[T]) -> Option<Vec<u8>> {
if list.is_empty() {
return None;
}
let mut ret = Vec::new();
let mut i = 0;
loop {
let mut c = None;
for word in list {
let word = word.as_ref();
if i == word.len() {
return Some(ret);
}
match c {
None => {
c = Some(word[i]);
}
Some(letter) if letter != word[i] => return Some(ret),
_ => continue,
}
}
if let Some(letter) = c {
ret.push(letter);
impl AutoComplete for FilePathCompleter {
fn update_input(&mut self, input: &str) -> Result<(), CustomUserError> {
let root = input;
self.paths.clear();

let mut input_path = std::path::PathBuf::from(root);
if let Some(parent) = input_path.parent() {
if !input_path.exists() || !input_path.is_dir() || !root.ends_with('/') {
input_path = parent.to_path_buf();
}
i += 1;
}
}
if root.is_empty() {
input_path = std::env::current_dir()?;
}
if !input_path.exists() {
return Ok(());
}

Ok(longest_common_prefix(&list_paths(input)?)
.map(|bytes| String::from_utf8_lossy(&bytes).to_string()))
}
let mut entries: Vec<DirEntry> =
std::fs::read_dir(input_path)?.collect::<Result<Vec<_>, _>>()?;

let mut idx = 0;
let limit = 15;

while idx < entries.len() && self.paths.len() < limit {
let entry = entries.get(idx).unwrap();

fn list_paths(root: &str) -> std::io::Result<Vec<String>> {
let mut suggestions = vec![];
let path = entry.path();
let path_str = path.to_string_lossy();

if path_str.starts_with(root) {
let path = if path.is_dir() {
let mut subentries: Vec<DirEntry> =
std::fs::read_dir(path.clone())?.collect::<Result<Vec<_>, _>>()?;
entries.append(&mut subentries);

format!("{}/", path_str)
} else {
path_str.to_string()
};
self.paths.push(path);
}

let mut input_path = std::path::PathBuf::from(root);
if let Some(parent) = input_path.parent() {
if !input_path.exists() || !input_path.is_dir() || !root.ends_with('/') {
input_path = parent.to_path_buf();
idx = idx.saturating_add(1);
}

self.paths.sort();

Ok(())
}
if root.is_empty() {
input_path = std::env::current_dir()?;

fn get_suggestions(&self) -> Result<Vec<String>, CustomUserError> {
Ok(self.paths.clone())
}
if !input_path.exists() {
return Ok(vec![]);

fn get_completion(
&self,
selected_suggestion: Option<(usize, &str)>,
) -> Result<inquire::autocompletion::Completion, CustomUserError> {
let completion = match selected_suggestion {
None => {
let lcp = longest_common_prefix(&self.paths)
.map(|bytes| String::from_utf8_lossy(&bytes).to_string());

match lcp {
Some(lcp) => Completion::Replace(lcp),
None => Completion::None,
}
}
Some(suggestion) => Completion::Replace(suggestion.1.to_owned()),
};

Ok(completion)
}
}

for entry in std::fs::read_dir(input_path)? {
let path = entry?.path();
let path_str = path.to_string_lossy();

if path_str.starts_with(root) {
let path = if path.is_dir() {
format!("{}/", path_str)
} else {
path_str.to_string()
};
suggestions.push(path);
// Implementation from https://rosettacode.org/wiki/Longest_common_prefix#Rust
fn longest_common_prefix<T: AsRef<[u8]>>(list: &[T]) -> Option<Vec<u8>> {
if list.is_empty() {
return None;
}
let mut ret = Vec::new();
let mut i = 0;
loop {
let mut c = None;
for word in list {
let word = word.as_ref();
if i == word.len() {
return Some(ret);
}
match c {
None => {
c = Some(word[i]);
}
Some(letter) if letter != word[i] => return Some(ret),
_ => continue,
}
}
if let Some(letter) = c {
ret.push(letter);
}
i += 1;
}

Ok(suggestions)
}
4 changes: 2 additions & 2 deletions examples/empty_render_config.rs
Expand Up @@ -16,13 +16,13 @@ fn main() -> InquireResult<()> {

let _payee = Text::new("Payee:")
.with_validator(required!("This field is required"))
.with_suggester(&payee_suggestor)
//.with_suggester(&payee_suggestor)
.with_help_message("e.g. Music Store")
.with_page_size(5)
.prompt()?;

let amount: f64 = CustomType::new("Amount:")
.with_formatter(&|i| format!("${}", i))
.with_formatter(&|i: f64| format!("${}", i))
.with_error_message("Please type a valid number")
.with_help_message("Type the amount in US dollars using a decimal point as a separator")
.prompt()
Expand Down
4 changes: 2 additions & 2 deletions examples/expense_tracker.rs
Expand Up @@ -12,13 +12,13 @@ fn main() -> InquireResult<()> {

let _payee = Text::new("Payee:")
.with_validator(required!("This field is required"))
.with_suggester(&payee_suggestor)
//.with_suggester(&payee_suggestor)
.with_help_message("e.g. Music Store")
.with_page_size(5)
.prompt()?;

let amount: f64 = CustomType::new("Amount:")
.with_formatter(&|i| format!("${}", i))
.with_formatter(&|i: f64| format!("${}", i))
.with_error_message("Please type a valid number")
.with_help_message("Type the amount in US dollars using a decimal point as a separator")
.prompt()
Expand Down
4 changes: 2 additions & 2 deletions examples/render_config.rs
Expand Up @@ -16,13 +16,13 @@ fn main() -> InquireResult<()> {

let _payee = Text::new("Payee:")
.with_validator(required!("This field is required"))
.with_suggester(&payee_suggestor)
//.with_suggester(&payee_suggestor)
.with_help_message("e.g. Music Store")
.with_page_size(5)
.prompt()?;

let amount: f64 = CustomType::new("Amount:")
.with_formatter(&|i| format!("${}", i))
.with_formatter(&|i: f64| format!("${}", i))
.with_error_message("Please type a valid number")
.with_help_message("Type the amount in US dollars using a decimal point as a separator")
.prompt()
Expand Down
5 changes: 2 additions & 3 deletions examples/text_options.rs
Expand Up @@ -2,7 +2,7 @@ use inquire::{error::CustomUserError, length, required, ui::RenderConfig, Text};

fn main() {
let answer = Text::new("What's your name?")
.with_suggester(&suggester)
//.with_suggester(&suggester)
.with_validator(required!())
.with_validator(length!(10))
.prompt()
Expand All @@ -19,8 +19,7 @@ fn main() {
formatter: Text::DEFAULT_FORMATTER,
validators: Vec::new(),
page_size: Text::DEFAULT_PAGE_SIZE,
suggester: None,
completer: None,
autocompleter: None,
render_config: RenderConfig::default(),
}
.prompt()
Expand Down
26 changes: 26 additions & 0 deletions src/autocompletion.rs
@@ -0,0 +1,26 @@
use dyn_clone::DynClone;

use crate::CustomUserError;

pub trait AutoComplete: DynClone {
fn update_input(&mut self, input: &str) -> Result<(), CustomUserError>;

fn get_suggestions(&self) -> Result<Vec<String>, CustomUserError>;

fn get_completion(
&self,
selected_suggestion: Option<(usize, &str)>,
) -> Result<Completion, CustomUserError>;
}

pub enum Completion {
Replace(String),
Append(String),
None,
}

impl Clone for Box<dyn AutoComplete> {
fn clone(&self) -> Self {
dyn_clone::clone_box(&**self)
}
}
9 changes: 7 additions & 2 deletions src/input.rs
Expand Up @@ -31,11 +31,16 @@ impl Input {
}
}

pub fn new_with(content: &str) -> Self {
pub fn new_with<S>(content: S) -> Self
where
S: Into<String>,
{
let content: String = content.into();

let len = content.graphemes(true).count();

Self {
content: String::from(content),
content,
placeholder: None,
length: len,
cursor: len,
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Expand Up @@ -66,6 +66,7 @@
#![warn(missing_docs)]
#![cfg_attr(docsrs, feature(doc_cfg))]

pub mod autocompletion;
mod config;
#[cfg(feature = "date")]
mod date_utils;
Expand Down

0 comments on commit a36cf14

Please sign in to comment.