helix-term: implement buffer completer

In order to implement this completer, the completion function needs to
be able to access the compositor's context (to allow it to get the
list of buffers currently open in the context's editor).
pull/1676/head
Cole Helbling 3 years ago committed by Blaž Hrastnik
parent a1207fd768
commit 6118486eb2

@ -1435,7 +1435,7 @@ fn select_regex(cx: &mut Context) {
cx, cx,
"select:".into(), "select:".into(),
Some(reg), Some(reg),
|_input: &str| Vec::new(), |_ctx: &compositor::Context, _input: &str| Vec::new(),
move |view, doc, regex, event| { move |view, doc, regex, event| {
if event != PromptEvent::Update { if event != PromptEvent::Update {
return; return;
@ -1458,7 +1458,7 @@ fn split_selection(cx: &mut Context) {
cx, cx,
"split:".into(), "split:".into(),
Some(reg), Some(reg),
|_input: &str| Vec::new(), |_ctx: &compositor::Context, _input: &str| Vec::new(),
move |view, doc, regex, event| { move |view, doc, regex, event| {
if event != PromptEvent::Update { if event != PromptEvent::Update {
return; return;
@ -1600,7 +1600,7 @@ fn searcher(cx: &mut Context, direction: Direction) {
cx, cx,
"search:".into(), "search:".into(),
Some(reg), Some(reg),
move |input: &str| { move |_ctx: &compositor::Context, input: &str| {
completions completions
.iter() .iter()
.filter(|comp| comp.starts_with(input)) .filter(|comp| comp.starts_with(input))
@ -1701,7 +1701,7 @@ fn global_search(cx: &mut Context) {
cx, cx,
"global-search:".into(), "global-search:".into(),
None, None,
move |input: &str| { move |_ctx: &compositor::Context, input: &str| {
completions completions
.iter() .iter()
.filter(|comp| comp.starts_with(input)) .filter(|comp| comp.starts_with(input))
@ -2079,26 +2079,54 @@ pub mod cmd {
Ok(()) Ok(())
} }
fn buffer_close_impl(
editor: &mut Editor,
args: &[Cow<str>],
force: bool,
) -> anyhow::Result<()> {
if args.is_empty() {
let doc_id = view!(editor).doc;
editor.close_document(doc_id, force)?;
return Ok(());
}
for arg in args {
let doc_id = editor.documents().find_map(|doc| {
let arg_path = Some(Path::new(arg.as_ref()));
if doc.path().map(|p| p.as_path()) == arg_path
|| doc.relative_path().as_deref() == arg_path
{
Some(doc.id())
} else {
None
}
});
match doc_id {
Some(doc_id) => editor.close_document(doc_id, force)?,
None => {
editor.set_error(format!("couldn't close buffer '{}': does not exist", arg));
}
}
}
Ok(())
}
fn buffer_close( fn buffer_close(
cx: &mut compositor::Context, cx: &mut compositor::Context,
_args: &[Cow<str>], args: &[Cow<str>],
_event: PromptEvent, _event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let view = view!(cx.editor); buffer_close_impl(cx.editor, args, false)
let doc_id = view.doc;
cx.editor.close_document(doc_id, false)?;
Ok(())
} }
fn force_buffer_close( fn force_buffer_close(
cx: &mut compositor::Context, cx: &mut compositor::Context,
_args: &[Cow<str>], args: &[Cow<str>],
_event: PromptEvent, _event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let view = view!(cx.editor); buffer_close_impl(cx.editor, args, true)
let doc_id = view.doc;
cx.editor.close_document(doc_id, true)?;
Ok(())
} }
fn write_impl(cx: &mut compositor::Context, path: Option<&Cow<str>>) -> anyhow::Result<()> { fn write_impl(cx: &mut compositor::Context, path: Option<&Cow<str>>) -> anyhow::Result<()> {
@ -2927,14 +2955,14 @@ pub mod cmd {
aliases: &["bc", "bclose"], aliases: &["bc", "bclose"],
doc: "Close the current buffer.", doc: "Close the current buffer.",
fun: buffer_close, fun: buffer_close,
completer: None, // FIXME: buffer completer completer: Some(completers::buffer),
}, },
TypableCommand { TypableCommand {
name: "buffer-close!", name: "buffer-close!",
aliases: &["bc!", "bclose!"], aliases: &["bc!", "bclose!"],
doc: "Close the current buffer forcefully (ignoring unsaved changes).", doc: "Close the current buffer forcefully (ignoring unsaved changes).",
fun: force_buffer_close, fun: force_buffer_close,
completer: None, // FIXME: buffer completer completer: Some(completers::buffer),
}, },
TypableCommand { TypableCommand {
name: "write", name: "write",
@ -3262,7 +3290,7 @@ fn command_mode(cx: &mut Context) {
let mut prompt = Prompt::new( let mut prompt = Prompt::new(
":".into(), ":".into(),
Some(':'), Some(':'),
|input: &str| { |ctx: &compositor::Context, input: &str| {
static FUZZY_MATCHER: Lazy<fuzzy_matcher::skim::SkimMatcherV2> = static FUZZY_MATCHER: Lazy<fuzzy_matcher::skim::SkimMatcherV2> =
Lazy::new(fuzzy_matcher::skim::SkimMatcherV2::default); Lazy::new(fuzzy_matcher::skim::SkimMatcherV2::default);
@ -3294,7 +3322,7 @@ fn command_mode(cx: &mut Context) {
.. ..
}) = cmd::TYPABLE_COMMAND_MAP.get(parts[0]) }) = cmd::TYPABLE_COMMAND_MAP.get(parts[0])
{ {
completer(part) completer(ctx, part)
.into_iter() .into_iter()
.map(|(range, file)| { .map(|(range, file)| {
// offset ranges to input // offset ranges to input
@ -5358,7 +5386,7 @@ fn keep_or_remove_selections_impl(cx: &mut Context, remove: bool) {
cx, cx,
if !remove { "keep:" } else { "remove:" }.into(), if !remove { "keep:" } else { "remove:" }.into(),
Some(reg), Some(reg),
|_input: &str| Vec::new(), |_ctx: &compositor::Context, _input: &str| Vec::new(),
move |view, doc, regex, event| { move |view, doc, regex, event| {
if event != PromptEvent::Update { if event != PromptEvent::Update {
return; return;
@ -6122,7 +6150,7 @@ fn shell_keep_pipe(cx: &mut Context) {
let prompt = Prompt::new( let prompt = Prompt::new(
"keep-pipe:".into(), "keep-pipe:".into(),
Some('|'), Some('|'),
|_input: &str| Vec::new(), |_ctx: &compositor::Context, _input: &str| Vec::new(),
move |cx: &mut compositor::Context, input: &str, event: PromptEvent| { move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
let shell = &cx.editor.config.shell; let shell = &cx.editor.config.shell;
if event != PromptEvent::Validate { if event != PromptEvent::Validate {
@ -6218,7 +6246,7 @@ fn shell(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) {
let prompt = Prompt::new( let prompt = Prompt::new(
prompt, prompt,
Some('|'), Some('|'),
|_input: &str| Vec::new(), |_ctx: &compositor::Context, _input: &str| Vec::new(),
move |cx: &mut compositor::Context, input: &str, event: PromptEvent| { move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
let shell = &cx.editor.config.shell; let shell = &cx.editor.config.shell;
if event != PromptEvent::Validate { if event != PromptEvent::Validate {
@ -6314,7 +6342,7 @@ fn rename_symbol(cx: &mut Context) {
let prompt = Prompt::new( let prompt = Prompt::new(
"rename-to:".into(), "rename-to:".into(),
None, None,
|_input: &str| Vec::new(), |_ctx: &compositor::Context, _input: &str| Vec::new(),
move |cx: &mut compositor::Context, input: &str, event: PromptEvent| { move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
if event != PromptEvent::Validate { if event != PromptEvent::Validate {
return; return;

@ -361,8 +361,9 @@ fn debug_parameter_prompt(
let completer = match field_type { let completer = match field_type {
"filename" => ui::completers::filename, "filename" => ui::completers::filename,
"directory" => ui::completers::directory, "directory" => ui::completers::directory,
_ => |_input: &str| Vec::new(), _ => ui::completers::none,
}; };
Prompt::new( Prompt::new(
format!("{}: ", name).into(), format!("{}: ", name).into(),
None, None,
@ -696,7 +697,7 @@ pub fn dap_edit_condition(cx: &mut Context) {
let mut prompt = Prompt::new( let mut prompt = Prompt::new(
"condition:".into(), "condition:".into(),
None, None,
|_input: &str| Vec::new(), ui::completers::none,
move |cx, input: &str, event: PromptEvent| { move |cx, input: &str, event: PromptEvent| {
if event != PromptEvent::Validate { if event != PromptEvent::Validate {
return; return;
@ -740,7 +741,7 @@ pub fn dap_edit_log(cx: &mut Context) {
let mut prompt = Prompt::new( let mut prompt = Prompt::new(
"log-message:".into(), "log-message:".into(),
None, None,
|_input: &str| Vec::new(), ui::completers::none,
move |cx, input: &str, event: PromptEvent| { move |cx, input: &str, event: PromptEvent| {
if event != PromptEvent::Validate { if event != PromptEvent::Validate {
return; return;

@ -30,7 +30,7 @@ pub fn regex_prompt(
cx: &mut crate::commands::Context, cx: &mut crate::commands::Context,
prompt: std::borrow::Cow<'static, str>, prompt: std::borrow::Cow<'static, str>,
history_register: Option<char>, history_register: Option<char>,
completion_fn: impl FnMut(&str) -> Vec<prompt::Completion> + 'static, completion_fn: impl FnMut(&crate::compositor::Context, &str) -> Vec<prompt::Completion> + 'static,
fun: impl Fn(&mut View, &mut Document, Regex, PromptEvent) + 'static, fun: impl Fn(&mut View, &mut Document, Regex, PromptEvent) + 'static,
) -> Prompt { ) -> Prompt {
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
@ -168,18 +168,53 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi
} }
pub mod completers { pub mod completers {
use crate::compositor::Context;
use crate::ui::prompt::Completion; use crate::ui::prompt::Completion;
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher; use fuzzy_matcher::FuzzyMatcher;
use helix_view::document::SCRATCH_BUFFER_NAME;
use helix_view::editor::Config; use helix_view::editor::Config;
use helix_view::theme; use helix_view::theme;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::borrow::Cow; use std::borrow::Cow;
use std::cmp::Reverse; use std::cmp::Reverse;
pub type Completer = fn(&str) -> Vec<Completion>; pub type Completer = fn(&Context, &str) -> Vec<Completion>;
pub fn theme(input: &str) -> Vec<Completion> { pub fn none(_cx: &Context, _input: &str) -> Vec<Completion> {
Vec::new()
}
pub fn buffer(cx: &Context, input: &str) -> Vec<Completion> {
let mut names: Vec<_> = cx
.editor
.documents
.iter()
.map(|(_id, doc)| {
let name = doc
.relative_path()
.map(|p| p.display().to_string())
.unwrap_or_else(|| String::from(SCRATCH_BUFFER_NAME));
((0..), Cow::from(name))
})
.collect();
let matcher = Matcher::default();
let mut matches: Vec<_> = names
.into_iter()
.filter_map(|(_range, name)| {
matcher.fuzzy_match(&name, input).map(|score| (name, score))
})
.collect();
matches.sort_unstable_by_key(|(_file, score)| Reverse(*score));
names = matches.into_iter().map(|(name, _)| ((0..), name)).collect();
names
}
pub fn theme(_cx: &Context, input: &str) -> Vec<Completion> {
let mut names = theme::Loader::read_names(&helix_core::runtime_dir().join("themes")); let mut names = theme::Loader::read_names(&helix_core::runtime_dir().join("themes"));
names.extend(theme::Loader::read_names( names.extend(theme::Loader::read_names(
&helix_core::config_dir().join("themes"), &helix_core::config_dir().join("themes"),
@ -207,7 +242,7 @@ pub mod completers {
names names
} }
pub fn setting(input: &str) -> Vec<Completion> { pub fn setting(_cx: &Context, input: &str) -> Vec<Completion> {
static KEYS: Lazy<Vec<String>> = Lazy::new(|| { static KEYS: Lazy<Vec<String>> = Lazy::new(|| {
serde_json::to_value(Config::default()) serde_json::to_value(Config::default())
.unwrap() .unwrap()
@ -232,7 +267,7 @@ pub mod completers {
.collect() .collect()
} }
pub fn filename(input: &str) -> Vec<Completion> { pub fn filename(_cx: &Context, input: &str) -> Vec<Completion> {
filename_impl(input, |entry| { filename_impl(input, |entry| {
let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir()); let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir());
@ -244,7 +279,7 @@ pub mod completers {
}) })
} }
pub fn directory(input: &str) -> Vec<Completion> { pub fn directory(_cx: &Context, input: &str) -> Vec<Completion> {
filename_impl(input, |entry| { filename_impl(input, |entry| {
let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir()); let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir());

@ -302,7 +302,7 @@ impl<T> Picker<T> {
let prompt = Prompt::new( let prompt = Prompt::new(
"".into(), "".into(),
None, None,
|_pattern: &str| Vec::new(), |_ctx: &Context, _pattern: &str| Vec::new(),
|_editor: &mut Context, _pattern: &str, _event: PromptEvent| { |_editor: &mut Context, _pattern: &str, _event: PromptEvent| {
// //
}, },
@ -395,12 +395,12 @@ impl<T> Picker<T> {
.map(|(index, _score)| &self.options[*index]) .map(|(index, _score)| &self.options[*index])
} }
pub fn save_filter(&mut self) { pub fn save_filter(&mut self, cx: &Context) {
self.filters.clear(); self.filters.clear();
self.filters self.filters
.extend(self.matches.iter().map(|(index, _)| *index)); .extend(self.matches.iter().map(|(index, _)| *index));
self.filters.sort_unstable(); // used for binary search later self.filters.sort_unstable(); // used for binary search later
self.prompt.clear(); self.prompt.clear(cx);
} }
} }
@ -468,7 +468,7 @@ impl<T: 'static> Component for Picker<T> {
return close_fn; return close_fn;
} }
ctrl!(' ') => { ctrl!(' ') => {
self.save_filter(); self.save_filter(cx);
} }
_ => { _ => {
if let EventResult::Consumed(_) = self.prompt.handle_event(event, cx) { if let EventResult::Consumed(_) = self.prompt.handle_event(event, cx) {

@ -24,7 +24,7 @@ pub struct Prompt {
selection: Option<usize>, selection: Option<usize>,
history_register: Option<char>, history_register: Option<char>,
history_pos: Option<usize>, history_pos: Option<usize>,
completion_fn: Box<dyn FnMut(&str) -> Vec<Completion>>, completion_fn: Box<dyn FnMut(&Context, &str) -> Vec<Completion>>,
callback_fn: Box<dyn FnMut(&mut Context, &str, PromptEvent)>, callback_fn: Box<dyn FnMut(&mut Context, &str, PromptEvent)>,
pub doc_fn: Box<dyn Fn(&str) -> Option<&'static str>>, pub doc_fn: Box<dyn Fn(&str) -> Option<&'static str>>,
} }
@ -59,14 +59,14 @@ impl Prompt {
pub fn new( pub fn new(
prompt: Cow<'static, str>, prompt: Cow<'static, str>,
history_register: Option<char>, history_register: Option<char>,
mut completion_fn: impl FnMut(&str) -> Vec<Completion> + 'static, completion_fn: impl FnMut(&Context, &str) -> Vec<Completion> + 'static,
callback_fn: impl FnMut(&mut Context, &str, PromptEvent) + 'static, callback_fn: impl FnMut(&mut Context, &str, PromptEvent) + 'static,
) -> Self { ) -> Self {
Self { Self {
prompt, prompt,
line: String::new(), line: String::new(),
cursor: 0, cursor: 0,
completion: completion_fn(""), completion: Vec::new(),
selection: None, selection: None,
history_register, history_register,
history_pos: None, history_pos: None,
@ -177,13 +177,13 @@ impl Prompt {
} }
} }
pub fn insert_char(&mut self, c: char) { pub fn insert_char(&mut self, c: char, cx: &Context) {
self.line.insert(self.cursor, c); self.line.insert(self.cursor, c);
let mut cursor = GraphemeCursor::new(self.cursor, self.line.len(), false); let mut cursor = GraphemeCursor::new(self.cursor, self.line.len(), false);
if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) { if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) {
self.cursor = pos; self.cursor = pos;
} }
self.completion = (self.completion_fn)(&self.line); self.completion = (self.completion_fn)(cx, &self.line);
self.exit_selection(); self.exit_selection();
} }
@ -205,61 +205,61 @@ impl Prompt {
self.cursor = self.line.len(); self.cursor = self.line.len();
} }
pub fn delete_char_backwards(&mut self) { pub fn delete_char_backwards(&mut self, cx: &Context) {
let pos = self.eval_movement(Movement::BackwardChar(1)); let pos = self.eval_movement(Movement::BackwardChar(1));
self.line.replace_range(pos..self.cursor, ""); self.line.replace_range(pos..self.cursor, "");
self.cursor = pos; self.cursor = pos;
self.exit_selection(); self.exit_selection();
self.completion = (self.completion_fn)(&self.line); self.completion = (self.completion_fn)(cx, &self.line);
} }
pub fn delete_char_forwards(&mut self) { pub fn delete_char_forwards(&mut self, cx: &Context) {
let pos = self.eval_movement(Movement::ForwardChar(1)); let pos = self.eval_movement(Movement::ForwardChar(1));
self.line.replace_range(self.cursor..pos, ""); self.line.replace_range(self.cursor..pos, "");
self.exit_selection(); self.exit_selection();
self.completion = (self.completion_fn)(&self.line); self.completion = (self.completion_fn)(cx, &self.line);
} }
pub fn delete_word_backwards(&mut self) { pub fn delete_word_backwards(&mut self, cx: &Context) {
let pos = self.eval_movement(Movement::BackwardWord(1)); let pos = self.eval_movement(Movement::BackwardWord(1));
self.line.replace_range(pos..self.cursor, ""); self.line.replace_range(pos..self.cursor, "");
self.cursor = pos; self.cursor = pos;
self.exit_selection(); self.exit_selection();
self.completion = (self.completion_fn)(&self.line); self.completion = (self.completion_fn)(cx, &self.line);
} }
pub fn delete_word_forwards(&mut self) { pub fn delete_word_forwards(&mut self, cx: &Context) {
let pos = self.eval_movement(Movement::ForwardWord(1)); let pos = self.eval_movement(Movement::ForwardWord(1));
self.line.replace_range(self.cursor..pos, ""); self.line.replace_range(self.cursor..pos, "");
self.exit_selection(); self.exit_selection();
self.completion = (self.completion_fn)(&self.line); self.completion = (self.completion_fn)(cx, &self.line);
} }
pub fn kill_to_start_of_line(&mut self) { pub fn kill_to_start_of_line(&mut self, cx: &Context) {
let pos = self.eval_movement(Movement::StartOfLine); let pos = self.eval_movement(Movement::StartOfLine);
self.line.replace_range(pos..self.cursor, ""); self.line.replace_range(pos..self.cursor, "");
self.cursor = pos; self.cursor = pos;
self.exit_selection(); self.exit_selection();
self.completion = (self.completion_fn)(&self.line); self.completion = (self.completion_fn)(cx, &self.line);
} }
pub fn kill_to_end_of_line(&mut self) { pub fn kill_to_end_of_line(&mut self, cx: &Context) {
let pos = self.eval_movement(Movement::EndOfLine); let pos = self.eval_movement(Movement::EndOfLine);
self.line.replace_range(self.cursor..pos, ""); self.line.replace_range(self.cursor..pos, "");
self.exit_selection(); self.exit_selection();
self.completion = (self.completion_fn)(&self.line); self.completion = (self.completion_fn)(cx, &self.line);
} }
pub fn clear(&mut self) { pub fn clear(&mut self, cx: &Context) {
self.line.clear(); self.line.clear();
self.cursor = 0; self.cursor = 0;
self.completion = (self.completion_fn)(&self.line); self.completion = (self.completion_fn)(cx, &self.line);
self.exit_selection(); self.exit_selection();
} }
@ -442,16 +442,16 @@ impl Component for Prompt {
ctrl!('f') | key!(Right) => self.move_cursor(Movement::ForwardChar(1)), ctrl!('f') | key!(Right) => self.move_cursor(Movement::ForwardChar(1)),
ctrl!('e') | key!(End) => self.move_end(), ctrl!('e') | key!(End) => self.move_end(),
ctrl!('a') | key!(Home) => self.move_start(), ctrl!('a') | key!(Home) => self.move_start(),
ctrl!('w') => self.delete_word_backwards(), ctrl!('w') => self.delete_word_backwards(cx),
alt!('d') => self.delete_word_forwards(), alt!('d') => self.delete_word_forwards(cx),
ctrl!('k') => self.kill_to_end_of_line(), ctrl!('k') => self.kill_to_end_of_line(cx),
ctrl!('u') => self.kill_to_start_of_line(), ctrl!('u') => self.kill_to_start_of_line(cx),
ctrl!('h') | key!(Backspace) => { ctrl!('h') | key!(Backspace) => {
self.delete_char_backwards(); self.delete_char_backwards(cx);
(self.callback_fn)(cx, &self.line, PromptEvent::Update); (self.callback_fn)(cx, &self.line, PromptEvent::Update);
} }
ctrl!('d') | key!(Delete) => { ctrl!('d') | key!(Delete) => {
self.delete_char_forwards(); self.delete_char_forwards(cx);
(self.callback_fn)(cx, &self.line, PromptEvent::Update); (self.callback_fn)(cx, &self.line, PromptEvent::Update);
} }
ctrl!('s') => { ctrl!('s') => {
@ -474,7 +474,7 @@ impl Component for Prompt {
} }
key!(Enter) => { key!(Enter) => {
if self.selection.is_some() && self.line.ends_with(std::path::MAIN_SEPARATOR) { if self.selection.is_some() && self.line.ends_with(std::path::MAIN_SEPARATOR) {
self.completion = (self.completion_fn)(&self.line); self.completion = (self.completion_fn)(cx, &self.line);
self.exit_selection(); self.exit_selection();
} else { } else {
(self.callback_fn)(cx, &self.line, PromptEvent::Validate); (self.callback_fn)(cx, &self.line, PromptEvent::Validate);
@ -515,7 +515,7 @@ impl Component for Prompt {
code: KeyCode::Char(c), code: KeyCode::Char(c),
modifiers, modifiers,
} if !modifiers.contains(KeyModifiers::CONTROL) => { } if !modifiers.contains(KeyModifiers::CONTROL) => {
self.insert_char(c); self.insert_char(c, cx);
(self.callback_fn)(cx, &self.line, PromptEvent::Update); (self.callback_fn)(cx, &self.line, PromptEvent::Update);
} }
_ => (), _ => (),
@ -525,6 +525,7 @@ impl Component for Prompt {
} }
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
self.completion = (self.completion_fn)(cx, &self.line);
self.render_prompt(area, surface, cx) self.render_prompt(area, surface, cx)
} }

Loading…
Cancel
Save