diff --git a/helix-dap/src/client.rs b/helix-dap/src/client.rs index 9498c64c..371cf303 100644 --- a/helix-dap/src/client.rs +++ b/helix-dap/src/client.rs @@ -34,7 +34,7 @@ pub struct Client { pub caps: Option, // thread_id -> frames pub stack_frames: HashMap>, - pub thread_states: HashMap, + pub thread_states: ThreadStates, pub thread_id: Option, /// Currently active frame for the current thread. pub active_frame: Option, diff --git a/helix-dap/src/types.rs b/helix-dap/src/types.rs index 2c3df9c3..fd8456a4 100644 --- a/helix-dap/src/types.rs +++ b/helix-dap/src/types.rs @@ -14,6 +14,8 @@ impl std::fmt::Display for ThreadId { } } +pub type ThreadStates = HashMap; + pub trait Request { type Arguments: serde::de::DeserializeOwned + serde::Serialize; type Result: serde::de::DeserializeOwned + serde::Serialize; diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 59ca2e3b..df4867fc 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -45,6 +45,7 @@ use movement::Movement; use crate::{ args, compositor::{self, Component, Compositor}, + keymap::ReverseKeymap, ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent}, }; @@ -1744,8 +1745,42 @@ fn search_selection(cx: &mut Context) { } fn global_search(cx: &mut Context) { - let (all_matches_sx, all_matches_rx) = - tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>(); + #[derive(Debug)] + struct FileResult { + path: PathBuf, + /// 0 indexed lines + line_num: usize, + } + + impl FileResult { + fn new(path: &Path, line_num: usize) -> Self { + Self { + path: path.to_path_buf(), + line_num, + } + } + } + + impl ui::menu::Item for FileResult { + type Data = Option; + + fn label(&self, current_path: &Self::Data) -> Spans { + let relative_path = helix_core::path::get_relative_path(&self.path) + .to_string_lossy() + .into_owned(); + if current_path + .as_ref() + .map(|p| p == &self.path) + .unwrap_or(false) + { + format!("{} (*)", relative_path).into() + } else { + relative_path.into() + } + } + } + + let (all_matches_sx, all_matches_rx) = tokio::sync::mpsc::unbounded_channel::(); let config = cx.editor.config(); let smart_case = config.search.smart_case; let file_picker_config = config.file_picker.clone(); @@ -1809,7 +1844,7 @@ fn global_search(cx: &mut Context) { entry.path(), sinks::UTF8(|line_num, _| { all_matches_sx - .send((line_num as usize - 1, entry.path().to_path_buf())) + .send(FileResult::new(entry.path(), line_num as usize - 1)) .unwrap(); Ok(true) @@ -1836,7 +1871,7 @@ fn global_search(cx: &mut Context) { let current_path = doc_mut!(cx.editor).path().cloned(); let show_picker = async move { - let all_matches: Vec<(usize, PathBuf)> = + let all_matches: Vec = UnboundedReceiverStream::new(all_matches_rx).collect().await; let call: job::Callback = Box::new(move |editor: &mut Editor, compositor: &mut Compositor| { @@ -1847,17 +1882,8 @@ fn global_search(cx: &mut Context) { let picker = FilePicker::new( all_matches, - move |(_line_num, path)| { - let relative_path = helix_core::path::get_relative_path(path) - .to_string_lossy() - .into_owned(); - if current_path.as_ref().map(|p| p == path).unwrap_or(false) { - format!("{} (*)", relative_path).into() - } else { - relative_path.into() - } - }, - move |cx, (line_num, path), action| { + current_path, + move |cx, FileResult { path, line_num }, action| { match cx.editor.open(path, action) { Ok(_) => {} Err(e) => { @@ -1879,7 +1905,9 @@ fn global_search(cx: &mut Context) { doc.set_selection(view.id, Selection::single(start, end)); align_view(doc, view, Align::Center); }, - |_editor, (line_num, path)| Some((path.clone(), Some((*line_num, *line_num)))), + |_editor, FileResult { path, line_num }| { + Some((path.clone(), Some((*line_num, *line_num)))) + }, ); compositor.push(Box::new(overlayed(picker))); }); @@ -2172,8 +2200,10 @@ fn buffer_picker(cx: &mut Context) { is_current: bool, } - impl BufferMeta { - fn format(&self) -> Spans { + impl ui::menu::Item for BufferMeta { + type Data = (); + + fn label(&self, _data: &Self::Data) -> Spans { let path = self .path .as_deref() @@ -2213,7 +2243,7 @@ fn buffer_picker(cx: &mut Context) { .iter() .map(|(_, doc)| new_meta(doc)) .collect(), - BufferMeta::format, + (), |cx, meta, action| { cx.editor.switch(meta.id, action); }, @@ -2230,6 +2260,38 @@ fn buffer_picker(cx: &mut Context) { cx.push_layer(Box::new(overlayed(picker))); } +impl ui::menu::Item for MappableCommand { + type Data = ReverseKeymap; + + fn label(&self, keymap: &Self::Data) -> Spans { + // formats key bindings, multiple bindings are comma separated, + // individual key presses are joined with `+` + let fmt_binding = |bindings: &Vec>| -> String { + bindings + .iter() + .map(|bind| { + bind.iter() + .map(|key| key.to_string()) + .collect::>() + .join("+") + }) + .collect::>() + .join(", ") + }; + + match self { + MappableCommand::Typable { doc, name, .. } => match keymap.get(name as &String) { + Some(bindings) => format!("{} ({})", doc, fmt_binding(bindings)).into(), + None => doc.as_str().into(), + }, + MappableCommand::Static { doc, name, .. } => match keymap.get(*name) { + Some(bindings) => format!("{} ({})", doc, fmt_binding(bindings)).into(), + None => (*doc).into(), + }, + } + } +} + pub fn command_palette(cx: &mut Context) { cx.callback = Some(Box::new( move |compositor: &mut Compositor, cx: &mut compositor::Context| { @@ -2246,44 +2308,17 @@ pub fn command_palette(cx: &mut Context) { } })); - // formats key bindings, multiple bindings are comma separated, - // individual key presses are joined with `+` - let fmt_binding = |bindings: &Vec>| -> String { - bindings - .iter() - .map(|bind| { - bind.iter() - .map(|key| key.key_sequence_format()) - .collect::() - }) - .collect::>() - .join(", ") - }; - - let picker = Picker::new( - commands, - move |command| match command { - MappableCommand::Typable { doc, name, .. } => match keymap.get(name) { - Some(bindings) => format!("{} ({})", doc, fmt_binding(bindings)).into(), - None => doc.as_str().into(), - }, - MappableCommand::Static { doc, name, .. } => match keymap.get(*name) { - Some(bindings) => format!("{} ({})", doc, fmt_binding(bindings)).into(), - None => (*doc).into(), - }, - }, - move |cx, command, _action| { - let mut ctx = Context { - register: None, - count: std::num::NonZeroUsize::new(1), - editor: cx.editor, - callback: None, - on_next_key_callback: None, - jobs: cx.jobs, - }; - command.execute(&mut ctx); - }, - ); + let picker = Picker::new(commands, keymap, move |cx, command, _action| { + let mut ctx = Context { + register: None, + count: std::num::NonZeroUsize::new(1), + editor: cx.editor, + callback: None, + on_next_key_callback: None, + jobs: cx.jobs, + }; + command.execute(&mut ctx); + }); compositor.push(Box::new(overlayed(picker))); }, )); diff --git a/helix-term/src/commands/dap.rs b/helix-term/src/commands/dap.rs index b897b2d5..9f6f4c15 100644 --- a/helix-term/src/commands/dap.rs +++ b/helix-term/src/commands/dap.rs @@ -4,13 +4,15 @@ use crate::{ job::{Callback, Jobs}, ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent, Text}, }; -use helix_core::syntax::{DebugArgumentValue, DebugConfigCompletion}; +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 serde_json::{to_value, Value}; use tokio_stream::wrappers::UnboundedReceiverStream; +use tui::text::Spans; use std::collections::HashMap; use std::future::Future; @@ -20,6 +22,38 @@ use anyhow::{anyhow, bail}; use helix_view::handlers::dap::{breakpoints_changed, jump_to_stack_frame, select_thread_id}; +impl ui::menu::Item for StackFrame { + type Data = (); + + fn label(&self, _data: &Self::Data) -> Spans { + self.name.as_str().into() // TODO: include thread_states in the label + } +} + +impl ui::menu::Item for DebugTemplate { + type Data = (); + + fn label(&self, _data: &Self::Data) -> Spans { + self.name.as_str().into() + } +} + +impl ui::menu::Item for Thread { + type Data = ThreadStates; + + fn label(&self, thread_states: &Self::Data) -> Spans { + format!( + "{} ({})", + self.name, + thread_states + .get(&self.id) + .map(|state| state.as_str()) + .unwrap_or("unknown") + ) + .into() + } +} + fn thread_picker( cx: &mut Context, callback_fn: impl Fn(&mut Editor, &dap::Thread) + Send + 'static, @@ -41,17 +75,7 @@ fn thread_picker( let thread_states = debugger.thread_states.clone(); let picker = FilePicker::new( threads, - move |thread| { - format!( - "{} ({})", - thread.name, - thread_states - .get(&thread.id) - .map(|state| state.as_str()) - .unwrap_or("unknown") - ) - .into() - }, + thread_states, move |cx, thread, _action| callback_fn(cx.editor, thread), move |editor, thread| { let frames = editor.debugger.as_ref()?.stack_frames.get(&thread.id)?; @@ -243,7 +267,7 @@ pub fn dap_launch(cx: &mut Context) { cx.push_layer(Box::new(overlayed(Picker::new( templates, - |template| template.name.as_str().into(), + (), |cx, template, _action| { let completions = template.completion.clone(); let name = template.name.clone(); @@ -475,7 +499,7 @@ pub fn dap_variables(cx: &mut Context) { for scope in scopes.iter() { // use helix_view::graphics::Style; - use tui::text::{Span, Spans}; + use tui::text::Span; let response = block_on(debugger.variables(scope.variables_reference)); variables.push(Spans::from(Span::styled( @@ -652,7 +676,7 @@ pub fn dap_switch_stack_frame(cx: &mut Context) { let picker = FilePicker::new( frames, - |frame| frame.name.as_str().into(), // TODO: include thread_states in the label + (), 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 d11c44cd..7f82394a 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -19,7 +19,8 @@ use crate::{ ui::{self, overlay::overlayed, FileLocation, FilePicker, Popup, PromptEvent}, }; -use std::{borrow::Cow, collections::BTreeMap}; +use std::collections::BTreeMap; +use std::{borrow::Cow, path::PathBuf}; /// Gets the language server that is attached to a document, and /// if it's not active displays a status message. Using this macro @@ -39,6 +40,112 @@ macro_rules! language_server { }; } +impl ui::menu::Item for lsp::Location { + /// Current working directory. + type Data = PathBuf; + + fn label(&self, cwdir: &Self::Data) -> Spans { + let file: Cow<'_, str> = (self.uri.scheme() == "file") + .then(|| { + self.uri + .to_file_path() + .map(|path| { + // strip root prefix + path.strip_prefix(&cwdir) + .map(|path| path.to_path_buf()) + .unwrap_or(path) + }) + .map(|path| Cow::from(path.to_string_lossy().into_owned())) + .ok() + }) + .flatten() + .unwrap_or_else(|| self.uri.as_str().into()); + let line = self.range.start.line; + format!("{}:{}", file, line).into() + } +} + +impl ui::menu::Item for lsp::SymbolInformation { + /// Path to currently focussed document + type Data = Option; + + fn label(&self, current_doc_path: &Self::Data) -> Spans { + if current_doc_path.as_ref() == Some(&self.location.uri) { + self.name.as_str().into() + } else { + match self.location.uri.to_file_path() { + Ok(path) => { + let relative_path = helix_core::path::get_relative_path(path.as_path()) + .to_string_lossy() + .into_owned(); + format!("{} ({})", &self.name, relative_path).into() + } + Err(_) => format!("{} ({})", &self.name, &self.location.uri).into(), + } + } + } +} + +struct DiagnosticStyles { + hint: Style, + info: Style, + warning: Style, + error: Style, +} + +struct PickerDiagnostic { + url: lsp::Url, + diag: lsp::Diagnostic, +} + +impl ui::menu::Item for PickerDiagnostic { + type Data = DiagnosticStyles; + + fn label(&self, styles: &Self::Data) -> Spans { + let mut style = self + .diag + .severity + .map(|s| match s { + DiagnosticSeverity::HINT => styles.hint, + DiagnosticSeverity::INFORMATION => styles.info, + DiagnosticSeverity::WARNING => styles.warning, + DiagnosticSeverity::ERROR => styles.error, + _ => Style::default(), + }) + .unwrap_or_default(); + + // remove background as it is distracting in the picker list + style.bg = None; + + let code = self + .diag + .code + .as_ref() + .map(|c| match c { + NumberOrString::Number(n) => n.to_string(), + NumberOrString::String(s) => s.to_string(), + }) + .unwrap_or_default(); + + let truncated_path = path::get_truncated_path(self.url.path()) + .to_string_lossy() + .into_owned(); + + Spans::from(vec![ + Span::styled( + self.diag.source.clone().unwrap_or_default(), + style.add_modifier(Modifier::BOLD), + ), + Span::raw(": "), + Span::styled(truncated_path, style), + Span::raw(" - "), + Span::styled(code, style.add_modifier(Modifier::BOLD)), + Span::raw(": "), + Span::styled(&self.diag.message, style), + ]) + } +} + fn location_to_file_location(location: &lsp::Location) -> FileLocation { let path = location.uri.to_file_path().unwrap(); let line = Some(( @@ -93,29 +200,14 @@ fn sym_picker( offset_encoding: OffsetEncoding, ) -> FilePicker { // TODO: drop current_path comparison and instead use workspace: bool flag? - let current_path2 = current_path.clone(); FilePicker::new( symbols, - move |symbol| { - if current_path.as_ref() == Some(&symbol.location.uri) { - symbol.name.as_str().into() - } else { - match symbol.location.uri.to_file_path() { - Ok(path) => { - let relative_path = helix_core::path::get_relative_path(path.as_path()) - .to_string_lossy() - .into_owned(); - format!("{} ({})", &symbol.name, relative_path).into() - } - Err(_) => format!("{} ({})", &symbol.name, &symbol.location.uri).into(), - } - } - }, + current_path.clone(), move |cx, symbol, action| { let (view, doc) = current!(cx.editor); push_jump(view, doc); - if current_path2.as_ref() != Some(&symbol.location.uri) { + if current_path.as_ref() != Some(&symbol.location.uri) { let uri = &symbol.location.uri; let path = match uri.to_file_path() { Ok(path) => path, @@ -155,7 +247,7 @@ fn diag_picker( diagnostics: BTreeMap>, current_path: Option, offset_encoding: OffsetEncoding, -) -> FilePicker<(lsp::Url, lsp::Diagnostic)> { +) -> FilePicker { // TODO: drop current_path comparison and instead use workspace: bool flag? // flatten the map to a vec of (url, diag) pairs @@ -163,59 +255,24 @@ fn diag_picker( for (url, diags) in diagnostics { flat_diag.reserve(diags.len()); for diag in diags { - flat_diag.push((url.clone(), diag)); + flat_diag.push(PickerDiagnostic { + url: url.clone(), + diag, + }); } } - let hint = cx.editor.theme.get("hint"); - let info = cx.editor.theme.get("info"); - let warning = cx.editor.theme.get("warning"); - let error = cx.editor.theme.get("error"); + let styles = DiagnosticStyles { + hint: cx.editor.theme.get("hint"), + info: cx.editor.theme.get("info"), + warning: cx.editor.theme.get("warning"), + error: cx.editor.theme.get("error"), + }; FilePicker::new( flat_diag, - move |(url, diag)| { - let mut style = diag - .severity - .map(|s| match s { - DiagnosticSeverity::HINT => hint, - DiagnosticSeverity::INFORMATION => info, - DiagnosticSeverity::WARNING => warning, - DiagnosticSeverity::ERROR => error, - _ => Style::default(), - }) - .unwrap_or_default(); - - // remove background as it is distracting in the picker list - style.bg = None; - - let code = diag - .code - .as_ref() - .map(|c| match c { - NumberOrString::Number(n) => n.to_string(), - NumberOrString::String(s) => s.to_string(), - }) - .unwrap_or_default(); - - let truncated_path = path::get_truncated_path(url.path()) - .to_string_lossy() - .into_owned(); - - Spans::from(vec![ - Span::styled( - diag.source.clone().unwrap_or_default(), - style.add_modifier(Modifier::BOLD), - ), - Span::raw(": "), - Span::styled(truncated_path, style), - Span::raw(" - "), - Span::styled(code, style.add_modifier(Modifier::BOLD)), - Span::raw(": "), - Span::styled(&diag.message, style), - ]) - }, - move |cx, (url, diag), action| { + styles, + move |cx, PickerDiagnostic { url, diag }, action| { if current_path.as_ref() == Some(url) { let (view, doc) = current!(cx.editor); push_jump(view, doc); @@ -233,7 +290,7 @@ fn diag_picker( align_view(doc, view, Align::Center); } }, - move |_editor, (url, diag)| { + move |_editor, PickerDiagnostic { url, diag }| { let location = lsp::Location::new(url.clone(), diag.range); Some(location_to_file_location(&location)) }, @@ -343,10 +400,11 @@ pub fn workspace_diagnostics_picker(cx: &mut Context) { } impl ui::menu::Item for lsp::CodeActionOrCommand { - fn label(&self) -> &str { + type Data = (); + fn label(&self, _data: &Self::Data) -> Spans { match self { - lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str(), - lsp::CodeActionOrCommand::Command(command) => command.title.as_str(), + lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str().into(), + lsp::CodeActionOrCommand::Command(command) => command.title.as_str().into(), } } } @@ -391,7 +449,7 @@ pub fn code_action(cx: &mut Context) { return; } - let mut picker = ui::Menu::new(actions, move |editor, code_action, event| { + let mut picker = ui::Menu::new(actions, (), move |editor, code_action, event| { if event != PromptEvent::Validate { return; } @@ -619,6 +677,7 @@ pub fn apply_workspace_edit( } } } + fn goto_impl( editor: &mut Editor, compositor: &mut Compositor, @@ -637,26 +696,7 @@ fn goto_impl( _locations => { let picker = FilePicker::new( locations, - move |location| { - let file: Cow<'_, str> = (location.uri.scheme() == "file") - .then(|| { - location - .uri - .to_file_path() - .map(|path| { - // strip root prefix - path.strip_prefix(&cwdir) - .map(|path| path.to_path_buf()) - .unwrap_or(path) - }) - .map(|path| Cow::from(path.to_string_lossy().into_owned())) - .ok() - }) - .flatten() - .unwrap_or_else(|| location.uri.as_str().into()); - let line = location.range.start.line; - format!("{}:{}", file, line).into() - }, + cwdir, move |cx, location, action| { jump_to_location(cx.editor, location, offset_encoding, action) }, diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index db958833..59204889 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -208,18 +208,17 @@ pub struct Keymap { root: KeyTrie, } +/// A map of command names to keybinds that will execute the command. +pub type ReverseKeymap = HashMap>>; + impl Keymap { pub fn new(root: KeyTrie) -> Self { Keymap { root } } - pub fn reverse_map(&self) -> HashMap>> { + pub fn reverse_map(&self) -> ReverseKeymap { // recursively visit all nodes in keymap - fn map_node( - cmd_map: &mut HashMap>>, - node: &KeyTrie, - keys: &mut Vec, - ) { + fn map_node(cmd_map: &mut ReverseKeymap, node: &KeyTrie, keys: &mut Vec) { match node { KeyTrie::Leaf(cmd) => match cmd { MappableCommand::Typable { name, .. } => { diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 38005aad..a3637415 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -2,6 +2,7 @@ use crate::compositor::{Component, Context, EventResult}; use crossterm::event::{Event, KeyCode, KeyEvent}; use helix_view::editor::CompleteAction; use tui::buffer::Buffer as Surface; +use tui::text::Spans; use std::borrow::Cow; @@ -15,19 +16,25 @@ use helix_lsp::{lsp, util}; use lsp::CompletionItem; impl menu::Item for CompletionItem { - fn sort_text(&self) -> &str { - self.filter_text.as_ref().unwrap_or(&self.label).as_str() + type Data = (); + fn sort_text(&self, data: &Self::Data) -> Cow { + self.filter_text(data) } - fn filter_text(&self) -> &str { - self.filter_text.as_ref().unwrap_or(&self.label).as_str() + #[inline] + fn filter_text(&self, _data: &Self::Data) -> Cow { + self.filter_text + .as_ref() + .unwrap_or(&self.label) + .as_str() + .into() } - fn label(&self) -> &str { - self.label.as_str() + fn label(&self, _data: &Self::Data) -> Spans { + self.label.as_str().into() } - fn row(&self) -> menu::Row { + fn row(&self, _data: &Self::Data) -> menu::Row { menu::Row::new(vec![ menu::Cell::from(self.label.as_str()), menu::Cell::from(match self.kind { @@ -85,7 +92,7 @@ impl Completion { start_offset: usize, trigger_offset: usize, ) -> Self { - let menu = Menu::new(items, move |editor: &mut Editor, item, event| { + let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| { fn item_to_transaction( doc: &Document, item: &CompletionItem, diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index 0519374a..6bb64139 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -1,9 +1,11 @@ +use std::{borrow::Cow, path::PathBuf}; + use crate::{ compositor::{Callback, Component, Compositor, Context, EventResult}, ctrl, key, shift, }; use crossterm::event::Event; -use tui::{buffer::Buffer as Surface, widgets::Table}; +use tui::{buffer::Buffer as Surface, text::Spans, widgets::Table}; pub use tui::widgets::{Cell, Row}; @@ -14,22 +16,41 @@ use helix_view::{graphics::Rect, Editor}; use tui::layout::Constraint; pub trait Item { - fn label(&self) -> &str; + /// Additional editor state that is used for label calculation. + type Data; - fn sort_text(&self) -> &str { - self.label() - } - fn filter_text(&self) -> &str { - self.label() + fn label(&self, data: &Self::Data) -> Spans; + + fn sort_text(&self, data: &Self::Data) -> Cow { + let label: String = self.label(data).into(); + label.into() } - fn row(&self) -> Row { - Row::new(vec![Cell::from(self.label())]) + fn filter_text(&self, data: &Self::Data) -> Cow { + let label: String = self.label(data).into(); + label.into() + } + + fn row(&self, data: &Self::Data) -> Row { + Row::new(vec![Cell::from(self.label(data))]) + } +} + +impl Item for PathBuf { + /// Root prefix to strip. + type Data = PathBuf; + + fn label(&self, root_path: &Self::Data) -> Spans { + self.strip_prefix(&root_path) + .unwrap_or(self) + .to_string_lossy() + .into() } } pub struct Menu { options: Vec, + editor_data: T::Data, cursor: Option, @@ -54,10 +75,12 @@ impl Menu { // rendering) pub fn new( options: Vec, + editor_data: ::Data, callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static, ) -> Self { let mut menu = Self { options, + editor_data, matcher: Box::new(Matcher::default()), matches: Vec::new(), cursor: None, @@ -83,16 +106,17 @@ impl Menu { .iter() .enumerate() .filter_map(|(index, option)| { - let text = option.filter_text(); + let text: String = option.filter_text(&self.editor_data).into(); // TODO: using fuzzy_indices could give us the char idx for match highlighting self.matcher - .fuzzy_match(text, pattern) + .fuzzy_match(&text, pattern) .map(|score| (index, score)) }), ); // matches.sort_unstable_by_key(|(_, score)| -score); - self.matches - .sort_unstable_by_key(|(index, _score)| self.options[*index].sort_text()); + self.matches.sort_unstable_by_key(|(index, _score)| { + self.options[*index].sort_text(&self.editor_data) + }); // reset cursor position self.cursor = None; @@ -127,10 +151,10 @@ impl Menu { let n = self .options .first() - .map(|option| option.row().cells.len()) + .map(|option| option.row(&self.editor_data).cells.len()) .unwrap_or_default(); let max_lens = self.options.iter().fold(vec![0; n], |mut acc, option| { - let row = option.row(); + let row = option.row(&self.editor_data); // maintain max for each column for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) { let width = cell.content.width(); @@ -300,7 +324,7 @@ impl Component for Menu { let scroll_line = (win_height - scroll_height) * scroll / std::cmp::max(1, len.saturating_sub(win_height)); - let rows = options.iter().map(|option| option.row()); + let rows = options.iter().map(|option| option.row(&self.editor_data)); 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 948a5f2b..8d2bd325 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -23,8 +23,6 @@ pub use text::Text; use helix_core::regex::Regex; use helix_core::regex::RegexBuilder; use helix_view::{Document, Editor, View}; -use tui; -use tui::text::Spans; use std::path::PathBuf; @@ -172,10 +170,7 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi FilePicker::new( files, - move |path: &PathBuf| { - // format_fn - Spans::from(path.strip_prefix(&root).unwrap_or(path).to_string_lossy()) - }, + root, 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 1581b0a1..01fea718 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -6,7 +6,6 @@ use crate::{ use crossterm::event::Event; use tui::{ buffer::Buffer as Surface, - text::Spans, widgets::{Block, BorderType, Borders}, }; @@ -30,6 +29,8 @@ use helix_view::{ Document, Editor, }; +use super::menu::Item; + pub const MIN_AREA_WIDTH_FOR_PREVIEW: u16 = 72; /// Biggest file size to preview in bytes pub const MAX_FILE_SIZE_FOR_PREVIEW: u64 = 10 * 1024 * 1024; @@ -37,7 +38,7 @@ pub const MAX_FILE_SIZE_FOR_PREVIEW: u64 = 10 * 1024 * 1024; /// File path and range of lines (used to align and highlight lines) pub type FileLocation = (PathBuf, Option<(usize, usize)>); -pub struct FilePicker { +pub struct FilePicker { picker: Picker, pub truncate_start: bool, /// Caches paths to documents @@ -84,15 +85,15 @@ impl Preview<'_, '_> { } } -impl FilePicker { +impl FilePicker { pub fn new( options: Vec, - format_fn: impl Fn(&T) -> Spans + 'static, + editor_data: T::Data, 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, format_fn, callback_fn); + let mut picker = Picker::new(options, editor_data, callback_fn); picker.truncate_start = truncate_start; Self { @@ -163,7 +164,7 @@ impl FilePicker { } } -impl Component for FilePicker { +impl Component for FilePicker { fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { // +---------+ +---------+ // |prompt | |preview | @@ -280,8 +281,9 @@ impl Component for FilePicker { } } -pub struct Picker { +pub struct Picker { options: Vec, + editor_data: T::Data, // filter: String, matcher: Box, /// (index, score) @@ -299,14 +301,13 @@ pub struct Picker { /// Whether to truncate the start (default true) pub truncate_start: bool, - format_fn: Box Spans>, callback_fn: Box, } -impl Picker { +impl Picker { pub fn new( options: Vec, - format_fn: impl Fn(&T) -> Spans + 'static, + editor_data: T::Data, callback_fn: impl Fn(&mut Context, &T, Action) + 'static, ) -> Self { let prompt = Prompt::new( @@ -318,6 +319,7 @@ impl Picker { let mut picker = Self { options, + editor_data, matcher: Box::new(Matcher::default()), matches: Vec::new(), filters: Vec::new(), @@ -325,7 +327,6 @@ impl Picker { prompt, previous_pattern: String::new(), truncate_start: true, - format_fn: Box::new(format_fn), callback_fn: Box::new(callback_fn), completion_height: 0, }; @@ -371,9 +372,9 @@ impl Picker { #[allow(unstable_name_collisions)] self.matches.retain_mut(|(index, score)| { let option = &self.options[*index]; - // TODO: maybe using format_fn isn't the best idea here - let line: String = (self.format_fn)(option).into(); - match self.matcher.fuzzy_match(&line, pattern) { + let text = option.sort_text(&self.editor_data); + + match self.matcher.fuzzy_match(&text, pattern) { Some(s) => { // Update the score *score = s; @@ -399,11 +400,10 @@ impl Picker { self.filters.binary_search(&index).ok()?; } - // TODO: maybe using format_fn isn't the best idea here - let line: String = (self.format_fn)(option).into(); + let text = option.filter_text(&self.editor_data); self.matcher - .fuzzy_match(&line, pattern) + .fuzzy_match(&text, pattern) .map(|score| (index, score)) }), ); @@ -477,7 +477,7 @@ impl Picker { // - on input change: // - score all the names in relation to input -impl Component for Picker { +impl Component for Picker { fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { self.completion_height = viewport.1.saturating_sub(4); Some(viewport) @@ -610,7 +610,7 @@ impl Component for Picker { surface.set_string(inner.x.saturating_sub(2), inner.y + i as u16, ">", selected); } - let spans = (self.format_fn)(option); + let spans = option.label(&self.editor_data); let (_score, highlights) = self .matcher .fuzzy_indices(&String::from(&spans), self.prompt.line()) diff --git a/helix-tui/src/text.rs b/helix-tui/src/text.rs index b4278c86..602090e5 100644 --- a/helix-tui/src/text.rs +++ b/helix-tui/src/text.rs @@ -402,6 +402,12 @@ impl<'a> From<&'a str> for Text<'a> { } } +impl<'a> From> for Text<'a> { + fn from(s: Cow<'a, str>) -> Text<'a> { + Text::raw(s) + } +} + impl<'a> From> for Text<'a> { fn from(span: Span<'a>) -> Text<'a> { Text {