Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-term/src/commands/typed.rs')
-rw-r--r--helix-term/src/commands/typed.rs1541
1 files changed, 1025 insertions, 516 deletions
diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs
index 3953457c..1d57930c 100644
--- a/helix-term/src/commands/typed.rs
+++ b/helix-term/src/commands/typed.rs
@@ -1,17 +1,19 @@
use std::fmt::Write;
use std::io::BufReader;
-use std::ops::Deref;
+use std::ops::{self, Deref};
use crate::job::Job;
use super::*;
+use helix_core::command_line::{Args, Flag, Signature, Token, TokenKind};
use helix_core::fuzzy::fuzzy_match;
use helix_core::indent::MAX_INDENT;
-use helix_core::{line_ending, shellwords::Shellwords};
+use helix_core::line_ending;
use helix_stdx::path::home_dir;
use helix_view::document::{read_to_string, DEFAULT_LANGUAGE_NAME};
use helix_view::editor::{CloseError, ConfigEvent};
+use helix_view::expansion;
use serde_json::Value;
use ui::completers::{self, Completer};
@@ -21,22 +23,23 @@ pub struct TypableCommand {
pub aliases: &'static [&'static str],
pub doc: &'static str,
// params, flags, helper, completer
- pub fun: fn(&mut compositor::Context, &[Cow<str>], 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,
+ pub completer: CommandCompleter,
+ pub signature: Signature,
}
impl TypableCommand {
fn completer_for_argument_number(&self, n: usize) -> &Completer {
- match self.signature.positional_args.get(n) {
+ match self.completer.positional_args.get(n) {
Some(completer) => completer,
- _ => &self.signature.var_args,
+ _ => &self.completer.var_args,
}
}
}
#[derive(Clone)]
-pub struct CommandSignature {
+pub struct CommandCompleter {
// Arguments with specific completion methods based on their position.
positional_args: &'static [Completer],
@@ -44,7 +47,7 @@ pub struct CommandSignature {
var_args: Completer,
}
-impl CommandSignature {
+impl CommandCompleter {
const fn none() -> Self {
Self {
positional_args: &[],
@@ -67,15 +70,13 @@ impl CommandSignature {
}
}
-fn quit(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) -> anyhow::Result<()> {
+fn quit(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> {
log::debug!("quitting...");
if event != PromptEvent::Validate {
return Ok(());
}
- ensure!(args.is_empty(), ":quit takes no arguments");
-
// last view and we have unsaved changes
if cx.editor.tree.views().count() == 1 {
buffers_remaining_impl(cx.editor)?
@@ -87,31 +88,24 @@ fn quit(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) ->
Ok(())
}
-fn force_quit(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- event: PromptEvent,
-) -> anyhow::Result<()> {
+fn force_quit(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
- ensure!(args.is_empty(), ":quit! takes no arguments");
-
cx.block_try_flush_writes()?;
cx.editor.close(view!(cx.editor).id);
Ok(())
}
-fn open(cx: &mut compositor::Context, args: &[Cow<str>], 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");
for arg in args {
- let (path, pos) = args::parse_file(arg);
+ let (path, pos) = crate::args::parse_file(&arg);
let path = helix_stdx::path::expand_tilde(path);
// If the path is a directory, open a file picker on that directory and update the status
// message
@@ -175,7 +169,7 @@ fn buffer_close_by_ids_impl(
Ok(())
}
-fn buffer_gather_paths_impl(editor: &mut Editor, args: &[Cow<str>]) -> Vec<DocumentId> {
+fn buffer_gather_paths_impl(editor: &mut Editor, args: Args) -> Vec<DocumentId> {
// No arguments implies current document
if args.is_empty() {
let doc_id = view!(editor).doc;
@@ -212,7 +206,7 @@ fn buffer_gather_paths_impl(editor: &mut Editor, args: &[Cow<str>]) -> Vec<Docum
fn buffer_close(
cx: &mut compositor::Context,
- args: &[Cow<str>],
+ args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -225,7 +219,7 @@ fn buffer_close(
fn force_buffer_close(
cx: &mut compositor::Context,
- args: &[Cow<str>],
+ args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -247,7 +241,7 @@ fn buffer_gather_others_impl(editor: &mut Editor) -> Vec<DocumentId> {
fn buffer_close_others(
cx: &mut compositor::Context,
- _args: &[Cow<str>],
+ _args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -260,7 +254,7 @@ fn buffer_close_others(
fn force_buffer_close_others(
cx: &mut compositor::Context,
- _args: &[Cow<str>],
+ _args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -277,7 +271,7 @@ fn buffer_gather_all_impl(editor: &mut Editor) -> Vec<DocumentId> {
fn buffer_close_all(
cx: &mut compositor::Context,
- _args: &[Cow<str>],
+ _args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -290,7 +284,7 @@ fn buffer_close_all(
fn force_buffer_close_all(
cx: &mut compositor::Context,
- _args: &[Cow<str>],
+ _args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -303,7 +297,7 @@ fn force_buffer_close_all(
fn buffer_next(
cx: &mut compositor::Context,
- _args: &[Cow<str>],
+ _args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -316,7 +310,7 @@ fn buffer_next(
fn buffer_previous(
cx: &mut compositor::Context,
- _args: &[Cow<str>],
+ _args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -327,15 +321,10 @@ fn buffer_previous(
Ok(())
}
-fn write_impl(
- cx: &mut compositor::Context,
- path: Option<&Cow<str>>,
- 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);
@@ -377,11 +366,7 @@ fn insert_final_newline(doc: &mut Document, view_id: ViewId) {
}
}
-fn write(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- event: PromptEvent,
-) -> anyhow::Result<()> {
+fn write(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
@@ -389,11 +374,7 @@ fn write(
write_impl(cx, args.first(), false)
}
-fn force_write(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- event: PromptEvent,
-) -> anyhow::Result<()> {
+fn force_write(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
@@ -403,7 +384,7 @@ fn force_write(
fn write_buffer_close(
cx: &mut compositor::Context,
- args: &[Cow<str>],
+ args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -418,7 +399,7 @@ fn write_buffer_close(
fn force_write_buffer_close(
cx: &mut compositor::Context,
- args: &[Cow<str>],
+ args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -431,11 +412,7 @@ fn force_write_buffer_close(
buffer_close_by_ids_impl(cx, &document_ids, false)
}
-fn new_file(
- cx: &mut compositor::Context,
- _args: &[Cow<str>],
- event: PromptEvent,
-) -> anyhow::Result<()> {
+fn new_file(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
@@ -445,11 +422,7 @@ fn new_file(
Ok(())
}
-fn format(
- cx: &mut compositor::Context,
- _args: &[Cow<str>],
- event: PromptEvent,
-) -> anyhow::Result<()> {
+fn format(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
@@ -466,7 +439,7 @@ fn format(
fn set_indent_style(
cx: &mut compositor::Context,
- args: &[Cow<str>],
+ args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -489,7 +462,7 @@ fn set_indent_style(
// Attempt to parse argument as an indent style.
let style = match args.first() {
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::<u8>()
.ok()
@@ -508,7 +481,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<str>],
+ args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -578,11 +551,7 @@ fn set_line_ending(
Ok(())
}
-fn earlier(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- event: PromptEvent,
-) -> anyhow::Result<()> {
+fn earlier(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
@@ -598,11 +567,7 @@ fn earlier(
Ok(())
}
-fn later(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- event: PromptEvent,
-) -> anyhow::Result<()> {
+fn later(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
@@ -617,23 +582,19 @@ fn later(
Ok(())
}
-fn write_quit(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- event: PromptEvent,
-) -> anyhow::Result<()> {
+fn write_quit(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
write_impl(cx, args.first(), false)?;
cx.block_try_flush_writes()?;
- quit(cx, &[], event)
+ quit(cx, Args::default(), event)
}
fn force_write_quit(
cx: &mut compositor::Context,
- args: &[Cow<str>],
+ args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -642,7 +603,7 @@ fn force_write_quit(
write_impl(cx, args.first(), true)?;
cx.block_try_flush_writes()?;
- force_quit(cx, &[], event)
+ force_quit(cx, Args::default(), event)
}
/// Results in an error if there are modified buffers remaining and sets editor
@@ -755,11 +716,7 @@ pub fn write_all_impl(
Ok(())
}
-fn write_all(
- cx: &mut compositor::Context,
- _args: &[Cow<str>],
- event: PromptEvent,
-) -> anyhow::Result<()> {
+fn write_all(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
@@ -776,7 +733,7 @@ fn write_all(
fn force_write_all(
cx: &mut compositor::Context,
- _args: &[Cow<str>],
+ _args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -795,7 +752,7 @@ fn force_write_all(
fn write_all_quit(
cx: &mut compositor::Context,
- _args: &[Cow<str>],
+ _args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -814,7 +771,7 @@ fn write_all_quit(
fn force_write_all_quit(
cx: &mut compositor::Context,
- _args: &[Cow<str>],
+ _args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -846,11 +803,7 @@ fn quit_all_impl(cx: &mut compositor::Context, force: bool) -> anyhow::Result<()
Ok(())
}
-fn quit_all(
- cx: &mut compositor::Context,
- _args: &[Cow<str>],
- event: PromptEvent,
-) -> anyhow::Result<()> {
+fn quit_all(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
@@ -860,7 +813,7 @@ fn quit_all(
fn force_quit_all(
cx: &mut compositor::Context,
- _args: &[Cow<str>],
+ _args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -870,11 +823,7 @@ fn force_quit_all(
quit_all_impl(cx, true)
}
-fn cquit(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- event: PromptEvent,
-) -> anyhow::Result<()> {
+fn cquit(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
@@ -888,11 +837,7 @@ fn cquit(
quit_all_impl(cx, false)
}
-fn force_cquit(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- event: PromptEvent,
-) -> anyhow::Result<()> {
+fn force_cquit(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
@@ -906,11 +851,7 @@ fn force_cquit(
quit_all_impl(cx, true)
}
-fn theme(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- event: PromptEvent,
-) -> anyhow::Result<()> {
+fn theme(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> {
let true_color = cx.editor.config.load().true_color || crate::true_color();
match event {
PromptEvent::Abort => {
@@ -953,7 +894,7 @@ fn theme(
fn yank_main_selection_to_clipboard(
cx: &mut compositor::Context,
- _args: &[Cow<str>],
+ _args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -964,17 +905,11 @@ fn yank_main_selection_to_clipboard(
Ok(())
}
-fn yank_joined(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- 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);
@@ -988,7 +923,7 @@ fn yank_joined(
fn yank_joined_to_clipboard(
cx: &mut compositor::Context,
- args: &[Cow<str>],
+ args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -1004,7 +939,7 @@ fn yank_joined_to_clipboard(
fn yank_main_selection_to_primary_clipboard(
cx: &mut compositor::Context,
- _args: &[Cow<str>],
+ _args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -1017,7 +952,7 @@ fn yank_main_selection_to_primary_clipboard(
fn yank_joined_to_primary_clipboard(
cx: &mut compositor::Context,
- args: &[Cow<str>],
+ args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -1033,7 +968,7 @@ fn yank_joined_to_primary_clipboard(
fn paste_clipboard_after(
cx: &mut compositor::Context,
- _args: &[Cow<str>],
+ _args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -1046,7 +981,7 @@ fn paste_clipboard_after(
fn paste_clipboard_before(
cx: &mut compositor::Context,
- _args: &[Cow<str>],
+ _args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -1059,7 +994,7 @@ fn paste_clipboard_before(
fn paste_primary_clipboard_after(
cx: &mut compositor::Context,
- _args: &[Cow<str>],
+ _args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -1072,7 +1007,7 @@ fn paste_primary_clipboard_after(
fn paste_primary_clipboard_before(
cx: &mut compositor::Context,
- _args: &[Cow<str>],
+ _args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -1085,7 +1020,7 @@ fn paste_primary_clipboard_before(
fn replace_selections_with_clipboard(
cx: &mut compositor::Context,
- _args: &[Cow<str>],
+ _args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -1098,7 +1033,7 @@ fn replace_selections_with_clipboard(
fn replace_selections_with_primary_clipboard(
cx: &mut compositor::Context,
- _args: &[Cow<str>],
+ _args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -1111,7 +1046,7 @@ fn replace_selections_with_primary_clipboard(
fn show_clipboard_provider(
cx: &mut compositor::Context,
- _args: &[Cow<str>],
+ _args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -1125,7 +1060,7 @@ fn show_clipboard_provider(
fn change_current_directory(
cx: &mut compositor::Context,
- args: &[Cow<str>],
+ args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -1159,7 +1094,7 @@ fn change_current_directory(
fn show_current_directory(
cx: &mut compositor::Context,
- _args: &[Cow<str>],
+ _args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -1180,7 +1115,7 @@ fn show_current_directory(
/// Sets the [`Document`]'s encoding..
fn set_encoding(
cx: &mut compositor::Context,
- args: &[Cow<str>],
+ args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -1200,7 +1135,7 @@ fn set_encoding(
/// Shows info about the character under the primary cursor.
fn get_character_info(
cx: &mut compositor::Context,
- _args: &[Cow<str>],
+ _args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -1323,11 +1258,7 @@ fn get_character_info(
}
/// Reload the [`Document`] from its source file.
-fn reload(
- cx: &mut compositor::Context,
- _args: &[Cow<str>],
- event: PromptEvent,
-) -> anyhow::Result<()> {
+fn reload(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
@@ -1346,11 +1277,7 @@ fn reload(
Ok(())
}
-fn reload_all(
- cx: &mut compositor::Context,
- _args: &[Cow<str>],
- event: PromptEvent,
-) -> anyhow::Result<()> {
+fn reload_all(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
@@ -1406,11 +1333,7 @@ fn reload_all(
}
/// Update the [`Document`] if it has been modified.
-fn update(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- event: PromptEvent,
-) -> anyhow::Result<()> {
+fn update(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
@@ -1425,7 +1348,7 @@ fn update(
fn lsp_workspace_command(
cx: &mut compositor::Context,
- args: &[Cow<str>],
+ args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -1481,19 +1404,30 @@ fn lsp_workspace_command(
};
cx.jobs.callback(callback);
} else {
- let command = args.join(" ");
+ let command = args[0].to_string();
let matches: Vec<_> = ls_id_commands
.filter(|(_ls_id, c)| *c == &command)
.collect();
match matches.as_slice() {
[(ls_id, _command)] => {
+ let arguments = args
+ .get(1)
+ .map(|rest| {
+ serde_json::Deserializer::from_str(rest)
+ .into_iter()
+ .collect::<Result<Vec<Value>, _>>()
+ .map_err(|err| anyhow!("failed to parse arguments: {err}"))
+ })
+ .transpose()?
+ .filter(|args| !args.is_empty());
+
execute_lsp_command(
cx.editor,
*ls_id,
helix_lsp::lsp::Command {
title: command.clone(),
- arguments: None,
+ arguments,
command,
},
);
@@ -1513,11 +1447,7 @@ fn lsp_workspace_command(
Ok(())
}
-fn lsp_restart(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- event: PromptEvent,
-) -> anyhow::Result<()> {
+fn lsp_restart(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
@@ -1602,11 +1532,7 @@ fn lsp_restart(
}
}
-fn lsp_stop(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- event: PromptEvent,
-) -> anyhow::Result<()> {
+fn lsp_stop(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
@@ -1647,7 +1573,7 @@ fn lsp_stop(
fn tree_sitter_scopes(
cx: &mut compositor::Context,
- _args: &[Cow<str>],
+ _args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -1680,7 +1606,7 @@ fn tree_sitter_scopes(
fn tree_sitter_highlight_name(
cx: &mut compositor::Context,
- _args: &[Cow<str>],
+ _args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
fn find_highlight_at_cursor(
@@ -1753,11 +1679,7 @@ fn tree_sitter_highlight_name(
Ok(())
}
-fn vsplit(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- event: PromptEvent,
-) -> anyhow::Result<()> {
+fn vsplit(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
@@ -1774,11 +1696,7 @@ fn vsplit(
Ok(())
}
-fn hsplit(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- event: PromptEvent,
-) -> anyhow::Result<()> {
+fn hsplit(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
@@ -1795,11 +1713,7 @@ fn hsplit(
Ok(())
}
-fn vsplit_new(
- cx: &mut compositor::Context,
- _args: &[Cow<str>],
- event: PromptEvent,
-) -> anyhow::Result<()> {
+fn vsplit_new(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
@@ -1809,11 +1723,7 @@ fn vsplit_new(
Ok(())
}
-fn hsplit_new(
- cx: &mut compositor::Context,
- _args: &[Cow<str>],
- event: PromptEvent,
-) -> anyhow::Result<()> {
+fn hsplit_new(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
@@ -1823,11 +1733,7 @@ fn hsplit_new(
Ok(())
}
-fn debug_eval(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- event: PromptEvent,
-) -> anyhow::Result<()> {
+fn debug_eval(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
@@ -1849,16 +1755,12 @@ fn debug_eval(
Ok(())
}
-fn debug_start(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- event: PromptEvent,
-) -> anyhow::Result<()> {
+fn debug_start(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
- let mut args = args.to_owned();
+ let mut args: Vec<_> = args.into_iter().collect();
let name = match args.len() {
0 => None,
_ => Some(args.remove(0)),
@@ -1868,14 +1770,14 @@ fn debug_start(
fn debug_remote(
cx: &mut compositor::Context,
- args: &[Cow<str>],
+ args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
- let mut args = args.to_owned();
+ let mut args: Vec<_> = args.into_iter().collect();
let address = match args.len() {
0 => None,
_ => Some(args.remove(0).parse()?),
@@ -1887,11 +1789,7 @@ fn debug_remote(
dap_start_impl(cx, name.as_deref(), address, Some(args))
}
-fn tutor(
- cx: &mut compositor::Context,
- _args: &[Cow<str>],
- event: PromptEvent,
-) -> anyhow::Result<()> {
+fn tutor(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
@@ -1913,10 +1811,7 @@ fn abort_goto_line_number_preview(cx: &mut compositor::Context) {
}
}
-fn update_goto_line_number_preview(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
-) -> anyhow::Result<()> {
+fn update_goto_line_number_preview(cx: &mut compositor::Context, args: Args) -> anyhow::Result<()> {
cx.editor.last_selection.get_or_insert_with(|| {
let (view, doc) = current!(cx.editor);
doc.selection(view.id).clone()
@@ -1934,14 +1829,12 @@ fn update_goto_line_number_preview(
pub(super) fn goto_line_number(
cx: &mut compositor::Context,
- args: &[Cow<str>],
+ args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
match event {
PromptEvent::Abort => abort_goto_line_number_preview(cx),
PromptEvent::Validate => {
- ensure!(!args.is_empty(), "Line number required");
-
// If we are invoked directly via a keybinding, Validate is
// sent without any prior Update events. Ensure the cursor
// is moved to the appropriate location.
@@ -1968,19 +1861,11 @@ 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<str>],
- event: PromptEvent,
-) -> anyhow::Result<()> {
+fn get_option(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
- if args.len() != 1 {
- anyhow::bail!("Bad arguments. Usage: `:get key`");
- }
-
let key = &args[0].to_lowercase();
let key_error = || anyhow::anyhow!("Unknown key `{}`", key);
@@ -1994,19 +1879,12 @@ fn get_option(
/// Change config at runtime. Access nested values by dot syntax, for
/// example to disable smart case search, use `:set search.smart-case false`.
-fn set_option(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- event: PromptEvent,
-) -> anyhow::Result<()> {
+fn set_option(cx: &mut compositor::Context, 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 (key, arg) = (&args[0].to_lowercase(), args[1].trim());
let key_error = || anyhow::anyhow!("Unknown key `{}`", key);
let field_error = |_| anyhow::anyhow!("Could not parse field `{}`", arg);
@@ -2035,16 +1913,13 @@ fn set_option(
/// case`.
fn toggle_option(
cx: &mut compositor::Context,
- args: &[Cow<str>],
+ args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
- if args.is_empty() {
- anyhow::bail!("Bad arguments. Usage: `:toggle key [values]?`");
- }
let key = &args[0].to_lowercase();
let key_error = || anyhow::anyhow!("Unknown key `{}`", key);
@@ -2057,48 +1932,69 @@ fn toggle_option(
Value::Bool(ref value) => {
ensure!(
args.len() == 1,
- "Bad arguments. For boolean configurations use: `:toggle key`"
+ "Bad arguments. For boolean configurations use: `:toggle {key}`"
);
Value::Bool(!value)
}
Value::String(ref value) => {
ensure!(
- args.len() > 2,
- "Bad arguments. For string configurations use: `:toggle key val1 val2 ...`",
+ args.len() == 2,
+ "Bad arguments. For string configurations use: `:toggle {key} val1 val2 ...`",
);
+ // For string values, parse the input according to normal command line rules.
+ let values: Vec<_> = command_line::Tokenizer::new(&args[1], true)
+ .map(|res| res.map(|token| token.content))
+ .collect::<Result<_, _>>()
+ .map_err(|err| anyhow!("failed to parse values: {err}"))?;
Value::String(
- args[1..]
+ values
.iter()
.skip_while(|e| *e != value)
.nth(1)
- .unwrap_or_else(|| &args[1])
+ .map(AsRef::as_ref)
+ .unwrap_or_else(|| &values[0])
.to_string(),
)
}
- Value::Number(ref value) => {
+ Value::Null => bail!("Configuration {key} cannot be toggled"),
+ Value::Number(_) | Value::Array(_) | Value::Object(_) => {
ensure!(
- args.len() > 2,
- "Bad arguments. For number configurations use: `:toggle key val1 val2 ...`",
+ args.len() == 2,
+ "Bad arguments. For {kind} configurations use: `:toggle {key} val1 val2 ...`",
+ kind = match value {
+ Value::Number(_) => "number",
+ Value::Array(_) => "array",
+ Value::Object(_) => "object",
+ _ => unreachable!(),
+ }
);
+ // For numbers, arrays and objects, parse each argument with
+ // `serde_json::StreamDeserializer`.
+ let values: Vec<Value> = serde_json::Deserializer::from_str(&args[1])
+ .into_iter()
+ .collect::<Result<_, _>>()
+ .map_err(|err| anyhow!("failed to parse value: {err}"))?;
- Value::Number(
- args[1..]
- .iter()
- .skip_while(|&e| value.to_string() != *e.to_string())
- .nth(1)
- .unwrap_or_else(|| &args[1])
- .parse()?,
- )
- }
- Value::Null | Value::Object(_) | Value::Array(_) => {
- anyhow::bail!("Configuration {key} does not support toggle yet")
+ if let Some(wrongly_typed_value) = values
+ .iter()
+ .find(|v| std::mem::discriminant(*v) != std::mem::discriminant(&*value))
+ {
+ bail!("value '{wrongly_typed_value}' has a different type than '{value}'");
+ }
+
+ values
+ .iter()
+ .skip_while(|e| *e != value)
+ .nth(1)
+ .unwrap_or(&values[0])
+ .clone()
}
};
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))?;
+ .map_err(|err| anyhow::anyhow!("Failed to parse config: {err}"))?;
cx.editor
.config_events
@@ -2109,11 +2005,7 @@ fn toggle_option(
}
/// Change the language of the current buffer at runtime.
-fn language(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- event: PromptEvent,
-) -> anyhow::Result<()> {
+fn language(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
@@ -2125,13 +2017,9 @@ fn language(
return Ok(());
}
- if args.len() != 1 {
- anyhow::bail!("Bad arguments. Usage: `:set-language language`");
- }
-
let doc = doc_mut!(cx.editor);
- if args[0] == DEFAULT_LANGUAGE_NAME {
+ if &args[0] == DEFAULT_LANGUAGE_NAME {
doc.set_language(None, None)
} else {
doc.set_language_by_language_id(&args[0], cx.editor.syn_loader.clone())?;
@@ -2147,31 +2035,15 @@ fn language(
Ok(())
}
-fn sort(cx: &mut compositor::Context, args: &[Cow<str>], 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)
+ sort_impl(cx, args.has_flag("reverse"))
}
-fn sort_reverse(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- event: PromptEvent,
-) -> anyhow::Result<()> {
- if event != PromptEvent::Validate {
- return Ok(());
- }
-
- sort_impl(cx, args, true)
-}
-
-fn sort_impl(
- cx: &mut compositor::Context,
- _args: &[Cow<str>],
- reverse: bool,
-) -> anyhow::Result<()> {
+fn sort_impl(cx: &mut compositor::Context, reverse: bool) -> anyhow::Result<()> {
let scrolloff = cx.editor.config().scrolloff;
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
@@ -2207,11 +2079,7 @@ fn sort_impl(
Ok(())
}
-fn reflow(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- event: PromptEvent,
-) -> anyhow::Result<()> {
+fn reflow(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
@@ -2250,7 +2118,7 @@ fn reflow(
fn tree_sitter_subtree(
cx: &mut compositor::Context,
- _args: &[Cow<str>],
+ _args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -2289,7 +2157,7 @@ fn tree_sitter_subtree(
fn open_config(
cx: &mut compositor::Context,
- _args: &[Cow<str>],
+ _args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -2303,7 +2171,7 @@ fn open_config(
fn open_workspace_config(
cx: &mut compositor::Context,
- _args: &[Cow<str>],
+ _args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -2315,11 +2183,7 @@ fn open_workspace_config(
Ok(())
}
-fn open_log(
- cx: &mut compositor::Context,
- _args: &[Cow<str>],
- event: PromptEvent,
-) -> anyhow::Result<()> {
+fn open_log(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
@@ -2330,7 +2194,7 @@ fn open_log(
fn refresh_config(
cx: &mut compositor::Context,
- _args: &[Cow<str>],
+ _args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -2343,47 +2207,41 @@ fn refresh_config(
fn append_output(
cx: &mut compositor::Context,
- args: &[Cow<str>],
+ 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);
Ok(())
}
fn insert_output(
cx: &mut compositor::Context,
- args: &[Cow<str>],
+ 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);
Ok(())
}
-fn pipe_to(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- 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<str>], 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<str>],
+ args: Args,
event: PromptEvent,
behavior: &ShellBehavior,
) -> anyhow::Result<()> {
@@ -2391,14 +2249,13 @@ fn pipe_impl(
return Ok(());
}
- ensure!(!args.is_empty(), "Shell command required");
shell(cx, &args.join(" "), behavior);
Ok(())
}
fn run_shell_command(
cx: &mut compositor::Context,
- args: &[Cow<str>],
+ args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -2434,13 +2291,12 @@ fn run_shell_command(
fn reset_diff_change(
cx: &mut compositor::Context,
- args: &[Cow<str>],
+ _args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
- ensure!(args.is_empty(), ":reset-diff-change takes no arguments");
let editor = &mut cx.editor;
let scrolloff = editor.config().scrolloff;
@@ -2487,14 +2343,13 @@ fn reset_diff_change(
fn clear_register(
cx: &mut compositor::Context,
- args: &[Cow<str>],
+ args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
- ensure!(args.len() <= 1, ":clear-register takes at most 1 argument");
if args.is_empty() {
cx.editor.registers.clear();
cx.editor.set_status("All registers cleared");
@@ -2503,7 +2358,7 @@ fn clear_register(
ensure!(
args[0].chars().count() == 1,
- format!("Invalid register {}", args[0])
+ format!("Invalid register {}", &args[0])
);
let register = args[0].chars().next().unwrap_or_default();
if cx.editor.registers.remove(register) {
@@ -2516,11 +2371,7 @@ fn clear_register(
Ok(())
}
-fn redraw(
- cx: &mut compositor::Context,
- _args: &[Cow<str>],
- event: PromptEvent,
-) -> anyhow::Result<()> {
+fn redraw(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
@@ -2539,16 +2390,11 @@ fn redraw(
Ok(())
}
-fn move_buffer(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- event: PromptEvent,
-) -> anyhow::Result<()> {
+fn move_buffer(cx: &mut compositor::Context, 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
.path()
@@ -2563,7 +2409,7 @@ fn move_buffer(
fn yank_diagnostic(
cx: &mut compositor::Context,
- args: &[Cow<str>],
+ args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -2601,7 +2447,7 @@ fn yank_diagnostic(
Ok(())
}
-fn read(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) -> anyhow::Result<()> {
+fn read(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
@@ -2609,9 +2455,6 @@ fn read(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) ->
let scrolloff = cx.editor.config().scrolloff;
let (view, doc) = current!(cx.editor);
- ensure!(!args.is_empty(), "file name is expected");
- ensure!(args.len() == 1, "only the file name is expected");
-
let filename = args.first().unwrap();
let path = helix_stdx::path::expand_tilde(PathBuf::from(filename.to_string()));
@@ -2635,132 +2478,239 @@ fn read(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) ->
Ok(())
}
+fn echo(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> {
+ if event != PromptEvent::Validate {
+ return Ok(());
+ }
+
+ let output = args.into_iter().fold(String::new(), |mut acc, arg| {
+ if !acc.is_empty() {
+ acc.push(' ');
+ }
+ acc.push_str(&arg);
+ acc
+ });
+ cx.editor.set_status(output);
+
+ Ok(())
+}
+
+fn noop(_cx: &mut compositor::Context, _args: Args, _event: PromptEvent) -> anyhow::Result<()> {
+ Ok(())
+}
+
+/// This command handles all of its input as-is with no quoting or flags.
+const SHELL_SIGNATURE: Signature = Signature {
+ positionals: (1, Some(2)),
+ raw_after: Some(1),
+ ..Signature::DEFAULT
+};
+
+const SHELL_COMPLETER: CommandCompleter = CommandCompleter::positional(&[
+ // Command name (TODO: consider a command completer - Kakoune has prior art)
+ completers::none,
+ // Shell argument(s)
+ completers::filename,
+]);
+
pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
TypableCommand {
name: "quit",
aliases: &["q"],
doc: "Close the current view.",
fun: quit,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "quit!",
aliases: &["q!"],
doc: "Force close the current view, ignoring unsaved changes.",
fun: force_quit,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "open",
aliases: &["o", "edit", "e"],
doc: "Open a file from disk into the current view.",
fun: open,
- signature: CommandSignature::all(completers::filename),
+ completer: CommandCompleter::all(completers::filename),
+ signature: Signature {
+ positionals: (1, None),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "buffer-close",
aliases: &["bc", "bclose"],
doc: "Close the current buffer.",
fun: buffer_close,
- signature: CommandSignature::all(completers::buffer),
+ completer: CommandCompleter::all(completers::buffer),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "buffer-close!",
aliases: &["bc!", "bclose!"],
doc: "Close the current buffer forcefully, ignoring unsaved changes.",
fun: force_buffer_close,
- signature: CommandSignature::all(completers::buffer)
+ completer: CommandCompleter::all(completers::buffer),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "buffer-close-others",
aliases: &["bco", "bcloseother"],
doc: "Close all buffers but the currently focused one.",
fun: buffer_close_others,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "buffer-close-others!",
aliases: &["bco!", "bcloseother!"],
doc: "Force close all buffers but the currently focused one.",
fun: force_buffer_close_others,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "buffer-close-all",
aliases: &["bca", "bcloseall"],
doc: "Close all buffers without quitting.",
fun: buffer_close_all,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "buffer-close-all!",
aliases: &["bca!", "bcloseall!"],
doc: "Force close all buffers ignoring unsaved changes without quitting.",
fun: force_buffer_close_all,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "buffer-next",
aliases: &["bn", "bnext"],
doc: "Goto next buffer.",
fun: buffer_next,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "buffer-previous",
aliases: &["bp", "bprev"],
doc: "Goto previous buffer.",
fun: buffer_previous,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "write",
aliases: &["w"],
doc: "Write changes to disk. Accepts an optional path (:write some/path.txt)",
fun: write,
- signature: CommandSignature::positional(&[completers::filename]),
+ completer: CommandCompleter::positional(&[completers::filename]),
+ signature: Signature {
+ positionals: (0, Some(1)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "write!",
aliases: &["w!"],
doc: "Force write changes to disk creating necessary subdirectories. Accepts an optional path (:write! some/path.txt)",
fun: force_write,
- signature: CommandSignature::positional(&[completers::filename]),
+ completer: CommandCompleter::positional(&[completers::filename]),
+ signature: Signature {
+ positionals: (0, Some(1)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "write-buffer-close",
aliases: &["wbc"],
doc: "Write changes to disk and closes the buffer. Accepts an optional path (:write-buffer-close some/path.txt)",
fun: write_buffer_close,
- signature: CommandSignature::positional(&[completers::filename]),
+ completer: CommandCompleter::positional(&[completers::filename]),
+ signature: Signature {
+ positionals: (0, Some(1)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "write-buffer-close!",
aliases: &["wbc!"],
doc: "Force write changes to disk creating necessary subdirectories and closes the buffer. Accepts an optional path (:write-buffer-close! some/path.txt)",
fun: force_write_buffer_close,
- signature: CommandSignature::positional(&[completers::filename]),
+ completer: CommandCompleter::positional(&[completers::filename]),
+ signature: Signature {
+ positionals: (0, Some(1)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "new",
aliases: &["n"],
doc: "Create a new scratch buffer.",
fun: new_file,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "format",
aliases: &["fmt"],
doc: "Format the file using an external formatter or language server.",
fun: format,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "indent-style",
aliases: &[],
doc: "Set the indentation style for editing. ('t' for tabs or 1-16 for number of spaces.)",
fun: set_indent_style,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(1)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "line-ending",
@@ -2770,336 +2720,529 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
#[cfg(feature = "unicode-lines")]
doc: "Set the document's default line ending. Options: crlf, lf, cr, ff, nel.",
fun: set_line_ending,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(1)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "earlier",
aliases: &["ear"],
doc: "Jump back to an earlier point in edit history. Accepts a number of steps or a time span.",
fun: earlier,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(1)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "later",
aliases: &["lat"],
doc: "Jump to a later point in edit history. Accepts a number of steps or a time span.",
fun: later,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(1)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "write-quit",
aliases: &["wq", "x"],
doc: "Write changes to disk and close the current view. Accepts an optional path (:wq some/path.txt)",
fun: write_quit,
- signature: CommandSignature::positional(&[completers::filename]),
+ completer: CommandCompleter::positional(&[completers::filename]),
+ signature: Signature {
+ positionals: (0, Some(1)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "write-quit!",
aliases: &["wq!", "x!"],
doc: "Write changes to disk and close the current view forcefully. Accepts an optional path (:wq! some/path.txt)",
fun: force_write_quit,
- signature: CommandSignature::positional(&[completers::filename]),
+ completer: CommandCompleter::positional(&[completers::filename]),
+ signature: Signature {
+ positionals: (0, Some(1)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "write-all",
aliases: &["wa"],
doc: "Write changes from all buffers to disk.",
fun: write_all,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "write-all!",
aliases: &["wa!"],
doc: "Forcefully write changes from all buffers to disk creating necessary subdirectories.",
fun: force_write_all,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "write-quit-all",
aliases: &["wqa", "xa"],
doc: "Write changes from all buffers to disk and close all views.",
fun: write_all_quit,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "write-quit-all!",
aliases: &["wqa!", "xa!"],
doc: "Write changes from all buffers to disk and close all views forcefully (ignoring unsaved changes).",
fun: force_write_all_quit,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "quit-all",
aliases: &["qa"],
doc: "Close all views.",
fun: quit_all,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "quit-all!",
aliases: &["qa!"],
doc: "Force close all views ignoring unsaved changes.",
fun: force_quit_all,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "cquit",
aliases: &["cq"],
doc: "Quit with exit code (default 1). Accepts an optional integer exit code (:cq 2).",
fun: cquit,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(1)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "cquit!",
aliases: &["cq!"],
doc: "Force quit with exit code (default 1) ignoring unsaved changes. Accepts an optional integer exit code (:cq! 2).",
fun: force_cquit,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(1)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "theme",
aliases: &[],
doc: "Change the editor theme (show current theme if no name specified).",
fun: theme,
- signature: CommandSignature::positional(&[completers::theme]),
+ completer: CommandCompleter::positional(&[completers::theme]),
+ signature: Signature {
+ positionals: (1, Some(1)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "yank-join",
aliases: &[],
doc: "Yank joined selections. A separator can be provided as first argument. Default value is newline.",
fun: yank_joined,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(1)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "clipboard-yank",
aliases: &[],
doc: "Yank main selection into system clipboard.",
fun: yank_main_selection_to_clipboard,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "clipboard-yank-join",
aliases: &[],
doc: "Yank joined selections into system clipboard. A separator can be provided as first argument. Default value is newline.", // FIXME: current UI can't display long doc.
fun: yank_joined_to_clipboard,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(1)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "primary-clipboard-yank",
aliases: &[],
doc: "Yank main selection into system primary clipboard.",
fun: yank_main_selection_to_primary_clipboard,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "primary-clipboard-yank-join",
aliases: &[],
doc: "Yank joined selections into system primary clipboard. A separator can be provided as first argument. Default value is newline.", // FIXME: current UI can't display long doc.
fun: yank_joined_to_primary_clipboard,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(1)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "clipboard-paste-after",
aliases: &[],
doc: "Paste system clipboard after selections.",
fun: paste_clipboard_after,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "clipboard-paste-before",
aliases: &[],
doc: "Paste system clipboard before selections.",
fun: paste_clipboard_before,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "clipboard-paste-replace",
aliases: &[],
doc: "Replace selections with content of system clipboard.",
fun: replace_selections_with_clipboard,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "primary-clipboard-paste-after",
aliases: &[],
doc: "Paste primary clipboard after selections.",
fun: paste_primary_clipboard_after,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "primary-clipboard-paste-before",
aliases: &[],
doc: "Paste primary clipboard before selections.",
fun: paste_primary_clipboard_before,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "primary-clipboard-paste-replace",
aliases: &[],
doc: "Replace selections with content of system primary clipboard.",
fun: replace_selections_with_primary_clipboard,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "show-clipboard-provider",
aliases: &[],
doc: "Show clipboard provider name in status bar.",
fun: show_clipboard_provider,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "change-current-directory",
aliases: &["cd"],
doc: "Change the current working directory.",
fun: change_current_directory,
- signature: CommandSignature::positional(&[completers::directory]),
+ completer: CommandCompleter::positional(&[completers::directory]),
+ signature: Signature {
+ positionals: (1, Some(1)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "show-directory",
aliases: &["pwd"],
doc: "Show the current working directory.",
fun: show_current_directory,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "encoding",
aliases: &[],
doc: "Set encoding. Based on `https://encoding.spec.whatwg.org`.",
fun: set_encoding,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(1)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "character-info",
aliases: &["char"],
doc: "Get info about the character under the primary cursor.",
fun: get_character_info,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "reload",
aliases: &["rl"],
doc: "Discard changes and reload from the source file.",
fun: reload,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "reload-all",
aliases: &["rla"],
doc: "Discard changes and reload all documents from the source files.",
fun: reload_all,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "update",
aliases: &["u"],
doc: "Write changes only if the file has been modified.",
fun: update,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "lsp-workspace-command",
aliases: &[],
doc: "Open workspace command picker",
fun: lsp_workspace_command,
- signature: CommandSignature::positional(&[completers::lsp_workspace_command]),
+ completer: CommandCompleter::positional(&[completers::lsp_workspace_command]),
+ signature: Signature {
+ positionals: (0, None),
+ raw_after: Some(1),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "lsp-restart",
aliases: &[],
doc: "Restarts the given language servers, or all language servers that are used by the current file if no arguments are supplied",
fun: lsp_restart,
- signature: CommandSignature::all(completers::configured_language_servers),
+ completer: CommandCompleter::all(completers::configured_language_servers),
+ signature: Signature {
+ positionals: (0, None),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "lsp-stop",
aliases: &[],
doc: "Stops the given language servers, or all language servers that are used by the current file if no arguments are supplied",
fun: lsp_stop,
- signature: CommandSignature::all(completers::active_language_servers),
+ completer: CommandCompleter::all(completers::active_language_servers),
+ signature: Signature {
+ positionals: (0, None),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "tree-sitter-scopes",
aliases: &[],
doc: "Display tree sitter scopes, primarily for theming and development.",
fun: tree_sitter_scopes,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "tree-sitter-highlight-name",
aliases: &[],
doc: "Display name of tree-sitter highlight scope under the cursor.",
fun: tree_sitter_highlight_name,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "debug-start",
aliases: &["dbg"],
doc: "Start a debug session from a given template with given parameters.",
fun: debug_start,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, None),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "debug-remote",
aliases: &["dbg-tcp"],
doc: "Connect to a debug adapter by TCP address and start a debugging session from a given template with given parameters.",
fun: debug_remote,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, None),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "debug-eval",
aliases: &[],
doc: "Evaluate expression in current debug context.",
fun: debug_eval,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (1, Some(1)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "vsplit",
aliases: &["vs"],
doc: "Open the file in a vertical split.",
fun: vsplit,
- signature: CommandSignature::all(completers::filename)
+ completer: CommandCompleter::all(completers::filename),
+ signature: Signature {
+ positionals: (0, None),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "vsplit-new",
aliases: &["vnew"],
doc: "Open a scratch buffer in a vertical split.",
fun: vsplit_new,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "hsplit",
aliases: &["hs", "sp"],
doc: "Open the file in a horizontal split.",
fun: hsplit,
- signature: CommandSignature::all(completers::filename)
+ completer: CommandCompleter::all(completers::filename),
+ signature: Signature {
+ positionals: (0, None),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "hsplit-new",
aliases: &["hnew"],
doc: "Open a scratch buffer in a horizontal split.",
fun: hsplit_new,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "tutor",
aliases: &[],
doc: "Open the tutorial.",
fun: tutor,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "goto",
aliases: &["g"],
doc: "Goto line number.",
fun: goto_line_number,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (1, Some(1)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "set-language",
aliases: &["lang"],
doc: "Set the language of current buffer (show current language if no value specified).",
fun: language,
- signature: CommandSignature::positional(&[completers::language]),
+ completer: CommandCompleter::positional(&[completers::language]),
+ signature: Signature {
+ positionals: (0, Some(1)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "set-option",
@@ -3107,154 +3250,248 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
doc: "Set a config option at runtime.\nFor example to disable smart case search, use `:set search.smart-case false`.",
fun: set_option,
// TODO: Add support for completion of the options value(s), when appropriate.
- signature: CommandSignature::positional(&[completers::setting]),
+ completer: CommandCompleter::positional(&[completers::setting]),
+ signature: Signature {
+ positionals: (2, Some(2)),
+ raw_after: Some(1),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "toggle-option",
aliases: &["toggle"],
- doc: "Toggle a boolean config option at runtime.\nFor example to toggle smart case search, use `:toggle search.smart-case`.",
+ doc: "Toggle a config option at runtime.\nFor example to toggle smart case search, use `:toggle search.smart-case`.",
fun: toggle_option,
- signature: CommandSignature::positional(&[completers::setting]),
+ completer: CommandCompleter::positional(&[completers::setting]),
+ signature: Signature {
+ positionals: (1, None),
+ raw_after: Some(1),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "get-option",
aliases: &["get"],
doc: "Get the current value of a config option.",
fun: get_option,
- signature: CommandSignature::positional(&[completers::setting]),
+ completer: CommandCompleter::positional(&[completers::setting]),
+ signature: Signature {
+ positionals: (1, Some(1)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "sort",
aliases: &[],
doc: "Sort ranges in selection.",
fun: sort,
- signature: CommandSignature::none(),
- },
- TypableCommand {
- name: "rsort",
- aliases: &[],
- doc: "Sort ranges in selection in reverse order.",
- fun: sort_reverse,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ flags: &[
+ Flag {
+ name: "reverse",
+ alias: Some('r'),
+ doc: "sort ranges in reverse order",
+ ..Flag::DEFAULT
+ },
+ ],
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "reflow",
aliases: &[],
doc: "Hard-wrap the current selection of lines to a given width.",
fun: reflow,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "tree-sitter-subtree",
aliases: &["ts-subtree"],
doc: "Display the smallest tree-sitter subtree that spans the primary selection, primarily for debugging queries.",
fun: tree_sitter_subtree,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "config-reload",
aliases: &[],
doc: "Refresh user config.",
fun: refresh_config,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "config-open",
aliases: &[],
doc: "Open the user config.toml file.",
fun: open_config,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "config-open-workspace",
aliases: &[],
doc: "Open the workspace config.toml file.",
fun: open_workspace_config,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "log-open",
aliases: &[],
doc: "Open the helix log file.",
fun: open_log,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "insert-output",
aliases: &[],
doc: "Run shell command, inserting output before each selection.",
fun: insert_output,
- signature: CommandSignature::none(),
+ completer: SHELL_COMPLETER,
+ signature: SHELL_SIGNATURE,
},
TypableCommand {
name: "append-output",
aliases: &[],
doc: "Run shell command, appending output after each selection.",
fun: append_output,
- signature: CommandSignature::none(),
+ completer: SHELL_COMPLETER,
+ signature: SHELL_SIGNATURE,
},
TypableCommand {
name: "pipe",
aliases: &[],
doc: "Pipe each selection to the shell command.",
fun: pipe,
- signature: CommandSignature::none(),
+ completer: SHELL_COMPLETER,
+ signature: SHELL_SIGNATURE,
},
TypableCommand {
name: "pipe-to",
aliases: &[],
doc: "Pipe each selection to the shell command, ignoring output.",
fun: pipe_to,
- signature: CommandSignature::none(),
+ completer: SHELL_COMPLETER,
+ signature: SHELL_SIGNATURE,
},
TypableCommand {
name: "run-shell-command",
aliases: &["sh"],
doc: "Run a shell command",
fun: run_shell_command,
- signature: CommandSignature::all(completers::filename)
+ completer: SHELL_COMPLETER,
+ signature: SHELL_SIGNATURE,
},
TypableCommand {
name: "reset-diff-change",
aliases: &["diffget", "diffg"],
doc: "Reset the diff change at the cursor position.",
fun: reset_diff_change,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "clear-register",
aliases: &[],
doc: "Clear given register. If no argument is provided, clear all registers.",
fun: clear_register,
- signature: CommandSignature::all(completers::register),
+ completer: CommandCompleter::all(completers::register),
+ signature: Signature {
+ positionals: (0, Some(1)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "redraw",
aliases: &[],
doc: "Clear and re-render the whole UI",
fun: redraw,
- signature: CommandSignature::none(),
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, Some(0)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "move",
aliases: &["mv"],
doc: "Move the current buffer and its corresponding file to a different path",
fun: move_buffer,
- signature: CommandSignature::positional(&[completers::filename]),
+ completer: CommandCompleter::positional(&[completers::filename]),
+ signature: Signature {
+ positionals: (1, Some(1)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "yank-diagnostic",
aliases: &[],
doc: "Yank diagnostic(s) under primary cursor to register, or clipboard by default",
fun: yank_diagnostic,
- signature: CommandSignature::all(completers::register),
+ completer: CommandCompleter::all(completers::register),
+ signature: Signature {
+ positionals: (0, Some(1)),
+ ..Signature::DEFAULT
+ },
},
TypableCommand {
name: "read",
aliases: &["r"],
doc: "Load a file into buffer",
fun: read,
- signature: CommandSignature::positional(&[completers::filename]),
+ completer: CommandCompleter::positional(&[completers::filename]),
+ signature: Signature {
+ positionals: (1, Some(1)),
+ ..Signature::DEFAULT
+ },
+ },
+ TypableCommand {
+ name: "echo",
+ aliases: &[],
+ doc: "Prints the given arguments to the statusline.",
+ fun: echo,
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (1, None),
+ ..Signature::DEFAULT
+ },
+ },
+ TypableCommand {
+ name: "noop",
+ aliases: &[],
+ doc: "Does nothing.",
+ fun: noop,
+ completer: CommandCompleter::none(),
+ signature: Signature {
+ positionals: (0, None),
+ ..Signature::DEFAULT
+ },
},
];
@@ -3269,125 +3506,397 @@ pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableComma
.collect()
});
+fn execute_command_line(
+ cx: &mut compositor::Context,
+ input: &str,
+ event: PromptEvent,
+) -> anyhow::Result<()> {
+ let (command, rest, _) = command_line::split(input);
+ if command.is_empty() {
+ return Ok(());
+ }
+
+ // If command is numeric, interpret as line number and go there.
+ if command.parse::<usize>().is_ok() && rest.trim().is_empty() {
+ let cmd = TYPABLE_COMMAND_MAP.get("goto").unwrap();
+ return execute_command(cx, cmd, command, event);
+ }
+
+ match typed::TYPABLE_COMMAND_MAP.get(command) {
+ Some(cmd) => execute_command(cx, cmd, rest, event),
+ None if event == PromptEvent::Validate => Err(anyhow!("no such command: '{command}'")),
+ None => Ok(()),
+ }
+}
+
+pub(super) fn execute_command(
+ cx: &mut compositor::Context,
+ cmd: &TypableCommand,
+ args: &str,
+ event: PromptEvent,
+) -> anyhow::Result<()> {
+ let args = if event == PromptEvent::Validate {
+ Args::parse(args, cmd.signature, true, |token| {
+ expansion::expand(cx.editor, token).map_err(|err| err.into())
+ })
+ .map_err(|err| anyhow!("'{}': {err}", cmd.name))?
+ } else {
+ Args::parse(args, cmd.signature, false, |token| Ok(token.content))
+ .expect("arg parsing cannot fail when validation is turned off")
+ };
+
+ (cmd.fun)(cx, args, event).map_err(|err| anyhow!("'{}': {err}", cmd.name))
+}
+
#[allow(clippy::unnecessary_unwrap)]
pub(super) fn command_mode(cx: &mut Context) {
let mut prompt = Prompt::new(
":".into(),
Some(':'),
- |editor: &Editor, input: &str| {
- let shellwords = Shellwords::from(input);
- let words = shellwords.words();
-
- if words.is_empty() || (words.len() == 1 && !shellwords.ends_with_whitespace()) {
- fuzzy_match(
- input,
- TYPABLE_COMMAND_LIST.iter().map(|command| command.name),
- false,
- )
- .into_iter()
- .map(|(name, _)| (0.., name.into()))
- .collect()
+ complete_command_line,
+ move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
+ if let Err(err) = execute_command_line(cx, input, event) {
+ cx.editor.set_error(err.to_string());
+ }
+ },
+ );
+ prompt.doc_fn = Box::new(command_line_doc);
+
+ // Calculate initial completion
+ prompt.recalculate_completion(cx.editor);
+ cx.push_layer(Box::new(prompt));
+}
+
+fn command_line_doc(input: &str) -> Option<Cow<str>> {
+ let (command, _, _) = command_line::split(input);
+ let command = TYPABLE_COMMAND_MAP.get(command)?;
+
+ if command.aliases.is_empty() && command.signature.flags.is_empty() {
+ return Some(Cow::Borrowed(command.doc));
+ }
+
+ let mut doc = command.doc.to_string();
+
+ if !command.aliases.is_empty() {
+ write!(doc, "\nAliases: {}", command.aliases.join(", ")).unwrap();
+ }
+
+ if !command.signature.flags.is_empty() {
+ const ARG_PLACEHOLDER: &str = " <arg>";
+
+ fn flag_len(flag: &Flag) -> usize {
+ let name_len = flag.name.len();
+ let alias_len = if let Some(alias) = flag.alias {
+ "/-".len() + alias.len_utf8()
} 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())
- };
+ 0
+ };
+ let arg_len = if flag.completions.is_some() {
+ ARG_PLACEHOLDER.len()
+ } else {
+ 0
+ };
+ name_len + alias_len + arg_len
+ }
+
+ doc.push_str("\nFlags:");
+
+ let max_flag_len = command.signature.flags.iter().map(flag_len).max().unwrap();
+
+ for flag in command.signature.flags {
+ let mut buf = [0u8; 4];
+ let this_flag_len = flag_len(flag);
+ write!(
+ doc,
+ "\n --{flag_text}{spacer:spacing$} {doc}",
+ doc = flag.doc,
+ // `fmt::Arguments` does not respect width controls so we must place the spacers
+ // explicitly:
+ spacer = "",
+ spacing = max_flag_len - this_flag_len,
+ flag_text = format_args!(
+ "{}{}{}{}",
+ flag.name,
+ // Ideally this would be written as a `format_args!` too but the borrow
+ // checker is not yet smart enough.
+ if flag.alias.is_some() { "/-" } else { "" },
+ if let Some(alias) = flag.alias {
+ alias.encode_utf8(&mut buf)
+ } else {
+ ""
+ },
+ if flag.completions.is_some() {
+ ARG_PLACEHOLDER
+ } else {
+ ""
+ }
+ ),
+ )
+ .unwrap();
+ }
+ }
- let argument_number = argument_number_of(&shellwords);
+ Some(Cow::Owned(doc))
+}
- if let Some(completer) = TYPABLE_COMMAND_MAP
- .get(&words[0] as &str)
- .map(|tc| tc.completer_for_argument_number(argument_number))
- {
- completer(editor, word)
+fn complete_command_line(editor: &Editor, input: &str) -> Vec<ui::prompt::Completion> {
+ let (command, rest, complete_command) = command_line::split(input);
+
+ if complete_command {
+ fuzzy_match(
+ input,
+ TYPABLE_COMMAND_LIST.iter().map(|command| command.name),
+ false,
+ )
+ .into_iter()
+ .map(|(name, _)| (0.., name.into()))
+ .collect()
+ } else {
+ TYPABLE_COMMAND_MAP
+ .get(command)
+ .map_or_else(Vec::new, |cmd| {
+ let args_offset = command.len() + 1;
+ complete_command_args(editor, cmd, rest, args_offset)
+ })
+ }
+}
+
+fn complete_command_args(
+ editor: &Editor,
+ command: &TypableCommand,
+ input: &str,
+ offset: usize,
+) -> Vec<ui::prompt::Completion> {
+ use command_line::{CompletionState, ExpansionKind, Tokenizer};
+
+ // TODO: completion should depend on the location of the cursor instead of the end of the
+ // string. This refactor is left for the future but the below completion code should respect
+ // the cursor position if it becomes a parameter.
+ let cursor = input.len();
+ let prefix = &input[..cursor];
+ let mut tokenizer = Tokenizer::new(prefix, false);
+ let mut args = Args::new(command.signature, false);
+ let mut final_token = None;
+ let mut is_last_token = true;
+
+ while let Some(token) = args
+ .read_token(&mut tokenizer)
+ .expect("arg parsing cannot fail when validation is turned off")
+ {
+ final_token = Some(token.clone());
+ args.push(token.content)
+ .expect("arg parsing cannot fail when validation is turned off");
+ if tokenizer.pos() >= cursor {
+ is_last_token = false;
+ }
+ }
+
+ // Use a fake final token when the input is not terminated with a token. This simulates an
+ // empty argument, causing completion on an empty value whenever you type space/tab. For
+ // example if you say `":open README.md "` (with that trailing space) you should see the
+ // files in the current dir - completing `""` rather than completions for `"README.md"` or
+ // `"README.md "`.
+ let token = if is_last_token {
+ let token = Token::empty_at(prefix.len());
+ args.push(token.content.clone()).unwrap();
+ token
+ } else {
+ final_token.unwrap()
+ };
+
+ // Don't complete on closed tokens, for example after writing a closing double quote.
+ if token.is_terminated {
+ return Vec::new();
+ }
+
+ match token.kind {
+ TokenKind::Unquoted | TokenKind::Quoted(_) => {
+ match args.completion_state() {
+ CompletionState::Positional => {
+ // If the completion state is positional there must be at least one positional
+ // in `args`.
+ let n = args
+ .len()
+ .checked_sub(1)
+ .expect("completion state to be positional");
+ let completer = command.completer_for_argument_number(n);
+
+ completer(editor, &token.content)
.into_iter()
- .map(|(range, mut file)| {
- file.content = shellwords::escape(file.content);
-
- // offset ranges to input
- let offset = input.len() - word_len;
- let range = (range.start + offset)..;
- (range, file)
- })
+ .map(|(range, span)| quote_completion(&token, range, span, offset))
.collect()
- } else {
- Vec::new()
}
+ CompletionState::Flag(_) => fuzzy_match(
+ token.content.trim_start_matches('-'),
+ command.signature.flags.iter().map(|flag| flag.name),
+ false,
+ )
+ .into_iter()
+ .map(|(name, _)| ((offset + token.content_start).., format!("--{name}").into()))
+ .collect(),
+ CompletionState::FlagArgument(flag) => fuzzy_match(
+ &token.content,
+ flag.completions
+ .expect("flags in FlagArgument always have completions"),
+ false,
+ )
+ .into_iter()
+ .map(|(value, _)| ((offset + token.content_start).., (*value).into()))
+ .collect(),
}
- }, // completion
- move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
- let parts = input.split_whitespace().collect::<Vec<&str>>();
- if parts.is_empty() {
- return;
- }
+ }
+ TokenKind::Expand | TokenKind::Expansion(ExpansionKind::Shell) => {
+ // See the comment about the checked sub expect above.
+ let arg_completer = matches!(args.completion_state(), CompletionState::Positional)
+ .then(|| {
+ let n = args
+ .len()
+ .checked_sub(1)
+ .expect("completion state to be positional");
+ command.completer_for_argument_number(n)
+ });
+ complete_expand(editor, &token, arg_completer, offset + token.content_start)
+ }
+ TokenKind::Expansion(ExpansionKind::Variable) => {
+ complete_variable_expansion(&token.content, offset + token.content_start)
+ }
+ TokenKind::Expansion(ExpansionKind::Unicode) => Vec::new(),
+ TokenKind::ExpansionKind => {
+ complete_expansion_kind(&token.content, offset + token.content_start)
+ }
+ }
+}
- // If command is numeric, interpret as line number and go there.
- if parts.len() == 1 && parts[0].parse::<usize>().ok().is_some() {
- if let Err(e) = typed::goto_line_number(cx, &[Cow::from(parts[0])], event) {
- cx.editor.set_error(format!("{}", e));
- }
- return;
- }
+/// Replace the content and optionally update the range of a positional's completion to account
+/// for quoting.
+///
+/// This is used to handle completions of file or directory names for example. When completing a
+/// file with a space, tab or percent character in the name, the space should be escaped by
+/// quoting the entire token. If the token being completed is already quoted, any quotes within
+/// the completion text should be escaped by doubling them.
+fn quote_completion<'a>(
+ token: &Token,
+ range: ops::RangeFrom<usize>,
+ mut span: Span<'a>,
+ offset: usize,
+) -> (ops::RangeFrom<usize>, Span<'a>) {
+ fn replace<'a>(text: Cow<'a, str>, from: char, to: &str) -> Cow<'a, str> {
+ if text.contains(from) {
+ Cow::Owned(text.replace(from, to))
+ } else {
+ text
+ }
+ }
- // Handle typable commands
- if let Some(cmd) = typed::TYPABLE_COMMAND_MAP.get(parts[0]) {
- let shellwords = Shellwords::from(input);
- let args = shellwords.words();
+ match token.kind {
+ TokenKind::Unquoted if span.content.contains([' ', '\t', '%']) => {
+ span.content = Cow::Owned(format!(
+ "'{}{}'",
+ // Escape any inner single quotes by doubling them.
+ replace(token.content.as_ref().into(), '\'', "''"),
+ replace(span.content, '\'', "''")
+ ));
+ // Ignore `range.start` here since we're replacing the entire token.
+ ((offset + token.content_start).., span)
+ }
+ TokenKind::Quoted(quote) => {
+ span.content = replace(span.content, quote.char(), quote.escape());
+ ((range.start + offset + token.content_start).., span)
+ }
+ TokenKind::Expand => {
+ // NOTE: `token.content_start` is already accounted for in `offset` for `Expand`
+ // tokens.
+ span.content = replace(span.content, '"', "\"\"");
+ ((range.start + offset).., span)
+ }
+ _ => ((range.start + offset + token.content_start).., span),
+ }
+}
- if let Err(e) = (cmd.fun)(cx, &args[1..], event) {
- cx.editor.set_error(format!("{}", e));
- }
- } else if event == PromptEvent::Validate {
- cx.editor
- .set_error(format!("no such command: '{}'", parts[0]));
- }
- },
- );
- prompt.doc_fn = Box::new(|input: &str| {
- let part = input.split(' ').next().unwrap_or_default();
+fn complete_expand(
+ editor: &Editor,
+ token: &Token,
+ completer: Option<&Completer>,
+ offset: usize,
+) -> Vec<ui::prompt::Completion> {
+ use command_line::{ExpansionKind, Tokenizer};
- if let Some(typed::TypableCommand { doc, aliases, .. }) =
- typed::TYPABLE_COMMAND_MAP.get(part)
- {
- if aliases.is_empty() {
- return Some((*doc).into());
+ let mut start = 0;
+
+ // If the expand token contains expansions, complete those.
+ while let Some(idx) = token.content[start..].find('%') {
+ let idx = start + idx;
+ if token.content.as_bytes().get(idx + '%'.len_utf8()).copied() == Some(b'%') {
+ // Two percents together are skipped.
+ start = idx + ('%'.len_utf8() * 2);
+ } else {
+ let mut tokenizer = Tokenizer::new(&token.content[idx..], false);
+ let token = tokenizer
+ .parse_percent_token()
+ .map(|token| token.expect("arg parser cannot fail when validation is disabled"));
+ start = idx + tokenizer.pos();
+
+ // Like closing quote characters in `complete_command_args` above, don't provide
+ // completions if the token is already terminated. This also skips expansions
+ // which have already been fully written, for example
+ // `"%{cursor_line}:%{cursor_col` should complete `cursor_column` instead of
+ // `cursor_line`.
+ let Some(token) = token.filter(|t| !t.is_terminated) else {
+ continue;
+ };
+
+ let local_offset = offset + idx + token.content_start;
+ match token.kind {
+ TokenKind::Expansion(ExpansionKind::Variable) => {
+ return complete_variable_expansion(&token.content, local_offset);
+ }
+ TokenKind::Expansion(ExpansionKind::Shell) => {
+ return complete_expand(editor, &token, None, local_offset);
+ }
+ TokenKind::ExpansionKind => {
+ return complete_expansion_kind(&token.content, local_offset);
+ }
+ _ => continue,
}
- return Some(format!("{}\nAliases: {}", doc, aliases.join(", ")).into());
}
+ }
- None
- });
-
- // Calculate initial completion
- prompt.recalculate_completion(cx.editor);
- cx.push_layer(Box::new(prompt));
+ match completer {
+ // If no expansions were found and an argument is being completed,
+ Some(completer) if start == 0 => completer(editor, &token.content)
+ .into_iter()
+ .map(|(range, span)| quote_completion(token, range, span, offset))
+ .collect(),
+ _ => Vec::new(),
+ }
}
-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)
- }
+fn complete_variable_expansion(content: &str, offset: usize) -> Vec<ui::prompt::Completion> {
+ use expansion::Variable;
+
+ fuzzy_match(
+ content,
+ Variable::VARIANTS.iter().map(Variable::as_str),
+ false,
+ )
+ .into_iter()
+ .map(|(name, _)| (offset.., (*name).into()))
+ .collect()
}
-#[test]
-fn test_argument_number_of() {
- let cases = vec![
- ("set-option", 0),
- ("set-option ", 0),
- ("set-option a", 0),
- ("set-option asdf", 0),
- ("set-option asdf ", 1),
- ("set-option asdf xyz", 1),
- ("set-option asdf xyz abc", 2),
- ("set-option asdf xyz abc ", 3),
- ];
+fn complete_expansion_kind(content: &str, offset: usize) -> Vec<ui::prompt::Completion> {
+ use command_line::ExpansionKind;
- for case in cases {
- assert_eq!(case.1, argument_number_of(&Shellwords::from(case.0)));
- }
+ fuzzy_match(
+ content,
+ // Skip `ExpansionKind::Variable` since its kind string is empty.
+ ExpansionKind::VARIANTS
+ .iter()
+ .skip(1)
+ .map(ExpansionKind::as_str),
+ false,
+ )
+ .into_iter()
+ .map(|(name, _)| (offset.., (*name).into()))
+ .collect()
}