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 01/14] 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 02/14] 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 03/14] 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 04/14] 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 05/14] 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 06/14] 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 07/14] 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 08/14] 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 From 0a0e5780aeb1440359439ed18cf752f0d6c28528 Mon Sep 17 00:00:00 2001 From: ysthakur <45539777+ysthakur@users.noreply.github.com> Date: Tue, 15 Apr 2025 23:51:21 -0400 Subject: [PATCH 09/14] Extract ANSI escapes from strings to apply match highlighting --- Cargo.lock | 17 +++++ Cargo.toml | 1 + examples/fuzzy_completions.rs | 52 +++++++-------- src/completion/base.rs | 2 +- src/completion/default.rs | 32 ++++----- src/completion/history.rs | 2 +- src/menu/columnar_menu.rs | 24 ++++--- src/menu/ide_menu.rs | 13 ++-- src/menu/menu_functions.rs | 122 +++++++++++++++++++++------------- 9 files changed, 161 insertions(+), 104 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c9e9adbf..7e010cdf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -436,6 +436,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.153" @@ -627,6 +633,16 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "parse-ansi" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c48b4d524f8a10bf6ab37dc0b7583f17c8ec88b617b364ddfc3baee4dcf878" +dependencies = [ + "lazy_static", + "regex", +] + [[package]] name = "petgraph" version = "0.6.4" @@ -701,6 +717,7 @@ dependencies = [ "gethostname", "itertools", "nu-ansi-term", + "parse-ansi", "pretty_assertions", "rstest", "rusqlite", diff --git a/Cargo.toml b/Cargo.toml index 5c387c50..210729bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ crossterm = { version = "0.28.1", features = ["serde"] } fd-lock = "4.0.2" itertools = "0.13.0" nu-ansi-term = "0.50.0" +parse-ansi = "0.1.6" rusqlite = { version = "0.31.0", optional = true } serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0.79", optional = true } diff --git a/examples/fuzzy_completions.rs b/examples/fuzzy_completions.rs index 65c83e41..3333c9d5 100644 --- a/examples/fuzzy_completions.rs +++ b/examples/fuzzy_completions.rs @@ -4,12 +4,14 @@ // 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 nu_ansi_term::{Color, Style}; use reedline::{ default_emacs_keybindings, ColumnarMenu, Completer, DefaultPrompt, EditCommand, Emacs, KeyCode, KeyModifiers, Keybindings, MenuBuilder, Reedline, ReedlineEvent, ReedlineMenu, Signal, Span, Suggestion, }; use std::io; +use unicode_segmentation::UnicodeSegmentation; struct HomegrownFuzzyCompleter(Vec); @@ -19,35 +21,30 @@ impl Completer for HomegrownFuzzyCompleter { self.0 .iter() .filter_map(|command_str| { - let command = command_str.as_bytes(); - let mut start = 0; + let command = command_str.graphemes(true).collect::>(); + let mut ind = 0; let mut match_indices = Vec::new(); - for l in line.as_bytes() { - if start == command.len() { - break; + for g in line[..pos].graphemes(true) { + while ind < command.len() && command[ind] != g { + ind += 1; } - let mut i = start; - while i < command.len() && *l != command[i] { - i += 1; + if ind == command.len() { + return None; } - 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), - }) + match_indices.push(ind); + ind += 1; } + + Some(Suggestion { + value: command_str.to_string(), + description: None, + style: None, + extra: None, + span: Span::new(0, pos), + append_whitespace: false, + match_indices: Some(match_indices), + ..Default::default() + }) }) .collect() } @@ -99,6 +96,7 @@ fn main() -> io::Result<()> { "abacaxyc".into(), "abadarabc".into(), "multiple 汉 by̆tes字👩🏾".into(), + "ab汉 by̆tes👩🏾".into(), ]; let completer = Box::new(HomegrownFuzzyCompleter(commands)); @@ -108,7 +106,9 @@ fn main() -> io::Result<()> { .with_name("completion_menu") .with_columns(columns) .with_column_width(col_width) - .with_column_padding(col_padding); + .with_column_padding(col_padding) + .with_text_style(Style::new().italic().on(Color::LightGreen)) + .with_match_text_style(Style::new().on(Color::LightBlue)); let completion_menu = Box::new(columnar_menu); diff --git a/src/completion/base.rs b/src/completion/base.rs index 77f207e0..bfbec118 100644 --- a/src/completion/base.rs +++ b/src/completion/base.rs @@ -90,7 +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. + /// Indices of the graphemes 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 bfcb815e..a4f4d607 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, 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}, + /// Suggestion {value: "batcave".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 3 }, append_whitespace: false, ..Default::default()}, + /// Suggestion {value: "batman".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 3 }, append_whitespace: false, ..Default::default()}, + /// Suggestion {value: "batmobile".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 3 }, append_whitespace: false, ..Default::default()}, /// ]); /// /// 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, 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}, + /// Suggestion {value: "batcave".into(), description: None, style: None, extra: None, span: Span { start: 8, end: 11 }, append_whitespace: false, ..Default::default()}, + /// Suggestion {value: "batman".into(), description: None, style: None, extra: None, span: Span { start: 8, end: 11 }, append_whitespace: false, ..Default::default()}, + /// Suggestion {value: "batmobile".into(), description: None, style: None, extra: None, span: Span { start: 8, end: 11 }, append_whitespace: false, ..Default::default()}, /// ]); /// ``` fn complete(&mut self, line: &str, pos: usize) -> Vec { @@ -110,7 +110,7 @@ impl Completer for DefaultCompleter { extra: None, span, append_whitespace: false, - match_indices: None, + ..Default::default() } }) .filter(|t| t.value.len() > (t.span.end - t.span.start)) @@ -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, match_indices: None}]); + /// vec![Suggestion {value: "test".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 2 }, append_whitespace: false, ..Default::default()}]); /// /// 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, 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}, + /// Suggestion {value: "test-hyphen".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 2 }, append_whitespace: false, ..Default::default()}, + /// Suggestion {value: "test_underscore".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 2 }, append_whitespace: false, ..Default::default()}, /// ]); /// ``` pub fn with_inclusions(incl: &[char]) -> Self { @@ -385,7 +385,7 @@ mod tests { extra: None, span: Span { start: 0, end: 3 }, append_whitespace: false, - match_indices: None, + ..Default::default() }, Suggestion { value: "number".into(), @@ -394,7 +394,7 @@ mod tests { extra: None, span: Span { start: 0, end: 3 }, append_whitespace: false, - match_indices: None, + ..Default::default() }, Suggestion { value: "nushell".into(), @@ -403,7 +403,7 @@ mod tests { extra: None, span: Span { start: 0, end: 3 }, append_whitespace: false, - match_indices: None, + ..Default::default() }, ] ); @@ -432,7 +432,7 @@ mod tests { extra: None, span: Span { start: 8, end: 9 }, append_whitespace: false, - match_indices: None, + ..Default::default() }, Suggestion { value: "this is the reedline crate".into(), @@ -441,7 +441,7 @@ mod tests { extra: None, span: Span { start: 8, end: 9 }, append_whitespace: false, - match_indices: None, + ..Default::default() }, Suggestion { value: "this is the reedline crate".into(), @@ -450,7 +450,7 @@ mod tests { extra: None, span: Span { start: 0, end: 9 }, append_whitespace: false, - match_indices: None, + ..Default::default() }, ] ); diff --git a/src/completion/history.rs b/src/completion/history.rs index 6e995bbc..a48cffca 100644 --- a/src/completion/history.rs +++ b/src/completion/history.rs @@ -65,7 +65,7 @@ impl<'menu> HistoryCompleter<'menu> { extra: None, span, append_whitespace: false, - match_indices: None, + ..Default::default() } } } diff --git a/src/menu/columnar_menu.rs b/src/menu/columnar_menu.rs index 937e6f0d..a3937cba 100644 --- a/src/menu/columnar_menu.rs +++ b/src/menu/columnar_menu.rs @@ -325,10 +325,14 @@ impl ColumnarMenu { format!( "{:left_text_size$}{}{}{}{}{}", style_suggestion( - &suggestion.value, + &self + .settings + .color + .selected_text_style + .paint(&suggestion.value) + .to_string(), 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(), @@ -344,10 +348,14 @@ impl ColumnarMenu { format!( "{}{:>empty$}{}", style_suggestion( - &suggestion.value, + &self + .settings + .color + .selected_text_style + .paint(&suggestion.value) + .to_string(), match_indices, &self.settings.color.selected_match_style, - &self.settings.color.selected_text_style ), "", self.end_of_line(column), @@ -358,10 +366,9 @@ impl ColumnarMenu { format!( "{:left_text_size$}{}{}{}{}", style_suggestion( - &suggestion.value, + &suggestion_style.paint(&suggestion.value).to_string(), match_indices, &self.settings.color.match_style, - &suggestion_style ), self.settings.color.description_style.prefix(), description @@ -376,10 +383,9 @@ impl ColumnarMenu { format!( "{}{}{:>empty$}{}{}", style_suggestion( - &suggestion.value, + &suggestion_style.paint(&suggestion.value).to_string(), match_indices, &self.settings.color.match_style, - &suggestion_style ), self.settings.color.description_style.prefix(), "", @@ -749,7 +755,7 @@ mod tests { extra: None, span: Span { start: 0, end: pos }, append_whitespace: false, - match_indices: None, + ..Default::default() } } diff --git a/src/menu/ide_menu.rs b/src/menu/ide_menu.rs index db38d9e8..5d04e67c 100644 --- a/src/menu/ide_menu.rs +++ b/src/menu/ide_menu.rs @@ -537,10 +537,14 @@ impl IdeMenu { suggestion_style.prefix(), " ".repeat(padding), style_suggestion( - &suggestion.value, + &self + .settings + .color + .selected_text_style + .paint(&suggestion.value) + .to_string(), match_indices, &self.settings.color.selected_match_style, - &self.settings.color.selected_text_style ), " ".repeat(padding_right), RESET, @@ -553,10 +557,9 @@ impl IdeMenu { suggestion_style.prefix(), " ".repeat(padding), style_suggestion( - &suggestion.value, + &suggestion_style.paint(&suggestion.value).to_string(), match_indices, &self.settings.color.match_style, - &suggestion_style ), " ".repeat(padding_right), RESET, @@ -1387,7 +1390,7 @@ mod tests { extra: None, span: Span { start: 0, end: pos }, append_whitespace: false, - match_indices: None, + ..Default::default() } } diff --git a/src/menu/menu_functions.rs b/src/menu/menu_functions.rs index 1abb85fb..7a555a4f 100644 --- a/src/menu/menu_functions.rs +++ b/src/menu/menu_functions.rs @@ -1,5 +1,5 @@ //! Collection of common functions that can be used to create menus -use nu_ansi_term::{AnsiStrings, Style}; +use nu_ansi_term::{ansi::RESET, Style}; use unicode_segmentation::UnicodeSegmentation; use crate::{Editor, Suggestion, UndoBehavior}; @@ -360,43 +360,61 @@ 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], - match_style: &Style, - text_style: &Style, -) -> String { - let mut parts = Vec::new(); - let mut prev_styled = false; - 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])); +pub fn style_suggestion(suggestion: &str, match_indices: &[usize], match_style: &Style) -> String { + let escapes = parse_ansi::parse_bytes(suggestion.as_bytes()).collect::>(); + let mut segments = Vec::new(); + if escapes.is_empty() { + segments.push((0, 0, suggestion.len())); + } else if escapes[0].start() > 0 { + segments.push((0, 0, escapes[0].start())); + } + for (i, m) in escapes.iter().enumerate() { + let next = if i + 1 == escapes.len() { + suggestion.len() + } else { + escapes[i + 1].start() + }; + segments.push((m.start(), m.end(), next)); + } + + let mut res = String::new(); + + let mut offset = 0; + for (escape_start, text_start, text_end) in segments { + let escape = &suggestion[escape_start..text_start]; + let text = &suggestion[text_start..text_end]; + let graphemes = text.graphemes(true).collect::>(); + let mut prev_matched = false; + + res.push_str(escape); + for (i, grapheme) in graphemes.iter().enumerate() { + let is_match = match_indices.contains(&(i + offset)); + + if is_match && !prev_matched { + res.push_str(&match_style.prefix().to_string()); + } else if !is_match && prev_matched && i != 0 { + res.push_str(RESET); + res.push_str(&suggestion[escape_start..text_start]); } - part_start = grapheme_start; - prev_styled = false; + res.push_str(grapheme); + prev_matched = is_match; } - } - let last_style = if prev_styled { match_style } else { text_style }; - parts.push(last_style.paint(&suggestion[part_start..])); + if prev_matched { + res.push_str(RESET); + } - AnsiStrings(&parts).to_string() + offset += graphemes.len(); + } + + res } #[cfg(test)] mod tests { use super::*; use crate::{EditCommand, LineBuffer, Span}; + use nu_ansi_term::Color; use rstest::rstest; #[test] @@ -651,7 +669,7 @@ mod tests { extra: None, span: Span::new(0, s.len()), append_whitespace: false, - match_indices: None, + ..Default::default() }) .collect(); let res = find_common_string(&input); @@ -672,7 +690,7 @@ mod tests { extra: None, span: Span::new(0, s.len()), append_whitespace: false, - match_indices: None, + ..Default::default() }) .collect(); let res = find_common_string(&input); @@ -728,7 +746,7 @@ mod tests { extra: None, span: Span::new(start, end), append_whitespace: false, - match_indices: None, + ..Default::default() }), &mut editor, ); @@ -742,26 +760,38 @@ mod tests { #[test] fn style_fuzzy_suggestion() { - let match_style = Style::new().italic(); - let text_style = Style::new().dimmed(); - - let expected = AnsiStrings(&[ - match_style.paint("ab"), - text_style.paint("c"), + let match_style = Style::new().underline(); + let style1 = Style::new().on(Color::Blue); + let style2 = Style::new().on(Color::Green); + + let expected = format!( + "{}{}{}{}{}{}{}{}{}{}{}{}{}", + style1.prefix(), + "ab", match_style.paint("汉"), - text_style.paint("d"), - match_style.paint("y̆"), - text_style.paint("e"), - ]) - .to_string(); + style1.prefix(), + "d", + RESET, + style2.prefix(), + match_style.paint("y̆👩🏾"), + style2.prefix(), + "e", + RESET, + "ba", + match_style.paint("r"), + ); let match_indices = &[ - 0, 1, // ab - 5, // the last (third) byte of 汉 - 7, // the first byte of y̆ + 2, // 汉 + 4, 5, // y̆👩🏾 + 9, // r ]; assert_eq!( expected, - style_suggestion("abc汉dy̆e", match_indices, &match_style, &text_style) + style_suggestion( + &format!("{}{}{}", style1.paint("ab汉d"), style2.paint("y̆👩🏾e"), "bar"), + match_indices, + &match_style + ) ); } } From 2ae667305852bfc5ce504a1e6ec8645bea77675f Mon Sep 17 00:00:00 2001 From: ysthakur <45539777+ysthakur@users.noreply.github.com> Date: Tue, 15 Apr 2025 23:55:24 -0400 Subject: [PATCH 10/14] Fix clippy lint for fuzzy completion example --- examples/fuzzy_completions.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/fuzzy_completions.rs b/examples/fuzzy_completions.rs index 3333c9d5..7a2fb5a6 100644 --- a/examples/fuzzy_completions.rs +++ b/examples/fuzzy_completions.rs @@ -43,7 +43,6 @@ impl Completer for HomegrownFuzzyCompleter { span: Span::new(0, pos), append_whitespace: false, match_indices: Some(match_indices), - ..Default::default() }) }) .collect() From 9f447da349c6bac7672d6a4b12a6e0276b778c09 Mon Sep 17 00:00:00 2001 From: ysthakur <45539777+ysthakur@users.noreply.github.com> Date: Tue, 15 Apr 2025 23:56:46 -0400 Subject: [PATCH 11/14] Shut the typo checker up --- src/menu/menu_functions.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/menu/menu_functions.rs b/src/menu/menu_functions.rs index 7a555a4f..4b7e7c51 100644 --- a/src/menu/menu_functions.rs +++ b/src/menu/menu_functions.rs @@ -777,7 +777,7 @@ mod tests { style2.prefix(), "e", RESET, - "ba", + "b@", match_style.paint("r"), ); let match_indices = &[ @@ -788,7 +788,7 @@ mod tests { assert_eq!( expected, style_suggestion( - &format!("{}{}{}", style1.paint("ab汉d"), style2.paint("y̆👩🏾e"), "bar"), + &format!("{}{}{}", style1.paint("ab汉d"), style2.paint("y̆👩🏾e"), "b@r"), match_indices, &match_style ) From 7f8db1eb88650570415351efada4af8a94b6e10f Mon Sep 17 00:00:00 2001 From: Yash Thakur <45539777+ysthakur@users.noreply.github.com> Date: Wed, 16 Apr 2025 16:21:20 -0400 Subject: [PATCH 12/14] Use existing variable `escape` --- src/menu/menu_functions.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/menu/menu_functions.rs b/src/menu/menu_functions.rs index 4b7e7c51..986ee2fd 100644 --- a/src/menu/menu_functions.rs +++ b/src/menu/menu_functions.rs @@ -394,7 +394,7 @@ pub fn style_suggestion(suggestion: &str, match_indices: &[usize], match_style: res.push_str(&match_style.prefix().to_string()); } else if !is_match && prev_matched && i != 0 { res.push_str(RESET); - res.push_str(&suggestion[escape_start..text_start]); + res.push_str(escape); } res.push_str(grapheme); prev_matched = is_match; From f79fe9bdfc8b80783a9d842b637da35f800145fa Mon Sep 17 00:00:00 2001 From: ysthakur <45539777+ysthakur@users.noreply.github.com> Date: Thu, 24 Apr 2025 00:10:24 -0400 Subject: [PATCH 13/14] Copy regex from parse-ansi crate --- Cargo.lock | 18 +----------------- Cargo.toml | 2 +- src/menu/menu_functions.rs | 24 ++++++++++++++++++++++-- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7e010cdf..9c64dc15 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -436,12 +436,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - [[package]] name = "libc" version = "0.2.153" @@ -633,16 +627,6 @@ dependencies = [ "windows-targets 0.48.5", ] -[[package]] -name = "parse-ansi" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50c48b4d524f8a10bf6ab37dc0b7583f17c8ec88b617b364ddfc3baee4dcf878" -dependencies = [ - "lazy_static", - "regex", -] - [[package]] name = "petgraph" version = "0.6.4" @@ -717,8 +701,8 @@ dependencies = [ "gethostname", "itertools", "nu-ansi-term", - "parse-ansi", "pretty_assertions", + "regex", "rstest", "rusqlite", "serde", diff --git a/Cargo.toml b/Cargo.toml index 210729bd..d85df3ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ crossterm = { version = "0.28.1", features = ["serde"] } fd-lock = "4.0.2" itertools = "0.13.0" nu-ansi-term = "0.50.0" -parse-ansi = "0.1.6" +regex = "1.11" rusqlite = { version = "0.31.0", optional = true } serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0.79", optional = true } diff --git a/src/menu/menu_functions.rs b/src/menu/menu_functions.rs index 986ee2fd..affa9a15 100644 --- a/src/menu/menu_functions.rs +++ b/src/menu/menu_functions.rs @@ -1,9 +1,17 @@ //! Collection of common functions that can be used to create menus use nu_ansi_term::{ansi::RESET, Style}; +use regex::Regex; +use std::sync::LazyLock; use unicode_segmentation::UnicodeSegmentation; use crate::{Editor, Suggestion, UndoBehavior}; +/// Matches ANSI escapes. Stolen from https://github.com/dbkaplun/parse-ansi, which got it from +/// https://github.com/nodejs/node/blob/641d4a4159aaa96eece8356e03ec6c7248ae3e73/lib/internal/readline.js#L9 +static ANSI_REGEX: LazyLock = LazyLock::new(|| { + Regex::new(r"[\x1b\x9b]\[[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]").unwrap() +}); + /// Index result obtained from parsing a string with an index marker /// For example, the next string: /// "this is an example :10" @@ -359,9 +367,10 @@ 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 +/// * `match_indices` - Indices of graphemes (NOT bytes or chars) that matched the typed text +/// * `match_style` - Style to use for matched characters pub fn style_suggestion(suggestion: &str, match_indices: &[usize], match_style: &Style) -> String { - let escapes = parse_ansi::parse_bytes(suggestion.as_bytes()).collect::>(); + let escapes = ANSI_REGEX.find_iter(suggestion).collect::>(); let mut segments = Vec::new(); if escapes.is_empty() { segments.push((0, 0, suggestion.len())); @@ -758,6 +767,17 @@ mod tests { assert_eq!(orig_insertion_point, editor.insertion_point()); } + #[test] + fn parse_ansi() { + assert_eq!( + ANSI_REGEX + .find_iter("before \x1b[31;4mred underline\x1b[0m after") + .map(|m| (m.start(), m.end(), m.as_str())) + .collect::>(), + vec![(7, 14, "\x1b[31;4m"), (27, 31, "\x1b[0m")] + ); + } + #[test] fn style_fuzzy_suggestion() { let match_style = Style::new().underline(); From f42390b9fc179e75c19dc46dd64c596d0227a5e3 Mon Sep 17 00:00:00 2001 From: Divanshu Grover Date: Fri, 25 Apr 2025 23:38:31 +0530 Subject: [PATCH 14/14] replace LazyLock with lazy_static that works with Rust 1.63.0 (#2) --- Cargo.lock | 7 +++++++ Cargo.toml | 1 + src/menu/menu_functions.rs | 12 ++++++------ 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9c64dc15..eff47941 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -436,6 +436,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.153" @@ -700,6 +706,7 @@ dependencies = [ "fd-lock", "gethostname", "itertools", + "lazy_static", "nu-ansi-term", "pretty_assertions", "regex", diff --git a/Cargo.toml b/Cargo.toml index d85df3ed..04dffb2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ crossbeam = { version = "0.8.2", optional = true } crossterm = { version = "0.28.1", features = ["serde"] } fd-lock = "4.0.2" itertools = "0.13.0" +lazy_static = "1.5.0" nu-ansi-term = "0.50.0" regex = "1.11" rusqlite = { version = "0.31.0", optional = true } diff --git a/src/menu/menu_functions.rs b/src/menu/menu_functions.rs index affa9a15..676f7df0 100644 --- a/src/menu/menu_functions.rs +++ b/src/menu/menu_functions.rs @@ -1,16 +1,16 @@ //! Collection of common functions that can be used to create menus use nu_ansi_term::{ansi::RESET, Style}; use regex::Regex; -use std::sync::LazyLock; +use lazy_static::lazy_static; use unicode_segmentation::UnicodeSegmentation; use crate::{Editor, Suggestion, UndoBehavior}; -/// Matches ANSI escapes. Stolen from https://github.com/dbkaplun/parse-ansi, which got it from -/// https://github.com/nodejs/node/blob/641d4a4159aaa96eece8356e03ec6c7248ae3e73/lib/internal/readline.js#L9 -static ANSI_REGEX: LazyLock = LazyLock::new(|| { - Regex::new(r"[\x1b\x9b]\[[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]").unwrap() -}); +lazy_static! { + /// Matches ANSI escapes. Stolen from https://github.com/dbkaplun/parse-ansi, which got it from + /// https://github.com/nodejs/node/blob/641d4a4159aaa96eece8356e03ec6c7248ae3e73/lib/internal/readline.js#L9 + static ref ANSI_REGEX: Regex = Regex::new(r"[\x1b\x9b]\[[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]").unwrap(); +} /// Index result obtained from parsing a string with an index marker /// For example, the next string: