diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index cc7b84c4b..b9b634580 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2098,7 +2098,8 @@ fn searcher(cx: &mut Context, direction: Direction) { .iter() .filter(|comp| comp.starts_with(input)) .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone()))) - .collect() + .collect::>() + .into() }, move |cx, regex, event| { if event == PromptEvent::Validate { @@ -2290,7 +2291,8 @@ fn global_search(cx: &mut Context) { .iter() .filter(|comp| comp.starts_with(input)) .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone()))) - .collect() + .collect::>() + .into() }, move |cx, _, input, event| { if event != PromptEvent::Validate { diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 0a1f90a61..ac11f3679 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1,16 +1,18 @@ +use helix_view::document::read_to_string; use std::fmt::Write; use std::io::BufReader; use std::ops::Deref; use crate::job::Job; +use crate::ui::CompletionResult; use super::*; use helix_core::fuzzy::fuzzy_match; use helix_core::indent::MAX_INDENT; -use helix_core::{line_ending, shellwords::Shellwords}; -use helix_view::document::{read_to_string, DEFAULT_LANGUAGE_NAME}; -use helix_view::editor::{CloseError, ConfigEvent}; +use helix_core::{encoding, line_ending, shellwords::Shellwords}; +use helix_view::document::DEFAULT_LANGUAGE_NAME; +use helix_view::editor::{Action, CloseError, CommandHints, ConfigEvent}; use serde_json::Value; use ui::completers::{self, Completer}; @@ -3132,14 +3134,18 @@ pub(super) fn command_mode(cx: &mut Context) { let words = shellwords.words(); if words.is_empty() || (words.len() == 1 && !shellwords.ends_with_whitespace()) { - fuzzy_match( + let mut result: CompletionResult = fuzzy_match( input, TYPABLE_COMMAND_LIST.iter().map(|command| command.name), false, ) .into_iter() .map(|(name, _)| (0.., name.into())) - .collect() + .collect::>() + .into(); + result.show_popup = matches!(editor.config().command_hints, CommandHints::Always); + + result } else { // Otherwise, use the command's completer and the last shellword // as completion input. @@ -3151,11 +3157,12 @@ pub(super) fn command_mode(cx: &mut Context) { let argument_number = argument_number_of(&shellwords); - if let Some(completer) = TYPABLE_COMMAND_MAP + let mut result = if let Some(completer) = TYPABLE_COMMAND_MAP .get(&words[0] as &str) .map(|tc| tc.completer_for_argument_number(argument_number)) { completer(editor, word) + .completions .into_iter() .map(|(range, file)| { let file = shellwords::escape(file); @@ -3165,10 +3172,18 @@ pub(super) fn command_mode(cx: &mut Context) { let range = (range.start + offset)..; (range, file) }) - .collect() + .collect::>() + .into() } else { - Vec::new() - } + CompletionResult::default() + }; + + result.show_popup = matches!( + editor.config().command_hints, + CommandHints::Always | CommandHints::OnlyArguments + ); + + result } }, // completion move |cx: &mut compositor::Context, input: &str, event: PromptEvent| { diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 5211c2e27..36d53eb23 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -23,7 +23,7 @@ pub use markdown::Markdown; pub use menu::Menu; pub use picker::{DynamicPicker, FileLocation, Picker}; pub use popup::Popup; -pub use prompt::{Prompt, PromptEvent}; +pub use prompt::{CompletionResult, Prompt, PromptEvent}; pub use spinner::{ProgressSpinners, Spinner}; pub use text::Text; @@ -35,7 +35,7 @@ pub fn prompt( cx: &mut crate::commands::Context, prompt: std::borrow::Cow<'static, str>, history_register: Option, - completion_fn: impl FnMut(&Editor, &str) -> Vec + 'static, + completion_fn: impl FnMut(&Editor, &str) -> CompletionResult + 'static, callback_fn: impl FnMut(&mut crate::compositor::Context, &str, PromptEvent) + 'static, ) { let mut prompt = Prompt::new(prompt, history_register, completion_fn, callback_fn); @@ -49,7 +49,7 @@ pub fn prompt_with_input( prompt: std::borrow::Cow<'static, str>, input: String, history_register: Option, - completion_fn: impl FnMut(&Editor, &str) -> Vec + 'static, + completion_fn: impl FnMut(&Editor, &str) -> CompletionResult + 'static, callback_fn: impl FnMut(&mut crate::compositor::Context, &str, PromptEvent) + 'static, ) { let prompt = Prompt::new(prompt, history_register, completion_fn, callback_fn) @@ -61,7 +61,7 @@ pub fn regex_prompt( cx: &mut crate::commands::Context, prompt: std::borrow::Cow<'static, str>, history_register: Option, - completion_fn: impl FnMut(&Editor, &str) -> Vec + 'static, + completion_fn: impl FnMut(&Editor, &str) -> CompletionResult + 'static, fun: impl Fn(&mut crate::compositor::Context, rope::Regex, PromptEvent) + 'static, ) { raw_regex_prompt( @@ -72,11 +72,12 @@ pub fn regex_prompt( move |cx, regex, _, event| fun(cx, regex, event), ); } + pub fn raw_regex_prompt( cx: &mut crate::commands::Context, prompt: std::borrow::Cow<'static, str>, history_register: Option, - completion_fn: impl FnMut(&Editor, &str) -> Vec + 'static, + completion_fn: impl FnMut(&Editor, &str) -> CompletionResult + 'static, fun: impl Fn(&mut crate::compositor::Context, rope::Regex, &str, PromptEvent) + 'static, ) { let (view, doc) = current!(cx.editor); @@ -254,6 +255,7 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker } pub mod completers { + use super::CompletionResult; use crate::ui::prompt::Completion; use helix_core::fuzzy::fuzzy_match; use helix_core::syntax::LanguageServerFeature; @@ -263,13 +265,13 @@ pub mod completers { use once_cell::sync::Lazy; use std::borrow::Cow; - pub type Completer = fn(&Editor, &str) -> Vec; + pub type Completer = fn(&Editor, &str) -> CompletionResult; - pub fn none(_editor: &Editor, _input: &str) -> Vec { - Vec::new() + pub fn none(_editor: &Editor, _input: &str) -> CompletionResult { + CompletionResult::default() } - pub fn buffer(editor: &Editor, input: &str) -> Vec { + pub fn buffer(editor: &Editor, input: &str) -> CompletionResult { let names = editor.documents.values().map(|doc| { doc.relative_path() .map(|p| p.display().to_string().into()) @@ -279,10 +281,11 @@ pub mod completers { fuzzy_match(input, names, true) .into_iter() .map(|(name, _)| ((0..), name)) - .collect() + .collect::>() + .into() } - pub fn theme(_editor: &Editor, input: &str) -> Vec { + pub fn theme(_editor: &Editor, input: &str) -> CompletionResult { let mut names = theme::Loader::read_names(&helix_loader::config_dir().join("themes")); for rt_dir in helix_loader::runtime_dirs() { names.extend(theme::Loader::read_names(&rt_dir.join("themes"))); @@ -295,7 +298,8 @@ pub mod completers { fuzzy_match(input, names, false) .into_iter() .map(|(name, _)| ((0..), name.into())) - .collect() + .collect::>() + .into() } /// Recursive function to get all keys from this value and add them to vec @@ -314,7 +318,7 @@ pub mod completers { } } - pub fn setting(_editor: &Editor, input: &str) -> Vec { + pub fn setting(_editor: &Editor, input: &str) -> CompletionResult { static KEYS: Lazy> = Lazy::new(|| { let mut keys = Vec::new(); let json = serde_json::json!(Config::default()); @@ -325,18 +329,19 @@ pub mod completers { fuzzy_match(input, &*KEYS, false) .into_iter() .map(|(name, _)| ((0..), name.into())) - .collect() + .collect::>() + .into() } - pub fn filename(editor: &Editor, input: &str) -> Vec { - filename_with_git_ignore(editor, input, true) + pub fn filename(editor: &Editor, input: &str) -> CompletionResult { + filename_with_git_ignore(editor, input, true).into() } pub fn filename_with_git_ignore( editor: &Editor, input: &str, git_ignore: bool, - ) -> Vec { + ) -> CompletionResult { filename_impl(editor, input, git_ignore, |entry| { let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir()); @@ -346,9 +351,10 @@ pub mod completers { FileMatch::Accept } }) + .into() } - pub fn language(editor: &Editor, input: &str) -> Vec { + pub fn language(editor: &Editor, input: &str) -> CompletionResult { let text: String = "text".into(); let loader = editor.syn_loader.load(); @@ -360,32 +366,34 @@ pub mod completers { fuzzy_match(input, language_ids, false) .into_iter() .map(|(name, _)| ((0..), name.to_owned().into())) - .collect() + .collect::>() + .into() } - pub fn lsp_workspace_command(editor: &Editor, input: &str) -> Vec { + pub fn lsp_workspace_command(editor: &Editor, input: &str) -> CompletionResult { let Some(options) = doc!(editor) .language_servers_with_feature(LanguageServerFeature::WorkspaceCommand) .find_map(|ls| ls.capabilities().execute_command_provider.as_ref()) else { - return vec![]; + return CompletionResult::default(); }; fuzzy_match(input, &options.commands, false) .into_iter() .map(|(name, _)| ((0..), name.to_owned().into())) - .collect() + .collect::>() + .into() } - pub fn directory(editor: &Editor, input: &str) -> Vec { - directory_with_git_ignore(editor, input, true) + pub fn directory(editor: &Editor, input: &str) -> CompletionResult { + directory_with_git_ignore(editor, input, true).into() } pub fn directory_with_git_ignore( editor: &Editor, input: &str, git_ignore: bool, - ) -> Vec { + ) -> CompletionResult { filename_impl(editor, input, git_ignore, |entry| { let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir()); @@ -395,6 +403,7 @@ pub mod completers { FileMatch::Reject } }) + .into() } #[derive(Copy, Clone, PartialEq, Eq)] @@ -414,7 +423,7 @@ pub mod completers { input: &str, git_ignore: bool, filter_fn: F, - ) -> Vec + ) -> CompletionResult where F: Fn(&ignore::DirEntry) -> FileMatch, { @@ -498,17 +507,18 @@ pub mod completers { fuzzy_match(&file_name, files, true) .into_iter() .map(|(name, _)| (range.clone(), name)) - .collect() + .collect::>() + .into() // TODO: complete to longest common match } else { let mut files: Vec<_> = files.map(|file| (end.clone(), file)).collect(); files.sort_unstable_by(|(_, path1), (_, path2)| path1.cmp(path2)); - files + files.into() } } - pub fn register(editor: &Editor, input: &str) -> Vec { + pub fn register(editor: &Editor, input: &str) -> CompletionResult { let iter = editor .registers .iter_preview() @@ -519,6 +529,7 @@ pub mod completers { fuzzy_match(input, iter, false) .into_iter() .map(|(name, _)| ((0..), name.into())) - .collect() + .collect::>() + .into() } } diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index a6ee7f05d..778441b6d 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -19,10 +19,25 @@ use helix_view::{ type PromptCharHandler = Box; pub type Completion = (RangeFrom, Cow<'static, str>); -type CompletionFn = Box Vec>; +type CompletionFn = Box CompletionResult>; type CallbackFn = Box; pub type DocFn = Box Option>>; +#[derive(Default)] +pub struct CompletionResult { + pub completions: Vec, + pub show_popup: bool, +} + +impl From> for CompletionResult { + fn from(completions: Vec) -> Self { + Self { + show_popup: true, + completions, + } + } +} + pub struct Prompt { prompt: Cow<'static, str>, line: String, @@ -34,6 +49,7 @@ pub struct Prompt { completion_fn: CompletionFn, callback_fn: CallbackFn, pub doc_fn: DocFn, + pub show_popup: bool, next_char_handler: Option, language: Option<(&'static str, Arc>)>, } @@ -72,7 +88,7 @@ impl Prompt { pub fn new( prompt: Cow<'static, str>, history_register: Option, - completion_fn: impl FnMut(&Editor, &str) -> Vec + 'static, + completion_fn: impl FnMut(&Editor, &str) -> CompletionResult + 'static, callback_fn: impl FnMut(&mut Context, &str, PromptEvent) + 'static, ) -> Self { Self { @@ -88,6 +104,7 @@ impl Prompt { doc_fn: Box::new(|_| None), next_char_handler: None, language: None, + show_popup: true, } } @@ -114,7 +131,13 @@ impl Prompt { pub fn recalculate_completion(&mut self, editor: &Editor) { self.exit_selection(); - self.completion = (self.completion_fn)(editor, &self.line); + let CompletionResult { + completions, + show_popup, + } = (self.completion_fn)(editor, &self.line); + + self.completion = completions; + self.show_popup = show_popup; } /// Compute the cursor position after applying movement @@ -367,14 +390,10 @@ impl Prompt { const BASE_WIDTH: u16 = 30; impl Prompt { - pub fn render_prompt(&self, area: Rect, surface: &mut Surface, cx: &mut Context) { + fn render_completion_hints(&self, area: Rect, surface: &mut Surface, cx: &mut Context) -> Rect { let theme = &cx.editor.theme; - let prompt_color = theme.get("ui.text"); let completion_color = theme.get("ui.menu"); let selected_color = theme.get("ui.menu.selected"); - let suggestion_color = theme.get("ui.text.inactive"); - let background = theme.get("ui.background"); - // completion let max_len = self .completion @@ -437,6 +456,21 @@ impl Prompt { } } + completion_area + } + + pub fn render_prompt(&self, area: Rect, surface: &mut Surface, cx: &mut Context) { + let completion_area = if self.show_popup { + self.render_completion_hints(area, surface, cx) + } else { + Rect::new(area.x, area.height.saturating_sub(1), area.width, 0) + }; + + let theme = &cx.editor.theme; + let prompt_color = theme.get("ui.text"); + let suggestion_color = theme.get("ui.text.inactive"); + let background = theme.get("ui.background"); + if let Some(doc) = (self.doc_fn)(&self.line) { let mut text = ui::Text::new(doc.to_string()); diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 9d3e5dbc3..8eacd05fa 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -351,6 +351,7 @@ pub enum CommandHints { /// Never show it Never, /// Show only for command's arguments + #[serde(rename = "only-args")] OnlyArguments, }