Reuse table in picker

pull/5568/head
Gokul Soumya 2 years ago committed by Blaž Hrastnik
parent deae13f404
commit 9aafcb2b9a

@ -7,7 +7,9 @@ use crate::{
use futures_util::future::BoxFuture; use futures_util::future::BoxFuture;
use tui::{ use tui::{
buffer::Buffer as Surface, 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; use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
@ -20,10 +22,11 @@ use std::{
use std::{collections::HashMap, io::Read, path::PathBuf}; use std::{collections::HashMap, io::Read, path::PathBuf};
use crate::ui::{Prompt, PromptEvent}; use crate::ui::{Prompt, PromptEvent};
use helix_core::{movement::Direction, Position}; use helix_core::{movement::Direction, unicode::segmentation::UnicodeSegmentation, Position};
use helix_view::{ use helix_view::{
editor::Action, editor::Action,
graphics::{CursorKind, Margin, Modifier, Rect}, graphics::{CursorKind, Margin, Modifier, Rect},
theme::Style,
Document, DocumentId, Editor, Document, DocumentId, Editor,
}; };
@ -389,6 +392,8 @@ pub struct Picker<T: Item> {
pub truncate_start: bool, pub truncate_start: bool,
/// Whether to show the preview panel (default true) /// Whether to show the preview panel (default true)
show_preview: bool, show_preview: bool,
/// Constraints for tabular formatting
widths: Vec<Constraint>,
callback_fn: Box<dyn Fn(&mut Context, &T, Action)>, callback_fn: Box<dyn Fn(&mut Context, &T, Action)>,
} }
@ -406,6 +411,26 @@ impl<T: Item> Picker<T> {
|_editor: &mut Context, _pattern: &str, _event: PromptEvent| {}, |_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 { let mut picker = Self {
options, options,
editor_data, editor_data,
@ -418,6 +443,7 @@ impl<T: Item> Picker<T> {
show_preview: true, show_preview: true,
callback_fn: Box::new(callback_fn), callback_fn: Box::new(callback_fn),
completion_height: 0, completion_height: 0,
widths,
}; };
// scoring on empty input: // scoring on empty input:
@ -437,8 +463,6 @@ impl<T: Item> Picker<T> {
} }
pub fn score(&mut self) { pub fn score(&mut self) {
let now = Instant::now();
let pattern = self.prompt.line(); let pattern = self.prompt.line();
if pattern == &self.previous_pattern { if pattern == &self.previous_pattern {
@ -480,8 +504,6 @@ impl<T: Item> Picker<T> {
self.force_score(); self.force_score();
} }
log::debug!("picker score {:?}", Instant::now().duration_since(now));
// reset cursor position // reset cursor position
self.cursor = 0; self.cursor = 0;
let pattern = self.prompt.line(); let pattern = self.prompt.line();
@ -657,7 +679,7 @@ impl<T: Item + 'static> Component for Picker<T> {
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
let text_style = cx.editor.theme.get("ui.text"); let text_style = cx.editor.theme.get("ui.text");
let selected = cx.editor.theme.get("ui.text.focus"); 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: // -- Render the frame:
// clear area // clear area
@ -697,61 +719,119 @@ impl<T: Item + 'static> Component for Picker<T> {
} }
// -- Render the contents: // -- Render the contents:
// subtract area of prompt from top and current item marker " > " from left // subtract area of prompt from top
let inner = inner.clip_top(2).clip_left(3); let inner = inner.clip_top(2);
let rows = inner.height; let rows = inner.height;
let offset = self.cursor - (self.cursor % std::cmp::max(1, rows as usize)); 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 .matches
.iter() .iter()
.skip(offset) .skip(offset)
.map(|pmatch| (pmatch.index, self.options.get(pmatch.index).unwrap())); .take(rows as usize)
.map(|pmatch| &self.options[pmatch.index])
for (i, (_index, option)) in files.take(rows as usize).enumerate() { .map(|option| option.row(&self.editor_data))
let is_active = i == (self.cursor - offset); .map(|mut row| {
if is_active { const TEMP_CELL_SEP: &str = " ";
surface.set_string(
inner.x.saturating_sub(3), let line = row.cell_text().join(TEMP_CELL_SEP);
inner.y + i as u16,
" > ", // Items are filtered by using the text returned by menu::Item::filter_text
selected, // 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
surface.set_style( // text in Row is displayed to the end user.
Rect::new(inner.x, inner.y + i as u16, inner.width, 1), let (_score, highlights) = FuzzyQuery::new(self.prompt.line())
selected, .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 graphemes_with_style: Vec<_> = spans
let (_score, highlights) = FuzzyQuery::new(self.prompt.line()) .0
.fuzzy_indicies(&String::from(&spans), &self.matcher) .iter()
.unwrap_or_default(); .flat_map(|span| {
span.content
spans.0.into_iter().fold(inner, |pos, span| { .grapheme_indices(true)
let new_x = surface .zip(std::iter::repeat(span.style))
.set_string_truncated( })
pos.x, .map(|((grapheme_byte_offset, grapheme), style)| {
pos.y + i as u16, cell_len += grapheme.len();
&span.content, let start = cell_start_byte_offset;
pos.width as usize,
|idx| { let grapheme_byte_range =
if highlights.contains(&idx) { grapheme_byte_offset..grapheme_byte_offset + grapheme.len();
highlighted.patch(span.style)
} else if is_active { if highlight_byte_ranges.iter().any(|hl_rng| {
selected.patch(span.style) hl_rng.start >= start + grapheme_byte_range.start
&& hl_rng.end <= start + grapheme_byte_range.end
}) {
(grapheme, style.patch(highlight_style))
} else { } else {
text_style.patch(span.style) (grapheme, style)
} }
}, })
true, .collect();
self.truncate_start,
) let mut span_list: Vec<(String, Style)> = Vec::new();
.0; for (grapheme, style) in graphemes_with_style {
pos.clip_left(new_x - pos.x) 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> = 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<Position>, CursorKind) { fn cursor(&self, area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {

@ -436,6 +436,19 @@ impl<'a> From<Vec<Spans<'a>>> for Text<'a> {
} }
} }
impl<'a> From<Text<'a>> for String {
fn from(text: Text<'a>) -> String {
let lines: Vec<String> = 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<String> = text.lines.iter().map(String::from).collect();
lines.join("\n")
}
}
impl<'a> IntoIterator for Text<'a> { impl<'a> IntoIterator for Text<'a> {
type Item = Spans<'a>; type Item = Spans<'a>;
type IntoIter = std::vec::IntoIter<Self::Item>; type IntoIter = std::vec::IntoIter<Self::Item>;

@ -126,6 +126,14 @@ impl<'a> Row<'a> {
fn total_height(&self) -> u16 { fn total_height(&self) -> u16 {
self.height.saturating_add(self.bottom_margin) 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<String> {
self.cells
.iter()
.map(|cell| String::from(&cell.content))
.collect()
}
} }
/// A widget to display data in formatted columns. /// A widget to display data in formatted columns.
@ -477,6 +485,9 @@ impl<'a> Table<'a> {
}; };
let mut col = table_row_start_col; let mut col = table_row_start_col;
for (width, cell) in columns_widths.iter().zip(table_row.cells.iter()) { 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( render_cell(
buf, buf,
cell, cell,
@ -489,9 +500,6 @@ impl<'a> Table<'a> {
); );
col += *width + self.column_spacing; col += *width + self.column_spacing;
} }
if is_selected {
buf.set_style(table_row_area, self.highlight_style);
}
} }
} }
} }

Loading…
Cancel
Save