on master help sign in
Highlight matching text in file picker suggestions (#1635)

* Highlight matching text in file picker suggestions * Remove cache, specialize highlighting code * Fix outdated comments

commit59c691d2dbdf14c02d0a4b8f9b014112ead6cda5 authorLudwig Stecher <ludwig.stecher@gmx.de> dateTue 1 Mar 2022, 02:30 (5 years ago) parentb13d441
3 files changed+84−30
helix-term/src/ui/editor.rs+2−2 helix-term/src/ui/picker.rs+27−13 helix-tui/src/buffer.rs+55−15
helix-term/src/ui/editor.rs+2−2
@@ -688,12 +688,12 @@ impl EditorView {
688688 surface.set_string_truncated(
689689 viewport.x + 8, // 8: 1 space + 3 char mode string + 1 space + 1 spinner + 1 space
690690 viewport.y,
691− title,
691+ &title,
692692 viewport
693693 .width
694694 .saturating_sub(6)
695695 .saturating_sub(right_side_text.width() as u16 + 1) as usize, // "+ 1": a space between the title and the selection info
696− base_style,
696+ |_| base_style,
697697 true,
698698 true,
699699 );
helix-term/src/ui/picker.rs+27−13
@@ -24,7 +24,7 @@ use crate::ui::{Prompt, PromptEvent};
2424use helix_core::{movement::Direction, Position};
2525use helix_view::{
2626 editor::Action,
27− graphics::{Color, CursorKind, Margin, Rect, Style},
27+ graphics::{Color, CursorKind, Margin, Modifier, Rect, Style},
2828 Document, Editor,
2929};
3030
@@ -343,7 +343,7 @@ impl<T> Picker<T> {
343343 }
344344 // TODO: maybe using format_fn isn't the best idea here
345345 let text = (self.format_fn)(option);
346− // TODO: using fuzzy_indices could give us the char idx for match highlighting
346+ // Highlight indices are computed lazily in the render function
347347 self.matcher
348348 .fuzzy_match(&text, pattern)
349349 .map(|score| (index, score))
@@ -483,6 +483,8 @@ impl<T: 'static> Component for Picker<T> {
483483
484484 fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
485485 let text_style = cx.editor.theme.get("ui.text");
486+ let selected = cx.editor.theme.get("ui.text.focus");
487+ let highlighted = cx.editor.theme.get("special").add_modifier(Modifier::BOLD);
486488
487489 // -- Render the frame:
488490 // clear area
@@ -525,29 +527,41 @@ impl<T: 'static> Component for Picker<T> {
525527 // subtract area of prompt from top and current item marker " > " from left
526528 let inner = inner.clip_top(2).clip_left(3);
527529
528− let selected = cx.editor.theme.get("ui.text.focus");
529−
530530 let rows = inner.height;
531531 let offset = self.cursor - (self.cursor % std::cmp::max(1, rows as usize));
532532
533− let files = self.matches.iter().skip(offset).map(|(index, _score)| {
534− (index, self.options.get(*index).unwrap()) // get_unchecked
535− });
533+ let files = self
534+ .matches
535+ .iter_mut()
536+ .skip(offset)
537+ .map(|(index, _score)| (*index, self.options.get(*index).unwrap()));
536538
537539 for (i, (_index, option)) in files.take(rows as usize).enumerate() {
538− if i == (self.cursor - offset) {
540+ let is_active = i == (self.cursor - offset);
541+ if is_active {
539542 surface.set_string(inner.x.saturating_sub(2), inner.y + i as u16, ">", selected);
540543 }
541544
545+ let formatted = (self.format_fn)(option);
546+
547+ let (_score, highlights) = self
548+ .matcher
549+ .fuzzy_indices(&formatted, &self.prompt.line)
550+ .unwrap_or_default();
551+
542552 surface.set_string_truncated(
543553 inner.x,
544554 inner.y + i as u16,
545− (self.format_fn)(option),
555+ &formatted,
546556 inner.width as usize,
547− if i == (self.cursor - offset) {
548− selected
549− } else {
550− text_style
557+ |idx| {
558+ if highlights.contains(&idx) {
559+ highlighted
560+ } else if is_active {
561+ selected
562+ } else {
563+ text_style
564+ }
551565 },
552566 true,
553567 self.truncate_start,
helix-tui/src/buffer.rs+55−15
@@ -287,7 +287,7 @@ impl Buffer {
287287 where
288288 S: AsRef<str>,
289289 {
290− self.set_string_truncated(x, y, string, width, style, false, false)
290+ self.set_string_truncated_at_end(x, y, string.as_ref(), width, style)
291291 }
292292
293293 /// Print at most the first `width` characters of a string if enough space is available
@@ -295,19 +295,16 @@ impl Buffer {
295295 /// truncated lines. If `truncate_start` is `true`, truncate the beginning of the string
296296 /// instead of the end.
297297 #[allow(clippy::too_many_arguments)]
298− pub fn set_string_truncated<S>(
298+ pub fn set_string_truncated(
299299 &mut self,
300300 x: u16,
301301 y: u16,
302− string: S,
302+ string: &str,
303303 width: usize,
304− style: Style,
304+ style: impl Fn(usize) -> Style, // Map a grapheme's string offset to a style
305305 ellipsis: bool,
306306 truncate_start: bool,
307− ) -> (u16, u16)
308− where
309− S: AsRef<str>,
310− {
307+ ) -> (u16, u16) {
311308 // prevent panic if out of range
312309 if !self.in_bounds(x, y) || width == 0 {
313310 return (x, y);
@@ -316,10 +313,10 @@ impl Buffer {
316313 let mut index = self.index_of(x, y);
317314 let mut x_offset = x as usize;
318315 let width = if ellipsis { width - 1 } else { width };
319− let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true);
316+ let graphemes = string.grapheme_indices(true);
320317 let max_offset = min(self.area.right() as usize, width.saturating_add(x as usize));
321318 if !truncate_start {
322− for s in graphemes {
319+ for (byte_offset, s) in graphemes {
323320 let width = s.width();
324321 if width == 0 {
325322 continue;
@@ -331,7 +328,7 @@ impl Buffer {
331328 }
332329
333330 self.content[index].set_symbol(s);
334− self.content[index].set_style(style);
331+ self.content[index].set_style(style(byte_offset));
335332 // Reset following cells if multi-width (they would be hidden by the grapheme),
336333 for i in index + 1..index + width {
337334 self.content[i].reset();
@@ -339,14 +336,14 @@ impl Buffer {
339336 index += width;
340337 x_offset += width;
341338 }
342− if ellipsis && x_offset - (x as usize) < string.as_ref().width() {
339+ if ellipsis && x_offset - (x as usize) < string.width() {
343340 self.content[index].set_symbol("…");
344341 }
345342 } else {
346343 let mut start_index = self.index_of(x, y);
347344 let mut index = self.index_of(max_offset as u16, y);
348345
349− let total_width = string.as_ref().width();
346+ let total_width = string.width();
350347 let truncated = total_width > width;
351348 if ellipsis && truncated {
352349 self.content[start_index].set_symbol("…");
@@ -355,7 +352,7 @@ impl Buffer {
355352 if !truncated {
356353 index -= width - total_width;
357354 }
358− for s in graphemes.rev() {
355+ for (byte_offset, s) in graphemes.rev() {
359356 let width = s.width();
360357 if width == 0 {
361358 continue;
@@ -365,7 +362,7 @@ impl Buffer {
365362 break;
366363 }
367364 self.content[start].set_symbol(s);
368− self.content[start].set_style(style);
365+ self.content[start].set_style(style(byte_offset));
369366 for i in start + 1..index {
370367 self.content[i].reset();
371368 }
@@ -375,6 +372,49 @@ impl Buffer {
375372 (x_offset as u16, y)
376373 }
377374
375+ /// Print at most the first `width` characters of a string if enough space is available
376+ /// until the end of the line.
377+ pub fn set_string_truncated_at_end(
378+ &mut self,
379+ x: u16,
380+ y: u16,
381+ string: &str,
382+ width: usize,
383+ style: Style,
384+ ) -> (u16, u16) {
385+ // prevent panic if out of range
386+ if !self.in_bounds(x, y) {
387+ return (x, y);
388+ }
389+
390+ let mut index = self.index_of(x, y);
391+ let mut x_offset = x as usize;
392+ let max_x_offset = min(self.area.right() as usize, width.saturating_add(x as usize));
393+
394+ for s in string.graphemes(true) {
395+ let width = s.width();
396+ if width == 0 {
397+ continue;
398+ }
399+ // `x_offset + width > max_offset` could be integer overflow on 32-bit machines if we
400+ // change dimensions to usize or u32 and someone resizes the terminal to 1x2^32.
401+ if width > max_x_offset.saturating_sub(x_offset) {
402+ break;
403+ }
404+
405+ self.content[index].set_symbol(s);
406+ self.content[index].set_style(style);
407+ // Reset following cells if multi-width (they would be hidden by the grapheme),
408+ for i in index + 1..index + width {
409+ self.content[i].reset();
410+ }
411+ index += width;
412+ x_offset += width;
413+ }
414+
415+ (x_offset as u16, y)
416+ }
417+
378418 pub fn set_spans<'a>(&mut self, x: u16, y: u16, spans: &Spans<'a>, width: u16) -> (u16, u16) {
379419 let mut remaining_width = width;
380420 let mut x = x;