@@ -7,7 +7,9 @@ use crate::{
7use futures_util::future::BoxFuture;
7use futures_util::future::BoxFuture;
9 buffer::Buffer as Surface,
9 buffer::Buffer as Surface,
10− widgets::{Block, BorderType, Borders},
12+ widgets::{Block, BorderType, Borders, Cell, Table},
13use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
15use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
@@ -20,10 +22,11 @@ use std::{
20use std::{collections::HashMap, io::Read, path::PathBuf};
22use std::{collections::HashMap, io::Read, path::PathBuf};
22use crate::ui::{Prompt, PromptEvent};
24use crate::ui::{Prompt, PromptEvent};
23−use helix_core::{movement::Direction, Position};
25+use helix_core::{movement::Direction, unicode::segmentation::UnicodeSegmentation, Position};
24use helix_view::{
26use helix_view::{
25 editor::Action,
27 editor::Action,
26 graphics::{CursorKind, Margin, Modifier, Rect},
28 graphics::{CursorKind, Margin, Modifier, Rect},
27 Document, DocumentId, Editor,
30 Document, DocumentId, Editor,
@@ -389,6 +392,8 @@ pub struct Picker<T: Item> {
389 pub truncate_start: bool,
392 pub truncate_start: bool,
390 /// Whether to show the preview panel (default true)
393 /// Whether to show the preview panel (default true)
391 show_preview: bool,
394 show_preview: bool,
395+ /// Constraints for tabular formatting
396+ widths: Vec<Constraint>,
393 callback_fn: Box<dyn Fn(&mut Context, &T, Action)>,
398 callback_fn: Box<dyn Fn(&mut Context, &T, Action)>,
@@ -406,6 +411,26 @@ impl<T: Item> Picker<T> {
406 |_editor: &mut Context, _pattern: &str, _event: PromptEvent| {},
411 |_editor: &mut Context, _pattern: &str, _event: PromptEvent| {},
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();
429+ let widths = max_lens
431+ .map(|len| Constraint::Length(len as u16))
409 let mut picker = Self {
434 let mut picker = Self {
411 editor_data,
436 editor_data,
@@ -418,6 +443,7 @@ impl<T: Item> Picker<T> {
418 show_preview: true,
443 show_preview: true,
419 callback_fn: Box::new(callback_fn),
444 callback_fn: Box::new(callback_fn),
420 completion_height: 0,
445 completion_height: 0,
423 // scoring on empty input:
449 // scoring on empty input:
@@ -437,8 +463,6 @@ impl<T: Item> Picker<T> {
439 pub fn score(&mut self) {
465 pub fn score(&mut self) {
440− let now = Instant::now();
442 let pattern = self.prompt.line();
466 let pattern = self.prompt.line();
444 if pattern == &self.previous_pattern {
468 if pattern == &self.previous_pattern {
@@ -480,8 +504,6 @@ impl<T: Item> Picker<T> {
480 self.force_score();
504 self.force_score();
483− log::debug!("picker score {:?}", Instant::now().duration_since(now));
485 // reset cursor position
507 // reset cursor position
486 self.cursor = 0;
508 self.cursor = 0;
487 let pattern = self.prompt.line();
509 let pattern = self.prompt.line();
@@ -657,7 +679,7 @@ impl<T: Item + 'static> Component for Picker<T> {
657 fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
679 fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
658 let text_style = cx.editor.theme.get("ui.text");
680 let text_style = cx.editor.theme.get("ui.text");
659 let selected = cx.editor.theme.get("ui.text.focus");
681 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);
662 // -- Render the frame:
684 // -- Render the frame:
663 // clear area
685 // clear area
@@ -697,61 +719,119 @@ impl<T: Item + 'static> Component for Picker<T> {
699 // -- Render the contents:
721 // -- Render the contents:
700− // subtract area of prompt from top and current item marker " > " from left
722+ // subtract area of prompt from top
701− let inner = inner.clip_top(2).clip_left(3);
723+ let inner = inner.clip_top(2);
703 let rows = inner.height;
725 let rows = inner.height;
704 let offset = self.cursor - (self.cursor % std::cmp::max(1, rows as usize));
726 let offset = self.cursor - (self.cursor % std::cmp::max(1, rows as usize));
727+ let cursor = self.cursor.saturating_sub(offset);
706− let files = self
729+ let options = self
709 .skip(offset)
732 .skip(offset)
710− .map(|pmatch| (pmatch.index, self.options.get(pmatch.index).unwrap()));
712− for (i, (_index, option)) in files.take(rows as usize).enumerate() {
733+ .take(rows as usize)
713− let is_active = i == (self.cursor - offset);
734+ .map(|pmatch| &self.options[pmatch.index])
714− if is_active {
735+ .map(|option| option.row(&self.editor_data))
715− surface.set_string(
736+ .map(|mut row| {
716− inner.x.saturating_sub(3),
737+ const TEMP_CELL_SEP: &str = " ";
717− inner.y + i as u16,
738+
718− " > ",
739+ let line = row.cell_text().join(TEMP_CELL_SEP);
720− );
741+ // Items are filtered by using the text returned by menu::Item::filter_text
721− surface.set_style(
742+ // but we do highlighting here using the text in Row and therefore there
722− Rect::new(inner.x, inner.y + i as u16, inner.width, 1),
743+ // might be inconsistencies. This is the best we can do since only the
723− selected,
744+ // text in Row is displayed to the end user.
724− );
745+ let (_score, highlights) = FuzzyQuery::new(self.prompt.line())
746+ .fuzzy_indicies(&line, &self.matcher)
747+ .unwrap_or_default();
749+ let highlight_byte_ranges: Vec<_> = line
752+ .filter_map(|(char_idx, (byte_offset, ch))| {
754+ .contains(&char_idx)
755+ .then(|| byte_offset..byte_offset + ch.len_utf8())
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) {
767+ let mut cell_len = 0;
727− let spans = option.label(&self.editor_data);
769+ let graphemes_with_style: Vec<_> = spans
728− let (_score, highlights) = FuzzyQuery::new(self.prompt.line())
770+ .0
729− .fuzzy_indicies(&String::from(&spans), &self.matcher)
771+ .iter()
730− .unwrap_or_default();
772+ .flat_map(|span| {
732− spans.0.into_iter().fold(inner, |pos, span| {
734− .set_string_truncated(
737− &span.content,
773+ span.content
738− pos.width as usize,
774+ .grapheme_indices(true)
739− |idx| {
775+ .zip(std::iter::repeat(span.style))
740− if highlights.contains(&idx) {
776+ })
741− highlighted.patch(span.style)
777+ .map(|((grapheme_byte_offset, grapheme), style)| {
742− } else if is_active {
778+ cell_len += grapheme.len();
743− selected.patch(span.style)
779+ let start = cell_start_byte_offset;
781+ let grapheme_byte_range =
782+ grapheme_byte_offset..grapheme_byte_offset + grapheme.len();
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
788+ (grapheme, style.patch(highlight_style))
745− text_style.patch(span.style)
790+ (grapheme, style)
748− true,
793+ .collect();
749− self.truncate_start,
794+
750− )
795+ let mut span_list: Vec<(String, Style)> = Vec::new();
751− .0;
796+ for (grapheme, style) in graphemes_with_style {
752− pos.clip_left(new_x - pos.x)
797+ if span_list.last().map(|(_, sty)| sty) == Some(&style) {
798+ let (string, _) = span_list.last_mut().unwrap();
799+ string.push_str(grapheme);
801+ span_list.push((String::from(grapheme), style))
805+ let spans: Vec<Span> = span_list
807+ .map(|(string, style)| Span::styled(string, style))
809+ let spans: Spans = spans.into();
810+ *cell = Cell::from(spans);
812+ cell_start_byte_offset += cell_len + TEMP_CELL_SEP.len();
818+ let table = Table::new(options)
820+ .highlight_style(selected)
821+ .highlight_symbol(" > ")
823+ .widths(&self.widths);
825+ use tui::widgets::TableState;
832+ selected: Some(cursor),
757 fn cursor(&self, area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
837 fn cursor(&self, area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {