From 09e89de0d269b60bc2710d39bfa253bedc8bbc42 Mon Sep 17 00:00:00 2001 From: Cole Helbling Date: Sat, 20 Nov 2021 13:03:39 -0800 Subject: [PATCH] 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). --- helix-term/src/commands.rs | 20 +++++++------- helix-term/src/ui/mod.rs | 41 ++++++++++++++++++++++++---- helix-term/src/ui/picker.rs | 8 +++--- helix-term/src/ui/prompt.rs | 54 ++++++++++++++++++------------------- 4 files changed, 77 insertions(+), 46 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 15119d0a4e776..a611927314080 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1201,7 +1201,7 @@ fn select_regex(cx: &mut Context) { cx, "select:".into(), Some(reg), - |_input: &str| Vec::new(), + |_input: &str, _ctx: &compositor::Context| Vec::new(), move |view, doc, regex, event| { if event != PromptEvent::Update { return; @@ -1224,7 +1224,7 @@ fn split_selection(cx: &mut Context) { cx, "split:".into(), Some(reg), - |_input: &str| Vec::new(), + |_input: &str, _ctx: &compositor::Context| Vec::new(), move |view, doc, regex, event| { if event != PromptEvent::Update { return; @@ -1358,7 +1358,7 @@ fn searcher(cx: &mut Context, direction: Direction) { cx, "search:".into(), Some(reg), - move |input: &str| { + move |input: &str, _ctx: &compositor::Context| { completions .iter() .filter(|comp| comp.starts_with(input)) @@ -1447,7 +1447,7 @@ fn global_search(cx: &mut Context) { cx, "global-search:".into(), None, - move |input: &str| { + move |input: &str, _ctx: &compositor::Context| { completions .iter() .filter(|comp| comp.starts_with(input)) @@ -2687,7 +2687,7 @@ fn command_mode(cx: &mut Context) { let mut prompt = Prompt::new( ":".into(), Some(':'), - |input: &str| { + |input: &str, ctx: &compositor::Context| { // we use .this over split_whitespace() because we care about empty segments let parts = input.split(' ').collect::>(); @@ -2708,7 +2708,7 @@ fn command_mode(cx: &mut Context) { .. }) = cmd::COMMANDS.get(parts[0]) { - completer(part) + completer(part, ctx) .into_iter() .map(|(range, file)| { // offset ranges to input @@ -4648,7 +4648,7 @@ fn keep_or_remove_selections_impl(cx: &mut Context, remove: bool) { cx, if !remove { "keep:" } else { "remove:" }.into(), Some(reg), - |_input: &str| Vec::new(), + |_input: &str, _ctx: &compositor::Context| Vec::new(), move |view, doc, regex, event| { if event != PromptEvent::Update { return; @@ -5301,7 +5301,7 @@ fn shell_keep_pipe(cx: &mut Context) { let prompt = Prompt::new( "keep-pipe:".into(), Some('|'), - |_input: &str| Vec::new(), + |_input: &str, _ctx: &compositor::Context| Vec::new(), move |cx: &mut compositor::Context, input: &str, event: PromptEvent| { let shell = &cx.editor.config.shell; if event != PromptEvent::Validate { @@ -5398,7 +5398,7 @@ fn shell(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) { let prompt = Prompt::new( prompt, Some('|'), - |_input: &str| Vec::new(), + |_input: &str, _ctx: &compositor::Context| Vec::new(), move |cx: &mut compositor::Context, input: &str, event: PromptEvent| { let shell = &cx.editor.config.shell; if event != PromptEvent::Validate { @@ -5496,7 +5496,7 @@ fn rename_symbol(cx: &mut Context) { let prompt = Prompt::new( "rename-to:".into(), None, - |_input: &str| Vec::new(), + |_input: &str, _ctx: &compositor::Context| Vec::new(), move |cx: &mut compositor::Context, input: &str, event: PromptEvent| { if event != PromptEvent::Validate { return; diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index cdf4231107047..3f6225635a6d6 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -29,7 +29,7 @@ pub fn regex_prompt( cx: &mut crate::commands::Context, prompt: std::borrow::Cow<'static, str>, history_register: Option, - completion_fn: impl FnMut(&str) -> Vec + 'static, + completion_fn: impl FnMut(&str, &crate::compositor::Context) -> Vec + 'static, fun: impl Fn(&mut View, &mut Document, Regex, PromptEvent) + 'static, ) -> Prompt { let (view, doc) = current!(cx.editor); @@ -171,16 +171,47 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi } pub mod completers { + use crate::compositor::Context; use crate::ui::prompt::Completion; use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; use fuzzy_matcher::FuzzyMatcher; + use helix_view::document::SCRATCH_BUFFER_NAME; use helix_view::theme; use std::borrow::Cow; use std::cmp::Reverse; - pub type Completer = fn(&str) -> Vec; + pub type Completer = fn(&str, &Context) -> Vec; + + pub fn buffer(input: &str, cx: &Context) -> Vec { + 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(input: &str) -> Vec { + pub fn theme(input: &str, _cx: &Context) -> Vec { let mut names = theme::Loader::read_names(&helix_core::runtime_dir().join("themes")); names.extend(theme::Loader::read_names( &helix_core::config_dir().join("themes"), @@ -207,7 +238,7 @@ pub mod completers { names } - pub fn filename(input: &str) -> Vec { + pub fn filename(input: &str, _cx: &Context) -> Vec { filename_impl(input, |entry| { let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir()); @@ -219,7 +250,7 @@ pub mod completers { }) } - pub fn directory(input: &str) -> Vec { + pub fn directory(input: &str, _cx: &Context) -> Vec { filename_impl(input, |entry| { let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir()); diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 6b1c58321ab74..8b7155d03dcf7 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -297,7 +297,7 @@ impl Picker { let prompt = Prompt::new( "".into(), None, - |_pattern: &str| Vec::new(), + |_pattern: &str, _ctx: &Context| Vec::new(), |_editor: &mut Context, _pattern: &str, _event: PromptEvent| { // }, @@ -374,12 +374,12 @@ impl Picker { .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 .extend(self.matches.iter().map(|(index, _)| *index)); self.filters.sort_unstable(); // used for binary search later - self.prompt.clear(); + self.prompt.clear(cx); } } @@ -438,7 +438,7 @@ impl Component for Picker { return close_fn; } ctrl!(' ') => { - self.save_filter(); + self.save_filter(cx); } _ => { if let EventResult::Consumed(_) = self.prompt.handle_event(event, cx) { diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index e90b0772785ea..11103e96ce2c3 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -24,7 +24,7 @@ pub struct Prompt { selection: Option, history_register: Option, history_pos: Option, - completion_fn: Box Vec>, + completion_fn: Box Vec>, callback_fn: Box, pub doc_fn: Box Option<&'static str>>, } @@ -59,14 +59,14 @@ impl Prompt { pub fn new( prompt: Cow<'static, str>, history_register: Option, - mut completion_fn: impl FnMut(&str) -> Vec + 'static, + completion_fn: impl FnMut(&str, &Context) -> Vec + 'static, callback_fn: impl FnMut(&mut Context, &str, PromptEvent) + 'static, ) -> Self { Self { prompt, line: String::new(), cursor: 0, - completion: completion_fn(""), + completion: Vec::new(), selection: None, history_register, 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); let mut cursor = GraphemeCursor::new(self.cursor, self.line.len(), false); if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) { self.cursor = pos; } - self.completion = (self.completion_fn)(&self.line); + self.completion = (self.completion_fn)(&self.line, cx); self.exit_selection(); } @@ -205,61 +205,61 @@ impl Prompt { 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)); self.line.replace_range(pos..self.cursor, ""); self.cursor = pos; self.exit_selection(); - self.completion = (self.completion_fn)(&self.line); + self.completion = (self.completion_fn)(&self.line, cx); } - pub fn delete_char_forwards(&mut self) { + pub fn delete_char_forwards(&mut self, cx: &Context) { let pos = self.eval_movement(Movement::ForwardChar(1)); self.line.replace_range(self.cursor..pos, ""); self.exit_selection(); - self.completion = (self.completion_fn)(&self.line); + self.completion = (self.completion_fn)(&self.line, cx); } - pub fn delete_word_backwards(&mut self) { + pub fn delete_word_backwards(&mut self, cx: &Context) { let pos = self.eval_movement(Movement::BackwardWord(1)); self.line.replace_range(pos..self.cursor, ""); self.cursor = pos; self.exit_selection(); - self.completion = (self.completion_fn)(&self.line); + self.completion = (self.completion_fn)(&self.line, cx); } - pub fn delete_word_forwards(&mut self) { + pub fn delete_word_forwards(&mut self, cx: &Context) { let pos = self.eval_movement(Movement::ForwardWord(1)); self.line.replace_range(self.cursor..pos, ""); self.exit_selection(); - self.completion = (self.completion_fn)(&self.line); + self.completion = (self.completion_fn)(&self.line, cx); } - 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); self.line.replace_range(pos..self.cursor, ""); self.cursor = pos; self.exit_selection(); - self.completion = (self.completion_fn)(&self.line); + self.completion = (self.completion_fn)(&self.line, cx); } - 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); self.line.replace_range(self.cursor..pos, ""); self.exit_selection(); - self.completion = (self.completion_fn)(&self.line); + self.completion = (self.completion_fn)(&self.line, cx); } - pub fn clear(&mut self) { + pub fn clear(&mut self, cx: &Context) { self.line.clear(); self.cursor = 0; - self.completion = (self.completion_fn)(&self.line); + self.completion = (self.completion_fn)(&self.line, cx); self.exit_selection(); } @@ -442,16 +442,16 @@ impl Component for Prompt { ctrl!('f') | key!(Right) => self.move_cursor(Movement::ForwardChar(1)), ctrl!('e') | key!(End) => self.move_end(), ctrl!('a') | key!(Home) => self.move_start(), - ctrl!('w') => self.delete_word_backwards(), - alt!('d') => self.delete_word_forwards(), - ctrl!('k') => self.kill_to_end_of_line(), - ctrl!('u') => self.kill_to_start_of_line(), + ctrl!('w') => self.delete_word_backwards(cx), + alt!('d') => self.delete_word_forwards(cx), + ctrl!('k') => self.kill_to_end_of_line(cx), + ctrl!('u') => self.kill_to_start_of_line(cx), ctrl!('h') | key!(Backspace) => { - self.delete_char_backwards(); + self.delete_char_backwards(cx); (self.callback_fn)(cx, &self.line, PromptEvent::Update); } ctrl!('d') | key!(Delete) => { - self.delete_char_forwards(); + self.delete_char_forwards(cx); (self.callback_fn)(cx, &self.line, PromptEvent::Update); } ctrl!('s') => { @@ -474,7 +474,7 @@ impl Component for Prompt { } key!(Enter) => { if self.selection.is_some() && self.line.ends_with('/') { - self.completion = (self.completion_fn)(&self.line); + self.completion = (self.completion_fn)(&self.line, cx); self.exit_selection(); } else { (self.callback_fn)(cx, &self.line, PromptEvent::Validate); @@ -515,7 +515,7 @@ impl Component for Prompt { code: KeyCode::Char(c), modifiers, } if !modifiers.contains(KeyModifiers::CONTROL) => { - self.insert_char(c); + self.insert_char(c, cx); (self.callback_fn)(cx, &self.line, PromptEvent::Update); } _ => (),