Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add match_indices field to Suggestion #798

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 139 additions & 0 deletions examples/fuzzy_completions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Modifies the completions example to demonstrate highlighting of fuzzy completions
// cargo run --example fuzzy_completions
//
// One of the suggestions is "multiple 汉 by̆tes字👩🏾". Try typing in "y" or "👩" and note how
// the entire grapheme "y̆" or "👩🏾" is highlighted (might not look right in your terminal).

use reedline::{
default_emacs_keybindings, ColumnarMenu, Completer, DefaultPrompt, EditCommand, Emacs, KeyCode,
KeyModifiers, Keybindings, MenuBuilder, Reedline, ReedlineEvent, ReedlineMenu, Signal, Span,
Suggestion,
};
use std::io;

struct HomegrownFuzzyCompleter(Vec<String>);

impl Completer for HomegrownFuzzyCompleter {
fn complete(&mut self, line: &str, pos: usize) -> Vec<reedline::Suggestion> {
// Grandma's fuzzy matching recipe. She swears it's better than that crates.io-bought stuff
self.0
.iter()
.filter_map(|command_str| {
let command = command_str.as_bytes();
let mut start = 0;
let mut match_indices = Vec::new();
for l in line.as_bytes() {
if start == command.len() {
break;
}
let mut i = start;
while i < command.len() && *l != command[i] {
i += 1;
}
if i < command.len() {
match_indices.push(i);
start = i + 1;
}
}
if match_indices.is_empty() || match_indices.len() * 2 < pos {
None
} else {
Some(Suggestion {
value: command_str.to_string(),
description: None,
style: None,
extra: None,
span: Span::new(pos - line.len(), pos),
append_whitespace: false,
match_indices: Some(match_indices),
})
}
})
.collect()
}
}

fn add_menu_keybindings(keybindings: &mut Keybindings) {
keybindings.add_binding(
KeyModifiers::NONE,
KeyCode::Tab,
ReedlineEvent::UntilFound(vec![
ReedlineEvent::Menu("completion_menu".to_string()),
ReedlineEvent::MenuNext,
]),
);
keybindings.add_binding(
KeyModifiers::ALT,
KeyCode::Enter,
ReedlineEvent::Edit(vec![EditCommand::InsertNewline]),
);
}

