diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index eb935e56..e2e72b6e 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -7,7 +7,9 @@ use crate::{ use futures_util::future::BoxFuture; use tui::{ buffer::Buffer as Surface, - widgets::{Block, BorderType, Borders}, + layout::Constraint, + text::{Span, Spans}, + widgets::{Block, BorderType, Borders, Cell, Table}, }; use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; @@ -20,10 +22,11 @@ use std::{ use std::{collections::HashMap, io::Read, path::PathBuf}; use crate::ui::{Prompt, PromptEvent}; -use helix_core::{movement::Direction, Position}; +use helix_core::{movement::Direction, unicode::segmentation::UnicodeSegmentation, Position}; use helix_view::{ editor::Action, graphics::{CursorKind, Margin, Modifier, Rect}, + theme::Style, Document, DocumentId, Editor, }; @@ -389,6 +392,8 @@ pub struct Picker { pub truncate_start: bool, /// Whether to show the preview panel (default true) show_preview: bool, + /// Constraints for tabular formatting + widths: Vec, callback_fn: Box, } @@ -406,6 +411,26 @@ impl Picker { |_editor: &mut Context, _pattern: &str, _event: PromptEvent| {}, ); + let n = options + .first() + .map(|option| option.row(&editor_data).cells.len()) + .unwrap_or_default(); + let max_lens = options.iter().fold(vec![0; n], |mut acc, option| { + let row = option.row(&editor_data); + // maintain max for each column + for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) { + let width = cell.content.width(); + if width > *acc { + *acc = width; + } + } + acc + }); + let widths = max_lens + .into_iter() + .map(|len| Constraint::Length(len as u16)) + .collect(); + let mut picker = Self { options, editor_data, @@ -418,6 +443,7 @@ impl Picker { show_preview: true, callback_fn: Box::new(callback_fn), completion_height: 0, + widths, }; // scoring on empty input: @@ -437,8 +463,6 @@ impl Picker { } pub fn score(&mut self) { - let now = Instant::now(); - let pattern = self.prompt.line(); if pattern == &self.previous_pattern { @@ -480,8 +504,6 @@ impl Picker { self.force_score(); } - log::debug!("picker score {:?}", Instant::now().duration_since(now)); - // reset cursor position self.cursor = 0; let pattern = self.prompt.line(); @@ -657,7 +679,7 @@ impl Component for Picker { 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); + let highlight_style = cx.editor.theme.get("special").add_modifier(Modifier::BOLD); // -- Render the frame: // clear area @@ -697,61 +719,119 @@ impl Component for Picker { } // -- Render the contents: - // subtract area of prompt from top and current item marker " > " from left - let inner = inner.clip_top(2).clip_left(3); + // subtract area of prompt from top + let inner = inner.clip_top(2); let rows = inner.height; let offset = self.cursor - (self.cursor % std::cmp::max(1, rows as usize)); + let cursor = self.cursor.saturating_sub(offset); - let files = self + let options = self .matches .iter() .skip(offset) - .map(|pmatch| (pmatch.index, self.options.get(pmatch.index).unwrap())); - - for (i, (_index, option)) in files.take(rows as usize).enumerate() { - let is_active = i == (self.cursor - offset); - if is_active { - surface.set_string( - inner.x.saturating_sub(3), - inner.y + i as u16, - " > ", - selected, - ); - surface.set_style( - Rect::new(inner.x, inner.y + i as u16, inner.width, 1), - selected, - ); - } + .take(rows as usize) + .map(|pmatch| &self.options[pmatch.index]) + .map(|option| option.row(&self.editor_data)) + .map(|mut row| { + const TEMP_CELL_SEP: &str = " "; + + let line = row.cell_text().join(TEMP_CELL_SEP); + + // Items are filtered by using the text returned by menu::Item::filter_text + // but we do highlighting here using the text in Row and therefore there + // might be inconsistencies. This is the best we can do since only the + // text in Row is displayed to the end user. + let (_score, highlights) = FuzzyQuery::new(self.prompt.line()) + .fuzzy_indicies(&line, &self.matcher) + .unwrap_or_default(); + + let highlight_byte_ranges: Vec<_> = line + .char_indices() + .enumerate() + .filter_map(|(char_idx, (byte_offset, ch))| { + highlights + .contains(&char_idx) + .then(|| byte_offset..byte_offset + ch.len_utf8()) + }) + .collect(); + + // The starting byte index of the current (iterating) cell + let mut cell_start_byte_offset = 0; + for cell in row.cells.iter_mut() { + let spans = match cell.content.lines.get(0) { + Some(s) => s, + None => continue, + }; + + let mut cell_len = 0; - let spans = option.label(&self.editor_data); - let (_score, highlights) = FuzzyQuery::new(self.prompt.line()) - .fuzzy_indicies(&String::from(&spans), &self.matcher) - .unwrap_or_default(); - - spans.0.into_iter().fold(inner, |pos, span| { - let new_x = surface - .set_string_truncated( - pos.x, - pos.y + i as u16, - &span.content, - pos.width as usize, - |idx| { - if highlights.contains(&idx) { - highlighted.patch(span.style) - } else if is_active { - selected.patch(span.style) + let graphemes_with_style: Vec<_> = spans + .0 + .iter() + .flat_map(|span| { + span.content + .grapheme_indices(true) + .zip(std::iter::repeat(span.style)) + }) + .map(|((grapheme_byte_offset, grapheme), style)| { + cell_len += grapheme.len(); + let start = cell_start_byte_offset; + + let grapheme_byte_range = + grapheme_byte_offset..grapheme_byte_offset + grapheme.len(); + + if highlight_byte_ranges.iter().any(|hl_rng| { + hl_rng.start >= start + grapheme_byte_range.start + && hl_rng.end <= start + grapheme_byte_range.end + }) { + (grapheme, style.patch(highlight_style)) } else { - text_style.patch(span.style) + (grapheme, style) } - }, - true, - self.truncate_start, - ) - .0; - pos.clip_left(new_x - pos.x) + }) + .collect(); + + let mut span_list: Vec<(String, Style)> = Vec::new(); + for (grapheme, style) in graphemes_with_style { + if span_list.last().map(|(_, sty)| sty) == Some(&style) { + let (string, _) = span_list.last_mut().unwrap(); + string.push_str(grapheme); + } else { + span_list.push((String::from(grapheme), style)) + } + } + + let spans: Vec = span_list + .into_iter() + .map(|(string, style)| Span::styled(string, style)) + .collect(); + let spans: Spans = spans.into(); + *cell = Cell::from(spans); + + cell_start_byte_offset += cell_len + TEMP_CELL_SEP.len(); + } + + row }); - } + + let table = Table::new(options) + .style(text_style) + .highlight_style(selected) + .highlight_symbol(" > ") + .column_spacing(1) + .widths(&self.widths); + + use tui::widgets::TableState; + + table.render_table( + inner, + surface, + &mut TableState { + offset: 0, + selected: Some(cursor), + }, + ); } fn cursor(&self, area: Rect, editor: &Editor) -> (Option, CursorKind) { diff --git a/helix-tui/src/text.rs b/helix-tui/src/text.rs index ccdafad5..6970634b 100644 --- a/helix-tui/src/text.rs +++ b/helix-tui/src/text.rs @@ -436,6 +436,19 @@ impl<'a> From>> for Text<'a> { } } +impl<'a> From> for String { + fn from(text: Text<'a>) -> String { + let lines: Vec = text.lines.iter().map(String::from).collect(); + lines.join("\n") + } +} + +impl<'a> From<&Text<'a>> for String { + fn from(text: &Text<'a>) -> String { + let lines: Vec = text.lines.iter().map(String::from).collect(); + lines.join("\n") + } +} impl<'a> IntoIterator for Text<'a> { type Item = Spans<'a>; type IntoIter = std::vec::IntoIter; diff --git a/helix-tui/src/widgets/table.rs b/helix-tui/src/widgets/table.rs index a8f428a7..26860e5c 100644 --- a/helix-tui/src/widgets/table.rs +++ b/helix-tui/src/widgets/table.rs @@ -126,6 +126,14 @@ impl<'a> Row<'a> { fn total_height(&self) -> u16 { self.height.saturating_add(self.bottom_margin) } + + /// Returns the contents of cells as plain text, without styles and colors. + pub fn cell_text(&self) -> Vec { + self.cells + .iter() + .map(|cell| String::from(&cell.content)) + .collect() + } } /// A widget to display data in formatted columns. @@ -477,6 +485,9 @@ impl<'a> Table<'a> { }; let mut col = table_row_start_col; for (width, cell) in columns_widths.iter().zip(table_row.cells.iter()) { + if is_selected { + buf.set_style(table_row_area, self.highlight_style); + } render_cell( buf, cell, @@ -489,9 +500,6 @@ impl<'a> Table<'a> { ); col += *width + self.column_spacing; } - if is_selected { - buf.set_style(table_row_area, self.highlight_style); - } } } }