on master help sign in
Reuse table in picker
9aafcb2Gokul Soumya
helix-term/src/ui/picker.rs+131−51
@@ -7,7 +7,9 @@ use crate::{
7use futures_util::future::BoxFuture;
7use futures_util::future::BoxFuture;
8use tui::{
8use tui::{
9 buffer::Buffer as Surface,
9 buffer::Buffer as Surface,
10+ layout::Constraint,
11+ text::{Span, Spans},
10− widgets::{Block, BorderType, Borders},
12+ widgets::{Block, BorderType, Borders, Cell, Table},
11};
13};
12
14
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};
21
23
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},
29+ theme::Style,
27 Document, DocumentId, Editor,
30 Document, DocumentId, Editor,
28};
31};
29
32
@@ -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>,
392
397
393 callback_fn: Box<dyn Fn(&mut Context, &T, Action)>,
398 callback_fn: Box<dyn Fn(&mut Context, &T, Action)>,
394}
399}
@@ -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| {},
407 );
412 );
408
413
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+
409 let mut picker = Self {
434 let mut picker = Self {
410 options,
435 options,
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,
446+ widths,
421 };
447 };
422
448
423 // scoring on empty input:
449 // scoring on empty input:
@@ -437,8 +463,6 @@ impl<T: Item> Picker<T> {
437 }
463 }
438
464
439 pub fn score(&mut self) {
465 pub fn score(&mut self) {
440− let now = Instant::now();
441−
442 let pattern = self.prompt.line();
466 let pattern = self.prompt.line();
443
467
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();
481 }
505 }
482
506
483− log::debug!("picker score {:?}", Instant::now().duration_since(now));
484−
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);
661
683
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> {
697 }
719 }
698
720
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);
702
724
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);
705
728
706− let files = self
729+ let options = self
707 .matches
730 .matches
708 .iter()
731 .iter()
709 .skip(offset)
732 .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() {
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);
719− selected,
740+
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();
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,
725− }
765+ };
766+
767+ let mut cell_len = 0;
726
768
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| {
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,
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;
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))
744 } else {
789 } else {
745− text_style.patch(span.style)
790+ (grapheme, style)
746 }
791 }
747− },
792+ })
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);
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
753 });
816 });
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),
754− }
833+ },
834+ );
755 }
835 }
756
836
757 fn cursor(&self, area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
837 fn cursor(&self, area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
helix-tui/src/text.rs+13
@@ -436,6 +436,19 @@ impl<'a> From<Vec<Spans<'a>>> for Text<'a> {
436 }
436 }
437}
437}
438
438
439+impl<'a> From<Text<'a>> for String {
440+ fn from(text: Text<'a>) -> String {
441+ let lines: Vec<String> = text.lines.iter().map(String::from).collect();
442+ lines.join("\n")
443+ }
444+}
445+
446+impl<'a> From<&Text<'a>> for String {
447+ fn from(text: &Text<'a>) -> String {
448+ let lines: Vec<String> = text.lines.iter().map(String::from).collect();
449+ lines.join("\n")
450+ }
451+}
439impl<'a> IntoIterator for Text<'a> {
452impl<'a> IntoIterator for Text<'a> {
440 type Item = Spans<'a>;
453 type Item = Spans<'a>;
441 type IntoIter = std::vec::IntoIter<Self::Item>;
454 type IntoIter = std::vec::IntoIter<Self::Item>;
helix-tui/src/widgets/table.rs+11−3
@@ -126,6 +126,14 @@ impl<'a> Row<'a> {
126 fn total_height(&self) -> u16 {
126 fn total_height(&self) -> u16 {
127 self.height.saturating_add(self.bottom_margin)
127 self.height.saturating_add(self.bottom_margin)
128 }
128 }
129+
130+ /// Returns the contents of cells as plain text, without styles and colors.
131+ pub fn cell_text(&self) -> Vec<String> {
132+ self.cells
133+ .iter()
134+ .map(|cell| String::from(&cell.content))
135+ .collect()
136+ }
129}
137}
130
138
131/// A widget to display data in formatted columns.
139/// A widget to display data in formatted columns.
@@ -477,6 +485,9 @@ impl<'a> Table<'a> {
477 };
485 };
478 let mut col = table_row_start_col;
486 let mut col = table_row_start_col;
479 for (width, cell) in columns_widths.iter().zip(table_row.cells.iter()) {
487 for (width, cell) in columns_widths.iter().zip(table_row.cells.iter()) {
488+ if is_selected {
489+ buf.set_style(table_row_area, self.highlight_style);
490+ }
480 render_cell(
491 render_cell(
481 buf,
492 buf,
482 cell,
493 cell,
@@ -489,9 +500,6 @@ impl<'a> Table<'a> {
489 );
500 );
490 col += *width + self.column_spacing;
501 col += *width + self.column_spacing;
491 }
502 }
492− if is_selected {
493− buf.set_style(table_row_area, self.highlight_style);
494− }
495 }
503 }
496 }
504 }
497}
505}