fn main() -> io::Result<()> {
// Number of columns
let columns: u16 = 4;
// Column width
let col_width: Option<usize> = None;
// Column padding
let col_padding: usize = 2;

let commands = vec![
"test".into(),
"clear".into(),
"exit".into(),
"history 1".into(),
"history 2".into(),
"logout".into(),
"login".into(),
"hello world".into(),
"hello world reedline".into(),
"hello world something".into(),
"hello world another".into(),
"hello world 1".into(),
"hello world 2".into(),
"hello another very large option for hello word that will force one column".into(),
"this is the reedline crate".into(),
"abaaabas".into(),
"abaaacas".into(),
"ababac".into(),
"abacaxyc".into(),
"abadarabc".into(),
"multiple 汉 by̆tes字👩🏾".into(),
];

let completer = Box::new(HomegrownFuzzyCompleter(commands));

// Use the interactive menu to select options from the completer
let columnar_menu = ColumnarMenu::default()
.with_name("completion_menu")
.with_columns(columns)
.with_column_width(col_width)
.with_column_padding(col_padding);

let completion_menu = Box::new(columnar_menu);

let mut keybindings = default_emacs_keybindings();
add_menu_keybindings(&mut keybindings);

let edit_mode = Box::new(Emacs::new(keybindings));

let mut line_editor = Reedline::create()
.with_completer(completer)
.with_menu(ReedlineMenu::EngineCompleter(completion_menu))
.with_edit_mode(edit_mode);

let prompt = DefaultPrompt::default();

loop {
let sig = line_editor.read_line(&prompt)?;
match sig {
Signal::Success(buffer) => {
println!("We processed: {buffer}");
}
Signal::CtrlD | Signal::CtrlC => {
println!("\nAborted!");
break Ok(());
}
}
}
}
3 changes: 3 additions & 0 deletions src/completion/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,7 @@ pub struct Suggestion {
/// Whether to append a space after selecting this suggestion.
/// This helps to avoid that a completer repeats the complete suggestion.
pub append_whitespace: bool,
/// Indices of the characters in the suggestion that matched the typed text.
/// Useful if using fuzzy matching.
pub match_indices: Option<Vec<usize>>,
}
25 changes: 16 additions & 9 deletions src/completion/default.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,17 +55,17 @@ impl Completer for DefaultCompleter {
/// assert_eq!(
/// completions.complete("bat",3),
/// vec![
/// Suggestion {value: "batcave".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 3 }, append_whitespace: false},
/// Suggestion {value: "batman".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 3 }, append_whitespace: false},
/// Suggestion {value: "batmobile".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 3 }, append_whitespace: false},
/// Suggestion {value: "batcave".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 3 }, append_whitespace: false, match_indices: None},
/// Suggestion {value: "batman".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 3 }, append_whitespace: false, match_indices: None},
/// Suggestion {value: "batmobile".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 3 }, append_whitespace: false, match_indices: None},
/// ]);
///
/// assert_eq!(
/// completions.complete("to the\r\nbat",11),
/// vec![
/// Suggestion {value: "batcave".into(), description: None, style: None, extra: None, span: Span { start: 8, end: 11 }, append_whitespace: false},
/// Suggestion {value: "batman".into(), description: None, style: None, extra: None, span: Span { start: 8, end: 11 }, append_whitespace: false},
/// Suggestion {value: "batmobile".into(), description: None, style: None, extra: None, span: Span { start: 8, end: 11 }, append_whitespace: false},
/// Suggestion {value: "batcave".into(), description: None, style: None, extra: None, span: Span { start: 8, end: 11 }, append_whitespace: false, match_indices: None},
/// Suggestion {value: "batman".into(), description: None, style: None, extra: None, span: Span { start: 8, end: 11 }, append_whitespace: false, match_indices: None},
/// Suggestion {value: "batmobile".into(), description: None, style: None, extra: None, span: Span { start: 8, end: 11 }, append_whitespace: false, match_indices: None},
/// ]);
/// ```
fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
Expand Down Expand Up @@ -110,6 +110,7 @@ impl Completer for DefaultCompleter {
extra: None,
span,
append_whitespace: false,
match_indices: None,
}
})
.filter(|t| t.value.len() > (t.span.end - t.span.start))
Expand Down Expand Up @@ -182,15 +183,15 @@ impl DefaultCompleter {
/// completions.insert(vec!["test-hyphen","test_underscore"].iter().map(|s| s.to_string()).collect());
/// assert_eq!(
/// completions.complete("te",2),
/// vec![Suggestion {value: "test".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 2 }, append_whitespace: false}]);
/// vec![Suggestion {value: "test".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 2 }, append_whitespace: false, match_indices: None}]);
///
/// let mut completions = DefaultCompleter::with_inclusions(&['-', '_']);
/// completions.insert(vec!["test-hyphen","test_underscore"].iter().map(|s| s.to_string()).collect());
/// assert_eq!(
/// completions.complete("te",2),
/// vec![
/// Suggestion {value: "test-hyphen".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 2 }, append_whitespace: false},
/// Suggestion {value: "test_underscore".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 2 }, append_whitespace: false},
/// Suggestion {value: "test-hyphen".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 2 }, append_whitespace: false, match_indices: None},
/// Suggestion {value: "test_underscore".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 2 }, append_whitespace: false, match_indices: None},
/// ]);
/// ```
pub fn with_inclusions(incl: &[char]) -> Self {
Expand Down Expand Up @@ -384,6 +385,7 @@ mod tests {
extra: None,
span: Span { start: 0, end: 3 },
append_whitespace: false,
match_indices: None,
},
Suggestion {
value: "number".into(),
Expand All @@ -392,6 +394,7 @@ mod tests {
extra: None,
span: Span { start: 0, end: 3 },
append_whitespace: false,
match_indices: None,
},
Suggestion {
value: "nushell".into(),
Expand All @@ -400,6 +403,7 @@ mod tests {
extra: None,
span: Span { start: 0, end: 3 },
append_whitespace: false,
match_indices: None,
},
]
);
Expand Down Expand Up @@ -428,6 +432,7 @@ mod tests {
extra: None,
span: Span { start: 8, end: 9 },
append_whitespace: false,
match_indices: None,
},
Suggestion {
value: "this is the reedline crate".into(),
Expand All @@ -436,6 +441,7 @@ mod tests {
extra: None,
span: Span { start: 8, end: 9 },
append_whitespace: false,
match_indices: None,
},
Suggestion {
value: "this is the reedline crate".into(),
Expand All @@ -444,6 +450,7 @@ mod tests {
extra: None,
span: Span { start: 0, end: 9 },
append_whitespace: false,
match_indices: None,
},
]
);
Expand Down
1 change: 1 addition & 0 deletions src/completion/history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ impl<'menu> HistoryCompleter<'menu> {
extra: None,
span,
append_whitespace: false,
match_indices: None,
}
}
}
Expand Down
82 changes: 38 additions & 44 deletions src/menu/columnar_menu.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use super::{Menu, MenuBuilder, MenuEvent, MenuSettings};
use crate::{
core_editor::Editor,
menu_functions::{can_partially_complete, completer_input, replace_in_buffer},
menu_functions::{
can_partially_complete, completer_input, replace_in_buffer, style_suggestion,
},
painting::Painter,
Completer, Suggestion,
};
Expand Down Expand Up @@ -301,32 +303,27 @@ impl ColumnarMenu {
if use_ansi_coloring {
let match_len = self.working_details.shortest_base_string.len();

// Split string so the match text can be styled
let (match_str, remaining_str) = suggestion.value.split_at(match_len);

let suggestion_style_prefix = suggestion
.style
.unwrap_or(self.settings.color.text_style)
.prefix();
let suggestion_style = suggestion.style.unwrap_or(self.settings.color.text_style);

let left_text_size = self.longest_suggestion + self.default_details.col_padding;
let right_text_size = self.get_width().saturating_sub(left_text_size);

let max_remaining = left_text_size.saturating_sub(match_str.width());
let max_match = max_remaining.saturating_sub(remaining_str.width());
let default_indices = (0..match_len).collect();
let match_indices = suggestion
.match_indices
.as_ref()
.unwrap_or(&default_indices);

if index == self.index() {
if let Some(description) = &suggestion.description {
format!(
"{}{}{}{}{}{:max_match$}{:max_remaining$}{}{}{}{}{}{}",
suggestion_style_prefix,
self.settings.color.selected_match_style.prefix(),
match_str,
RESET,
suggestion_style_prefix,
self.settings.color.selected_text_style.prefix(),
&remaining_str,
RESET,
"{:left_text_size$}{}{}{}{}{}",
style_suggestion(
&suggestion.value,
match_indices,
&self.settings.color.selected_match_style,
&self.settings.color.selected_text_style
),
self.settings.color.description_style.prefix(),
self.settings.color.selected_text_style.prefix(),
description
Expand All @@ -339,30 +336,27 @@ impl ColumnarMenu {
)
} else {
format!(
"{}{}{}{}{}{}{}{}{:>empty$}{}",
suggestion_style_prefix,
self.settings.color.selected_match_style.prefix(),
match_str,
RESET,
suggestion_style_prefix,
self.settings.color.selected_text_style.prefix(),
remaining_str,
RESET,
"{}{:>empty$}{}",
style_suggestion(
&suggestion.value,
match_indices,
&self.settings.color.selected_match_style,
&self.settings.color.selected_text_style
),
"",
self.end_of_line(column),
empty = empty_space,
)
}
} else if let Some(description) = &suggestion.description {
format!(
"{}{}{}{}{:max_match$}{:max_remaining$}{}{}{}{}{}",
suggestion_style_prefix,
self.settings.color.match_style.prefix(),
match_str,
RESET,
suggestion_style_prefix,
remaining_str,
RESET,
"{:left_text_size$}{}{}{}{}",
style_suggestion(
&suggestion.value,
match_indices,
&self.settings.color.match_style,
&suggestion_style
),
self.settings.color.description_style.prefix(),
description
.chars()
Expand All @@ -374,14 +368,13 @@ impl ColumnarMenu {
)
} else {
format!(
"{}{}{}{}{}{}{}{}{:>empty$}{}{}",
suggestion_style_prefix,
self.settings.color.match_style.prefix(),
match_str,
RESET,
suggestion_style_prefix,
remaining_str,
RESET,
"{}{}{:>empty$}{}{}",
style_suggestion(
&suggestion.value,
match_indices,
&self.settings.color.match_style,
&suggestion_style
),
self.settings.color.description_style.prefix(),
"",
RESET,
Expand Down Expand Up @@ -750,6 +743,7 @@ mod tests {
extra: None,
span: Span { start: 0, end: pos },
append_whitespace: false,
match_indices: None,
}
}

Expand Down
Loading
Loading