aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLudwig Stecher2022-03-01 01:30:02 +0000
committerGitHub2022-03-01 01:30:02 +0000
commit59c691d2dbdf14c02d0a4b8f9b014112ead6cda5 (patch)
tree2cca8faf55782aa8d6fff65a5108d1c0c5681229
parentb13d44156c0f2ebe700c4169e87839c976c4fedc (diff)
Highlight matching text in file picker suggestions (#1635)
* Highlight matching text in file picker suggestions * Remove cache, specialize highlighting code * Fix outdated comments
-rw-r--r--helix-term/src/ui/editor.rs4
-rw-r--r--helix-term/src/ui/picker.rs40
-rw-r--r--helix-tui/src/buffer.rs70
3 files changed, 84 insertions, 30 deletions
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
index 064c74ee..b6aaf9e0 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -688,12 +688,12 @@ impl EditorView {
surface.set_string_truncated(
viewport.x + 8, // 8: 1 space + 3 char mode string + 1 space + 1 spinner + 1 space
viewport.y,
- title,
+ &title,
viewport
.width
.saturating_sub(6)
.saturating_sub(right_side_text.width() as u16 + 1) as usize, // "+ 1": a space between the title and the selection info
- base_style,
+ |_| base_style,
true,
true,
);
diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs
index 34709e8b..06e50f51 100644
--- a/helix-term/src/ui/picker.rs
+++ b/helix-term/src/ui/picker.rs
@@ -24,7 +24,7 @@ use crate::ui::{Prompt, PromptEvent};
use helix_core::{movement::Direction, Position};
use helix_view::{
editor::Action,
- graphics::{Color, CursorKind, Margin, Rect, Style},
+ graphics::{Color, CursorKind, Margin, Modifier, Rect, Style},
Document, Editor,
};
@@ -343,7 +343,7 @@ impl<T> Picker<T> {
}
// TODO: maybe using format_fn isn't the best idea here
let text = (self.format_fn)(option);
- // TODO: using fuzzy_indices could give us the char idx for match highlighting
+ // Highlight indices are computed lazily in the render function
self.matcher
.fuzzy_match(&text, pattern)
.map(|score| (index, score))
@@ -483,6 +483,8 @@ impl<T: 'static> Component for Picker<T> {
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
let text_style = cx.editor.theme.get("ui.text");
+ let selected = cx.editor.theme.get("ui.text.focus");
+ let highlighted = cx.editor.theme.get("special").add_modifier(Modifier::BOLD);
// -- Render the frame:
// clear area
@@ -525,29 +527,41 @@ impl<T: 'static> Component for Picker<T> {
// subtract area of prompt from top and current item marker " > " from left
let inner = inner.clip_top(2).clip_left(3);
- let selected = cx.editor.theme.get("ui.text.focus");
-
let rows = inner.height;
let offset = self.cursor - (self.cursor % std::cmp::max(1, rows as usize));
- let files = self.matches.iter().skip(offset).map(|(index, _score)| {
- (index, self.options.get(*index).unwrap()) // get_unchecked
- });
+ let files = self
+ .matches
+ .iter_mut()
+ .skip(offset)
+ .map(|(index, _score)| (*index, self.options.get(*index).unwrap()));
for (i, (_index, option)) in files.take(rows as usize).enumerate() {
- if i == (self.cursor - offset) {
+ let is_active = i == (self.cursor - offset);
+ if is_active {
surface.set_string(inner.x.saturating_sub(2), inner.y + i as u16, ">", selected);
}
+ let formatted = (self.format_fn)(option);
+
+ let (_score, highlights) = self
+ .matcher
+ .fuzzy_indices(&formatted, &self.prompt.line)
+ .unwrap_or_default();
+
surface.set_string_truncated(
inner.x,
inner.y + i as u16,
- (self.format_fn)(option),
+ &formatted,
inner.width as usize,
- if i == (self.cursor - offset) {
- selected
- } else {
- text_style
+ |idx| {
+ if highlights.contains(&idx) {
+ highlighted
+ } else if is_active {
+ selected
+ } else {
+ text_style
+ }
},
true,
self.truncate_start,
diff --git a/helix-tui/src/buffer.rs b/helix-tui/src/buffer.rs
index f8673e43..22956b04 100644
--- a/helix-tui/src/buffer.rs
+++ b/helix-tui/src/buffer.rs
@@ -287,7 +287,7 @@ impl Buffer {
where
S: AsRef<str>,
{
- self.set_string_truncated(x, y, string, width, style, false, false)
+ self.set_string_truncated_at_end(x, y, string.as_ref(), width, style)
}
/// Print at most the first `width` characters of a string if enough space is available
@@ -295,19 +295,16 @@ impl Buffer {
/// truncated lines. If `truncate_start` is `true`, truncate the beginning of the string
/// instead of the end.
#[allow(clippy::too_many_arguments)]
- pub fn set_string_truncated<S>(
+ pub fn set_string_truncated(
&mut self,
x: u16,
y: u16,
- string: S,
+ string: &str,
width: usize,
- style: Style,
+ style: impl Fn(usize) -> Style, // Map a grapheme's string offset to a style
ellipsis: bool,
truncate_start: bool,
- ) -> (u16, u16)
- where
- S: AsRef<str>,
- {
+ ) -> (u16, u16) {
// prevent panic if out of range
if !self.in_bounds(x, y) || width == 0 {
return (x, y);
@@ -316,10 +313,10 @@ impl Buffer {
let mut index = self.index_of(x, y);
let mut x_offset = x as usize;
let width = if ellipsis { width - 1 } else { width };
- let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true);
+ let graphemes = string.grapheme_indices(true);
let max_offset = min(self.area.right() as usize, width.saturating_add(x as usize));
if !truncate_start {
- for s in graphemes {
+ for (byte_offset, s) in graphemes {
let width = s.width();
if width == 0 {
continue;
@@ -331,7 +328,7 @@ impl Buffer {
}
self.content[index].set_symbol(s);
- self.content[index].set_style(style);
+ self.content[index].set_style(style(byte_offset));
// Reset following cells if multi-width (they would be hidden by the grapheme),
for i in index + 1..index + width {
self.content[i].reset();
@@ -339,14 +336,14 @@ impl Buffer {
index += width;
x_offset += width;
}
- if ellipsis && x_offset - (x as usize) < string.as_ref().width() {
+ if ellipsis && x_offset - (x as usize) < string.width() {
self.content[index].set_symbol("…");
}
} else {
let mut start_index = self.index_of(x, y);
let mut index = self.index_of(max_offset as u16, y);
- let total_width = string.as_ref().width();
+ let total_width = string.width();
let truncated = total_width > width;
if ellipsis && truncated {
self.content[start_index].set_symbol("…");
@@ -355,7 +352,7 @@ impl Buffer {
if !truncated {
index -= width - total_width;
}
- for s in graphemes.rev() {
+ for (byte_offset, s) in graphemes.rev() {
let width = s.width();
if width == 0 {
continue;
@@ -365,7 +362,7 @@ impl Buffer {
break;
}
self.content[start].set_symbol(s);
- self.content[start].set_style(style);
+ self.content[start].set_style(style(byte_offset));
for i in start + 1..index {
self.content[i].reset();
}
@@ -375,6 +372,49 @@ impl Buffer {
(x_offset as u16, y)
}
+ /// Print at most the first `width` characters of a string if enough space is available
+ /// until the end of the line.
+ pub fn set_string_truncated_at_end(
+ &mut self,
+ x: u16,
+ y: u16,
+ string: &str,
+ width: usize,
+ style: Style,
+ ) -> (u16, u16) {
+ // prevent panic if out of range
+ if !self.in_bounds(x, y) {
+ return (x, y);
+ }
+
+ let mut index = self.index_of(x, y);
+ let mut x_offset = x as usize;
+ let max_x_offset = min(self.area.right() as usize, width.saturating_add(x as usize));
+
+ for s in string.graphemes(true) {
+ let width = s.width();
+ if width == 0 {
+ continue;
+ }
+ // `x_offset + width > max_offset` could be integer overflow on 32-bit machines if we
+ // change dimensions to usize or u32 and someone resizes the terminal to 1x2^32.
+ if width > max_x_offset.saturating_sub(x_offset) {
+ break;
+ }
+
+ self.content[index].set_symbol(s);
+ self.content[index].set_style(style);
+ // Reset following cells if multi-width (they would be hidden by the grapheme),
+ for i in index + 1..index + width {
+ self.content[i].reset();
+ }
+ index += width;
+ x_offset += width;
+ }
+
+ (x_offset as u16, y)
+ }
+
pub fn set_spans<'a>(&mut self, x: u16, y: u16, spans: &Spans<'a>, width: u16) -> (u16, u16) {
let mut remaining_width = width;
let mut x = x;