From 2546db8e7b47ea66b5f55d333985798faad08b44 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sun, 19 Jun 2022 23:20:51 +0530 Subject: [PATCH 1/6] Refactor menu::Item to accomodate external state Will be useful for storing editor state when reused by pickers. --- helix-term/src/commands/lsp.rs | 9 ++++--- helix-term/src/ui/completion.rs | 25 ++++++++++++------ helix-term/src/ui/menu.rs | 46 +++++++++++++++++++++++---------- helix-tui/src/text.rs | 6 +++++ 4 files changed, 61 insertions(+), 25 deletions(-) diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index b6bea8d617a3..9a49678ed5c8 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -209,10 +209,11 @@ pub fn workspace_symbol_picker(cx: &mut Context) { } impl ui::menu::Item for lsp::CodeActionOrCommand { - fn label(&self) -> &str { + type EditorData = (); + fn label(&self, _data: &Self::EditorData) -> Cow { 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(), } } } @@ -257,7 +258,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; } diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 38005aad033b..2e7fe0f17606 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -15,19 +15,28 @@ 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 EditorData = (); + fn sort_text(&self, _data: &Self::EditorData) -> Cow { + self.filter_text + .as_ref() + .unwrap_or(&self.label) + .as_str() + .into() } - fn filter_text(&self) -> &str { - self.filter_text.as_ref().unwrap_or(&self.label).as_str() + fn filter_text(&self, _data: &Self::EditorData) -> 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::EditorData) -> Cow { + self.label.as_str().into() } - fn row(&self) -> menu::Row { + fn row(&self, _data: &Self::EditorData) -> menu::Row { menu::Row::new(vec![ menu::Cell::from(self.label.as_str()), menu::Cell::from(match self.kind { @@ -85,7 +94,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 d67a429e37f3..cb1e6b4c9187 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -1,3 +1,5 @@ +use std::{borrow::Cow, path::PathBuf}; + use crate::{ compositor::{Callback, Component, Compositor, Context, EventResult}, ctrl, key, shift, @@ -14,22 +16,38 @@ 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 EditorData; + + fn label(&self, data: &Self::EditorData) -> Cow; - fn sort_text(&self) -> &str { - self.label() + fn sort_text(&self, data: &Self::EditorData) -> Cow { + self.label(data) } - fn filter_text(&self) -> &str { - self.label() + + fn filter_text(&self, data: &Self::EditorData) -> Cow { + self.label(data) } - fn row(&self) -> Row { - Row::new(vec![Cell::from(self.label())]) + fn row(&self, data: &Self::EditorData) -> Row { + Row::new(vec![Cell::from(self.label(data))]) + } +} + +impl Item for PathBuf { + /// Root prefix to strip. + type EditorData = PathBuf; + + fn label(&self, root_path: &Self::EditorData) -> Cow { + self.strip_prefix(&root_path) + .unwrap_or(self) + .to_string_lossy() } } pub struct Menu { options: Vec, + editor_data: T::EditorData, cursor: Option, @@ -52,10 +70,12 @@ impl Menu { // rendering) pub fn new( options: Vec, + editor_data: ::EditorData, 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, @@ -81,16 +101,16 @@ impl Menu { .iter() .enumerate() .filter_map(|(index, option)| { - let text = option.filter_text(); + let text = option.filter_text(&self.editor_data); // 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()); + .sort_unstable_by_key(|(index, _score)| self.options[*index].sort_text(&self.editor_data)); // reset cursor position self.cursor = None; @@ -125,10 +145,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(); @@ -296,7 +316,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-tui/src/text.rs b/helix-tui/src/text.rs index 8a974ddba465..91ab3904ea07 100644 --- a/helix-tui/src/text.rs +++ b/helix-tui/src/text.rs @@ -387,6 +387,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 { From a982978d4843cbe2af603064be701dcf09ded018 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sun, 19 Jun 2022 23:23:54 +0530 Subject: [PATCH 2/6] Add some type aliases for readability --- helix-dap/src/client.rs | 2 +- helix-dap/src/types.rs | 2 ++ helix-term/src/keymap.rs | 11 +++++------ 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/helix-dap/src/client.rs b/helix-dap/src/client.rs index 9498c64c141e..371cf3032e96 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 2c3df9c335bb..fd8456a430e5 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/keymap.rs b/helix-term/src/keymap.rs index db9588330855..592048898174 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, .. } => { From 0cec7fbb4224aae753dd23931795cb300257a50a Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sun, 19 Jun 2022 23:36:16 +0530 Subject: [PATCH 3/6] Reuse menu::Item trait in picker This opens the way for merging the menu and picker code in the future, since a picker is essentially a menu + prompt. More excitingly, this change will also allow aligning items in the picker, which would be useful (for example) in the command palette for aligning the descriptions to the left and the keybinds to the right in two separate columns. The item formatting of each picker has been kept as is, even though there is room for improvement now that we can format the data into columns, since that is better tackled in a separate PR. --- helix-term/src/commands.rs | 151 ++++++++++++++++++++------------- helix-term/src/commands/dap.rs | 51 ++++++++--- helix-term/src/commands/lsp.rs | 89 ++++++++++--------- helix-term/src/ui/menu.rs | 5 +- helix-term/src/ui/mod.rs | 5 +- helix-term/src/ui/picker.rs | 32 +++---- 6 files changed, 200 insertions(+), 133 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index c9c8e6a98c62..ab0745239fb5 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -44,6 +44,7 @@ use movement::Movement; use crate::{ args, compositor::{self, Component, Compositor}, + keymap::ReverseKeymap, ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent}, }; @@ -1737,8 +1738,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 EditorData = Option; + + fn label(&self, current_path: &Self::EditorData) -> Cow { + 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(); @@ -1800,7 +1835,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) @@ -1827,7 +1862,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| { @@ -1838,17 +1873,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.into(), action) { Ok(_) => {} Err(e) => { @@ -1870,7 +1896,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))); }); @@ -2152,8 +2180,10 @@ fn buffer_picker(cx: &mut Context) { is_current: bool, } - impl BufferMeta { - fn format(&self) -> Cow { + impl ui::menu::Item for BufferMeta { + type EditorData = (); + + fn label(&self, _data: &Self::EditorData) -> Cow { let path = self .path .as_deref() @@ -2193,7 +2223,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); }, @@ -2210,6 +2240,38 @@ fn buffer_picker(cx: &mut Context) { cx.push_layer(Box::new(overlayed(picker))); } +impl ui::menu::Item for MappableCommand { + type EditorData = ReverseKeymap; + + fn label(&self, keymap: &Self::EditorData) -> Cow { + // 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.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| { @@ -2226,46 +2288,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.to_string()) - .collect::>() - .join("+") - }) - .collect::>() - .join(", ") - }; - - let picker = Picker::new( - commands, - move |command| match command { - MappableCommand::Typable { doc, name, .. } => match keymap.get(name as &String) - { - Some(bindings) => format!("{} ({})", doc, fmt_binding(bindings)).into(), - None => doc.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 b897b2d5852b..92a94a9a64b6 100644 --- a/helix-term/src/commands/dap.rs +++ b/helix-term/src/commands/dap.rs @@ -4,7 +4,8 @@ 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; @@ -20,6 +21,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 EditorData = (); + + fn label(&self, _data: &Self::EditorData) -> std::borrow::Cow { + self.name.as_str().into() // TODO: include thread_states in the label + } +} + +impl ui::menu::Item for DebugTemplate { + type EditorData = (); + + fn label(&self, _data: &Self::EditorData) -> std::borrow::Cow { + self.name.as_str().into() + } +} + +impl ui::menu::Item for Thread { + type EditorData = ThreadStates; + + fn label(&self, thread_states: &Self::EditorData) -> std::borrow::Cow { + 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 +74,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 +266,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(); @@ -652,7 +675,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 9a49678ed5c8..8df22187a424 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -14,7 +14,7 @@ use crate::{ ui::{self, overlay::overlayed, FileLocation, FilePicker, Popup, PromptEvent}, }; -use std::borrow::Cow; +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 @@ -34,6 +34,52 @@ macro_rules! language_server { }; } +impl ui::menu::Item for lsp::Location { + /// Current working directory. + type EditorData = PathBuf; + + fn label(&self, cwdir: &Self::EditorData) -> Cow { + 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 EditorData = Option; + + fn label(&self, current_doc_path: &Self::EditorData) -> Cow { + 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(), + } + } + } +} + fn location_to_file_location(location: &lsp::Location) -> FileLocation { let path = location.uri.to_file_path().unwrap(); let line = Some(( @@ -81,29 +127,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, @@ -488,6 +519,7 @@ pub fn apply_workspace_edit( } } } + fn goto_impl( editor: &mut Editor, compositor: &mut Compositor, @@ -506,26 +538,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/ui/menu.rs b/helix-term/src/ui/menu.rs index cb1e6b4c9187..1d497f432fc9 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -109,8 +109,9 @@ impl Menu { }), ); // matches.sort_unstable_by_key(|(_, score)| -score); - self.matches - .sort_unstable_by_key(|(index, _score)| self.options[*index].sort_text(&self.editor_data)); + self.matches.sort_unstable_by_key(|(index, _score)| { + self.options[*index].sort_text(&self.editor_data) + }); // reset cursor position self.cursor = None; diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 23d0dca0a342..d3459e854714 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -170,10 +170,7 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi FilePicker::new( files, - move |path: &PathBuf| { - // format_fn - path.strip_prefix(&root).unwrap_or(path).to_string_lossy() - }, + root, move |cx, path: &PathBuf, action| { if let Err(e) = cx.editor.open(path.into(), 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 9ffe45c1b648..0cd25e8646d3 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -15,7 +15,6 @@ use tui::widgets::Widget; use std::time::Instant; use std::{ - borrow::Cow, cmp::Reverse, collections::HashMap, io::Read, @@ -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) -> Cow + 'static, + editor_data: T::EditorData, 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 | @@ -283,8 +284,9 @@ impl Component for FilePicker { } } -pub struct Picker { +pub struct Picker { options: Vec, + editor_data: T::EditorData, // filter: String, matcher: Box, /// (index, score) @@ -302,14 +304,13 @@ pub struct Picker { /// Whether to truncate the start (default true) pub truncate_start: bool, - format_fn: Box Cow>, callback_fn: Box, } -impl Picker { +impl Picker { pub fn new( options: Vec, - format_fn: impl Fn(&T) -> Cow + 'static, + editor_data: T::EditorData, callback_fn: impl Fn(&mut Context, &T, Action) + 'static, ) -> Self { let prompt = Prompt::new( @@ -321,6 +322,7 @@ impl Picker { let mut picker = Self { options, + editor_data, matcher: Box::new(Matcher::default()), matches: Vec::new(), filters: Vec::new(), @@ -328,7 +330,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, }; @@ -375,7 +376,7 @@ impl Picker { self.matches.retain_mut(|(index, score)| { let option = &self.options[*index]; // TODO: maybe using format_fn isn't the best idea here - let text = (self.format_fn)(option); + let text = option.label(&self.editor_data); match self.matcher.fuzzy_match(&text, pattern) { Some(s) => { @@ -403,8 +404,7 @@ impl Picker { self.filters.binary_search(&index).ok()?; } - // TODO: maybe using format_fn isn't the best idea here - let text = (self.format_fn)(option); + let text = option.filter_text(&self.editor_data); self.matcher .fuzzy_match(&text, pattern) @@ -481,7 +481,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) @@ -614,7 +614,7 @@ impl Component for Picker { surface.set_string(inner.x.saturating_sub(2), inner.y + i as u16, ">", selected); } - let formatted = (self.format_fn)(option); + let formatted = option.label(&self.editor_data); let (_score, highlights) = self .matcher From 96be454fde8c096a8b3161c686df6d5d62f97131 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Tue, 28 Jun 2022 19:45:07 +0530 Subject: [PATCH 4/6] Rename menu::Item::EditorData to Data --- helix-term/src/commands.rs | 12 ++++++------ helix-term/src/commands/dap.rs | 12 ++++++------ helix-term/src/commands/lsp.rs | 12 ++++++------ helix-term/src/ui/completion.rs | 10 +++++----- helix-term/src/ui/menu.rs | 18 +++++++++--------- helix-term/src/ui/picker.rs | 6 +++--- 6 files changed, 35 insertions(+), 35 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index b980b602cf95..fb071a764ec4 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1759,9 +1759,9 @@ fn global_search(cx: &mut Context) { } impl ui::menu::Item for FileResult { - type EditorData = Option; + type Data = Option; - fn label(&self, current_path: &Self::EditorData) -> Cow { + fn label(&self, current_path: &Self::Data) -> Cow { let relative_path = helix_core::path::get_relative_path(&self.path) .to_string_lossy() .into_owned(); @@ -2198,9 +2198,9 @@ fn buffer_picker(cx: &mut Context) { } impl ui::menu::Item for BufferMeta { - type EditorData = (); + type Data = (); - fn label(&self, _data: &Self::EditorData) -> Cow { + fn label(&self, _data: &Self::Data) -> Cow { let path = self .path .as_deref() @@ -2258,9 +2258,9 @@ fn buffer_picker(cx: &mut Context) { } impl ui::menu::Item for MappableCommand { - type EditorData = ReverseKeymap; + type Data = ReverseKeymap; - fn label(&self, keymap: &Self::EditorData) -> Cow { + fn label(&self, keymap: &Self::Data) -> Cow { // formats key bindings, multiple bindings are comma separated, // individual key presses are joined with `+` let fmt_binding = |bindings: &Vec>| -> String { diff --git a/helix-term/src/commands/dap.rs b/helix-term/src/commands/dap.rs index 92a94a9a64b6..32aff8dd3cdc 100644 --- a/helix-term/src/commands/dap.rs +++ b/helix-term/src/commands/dap.rs @@ -22,25 +22,25 @@ 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 EditorData = (); + type Data = (); - fn label(&self, _data: &Self::EditorData) -> std::borrow::Cow { + fn label(&self, _data: &Self::Data) -> std::borrow::Cow { self.name.as_str().into() // TODO: include thread_states in the label } } impl ui::menu::Item for DebugTemplate { - type EditorData = (); + type Data = (); - fn label(&self, _data: &Self::EditorData) -> std::borrow::Cow { + fn label(&self, _data: &Self::Data) -> std::borrow::Cow { self.name.as_str().into() } } impl ui::menu::Item for Thread { - type EditorData = ThreadStates; + type Data = ThreadStates; - fn label(&self, thread_states: &Self::EditorData) -> std::borrow::Cow { + fn label(&self, thread_states: &Self::Data) -> std::borrow::Cow { format!( "{} ({})", self.name, diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 4bc2f4caa8b1..aa7c3403ffd6 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -36,9 +36,9 @@ macro_rules! language_server { impl ui::menu::Item for lsp::Location { /// Current working directory. - type EditorData = PathBuf; + type Data = PathBuf; - fn label(&self, cwdir: &Self::EditorData) -> Cow { + fn label(&self, cwdir: &Self::Data) -> Cow { let file: Cow<'_, str> = (self.uri.scheme() == "file") .then(|| { self.uri @@ -61,9 +61,9 @@ impl ui::menu::Item for lsp::Location { impl ui::menu::Item for lsp::SymbolInformation { /// Path to currently focussed document - type EditorData = Option; + type Data = Option; - fn label(&self, current_doc_path: &Self::EditorData) -> Cow { + fn label(&self, current_doc_path: &Self::Data) -> Cow { if current_doc_path.as_ref() == Some(&self.location.uri) { self.name.as_str().into() } else { @@ -247,8 +247,8 @@ pub fn workspace_symbol_picker(cx: &mut Context) { } impl ui::menu::Item for lsp::CodeActionOrCommand { - type EditorData = (); - fn label(&self, _data: &Self::EditorData) -> Cow { + type Data = (); + fn label(&self, _data: &Self::Data) -> Cow { match self { lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str().into(), lsp::CodeActionOrCommand::Command(command) => command.title.as_str().into(), diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 2e7fe0f17606..708872a5914d 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -15,8 +15,8 @@ use helix_lsp::{lsp, util}; use lsp::CompletionItem; impl menu::Item for CompletionItem { - type EditorData = (); - fn sort_text(&self, _data: &Self::EditorData) -> Cow { + type Data = (); + fn sort_text(&self, _data: &Self::Data) -> Cow { self.filter_text .as_ref() .unwrap_or(&self.label) @@ -24,7 +24,7 @@ impl menu::Item for CompletionItem { .into() } - fn filter_text(&self, _data: &Self::EditorData) -> Cow { + fn filter_text(&self, _data: &Self::Data) -> Cow { self.filter_text .as_ref() .unwrap_or(&self.label) @@ -32,11 +32,11 @@ impl menu::Item for CompletionItem { .into() } - fn label(&self, _data: &Self::EditorData) -> Cow { + fn label(&self, _data: &Self::Data) -> Cow { self.label.as_str().into() } - fn row(&self, _data: &Self::EditorData) -> 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 { diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index b17e20ef5321..c2db19f3be62 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -17,28 +17,28 @@ use tui::layout::Constraint; pub trait Item { /// Additional editor state that is used for label calculation. - type EditorData; + type Data; - fn label(&self, data: &Self::EditorData) -> Cow; + fn label(&self, data: &Self::Data) -> Cow; - fn sort_text(&self, data: &Self::EditorData) -> Cow { + fn sort_text(&self, data: &Self::Data) -> Cow { self.label(data) } - fn filter_text(&self, data: &Self::EditorData) -> Cow { + fn filter_text(&self, data: &Self::Data) -> Cow { self.label(data) } - fn row(&self, data: &Self::EditorData) -> Row { + fn row(&self, data: &Self::Data) -> Row { Row::new(vec![Cell::from(self.label(data))]) } } impl Item for PathBuf { /// Root prefix to strip. - type EditorData = PathBuf; + type Data = PathBuf; - fn label(&self, root_path: &Self::EditorData) -> Cow { + fn label(&self, root_path: &Self::Data) -> Cow { self.strip_prefix(&root_path) .unwrap_or(self) .to_string_lossy() @@ -47,7 +47,7 @@ impl Item for PathBuf { pub struct Menu { options: Vec, - editor_data: T::EditorData, + editor_data: T::Data, cursor: Option, @@ -72,7 +72,7 @@ impl Menu { // rendering) pub fn new( options: Vec, - editor_data: ::EditorData, + editor_data: ::Data, callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static, ) -> Self { let mut menu = Self { diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index d128c58793a2..2d29d09351cc 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -88,7 +88,7 @@ impl Preview<'_, '_> { impl FilePicker { pub fn new( options: Vec, - editor_data: T::EditorData, + editor_data: T::Data, callback_fn: impl Fn(&mut Context, &T, Action) + 'static, preview_fn: impl Fn(&Editor, &T) -> Option + 'static, ) -> Self { @@ -283,7 +283,7 @@ impl Component for FilePicker { pub struct Picker { options: Vec, - editor_data: T::EditorData, + editor_data: T::Data, // filter: String, matcher: Box, /// (index, score) @@ -307,7 +307,7 @@ pub struct Picker { impl Picker { pub fn new( options: Vec, - editor_data: T::EditorData, + editor_data: T::Data, callback_fn: impl Fn(&mut Context, &T, Action) + 'static, ) -> Self { let prompt = Prompt::new( From 023f3b4806be31f313f9892bf72fb68c1526c5b6 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Tue, 28 Jun 2022 19:50:45 +0530 Subject: [PATCH 5/6] Call and inline filter_text() in sort_text() completion --- helix-term/src/ui/completion.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 708872a5914d..f37fd789310b 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -16,14 +16,11 @@ use lsp::CompletionItem; impl menu::Item for CompletionItem { type Data = (); - fn sort_text(&self, _data: &Self::Data) -> Cow { - self.filter_text - .as_ref() - .unwrap_or(&self.label) - .as_str() - .into() + fn sort_text(&self, data: &Self::Data) -> Cow { + self.filter_text(data) } + #[inline] fn filter_text(&self, _data: &Self::Data) -> Cow { self.filter_text .as_ref() From 5e79071321d6c77d0e3722c08249bcd2ad60a659 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sat, 2 Jul 2022 14:35:12 +0530 Subject: [PATCH 6/6] Rename diagnostic picker's Item::Data --- helix-term/src/commands/lsp.rs | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index deaccb48e8a1..7f82394ac622 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -93,23 +93,23 @@ struct DiagnosticStyles { error: Style, } -struct Diagnostic { +struct PickerDiagnostic { url: lsp::Url, - info: lsp::Diagnostic, + diag: lsp::Diagnostic, } -impl ui::menu::Item for Diagnostic { +impl ui::menu::Item for PickerDiagnostic { type Data = DiagnosticStyles; - fn label(&self, data: &Self::Data) -> Spans { + fn label(&self, styles: &Self::Data) -> Spans { let mut style = self - .info + .diag .severity .map(|s| match s { - DiagnosticSeverity::HINT => data.hint, - DiagnosticSeverity::INFORMATION => data.info, - DiagnosticSeverity::WARNING => data.warning, - DiagnosticSeverity::ERROR => data.error, + DiagnosticSeverity::HINT => styles.hint, + DiagnosticSeverity::INFORMATION => styles.info, + DiagnosticSeverity::WARNING => styles.warning, + DiagnosticSeverity::ERROR => styles.error, _ => Style::default(), }) .unwrap_or_default(); @@ -118,7 +118,7 @@ impl ui::menu::Item for Diagnostic { style.bg = None; let code = self - .info + .diag .code .as_ref() .map(|c| match c { @@ -133,7 +133,7 @@ impl ui::menu::Item for Diagnostic { Spans::from(vec![ Span::styled( - self.info.source.clone().unwrap_or_default(), + self.diag.source.clone().unwrap_or_default(), style.add_modifier(Modifier::BOLD), ), Span::raw(": "), @@ -141,7 +141,7 @@ impl ui::menu::Item for Diagnostic { Span::raw(" - "), Span::styled(code, style.add_modifier(Modifier::BOLD)), Span::raw(": "), - Span::styled(&self.info.message, style), + Span::styled(&self.diag.message, style), ]) } } @@ -247,7 +247,7 @@ fn diag_picker( diagnostics: BTreeMap>, current_path: Option, offset_encoding: OffsetEncoding, -) -> FilePicker { +) -> FilePicker { // TODO: drop current_path comparison and instead use workspace: bool flag? // flatten the map to a vec of (url, diag) pairs @@ -255,9 +255,9 @@ fn diag_picker( for (url, diags) in diagnostics { flat_diag.reserve(diags.len()); for diag in diags { - flat_diag.push(Diagnostic { + flat_diag.push(PickerDiagnostic { url: url.clone(), - info: diag, + diag, }); } } @@ -272,7 +272,7 @@ fn diag_picker( FilePicker::new( flat_diag, styles, - move |cx, Diagnostic { url, info: diag }, action| { + move |cx, PickerDiagnostic { url, diag }, action| { if current_path.as_ref() == Some(url) { let (view, doc) = current!(cx.editor); push_jump(view, doc); @@ -290,7 +290,7 @@ fn diag_picker( align_view(doc, view, Align::Center); } }, - move |_editor, Diagnostic { url, info: diag }| { + move |_editor, PickerDiagnostic { url, diag }| { let location = lsp::Location::new(url.clone(), diag.range); Some(location_to_file_location(&location)) },