Skip to content

Commit 40c562c

Browse files
Add match_indices field to Suggestion (#798)
* Add match_indices field to Suggestion Make columnar_menu use match indices Make ide menu use match indices Add fuzzy completions example Test style_suggestion Make doctests in default.rs pass Highlight entire graphemes Extract ANSI escapes from strings to apply match highlighting Fix clippy lint for fuzzy completion example Shut the typo checker up Use existing variable `escape` Copy regex from parse-ansi crate * replace LazyLock with lazy_static that works with Rust 1.63.0 (#2) * Homegrown ANSI parser Fix padding for columnar menu Highlight substring matches too by default Simplify (?) columnar menu * Fix clippy lints after rebase * Use get_match_indices helper * Stop using 'fo' because it's a typo? Fo shizzle. * Use to_string() instead of as_str() * Style entire suggestion same color * RESET after suggestion --------- Co-authored-by: Divanshu Grover <divanshugrover2009@gmail.com>
1 parent 0bb9aca commit 40c562c

File tree

6 files changed

+260
-183
lines changed

6 files changed

+260
-183
lines changed

src/completion/base.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,7 @@ pub struct Suggestion {
9090
/// Whether to append a space after selecting this suggestion.
9191
/// This helps to avoid that a completer repeats the complete suggestion.
9292
pub append_whitespace: bool,
93+
/// Indices of the graphemes in the suggestion that matched the typed text.
94+
/// Useful if using fuzzy matching.
95+
pub match_indices: Option<Vec<usize>>,
9396
}

src/completion/default.rs

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,17 +55,17 @@ impl Completer for DefaultCompleter {
5555
/// assert_eq!(
5656
/// completions.complete("bat",3),
5757
/// vec![
58-
/// Suggestion {value: "batcave".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 3 }, append_whitespace: false},
59-
/// Suggestion {value: "batman".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 3 }, append_whitespace: false},
60-
/// Suggestion {value: "batmobile".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 3 }, append_whitespace: false},
58+
/// Suggestion {value: "batcave".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 3 }, append_whitespace: false, ..Default::default()},
59+
/// Suggestion {value: "batman".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 3 }, append_whitespace: false, ..Default::default()},
60+
/// Suggestion {value: "batmobile".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 3 }, append_whitespace: false, ..Default::default()},
6161
/// ]);
6262
///
6363
/// assert_eq!(
6464
/// completions.complete("to the\r\nbat",11),
6565
/// vec![
66-
/// Suggestion {value: "batcave".into(), description: None, style: None, extra: None, span: Span { start: 8, end: 11 }, append_whitespace: false},
67-
/// Suggestion {value: "batman".into(), description: None, style: None, extra: None, span: Span { start: 8, end: 11 }, append_whitespace: false},
68-
/// Suggestion {value: "batmobile".into(), description: None, style: None, extra: None, span: Span { start: 8, end: 11 }, append_whitespace: false},
66+
/// Suggestion {value: "batcave".into(), description: None, style: None, extra: None, span: Span { start: 8, end: 11 }, append_whitespace: false, ..Default::default()},
67+
/// Suggestion {value: "batman".into(), description: None, style: None, extra: None, span: Span { start: 8, end: 11 }, append_whitespace: false, ..Default::default()},
68+
/// Suggestion {value: "batmobile".into(), description: None, style: None, extra: None, span: Span { start: 8, end: 11 }, append_whitespace: false, ..Default::default()},
6969
/// ]);
7070
/// ```
7171
fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
@@ -110,6 +110,7 @@ impl Completer for DefaultCompleter {
110110
extra: None,
111111
span,
112112
append_whitespace: false,
113+
..Default::default()
113114
}
114115
})
115116
.filter(|t| t.value.len() > (t.span.end - t.span.start))
@@ -182,15 +183,15 @@ impl DefaultCompleter {
182183
/// completions.insert(vec!["test-hyphen","test_underscore"].iter().map(|s| s.to_string()).collect());
183184
/// assert_eq!(
184185
/// completions.complete("te",2),
185-
/// vec![Suggestion {value: "test".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 2 }, append_whitespace: false}]);
186+
/// vec![Suggestion {value: "test".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 2 }, append_whitespace: false, ..Default::default()}]);
186187
///
187188
/// let mut completions = DefaultCompleter::with_inclusions(&['-', '_']);
188189
/// completions.insert(vec!["test-hyphen","test_underscore"].iter().map(|s| s.to_string()).collect());
189190
/// assert_eq!(
190191
/// completions.complete("te",2),
191192
/// vec![
192-
/// Suggestion {value: "test-hyphen".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 2 }, append_whitespace: false},
193-
/// Suggestion {value: "test_underscore".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 2 }, append_whitespace: false},
193+
/// Suggestion {value: "test-hyphen".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 2 }, append_whitespace: false, ..Default::default()},
194+
/// Suggestion {value: "test_underscore".into(), description: None, style: None, extra: None, span: Span { start: 0, end: 2 }, append_whitespace: false, ..Default::default()},
194195
/// ]);
195196
/// ```
196197
pub fn with_inclusions(incl: &[char]) -> Self {
@@ -384,6 +385,7 @@ mod tests {
384385
extra: None,
385386
span: Span { start: 0, end: 3 },
386387
append_whitespace: false,
388+
..Default::default()
387389
},
388390
Suggestion {
389391
value: "number".into(),
@@ -392,6 +394,7 @@ mod tests {
392394
extra: None,
393395
span: Span { start: 0, end: 3 },
394396
append_whitespace: false,
397+
..Default::default()
395398
},
396399
Suggestion {
397400
value: "nushell".into(),
@@ -400,6 +403,7 @@ mod tests {
400403
extra: None,
401404
span: Span { start: 0, end: 3 },
402405
append_whitespace: false,
406+
..Default::default()
403407
},
404408
]
405409
);
@@ -428,6 +432,7 @@ mod tests {
428432
extra: None,
429433
span: Span { start: 8, end: 9 },
430434
append_whitespace: false,
435+
..Default::default()
431436
},
432437
Suggestion {
433438
value: "this is the reedline crate".into(),
@@ -436,6 +441,7 @@ mod tests {
436441
extra: None,
437442
span: Span { start: 8, end: 9 },
438443
append_whitespace: false,
444+
..Default::default()
439445
},
440446
Suggestion {
441447
value: "this is the reedline crate".into(),
@@ -444,6 +450,7 @@ mod tests {
444450
extra: None,
445451
span: Span { start: 0, end: 9 },
446452
append_whitespace: false,
453+
..Default::default()
447454
},
448455
]
449456
);

src/completion/history.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ impl<'menu> HistoryCompleter<'menu> {
6565
extra: None,
6666
span,
6767
append_whitespace: false,
68+
..Default::default()
6869
}
6970
}
7071
}

src/menu/columnar_menu.rs

Lines changed: 44 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ use super::{Menu, MenuBuilder, MenuEvent, MenuSettings};
22
use crate::{
33
core_editor::Editor,
44
menu_functions::{
5-
can_partially_complete, completer_input, floor_char_boundary, replace_in_buffer,
5+
can_partially_complete, completer_input, floor_char_boundary, get_match_indices,
6+
replace_in_buffer, style_suggestion,
67
},
78
painting::Painter,
89
Completer, Suggestion,
@@ -377,135 +378,68 @@ impl ColumnarMenu {
377378
&self,
378379
suggestion: &Suggestion,
379380
index: usize,
380-
empty_space: usize,
381381
use_ansi_coloring: bool,
382382
) -> String {
383+
let selected = index == self.index();
384+
let empty_space = self.get_width().saturating_sub(suggestion.value.width());
385+
383386
if use_ansi_coloring {
384-
// strip quotes
387+
// TODO(ysthakur): let the user strip quotes, rather than doing it here
385388
let is_quote = |c: char| "`'\"".contains(c);
386389
let shortest_base = &self.working_details.shortest_base_string;
387390
let shortest_base = shortest_base
388391
.strip_prefix(is_quote)
389392
.unwrap_or(shortest_base);
390-
let match_len = shortest_base.chars().count();
391-
392-
// Find match position - look for the base string in the suggestion (case-insensitive)
393-
let match_position = suggestion
394-
.value
395-
.to_lowercase()
396-
.find(&shortest_base.to_lowercase())
397-
.unwrap_or(0);
398-
399-
// The match is just the part that matches the shortest_base
400-
let match_str = {
401-
let match_str = &suggestion.value[match_position..];
402-
let match_len_bytes = match_str
403-
.char_indices()
404-
.nth(match_len)
405-
.map(|(i, _)| i)
406-
.unwrap_or_else(|| match_str.len());
407-
&suggestion.value[match_position..match_position + match_len_bytes]
408-
};
409-
410-
// Prefix is everything before the match
411-
let prefix = &suggestion.value[..match_position];
412-
413-
// Remaining is everything after the match
414-
let remaining_str = &suggestion.value[match_position + match_str.len()..];
415393

416-
let suggestion_style_prefix = suggestion
417-
.style
418-
.unwrap_or(self.settings.color.text_style)
419-
.prefix();
394+
let match_indices =
395+
get_match_indices(&suggestion.value, &suggestion.match_indices, shortest_base);
420396

421397
let left_text_size = self.longest_suggestion + self.default_details.col_padding;
422-
let right_text_size = self.get_width().saturating_sub(left_text_size);
398+
let description_size = self.get_width().saturating_sub(left_text_size);
399+
let padding = left_text_size.saturating_sub(suggestion.value.len());
423400

424-
let max_remaining = left_text_size.saturating_sub(match_str.width() + prefix.width());
425-
let max_match = max_remaining.saturating_sub(remaining_str.width());
426-
427-
if index == self.index() {
428-
if let Some(description) = &suggestion.description {
401+
let value_style = if selected {
402+
&self.settings.color.selected_text_style
403+
} else {
404+
&suggestion.style.unwrap_or(self.settings.color.text_style)
405+
};
406+
let styled_value = style_suggestion(
407+
&value_style.paint(&suggestion.value).to_string(),
408+
match_indices.as_ref(),
409+
&self.settings.color.match_style,
410+
);
411+
412+
if let Some(description) = &suggestion.description {
413+
let desc_trunc = description
414+
.chars()
415+
.take(description_size)
416+
.collect::<String>()
417+
.replace('\n', " ");
418+
if selected {
429419
format!(
430-
"{}{}{}{}{}{}{}{}{}{:max_match$}{:max_remaining$}{}{}{}{}{}",
431-
suggestion_style_prefix,
432-
self.settings.color.selected_text_style.prefix(),
433-
prefix,
434-
RESET,
435-
suggestion_style_prefix,
436-
self.settings.color.selected_match_style.prefix(),
437-
match_str,
438-
RESET,
439-
suggestion_style_prefix,
440-
self.settings.color.selected_text_style.prefix(),
441-
remaining_str,
442-
RESET,
443-
self.settings.color.description_style.prefix(),
444-
self.settings.color.selected_text_style.prefix(),
445-
description
446-
.chars()
447-
.take(right_text_size)
448-
.collect::<String>()
449-
.replace('\n', " "),
420+
"{}{}{}{}{}",
421+
styled_value,
422+
value_style.prefix(),
423+
" ".repeat(padding),
424+
desc_trunc,
450425
RESET,
451426
)
452427
} else {
453428
format!(
454-
"{}{}{}{}{}{}{}{}{}{}{}{}{:>empty$}",
455-
suggestion_style_prefix,
456-
self.settings.color.selected_text_style.prefix(),
457-
prefix,
429+
"{}{}{}{}",
430+
styled_value,
431+
" ".repeat(padding),
432+
self.settings.color.description_style.paint(desc_trunc),
458433
RESET,
459-
suggestion_style_prefix,
460-
self.settings.color.selected_match_style.prefix(),
461-
match_str,
462-
RESET,
463-
suggestion_style_prefix,
464-
self.settings.color.selected_text_style.prefix(),
465-
remaining_str,
466-
RESET,
467-
"",
468-
empty = empty_space,
469434
)
470435
}
471-
} else if let Some(description) = &suggestion.description {
472-
format!(
473-
"{}{}{}{}{}{}{}{:max_match$}{:max_remaining$}{}{}{}{}",
474-
suggestion_style_prefix,
475-
prefix,
476-
RESET,
477-
suggestion_style_prefix,
478-
self.settings.color.match_style.prefix(),
479-
match_str,
480-
RESET,
481-
suggestion_style_prefix,
482-
remaining_str,
483-
RESET,
484-
self.settings.color.description_style.prefix(),
485-
description
486-
.chars()
487-
.take(right_text_size)
488-
.collect::<String>()
489-
.replace('\n', " "),
490-
RESET,
491-
)
492436
} else {
493437
format!(
494-
"{}{}{}{}{}{}{}{}{}{}{}{:>empty$}{}",
495-
suggestion_style_prefix,
496-
prefix,
497-
RESET,
498-
suggestion_style_prefix,
499-
self.settings.color.match_style.prefix(),
500-
match_str,
501-
RESET,
502-
suggestion_style_prefix,
503-
remaining_str,
438+
"{}{}{:>empty$}",
439+
styled_value,
504440
RESET,
505-
self.settings.color.description_style.prefix(),
506441
"",
507-
RESET,
508-
empty = empty_space,
442+
empty = empty_space
509443
)
510444
}
511445
} else {
@@ -538,7 +472,7 @@ impl ColumnarMenu {
538472
)
539473
};
540474

541-
if index == self.index() {
475+
if selected {
542476
line.to_uppercase()
543477
} else {
544478
line
@@ -789,14 +723,7 @@ impl Menu for ColumnarMenu {
789723
.step_by(num_rows)
790724
.take(self.get_cols().into())
791725
.map(|(index, suggestion)| {
792-
let empty_space =
793-
self.get_width().saturating_sub(suggestion.value.width());
794-
self.create_string(
795-
suggestion,
796-
index,
797-
empty_space,
798-
use_ansi_coloring,
799-
)
726+
self.create_string(suggestion, index, use_ansi_coloring)
800727
})
801728
.collect();
802729
menu_string.push_str(&row_string);
@@ -817,8 +744,6 @@ impl Menu for ColumnarMenu {
817744
// Correcting the enumerate index based on the number of skipped values
818745
let index = index + skip_values;
819746
let column = index % self.get_cols() as usize;
820-
let empty_space =
821-
self.get_width().saturating_sub(suggestion.value.width());
822747

823748
let end_of_line =
824749
if column == self.get_cols().saturating_sub(1) as usize {
@@ -828,12 +753,7 @@ impl Menu for ColumnarMenu {
828753
};
829754
format!(
830755
"{}{}",
831-
self.create_string(
832-
suggestion,
833-
index,
834-
empty_space,
835-
use_ansi_coloring
836-
),
756+
self.create_string(suggestion, index, use_ansi_coloring),
837757
end_of_line
838758
)
839759
})
@@ -931,6 +851,7 @@ mod tests {
931851
extra: None,
932852
span: Span { start: 0, end: pos },
933853
append_whitespace: false,
854+
..Default::default()
934855
}
935856
}
936857

0 commit comments

Comments
 (0)