From 18945587ffb5970cb856d689a426d12d55525cb4 Mon Sep 17 00:00:00 2001 From: LazyTanuki <43273245+lazytanuki@users.noreply.github.com> Date: Fri, 10 Mar 2023 18:37:22 +0100 Subject: [PATCH] feat: add icons to pickers --- helix-term/src/application.rs | 2 +- helix-term/src/commands.rs | 57 +++++++++++++---- helix-term/src/commands/dap.rs | 11 ++-- helix-term/src/commands/lsp.rs | 105 ++++++++++++++++++++++++------- helix-term/src/commands/typed.rs | 4 +- helix-term/src/ui/completion.rs | 4 +- helix-term/src/ui/menu.rs | 28 +++++---- helix-term/src/ui/mod.rs | 8 ++- helix-term/src/ui/picker.rs | 28 ++++++--- helix-tui/src/text.rs | 10 +++ 10 files changed, 194 insertions(+), 63 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 3da88c85..a0c4b552 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -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(); diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 17669924..45158a27 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -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; - 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>| -> 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), diff --git a/helix-term/src/commands/dap.rs b/helix-term/src/commands/dap.rs index 8efdc9cf..3c0214b0 100644 --- a/helix-term/src/commands/dap.rs +++ b/helix-term/src/commands/dap.rs @@ -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 diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 7a26b3cf..7b6499d4 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -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; - 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, current_path: Option, offset_encoding: OffsetEncoding, + editor: &Editor, ) -> FilePicker { // 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>| { + move |editor, compositor, response: Option>| { 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) }, diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 1d4b6285..e95aaec4 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -115,7 +115,7 @@ fn open(cx: &mut compositor::Context, args: &[Cow], 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))) diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index bc216509..efd41ef1 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -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) diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index bdad2e40..be94eeee 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -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 { - 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 { - 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 Menu { 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 Component for Menu { 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) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 80454c0f..abe0c112 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -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 { +pub fn file_picker( + root: PathBuf, + config: &helix_view::editor::Config, + icons: &Icons, +) -> FilePicker { 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() { diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index e7a7de90..18ff7313 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -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 FilePicker { pub fn new( options: Vec, editor_data: T::Data, + icons: Option<&'_ Icons>, callback_fn: impl Fn(&mut Context, &T, Action) + 'static, preview_fn: impl Fn(&Editor, &T) -> Option + '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 { widths: Vec, callback_fn: PickerCallback, + has_icons: bool, } impl Picker { pub fn new( options: Vec, 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 Picker { 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 Picker { picker } - pub fn set_options(&mut self, new_options: Vec) { + pub fn set_options(&mut self, new_options: Vec, 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 Component for Picker { .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 Component for DynamicPicker { 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) diff --git a/helix-tui/src/text.rs b/helix-tui/src/text.rs index 076766dd..b9836b3a 100644 --- a/helix-tui/src/text.rs +++ b/helix-tui/src/text.rs @@ -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> 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>);