diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index ee2949fa0..440201548 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -30,7 +30,9 @@ use helix_core::{ object, pos_at_coords, regex::{self, Regex}, search::{self, CharMatcher}, - selection, shellwords, surround, + selection, + shellwords::{self, Args}, + surround, syntax::{BlockCommentToken, LanguageServerFeature}, text_annotations::{Overlay, TextAnnotations}, textobject, @@ -190,7 +192,7 @@ use helix_view::{align_view, Align}; pub enum MappableCommand { Typable { name: String, - args: Vec, + args: String, doc: String, }, Static { @@ -225,15 +227,17 @@ impl MappableCommand { pub fn execute(&self, cx: &mut Context) { match &self { Self::Typable { name, args, doc: _ } => { - let args: Vec> = args.iter().map(Cow::from).collect(); if let Some(command) = typed::TYPABLE_COMMAND_MAP.get(name.as_str()) { let mut cx = compositor::Context { editor: cx.editor, jobs: cx.jobs, scroll: None, }; - if let Err(e) = (command.fun)(&mut cx, &args[..], PromptEvent::Validate) { - cx.editor.set_error(format!("{}", e)); + + if let Err(err) = + (command.fun)(&mut cx, Args::from(args), PromptEvent::Validate) + { + cx.editor.set_error(format!("{err}")); } } } @@ -601,21 +605,15 @@ impl std::str::FromStr for MappableCommand { fn from_str(s: &str) -> Result { if let Some(suffix) = s.strip_prefix(':') { - let mut typable_command = suffix.split(' ').map(|arg| arg.trim()); - let name = typable_command - .next() - .ok_or_else(|| anyhow!("Expected typable command name"))?; - let args = typable_command - .map(|s| s.to_owned()) - .collect::>(); + let (name, args) = suffix.split_once(' ').unwrap_or((suffix, "")); typed::TYPABLE_COMMAND_MAP .get(name) .map(|cmd| MappableCommand::Typable { name: cmd.name.to_owned(), doc: format!(":{} {:?}", cmd.name, args), - args, + args: args.to_string(), }) - .ok_or_else(|| anyhow!("No TypableCommand named '{}'", s)) + .ok_or_else(|| anyhow!("No TypableCommand named '{}'", name)) } else if let Some(suffix) = s.strip_prefix('@') { helix_view::input::parse_macro(suffix).map(|keys| Self::Macro { name: s.to_string(), @@ -3216,7 +3214,7 @@ pub fn command_palette(cx: &mut Context) { .iter() .map(|cmd| MappableCommand::Typable { name: cmd.name.to_owned(), - args: Vec::new(), + args: String::new(), doc: cmd.doc.to_owned(), }), ); @@ -4265,7 +4263,7 @@ fn yank_joined_impl(editor: &mut Editor, separator: &str, register: char) { .fragments(text) .fold(String::new(), |mut acc, fragment| { if !acc.is_empty() { - acc.push_str(separator); + acc.push_str(&helix_core::shellwords::unescape(separator)); } acc.push_str(&fragment); acc diff --git a/helix-term/src/commands/dap.rs b/helix-term/src/commands/dap.rs index 0b754bc21..c93807b28 100644 --- a/helix-term/src/commands/dap.rs +++ b/helix-term/src/commands/dap.rs @@ -109,6 +109,7 @@ fn dap_callback( jobs.callback(callback); } +// TODO: transition to `shellwords::Args` instead of `Option>>` pub fn dap_start_impl( cx: &mut compositor::Context, name: Option<&str>, @@ -312,6 +313,7 @@ pub fn dap_restart(cx: &mut Context) { ); } +// TODO: transition to `shellwords::Args` instead of `Vec` fn debug_parameter_prompt( completions: Vec, config_name: String, diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index fadbe2688..46fa57431 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -12,6 +12,7 @@ use helix_core::{line_ending, shellwords::Shellwords}; use helix_view::document::{read_to_string, DEFAULT_LANGUAGE_NAME}; use helix_view::editor::{CloseError, ConfigEvent}; use serde_json::Value; +use shellwords::Args; use ui::completers::{self, Completer}; #[derive(Clone)] @@ -20,17 +21,17 @@ pub struct TypableCommand { pub aliases: &'static [&'static str], pub doc: &'static str, // params, flags, helper, completer - pub fun: fn(&mut compositor::Context, &[Cow], PromptEvent) -> anyhow::Result<()>, + pub fun: fn(&mut compositor::Context, Args, PromptEvent) -> anyhow::Result<()>, /// What completion methods, if any, does this command have? pub signature: CommandSignature, } impl TypableCommand { fn completer_for_argument_number(&self, n: usize) -> &Completer { - match self.signature.positional_args.get(n) { - Some(completer) => completer, - _ => &self.signature.var_args, - } + self.signature + .positional_args + .get(n) + .map_or(&self.signature.var_args, |completer| completer) } } @@ -66,7 +67,7 @@ impl CommandSignature { } } -fn quit(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> anyhow::Result<()> { +fn quit(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> { log::debug!("quitting..."); if event != PromptEvent::Validate { @@ -77,7 +78,7 @@ fn quit(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> // last view and we have unsaved changes if cx.editor.tree.views().count() == 1 { - buffers_remaining_impl(cx.editor)? + buffers_remaining_impl(cx.editor)?; } cx.block_try_flush_writes()?; @@ -86,11 +87,7 @@ fn quit(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> Ok(()) } -fn force_quit( - cx: &mut compositor::Context, - args: &[Cow], - event: PromptEvent, -) -> anyhow::Result<()> { +fn force_quit(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } @@ -103,12 +100,13 @@ fn force_quit( Ok(()) } -fn open(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> anyhow::Result<()> { +fn open(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - ensure!(!args.is_empty(), "wrong argument count"); + ensure!(!args.is_empty(), ":open needs at least one argument"); + for arg in args { let (path, pos) = args::parse_file(arg); let path = helix_stdx::path::expand_tilde(path); @@ -174,7 +172,7 @@ fn buffer_close_by_ids_impl( Ok(()) } -fn buffer_gather_paths_impl(editor: &mut Editor, args: &[Cow]) -> Vec { +fn buffer_gather_paths_impl(editor: &mut Editor, args: Args) -> Vec { // No arguments implies current document if args.is_empty() { let doc_id = view!(editor).doc; @@ -185,7 +183,7 @@ fn buffer_gather_paths_impl(editor: &mut Editor, args: &[Cow]) -> Vec]) -> Vec], + args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -226,7 +224,7 @@ fn buffer_close( fn force_buffer_close( cx: &mut compositor::Context, - args: &[Cow], + args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -248,7 +246,7 @@ fn buffer_gather_others_impl(editor: &mut Editor) -> Vec { fn buffer_close_others( cx: &mut compositor::Context, - _args: &[Cow], + _args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -261,7 +259,7 @@ fn buffer_close_others( fn force_buffer_close_others( cx: &mut compositor::Context, - _args: &[Cow], + _args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -278,7 +276,7 @@ fn buffer_gather_all_impl(editor: &mut Editor) -> Vec { fn buffer_close_all( cx: &mut compositor::Context, - _args: &[Cow], + _args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -291,7 +289,7 @@ fn buffer_close_all( fn force_buffer_close_all( cx: &mut compositor::Context, - _args: &[Cow], + _args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -304,7 +302,7 @@ fn force_buffer_close_all( fn buffer_next( cx: &mut compositor::Context, - _args: &[Cow], + _args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -317,7 +315,7 @@ fn buffer_next( fn buffer_previous( cx: &mut compositor::Context, - _args: &[Cow], + _args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -328,15 +326,10 @@ fn buffer_previous( Ok(()) } -fn write_impl( - cx: &mut compositor::Context, - path: Option<&Cow>, - force: bool, -) -> anyhow::Result<()> { +fn write_impl(cx: &mut compositor::Context, path: Option<&str>, force: bool) -> anyhow::Result<()> { let config = cx.editor.config(); let jobs = &mut cx.jobs; let (view, doc) = current!(cx.editor); - let path = path.map(AsRef::as_ref); if config.insert_final_newline { insert_final_newline(doc, view.id); @@ -378,40 +371,36 @@ fn insert_final_newline(doc: &mut Document, view_id: ViewId) { } } -fn write( - cx: &mut compositor::Context, - args: &[Cow], - event: PromptEvent, -) -> anyhow::Result<()> { +fn write(cx: &mut compositor::Context, mut args: Args, event: PromptEvent) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - write_impl(cx, args.first(), false) + write_impl(cx, args.next(), false) } fn force_write( cx: &mut compositor::Context, - args: &[Cow], + mut args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - write_impl(cx, args.first(), true) + write_impl(cx, args.next(), true) } fn write_buffer_close( cx: &mut compositor::Context, - args: &[Cow], + mut args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - write_impl(cx, args.first(), false)?; + write_impl(cx, args.next(), false)?; let document_ids = buffer_gather_paths_impl(cx.editor, args); buffer_close_by_ids_impl(cx, &document_ids, false) @@ -419,24 +408,20 @@ fn write_buffer_close( fn force_write_buffer_close( cx: &mut compositor::Context, - args: &[Cow], + mut args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - write_impl(cx, args.first(), true)?; + write_impl(cx, args.next(), true)?; let document_ids = buffer_gather_paths_impl(cx.editor, args); buffer_close_by_ids_impl(cx, &document_ids, false) } -fn new_file( - cx: &mut compositor::Context, - _args: &[Cow], - event: PromptEvent, -) -> anyhow::Result<()> { +fn new_file(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } @@ -446,11 +431,7 @@ fn new_file( Ok(()) } -fn format( - cx: &mut compositor::Context, - _args: &[Cow], - event: PromptEvent, -) -> anyhow::Result<()> { +fn format(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } @@ -465,7 +446,7 @@ fn format( } fn set_indent_style( cx: &mut compositor::Context, - args: &[Cow], + mut args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -486,9 +467,9 @@ fn set_indent_style( } // Attempt to parse argument as an indent style. - let style = match args.first() { + let style = match args.next() { Some(arg) if "tabs".starts_with(&arg.to_lowercase()) => Some(Tabs), - Some(Cow::Borrowed("0")) => Some(Tabs), + Some("0") => Some(Tabs), Some(arg) => arg .parse::() .ok() @@ -507,7 +488,7 @@ fn set_indent_style( /// Sets or reports the current document's line ending setting. fn set_line_ending( cx: &mut compositor::Context, - args: &[Cow], + mut args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -538,7 +519,7 @@ fn set_line_ending( } let arg = args - .first() + .next() .context("argument missing")? .to_ascii_lowercase(); @@ -577,16 +558,12 @@ fn set_line_ending( Ok(()) } -fn earlier( - cx: &mut compositor::Context, - args: &[Cow], - event: PromptEvent, -) -> anyhow::Result<()> { +fn earlier(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - let uk = args.join(" ").parse::().map_err(|s| anyhow!(s))?; + let uk = args.raw().parse::().map_err(|s| anyhow!(s))?; let (view, doc) = current!(cx.editor); let success = doc.earlier(view, uk); @@ -597,16 +574,13 @@ fn earlier( Ok(()) } -fn later( - cx: &mut compositor::Context, - args: &[Cow], - event: PromptEvent, -) -> anyhow::Result<()> { +fn later(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - let uk = args.join(" ").parse::().map_err(|s| anyhow!(s))?; + let uk = args.raw().parse::().map_err(|s| anyhow!(s))?; + let (view, doc) = current!(cx.editor); let success = doc.later(view, uk); if !success { @@ -618,30 +592,30 @@ fn later( fn write_quit( cx: &mut compositor::Context, - args: &[Cow], + mut args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - write_impl(cx, args.first(), false)?; + write_impl(cx, args.next(), false)?; cx.block_try_flush_writes()?; - quit(cx, &[], event) + quit(cx, Args::empty(), event) } fn force_write_quit( cx: &mut compositor::Context, - args: &[Cow], + mut args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - write_impl(cx, args.first(), true)?; + write_impl(cx, args.next(), true)?; cx.block_try_flush_writes()?; - force_quit(cx, &[], event) + force_quit(cx, Args::empty(), event) } /// Results in an error if there are modified buffers remaining and sets editor @@ -741,11 +715,7 @@ pub fn write_all_impl( Ok(()) } -fn write_all( - cx: &mut compositor::Context, - _args: &[Cow], - event: PromptEvent, -) -> anyhow::Result<()> { +fn write_all(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } @@ -755,7 +725,7 @@ fn write_all( fn force_write_all( cx: &mut compositor::Context, - _args: &[Cow], + _args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -767,7 +737,7 @@ fn force_write_all( fn write_all_quit( cx: &mut compositor::Context, - _args: &[Cow], + _args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -779,7 +749,7 @@ fn write_all_quit( fn force_write_all_quit( cx: &mut compositor::Context, - _args: &[Cow], + _args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -804,41 +774,31 @@ fn quit_all_impl(cx: &mut compositor::Context, force: bool) -> anyhow::Result<() Ok(()) } -fn quit_all( - cx: &mut compositor::Context, - _args: &[Cow], - event: PromptEvent, -) -> anyhow::Result<()> { +fn quit_all(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - quit_all_impl(cx, false) } fn force_quit_all( cx: &mut compositor::Context, - _args: &[Cow], + _args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - quit_all_impl(cx, true) } -fn cquit( - cx: &mut compositor::Context, - args: &[Cow], - event: PromptEvent, -) -> anyhow::Result<()> { +fn cquit(cx: &mut compositor::Context, mut args: Args, event: PromptEvent) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } let exit_code = args - .first() + .next() .and_then(|code| code.parse::().ok()) .unwrap_or(1); @@ -848,7 +808,7 @@ fn cquit( fn force_cquit( cx: &mut compositor::Context, - args: &[Cow], + mut args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -856,7 +816,7 @@ fn force_cquit( } let exit_code = args - .first() + .next() .and_then(|code| code.parse::().ok()) .unwrap_or(1); cx.editor.exit_code = exit_code; @@ -864,11 +824,7 @@ fn force_cquit( quit_all_impl(cx, true) } -fn theme( - cx: &mut compositor::Context, - args: &[Cow], - event: PromptEvent, -) -> anyhow::Result<()> { +fn theme(cx: &mut compositor::Context, mut args: Args, event: PromptEvent) -> anyhow::Result<()> { let true_color = cx.editor.config.load().true_color || crate::true_color(); match event { PromptEvent::Abort => { @@ -878,7 +834,7 @@ fn theme( if args.is_empty() { // Ensures that a preview theme gets cleaned up if the user backspaces until the prompt is empty. cx.editor.unset_theme_preview(); - } else if let Some(theme_name) = args.first() { + } else if let Some(theme_name) = args.next() { if let Ok(theme) = cx.editor.theme_loader.load(theme_name) { if !(true_color || theme.is_16_color()) { bail!("Unsupported theme: theme requires true color support"); @@ -888,7 +844,7 @@ fn theme( }; } PromptEvent::Validate => { - if let Some(theme_name) = args.first() { + if let Some(theme_name) = args.next() { let theme = cx .editor .theme_loader @@ -911,168 +867,142 @@ fn theme( fn yank_main_selection_to_clipboard( cx: &mut compositor::Context, - _args: &[Cow], + _args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - yank_primary_selection_impl(cx.editor, '+'); Ok(()) } -fn yank_joined( - cx: &mut compositor::Context, - args: &[Cow], - event: PromptEvent, -) -> anyhow::Result<()> { +fn yank_joined(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - - ensure!(args.len() <= 1, ":yank-join takes at most 1 argument"); - - let doc = doc!(cx.editor); - let default_sep = Cow::Borrowed(doc.line_ending.as_str()); - let separator = args.first().unwrap_or(&default_sep); let register = cx.editor.selected_register.unwrap_or('"'); - yank_joined_impl(cx.editor, separator, register); + yank_joined_impl(cx.editor, args.raw(), register); Ok(()) } fn yank_joined_to_clipboard( cx: &mut compositor::Context, - args: &[Cow], + args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - - let doc = doc!(cx.editor); - let default_sep = Cow::Borrowed(doc.line_ending.as_str()); - let separator = args.first().unwrap_or(&default_sep); - yank_joined_impl(cx.editor, separator, '+'); + yank_joined_impl(cx.editor, args.raw(), '+'); Ok(()) } fn yank_main_selection_to_primary_clipboard( cx: &mut compositor::Context, - _args: &[Cow], + _args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - yank_primary_selection_impl(cx.editor, '*'); Ok(()) } fn yank_joined_to_primary_clipboard( cx: &mut compositor::Context, - args: &[Cow], + args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - let doc = doc!(cx.editor); - let default_sep = Cow::Borrowed(doc.line_ending.as_str()); - let separator = args.first().unwrap_or(&default_sep); - yank_joined_impl(cx.editor, separator, '*'); + yank_joined_impl(cx.editor, args.raw(), '*'); Ok(()) } fn paste_clipboard_after( cx: &mut compositor::Context, - _args: &[Cow], + _args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - paste(cx.editor, '+', Paste::After, 1); Ok(()) } fn paste_clipboard_before( cx: &mut compositor::Context, - _args: &[Cow], + _args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - paste(cx.editor, '+', Paste::Before, 1); Ok(()) } fn paste_primary_clipboard_after( cx: &mut compositor::Context, - _args: &[Cow], + _args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - paste(cx.editor, '*', Paste::After, 1); Ok(()) } fn paste_primary_clipboard_before( cx: &mut compositor::Context, - _args: &[Cow], + _args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - paste(cx.editor, '*', Paste::Before, 1); Ok(()) } fn replace_selections_with_clipboard( cx: &mut compositor::Context, - _args: &[Cow], + _args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - replace_with_yanked_impl(cx.editor, '+', 1); Ok(()) } fn replace_selections_with_primary_clipboard( cx: &mut compositor::Context, - _args: &[Cow], + _args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - replace_with_yanked_impl(cx.editor, '*', 1); Ok(()) } fn show_clipboard_provider( cx: &mut compositor::Context, - _args: &[Cow], + _args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - cx.editor .set_status(cx.editor.registers.clipboard_provider_name().to_string()); Ok(()) @@ -1080,17 +1010,14 @@ fn show_clipboard_provider( fn change_current_directory( cx: &mut compositor::Context, - args: &[Cow], + mut args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - let dir = args - .first() - .context("target directory not provided")? - .as_ref(); + let dir = args.next().context("target directory not provided")?; let dir = helix_stdx::path::expand_tilde(Path::new(dir)); helix_stdx::env::set_current_working_dir(dir)?; @@ -1104,7 +1031,7 @@ fn change_current_directory( fn show_current_directory( cx: &mut compositor::Context, - _args: &[Cow], + _args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -1117,7 +1044,7 @@ fn show_current_directory( if cwd.exists() { cx.editor.set_status(message); } else { - cx.editor.set_error(format!("{} (deleted)", message)); + cx.editor.set_error(format!("{message} (deleted)")); } Ok(()) } @@ -1125,7 +1052,7 @@ fn show_current_directory( /// Sets the [`Document`]'s encoding.. fn set_encoding( cx: &mut compositor::Context, - args: &[Cow], + mut args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -1133,7 +1060,7 @@ fn set_encoding( } let doc = doc_mut!(cx.editor); - if let Some(label) = args.first() { + if let Some(label) = args.next() { doc.set_encoding(label) } else { let encoding = doc.encoding().name().to_owned(); @@ -1145,7 +1072,7 @@ fn set_encoding( /// Shows info about the character under the primary cursor. fn get_character_info( cx: &mut compositor::Context, - _args: &[Cow], + _args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -1268,11 +1195,7 @@ fn get_character_info( } /// Reload the [`Document`] from its source file. -fn reload( - cx: &mut compositor::Context, - _args: &[Cow], - event: PromptEvent, -) -> anyhow::Result<()> { +fn reload(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } @@ -1291,11 +1214,7 @@ fn reload( Ok(()) } -fn reload_all( - cx: &mut compositor::Context, - _args: &[Cow], - event: PromptEvent, -) -> anyhow::Result<()> { +fn reload_all(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } @@ -1351,11 +1270,7 @@ fn reload_all( } /// Update the [`Document`] if it has been modified. -fn update( - cx: &mut compositor::Context, - args: &[Cow], - event: PromptEvent, -) -> anyhow::Result<()> { +fn update(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } @@ -1370,7 +1285,7 @@ fn update( fn lsp_workspace_command( cx: &mut compositor::Context, - args: &[Cow], + args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -1426,7 +1341,8 @@ fn lsp_workspace_command( }; cx.jobs.callback(callback); } else { - let command = args.join(" "); + let command = args.raw().to_string(); + let matches: Vec<_> = ls_id_commands .filter(|(_ls_id, c)| *c == &command) .collect(); @@ -1460,7 +1376,7 @@ fn lsp_workspace_command( fn lsp_restart( cx: &mut compositor::Context, - _args: &[Cow], + _args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -1506,11 +1422,7 @@ fn lsp_restart( Ok(()) } -fn lsp_stop( - cx: &mut compositor::Context, - _args: &[Cow], - event: PromptEvent, -) -> anyhow::Result<()> { +fn lsp_stop(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } @@ -1537,7 +1449,7 @@ fn lsp_stop( fn tree_sitter_scopes( cx: &mut compositor::Context, - _args: &[Cow], + _args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -1570,7 +1482,7 @@ fn tree_sitter_scopes( fn tree_sitter_highlight_name( cx: &mut compositor::Context, - _args: &[Cow], + _args: Args, event: PromptEvent, ) -> anyhow::Result<()> { fn find_highlight_at_cursor( @@ -1643,81 +1555,50 @@ fn tree_sitter_highlight_name( Ok(()) } -fn vsplit( - cx: &mut compositor::Context, - args: &[Cow], - event: PromptEvent, -) -> anyhow::Result<()> { +fn vsplit(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); - } - - if args.is_empty() { + } else if args.is_empty() { split(cx.editor, Action::VerticalSplit); } else { for arg in args { - cx.editor - .open(&PathBuf::from(arg.as_ref()), Action::VerticalSplit)?; + cx.editor.open(&PathBuf::from(arg), Action::VerticalSplit)?; } } - Ok(()) } -fn hsplit( - cx: &mut compositor::Context, - args: &[Cow], - event: PromptEvent, -) -> anyhow::Result<()> { +fn hsplit(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); - } - - if args.is_empty() { + } else if args.is_empty() { split(cx.editor, Action::HorizontalSplit); } else { for arg in args { cx.editor - .open(&PathBuf::from(arg.as_ref()), Action::HorizontalSplit)?; + .open(&PathBuf::from(arg), Action::HorizontalSplit)?; } } - Ok(()) } -fn vsplit_new( - cx: &mut compositor::Context, - _args: &[Cow], - event: PromptEvent, -) -> anyhow::Result<()> { +fn vsplit_new(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - cx.editor.new_file(Action::VerticalSplit); - Ok(()) } -fn hsplit_new( - cx: &mut compositor::Context, - _args: &[Cow], - event: PromptEvent, -) -> anyhow::Result<()> { +fn hsplit_new(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - cx.editor.new_file(Action::HorizontalSplit); - Ok(()) } -fn debug_eval( - cx: &mut compositor::Context, - args: &[Cow], - event: PromptEvent, -) -> anyhow::Result<()> { +fn debug_eval(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } @@ -1731,9 +1612,10 @@ fn debug_eval( }; // TODO: support no frame_id - let frame_id = debugger.stack_frames[&thread_id][frame].id; - let response = helix_lsp::block_on(debugger.eval(args.join(" "), Some(frame_id)))?; + let expression = args.raw().to_string(); + + let response = helix_lsp::block_on(debugger.eval(expression, Some(frame_id)))?; cx.editor.set_status(response.result); } Ok(()) @@ -1741,47 +1623,33 @@ fn debug_eval( fn debug_start( cx: &mut compositor::Context, - args: &[Cow], + mut args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - - let mut args = args.to_owned(); - let name = match args.len() { - 0 => None, - _ => Some(args.remove(0)), - }; - dap_start_impl(cx, name.as_deref(), None, Some(args)) + dap_start_impl(cx, args.next(), None, Some(args.map(Into::into).collect())) } fn debug_remote( cx: &mut compositor::Context, - args: &[Cow], + mut args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - - let mut args = args.to_owned(); - let address = match args.len() { - 0 => None, - _ => Some(args.remove(0).parse()?), - }; - let name = match args.len() { - 0 => None, - _ => Some(args.remove(0)), - }; - dap_start_impl(cx, name.as_deref(), address, Some(args)) + let address = args.next().map(|addr| addr.parse()).transpose()?; + dap_start_impl( + cx, + args.next(), + address, + Some(args.map(Into::into).collect()), + ) } -fn tutor( - cx: &mut compositor::Context, - _args: &[Cow], - event: PromptEvent, -) -> anyhow::Result<()> { +fn tutor(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } @@ -1805,7 +1673,7 @@ fn abort_goto_line_number_preview(cx: &mut compositor::Context) { fn update_goto_line_number_preview( cx: &mut compositor::Context, - args: &[Cow], + mut args: Args, ) -> anyhow::Result<()> { cx.editor.last_selection.get_or_insert_with(|| { let (view, doc) = current!(cx.editor); @@ -1813,7 +1681,7 @@ fn update_goto_line_number_preview( }); let scrolloff = cx.editor.config().scrolloff; - let line = args[0].parse::()?; + let line = args.next().unwrap().parse::()?; goto_line_without_jumplist(cx.editor, NonZeroUsize::new(line)); let (view, doc) = current!(cx.editor); @@ -1824,7 +1692,7 @@ fn update_goto_line_number_preview( pub(super) fn goto_line_number( cx: &mut compositor::Context, - args: &[Cow], + args: Args, event: PromptEvent, ) -> anyhow::Result<()> { match event { @@ -1860,18 +1728,18 @@ pub(super) fn goto_line_number( // Fetch the current value of a config option and output as status. fn get_option( cx: &mut compositor::Context, - args: &[Cow], + mut args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - if args.len() != 1 { + if args.count() != 1 { anyhow::bail!("Bad arguments. Usage: `:get key`"); } - let key = &args[0].to_lowercase(); + let key = args.next().unwrap().to_lowercase(); let key_error = || anyhow::anyhow!("Unknown key `{}`", key); let config = serde_json::json!(cx.editor.config().deref()); @@ -1886,46 +1754,61 @@ fn get_option( /// example to disable smart case search, use `:set search.smart-case false`. fn set_option( cx: &mut compositor::Context, - args: &[Cow], + mut args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - if args.len() != 2 { - anyhow::bail!("Bad arguments. Usage: `:set key field`"); - } - let (key, arg) = (&args[0].to_lowercase(), &args[1]); + let Some(key) = args.next().map(|arg| arg.to_lowercase()) else { + anyhow::bail!("Bad arguments. Usage: `:set key field`, didn't provide `key`"); + }; - let key_error = || anyhow::anyhow!("Unknown key `{}`", key); - let field_error = |_| anyhow::anyhow!("Could not parse field `{}`", arg); + let field = args.rest(); - let mut config = serde_json::json!(&cx.editor.config().deref()); + if field.is_empty() { + anyhow::bail!("Bad arguments. Usage: `:set key field`, didn't provide `field`"); + } + + let mut config = serde_json::json!(&*cx.editor.config()); let pointer = format!("/{}", key.replace('.', "/")); - let value = config.pointer_mut(&pointer).ok_or_else(key_error)?; + let value = config + .pointer_mut(&pointer) + .ok_or_else(|| anyhow::anyhow!("Unknown key `{key}`"))?; *value = if value.is_string() { // JSON strings require quotes, so we can't .parse() directly - Value::String(arg.to_string()) + Value::String(field.to_string()) } else { - arg.parse().map_err(field_error)? + field + .parse() + .map_err(|err| anyhow::anyhow!("Could not parse field `{field}`: {err}"))? }; - let config = serde_json::from_value(config).map_err(field_error)?; + + let config = serde_json::from_value(config).expect( + "`Config` was already deserialized, serialization is just a 'repacking' and should be valid", + ); cx.editor .config_events .0 .send(ConfigEvent::Update(config))?; + + cx.editor + .set_status(format!("'{key}' is now set to {field}")); + Ok(()) } /// Toggle boolean config option at runtime. Access nested values by dot -/// syntax, for example to toggle smart case search, use `:toggle search.smart- -/// case`. +/// syntax. +/// Example: +/// - `:toggle search.smart-case` (bool) +/// - `:toggle line-number relative absolute` (string) fn toggle_option( cx: &mut compositor::Context, - args: &[Cow], + mut args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -1935,73 +1818,94 @@ fn toggle_option( if args.is_empty() { anyhow::bail!("Bad arguments. Usage: `:toggle key [values]?`"); } - let key = &args[0].to_lowercase(); + let key = args.next().unwrap().to_lowercase(); - let key_error = || anyhow::anyhow!("Unknown key `{}`", key); - - let mut config = serde_json::json!(&cx.editor.config().deref()); + let mut config = serde_json::json!(&*cx.editor.config()); let pointer = format!("/{}", key.replace('.', "/")); - let value = config.pointer_mut(&pointer).ok_or_else(key_error)?; + let value = config + .pointer_mut(&pointer) + .ok_or_else(|| anyhow::anyhow!("Unknown key `{}`", key))?; *value = match value { Value::Bool(ref value) => { ensure!( - args.len() == 1, + args.next().is_none(), "Bad arguments. For boolean configurations use: `:toggle key`" ); Value::Bool(!value) } Value::String(ref value) => { ensure!( - args.len() > 2, + args.count() >= 2, "Bad arguments. For string configurations use: `:toggle key val1 val2 ...`", ); Value::String( - args[1..] - .iter() - .skip_while(|e| *e != value) + args.skip_while(|e| *e != value) .nth(1) - .unwrap_or_else(|| &args[1]) + .unwrap_or_else(|| args.nth(1).unwrap()) .to_string(), ) } Value::Number(ref value) => { ensure!( - args.len() > 2, + args.count() >= 2, "Bad arguments. For number configurations use: `:toggle key val1 val2 ...`", ); + let value = value.to_string(); + Value::Number( - args[1..] - .iter() - .skip_while(|&e| value.to_string() != *e.to_string()) + args.skip_while(|e| *e != value) .nth(1) - .unwrap_or_else(|| &args[1]) + .unwrap_or_else(|| args.nth(1).unwrap()) .parse()?, ) } - Value::Null | Value::Object(_) | Value::Array(_) => { + Value::Array(value) => { + let mut lists = serde_json::Deserializer::from_str(args.rest()).into_iter::(); + + let (Some(first), Some(second)) = + (lists.next().transpose()?, lists.next().transpose()?) + else { + anyhow::bail!( + "Bad arguments. For list configurations use: `:toggle key [...] [...]`", + ) + }; + + match (&first, &second) { + (Value::Array(list), Value::Array(_)) => { + if list == value { + second + } else { + first + } + } + _ => anyhow::bail!("values must be lists"), + } + } + Value::Null | Value::Object(_) => { anyhow::bail!("Configuration {key} does not support toggle yet") } }; let status = format!("'{key}' is now set to {value}"); - let config = serde_json::from_value(config) - .map_err(|err| anyhow::anyhow!("Cannot parse `{:?}`, {}", &args, err))?; + let config = serde_json::from_value(config)?; cx.editor .config_events .0 .send(ConfigEvent::Update(config))?; + cx.editor.set_status(status); + Ok(()) } /// Change the language of the current buffer at runtime. fn language( cx: &mut compositor::Context, - args: &[Cow], + mut args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -2010,21 +1914,22 @@ fn language( if args.is_empty() { let doc = doc!(cx.editor); - let language = &doc.language_name().unwrap_or(DEFAULT_LANGUAGE_NAME); + let language = doc.language_name().unwrap_or(DEFAULT_LANGUAGE_NAME); cx.editor.set_status(language.to_string()); return Ok(()); } - if args.len() != 1 { + if args.count() != 1 { anyhow::bail!("Bad arguments. Usage: `:set-language language`"); } let doc = doc_mut!(cx.editor); - if args[0] == DEFAULT_LANGUAGE_NAME { - doc.set_language(None, None) + let language_id = args.next().unwrap(); + if language_id == DEFAULT_LANGUAGE_NAME { + doc.set_language(None, None); } else { - doc.set_language_by_language_id(&args[0], cx.editor.syn_loader.clone())?; + doc.set_language_by_language_id(language_id, cx.editor.syn_loader.clone())?; } doc.detect_indent_and_line_ending(); @@ -2037,31 +1942,25 @@ fn language( Ok(()) } -fn sort(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> anyhow::Result<()> { +fn sort(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - sort_impl(cx, args, false) } fn sort_reverse( cx: &mut compositor::Context, - args: &[Cow], + args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - sort_impl(cx, args, true) } -fn sort_impl( - cx: &mut compositor::Context, - _args: &[Cow], - reverse: bool, -) -> anyhow::Result<()> { +fn sort_impl(cx: &mut compositor::Context, _args: Args, reverse: bool) -> anyhow::Result<()> { let scrolloff = cx.editor.config().scrolloff; let (view, doc) = current!(cx.editor); let text = doc.text().slice(..); @@ -2093,11 +1992,7 @@ fn sort_impl( Ok(()) } -fn reflow( - cx: &mut compositor::Context, - args: &[Cow], - event: PromptEvent, -) -> anyhow::Result<()> { +fn reflow(cx: &mut compositor::Context, mut args: Args, event: PromptEvent) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } @@ -2111,7 +2006,7 @@ fn reflow( // - The configured text-width for this language in languages.toml // - The configured text-width in the config.toml let text_width: usize = args - .first() + .next() .map(|num| num.parse::()) .transpose()? .or_else(|| doc.language_config().and_then(|config| config.text_width)) @@ -2136,7 +2031,7 @@ fn reflow( fn tree_sitter_subtree( cx: &mut compositor::Context, - _args: &[Cow], + _args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -2175,13 +2070,12 @@ fn tree_sitter_subtree( fn open_config( cx: &mut compositor::Context, - _args: &[Cow], + _args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - cx.editor .open(&helix_loader::config_file(), Action::Replace)?; Ok(()) @@ -2189,34 +2083,28 @@ fn open_config( fn open_workspace_config( cx: &mut compositor::Context, - _args: &[Cow], + _args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - cx.editor .open(&helix_loader::workspace_config_file(), Action::Replace)?; Ok(()) } -fn open_log( - cx: &mut compositor::Context, - _args: &[Cow], - event: PromptEvent, -) -> anyhow::Result<()> { +fn open_log(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - cx.editor.open(&helix_loader::log_file(), Action::Replace)?; Ok(()) } fn refresh_config( cx: &mut compositor::Context, - _args: &[Cow], + _args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -2229,62 +2117,58 @@ fn refresh_config( fn append_output( cx: &mut compositor::Context, - args: &[Cow], + args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - ensure!(!args.is_empty(), "Shell command required"); - shell(cx, &args.join(" "), &ShellBehavior::Append); + let cmd = helix_core::shellwords::unescape(args.raw()); + shell(cx, &cmd, &ShellBehavior::Append); Ok(()) } fn insert_output( cx: &mut compositor::Context, - args: &[Cow], + args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - ensure!(!args.is_empty(), "Shell command required"); - shell(cx, &args.join(" "), &ShellBehavior::Insert); + let cmd = helix_core::shellwords::unescape(args.raw()); + shell(cx, &cmd, &ShellBehavior::Insert); Ok(()) } -fn pipe_to( - cx: &mut compositor::Context, - args: &[Cow], - event: PromptEvent, -) -> anyhow::Result<()> { +fn pipe_to(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> { pipe_impl(cx, args, event, &ShellBehavior::Ignore) } -fn pipe(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> anyhow::Result<()> { +fn pipe(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> { pipe_impl(cx, args, event, &ShellBehavior::Replace) } fn pipe_impl( cx: &mut compositor::Context, - args: &[Cow], + args: Args, event: PromptEvent, behavior: &ShellBehavior, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - ensure!(!args.is_empty(), "Shell command required"); - shell(cx, &args.join(" "), behavior); + let cmd = helix_core::shellwords::unescape(args.raw()); + shell(cx, &cmd, behavior); Ok(()) } fn run_shell_command( cx: &mut compositor::Context, - args: &[Cow], + args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -2292,7 +2176,8 @@ fn run_shell_command( } let shell = cx.editor.config().shell.clone(); - let args = args.join(" "); + + let args = helix_core::shellwords::unescape(args.raw()).into_owned(); let callback = async move { let output = shell_impl_async(&shell, &args, None).await?; @@ -2320,7 +2205,7 @@ fn run_shell_command( fn reset_diff_change( cx: &mut compositor::Context, - args: &[Cow], + args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -2328,10 +2213,8 @@ fn reset_diff_change( } ensure!(args.is_empty(), ":reset-diff-change takes no arguments"); - let editor = &mut cx.editor; - let scrolloff = editor.config().scrolloff; - - let (view, doc) = current!(editor); + let scrolloff = cx.editor.config().scrolloff; + let (view, doc) = current!(cx.editor); let Some(handle) = doc.diff_handle() else { bail!("Diff is not available in the current buffer") }; @@ -2373,40 +2256,42 @@ fn reset_diff_change( fn clear_register( cx: &mut compositor::Context, - args: &[Cow], + mut args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - ensure!(args.len() <= 1, ":clear-register takes at most 1 argument"); + ensure!( + args.count() <= 1, + ":clear-register takes at most 1 argument" + ); + if args.is_empty() { cx.editor.registers.clear(); cx.editor.set_status("All registers cleared"); return Ok(()); } + let register = args.next().unwrap(); + ensure!( - args[0].chars().count() == 1, - format!("Invalid register {}", args[0]) + register.chars().count() == 1, + format!("Invalid register {register}") ); - let register = args[0].chars().next().unwrap_or_default(); + + let register = register.chars().next().unwrap_or_default(); if cx.editor.registers.remove(register) { - cx.editor - .set_status(format!("Register {} cleared", register)); + cx.editor.set_status(format!("Register {register} cleared")); } else { cx.editor - .set_error(format!("Register {} not found", register)); + .set_error(format!("Register {register} not found")); } Ok(()) } -fn redraw( - cx: &mut compositor::Context, - _args: &[Cow], - event: PromptEvent, -) -> anyhow::Result<()> { +fn redraw(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } @@ -2427,20 +2312,22 @@ fn redraw( fn move_buffer( cx: &mut compositor::Context, - args: &[Cow], + mut args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - ensure!(args.len() == 1, format!(":move takes one argument")); - let doc = doc!(cx.editor); - let old_path = doc + ensure!(args.count() == 1, format!(":move takes one argument")); + + let old_path = doc!(cx.editor) .path() .context("Scratch buffer cannot be moved. Use :write instead")? .clone(); - let new_path = args.first().unwrap().to_string(); + + let new_path = args.next().unwrap(); + if let Err(err) = cx.editor.move_path(&old_path, new_path.as_ref()) { bail!("Could not move file: {err}"); } @@ -2449,14 +2336,14 @@ fn move_buffer( fn yank_diagnostic( cx: &mut compositor::Context, - args: &[Cow], + mut args: Args, event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - let reg = match args.first() { + let reg = match args.next() { Some(s) => { ensure!(s.chars().count() == 1, format!("Invalid register {s}")); s.chars().next().unwrap() @@ -2487,7 +2374,7 @@ fn yank_diagnostic( Ok(()) } -fn read(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> anyhow::Result<()> { +fn read(cx: &mut compositor::Context, mut args: Args, event: PromptEvent) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } @@ -2496,10 +2383,10 @@ fn read(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> let (view, doc) = current!(cx.editor); ensure!(!args.is_empty(), "file name is expected"); - ensure!(args.len() == 1, "only the file name is expected"); + ensure!(args.count() == 1, "only the file name is expected"); - let filename = args.first().unwrap(); - let path = PathBuf::from(filename.to_string()); + let filename = args.next().unwrap(); + let path = PathBuf::from(filename); ensure!( path.exists() && path.is_file(), "path is not a file: {:?}", @@ -3161,9 +3048,11 @@ pub(super) fn command_mode(cx: &mut Context) { Some(':'), |editor: &Editor, input: &str| { let shellwords = Shellwords::from(input); - let words = shellwords.words(); + let command = shellwords.command(); - if words.is_empty() || (words.len() == 1 && !shellwords.ends_with_whitespace()) { + if command.is_empty() + || (shellwords.args().next().is_none() && !shellwords.ends_with_whitespace()) + { fuzzy_match( input, TYPABLE_COMMAND_LIST.iter().map(|command| command.name), @@ -3175,68 +3064,61 @@ pub(super) fn command_mode(cx: &mut Context) { } else { // Otherwise, use the command's completer and the last shellword // as completion input. - let (word, word_len) = if words.len() == 1 || shellwords.ends_with_whitespace() { - (&Cow::Borrowed(""), 0) - } else { - (words.last().unwrap(), words.last().unwrap().len()) - }; - - let argument_number = argument_number_of(&shellwords); - - if let Some(completer) = TYPABLE_COMMAND_MAP - .get(&words[0] as &str) - .map(|tc| tc.completer_for_argument_number(argument_number)) - { - completer(editor, word) - .into_iter() - .map(|(range, file)| { - // TEST: Might not need to escape with new changes? - // let file = shellwords::escape(&file); - - // offset ranges to input - let offset = input.len() - word_len; - let range = (range.start + offset)..; - (range, file) - }) - .collect() - } else { - Vec::new() - } + let (word, len) = shellwords + .args() + .last() + .map_or(("", 0), |last| (last, last.len())); + + TYPABLE_COMMAND_MAP + .get(command) + .map(|tc| tc.completer_for_argument_number(argument_number_of(&shellwords))) + .map_or_else(Vec::new, |completer| { + completer(editor, word) + .into_iter() + .map(|(range, file)| { + let file = shellwords::escape(file); + + // offset ranges to input + let offset = input.len() - len; + let range = (range.start + offset)..; + (range, file) + }) + .collect() + }) } }, // completion move |cx: &mut compositor::Context, input: &str, event: PromptEvent| { - let parts = input.split_whitespace().collect::>(); - if parts.is_empty() { + let shellwords = Shellwords::from(input); + let command = shellwords.command(); + + if command.is_empty() { return; } - // If command is numeric, interpret as line number and go there. - if parts.len() == 1 && parts[0].parse::().ok().is_some() { - if let Err(e) = typed::goto_line_number(cx, &[Cow::from(parts[0])], event) { - cx.editor.set_error(format!("{}", e)); + // If input is `:NUMBER`, interpret as line number and go there. + if command.parse::().is_ok() { + if let Err(err) = typed::goto_line_number(cx, Args::from(command), event) { + cx.editor.set_error(format!("{err}")); } return; } // Handle typable commands - if let Some(cmd) = typed::TYPABLE_COMMAND_MAP.get(parts[0]) { - let shellwords = Shellwords::from(input); - let args = shellwords.words(); - - if let Err(e) = (cmd.fun)(cx, &args[1..], event) { - cx.editor.set_error(format!("{}", e)); + if let Some(cmd) = typed::TYPABLE_COMMAND_MAP.get(command) { + if let Err(err) = (cmd.fun)(cx, shellwords.args(), event) { + cx.editor.set_error(format!("{err}")); } } else if event == PromptEvent::Validate { - cx.editor - .set_error(format!("no such command: '{}'", parts[0])); + cx.editor.set_error(format!("no such command: '{command}'")); } }, ); + prompt.doc_fn = Box::new(|input: &str| { - let part = input.split(' ').next().unwrap_or_default(); + let shellwords = Shellwords::from(input); if let Some(typed::TypableCommand { doc, aliases, .. }) = - typed::TYPABLE_COMMAND_MAP.get(part) + typed::TYPABLE_COMMAND_MAP.get(shellwords.command()) { if aliases.is_empty() { return Some((*doc).into()); @@ -3253,11 +3135,10 @@ pub(super) fn command_mode(cx: &mut Context) { } fn argument_number_of(shellwords: &Shellwords) -> usize { - if shellwords.ends_with_whitespace() { - shellwords.words().len().saturating_sub(1) - } else { - shellwords.words().len().saturating_sub(2) - } + shellwords + .args() + .count() + .saturating_sub(1 - usize::from(shellwords.ends_with_whitespace())) } #[test] diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 020ecaf40..aa9cafd31 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -597,18 +597,14 @@ mod tests { let expectation = KeyTrie::Node(KeyTrieNode::new( "", hashmap! { - key => KeyTrie::Sequence(vec!{ + key => KeyTrie::Sequence(vec![ MappableCommand::select_all, MappableCommand::Typable { name: "pipe".to_string(), - args: vec!{ - "sed".to_string(), - "-E".to_string(), - "'s/\\s+$//g'".to_string() - }, - doc: "".to_string(), + args: String::from("sed -E 's/\\s+$//g'"), + doc: String::new(), }, - }) + ]) }, vec![key], ));