@@ -7,7 +7,9 @@ use crate::{
77use futures_util::future::BoxFuture;
88use tui::{
99 buffer::Buffer as Surface,
10− widgets::{Block, BorderType, Borders},
10+ layout::Constraint,
11+ text::{Span, Spans},
12+ widgets::{Block, BorderType, Borders, Cell, Table},
1113};
1214
1315use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
@@ -20,10 +22,11 @@ use std::{
2022use std::{collections::HashMap, io::Read, path::PathBuf};
2123
2224use crate::ui::{Prompt, PromptEvent};
23−use helix_core::{movement::Direction, Position};
25+use helix_core::{movement::Direction, unicode::segmentation::UnicodeSegmentation, Position};
2426use helix_view::{
2527 editor::Action,
2628 graphics::{CursorKind, Margin, Modifier, Rect},
29+ theme::Style,
2730 Document, DocumentId, Editor,
2831};
2932
@@ -389,6 +392,8 @@ pub struct Picker<T: Item> {
389392 pub truncate_start: bool,
390393 /// Whether to show the preview panel (default true)
391394 show_preview: bool,
395+ /// Constraints for tabular formatting
396+ widths: Vec<Constraint>,
392397
393398 callback_fn: Box<dyn Fn(&mut Context, &T, Action)>,
394399}
@@ -406,6 +411,26 @@ impl<T: Item> Picker<T> {
406411 |_editor: &mut Context, _pattern: &str, _event: PromptEvent| {},
407412 );
408413
414+ let n = options
415+ .first()
416+ .map(|option| option.row(&editor_data).cells.len())
417+ .unwrap_or_default();
418+ let max_lens = options.iter().fold(vec![0; n], |mut acc, option| {
419+ let row = option.row(&editor_data);
420+ // maintain max for each column
421+ for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) {
422+ let width = cell.content.width();
423+ if width > *acc {
424+ *acc = width;
425+ }
426+ }
427+ acc
428+ });
429+ let widths = max_lens
430+ .into_iter()
431+ .map(|len| Constraint::Length(len as u16))
432+ .collect();
433+
409434 let mut picker = Self {
410435 options,
411436 editor_data,
@@ -418,6 +443,7 @@ impl<T: Item> Picker<T> {
418443 show_preview: true,
419444 callback_fn: Box::new(callback_fn),
420445 completion_height: 0,
446+ widths,
421447 };
422448
423449 // scoring on empty input:
@@ -437,8 +463,6 @@ impl<T: Item> Picker<T> {
437463 }
438464
439465 pub fn score(&mut self) {
440− let now = Instant::now();
441−
442466 let pattern = self.prompt.line();
443467
444468 if pattern == &self.previous_pattern {
@@ -480,8 +504,6 @@ impl<T: Item> Picker<T> {
480504 self.force_score();
481505 }
482506
483− log::debug!("picker score {:?}", Instant::now().duration_since(now));
484−
485507 // reset cursor position
486508 self.cursor = 0;
487509 let pattern = self.prompt.line();
@@ -657,7 +679,7 @@ impl<T: Item + 'static> Component for Picker<T> {
657679 fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
658680 let text_style = cx.editor.theme.get("ui.text");
659681 let selected = cx.editor.theme.get("ui.text.focus");
660− let highlighted = cx.editor.theme.get("special").add_modifier(Modifier::BOLD);
682+ let highlight_style = cx.editor.theme.get("special").add_modifier(Modifier::BOLD);
661683
662684 // -- Render the frame:
663685 // clear area
@@ -697,61 +719,119 @@ impl<T: Item + 'static> Component for Picker<T> {
697719 }
698720
699721 // -- Render the contents:
700− // subtract area of prompt from top and current item marker " > " from left
701− let inner = inner.clip_top(2).clip_left(3);
722+ // subtract area of prompt from top
723+ let inner = inner.clip_top(2);
702724
703725 let rows = inner.height;
704726 let offset = self.cursor - (self.cursor % std::cmp::max(1, rows as usize));
727+ let cursor = self.cursor.saturating_sub(offset);
705728
706− let files = self
729+ let options = self
707730 .matches
708731 .iter()
709732 .skip(offset)
710− .map(|pmatch| (pmatch.index, self.options.get(pmatch.index).unwrap()));
711−
712− for (i, (_index, option)) in files.take(rows as usize).enumerate() {
713− let is_active = i == (self.cursor - offset);
714− if is_active {
715− surface.set_string(
716− inner.x.saturating_sub(3),
717− inner.y + i as u16,
718− " > ",
719− selected,
720− );
721− surface.set_style(
722− Rect::new(inner.x, inner.y + i as u16, inner.width, 1),
723− selected,
724− );
725− }
733+ .take(rows as usize)
734+ .map(|pmatch| &self.options[pmatch.index])
735+ .map(|option| option.row(&self.editor_data))
736+ .map(|mut row| {
737+ const TEMP_CELL_SEP: &str = " ";
738+
739+ let line = row.cell_text().join(TEMP_CELL_SEP);
740+
741+ // Items are filtered by using the text returned by menu::Item::filter_text
742+ // but we do highlighting here using the text in Row and therefore there
743+ // might be inconsistencies. This is the best we can do since only the
744+ // text in Row is displayed to the end user.
745+ let (_score, highlights) = FuzzyQuery::new(self.prompt.line())
746+ .fuzzy_indicies(&line, &self.matcher)
747+ .unwrap_or_default();
748+
749+ let highlight_byte_ranges: Vec<_> = line
750+ .char_indices()
751+ .enumerate()
752+ .filter_map(|(char_idx, (byte_offset, ch))| {
753+ highlights
754+ .contains(&char_idx)
755+ .then(|| byte_offset..byte_offset + ch.len_utf8())
756+ })
757+ .collect();
758+
759+ // The starting byte index of the current (iterating) cell
760+ let mut cell_start_byte_offset = 0;
761+ for cell in row.cells.iter_mut() {
762+ let spans = match cell.content.lines.get(0) {
763+ Some(s) => s,
764+ None => continue,
765+ };
766+
767+ let mut cell_len = 0;
726768
727− let spans = option.label(&self.editor_data);
728− let (_score, highlights) = FuzzyQuery::new(self.prompt.line())
729− .fuzzy_indicies(&String::from(&spans), &self.matcher)
730− .unwrap_or_default();
731−
732− spans.0.into_iter().fold(inner, |pos, span| {
733− let new_x = surface
734− .set_string_truncated(
735− pos.x,
736− pos.y + i as u16,
737− &span.content,
738− pos.width as usize,
739− |idx| {
740− if highlights.contains(&idx) {
741− highlighted.patch(span.style)
742− } else if is_active {
743− selected.patch(span.style)
769+ let graphemes_with_style: Vec<_> = spans
770+ .0
771+ .iter()
772+ .flat_map(|span| {
773+ span.content
774+ .grapheme_indices(true)
775+ .zip(std::iter::repeat(span.style))
776+ })
777+ .map(|((grapheme_byte_offset, grapheme), style)| {
778+ cell_len += grapheme.len();
779+ let start = cell_start_byte_offset;
780+
781+ let grapheme_byte_range =
782+ grapheme_byte_offset..grapheme_byte_offset + grapheme.len();
783+
784+ if highlight_byte_ranges.iter().any(|hl_rng| {
785+ hl_rng.start >= start + grapheme_byte_range.start
786+ && hl_rng.end <= start + grapheme_byte_range.end
787+ }) {
788+ (grapheme, style.patch(highlight_style))
744789 } else {
745− text_style.patch(span.style)
790+ (grapheme, style)
746791 }
747− },
748− true,
749− self.truncate_start,
750− )
751− .0;
752− pos.clip_left(new_x - pos.x)
792+ })
793+ .collect();
794+
795+ let mut span_list: Vec<(String, Style)> = Vec::new();
796+ for (grapheme, style) in graphemes_with_style {
797+ if span_list.last().map(|(_, sty)| sty) == Some(&style) {
798+ let (string, _) = span_list.last_mut().unwrap();
799+ string.push_str(grapheme);
800+ } else {
801+ span_list.push((String::from(grapheme), style))
802+ }
803+ }
804+
805+ let spans: Vec<Span> = span_list
806+ .into_iter()
807+ .map(|(string, style)| Span::styled(string, style))
808+ .collect();
809+ let spans: Spans = spans.into();
810+ *cell = Cell::from(spans);
811+
812+ cell_start_byte_offset += cell_len + TEMP_CELL_SEP.len();
813+ }
814+
815+ row
753816 });
754− }
817+
818+ let table = Table::new(options)
819+ .style(text_style)
820+ .highlight_style(selected)
821+ .highlight_symbol(" > ")
822+ .column_spacing(1)
823+ .widths(&self.widths);
824+
825+ use tui::widgets::TableState;
826+
827+ table.render_table(
828+ inner,
829+ surface,
830+ &mut TableState {
831+ offset: 0,
832+ selected: Some(cursor),
833+ },
834+ );
755835 }
756836
757837 fn cursor(&self, area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {