on master help sign in
Reuse menu::Item trait in picker (#2814)

* Refactor menu::Item to accomodate external state Will be useful for storing editor state when reused by pickers. * Add some type aliases for readability * 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. * Rename menu::Item::EditorData to Data * Call and inline filter_text() in sort_text() completion * Rename diagnostic picker's Item::Data

commit6e2aaed5c2cbcedc9ee4e225510cae4f357888aa authorGokul Soumya <gokulps15@gmail.com> dateSat 2 Jul 2022, 16:51 (4 years ago) parent290b3eb
11 files changed+350−218
helix-dap/src/client.rs+1−1 helix-dap/src/types.rs+2 helix-term/src/commands.rs+92−57 helix-term/src/commands/dap.rs+39−15 helix-term/src/commands/lsp.rs+131−91 helix-term/src/keymap.rs+5−6 helix-term/src/ui/completion.rs+15−8 helix-term/src/ui/menu.rs+39−15 helix-term/src/ui/mod.rs+1−6 helix-term/src/ui/picker.rs+19−19 helix-tui/src/text.rs+6
helix-dap/src/client.rs+1−1
@@ -34,7 +34,7 @@ pub struct Client {
3434 pub caps: Option<DebuggerCapabilities>,
3535 // thread_id -> frames
3636 pub stack_frames: HashMap<ThreadId, Vec<StackFrame>>,
37− pub thread_states: HashMap<ThreadId, String>,
37+ pub thread_states: ThreadStates,
3838 pub thread_id: Option<ThreadId>,
3939 /// Currently active frame for the current thread.
4040 pub active_frame: Option<usize>,
helix-dap/src/types.rs+2
@@ -14,6 +14,8 @@ impl std::fmt::Display for ThreadId {
1414 }
1515}
1616
17+pub type ThreadStates = HashMap<ThreadId, String>;
18+
1719pub trait Request {
1820 type Arguments: serde::de::DeserializeOwned + serde::Serialize;
1921 type Result: serde::de::DeserializeOwned + serde::Serialize;
helix-term/src/commands.rs+92−57
@@ -45,6 +45,7 @@ use movement::Movement;
4545use crate::{
4646 args,
4747 compositor::{self, Component, Compositor},
48+ keymap::ReverseKeymap,
4849 ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent},
4950};
5051
@@ -1744,8 +1745,42 @@ fn search_selection(cx: &mut Context) {
17441745}
17451746
17461747fn global_search(cx: &mut Context) {
1747− let (all_matches_sx, all_matches_rx) =
1748− tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>();
1748+ #[derive(Debug)]
1749+ struct FileResult {
1750+ path: PathBuf,
1751+ /// 0 indexed lines
1752+ line_num: usize,
1753+ }
1754+
1755+ impl FileResult {
1756+ fn new(path: &Path, line_num: usize) -> Self {
1757+ Self {
1758+ path: path.to_path_buf(),
1759+ line_num,
1760+ }
1761+ }
1762+ }
1763+
1764+ impl ui::menu::Item for FileResult {
1765+ type Data = Option<PathBuf>;
1766+
1767+ fn label(&self, current_path: &Self::Data) -> Spans {
1768+ let relative_path = helix_core::path::get_relative_path(&self.path)
1769+ .to_string_lossy()
1770+ .into_owned();
1771+ if current_path
1772+ .as_ref()
1773+ .map(|p| p == &self.path)
1774+ .unwrap_or(false)
1775+ {
1776+ format!("{} (*)", relative_path).into()
1777+ } else {
1778+ relative_path.into()
1779+ }
1780+ }
1781+ }
1782+
1783+ let (all_matches_sx, all_matches_rx) = tokio::sync::mpsc::unbounded_channel::<FileResult>();
17491784 let config = cx.editor.config();
17501785 let smart_case = config.search.smart_case;
17511786 let file_picker_config = config.file_picker.clone();
@@ -1809,7 +1844,7 @@ fn global_search(cx: &mut Context) {
18091844 entry.path(),
18101845 sinks::UTF8(|line_num, _| {
18111846 all_matches_sx
1812− .send((line_num as usize - 1, entry.path().to_path_buf()))
1847+ .send(FileResult::new(entry.path(), line_num as usize - 1))
18131848 .unwrap();
18141849
18151850 Ok(true)
@@ -1836,7 +1871,7 @@ fn global_search(cx: &mut Context) {
18361871 let current_path = doc_mut!(cx.editor).path().cloned();
18371872
18381873 let show_picker = async move {
1839− let all_matches: Vec<(usize, PathBuf)> =
1874+ let all_matches: Vec<FileResult> =
18401875 UnboundedReceiverStream::new(all_matches_rx).collect().await;
18411876 let call: job::Callback =
18421877 Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
@@ -1847,17 +1882,8 @@ fn global_search(cx: &mut Context) {
18471882
18481883 let picker = FilePicker::new(
18491884 all_matches,
1850− move |(_line_num, path)| {
1851− let relative_path = helix_core::path::get_relative_path(path)
1852− .to_string_lossy()
1853− .into_owned();
1854− if current_path.as_ref().map(|p| p == path).unwrap_or(false) {
1855− format!("{} (*)", relative_path).into()
1856− } else {
1857− relative_path.into()
1858− }
1859− },
1860− move |cx, (line_num, path), action| {
1885+ current_path,
1886+ move |cx, FileResult { path, line_num }, action| {
18611887 match cx.editor.open(path, action) {
18621888 Ok(_) => {}
18631889 Err(e) => {
@@ -1879,7 +1905,9 @@ fn global_search(cx: &mut Context) {
18791905 doc.set_selection(view.id, Selection::single(start, end));
18801906 align_view(doc, view, Align::Center);
18811907 },
1882− |_editor, (line_num, path)| Some((path.clone(), Some((*line_num, *line_num)))),
1908+ |_editor, FileResult { path, line_num }| {
1909+ Some((path.clone(), Some((*line_num, *line_num))))
1910+ },
18831911 );
18841912 compositor.push(Box::new(overlayed(picker)));
18851913 });
@@ -2172,8 +2200,10 @@ fn buffer_picker(cx: &mut Context) {
21722200 is_current: bool,
21732201 }
21742202
2175− impl BufferMeta {
2176− fn format(&self) -> Spans {
2203+ impl ui::menu::Item for BufferMeta {
2204+ type Data = ();
2205+
2206+ fn label(&self, _data: &Self::Data) -> Spans {
21772207 let path = self
21782208 .path
21792209 .as_deref()
@@ -2213,7 +2243,7 @@ fn buffer_picker(cx: &mut Context) {
22132243 .iter()
22142244 .map(|(_, doc)| new_meta(doc))
22152245 .collect(),
2216− BufferMeta::format,
2246+ (),
22172247 |cx, meta, action| {
22182248 cx.editor.switch(meta.id, action);
22192249 },
@@ -2230,6 +2260,38 @@ fn buffer_picker(cx: &mut Context) {
22302260 cx.push_layer(Box::new(overlayed(picker)));
22312261}
22322262
2263+impl ui::menu::Item for MappableCommand {
2264+ type Data = ReverseKeymap;
2265+
2266+ fn label(&self, keymap: &Self::Data) -> Spans {
2267+ // formats key bindings, multiple bindings are comma separated,
2268+ // individual key presses are joined with `+`
2269+ let fmt_binding = |bindings: &Vec<Vec<KeyEvent>>| -> String {
2270+ bindings
2271+ .iter()
2272+ .map(|bind| {
2273+ bind.iter()
2274+ .map(|key| key.to_string())
2275+ .collect::<Vec<String>>()
2276+ .join("+")
2277+ })
2278+ .collect::<Vec<String>>()
2279+ .join(", ")
2280+ };
2281+
2282+ match self {
2283+ MappableCommand::Typable { doc, name, .. } => match keymap.get(name as &String) {
2284+ Some(bindings) => format!("{} ({})", doc, fmt_binding(bindings)).into(),
2285+ None => doc.as_str().into(),
2286+ },
2287+ MappableCommand::Static { doc, name, .. } => match keymap.get(*name) {
2288+ Some(bindings) => format!("{} ({})", doc, fmt_binding(bindings)).into(),
2289+ None => (*doc).into(),
2290+ },
2291+ }
2292+ }
2293+}
2294+
22332295pub fn command_palette(cx: &mut Context) {
22342296 cx.callback = Some(Box::new(
22352297 move |compositor: &mut Compositor, cx: &mut compositor::Context| {
@@ -2246,44 +2308,17 @@ pub fn command_palette(cx: &mut Context) {
22462308 }
22472309 }));
22482310
2249− // formats key bindings, multiple bindings are comma separated,
2250− // individual key presses are joined with `+`
2251− let fmt_binding = |bindings: &Vec<Vec<KeyEvent>>| -> String {
2252− bindings
2253− .iter()
2254− .map(|bind| {
2255− bind.iter()
2256− .map(|key| key.key_sequence_format())
2257− .collect::<String>()
2258− })
2259− .collect::<Vec<String>>()
2260− .join(", ")
2261− };
2262−
2263− let picker = Picker::new(
2264− commands,
2265− move |command| match command {
2266− MappableCommand::Typable { doc, name, .. } => match keymap.get(name) {
2267− Some(bindings) => format!("{} ({})", doc, fmt_binding(bindings)).into(),
2268− None => doc.as_str().into(),
2269− },
2270− MappableCommand::Static { doc, name, .. } => match keymap.get(*name) {
2271− Some(bindings) => format!("{} ({})", doc, fmt_binding(bindings)).into(),
2272− None => (*doc).into(),
2273− },
2274− },
2275− move |cx, command, _action| {
2276− let mut ctx = Context {
2277− register: None,
2278− count: std::num::NonZeroUsize::new(1),
2279− editor: cx.editor,
2280− callback: None,
2281− on_next_key_callback: None,
2282− jobs: cx.jobs,
2283− };
2284− command.execute(&mut ctx);
2285− },
2286− );
2311+ let picker = Picker::new(commands, keymap, move |cx, command, _action| {
2312+ let mut ctx = Context {
2313+ register: None,
2314+ count: std::num::NonZeroUsize::new(1),
2315+ editor: cx.editor,
2316+ callback: None,
2317+ on_next_key_callback: None,
2318+ jobs: cx.jobs,
2319+ };
2320+ command.execute(&mut ctx);
2321+ });
22872322 compositor.push(Box::new(overlayed(picker)));
22882323 },
22892324 ));
helix-term/src/commands/dap.rs+39−15
@@ -4,13 +4,15 @@ use crate::{
44 job::{Callback, Jobs},
55 ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent, Text},
66};
7−use helix_core::syntax::{DebugArgumentValue, DebugConfigCompletion};
7+use dap::{StackFrame, Thread, ThreadStates};
8+use helix_core::syntax::{DebugArgumentValue, DebugConfigCompletion, DebugTemplate};
89use helix_dap::{self as dap, Client};
910use helix_lsp::block_on;
1011use helix_view::editor::Breakpoint;
1112
1213use serde_json::{to_value, Value};
1314use tokio_stream::wrappers::UnboundedReceiverStream;
15+use tui::text::Spans;
1416
1517use std::collections::HashMap;
1618use std::future::Future;
@@ -20,6 +22,38 @@ use anyhow::{anyhow, bail};
2022
2123use helix_view::handlers::dap::{breakpoints_changed, jump_to_stack_frame, select_thread_id};
2224
25+impl ui::menu::Item for StackFrame {
26+ type Data = ();
27+
28+ fn label(&self, _data: &Self::Data) -> Spans {
29+ self.name.as_str().into() // TODO: include thread_states in the label
30+ }
31+}
32+
33+impl ui::menu::Item for DebugTemplate {
34+ type Data = ();
35+
36+ fn label(&self, _data: &Self::Data) -> Spans {
37+ self.name.as_str().into()
38+ }
39+}
40+
41+impl ui::menu::Item for Thread {
42+ type Data = ThreadStates;
43+
44+ fn label(&self, thread_states: &Self::Data) -> Spans {
45+ format!(
46+ "{} ({})",
47+ self.name,
48+ thread_states
49+ .get(&self.id)
50+ .map(|state| state.as_str())
51+ .unwrap_or("unknown")
52+ )
53+ .into()
54+ }
55+}
56+
2357fn thread_picker(
2458 cx: &mut Context,
2559 callback_fn: impl Fn(&mut Editor, &dap::Thread) + Send + 'static,
@@ -41,17 +75,7 @@ fn thread_picker(
4175 let thread_states = debugger.thread_states.clone();
4276 let picker = FilePicker::new(
4377 threads,
44− move |thread| {
45− format!(
46− "{} ({})",
47− thread.name,
48− thread_states
49− .get(&thread.id)
50− .map(|state| state.as_str())
51− .unwrap_or("unknown")
52− )
53− .into()
54− },
78+ thread_states,
5579 move |cx, thread, _action| callback_fn(cx.editor, thread),
5680 move |editor, thread| {
5781 let frames = editor.debugger.as_ref()?.stack_frames.get(&thread.id)?;
@@ -243,7 +267,7 @@ pub fn dap_launch(cx: &mut Context) {
243267
244268 cx.push_layer(Box::new(overlayed(Picker::new(
245269 templates,
246− |template| template.name.as_str().into(),
270+ (),
247271 |cx, template, _action| {
248272 let completions = template.completion.clone();
249273 let name = template.name.clone();
@@ -475,7 +499,7 @@ pub fn dap_variables(cx: &mut Context) {
475499
476500 for scope in scopes.iter() {
477501 // use helix_view::graphics::Style;
478− use tui::text::{Span, Spans};
502+ use tui::text::Span;
479503 let response = block_on(debugger.variables(scope.variables_reference));
480504
481505 variables.push(Spans::from(Span::styled(
@@ -652,7 +676,7 @@ pub fn dap_switch_stack_frame(cx: &mut Context) {
652676
653677 let picker = FilePicker::new(
654678 frames,
655− |frame| frame.name.as_str().into(), // TODO: include thread_states in the label
679+ (),
656680 move |cx, frame, _action| {
657681 let debugger = debugger!(cx.editor);
658682 // TODO: this should be simpler to find
helix-term/src/commands/lsp.rs+131−91
@@ -19,7 +19,8 @@ use crate::{
1919 ui::{self, overlay::overlayed, FileLocation, FilePicker, Popup, PromptEvent},
2020};
2121
22−use std::{borrow::Cow, collections::BTreeMap};
22+use std::collections::BTreeMap;
23+use std::{borrow::Cow, path::PathBuf};
2324
2425/// Gets the language server that is attached to a document, and
2526/// if it's not active displays a status message. Using this macro
@@ -39,6 +40,112 @@ macro_rules! language_server {
3940 };
4041}
4142
43+impl ui::menu::Item for lsp::Location {
44+ /// Current working directory.
45+ type Data = PathBuf;
46+
47+ fn label(&self, cwdir: &Self::Data) -> Spans {
48+ let file: Cow<'_, str> = (self.uri.scheme() == "file")
49+ .then(|| {
50+ self.uri
51+ .to_file_path()
52+ .map(|path| {
53+ // strip root prefix
54+ path.strip_prefix(&cwdir)
55+ .map(|path| path.to_path_buf())
56+ .unwrap_or(path)
57+ })
58+ .map(|path| Cow::from(path.to_string_lossy().into_owned()))
59+ .ok()
60+ })
61+ .flatten()
62+ .unwrap_or_else(|| self.uri.as_str().into());
63+ let line = self.range.start.line;
64+ format!("{}:{}", file, line).into()
65+ }
66+}
67+
68+impl ui::menu::Item for lsp::SymbolInformation {
69+ /// Path to currently focussed document
70+ type Data = Option<lsp::Url>;
71+
72+ fn label(&self, current_doc_path: &Self::Data) -> Spans {
73+ if current_doc_path.as_ref() == Some(&self.location.uri) {
74+ self.name.as_str().into()
75+ } else {
76+ match self.location.uri.to_file_path() {
77+ Ok(path) => {
78+ let relative_path = helix_core::path::get_relative_path(path.as_path())
79+ .to_string_lossy()
80+ .into_owned();
81+ format!("{} ({})", &self.name, relative_path).into()
82+ }
83+ Err(_) => format!("{} ({})", &self.name, &self.location.uri).into(),
84+ }
85+ }
86+ }
87+}
88+
89+struct DiagnosticStyles {
90+ hint: Style,
91+ info: Style,
92+ warning: Style,
93+ error: Style,
94+}
95+
96+struct PickerDiagnostic {
97+ url: lsp::Url,
98+ diag: lsp::Diagnostic,
99+}
100+
101+impl ui::menu::Item for PickerDiagnostic {
102+ type Data = DiagnosticStyles;
103+
104+ fn label(&self, styles: &Self::Data) -> Spans {
105+ let mut style = self
106+ .diag
107+ .severity
108+ .map(|s| match s {
109+ DiagnosticSeverity::HINT => styles.hint,
110+ DiagnosticSeverity::INFORMATION => styles.info,
111+ DiagnosticSeverity::WARNING => styles.warning,
112+ DiagnosticSeverity::ERROR => styles.error,
113+ _ => Style::default(),
114+ })
115+ .unwrap_or_default();
116+
117+ // remove background as it is distracting in the picker list
118+ style.bg = None;
119+
120+ let code = self
121+ .diag
122+ .code
123+ .as_ref()
124+ .map(|c| match c {
125+ NumberOrString::Number(n) => n.to_string(),
126+ NumberOrString::String(s) => s.to_string(),
127+ })
128+ .unwrap_or_default();
129+
130+ let truncated_path = path::get_truncated_path(self.url.path())
131+ .to_string_lossy()
132+ .into_owned();
133+
134+ Spans::from(vec![
135+ Span::styled(
136+ self.diag.source.clone().unwrap_or_default(),
137+ style.add_modifier(Modifier::BOLD),
138+ ),
139+ Span::raw(": "),
140+ Span::styled(truncated_path, style),
141+ Span::raw(" - "),
142+ Span::styled(code, style.add_modifier(Modifier::BOLD)),
143+ Span::raw(": "),
144+ Span::styled(&self.diag.message, style),
145+ ])
146+ }
147+}
148+
42149fn location_to_file_location(location: &lsp::Location) -> FileLocation {
43150 let path = location.uri.to_file_path().unwrap();
44151 let line = Some((
@@ -93,29 +200,14 @@ fn sym_picker(
93200 offset_encoding: OffsetEncoding,
94201) -> FilePicker<lsp::SymbolInformation> {
95202 // TODO: drop current_path comparison and instead use workspace: bool flag?
96− let current_path2 = current_path.clone();
97203 FilePicker::new(
98204 symbols,
99− move |symbol| {
100− if current_path.as_ref() == Some(&symbol.location.uri) {
101− symbol.name.as_str().into()
102− } else {
103− match symbol.location.uri.to_file_path() {
104− Ok(path) => {
105− let relative_path = helix_core::path::get_relative_path(path.as_path())
106− .to_string_lossy()
107− .into_owned();
108− format!("{} ({})", &symbol.name, relative_path).into()
109− }
110− Err(_) => format!("{} ({})", &symbol.name, &symbol.location.uri).into(),
111− }
112− }
113− },
205+ current_path.clone(),
114206 move |cx, symbol, action| {
115207 let (view, doc) = current!(cx.editor);
116208 push_jump(view, doc);
117209
118− if current_path2.as_ref() != Some(&symbol.location.uri) {
210+ if current_path.as_ref() != Some(&symbol.location.uri) {
119211 let uri = &symbol.location.uri;
120212 let path = match uri.to_file_path() {
121213 Ok(path) => path,
@@ -155,7 +247,7 @@ fn diag_picker(
155247 diagnostics: BTreeMap<lsp::Url, Vec<lsp::Diagnostic>>,
156248 current_path: Option<lsp::Url>,
157249 offset_encoding: OffsetEncoding,
158−) -> FilePicker<(lsp::Url, lsp::Diagnostic)> {
250+) -> FilePicker<PickerDiagnostic> {
159251 // TODO: drop current_path comparison and instead use workspace: bool flag?
160252
161253 // flatten the map to a vec of (url, diag) pairs
@@ -163,59 +255,24 @@ fn diag_picker(
163255 for (url, diags) in diagnostics {
164256 flat_diag.reserve(diags.len());
165257 for diag in diags {
166− flat_diag.push((url.clone(), diag));
258+ flat_diag.push(PickerDiagnostic {
259+ url: url.clone(),
260+ diag,
261+ });
167262 }
168263 }
169264
170− let hint = cx.editor.theme.get("hint");
171− let info = cx.editor.theme.get("info");
172− let warning = cx.editor.theme.get("warning");
173− let error = cx.editor.theme.get("error");
265+ let styles = DiagnosticStyles {
266+ hint: cx.editor.theme.get("hint"),
267+ info: cx.editor.theme.get("info"),
268+ warning: cx.editor.theme.get("warning"),
269+ error: cx.editor.theme.get("error"),
270+ };
174271
175272 FilePicker::new(
176273 flat_diag,
177− move |(url, diag)| {
178− let mut style = diag
179− .severity
180− .map(|s| match s {
181− DiagnosticSeverity::HINT => hint,
182− DiagnosticSeverity::INFORMATION => info,
183− DiagnosticSeverity::WARNING => warning,
184− DiagnosticSeverity::ERROR => error,
185− _ => Style::default(),
186− })
187− .unwrap_or_default();
188−
189− // remove background as it is distracting in the picker list
190− style.bg = None;
191−
192− let code = diag
193− .code
194− .as_ref()
195− .map(|c| match c {
196− NumberOrString::Number(n) => n.to_string(),
197− NumberOrString::String(s) => s.to_string(),
198− })
199− .unwrap_or_default();
200−
201− let truncated_path = path::get_truncated_path(url.path())
202− .to_string_lossy()
203− .into_owned();
204−
205− Spans::from(vec![
206− Span::styled(
207− diag.source.clone().unwrap_or_default(),
208− style.add_modifier(Modifier::BOLD),
209− ),
210− Span::raw(": "),
211− Span::styled(truncated_path, style),
212− Span::raw(" - "),
213− Span::styled(code, style.add_modifier(Modifier::BOLD)),
214− Span::raw(": "),
215− Span::styled(&diag.message, style),
216− ])
217− },
218− move |cx, (url, diag), action| {
274+ styles,
275+ move |cx, PickerDiagnostic { url, diag }, action| {
219276 if current_path.as_ref() == Some(url) {
220277 let (view, doc) = current!(cx.editor);
221278 push_jump(view, doc);
@@ -233,7 +290,7 @@ fn diag_picker(
233290 align_view(doc, view, Align::Center);
234291 }
235292 },
236− move |_editor, (url, diag)| {
293+ move |_editor, PickerDiagnostic { url, diag }| {
237294 let location = lsp::Location::new(url.clone(), diag.range);
238295 Some(location_to_file_location(&location))
239296 },
@@ -343,10 +400,11 @@ pub fn workspace_diagnostics_picker(cx: &mut Context) {
343400}
344401
345402impl ui::menu::Item for lsp::CodeActionOrCommand {
346− fn label(&self) -> &str {
403+ type Data = ();
404+ fn label(&self, _data: &Self::Data) -> Spans {
347405 match self {
348− lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str(),
349− lsp::CodeActionOrCommand::Command(command) => command.title.as_str(),
406+ lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str().into(),
407+ lsp::CodeActionOrCommand::Command(command) => command.title.as_str().into(),
350408 }
351409 }
352410}
@@ -391,7 +449,7 @@ pub fn code_action(cx: &mut Context) {
391449 return;
392450 }
393451
394− let mut picker = ui::Menu::new(actions, move |editor, code_action, event| {
452+ let mut picker = ui::Menu::new(actions, (), move |editor, code_action, event| {
395453 if event != PromptEvent::Validate {
396454 return;
397455 }
@@ -619,6 +677,7 @@ pub fn apply_workspace_edit(
619677 }
620678 }
621679}
680+
622681fn goto_impl(
623682 editor: &mut Editor,
624683 compositor: &mut Compositor,
@@ -637,26 +696,7 @@ fn goto_impl(
637696 _locations => {
638697 let picker = FilePicker::new(
639698 locations,
640− move |location| {
641− let file: Cow<'_, str> = (location.uri.scheme() == "file")
642− .then(|| {
643− location
644− .uri
645− .to_file_path()
646− .map(|path| {
647− // strip root prefix
648− path.strip_prefix(&cwdir)
649− .map(|path| path.to_path_buf())
650− .unwrap_or(path)
651− })
652− .map(|path| Cow::from(path.to_string_lossy().into_owned()))
653− .ok()
654− })
655− .flatten()
656− .unwrap_or_else(|| location.uri.as_str().into());
657− let line = location.range.start.line;
658− format!("{}:{}", file, line).into()
659− },
699+ cwdir,
660700 move |cx, location, action| {
661701 jump_to_location(cx.editor, location, offset_encoding, action)
662702 },
helix-term/src/keymap.rs+5−6
@@ -208,18 +208,17 @@ pub struct Keymap {
208208 root: KeyTrie,
209209}
210210
211+/// A map of command names to keybinds that will execute the command.
212+pub type ReverseKeymap = HashMap<String, Vec<Vec<KeyEvent>>>;
213+
211214impl Keymap {
212215 pub fn new(root: KeyTrie) -> Self {
213216 Keymap { root }
214217 }
215218
216− pub fn reverse_map(&self) -> HashMap<String, Vec<Vec<KeyEvent>>> {
219+ pub fn reverse_map(&self) -> ReverseKeymap {
217220 // recursively visit all nodes in keymap
218− fn map_node(
219− cmd_map: &mut HashMap<String, Vec<Vec<KeyEvent>>>,
220− node: &KeyTrie,
221− keys: &mut Vec<KeyEvent>,
222− ) {
221+ fn map_node(cmd_map: &mut ReverseKeymap, node: &KeyTrie, keys: &mut Vec<KeyEvent>) {
223222 match node {
224223 KeyTrie::Leaf(cmd) => match cmd {
225224 MappableCommand::Typable { name, .. } => {
helix-term/src/ui/completion.rs+15−8
@@ -2,6 +2,7 @@ use crate::compositor::{Component, Context, EventResult};
22use crossterm::event::{Event, KeyCode, KeyEvent};
33use helix_view::editor::CompleteAction;
44use tui::buffer::Buffer as Surface;
5+use tui::text::Spans;
56
67use std::borrow::Cow;
78
@@ -15,19 +16,25 @@ use helix_lsp::{lsp, util};
1516use lsp::CompletionItem;
1617
1718impl menu::Item for CompletionItem {
18− fn sort_text(&self) -> &str {
19− self.filter_text.as_ref().unwrap_or(&self.label).as_str()
19+ type Data = ();
20+ fn sort_text(&self, data: &Self::Data) -> Cow<str> {
21+ self.filter_text(data)
2022 }
2123
22− fn filter_text(&self) -> &str {
23− self.filter_text.as_ref().unwrap_or(&self.label).as_str()
24+ #[inline]
25+ fn filter_text(&self, _data: &Self::Data) -> Cow<str> {
26+ self.filter_text
27+ .as_ref()
28+ .unwrap_or(&self.label)
29+ .as_str()
30+ .into()
2431 }
2532
26− fn label(&self) -> &str {
27− self.label.as_str()
33+ fn label(&self, _data: &Self::Data) -> Spans {
34+ self.label.as_str().into()
2835 }
2936
30− fn row(&self) -> menu::Row {
37+ fn row(&self, _data: &Self::Data) -> menu::Row {
3138 menu::Row::new(vec![
3239 menu::Cell::from(self.label.as_str()),
3340 menu::Cell::from(match self.kind {
@@ -85,7 +92,7 @@ impl Completion {
8592 start_offset: usize,
8693 trigger_offset: usize,
8794 ) -> Self {
88− let menu = Menu::new(items, move |editor: &mut Editor, item, event| {
95+ let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| {
8996 fn item_to_transaction(
9097 doc: &Document,
9198 item: &CompletionItem,
helix-term/src/ui/menu.rs+39−15
@@ -1,9 +1,11 @@
1+use std::{borrow::Cow, path::PathBuf};
2+
13use crate::{
24 compositor::{Callback, Component, Compositor, Context, EventResult},
35 ctrl, key, shift,
46};
57use crossterm::event::Event;
6−use tui::{buffer::Buffer as Surface, widgets::Table};
8+use tui::{buffer::Buffer as Surface, text::Spans, widgets::Table};
79
810pub use tui::widgets::{Cell, Row};
911
@@ -14,22 +16,41 @@ use helix_view::{graphics::Rect, Editor};
1416use tui::layout::Constraint;
1517
1618pub trait Item {
17− fn label(&self) -> &str;
19+ /// Additional editor state that is used for label calculation.
20+ type Data;
21+
22+ fn label(&self, data: &Self::Data) -> Spans;
23+
24+ fn sort_text(&self, data: &Self::Data) -> Cow<str> {
25+ let label: String = self.label(data).into();
26+ label.into()
27+ }
1828
19− fn sort_text(&self) -> &str {
20− self.label()
29+ fn filter_text(&self, data: &Self::Data) -> Cow<str> {
30+ let label: String = self.label(data).into();
31+ label.into()
2132 }
22− fn filter_text(&self) -> &str {
23− self.label()
33+
34+ fn row(&self, data: &Self::Data) -> Row {
35+ Row::new(vec![Cell::from(self.label(data))])
2436 }
37+}
2538
26− fn row(&self) -> Row {
27− Row::new(vec![Cell::from(self.label())])
39+impl Item for PathBuf {
40+ /// Root prefix to strip.
41+ type Data = PathBuf;
42+
43+ fn label(&self, root_path: &Self::Data) -> Spans {
44+ self.strip_prefix(&root_path)
45+ .unwrap_or(self)
46+ .to_string_lossy()
47+ .into()
2848 }
2949}
3050
3151pub struct Menu<T: Item> {
3252 options: Vec<T>,
53+ editor_data: T::Data,
3354
3455 cursor: Option<usize>,
3556
@@ -54,10 +75,12 @@ impl<T: Item> Menu<T> {
5475 // rendering)
5576 pub fn new(
5677 options: Vec<T>,
78+ editor_data: <T as Item>::Data,
5779 callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static,
5880 ) -> Self {
5981 let mut menu = Self {
6082 options,
83+ editor_data,
6184 matcher: Box::new(Matcher::default()),
6285 matches: Vec::new(),
6386 cursor: None,
@@ -83,16 +106,17 @@ impl<T: Item> Menu<T> {
83106 .iter()
84107 .enumerate()
85108 .filter_map(|(index, option)| {
86− let text = option.filter_text();
109+ let text: String = option.filter_text(&self.editor_data).into();
87110 // TODO: using fuzzy_indices could give us the char idx for match highlighting
88111 self.matcher
89− .fuzzy_match(text, pattern)
112+ .fuzzy_match(&text, pattern)
90113 .map(|score| (index, score))
91114 }),
92115 );
93116 // matches.sort_unstable_by_key(|(_, score)| -score);
94− self.matches
95− .sort_unstable_by_key(|(index, _score)| self.options[*index].sort_text());
117+ self.matches.sort_unstable_by_key(|(index, _score)| {
118+ self.options[*index].sort_text(&self.editor_data)
119+ });
96120
97121 // reset cursor position
98122 self.cursor = None;
@@ -127,10 +151,10 @@ impl<T: Item> Menu<T> {
127151 let n = self
128152 .options
129153 .first()
130− .map(|option| option.row().cells.len())
154+ .map(|option| option.row(&self.editor_data).cells.len())
131155 .unwrap_or_default();
132156 let max_lens = self.options.iter().fold(vec![0; n], |mut acc, option| {
133− let row = option.row();
157+ let row = option.row(&self.editor_data);
134158 // maintain max for each column
135159 for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) {
136160 let width = cell.content.width();
@@ -300,7 +324,7 @@ impl<T: Item + 'static> Component for Menu<T> {
300324 let scroll_line = (win_height - scroll_height) * scroll
301325 / std::cmp::max(1, len.saturating_sub(win_height));
302326
303− let rows = options.iter().map(|option| option.row());
327+ let rows = options.iter().map(|option| option.row(&self.editor_data));
304328 let table = Table::new(rows)
305329 .style(style)
306330 .highlight_style(selected)
helix-term/src/ui/mod.rs+1−6
@@ -23,8 +23,6 @@ pub use text::Text;
2323use helix_core::regex::Regex;
2424use helix_core::regex::RegexBuilder;
2525use helix_view::{Document, Editor, View};
26−use tui;
27−use tui::text::Spans;
2826
2927use std::path::PathBuf;
3028
@@ -172,10 +170,7 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi
172170
173171 FilePicker::new(
174172 files,
175− move |path: &PathBuf| {
176− // format_fn
177− Spans::from(path.strip_prefix(&root).unwrap_or(path).to_string_lossy())
178− },
173+ root,
179174 move |cx, path: &PathBuf, action| {
180175 if let Err(e) = cx.editor.open(path, action) {
181176 let err = if let Some(err) = e.source() {
helix-term/src/ui/picker.rs+19−19
@@ -6,7 +6,6 @@ use crate::{
66use crossterm::event::Event;
77use tui::{
88 buffer::Buffer as Surface,
9− text::Spans,
109 widgets::{Block, BorderType, Borders},
1110};
1211
@@ -30,6 +29,8 @@ use helix_view::{
3029 Document, Editor,
3130};
3231
32+use super::menu::Item;
33+
3334pub const MIN_AREA_WIDTH_FOR_PREVIEW: u16 = 72;
3435/// Biggest file size to preview in bytes
3536pub 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;
3738/// File path and range of lines (used to align and highlight lines)
3839pub type FileLocation = (PathBuf, Option<(usize, usize)>);
3940
40−pub struct FilePicker<T> {
41+pub struct FilePicker<T: Item> {
4142 picker: Picker<T>,
4243 pub truncate_start: bool,
4344 /// Caches paths to documents
@@ -84,15 +85,15 @@ impl Preview<'_, '_> {
8485 }
8586}
8687
87−impl<T> FilePicker<T> {
88+impl<T: Item> FilePicker<T> {
8889 pub fn new(
8990 options: Vec<T>,
90− format_fn: impl Fn(&T) -> Spans + 'static,
91+ editor_data: T::Data,
9192 callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
9293 preview_fn: impl Fn(&Editor, &T) -> Option<FileLocation> + 'static,
9394 ) -> Self {
9495 let truncate_start = true;
95− let mut picker = Picker::new(options, format_fn, callback_fn);
96+ let mut picker = Picker::new(options, editor_data, callback_fn);
9697 picker.truncate_start = truncate_start;
9798
9899 Self {
@@ -163,7 +164,7 @@ impl<T> FilePicker<T> {
163164 }
164165}
165166
166−impl<T: 'static> Component for FilePicker<T> {
167+impl<T: Item + 'static> Component for FilePicker<T> {
167168 fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
168169 // +---------+ +---------+
169170 // |prompt | |preview |
@@ -280,8 +281,9 @@ impl<T: 'static> Component for FilePicker<T> {
280281 }
281282}
282283
283−pub struct Picker<T> {
284+pub struct Picker<T: Item> {
284285 options: Vec<T>,
286+ editor_data: T::Data,
285287 // filter: String,
286288 matcher: Box<Matcher>,
287289 /// (index, score)
@@ -299,14 +301,13 @@ pub struct Picker<T> {
299301 /// Whether to truncate the start (default true)
300302 pub truncate_start: bool,
301303
302− format_fn: Box<dyn Fn(&T) -> Spans>,
303304 callback_fn: Box<dyn Fn(&mut Context, &T, Action)>,
304305}
305306
306−impl<T> Picker<T> {
307+impl<T: Item> Picker<T> {
307308 pub fn new(
308309 options: Vec<T>,
309− format_fn: impl Fn(&T) -> Spans + 'static,
310+ editor_data: T::Data,
310311 callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
311312 ) -> Self {
312313 let prompt = Prompt::new(
@@ -318,6 +319,7 @@ impl<T> Picker<T> {
318319
319320 let mut picker = Self {
320321 options,
322+ editor_data,
321323 matcher: Box::new(Matcher::default()),
322324 matches: Vec::new(),
323325 filters: Vec::new(),
@@ -325,7 +327,6 @@ impl<T> Picker<T> {
325327 prompt,
326328 previous_pattern: String::new(),
327329 truncate_start: true,
328− format_fn: Box::new(format_fn),
329330 callback_fn: Box::new(callback_fn),
330331 completion_height: 0,
331332 };
@@ -371,9 +372,9 @@ impl<T> Picker<T> {
371372 #[allow(unstable_name_collisions)]
372373 self.matches.retain_mut(|(index, score)| {
373374 let option = &self.options[*index];
374− // TODO: maybe using format_fn isn't the best idea here
375− let line: String = (self.format_fn)(option).into();
376− match self.matcher.fuzzy_match(&line, pattern) {
375+ let text = option.sort_text(&self.editor_data);
376+
377+ match self.matcher.fuzzy_match(&text, pattern) {
377378 Some(s) => {
378379 // Update the score
379380 *score = s;
@@ -399,11 +400,10 @@ impl<T> Picker<T> {
399400 self.filters.binary_search(&index).ok()?;
400401 }
401402
402− // TODO: maybe using format_fn isn't the best idea here
403− let line: String = (self.format_fn)(option).into();
403+ let text = option.filter_text(&self.editor_data);
404404
405405 self.matcher
406− .fuzzy_match(&line, pattern)
406+ .fuzzy_match(&text, pattern)
407407 .map(|score| (index, score))
408408 }),
409409 );
@@ -477,7 +477,7 @@ impl<T> Picker<T> {
477477// - on input change:
478478// - score all the names in relation to input
479479
480−impl<T: 'static> Component for Picker<T> {
480+impl<T: Item + 'static> Component for Picker<T> {
481481 fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
482482 self.completion_height = viewport.1.saturating_sub(4);
483483 Some(viewport)
@@ -610,7 +610,7 @@ impl<T: 'static> Component for Picker<T> {
610610 surface.set_string(inner.x.saturating_sub(2), inner.y + i as u16, ">", selected);
611611 }
612612
613− let spans = (self.format_fn)(option);
613+ let spans = option.label(&self.editor_data);
614614 let (_score, highlights) = self
615615 .matcher
616616 .fuzzy_indices(&String::from(&spans), self.prompt.line())
helix-tui/src/text.rs+6
@@ -402,6 +402,12 @@ impl<'a> From<&'a str> for Text<'a> {
402402 }
403403}
404404
405+impl<'a> From<Cow<'a, str>> for Text<'a> {
406+ fn from(s: Cow<'a, str>) -> Text<'a> {
407+ Text::raw(s)
408+ }
409+}
410+
405411impl<'a> From<Span<'a>> for Text<'a> {
406412 fn from(span: Span<'a>) -> Text<'a> {
407413 Text {