Skip to content

Commit

Permalink
Merge branch 'main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
mikaelmello authored Sep 29, 2024
2 parents 98854cb + 446ce3d commit 29f2bf1
Show file tree
Hide file tree
Showing 16 changed files with 111 additions and 75 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

## [Unreleased] <!-- ReleaseDate -->

- No changes since the latest release below.
- Fix autocomplete suggestions not being updated after a suggestion is accepted. Thanks @moritz-hoelting and @istudyatuni for reporting and fixing it!
- Fix incorrect cursor placement when inputting CJK characters. Thanks @phostann (#270) for reporting it!
- Removed unused dependency (newline-converter). Thanks @jonassmedegaard (#267) for catching it!

## [0.7.5] - 2024-04-23

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ If you'd like to see more examples, the [`date.rs`](./inquire/examples/date.rs)

## Terminal Back-end

Currently, there are like 3 major libraries to manipulate terminals: [crossterm](https://lib.rs/crates/crossterm), [console](https://lib.rs/crates/console) and [termion](https://lib.rs/crates/crossterm).
Currently, there are like 3 major libraries to manipulate terminals: [crossterm](https://lib.rs/crates/crossterm), [console](https://lib.rs/crates/console) and [termion](https://lib.rs/crates/termion).

Binary Rust applications that intend to manipulate terminals will probably pick any one of these 3 to power underlying abstractions. `inquire` chose to support crossterm by default in order to support many features on Windows out-of-the-box.

Expand Down
1 change: 0 additions & 1 deletion inquire/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ fuzzy-matcher = { version = "0.3.7", default-features = false, optional = true }

bitflags = "2"
dyn-clone = "1"
newline-converter = "0.3"
once_cell = "1.18.0"
unicode-segmentation = "1"
unicode-width = "0.1"
Expand Down
69 changes: 28 additions & 41 deletions inquire/examples/complex_autocompletion.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use std::io::ErrorKind;

use fuzzy_matcher::skim::SkimMatcherV2;
use fuzzy_matcher::FuzzyMatcher;
use inquire::{
autocompletion::{Autocomplete, Replacement},
CustomUserError, Text,
Expand Down Expand Up @@ -29,12 +31,11 @@ fn main() {
pub struct FilePathCompleter {
input: String,
paths: Vec<String>,
lcp: String,
}

impl FilePathCompleter {
fn update_input(&mut self, input: &str) -> Result<(), CustomUserError> {
if input == self.input {
if input == self.input && !self.paths.is_empty() {
return Ok(());
}

Expand Down Expand Up @@ -67,59 +68,43 @@ impl FilePathCompleter {
}?
.collect::<Result<Vec<_>, _>>()?;

let mut idx = 0;
let limit = 15;

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

for entry in entries {
let path = entry.path();
let path_str = if path.is_dir() {
format!("{}/", path.to_string_lossy())
} else {
path.to_string_lossy().to_string()
};

if path_str.starts_with(&self.input) && path_str.len() != self.input.len() {
self.paths.push(path_str);
}

idx = idx.saturating_add(1);
self.paths.push(path_str);
}

self.lcp = self.longest_common_prefix();

Ok(())
}

fn longest_common_prefix(&self) -> String {
let mut ret: String = String::new();

let mut sorted = self.paths.clone();
sorted.sort();
if sorted.is_empty() {
return ret;
}

let mut first_word = sorted.first().unwrap().chars();
let mut last_word = sorted.last().unwrap().chars();
fn fuzzy_sort(&self, input: &str) -> Vec<(String, i64)> {
let mut matches: Vec<(String, i64)> = self
.paths
.iter()
.filter_map(|path| {
SkimMatcherV2::default()
.smart_case()
.fuzzy_match(path, input)
.map(|score| (path.clone(), score))
})
.collect();

loop {
match (first_word.next(), last_word.next()) {
(Some(c1), Some(c2)) if c1 == c2 => {
ret.push(c1);
}
_ => return ret,
}
}
matches.sort_by(|a, b| b.1.cmp(&a.1));
matches
}
}

impl Autocomplete for FilePathCompleter {
fn get_suggestions(&mut self, input: &str) -> Result<Vec<String>, CustomUserError> {
self.update_input(input)?;

Ok(self.paths.clone())
let matches = self.fuzzy_sort(input);
Ok(matches.into_iter().take(15).map(|(path, _)| path).collect())
}

fn get_completion(
Expand All @@ -129,12 +114,14 @@ impl Autocomplete for FilePathCompleter {
) -> Result<Replacement, CustomUserError> {
self.update_input(input)?;

Ok(match highlighted_suggestion {
Some(suggestion) => Replacement::Some(suggestion),
None => match self.lcp.is_empty() {
true => Replacement::None,
false => Replacement::Some(self.lcp.clone()),
},
Ok(if let Some(suggestion) = highlighted_suggestion {
Replacement::Some(suggestion)
} else {
let matches = self.fuzzy_sort(input);
matches
.first()
.map(|(path, _)| Replacement::Some(path.clone()))
.unwrap_or(Replacement::None)
})
}
}
1 change: 1 addition & 0 deletions inquire/src/ansi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ impl<'a> Iterator for AnsiStrippedChars<'a> {

/// Constructs an iterator over the chars of the input string, stripping away ANSI escape codes.
pub trait AnsiStrippable {
#[allow(unused)]
fn ansi_stripped_chars(&self) -> AnsiStrippedChars<'_>;
}

Expand Down
12 changes: 6 additions & 6 deletions inquire/src/date_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ pub fn get_current_date() -> NaiveDate {
}

pub fn get_start_date(month: chrono::Month, year: i32) -> NaiveDate {
chrono::NaiveDate::from_ymd_opt(year, month.number_from_month(), 1).unwrap()
NaiveDate::from_ymd_opt(year, month.number_from_month(), 1).unwrap()
}

pub fn get_month(month: u32) -> chrono::Month {
Expand Down Expand Up @@ -43,23 +43,23 @@ mod tests {
fn test_get_start_date() {
assert_eq!(
get_start_date(chrono::Month::January, 2021),
chrono::NaiveDate::from_ymd_opt(2021, 1, 1).unwrap()
NaiveDate::from_ymd_opt(2021, 1, 1).unwrap()
);
assert_eq!(
get_start_date(chrono::Month::February, 2021),
chrono::NaiveDate::from_ymd_opt(2021, 2, 1).unwrap()
NaiveDate::from_ymd_opt(2021, 2, 1).unwrap()
);
assert_eq!(
get_start_date(chrono::Month::March, 2021),
chrono::NaiveDate::from_ymd_opt(2021, 3, 1).unwrap()
NaiveDate::from_ymd_opt(2021, 3, 1).unwrap()
);
assert_eq!(
get_start_date(chrono::Month::December, 1883),
chrono::NaiveDate::from_ymd_opt(1883, 12, 1).unwrap()
NaiveDate::from_ymd_opt(1883, 12, 1).unwrap()
);
assert_eq!(
get_start_date(chrono::Month::June, 3042),
chrono::NaiveDate::from_ymd_opt(3042, 6, 1).unwrap()
NaiveDate::from_ymd_opt(3042, 6, 1).unwrap()
);
}

Expand Down
20 changes: 20 additions & 0 deletions inquire/src/input/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,17 @@ fn new_with_content_is_correctly_initialized() {
let input = Input::new_with(content);
assert_eq!(11, input.length());
assert_eq!(11, input.cursor());
assert_eq!("great idea!", input.pre_cursor());
assert_eq!(content, input.content());
}

#[test]
fn new_with_chinese_content_is_correctly_initialized() {
let content = "请输";
let input = Input::new_with(content);
assert_eq!(2, input.length());
assert_eq!(2, input.cursor());
assert_eq!("请输", input.pre_cursor());
assert_eq!(content, input.content());
}

Expand All @@ -138,6 +149,15 @@ fn with_cursor_is_correctly_initialized() {
assert_eq!("great i", input.pre_cursor());
}

#[test]
fn chinese_with_cursor_is_correctly_initialized() {
let input = Input::new_with("请输入").with_cursor(1);
assert_eq!(3, input.length());
assert_eq!(1, input.cursor());
assert_eq!("请输入", input.content());
assert_eq!("请", input.pre_cursor());
}

#[test]
fn with_placeholder_is_correctly_initialized() {
let input = Input::new_with("great idea!").with_placeholder("placeholder");
Expand Down
1 change: 1 addition & 0 deletions inquire/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
//! [`Editor`]: crate::Editor
#![warn(missing_docs)]
#![deny(unused_crate_dependencies)]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![allow(clippy::bool_to_int_with_if)]
mod ansi;
Expand Down
2 changes: 1 addition & 1 deletion inquire/src/prompts/multiselect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ static DEFAULT_MATCHER: Lazy<SkimMatcherV2> = Lazy::new(|| SkimMatcherV2::defaul
///
/// # Example
///
/// For a full-featured example, check the [GitHub repository](https://github.com/mikaelmello/inquire/blob/main/examples/multiselect.rs).
/// For a full-featured example, check the [GitHub repository](https://github.com/mikaelmello/inquire/blob/main/inquire/examples/multiselect.rs).
///
/// [`InquireError::InvalidConfiguration`]: crate::error::InquireError::InvalidConfiguration
#[derive(Clone)]
Expand Down
28 changes: 14 additions & 14 deletions inquire/src/prompts/one_liners.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,8 @@ where
/// # Returns
///
/// * `InquireResult<NaiveDate>`: An enum that represents the result of the prompt operation. If the operation is successful,
/// it returns `InquireResult::Ok(NaiveDate)` where NaiveDate's value is the date selected by the user. If the operation
/// encounters an error, it returns `InquireResult::Err(InquireError)`.
/// it returns `InquireResult::Ok(NaiveDate)` where NaiveDate's value is the date selected by the user. If the operation
/// encounters an error, it returns `InquireResult::Err(InquireError)`.
///
/// # Example
///
Expand Down Expand Up @@ -158,8 +158,8 @@ where
/// # Returns
///
/// * `InquireResult<f64>`: An enum that represents the result of the prompt operation. If the operation is successful,
/// it returns `InquireResult::Ok(f64)` where f64 is the number parsed from the user's input. If the operation
/// encounters an error, it returns `InquireResult::Err(InquireError)`.
/// it returns `InquireResult::Ok(f64)` where f64 is the number parsed from the user's input. If the operation
/// encounters an error, it returns `InquireResult::Err(InquireError)`.
///
/// # Example
///
Expand Down Expand Up @@ -193,8 +193,8 @@ where
/// # Returns
///
/// * `InquireResult<f32>`: An enum that represents the result of the prompt operation. If the operation is successful,
/// it returns `InquireResult::Ok(f32)` where f32 is the number parsed from the user's input. If the operation
/// encounters an error, it returns `InquireResult::Err(InquireError)`.
/// it returns `InquireResult::Ok(f32)` where f32 is the number parsed from the user's input. If the operation
/// encounters an error, it returns `InquireResult::Err(InquireError)`.
///
/// # Example
///
Expand Down Expand Up @@ -228,8 +228,8 @@ where
/// # Returns
///
/// * `InquireResult<u64>`: An enum that represents the result of the prompt operation. If the operation is successful,
/// it returns `InquireResult::Ok(u64)` where u64 is the number parsed from the user's input. If the operation
/// encounters an error, it returns `InquireResult::Err(InquireError)`.
/// it returns `InquireResult::Ok(u64)` where u64 is the number parsed from the user's input. If the operation
/// encounters an error, it returns `InquireResult::Err(InquireError)`.
///
/// # Example
///
Expand Down Expand Up @@ -263,8 +263,8 @@ where
/// # Returns
///
/// * `InquireResult<u32>`: An enum that represents the result of the prompt operation. If the operation is successful,
/// it returns `InquireResult::Ok(u32)` where u32 is the number parsed from the user's input. If the operation
/// encounters an error, it returns `InquireResult::Err(InquireError)`.
/// it returns `InquireResult::Ok(u32)` where u32 is the number parsed from the user's input. If the operation
/// encounters an error, it returns `InquireResult::Err(InquireError)`.
///
/// # Example
///
Expand Down Expand Up @@ -298,8 +298,8 @@ where
/// # Returns
///
/// * `InquireResult<usize>`: An enum that represents the result of the prompt operation. If the operation is successful,
/// it returns `InquireResult::Ok(usize)` where usize is the number parsed from the user's input. If the operation
/// encounters an error, it returns `InquireResult::Err(InquireError)`.
/// it returns `InquireResult::Ok(usize)` where usize is the number parsed from the user's input. If the operation
/// encounters an error, it returns `InquireResult::Err(InquireError)`.
///
/// # Example
///
Expand Down Expand Up @@ -333,8 +333,8 @@ where
/// # Returns
///
/// * `InquireResult<u128>`: An enum that represents the result of the prompt operation. If the operation is successful,
/// it returns `InquireResult::Ok(u128)` where u128 is the number parsed from the user's input. If the operation
/// encounters an error, it returns `InquireResult::Err(InquireError)`.
/// it returns `InquireResult::Ok(u128)` where u128 is the number parsed from the user's input. If the operation
/// encounters an error, it returns `InquireResult::Err(InquireError)`.
///
/// # Example
///
Expand Down
10 changes: 9 additions & 1 deletion inquire/src/prompts/text/prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,15 @@ where
TextPromptAction::MoveToSuggestionPageDown => {
self.move_cursor_down(self.config.page_size)
}
TextPromptAction::UseCurrentSuggestion => self.use_current_suggestion()?,
TextPromptAction::UseCurrentSuggestion => {
let result = self.use_current_suggestion()?;

if let ActionResult::NeedsRedraw = result {
self.update_suggestions()?;
}

result
}
};

Ok(result)
Expand Down
2 changes: 1 addition & 1 deletion inquire/src/terminal/crossterm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ impl InputReader for CrosstermKeyReader {

impl CrosstermTerminal {
pub fn new() -> InquireResult<Self> {
crossterm::terminal::enable_raw_mode()?;
terminal::enable_raw_mode()?;

Ok(Self {
io: IO::Std(stderr()),
Expand Down
1 change: 1 addition & 0 deletions inquire/src/terminal/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub trait Terminal: Sized {
fn cursor_down(&mut self, cnt: u16) -> Result<()>;
fn cursor_left(&mut self, cnt: u16) -> Result<()>;
fn cursor_right(&mut self, cnt: u16) -> Result<()>;
#[allow(unused)]
fn cursor_move_to_column(&mut self, idx: u16) -> Result<()>;

fn flush(&mut self) -> Result<()>;
Expand Down
4 changes: 2 additions & 2 deletions inquire/src/terminal/termion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,11 +196,11 @@ impl<'a> Terminal for TermionTerminal<'a> {
}

fn cursor_hide(&mut self) -> Result<()> {
write!(self.get_writer(), "{}", termion::cursor::Hide)
write!(self.get_writer(), "{}", cursor::Hide)
}

fn cursor_show(&mut self) -> Result<()> {
write!(self.get_writer(), "{}", termion::cursor::Show)
write!(self.get_writer(), "{}", cursor::Show)
}
}

Expand Down
9 changes: 7 additions & 2 deletions inquire/src/ui/backend.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use std::{collections::BTreeSet, fmt::Display, io::Result};

use unicode_width::UnicodeWidthStr;

use crate::{
error::InquireResult,
input::Input,
Expand Down Expand Up @@ -185,9 +187,12 @@ where
fn print_input(&mut self, input: &Input) -> Result<()> {
self.frame_renderer.write(" ")?;

let cursor_offset = input.pre_cursor().chars().count();
// The cursor is at the beginning of the input line.
// From here it's easier to mark the wanted cursor position
// (based on the underlying input struct), as it's a simple
// cur_pos + offset calculation.
self.frame_renderer
.mark_cursor_position(cursor_offset as isize);
.mark_cursor_position(input.pre_cursor().width() as isize);

if input.is_empty() {
match input.placeholder() {
Expand Down
Loading

0 comments on commit 29f2bf1

Please sign in to comment.