on master help sign in
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).

commit6118486eb2dd7ed680ff38a7cc024dbfb26fbb7f authorCole Helbling <cole.e.helbling@outlook.com> dateSat 20 Nov 2021, 13:03 (5 years ago) parenta1207fd
5 files changed+127−62
helix-term/src/commands.rs+50−22 helix-term/src/commands/dap.rs+4−3 helix-term/src/ui/mod.rs+41−6 helix-term/src/ui/picker.rs+4−4 helix-term/src/ui/prompt.rs+28−27
helix-term/src/commands.rs+50−22
@@ -1435,7 +1435,7 @@ fn select_regex(cx: &mut Context) {
14351435 cx,
14361436 "select:".into(),
14371437 Some(reg),
1438− |_input: &str| Vec::new(),
1438+ |_ctx: &compositor::Context, _input: &str| Vec::new(),
14391439 move |view, doc, regex, event| {
14401440 if event != PromptEvent::Update {
14411441 return;
@@ -1458,7 +1458,7 @@ fn split_selection(cx: &mut Context) {
14581458 cx,
14591459 "split:".into(),
14601460 Some(reg),
1461− |_input: &str| Vec::new(),
1461+ |_ctx: &compositor::Context, _input: &str| Vec::new(),
14621462 move |view, doc, regex, event| {
14631463 if event != PromptEvent::Update {
14641464 return;
@@ -1600,7 +1600,7 @@ fn searcher(cx: &mut Context, direction: Direction) {
16001600 cx,
16011601 "search:".into(),
16021602 Some(reg),
1603− move |input: &str| {
1603+ move |_ctx: &compositor::Context, input: &str| {
16041604 completions
16051605 .iter()
16061606 .filter(|comp| comp.starts_with(input))
@@ -1701,7 +1701,7 @@ fn global_search(cx: &mut Context) {
17011701 cx,
17021702 "global-search:".into(),
17031703 None,
1704− move |input: &str| {
1704+ move |_ctx: &compositor::Context, input: &str| {
17051705 completions
17061706 .iter()
17071707 .filter(|comp| comp.starts_with(input))
@@ -2079,26 +2079,54 @@ pub mod cmd {
20792079 Ok(())
20802080 }
20812081
2082+ fn buffer_close_impl(
2083+ editor: &mut Editor,
2084+ args: &[Cow<str>],
2085+ force: bool,
2086+ ) -> anyhow::Result<()> {
2087+ if args.is_empty() {
2088+ let doc_id = view!(editor).doc;
2089+ editor.close_document(doc_id, force)?;
2090+ return Ok(());
2091+ }
2092+
2093+ for arg in args {
2094+ let doc_id = editor.documents().find_map(|doc| {
2095+ let arg_path = Some(Path::new(arg.as_ref()));
2096+ if doc.path().map(|p| p.as_path()) == arg_path
2097+ || doc.relative_path().as_deref() == arg_path
2098+ {
2099+ Some(doc.id())
2100+ } else {
2101+ None
2102+ }
2103+ });
2104+
2105+ match doc_id {
2106+ Some(doc_id) => editor.close_document(doc_id, force)?,
2107+ None => {
2108+ editor.set_error(format!("couldn't close buffer '{}': does not exist", arg));
2109+ }
2110+ }
2111+ }
2112+
2113+ Ok(())
2114+ }
2115+
20822116 fn buffer_close(
20832117 cx: &mut compositor::Context,
2084− _args: &[Cow<str>],
2118+ args: &[Cow<str>],
20852119 _event: PromptEvent,
20862120 ) -> anyhow::Result<()> {
2087− let view = view!(cx.editor);
2088− let doc_id = view.doc;
2089− cx.editor.close_document(doc_id, false)?;
2090− Ok(())
2121+ buffer_close_impl(cx.editor, args, false)
20912122 }
20922123
20932124 fn force_buffer_close(
20942125 cx: &mut compositor::Context,
2095− _args: &[Cow<str>],
2126+ args: &[Cow<str>],
20962127 _event: PromptEvent,
20972128 ) -> anyhow::Result<()> {
2098− let view = view!(cx.editor);
2099− let doc_id = view.doc;
2100− cx.editor.close_document(doc_id, true)?;
2101− Ok(())
2129+ buffer_close_impl(cx.editor, args, true)
21022130 }
21032131
21042132 fn write_impl(cx: &mut compositor::Context, path: Option<&Cow<str>>) -> anyhow::Result<()> {
@@ -2927,14 +2955,14 @@ pub mod cmd {
29272955 aliases: &["bc", "bclose"],
29282956 doc: "Close the current buffer.",
29292957 fun: buffer_close,
2930− completer: None, // FIXME: buffer completer
2958+ completer: Some(completers::buffer),
29312959 },
29322960 TypableCommand {
29332961 name: "buffer-close!",
29342962 aliases: &["bc!", "bclose!"],
29352963 doc: "Close the current buffer forcefully (ignoring unsaved changes).",
29362964 fun: force_buffer_close,
2937− completer: None, // FIXME: buffer completer
2965+ completer: Some(completers::buffer),
29382966 },
29392967 TypableCommand {
29402968 name: "write",
@@ -3262,7 +3290,7 @@ fn command_mode(cx: &mut Context) {
32623290 let mut prompt = Prompt::new(
32633291 ":".into(),
32643292 Some(':'),
3265− |input: &str| {
3293+ |ctx: &compositor::Context, input: &str| {
32663294 static FUZZY_MATCHER: Lazy<fuzzy_matcher::skim::SkimMatcherV2> =
32673295 Lazy::new(fuzzy_matcher::skim::SkimMatcherV2::default);
32683296
@@ -3294,7 +3322,7 @@ fn command_mode(cx: &mut Context) {
32943322 ..
32953323 }) = cmd::TYPABLE_COMMAND_MAP.get(parts[0])
32963324 {
3297− completer(part)
3325+ completer(ctx, part)
32983326 .into_iter()
32993327 .map(|(range, file)| {
33003328 // offset ranges to input
@@ -5358,7 +5386,7 @@ fn keep_or_remove_selections_impl(cx: &mut Context, remove: bool) {
53585386 cx,
53595387 if !remove { "keep:" } else { "remove:" }.into(),
53605388 Some(reg),
5361− |_input: &str| Vec::new(),
5389+ |_ctx: &compositor::Context, _input: &str| Vec::new(),
53625390 move |view, doc, regex, event| {
53635391 if event != PromptEvent::Update {
53645392 return;
@@ -6122,7 +6150,7 @@ fn shell_keep_pipe(cx: &mut Context) {
61226150 let prompt = Prompt::new(
61236151 "keep-pipe:".into(),
61246152 Some('|'),
6125− |_input: &str| Vec::new(),
6153+ |_ctx: &compositor::Context, _input: &str| Vec::new(),
61266154 move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
61276155 let shell = &cx.editor.config.shell;
61286156 if event != PromptEvent::Validate {
@@ -6218,7 +6246,7 @@ fn shell(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) {
62186246 let prompt = Prompt::new(
62196247 prompt,
62206248 Some('|'),
6221− |_input: &str| Vec::new(),
6249+ |_ctx: &compositor::Context, _input: &str| Vec::new(),
62226250 move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
62236251 let shell = &cx.editor.config.shell;
62246252 if event != PromptEvent::Validate {
@@ -6314,7 +6342,7 @@ fn rename_symbol(cx: &mut Context) {
63146342 let prompt = Prompt::new(
63156343 "rename-to:".into(),
63166344 None,
6317− |_input: &str| Vec::new(),
6345+ |_ctx: &compositor::Context, _input: &str| Vec::new(),
63186346 move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
63196347 if event != PromptEvent::Validate {
63206348 return;
helix-term/src/commands/dap.rs+4−3
@@ -361,8 +361,9 @@ fn debug_parameter_prompt(
361361 let completer = match field_type {
362362 "filename" => ui::completers::filename,
363363 "directory" => ui::completers::directory,
364− _ => |_input: &str| Vec::new(),
364+ _ => ui::completers::none,
365365 };
366+
366367 Prompt::new(
367368 format!("{}: ", name).into(),
368369 None,
@@ -696,7 +697,7 @@ pub fn dap_edit_condition(cx: &mut Context) {
696697 let mut prompt = Prompt::new(
697698 "condition:".into(),
698699 None,
699− |_input: &str| Vec::new(),
700+ ui::completers::none,
700701 move |cx, input: &str, event: PromptEvent| {
701702 if event != PromptEvent::Validate {
702703 return;
@@ -740,7 +741,7 @@ pub fn dap_edit_log(cx: &mut Context) {
740741 let mut prompt = Prompt::new(
741742 "log-message:".into(),
742743 None,
743− |_input: &str| Vec::new(),
744+ ui::completers::none,
744745 move |cx, input: &str, event: PromptEvent| {
745746 if event != PromptEvent::Validate {
746747 return;
helix-term/src/ui/mod.rs+41−6
@@ -30,7 +30,7 @@ pub fn regex_prompt(
3030 cx: &mut crate::commands::Context,
3131 prompt: std::borrow::Cow<'static, str>,
3232 history_register: Option<char>,
33− completion_fn: impl FnMut(&str) -> Vec<prompt::Completion> + 'static,
33+ completion_fn: impl FnMut(&crate::compositor::Context, &str) -> Vec<prompt::Completion> + 'static,
3434 fun: impl Fn(&mut View, &mut Document, Regex, PromptEvent) + 'static,
3535) -> Prompt {
3636 let (view, doc) = current!(cx.editor);
@@ -168,18 +168,53 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi
168168}
169169
170170pub mod completers {
171+ use crate::compositor::Context;
171172 use crate::ui::prompt::Completion;
172173 use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
173174 use fuzzy_matcher::FuzzyMatcher;
175+ use helix_view::document::SCRATCH_BUFFER_NAME;
174176 use helix_view::editor::Config;
175177 use helix_view::theme;
176178 use once_cell::sync::Lazy;
177179 use std::borrow::Cow;
178180 use std::cmp::Reverse;
179181
180− pub type Completer = fn(&str) -> Vec<Completion>;
182+ pub type Completer = fn(&Context, &str) -> Vec<Completion>;
181183
182− pub fn theme(input: &str) -> Vec<Completion> {
184+ pub fn none(_cx: &Context, _input: &str) -> Vec<Completion> {
185+ Vec::new()
186+ }
187+
188+ pub fn buffer(cx: &Context, input: &str) -> Vec<Completion> {
189+ let mut names: Vec<_> = cx
190+ .editor
191+ .documents
192+ .iter()
193+ .map(|(_id, doc)| {
194+ let name = doc
195+ .relative_path()
196+ .map(|p| p.display().to_string())
197+ .unwrap_or_else(|| String::from(SCRATCH_BUFFER_NAME));
198+ ((0..), Cow::from(name))
199+ })
200+ .collect();
201+
202+ let matcher = Matcher::default();
203+
204+ let mut matches: Vec<_> = names
205+ .into_iter()
206+ .filter_map(|(_range, name)| {
207+ matcher.fuzzy_match(&name, input).map(|score| (name, score))
208+ })
209+ .collect();
210+
211+ matches.sort_unstable_by_key(|(_file, score)| Reverse(*score));
212+ names = matches.into_iter().map(|(name, _)| ((0..), name)).collect();
213+
214+ names
215+ }
216+
217+ pub fn theme(_cx: &Context, input: &str) -> Vec<Completion> {
183218 let mut names = theme::Loader::read_names(&helix_core::runtime_dir().join("themes"));
184219 names.extend(theme::Loader::read_names(
185220 &helix_core::config_dir().join("themes"),
@@ -207,7 +242,7 @@ pub mod completers {
207242 names
208243 }
209244
210− pub fn setting(input: &str) -> Vec<Completion> {
245+ pub fn setting(_cx: &Context, input: &str) -> Vec<Completion> {
211246 static KEYS: Lazy<Vec<String>> = Lazy::new(|| {
212247 serde_json::to_value(Config::default())
213248 .unwrap()
@@ -232,7 +267,7 @@ pub mod completers {
232267 .collect()
233268 }
234269
235− pub fn filename(input: &str) -> Vec<Completion> {
270+ pub fn filename(_cx: &Context, input: &str) -> Vec<Completion> {
236271 filename_impl(input, |entry| {
237272 let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir());
238273
@@ -244,7 +279,7 @@ pub mod completers {
244279 })
245280 }
246281
247− pub fn directory(input: &str) -> Vec<Completion> {
282+ pub fn directory(_cx: &Context, input: &str) -> Vec<Completion> {
248283 filename_impl(input, |entry| {
249284 let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir());
250285
helix-term/src/ui/picker.rs+4−4
@@ -302,7 +302,7 @@ impl<T> Picker<T> {
302302 let prompt = Prompt::new(
303303 "".into(),
304304 None,
305− |_pattern: &str| Vec::new(),
305+ |_ctx: &Context, _pattern: &str| Vec::new(),
306306 |_editor: &mut Context, _pattern: &str, _event: PromptEvent| {
307307 //
308308 },
@@ -395,12 +395,12 @@ impl<T> Picker<T> {
395395 .map(|(index, _score)| &self.options[*index])
396396 }
397397
398− pub fn save_filter(&mut self) {
398+ pub fn save_filter(&mut self, cx: &Context) {
399399 self.filters.clear();
400400 self.filters
401401 .extend(self.matches.iter().map(|(index, _)| *index));
402402 self.filters.sort_unstable(); // used for binary search later
403− self.prompt.clear();
403+ self.prompt.clear(cx);
404404 }
405405}
406406
@@ -468,7 +468,7 @@ impl<T: 'static> Component for Picker<T> {
468468 return close_fn;
469469 }
470470 ctrl!(' ') => {
471− self.save_filter();
471+ self.save_filter(cx);
472472 }
473473 _ => {
474474 if let EventResult::Consumed(_) = self.prompt.handle_event(event, cx) {
helix-term/src/ui/prompt.rs+28−27
@@ -24,7 +24,7 @@ pub struct Prompt {
2424 selection: Option<usize>,
2525 history_register: Option<char>,
2626 history_pos: Option<usize>,
27− completion_fn: Box<dyn FnMut(&str) -> Vec<Completion>>,
27+ completion_fn: Box<dyn FnMut(&Context, &str) -> Vec<Completion>>,
2828 callback_fn: Box<dyn FnMut(&mut Context, &str, PromptEvent)>,
2929 pub doc_fn: Box<dyn Fn(&str) -> Option<&'static str>>,
3030}
@@ -59,14 +59,14 @@ impl Prompt {
5959 pub fn new(
6060 prompt: Cow<'static, str>,
6161 history_register: Option<char>,
62− mut completion_fn: impl FnMut(&str) -> Vec<Completion> + 'static,
62+ completion_fn: impl FnMut(&Context, &str) -> Vec<Completion> + 'static,
6363 callback_fn: impl FnMut(&mut Context, &str, PromptEvent) + 'static,
6464 ) -> Self {
6565 Self {
6666 prompt,
6767 line: String::new(),
6868 cursor: 0,
69− completion: completion_fn(""),
69+ completion: Vec::new(),
7070 selection: None,
7171 history_register,
7272 history_pos: None,
@@ -177,13 +177,13 @@ impl Prompt {
177177 }
178178 }
179179
180− pub fn insert_char(&mut self, c: char) {
180+ pub fn insert_char(&mut self, c: char, cx: &Context) {
181181 self.line.insert(self.cursor, c);
182182 let mut cursor = GraphemeCursor::new(self.cursor, self.line.len(), false);
183183 if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) {
184184 self.cursor = pos;
185185 }
186− self.completion = (self.completion_fn)(&self.line);
186+ self.completion = (self.completion_fn)(cx, &self.line);
187187 self.exit_selection();
188188 }
189189
@@ -205,61 +205,61 @@ impl Prompt {
205205 self.cursor = self.line.len();
206206 }
207207
208− pub fn delete_char_backwards(&mut self) {
208+ pub fn delete_char_backwards(&mut self, cx: &Context) {
209209 let pos = self.eval_movement(Movement::BackwardChar(1));
210210 self.line.replace_range(pos..self.cursor, "");
211211 self.cursor = pos;
212212
213213 self.exit_selection();
214− self.completion = (self.completion_fn)(&self.line);
214+ self.completion = (self.completion_fn)(cx, &self.line);
215215 }
216216
217− pub fn delete_char_forwards(&mut self) {
217+ pub fn delete_char_forwards(&mut self, cx: &Context) {
218218 let pos = self.eval_movement(Movement::ForwardChar(1));
219219 self.line.replace_range(self.cursor..pos, "");
220220
221221 self.exit_selection();
222− self.completion = (self.completion_fn)(&self.line);
222+ self.completion = (self.completion_fn)(cx, &self.line);
223223 }
224224
225− pub fn delete_word_backwards(&mut self) {
225+ pub fn delete_word_backwards(&mut self, cx: &Context) {
226226 let pos = self.eval_movement(Movement::BackwardWord(1));
227227 self.line.replace_range(pos..self.cursor, "");
228228 self.cursor = pos;
229229
230230 self.exit_selection();
231− self.completion = (self.completion_fn)(&self.line);
231+ self.completion = (self.completion_fn)(cx, &self.line);
232232 }
233233
234− pub fn delete_word_forwards(&mut self) {
234+ pub fn delete_word_forwards(&mut self, cx: &Context) {
235235 let pos = self.eval_movement(Movement::ForwardWord(1));
236236 self.line.replace_range(self.cursor..pos, "");
237237
238238 self.exit_selection();
239− self.completion = (self.completion_fn)(&self.line);
239+ self.completion = (self.completion_fn)(cx, &self.line);
240240 }
241241
242− pub fn kill_to_start_of_line(&mut self) {
242+ pub fn kill_to_start_of_line(&mut self, cx: &Context) {
243243 let pos = self.eval_movement(Movement::StartOfLine);
244244 self.line.replace_range(pos..self.cursor, "");
245245 self.cursor = pos;
246246
247247 self.exit_selection();
248− self.completion = (self.completion_fn)(&self.line);
248+ self.completion = (self.completion_fn)(cx, &self.line);
249249 }
250250
251− pub fn kill_to_end_of_line(&mut self) {
251+ pub fn kill_to_end_of_line(&mut self, cx: &Context) {
252252 let pos = self.eval_movement(Movement::EndOfLine);
253253 self.line.replace_range(self.cursor..pos, "");
254254
255255 self.exit_selection();
256− self.completion = (self.completion_fn)(&self.line);
256+ self.completion = (self.completion_fn)(cx, &self.line);
257257 }
258258
259− pub fn clear(&mut self) {
259+ pub fn clear(&mut self, cx: &Context) {
260260 self.line.clear();
261261 self.cursor = 0;
262− self.completion = (self.completion_fn)(&self.line);
262+ self.completion = (self.completion_fn)(cx, &self.line);
263263 self.exit_selection();
264264 }
265265
@@ -442,16 +442,16 @@ impl Component for Prompt {
442442 ctrl!('f') | key!(Right) => self.move_cursor(Movement::ForwardChar(1)),
443443 ctrl!('e') | key!(End) => self.move_end(),
444444 ctrl!('a') | key!(Home) => self.move_start(),
445− ctrl!('w') => self.delete_word_backwards(),
446− alt!('d') => self.delete_word_forwards(),
447− ctrl!('k') => self.kill_to_end_of_line(),
448− ctrl!('u') => self.kill_to_start_of_line(),
445+ ctrl!('w') => self.delete_word_backwards(cx),
446+ alt!('d') => self.delete_word_forwards(cx),
447+ ctrl!('k') => self.kill_to_end_of_line(cx),
448+ ctrl!('u') => self.kill_to_start_of_line(cx),
449449 ctrl!('h') | key!(Backspace) => {
450− self.delete_char_backwards();
450+ self.delete_char_backwards(cx);
451451 (self.callback_fn)(cx, &self.line, PromptEvent::Update);
452452 }
453453 ctrl!('d') | key!(Delete) => {
454− self.delete_char_forwards();
454+ self.delete_char_forwards(cx);
455455 (self.callback_fn)(cx, &self.line, PromptEvent::Update);
456456 }
457457 ctrl!('s') => {
@@ -474,7 +474,7 @@ impl Component for Prompt {
474474 }
475475 key!(Enter) => {
476476 if self.selection.is_some() && self.line.ends_with(std::path::MAIN_SEPARATOR) {
477− self.completion = (self.completion_fn)(&self.line);
477+ self.completion = (self.completion_fn)(cx, &self.line);
478478 self.exit_selection();
479479 } else {
480480 (self.callback_fn)(cx, &self.line, PromptEvent::Validate);
@@ -515,7 +515,7 @@ impl Component for Prompt {
515515 code: KeyCode::Char(c),
516516 modifiers,
517517 } if !modifiers.contains(KeyModifiers::CONTROL) => {
518− self.insert_char(c);
518+ self.insert_char(c, cx);
519519 (self.callback_fn)(cx, &self.line, PromptEvent::Update);
520520 }
521521 _ => (),
@@ -525,6 +525,7 @@ impl Component for Prompt {
525525 }
526526
527527 fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
528+ self.completion = (self.completion_fn)(cx, &self.line);
528529 self.render_prompt(area, surface, cx)
529530 }
530531