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
pull/1755/head
Gokul Soumya 2 years ago committed by GitHub
parent 290b3ebbbe
commit 6e2aaed5c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -34,7 +34,7 @@ pub struct Client {
pub caps: Option<DebuggerCapabilities>, pub caps: Option<DebuggerCapabilities>,
// thread_id -> frames // thread_id -> frames
pub stack_frames: HashMap<ThreadId, Vec<StackFrame>>, pub stack_frames: HashMap<ThreadId, Vec<StackFrame>>,
pub thread_states: HashMap<ThreadId, String>, pub thread_states: ThreadStates,
pub thread_id: Option<ThreadId>, pub thread_id: Option<ThreadId>,
/// Currently active frame for the current thread. /// Currently active frame for the current thread.
pub active_frame: Option<usize>, pub active_frame: Option<usize>,

@ -14,6 +14,8 @@ impl std::fmt::Display for ThreadId {
} }
} }
pub type ThreadStates = HashMap<ThreadId, String>;
pub trait Request { pub trait Request {
type Arguments: serde::de::DeserializeOwned + serde::Serialize; type Arguments: serde::de::DeserializeOwned + serde::Serialize;
type Result: serde::de::DeserializeOwned + serde::Serialize; type Result: serde::de::DeserializeOwned + serde::Serialize;

@ -45,6 +45,7 @@ use movement::Movement;
use crate::{ use crate::{
args, args,
compositor::{self, Component, Compositor}, compositor::{self, Component, Compositor},
keymap::ReverseKeymap,
ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent}, 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) { fn global_search(cx: &mut Context) {
let (all_matches_sx, all_matches_rx) = #[derive(Debug)]
tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>(); 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<PathBuf>;
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::<FileResult>();
let config = cx.editor.config(); let config = cx.editor.config();
let smart_case = config.search.smart_case; let smart_case = config.search.smart_case;
let file_picker_config = config.file_picker.clone(); let file_picker_config = config.file_picker.clone();
@ -1809,7 +1844,7 @@ fn global_search(cx: &mut Context) {
entry.path(), entry.path(),
sinks::UTF8(|line_num, _| { sinks::UTF8(|line_num, _| {
all_matches_sx 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(); .unwrap();
Ok(true) Ok(true)
@ -1836,7 +1871,7 @@ fn global_search(cx: &mut Context) {
let current_path = doc_mut!(cx.editor).path().cloned(); let current_path = doc_mut!(cx.editor).path().cloned();
let show_picker = async move { let show_picker = async move {
let all_matches: Vec<(usize, PathBuf)> = let all_matches: Vec<FileResult> =
UnboundedReceiverStream::new(all_matches_rx).collect().await; UnboundedReceiverStream::new(all_matches_rx).collect().await;
let call: job::Callback = let call: job::Callback =
Box::new(move |editor: &mut Editor, compositor: &mut Compositor| { Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
@ -1847,17 +1882,8 @@ fn global_search(cx: &mut Context) {
let picker = FilePicker::new( let picker = FilePicker::new(
all_matches, all_matches,
move |(_line_num, path)| { current_path,
let relative_path = helix_core::path::get_relative_path(path) move |cx, FileResult { path, line_num }, action| {
.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| {
match cx.editor.open(path, action) { match cx.editor.open(path, action) {
Ok(_) => {} Ok(_) => {}
Err(e) => { Err(e) => {
@ -1879,7 +1905,9 @@ fn global_search(cx: &mut Context) {
doc.set_selection(view.id, Selection::single(start, end)); doc.set_selection(view.id, Selection::single(start, end));
align_view(doc, view, Align::Center); 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))); compositor.push(Box::new(overlayed(picker)));
}); });
@ -2172,8 +2200,10 @@ fn buffer_picker(cx: &mut Context) {
is_current: bool, is_current: bool,
} }
impl BufferMeta { impl ui::menu::Item for BufferMeta {
fn format(&self) -> Spans { type Data = ();
fn label(&self, _data: &Self::Data) -> Spans {
let path = self let path = self
.path .path
.as_deref() .as_deref()
@ -2213,7 +2243,7 @@ fn buffer_picker(cx: &mut Context) {
.iter() .iter()
.map(|(_, doc)| new_meta(doc)) .map(|(_, doc)| new_meta(doc))
.collect(), .collect(),
BufferMeta::format, (),
|cx, meta, action| { |cx, meta, action| {
cx.editor.switch(meta.id, action); cx.editor.switch(meta.id, action);
}, },
@ -2230,6 +2260,38 @@ fn buffer_picker(cx: &mut Context) {
cx.push_layer(Box::new(overlayed(picker))); 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<Vec<KeyEvent>>| -> String {
bindings
.iter()
.map(|bind| {
bind.iter()
.map(|key| key.to_string())
.collect::<Vec<String>>()
.join("+")
})
.collect::<Vec<String>>()
.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) { pub fn command_palette(cx: &mut Context) {
cx.callback = Some(Box::new( cx.callback = Some(Box::new(
move |compositor: &mut Compositor, cx: &mut compositor::Context| { 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, let picker = Picker::new(commands, keymap, move |cx, command, _action| {
// individual key presses are joined with `+` let mut ctx = Context {
let fmt_binding = |bindings: &Vec<Vec<KeyEvent>>| -> String { register: None,
bindings count: std::num::NonZeroUsize::new(1),
.iter() editor: cx.editor,
.map(|bind| { callback: None,
bind.iter() on_next_key_callback: None,
.map(|key| key.key_sequence_format()) jobs: cx.jobs,
.collect::<String>() };
}) command.execute(&mut ctx);
.collect::<Vec<String>>() });
.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);
},
);
compositor.push(Box::new(overlayed(picker))); compositor.push(Box::new(overlayed(picker)));
}, },
)); ));

@ -4,13 +4,15 @@ use crate::{
job::{Callback, Jobs}, job::{Callback, Jobs},
ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent, Text}, 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_dap::{self as dap, Client};
use helix_lsp::block_on; use helix_lsp::block_on;
use helix_view::editor::Breakpoint; use helix_view::editor::Breakpoint;
use serde_json::{to_value, Value}; use serde_json::{to_value, Value};
use tokio_stream::wrappers::UnboundedReceiverStream; use tokio_stream::wrappers::UnboundedReceiverStream;
use tui::text::Spans;
use std::collections::HashMap; use std::collections::HashMap;
use std::future::Future; 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}; 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( fn thread_picker(
cx: &mut Context, cx: &mut Context,
callback_fn: impl Fn(&mut Editor, &dap::Thread) + Send + 'static, 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 thread_states = debugger.thread_states.clone();
let picker = FilePicker::new( let picker = FilePicker::new(
threads, threads,
move |thread| { thread_states,
format!(
"{} ({})",
thread.name,
thread_states
.get(&thread.id)
.map(|state| state.as_str())
.unwrap_or("unknown")
)
.into()
},
move |cx, thread, _action| callback_fn(cx.editor, thread), move |cx, thread, _action| callback_fn(cx.editor, thread),
move |editor, thread| { move |editor, thread| {
let frames = editor.debugger.as_ref()?.stack_frames.get(&thread.id)?; 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( cx.push_layer(Box::new(overlayed(Picker::new(
templates, templates,
|template| template.name.as_str().into(), (),
|cx, template, _action| { |cx, template, _action| {
let completions = template.completion.clone(); let completions = template.completion.clone();
let name = template.name.clone(); let name = template.name.clone();
@ -475,7 +499,7 @@ pub fn dap_variables(cx: &mut Context) {
for scope in scopes.iter() { for scope in scopes.iter() {
// use helix_view::graphics::Style; // use helix_view::graphics::Style;
use tui::text::{Span, Spans}; use tui::text::Span;
let response = block_on(debugger.variables(scope.variables_reference)); let response = block_on(debugger.variables(scope.variables_reference));
variables.push(Spans::from(Span::styled( variables.push(Spans::from(Span::styled(
@ -652,7 +676,7 @@ pub fn dap_switch_stack_frame(cx: &mut Context) {
let picker = FilePicker::new( let picker = FilePicker::new(
frames, frames,
|frame| frame.name.as_str().into(), // TODO: include thread_states in the label (),
move |cx, frame, _action| { move |cx, frame, _action| {
let debugger = debugger!(cx.editor); let debugger = debugger!(cx.editor);
// TODO: this should be simpler to find // TODO: this should be simpler to find

@ -19,7 +19,8 @@ use crate::{
ui::{self, overlay::overlayed, FileLocation, FilePicker, Popup, PromptEvent}, 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 /// Gets the language server that is attached to a document, and
/// if it's not active displays a status message. Using this macro /// 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<lsp::Url>;
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 { fn location_to_file_location(location: &lsp::Location) -> FileLocation {
let path = location.uri.to_file_path().unwrap(); let path = location.uri.to_file_path().unwrap();
let line = Some(( let line = Some((
@ -93,29 +200,14 @@ fn sym_picker(
offset_encoding: OffsetEncoding, offset_encoding: OffsetEncoding,
) -> FilePicker<lsp::SymbolInformation> { ) -> FilePicker<lsp::SymbolInformation> {
// TODO: drop current_path comparison and instead use workspace: bool flag? // TODO: drop current_path comparison and instead use workspace: bool flag?
let current_path2 = current_path.clone();
FilePicker::new( FilePicker::new(
symbols, symbols,
move |symbol| { current_path.clone(),
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(),
}
}
},
move |cx, symbol, action| { move |cx, symbol, action| {
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
push_jump(view, doc); 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 uri = &symbol.location.uri;
let path = match uri.to_file_path() { let path = match uri.to_file_path() {
Ok(path) => path, Ok(path) => path,
@ -155,7 +247,7 @@ fn diag_picker(
diagnostics: BTreeMap<lsp::Url, Vec<lsp::Diagnostic>>, diagnostics: BTreeMap<lsp::Url, Vec<lsp::Diagnostic>>,
current_path: Option<lsp::Url>, current_path: Option<lsp::Url>,
offset_encoding: OffsetEncoding, offset_encoding: OffsetEncoding,
) -> FilePicker<(lsp::Url, lsp::Diagnostic)> { ) -> FilePicker<PickerDiagnostic> {
// TODO: drop current_path comparison and instead use workspace: bool flag? // TODO: drop current_path comparison and instead use workspace: bool flag?
// flatten the map to a vec of (url, diag) pairs // flatten the map to a vec of (url, diag) pairs
@ -163,59 +255,24 @@ fn diag_picker(
for (url, diags) in diagnostics { for (url, diags) in diagnostics {
flat_diag.reserve(diags.len()); flat_diag.reserve(diags.len());
for diag in diags { 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 styles = DiagnosticStyles {
let info = cx.editor.theme.get("info"); hint: cx.editor.theme.get("hint"),
let warning = cx.editor.theme.get("warning"); info: cx.editor.theme.get("info"),
let error = cx.editor.theme.get("error"); warning: cx.editor.theme.get("warning"),
error: cx.editor.theme.get("error"),
};
FilePicker::new( FilePicker::new(
flat_diag, flat_diag,
move |(url, diag)| { styles,
let mut style = diag move |cx, PickerDiagnostic { url, diag }, action| {
.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| {
if current_path.as_ref() == Some(url) { if current_path.as_ref() == Some(url) {
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
push_jump(view, doc); push_jump(view, doc);
@ -233,7 +290,7 @@ fn diag_picker(
align_view(doc, view, Align::Center); align_view(doc, view, Align::Center);
} }
}, },
move |_editor, (url, diag)| { move |_editor, PickerDiagnostic { url, diag }| {
let location = lsp::Location::new(url.clone(), diag.range); let location = lsp::Location::new(url.clone(), diag.range);
Some(location_to_file_location(&location)) 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 { impl ui::menu::Item for lsp::CodeActionOrCommand {
fn label(&self) -> &str { type Data = ();
fn label(&self, _data: &Self::Data) -> Spans {
match self { match self {
lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str(), lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str().into(),
lsp::CodeActionOrCommand::Command(command) => command.title.as_str(), lsp::CodeActionOrCommand::Command(command) => command.title.as_str().into(),
} }
} }
} }
@ -391,7 +449,7 @@ pub fn code_action(cx: &mut Context) {
return; 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 { if event != PromptEvent::Validate {
return; return;
} }
@ -619,6 +677,7 @@ pub fn apply_workspace_edit(
} }
} }
} }
fn goto_impl( fn goto_impl(
editor: &mut Editor, editor: &mut Editor,
compositor: &mut Compositor, compositor: &mut Compositor,
@ -637,26 +696,7 @@ fn goto_impl(
_locations => { _locations => {
let picker = FilePicker::new( let picker = FilePicker::new(
locations, locations,
move |location| { cwdir,
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()
},
move |cx, location, action| { move |cx, location, action| {
jump_to_location(cx.editor, location, offset_encoding, action) jump_to_location(cx.editor, location, offset_encoding, action)
}, },

@ -208,18 +208,17 @@ pub struct Keymap {
root: KeyTrie, root: KeyTrie,
} }
/// A map of command names to keybinds that will execute the command.
pub type ReverseKeymap = HashMap<String, Vec<Vec<KeyEvent>>>;
impl Keymap { impl Keymap {
pub fn new(root: KeyTrie) -> Self { pub fn new(root: KeyTrie) -> Self {
Keymap { root } Keymap { root }
} }
pub fn reverse_map(&self) -> HashMap<String, Vec<Vec<KeyEvent>>> { pub fn reverse_map(&self) -> ReverseKeymap {
// recursively visit all nodes in keymap // recursively visit all nodes in keymap
fn map_node( fn map_node(cmd_map: &mut ReverseKeymap, node: &KeyTrie, keys: &mut Vec<KeyEvent>) {
cmd_map: &mut HashMap<String, Vec<Vec<KeyEvent>>>,
node: &KeyTrie,
keys: &mut Vec<KeyEvent>,
) {
match node { match node {
KeyTrie::Leaf(cmd) => match cmd { KeyTrie::Leaf(cmd) => match cmd {
MappableCommand::Typable { name, .. } => { MappableCommand::Typable { name, .. } => {

@ -2,6 +2,7 @@ use crate::compositor::{Component, Context, EventResult};
use crossterm::event::{Event, KeyCode, KeyEvent}; use crossterm::event::{Event, KeyCode, KeyEvent};
use helix_view::editor::CompleteAction; use helix_view::editor::CompleteAction;
use tui::buffer::Buffer as Surface; use tui::buffer::Buffer as Surface;
use tui::text::Spans;
use std::borrow::Cow; use std::borrow::Cow;
@ -15,19 +16,25 @@ use helix_lsp::{lsp, util};
use lsp::CompletionItem; use lsp::CompletionItem;
impl menu::Item for CompletionItem { impl menu::Item for CompletionItem {
fn sort_text(&self) -> &str { type Data = ();
self.filter_text.as_ref().unwrap_or(&self.label).as_str() fn sort_text(&self, data: &Self::Data) -> Cow<str> {
self.filter_text(data)
} }
fn filter_text(&self) -> &str { #[inline]
self.filter_text.as_ref().unwrap_or(&self.label).as_str() fn filter_text(&self, _data: &Self::Data) -> Cow<str> {
self.filter_text
.as_ref()
.unwrap_or(&self.label)
.as_str()
.into()
} }
fn label(&self) -> &str { fn label(&self, _data: &Self::Data) -> Spans {
self.label.as_str() self.label.as_str().into()
} }
fn row(&self) -> menu::Row { fn row(&self, _data: &Self::Data) -> menu::Row {
menu::Row::new(vec![ menu::Row::new(vec![
menu::Cell::from(self.label.as_str()), menu::Cell::from(self.label.as_str()),
menu::Cell::from(match self.kind { menu::Cell::from(match self.kind {
@ -85,7 +92,7 @@ impl Completion {
start_offset: usize, start_offset: usize,
trigger_offset: usize, trigger_offset: usize,
) -> Self { ) -> 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( fn item_to_transaction(
doc: &Document, doc: &Document,
item: &CompletionItem, item: &CompletionItem,

@ -1,9 +1,11 @@
use std::{borrow::Cow, path::PathBuf};
use crate::{ use crate::{
compositor::{Callback, Component, Compositor, Context, EventResult}, compositor::{Callback, Component, Compositor, Context, EventResult},
ctrl, key, shift, ctrl, key, shift,
}; };
use crossterm::event::Event; 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}; pub use tui::widgets::{Cell, Row};
@ -14,22 +16,41 @@ use helix_view::{graphics::Rect, Editor};
use tui::layout::Constraint; use tui::layout::Constraint;
pub trait Item { pub trait Item {
fn label(&self) -> &str; /// Additional editor state that is used for label calculation.
type Data;
fn label(&self, data: &Self::Data) -> Spans;
fn sort_text(&self, data: &Self::Data) -> Cow<str> {
let label: String = self.label(data).into();
label.into()
}
fn sort_text(&self) -> &str { fn filter_text(&self, data: &Self::Data) -> Cow<str> {
self.label() let label: String = self.label(data).into();
label.into()
} }
fn filter_text(&self) -> &str {
self.label() fn row(&self, data: &Self::Data) -> Row {
Row::new(vec![Cell::from(self.label(data))])
} }
}
fn row(&self) -> Row { impl Item for PathBuf {
Row::new(vec![Cell::from(self.label())]) /// 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<T: Item> { pub struct Menu<T: Item> {
options: Vec<T>, options: Vec<T>,
editor_data: T::Data,
cursor: Option<usize>, cursor: Option<usize>,
@ -54,10 +75,12 @@ impl<T: Item> Menu<T> {
// rendering) // rendering)
pub fn new( pub fn new(
options: Vec<T>, options: Vec<T>,
editor_data: <T as Item>::Data,
callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static, callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static,
) -> Self { ) -> Self {
let mut menu = Self { let mut menu = Self {
options, options,
editor_data,
matcher: Box::new(Matcher::default()), matcher: Box::new(Matcher::default()),
matches: Vec::new(), matches: Vec::new(),
cursor: None, cursor: None,
@ -83,16 +106,17 @@ impl<T: Item> Menu<T> {
.iter() .iter()
.enumerate() .enumerate()
.filter_map(|(index, option)| { .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 // TODO: using fuzzy_indices could give us the char idx for match highlighting
self.matcher self.matcher
.fuzzy_match(text, pattern) .fuzzy_match(&text, pattern)
.map(|score| (index, score)) .map(|score| (index, score))
}), }),
); );
// matches.sort_unstable_by_key(|(_, score)| -score); // matches.sort_unstable_by_key(|(_, score)| -score);
self.matches self.matches.sort_unstable_by_key(|(index, _score)| {
.sort_unstable_by_key(|(index, _score)| self.options[*index].sort_text()); self.options[*index].sort_text(&self.editor_data)
});
// reset cursor position // reset cursor position
self.cursor = None; self.cursor = None;
@ -127,10 +151,10 @@ impl<T: Item> Menu<T> {
let n = self let n = self
.options .options
.first() .first()
.map(|option| option.row().cells.len()) .map(|option| option.row(&self.editor_data).cells.len())
.unwrap_or_default(); .unwrap_or_default();
let max_lens = self.options.iter().fold(vec![0; n], |mut acc, option| { 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 // maintain max for each column
for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) { for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) {
let width = cell.content.width(); let width = cell.content.width();
@ -300,7 +324,7 @@ impl<T: Item + 'static> Component for Menu<T> {
let scroll_line = (win_height - scroll_height) * scroll let scroll_line = (win_height - scroll_height) * scroll
/ std::cmp::max(1, len.saturating_sub(win_height)); / 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) let table = Table::new(rows)
.style(style) .style(style)
.highlight_style(selected) .highlight_style(selected)

@ -23,8 +23,6 @@ pub use text::Text;
use helix_core::regex::Regex; use helix_core::regex::Regex;
use helix_core::regex::RegexBuilder; use helix_core::regex::RegexBuilder;
use helix_view::{Document, Editor, View}; use helix_view::{Document, Editor, View};
use tui;
use tui::text::Spans;
use std::path::PathBuf; use std::path::PathBuf;
@ -172,10 +170,7 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi
FilePicker::new( FilePicker::new(
files, files,
move |path: &PathBuf| { root,
// format_fn
Spans::from(path.strip_prefix(&root).unwrap_or(path).to_string_lossy())
},
move |cx, path: &PathBuf, action| { move |cx, path: &PathBuf, action| {
if let Err(e) = cx.editor.open(path, action) { if let Err(e) = cx.editor.open(path, action) {
let err = if let Some(err) = e.source() { let err = if let Some(err) = e.source() {

@ -6,7 +6,6 @@ use crate::{
use crossterm::event::Event; use crossterm::event::Event;
use tui::{ use tui::{
buffer::Buffer as Surface, buffer::Buffer as Surface,
text::Spans,
widgets::{Block, BorderType, Borders}, widgets::{Block, BorderType, Borders},
}; };
@ -30,6 +29,8 @@ use helix_view::{
Document, Editor, Document, Editor,
}; };
use super::menu::Item;
pub const MIN_AREA_WIDTH_FOR_PREVIEW: u16 = 72; pub const MIN_AREA_WIDTH_FOR_PREVIEW: u16 = 72;
/// Biggest file size to preview in bytes /// Biggest file size to preview in bytes
pub const MAX_FILE_SIZE_FOR_PREVIEW: u64 = 10 * 1024 * 1024; 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) /// File path and range of lines (used to align and highlight lines)
pub type FileLocation = (PathBuf, Option<(usize, usize)>); pub type FileLocation = (PathBuf, Option<(usize, usize)>);
pub struct FilePicker<T> { pub struct FilePicker<T: Item> {
picker: Picker<T>, picker: Picker<T>,
pub truncate_start: bool, pub truncate_start: bool,
/// Caches paths to documents /// Caches paths to documents
@ -84,15 +85,15 @@ impl Preview<'_, '_> {
} }
} }
impl<T> FilePicker<T> { impl<T: Item> FilePicker<T> {
pub fn new( pub fn new(
options: Vec<T>, options: Vec<T>,
format_fn: impl Fn(&T) -> Spans + 'static, editor_data: T::Data,
callback_fn: impl Fn(&mut Context, &T, Action) + 'static, callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
preview_fn: impl Fn(&Editor, &T) -> Option<FileLocation> + 'static, preview_fn: impl Fn(&Editor, &T) -> Option<FileLocation> + 'static,
) -> Self { ) -> Self {
let truncate_start = true; 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; picker.truncate_start = truncate_start;
Self { Self {
@ -163,7 +164,7 @@ impl<T> FilePicker<T> {
} }
} }
impl<T: 'static> Component for FilePicker<T> { impl<T: Item + 'static> Component for FilePicker<T> {
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
// +---------+ +---------+ // +---------+ +---------+
// |prompt | |preview | // |prompt | |preview |
@ -280,8 +281,9 @@ impl<T: 'static> Component for FilePicker<T> {
} }
} }
pub struct Picker<T> { pub struct Picker<T: Item> {
options: Vec<T>, options: Vec<T>,
editor_data: T::Data,
// filter: String, // filter: String,
matcher: Box<Matcher>, matcher: Box<Matcher>,
/// (index, score) /// (index, score)
@ -299,14 +301,13 @@ pub struct Picker<T> {
/// Whether to truncate the start (default true) /// Whether to truncate the start (default true)
pub truncate_start: bool, pub truncate_start: bool,
format_fn: Box<dyn Fn(&T) -> Spans>,
callback_fn: Box<dyn Fn(&mut Context, &T, Action)>, callback_fn: Box<dyn Fn(&mut Context, &T, Action)>,
} }
impl<T> Picker<T> { impl<T: Item> Picker<T> {
pub fn new( pub fn new(
options: Vec<T>, options: Vec<T>,
format_fn: impl Fn(&T) -> Spans + 'static, editor_data: T::Data,
callback_fn: impl Fn(&mut Context, &T, Action) + 'static, callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
) -> Self { ) -> Self {
let prompt = Prompt::new( let prompt = Prompt::new(
@ -318,6 +319,7 @@ impl<T> Picker<T> {
let mut picker = Self { let mut picker = Self {
options, options,
editor_data,
matcher: Box::new(Matcher::default()), matcher: Box::new(Matcher::default()),
matches: Vec::new(), matches: Vec::new(),
filters: Vec::new(), filters: Vec::new(),
@ -325,7 +327,6 @@ impl<T> Picker<T> {
prompt, prompt,
previous_pattern: String::new(), previous_pattern: String::new(),
truncate_start: true, truncate_start: true,
format_fn: Box::new(format_fn),
callback_fn: Box::new(callback_fn), callback_fn: Box::new(callback_fn),
completion_height: 0, completion_height: 0,
}; };
@ -371,9 +372,9 @@ impl<T> Picker<T> {
#[allow(unstable_name_collisions)] #[allow(unstable_name_collisions)]
self.matches.retain_mut(|(index, score)| { self.matches.retain_mut(|(index, score)| {
let option = &self.options[*index]; let option = &self.options[*index];
// TODO: maybe using format_fn isn't the best idea here let text = option.sort_text(&self.editor_data);
let line: String = (self.format_fn)(option).into();
match self.matcher.fuzzy_match(&line, pattern) { match self.matcher.fuzzy_match(&text, pattern) {
Some(s) => { Some(s) => {
// Update the score // Update the score
*score = s; *score = s;
@ -399,11 +400,10 @@ impl<T> Picker<T> {
self.filters.binary_search(&index).ok()?; self.filters.binary_search(&index).ok()?;
} }
// TODO: maybe using format_fn isn't the best idea here let text = option.filter_text(&self.editor_data);
let line: String = (self.format_fn)(option).into();
self.matcher self.matcher
.fuzzy_match(&line, pattern) .fuzzy_match(&text, pattern)
.map(|score| (index, score)) .map(|score| (index, score))
}), }),
); );
@ -477,7 +477,7 @@ impl<T> Picker<T> {
// - on input change: // - on input change:
// - score all the names in relation to input // - score all the names in relation to input
impl<T: 'static> Component for Picker<T> { impl<T: Item + 'static> Component for Picker<T> {
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
self.completion_height = viewport.1.saturating_sub(4); self.completion_height = viewport.1.saturating_sub(4);
Some(viewport) Some(viewport)
@ -610,7 +610,7 @@ impl<T: 'static> Component for Picker<T> {
surface.set_string(inner.x.saturating_sub(2), inner.y + i as u16, ">", selected); 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 let (_score, highlights) = self
.matcher .matcher
.fuzzy_indices(&String::from(&spans), self.prompt.line()) .fuzzy_indices(&String::from(&spans), self.prompt.line())

@ -402,6 +402,12 @@ impl<'a> From<&'a str> for Text<'a> {
} }
} }
impl<'a> From<Cow<'a, str>> for Text<'a> {
fn from(s: Cow<'a, str>) -> Text<'a> {
Text::raw(s)
}
}
impl<'a> From<Span<'a>> for Text<'a> { impl<'a> From<Span<'a>> for Text<'a> {
fn from(span: Span<'a>) -> Text<'a> { fn from(span: Span<'a>) -> Text<'a> {
Text { Text {

Loading…
Cancel
Save