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