diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index e2c4a9d9..bf87f446 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1435,7 +1435,7 @@ fn select_regex(cx: &mut Context) { cx, "select:".into(), Some(reg), - |_input: &str| Vec::new(), + |_ctx: &compositor::Context, _input: &str| Vec::new(), move |view, doc, regex, event| { if event != PromptEvent::Update { return; @@ -1458,7 +1458,7 @@ fn split_selection(cx: &mut Context) { cx, "split:".into(), Some(reg), - |_input: &str| Vec::new(), + |_ctx: &compositor::Context, _input: &str| Vec::new(), move |view, doc, regex, event| { if event != PromptEvent::Update { return; @@ -1600,7 +1600,7 @@ fn searcher(cx: &mut Context, direction: Direction) { cx, "search:".into(), Some(reg), - move |input: &str| { + move |_ctx: &compositor::Context, input: &str| { completions .iter() .filter(|comp| comp.starts_with(input)) @@ -1701,7 +1701,7 @@ fn global_search(cx: &mut Context) { cx, "global-search:".into(), None, - move |input: &str| { + move |_ctx: &compositor::Context, input: &str| { completions .iter() .filter(|comp| comp.starts_with(input)) @@ -2079,26 +2079,54 @@ pub mod cmd { Ok(()) } + fn buffer_close_impl( + editor: &mut Editor, + args: &[Cow], + 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( cx: &mut compositor::Context, - _args: &[Cow], + args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { - let view = view!(cx.editor); - let doc_id = view.doc; - cx.editor.close_document(doc_id, false)?; - Ok(()) + buffer_close_impl(cx.editor, args, false) } fn force_buffer_close( cx: &mut compositor::Context, - _args: &[Cow], + args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { - let view = view!(cx.editor); - let doc_id = view.doc; - cx.editor.close_document(doc_id, true)?; - Ok(()) + buffer_close_impl(cx.editor, args, true) } fn write_impl(cx: &mut compositor::Context, path: Option<&Cow>) -> anyhow::Result<()> { @@ -2927,14 +2955,14 @@ pub mod cmd { aliases: &["bc", "bclose"], doc: "Close the current buffer.", fun: buffer_close, - completer: None, // FIXME: buffer completer + completer: Some(completers::buffer), }, TypableCommand { name: "buffer-close!", aliases: &["bc!", "bclose!"], doc: "Close the current buffer forcefully (ignoring unsaved changes).", fun: force_buffer_close, - completer: None, // FIXME: buffer completer + completer: Some(completers::buffer), }, TypableCommand { name: "write", @@ -3262,7 +3290,7 @@ fn command_mode(cx: &mut Context) { let mut prompt = Prompt::new( ":".into(), Some(':'), - |input: &str| { + |ctx: &compositor::Context, input: &str| { static FUZZY_MATCHER: Lazy = Lazy::new(fuzzy_matcher::skim::SkimMatcherV2::default); @@ -3294,7 +3322,7 @@ fn command_mode(cx: &mut Context) { .. }) = cmd::TYPABLE_COMMAND_MAP.get(parts[0]) { - completer(part) + completer(ctx, part) .into_iter() .map(|(range, file)| { // offset ranges to input @@ -5358,7 +5386,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(), + |_ctx: &compositor::Context, _input: &str| Vec::new(), move |view, doc, regex, event| { if event != PromptEvent::Update { return; @@ -6122,7 +6150,7 @@ fn shell_keep_pipe(cx: &mut Context) { let prompt = Prompt::new( "keep-pipe:".into(), Some('|'), - |_input: &str| Vec::new(), + |_ctx: &compositor::Context, _input: &str| Vec::new(), move |cx: &mut compositor::Context, input: &str, event: PromptEvent| { let shell = &cx.editor.config.shell; if event != PromptEvent::Validate { @@ -6218,7 +6246,7 @@ fn shell(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) { let prompt = Prompt::new( prompt, Some('|'), - |_input: &str| Vec::new(), + |_ctx: &compositor::Context, _input: &str| Vec::new(), move |cx: &mut compositor::Context, input: &str, event: PromptEvent| { let shell = &cx.editor.config.shell; if event != PromptEvent::Validate { @@ -6314,7 +6342,7 @@ fn rename_symbol(cx: &mut Context) { let prompt = Prompt::new( "rename-to:".into(), None, - |_input: &str| Vec::new(), + |_ctx: &compositor::Context, _input: &str| Vec::new(), move |cx: &mut compositor::Context, input: &str, event: PromptEvent| { if event != PromptEvent::Validate { return; diff --git a/helix-term/src/commands/dap.rs b/helix-term/src/commands/dap.rs index 9da2715f..925c65c1 100644 --- a/helix-term/src/commands/dap.rs +++ b/helix-term/src/commands/dap.rs @@ -361,8 +361,9 @@ fn debug_parameter_prompt( let completer = match field_type { "filename" => ui::completers::filename, "directory" => ui::completers::directory, - _ => |_input: &str| Vec::new(), + _ => ui::completers::none, }; + Prompt::new( format!("{}: ", name).into(), None, @@ -696,7 +697,7 @@ pub fn dap_edit_condition(cx: &mut Context) { let mut prompt = Prompt::new( "condition:".into(), None, - |_input: &str| Vec::new(), + ui::completers::none, move |cx, input: &str, event: PromptEvent| { if event != PromptEvent::Validate { return; @@ -740,7 +741,7 @@ pub fn dap_edit_log(cx: &mut Context) { let mut prompt = Prompt::new( "log-message:".into(), None, - |_input: &str| Vec::new(), + ui::completers::none, move |cx, 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 7f6d9f7c..263342b7 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -30,7 +30,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(&crate::compositor::Context, &str) -> Vec + 'static, fun: impl Fn(&mut View, &mut Document, Regex, PromptEvent) + 'static, ) -> Prompt { 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 { + 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::editor::Config; use helix_view::theme; use once_cell::sync::Lazy; use std::borrow::Cow; use std::cmp::Reverse; - pub type Completer = fn(&str) -> Vec; + pub type Completer = fn(&Context, &str) -> Vec; - pub fn theme(input: &str) -> Vec { + pub fn none(_cx: &Context, _input: &str) -> Vec { + Vec::new() + } + + pub fn buffer(cx: &Context, input: &str) -> 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(_cx: &Context, input: &str) -> 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 +242,7 @@ pub mod completers { names } - pub fn setting(input: &str) -> Vec { + pub fn setting(_cx: &Context, input: &str) -> Vec { static KEYS: Lazy> = Lazy::new(|| { serde_json::to_value(Config::default()) .unwrap() @@ -232,7 +267,7 @@ pub mod completers { .collect() } - pub fn filename(input: &str) -> Vec { + pub fn filename(_cx: &Context, input: &str) -> Vec { filename_impl(input, |entry| { 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 { + pub fn directory(_cx: &Context, input: &str) -> 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 9cddbc60..dcc64002 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -302,7 +302,7 @@ impl Picker { let prompt = Prompt::new( "".into(), None, - |_pattern: &str| Vec::new(), + |_ctx: &Context, _pattern: &str| Vec::new(), |_editor: &mut Context, _pattern: &str, _event: PromptEvent| { // }, @@ -395,12 +395,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); } } @@ -468,7 +468,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 4c4fef26..ff6b8c76 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(&Context, &str) -> 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)(cx, &self.line); 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)(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)); self.line.replace_range(self.cursor..pos, ""); 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)); 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)(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)); self.line.replace_range(self.cursor..pos, ""); 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); 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)(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); self.line.replace_range(self.cursor..pos, ""); 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.cursor = 0; - self.completion = (self.completion_fn)(&self.line); + self.completion = (self.completion_fn)(cx, &self.line); 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(std::path::MAIN_SEPARATOR) { - self.completion = (self.completion_fn)(&self.line); + self.completion = (self.completion_fn)(cx, &self.line); 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); } _ => (), @@ -525,6 +525,7 @@ impl Component for Prompt { } 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) }