feat: add icons to pickers

pull/11/head
LazyTanuki 1 year ago
parent cfcf2ff4ff
commit 18945587ff

@ -188,7 +188,7 @@ impl Application {
if first.is_dir() {
std::env::set_current_dir(first).context("set current dir")?;
editor.new_file(Action::VerticalSplit);
let picker = ui::file_picker(".".into(), &config.load().editor);
let picker = ui::file_picker(".".into(), &config.load().editor, &editor.icons);
compositor.push(Box::new(overlaid(picker)));
} else {
let nr_of_files = args.files.len();

@ -6,7 +6,7 @@ pub use dap::*;
use helix_vcs::Hunk;
pub use lsp::*;
use tokio::sync::oneshot;
use tui::widgets::Row;
use tui::{text::Span, widgets::Row};
pub use typed::*;
use helix_core::{
@ -34,6 +34,7 @@ use helix_view::{
clipboard::ClipboardType,
document::{FormatterError, Mode, SCRATCH_BUFFER_NAME},
editor::{Action, Motion},
icons::Icons,
info::Info,
input::KeyEvent,
keyboard::KeyCode,
@ -1989,11 +1990,12 @@ fn global_search(cx: &mut Context) {
impl ui::menu::Item for FileResult {
type Data = Option<PathBuf>;
fn format(&self, current_path: &Self::Data) -> Row {
fn format<'a>(&self, current_path: &Self::Data, icons: Option<&'a Icons>) -> Row {
let icon = icons.and_then(|icons| icons.icon_from_path(Some(&self.path)));
let relative_path = helix_core::path::get_relative_path(&self.path)
.to_string_lossy()
.into_owned();
if current_path
let path_span: Span = if current_path
.as_ref()
.map(|p| p == &self.path)
.unwrap_or(false)
@ -2001,6 +2003,12 @@ fn global_search(cx: &mut Context) {
format!("{} (*)", relative_path).into()
} else {
relative_path.into()
};
if let Some(icon) = icon {
Row::new([icon.into(), path_span])
} else {
path_span.into()
}
}
}
@ -2117,6 +2125,7 @@ fn global_search(cx: &mut Context) {
let picker = FilePicker::new(
all_matches,
current_path,
editor.config().icons.picker.then_some(&editor.icons),
move |cx, FileResult { path, line_num }, action| {
match cx.editor.open(path, action) {
Ok(_) => {}
@ -2420,7 +2429,7 @@ fn append_mode(cx: &mut Context) {
fn file_picker(cx: &mut Context) {
let root = find_workspace().0;
let picker = ui::file_picker(root, &cx.editor.config());
let picker = ui::file_picker(root, &cx.editor.config(), &cx.editor.icons);
cx.push_layer(Box::new(overlaid(picker)));
}
@ -2437,12 +2446,12 @@ fn file_picker_in_current_buffer_directory(cx: &mut Context) {
}
};
let picker = ui::file_picker(path, &cx.editor.config());
let picker = ui::file_picker(path, &cx.editor.config(), &cx.editor.icons);
cx.push_layer(Box::new(overlaid(picker)));
}
fn file_picker_in_current_directory(cx: &mut Context) {
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("./"));
let picker = ui::file_picker(cwd, &cx.editor.config());
let picker = ui::file_picker(cwd, &cx.editor.config(), &cx.editor.icons);
cx.push_layer(Box::new(overlaid(picker)));
}
@ -2459,7 +2468,7 @@ fn buffer_picker(cx: &mut Context) {
impl ui::menu::Item for BufferMeta {
type Data = ();
fn format(&self, _data: &Self::Data) -> Row {
fn format<'a>(&self, _data: &Self::Data, icons: Option<&'a Icons>) -> Row {
let path = self
.path
.as_deref()
@ -2469,6 +2478,9 @@ fn buffer_picker(cx: &mut Context) {
None => SCRATCH_BUFFER_NAME,
};
// Get the filetype icon, or a "file" icon for scratch buffers
let icon = icons.and_then(|icons| icons.icon_from_path(self.path.as_ref()));
let mut flags = String::new();
if self.is_modified {
flags.push('+');
@ -2477,7 +2489,17 @@ fn buffer_picker(cx: &mut Context) {
flags.push('*');
}
Row::new([self.id.to_string(), flags, path.to_string()])
if let Some(icon) = icon {
let icon_span = Span::from(icon);
Row::new(vec![
icon_span,
self.id.to_string().into(),
flags.into(),
path.to_string().into(),
])
} else {
Row::new([self.id.to_string(), flags, path.to_string()])
}
}
}
@ -2495,6 +2517,7 @@ fn buffer_picker(cx: &mut Context) {
.map(|doc| new_meta(doc))
.collect(),
(),
cx.editor.config().icons.picker.then_some(&cx.editor.icons),
|cx, meta, action| {
cx.editor.switch(meta.id, action);
},
@ -2523,7 +2546,10 @@ fn jumplist_picker(cx: &mut Context) {
impl ui::menu::Item for JumpMeta {
type Data = ();
fn format(&self, _data: &Self::Data) -> Row {
fn format<'a>(&self, _data: &Self::Data, icons: Option<&'a Icons>) -> Row {
// Get the filetype icon, or a "file" icon for scratch buffers
let icon = icons.and_then(|icons| icons.icon_from_path(self.path.as_ref()));
let path = self
.path
.as_deref()
@ -2543,7 +2569,13 @@ fn jumplist_picker(cx: &mut Context) {
} else {
format!(" ({})", flags.join(""))
};
format!("{} {}{} {}", self.id, path, flag, self.text).into()
let path_span: Span = format!("{} {}{} {}", self.id, path, flag, self.text).into();
if let Some(icon) = icon {
Row::new(vec![icon.into(), path_span])
} else {
path_span.into()
}
}
}
@ -2577,6 +2609,7 @@ fn jumplist_picker(cx: &mut Context) {
})
.collect(),
(),
cx.editor.config().icons.picker.then_some(&cx.editor.icons),
|cx, meta, action| {
cx.editor.switch(meta.id, action);
let config = cx.editor.config();
@ -2596,7 +2629,7 @@ fn jumplist_picker(cx: &mut Context) {
impl ui::menu::Item for MappableCommand {
type Data = ReverseKeymap;
fn format(&self, keymap: &Self::Data) -> Row {
fn format<'a>(&self, keymap: &Self::Data, _icons: Option<&'a Icons>) -> Row {
let fmt_binding = |bindings: &Vec<Vec<KeyEvent>>| -> String {
bindings.iter().fold(String::new(), |mut acc, bind| {
if !acc.is_empty() {
@ -2638,7 +2671,7 @@ pub fn command_palette(cx: &mut Context) {
}
}));
let picker = Picker::new(commands, keymap, move |cx, command, _action| {
let picker = Picker::new(commands, keymap, None, move |cx, command, _action| {
let mut ctx = Context {
register: None,
count: std::num::NonZeroUsize::new(1),

@ -8,7 +8,7 @@ use dap::{StackFrame, Thread, ThreadStates};
use helix_core::syntax::{DebugArgumentValue, DebugConfigCompletion, DebugTemplate};
use helix_dap::{self as dap, Client};
use helix_lsp::block_on;
use helix_view::editor::Breakpoint;
use helix_view::{editor::Breakpoint, icons::Icons};
use serde_json::{to_value, Value};
use tokio_stream::wrappers::UnboundedReceiverStream;
@ -25,7 +25,7 @@ use helix_view::handlers::dap::{breakpoints_changed, jump_to_stack_frame, select
impl ui::menu::Item for StackFrame {
type Data = ();
fn format(&self, _data: &Self::Data) -> Row {
fn format<'a>(&self, _data: &Self::Data, _icons: Option<&'a Icons>) -> Row {
self.name.as_str().into() // TODO: include thread_states in the label
}
}
@ -33,7 +33,7 @@ impl ui::menu::Item for StackFrame {
impl ui::menu::Item for DebugTemplate {
type Data = ();
fn format(&self, _data: &Self::Data) -> Row {
fn format<'a>(&self, _data: &Self::Data, _icons: Option<&'a Icons>) -> Row {
self.name.as_str().into()
}
}
@ -41,7 +41,7 @@ impl ui::menu::Item for DebugTemplate {
impl ui::menu::Item for Thread {
type Data = ThreadStates;
fn format(&self, thread_states: &Self::Data) -> Row {
fn format<'a>(&self, thread_states: &Self::Data, _icons: Option<&'a Icons>) -> Row {
format!(
"{} ({})",
self.name,
@ -76,6 +76,7 @@ fn thread_picker(
let picker = FilePicker::new(
threads,
thread_states,
None,
move |cx, thread, _action| callback_fn(cx.editor, thread),
move |editor, thread| {
let frames = editor.debugger.as_ref()?.stack_frames.get(&thread.id)?;
@ -273,6 +274,7 @@ pub fn dap_launch(cx: &mut Context) {
cx.push_layer(Box::new(overlaid(Picker::new(
templates,
(),
None,
|cx, template, _action| {
let completions = template.completion.clone();
let name = template.name.clone();
@ -731,6 +733,7 @@ pub fn dap_switch_stack_frame(cx: &mut Context) {
let picker = FilePicker::new(
frames,
(),
None,
move |cx, frame, _action| {
let debugger = debugger!(cx.editor);
// TODO: this should be simpler to find

@ -3,15 +3,12 @@ use helix_lsp::{
block_on,
lsp::{
self, CodeAction, CodeActionOrCommand, CodeActionTriggerKind, DiagnosticSeverity,
NumberOrString,
NumberOrString, SymbolKind,
},
util::{diagnostic_to_lsp_diagnostic, lsp_range_to_range, range_to_lsp_range},
OffsetEncoding,
};
use tui::{
text::{Span, Spans},
widgets::Row,
};
use tui::{text::Span, widgets::Row};
use super::{align_view, push_jump, Align, Context, Editor, Open};
@ -19,6 +16,7 @@ use helix_core::{path, text_annotations::InlineAnnotation, Selection};
use helix_view::{
document::{DocumentInlayHints, DocumentInlayHintsId, Mode},
editor::Action,
icons::{self, Icon, Icons},
theme::Style,
Document, View,
};
@ -57,7 +55,7 @@ impl ui::menu::Item for lsp::Location {
/// Current working directory.
type Data = PathBuf;
fn format(&self, cwdir: &Self::Data) -> Row {
fn format<'a>(&self, cwdir: &Self::Data, _icons: Option<&'a Icons>) -> Row {
// The preallocation here will overallocate a few characters since it will account for the
// URL's scheme, which is not used most of the time since that scheme will be "file://".
// Those extra chars will be used to avoid allocating when writing the line number (in the
@ -91,16 +89,58 @@ impl ui::menu::Item for lsp::SymbolInformation {
/// Path to currently focussed document
type Data = Option<lsp::Url>;
fn format(&self, current_doc_path: &Self::Data) -> Row {
fn format<'a>(&self, current_doc_path: &Self::Data, icons: Option<&'a Icons>) -> Row {
let icon =
icons
.and_then(|icons| icons.symbol_kind.as_ref())
.and_then(|symbol_kind_icons| match self.kind {
SymbolKind::FILE => symbol_kind_icons.get("file"),
SymbolKind::MODULE => symbol_kind_icons.get("module"),
SymbolKind::NAMESPACE => symbol_kind_icons.get("namespace"),
SymbolKind::PACKAGE => symbol_kind_icons.get("package"),
SymbolKind::CLASS => symbol_kind_icons.get("class"),
SymbolKind::METHOD => symbol_kind_icons.get("method"),
SymbolKind::PROPERTY => symbol_kind_icons.get("property"),
SymbolKind::FIELD => symbol_kind_icons.get("field"),
SymbolKind::CONSTRUCTOR => symbol_kind_icons.get("constructor"),
SymbolKind::ENUM => symbol_kind_icons.get("enumeration"),
SymbolKind::INTERFACE => symbol_kind_icons.get("interface"),
SymbolKind::FUNCTION => symbol_kind_icons.get("function"),
SymbolKind::VARIABLE => symbol_kind_icons.get("variable"),
SymbolKind::CONSTANT => symbol_kind_icons.get("constant"),
SymbolKind::STRING => symbol_kind_icons.get("string"),
SymbolKind::NUMBER => symbol_kind_icons.get("number"),
SymbolKind::BOOLEAN => symbol_kind_icons.get("boolean"),
SymbolKind::ARRAY => symbol_kind_icons.get("array"),
SymbolKind::OBJECT => symbol_kind_icons.get("object"),
SymbolKind::KEY => symbol_kind_icons.get("key"),
SymbolKind::NULL => symbol_kind_icons.get("null"),
SymbolKind::ENUM_MEMBER => symbol_kind_icons.get("enum-member"),
SymbolKind::STRUCT => symbol_kind_icons.get("structure"),
SymbolKind::EVENT => symbol_kind_icons.get("event"),
SymbolKind::OPERATOR => symbol_kind_icons.get("operator"),
SymbolKind::TYPE_PARAMETER => symbol_kind_icons.get("type-parameter"),
_ => Some(&icons::BLANK_ICON),
});
if current_doc_path.as_ref() == Some(&self.location.uri) {
self.name.as_str().into()
if let Some(icon) = icon {
Row::new([Span::from(icon), self.name.as_str().into()])
} else {
self.name.as_str().into()
}
} else {
match self.location.uri.to_file_path() {
let symbol_span: Span = match self.location.uri.to_file_path() {
Ok(path) => {
let get_relative_path = path::get_relative_path(path.as_path());
format!("{} ({})", &self.name, get_relative_path.to_string_lossy()).into()
}
Err(_) => format!("{} ({})", &self.name, &self.location.uri).into(),
};
if let Some(icon) = icon {
Row::new([Span::from(icon), symbol_span])
} else {
Row::from(symbol_span)
}
}
}
@ -121,7 +161,18 @@ struct PickerDiagnostic {
impl ui::menu::Item for PickerDiagnostic {
type Data = (DiagnosticStyles, DiagnosticsFormat);
fn format(&self, (styles, format): &Self::Data) -> Row {
fn format<'a>(&self, (styles, format): &Self::Data, icons: Option<&'a Icons>) -> Row {
let icon: Option<&'a Icon> =
icons
.zip(self.diag.severity)
.map(|(icons, severity)| match severity {
DiagnosticSeverity::ERROR => &icons.diagnostic.error,
DiagnosticSeverity::WARNING => &icons.diagnostic.warning,
DiagnosticSeverity::HINT => &icons.diagnostic.hint,
DiagnosticSeverity::INFORMATION => &icons.diagnostic.info,
_ => &icons::BLANK_ICON,
});
let mut style = self
.diag
.severity
@ -152,12 +203,20 @@ impl ui::menu::Item for PickerDiagnostic {
}
};
Spans::from(vec![
Span::raw(path),
Span::styled(&self.diag.message, style),
Span::styled(code, style),
])
.into()
if let Some(icon) = icon {
Row::new(vec![
icon.into(),
Span::raw(path),
Span::styled(&self.diag.message, style),
Span::styled(code, style),
])
} else {
Row::new(vec![
Span::raw(path),
Span::styled(&self.diag.message, style),
Span::styled(code, style),
])
}
}
}
@ -213,11 +272,13 @@ fn sym_picker(
symbols: Vec<lsp::SymbolInformation>,
current_path: Option<lsp::Url>,
offset_encoding: OffsetEncoding,
editor: &Editor,
) -> FilePicker<lsp::SymbolInformation> {
// TODO: drop current_path comparison and instead use workspace: bool flag?
FilePicker::new(
symbols,
current_path.clone(),
editor.config().icons.picker.then_some(&editor.icons),
move |cx, symbol, action| {
let (view, doc) = current!(cx.editor);
push_jump(view, doc);
@ -293,6 +354,7 @@ fn diag_picker(
FilePicker::new(
flat_diag,
(styles, format),
cx.editor.config().icons.picker.then_some(&cx.editor.icons),
move |cx, PickerDiagnostic { url, diag }, action| {
if current_path.as_ref() == Some(url) {
let (view, doc) = current!(cx.editor);
@ -371,7 +433,7 @@ pub fn symbol_picker(cx: &mut Context) {
}
};
let picker = sym_picker(symbols, current_url, offset_encoding);
let picker = sym_picker(symbols, current_url, offset_encoding, editor);
compositor.push(Box::new(overlaid(picker)))
}
},
@ -394,9 +456,9 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
cx.callback(
future,
move |_editor, compositor, response: Option<Vec<lsp::SymbolInformation>>| {
move |editor, compositor, response: Option<Vec<lsp::SymbolInformation>>| {
let symbols = response.unwrap_or_default();
let picker = sym_picker(symbols, current_url, offset_encoding);
let picker = sym_picker(symbols, current_url, offset_encoding, editor);
let get_symbols = |query: String, editor: &mut Editor| {
let doc = doc!(editor);
let language_server = match doc.language_server() {
@ -476,7 +538,7 @@ pub fn workspace_diagnostics_picker(cx: &mut Context) {
impl ui::menu::Item for lsp::CodeActionOrCommand {
type Data = ();
fn format(&self, _data: &Self::Data) -> Row {
fn format<'a>(&self, _data: &Self::Data, _icons: Option<&'a Icons>) -> Row {
match self {
lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str().into(),
lsp::CodeActionOrCommand::Command(command) => command.title.as_str().into(),
@ -672,7 +734,7 @@ pub fn code_action(cx: &mut Context) {
impl ui::menu::Item for lsp::Command {
type Data = ();
fn format(&self, _data: &Self::Data) -> Row {
fn format<'a>(&self, _data: &Self::Data, _icons: Option<&'a Icons>) -> Row {
self.title.as_str().into()
}
}
@ -950,6 +1012,7 @@ fn goto_impl(
let picker = FilePicker::new(
locations,
cwdir,
None,
move |cx, location, action| {
jump_to_location(cx.editor, location, offset_encoding, action)
},

@ -115,7 +115,7 @@ fn open(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) ->
let callback = async move {
let call: job::Callback = job::Callback::EditorCompositor(Box::new(
move |editor: &mut Editor, compositor: &mut Compositor| {
let picker = ui::file_picker(path, &editor.config());
let picker = ui::file_picker(path, &editor.config(), &editor.icons);
compositor.push(Box::new(overlaid(picker)));
},
));
@ -1356,7 +1356,7 @@ fn lsp_workspace_command(
let callback = async move {
let call: job::Callback = Callback::EditorCompositor(Box::new(
move |_editor: &mut Editor, compositor: &mut Compositor| {
let picker = ui::Picker::new(commands, (), |cx, command, _action| {
let picker = ui::Picker::new(commands, (), None, |cx, command, _action| {
execute_lsp_command(cx.editor, command.clone());
});
compositor.push(Box::new(overlaid(picker)))

@ -2,6 +2,7 @@ use crate::compositor::{Component, Context, Event, EventResult};
use helix_view::{
document::SavePoint,
editor::CompleteAction,
icons::Icons,
theme::{Modifier, Style},
ViewId,
};
@ -33,7 +34,8 @@ impl menu::Item for CompletionItem {
.into()
}
fn format(&self, _data: &Self::Data) -> menu::Row {
// Before implementing icons for the `CompletionItemKind`s, something must be done to `Menu::required_size` and `Menu::recalculate_size` in order to have correct sizes even with icons.
fn format<'a>(&self, _data: &Self::Data, _icons: Option<&'a Icons>) -> menu::Row {
let deprecated = self.deprecated.unwrap_or_default()
|| self.tags.as_ref().map_or(false, |tags| {
tags.contains(&lsp::CompletionItemTag::DEPRECATED)

@ -4,29 +4,29 @@ use crate::{
compositor::{Callback, Component, Compositor, Context, Event, EventResult},
ctrl, key, shift,
};
use tui::{buffer::Buffer as Surface, widgets::Table};
use tui::{buffer::Buffer as Surface, text::Span, widgets::Table};
pub use tui::widgets::{Cell, Row};
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher;
use helix_view::{graphics::Rect, Editor};
use helix_view::{graphics::Rect, icons::Icons, Editor};
use tui::layout::Constraint;
pub trait Item {
/// Additional editor state that is used for label calculation.
type Data;
fn format(&self, data: &Self::Data) -> Row;
fn format<'a>(&self, data: &Self::Data, icons: Option<&'a Icons>) -> Row;
fn sort_text(&self, data: &Self::Data) -> Cow<str> {
let label: String = self.format(data).cell_text().collect();
let label: String = self.format(data, None).cell_text().collect();
label.into()
}
fn filter_text(&self, data: &Self::Data) -> Cow<str> {
let label: String = self.format(data).cell_text().collect();
let label: String = self.format(data, None).cell_text().collect();
label.into()
}
}
@ -35,11 +35,15 @@ impl Item for PathBuf {
/// Root prefix to strip.
type Data = PathBuf;
fn format(&self, root_path: &Self::Data) -> Row {
self.strip_prefix(root_path)
fn format<'a>(&self, root_path: &Self::Data, icons: Option<&'a Icons>) -> Row {
let path_str = self
.strip_prefix(root_path)
.unwrap_or(self)
.to_string_lossy()
.into()
.to_string_lossy();
match icons.and_then(|icons| icons.icon_from_path(Some(self))) {
Some(icon) => Row::new([icon.into(), Span::raw(path_str)]),
None => path_str.into(),
}
}
}
@ -142,10 +146,10 @@ impl<T: Item> Menu<T> {
let n = self
.options
.first()
.map(|option| option.format(&self.editor_data).cells.len())
.map(|option| option.format(&self.editor_data, None).cells.len())
.unwrap_or_default();
let max_lens = self.options.iter().fold(vec![0; n], |mut acc, option| {
let row = option.format(&self.editor_data);
let row = option.format(&self.editor_data, None);
// maintain max for each column
for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) {
let width = cell.content.width();
@ -331,7 +335,7 @@ impl<T: Item + 'static> Component for Menu<T> {
let rows = options
.iter()
.map(|option| option.format(&self.editor_data));
.map(|option| option.format(&self.editor_data, None));
let table = Table::new(rows)
.style(style)
.highlight_style(selected)

@ -19,6 +19,7 @@ use crate::filter_picker_entry;
use crate::job::{self, Callback};
pub use completion::Completion;
pub use editor::EditorView;
use helix_view::icons::Icons;
pub use markdown::Markdown;
pub use menu::Menu;
pub use picker::{DynamicPicker, FileLocation, FilePicker, Picker};
@ -158,7 +159,11 @@ pub fn regex_prompt(
cx.push_layer(Box::new(prompt));
}
pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePicker<PathBuf> {
pub fn file_picker(
root: PathBuf,
config: &helix_view::editor::Config,
icons: &Icons,
) -> FilePicker<PathBuf> {
use ignore::{types::TypesBuilder, WalkBuilder};
use std::time::Instant;
@ -220,6 +225,7 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi
FilePicker::new(
files,
root,
config.icons.picker.then_some(icons),
move |cx, path: &PathBuf, action| {
if let Err(e) = cx.editor.open(path, action) {
let err = if let Some(err) = e.source() {

@ -31,6 +31,7 @@ use helix_core::{
use helix_view::{
editor::Action,
graphics::{CursorKind, Margin, Modifier, Rect},
icons::Icons,
theme::Style,
view::ViewPosition,
Document, DocumentId, Editor,
@ -126,11 +127,12 @@ impl<T: Item> FilePicker<T> {
pub fn new(
options: Vec<T>,
editor_data: T::Data,
icons: Option<&'_ Icons>,
callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
preview_fn: impl Fn(&Editor, &T) -> Option<FileLocation> + 'static,
) -> Self {
let truncate_start = true;
let mut picker = Picker::new(options, editor_data, callback_fn);
let mut picker = Picker::new(options, editor_data, icons, callback_fn);
picker.truncate_start = truncate_start;
Self {
@ -424,12 +426,14 @@ pub struct Picker<T: Item> {
widths: Vec<Constraint>,
callback_fn: PickerCallback<T>,
has_icons: bool,
}
impl<T: Item> Picker<T> {
pub fn new(
options: Vec<T>,
editor_data: T::Data,
icons: Option<&'_ Icons>,
callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
) -> Self {
let prompt = Prompt::new(
@ -452,9 +456,10 @@ impl<T: Item> Picker<T> {
callback_fn: Box::new(callback_fn),
completion_height: 0,
widths: Vec::new(),
has_icons: icons.is_some(),
};
picker.calculate_column_widths();
picker.calculate_column_widths(icons);
// scoring on empty input
// TODO: just reuse score()
@ -472,23 +477,23 @@ impl<T: Item> Picker<T> {
picker
}
pub fn set_options(&mut self, new_options: Vec<T>) {
pub fn set_options(&mut self, new_options: Vec<T>, icons: &'_ Icons) {
self.options = new_options;
self.cursor = 0;
self.force_score();
self.calculate_column_widths();
self.calculate_column_widths(self.has_icons.then_some(icons));
}
/// Calculate the width constraints using the maximum widths of each column
/// for the current options.
fn calculate_column_widths(&mut self) {
fn calculate_column_widths(&mut self, icons: Option<&'_ Icons>) {
let n = self
.options
.first()
.map(|option| option.format(&self.editor_data).cells.len())
.map(|option| option.format(&self.editor_data, icons).cells.len())
.unwrap_or_default();
let max_lens = self.options.iter().fold(vec![0; n], |mut acc, option| {
let row = option.format(&self.editor_data);
let row = option.format(&self.editor_data, icons);
// maintain max for each column
for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) {
let width = cell.content.width();
@ -779,7 +784,12 @@ impl<T: Item + 'static> Component for Picker<T> {
.skip(offset)
.take(rows as usize)
.map(|pmatch| &self.options[pmatch.index])
.map(|option| option.format(&self.editor_data))
.map(|option| {
option.format(
&self.editor_data,
cx.editor.config().icons.picker.then_some(&cx.editor.icons),
)
})
.map(|mut row| {
const TEMP_CELL_SEP: &str = " ";
@ -953,7 +963,7 @@ impl<T: Item + Send + 'static> Component for DynamicPicker<T> {
Some(overlay) => &mut overlay.content.file_picker.picker,
None => return,
};
picker.set_options(new_options);
picker.set_options(new_options, &editor.icons);
editor.reset_idle_timer();
}));
anyhow::Ok(callback)

@ -49,6 +49,7 @@
use helix_core::line_ending::str_is_line_ending;
use helix_core::unicode::width::UnicodeWidthStr;
use helix_view::graphics::Style;
use helix_view::icons::Icon;
use std::borrow::Cow;
use unicode_segmentation::UnicodeSegmentation;
@ -208,6 +209,15 @@ impl<'a> From<Cow<'a, str>> for Span<'a> {
}
}
impl<'a, 'b> From<&'b Icon> for Span<'a> {
fn from(icon: &'b Icon) -> Self {
Span {
content: format!("{}", icon.icon_char).into(),
style: icon.style.unwrap_or_default().into(),
}
}
}
/// A string composed of clusters of graphemes, each with their own style.
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Spans<'a>(pub Vec<Span<'a>>);

Loading…
Cancel
Save