From 1e452e5577ebd805a3980dbd3e3e18198efc643a Mon Sep 17 00:00:00 2001 From: ysthakur <45539777+ysthakur@users.noreply.github.com> Date: Fri, 31 May 2024 21:57:30 -0400 Subject: [PATCH 1/8] Add match_indices field to Suggestion --- src/completion/base.rs | 3 +++ src/completion/default.rs | 7 +++++++ src/completion/history.rs | 1 + src/menu/columnar_menu.rs | 1 + src/menu/ide_menu.rs | 1 + src/menu/menu_functions.rs | 3 +++ 6 files changed, 16 insertions(+) diff --git a/src/completion/base.rs b/src/completion/base.rs index 909c467b..77f207e0 100644 --- a/src/completion/base.rs +++ b/src/completion/base.rs @@ -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>, } diff --git a/src/completion/default.rs b/src/completion/default.rs index 882debb6..a096480e 100644 --- a/src/completion/default.rs +++ b/src/completion/default.rs @@ -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)) @@ -384,6 +385,7 @@ mod tests { extra: None, span: Span { start: 0, end: 3 }, append_whitespace: false, + match_indices: None, }, Suggestion { value: "number".into(), @@ -392,6 +394,7 @@ mod tests { extra: None, span: Span { start: 0, end: 3 }, append_whitespace: false, + match_indices: None, }, Suggestion { value: "nushell".into(), @@ -400,6 +403,7 @@ mod tests { extra: None, span: Span { start: 0, end: 3 }, append_whitespace: false, + match_indices: None, }, ] ); @@ -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(), @@ -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(), @@ -444,6 +450,7 @@ mod tests { extra: None, span: Span { start: 0, end: 9 }, append_whitespace: false, + match_indices: None, }, ] ); diff --git a/src/completion/history.rs b/src/completion/history.rs index fda3384f..b21fe947 100644 --- a/src/completion/history.rs +++ b/src/completion/history.rs @@ -65,6 +65,7 @@ impl<'menu> HistoryCompleter<'menu> { extra: None, span, append_whitespace: false, + match_indices: None, } } } diff --git a/src/menu/columnar_menu.rs b/src/menu/columnar_menu.rs index 6eac27ab..451d9fff 100644 --- a/src/menu/columnar_menu.rs +++ b/src/menu/columnar_menu.rs @@ -750,6 +750,7 @@ mod tests { extra: None, span: Span { start: 0, end: pos }, append_whitespace: false, + match_indices: None, } } diff --git a/src/menu/ide_menu.rs b/src/menu/ide_menu.rs index 5dc325f7..08e4d6fe 100644 --- a/src/menu/ide_menu.rs +++ b/src/menu/ide_menu.rs @@ -1382,6 +1382,7 @@ mod tests { extra: None, span: Span { start: 0, end: pos }, append_whitespace: false, + match_indices: None, } } diff --git a/src/menu/menu_functions.rs b/src/menu/menu_functions.rs index bf9ded7b..fdead12c 100644 --- a/src/menu/menu_functions.rs +++ b/src/menu/menu_functions.rs @@ -611,6 +611,7 @@ mod tests { extra: None, span: Span::new(0, s.len()), append_whitespace: false, + match_indices: None, }) .collect(); let res = find_common_string(&input); @@ -631,6 +632,7 @@ mod tests { extra: None, span: Span::new(0, s.len()), append_whitespace: false, + match_indices: None, }) .collect(); let res = find_common_string(&input); @@ -686,6 +688,7 @@ mod tests { extra: None, span: Span::new(start, end), append_whitespace: false, + match_indices: None, }), &mut editor, ); From c6c3e167a7b67b504006a988959d1122e5f2f6b3 Mon Sep 17 00:00:00 2001 From: ysthakur <45539777+ysthakur@users.noreply.github.com> Date: Sat, 1 Jun 2024 13:01:15 -0400 Subject: [PATCH 2/8] Make columnar_menu use match indices --- src/menu/columnar_menu.rs | 82 +++++++++++++++++--------------------- src/menu/menu_functions.rs | 32 +++++++++++++++ 2 files changed, 69 insertions(+), 45 deletions(-) diff --git a/src/menu/columnar_menu.rs b/src/menu/columnar_menu.rs index 451d9fff..17d46928 100644 --- a/src/menu/columnar_menu.rs +++ b/src/menu/columnar_menu.rs @@ -1,12 +1,13 @@ 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, }; use nu_ansi_term::ansi::RESET; -use unicode_width::UnicodeWidthStr; /// Default values used as reference for the menu. These values are set during /// the initial declaration of the menu and are always kept as reference for the @@ -301,32 +302,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 @@ -339,15 +335,13 @@ 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, @@ -355,14 +349,13 @@ impl ColumnarMenu { } } 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() @@ -374,14 +367,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, diff --git a/src/menu/menu_functions.rs b/src/menu/menu_functions.rs index fdead12c..cb6f9e90 100644 --- a/src/menu/menu_functions.rs +++ b/src/menu/menu_functions.rs @@ -1,4 +1,6 @@ //! Collection of common functions that can be used to create menus +use nu_ansi_term::{AnsiStrings, Style}; + use crate::{Editor, Suggestion, UndoBehavior}; /// Index result obtained from parsing a string with an index marker @@ -353,6 +355,36 @@ pub fn can_partially_complete(values: &[Suggestion], editor: &mut Editor) -> boo } } +/// Style a suggestion to be shown in a completer menu +pub fn style_suggestion( + suggestion: &str, + match_indices: &[usize], + match_style: &Style, + text_style: &Style, +) -> String { + let mut parts = Vec::new(); + let mut prev_styled = false; + let mut start = 0; + for i in 0..suggestion.len() { + if match_indices.contains(&i) { + if !prev_styled { + parts.push(text_style.paint(&suggestion[start..i])); + start = i; + prev_styled = true; + } + } else if prev_styled { + parts.push(match_style.paint(&suggestion[start..i])); + start = i; + prev_styled = false; + } + } + + let last_style = if prev_styled { match_style } else { text_style }; + parts.push(last_style.paint(&suggestion[start..])); + + AnsiStrings(&parts).to_string() +} + #[cfg(test)] mod tests { use super::*; From fe28bc2dd7a57af5bf7cd96663023f7ef6301acb Mon Sep 17 00:00:00 2001 From: ysthakur <45539777+ysthakur@users.noreply.github.com> Date: Sat, 1 Jun 2024 13:56:11 -0400 Subject: [PATCH 3/8] Make ide menu use match indices --- src/menu/ide_menu.rs | 47 +++++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/src/menu/ide_menu.rs b/src/menu/ide_menu.rs index 08e4d6fe..0785a55a 100644 --- a/src/menu/ide_menu.rs +++ b/src/menu/ide_menu.rs @@ -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, }; @@ -514,41 +516,42 @@ impl IdeMenu { 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) = string.split_at(match_len); + let default_indices = (0..match_len).collect(); + let match_indices = suggestion + .match_indices + .as_ref() + .unwrap_or(&default_indices); - 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); if index == self.index() { format!( - "{}{}{}{}{}{}{}{}{}{}{}{}", + "{}{}{}{}{}{}{}", vertical_border, - suggestion_style_prefix, + suggestion_style.prefix(), " ".repeat(padding), - self.settings.color.selected_match_style.prefix(), - match_str, - RESET, - suggestion_style_prefix, - self.settings.color.selected_text_style.prefix(), - remaining_str, + style_suggestion( + &suggestion.value, + match_indices, + &self.settings.color.selected_match_style, + &self.settings.color.selected_text_style + ), " ".repeat(padding_right), RESET, vertical_border, ) } else { format!( - "{}{}{}{}{}{}{}{}{}{}{}", + "{}{}{}{}{}{}{}", vertical_border, - suggestion_style_prefix, + suggestion_style.prefix(), " ".repeat(padding), - self.settings.color.match_style.prefix(), - match_str, - RESET, - suggestion_style_prefix, - remaining_str, + style_suggestion( + &suggestion.value, + match_indices, + &self.settings.color.match_style, + &suggestion_style + ), " ".repeat(padding_right), RESET, vertical_border, From 9c1cc4078ce3a8ef268a092e7323009ff75ce43d Mon Sep 17 00:00:00 2001 From: ysthakur <45539777+ysthakur@users.noreply.github.com> Date: Sun, 9 Jun 2024 10:45:14 -0400 Subject: [PATCH 4/8] Add fuzzy completions example --- examples/fuzzy_completions.rs | 135 ++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 examples/fuzzy_completions.rs diff --git a/examples/fuzzy_completions.rs b/examples/fuzzy_completions.rs new file mode 100644 index 00000000..4df125a8 --- /dev/null +++ b/examples/fuzzy_completions.rs @@ -0,0 +1,135 @@ +// Modifies the completions example to demonstrate highlighting of fuzzy completions +// cargo run --example fuzzy_completions + +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); + +impl Completer for HomegrownFuzzyCompleter { + fn complete(&mut self, line: &str, pos: usize) -> Vec { + // 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.chars().collect::>(); + let mut start = 0; + let mut match_indices = Vec::new(); + for l in line.chars() { + 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 = 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(), + ]; + + 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(()); + } + } + } +} From f66f07cdc6c1b6a464a81aad9b2d6e62f852be93 Mon Sep 17 00:00:00 2001 From: ysthakur <45539777+ysthakur@users.noreply.github.com> Date: Sun, 9 Jun 2024 13:16:04 -0400 Subject: [PATCH 5/8] Test style_suggestion --- src/menu/menu_functions.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/menu/menu_functions.rs b/src/menu/menu_functions.rs index cb6f9e90..ced144db 100644 --- a/src/menu/menu_functions.rs +++ b/src/menu/menu_functions.rs @@ -731,4 +731,15 @@ mod tests { assert_eq!(orig_buffer, editor.get_buffer()); assert_eq!(orig_insertion_point, editor.insertion_point()); } + + #[test] + fn style_fuzzy_suggestion() { + let match_style = Style::new().italic(); + let text_style = Style::new().dimmed(); + + assert_eq!( + "\u{1b}[2m\u{1b}[0m\u{1b}[3mab\u{1b}[0m\u{1b}[2mcd\u{1b}[0m\u{1b}[3me\u{1b}[0m\u{1b}[2mfg\u{1b}[0m", + style_suggestion("abcdefg", &[0, 1, 4], &match_style, &text_style) + ); + } } From fe7cbb85feb5236673d89702409ca59362c3e338 Mon Sep 17 00:00:00 2001 From: ysthakur <45539777+ysthakur@users.noreply.github.com> Date: Sun, 9 Jun 2024 13:25:41 -0400 Subject: [PATCH 6/8] Make doctests in default.rs pass --- src/completion/default.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/completion/default.rs b/src/completion/default.rs index a096480e..bfcb815e 100644 --- a/src/completion/default.rs +++ b/src/completion/default.rs @@ -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 { @@ -183,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 { From 201f9e6f1c2d9eb6f17daa928ca0ed30172f9125 Mon Sep 17 00:00:00 2001 From: ysthakur <45539777+ysthakur@users.noreply.github.com> Date: Wed, 17 Jul 2024 20:42:22 -0400 Subject: [PATCH 7/8] Highlight entire graphemes --- examples/fuzzy_completions.rs | 10 +++++--- src/menu/menu_functions.rs | 47 +++++++++++++++++++++++++---------- 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/examples/fuzzy_completions.rs b/examples/fuzzy_completions.rs index 4df125a8..65c83e41 100644 --- a/examples/fuzzy_completions.rs +++ b/examples/fuzzy_completions.rs @@ -1,5 +1,8 @@ // 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, @@ -16,15 +19,15 @@ impl Completer for HomegrownFuzzyCompleter { self.0 .iter() .filter_map(|command_str| { - let command = command_str.chars().collect::>(); + let command = command_str.as_bytes(); let mut start = 0; let mut match_indices = Vec::new(); - for l in line.chars() { + for l in line.as_bytes() { if start == command.len() { break; } let mut i = start; - while i < command.len() && l != command[i] { + while i < command.len() && *l != command[i] { i += 1; } if i < command.len() { @@ -95,6 +98,7 @@ fn main() -> io::Result<()> { "ababac".into(), "abacaxyc".into(), "abadarabc".into(), + "multiple 汉 by̆tes字👩🏾".into(), ]; let completer = Box::new(HomegrownFuzzyCompleter(commands)); diff --git a/src/menu/menu_functions.rs b/src/menu/menu_functions.rs index ced144db..1ab3d4a2 100644 --- a/src/menu/menu_functions.rs +++ b/src/menu/menu_functions.rs @@ -1,5 +1,6 @@ //! Collection of common functions that can be used to create menus use nu_ansi_term::{AnsiStrings, Style}; +use unicode_segmentation::UnicodeSegmentation; use crate::{Editor, Suggestion, UndoBehavior}; @@ -356,6 +357,8 @@ pub fn can_partially_complete(values: &[Suggestion], editor: &mut Editor) -> boo } /// Style a suggestion to be shown in a completer menu +/// +/// * `match_indices` - Indices of bytes that matched the typed text pub fn style_suggestion( suggestion: &str, match_indices: &[usize], @@ -364,23 +367,27 @@ pub fn style_suggestion( ) -> String { let mut parts = Vec::new(); let mut prev_styled = false; - let mut start = 0; - for i in 0..suggestion.len() { - if match_indices.contains(&i) { - if !prev_styled { - parts.push(text_style.paint(&suggestion[start..i])); - start = i; - prev_styled = true; + let mut part_start = 0; + for (grapheme_start, grapheme) in suggestion.grapheme_indices(true) { + let is_match = + (grapheme_start..(grapheme_start + grapheme.len())).any(|i| match_indices.contains(&i)); + if is_match && !prev_styled { + if part_start < grapheme_start { + parts.push(text_style.paint(&suggestion[part_start..grapheme_start])); + } + part_start = grapheme_start; + prev_styled = true; + } else if !is_match && prev_styled { + if part_start < grapheme_start { + parts.push(match_style.paint(&suggestion[part_start..grapheme_start])); } - } else if prev_styled { - parts.push(match_style.paint(&suggestion[start..i])); - start = i; + part_start = grapheme_start; prev_styled = false; } } let last_style = if prev_styled { match_style } else { text_style }; - parts.push(last_style.paint(&suggestion[start..])); + parts.push(last_style.paint(&suggestion[part_start..])); AnsiStrings(&parts).to_string() } @@ -737,9 +744,23 @@ mod tests { let match_style = Style::new().italic(); let text_style = Style::new().dimmed(); + let expected = AnsiStrings(&[ + match_style.paint("ab"), + text_style.paint("c"), + match_style.paint("汉"), + text_style.paint("d"), + match_style.paint("y̆"), + text_style.paint("e"), + ]) + .to_string(); + let match_indices = &[ + 0, 1, // ab + 5, // the last (third) byte of 汉 + 7, // the first byte of y̆ + ]; assert_eq!( - "\u{1b}[2m\u{1b}[0m\u{1b}[3mab\u{1b}[0m\u{1b}[2mcd\u{1b}[0m\u{1b}[3me\u{1b}[0m\u{1b}[2mfg\u{1b}[0m", - style_suggestion("abcdefg", &[0, 1, 4], &match_style, &text_style) + expected, + style_suggestion("abc汉dy̆e", match_indices, &match_style, &text_style) ); } } From 44e18841807ef914d302f57fc9cb9b2c151c099b Mon Sep 17 00:00:00 2001 From: ysthakur <45539777+ysthakur@users.noreply.github.com> Date: Wed, 17 Jul 2024 20:47:11 -0400 Subject: [PATCH 8/8] Import unicode_width::UnicodeWidthStr again --- src/menu/columnar_menu.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/menu/columnar_menu.rs b/src/menu/columnar_menu.rs index 17d46928..d182c4a4 100644 --- a/src/menu/columnar_menu.rs +++ b/src/menu/columnar_menu.rs @@ -8,6 +8,7 @@ use crate::{ Completer, Suggestion, }; use nu_ansi_term::ansi::RESET; +use unicode_width::UnicodeWidthStr; /// Default values used as reference for the menu. These values are set during /// the initial declaration of the menu and are always kept as reference for the