diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 064c74ee0..b6aaf9e04 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -688,12 +688,12 @@ pub fn render_statusline( 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 34709e8b5..06e50f51c 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -24,7 +24,7 @@ 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 @@ pub fn score(&mut self) { } // 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 @@ fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult { 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 @@ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { // 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 f8673e436..22956b04c 100644 --- a/helix-tui/src/buffer.rs +++ b/helix-tui/src/buffer.rs @@ -287,7 +287,7 @@ pub fn set_stringn( where S: AsRef, { - 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 @@ pub fn set_stringn( /// 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( + 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, - { + ) -> (u16, u16) { // prevent panic if out of range if !self.in_bounds(x, y) || width == 0 { return (x, y); @@ -316,10 +313,10 @@ pub fn set_string_truncated( 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 @@ pub fn set_string_truncated( } 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 @@ pub fn set_string_truncated( 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 @@ pub fn set_string_truncated( 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 @@ pub fn set_string_truncated( 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 @@ pub fn set_string_truncated( (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;