aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBlaž Hrastnik2022-03-04 04:42:47 +0000
committerBlaž Hrastnik2022-03-07 05:41:28 +0000
commit19247ff0ec448a07b1f57b646738a606f73f61b5 (patch)
tree7145eeedd6da99c435080ee56bd0e619b353d800
parent9bfb0caf1b4bafdac8eb964f38f7820740056fff (diff)
Split out typable commands into a separate file
-rw-r--r--helix-term/src/commands.rs1491
-rw-r--r--helix-term/src/commands/typed.rs1465
-rw-r--r--xtask/src/main.rs2
3 files changed, 1478 insertions, 1480 deletions
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index 2a8f462d..45ab7659 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -1,8 +1,10 @@
pub(crate) mod dap;
pub(crate) mod lsp;
+pub(crate) mod typed;
pub use dap::*;
pub use lsp::*;
+pub use typed::*;
use helix_core::{
comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes,
@@ -179,7 +181,7 @@ impl MappableCommand {
match &self {
Self::Typable { name, args, doc: _ } => {
let args: Vec<Cow<str>> = args.iter().map(Cow::from).collect();
- if let Some(command) = cmd::TYPABLE_COMMAND_MAP.get(name.as_str()) {
+ if let Some(command) = typed::TYPABLE_COMMAND_MAP.get(name.as_str()) {
let mut cx = compositor::Context {
editor: cx.editor,
jobs: cx.jobs,
@@ -459,7 +461,7 @@ impl std::str::FromStr for MappableCommand {
let args = typable_command
.map(|s| s.to_owned())
.collect::<Vec<String>>();
- cmd::TYPABLE_COMMAND_MAP
+ typed::TYPABLE_COMMAND_MAP
.get(name)
.map(|cmd| MappableCommand::Typable {
name: cmd.name.to_owned(),
@@ -2017,1473 +2019,6 @@ fn append_mode(cx: &mut Context) {
doc.set_selection(view.id, selection);
}
-pub mod cmd {
- use super::*;
-
- use helix_view::editor::Action;
- use ui::completers::{self, Completer};
-
- #[derive(Clone)]
- pub struct TypableCommand {
- pub name: &'static str,
- 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 completer: Option<Completer>,
- }
-
- fn quit(
- cx: &mut compositor::Context,
- _args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- // last view and we have unsaved changes
- if cx.editor.tree.views().count() == 1 {
- buffers_remaining_impl(cx.editor)?
- }
-
- cx.editor.close(view!(cx.editor).id);
-
- Ok(())
- }
-
- fn force_quit(
- cx: &mut compositor::Context,
- _args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- cx.editor.close(view!(cx.editor).id);
-
- Ok(())
- }
-
- fn open(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- ensure!(!args.is_empty(), "wrong argument count");
- for arg in args {
- let (path, pos) = args::parse_file(arg);
- let _ = cx.editor.open(path, Action::Replace)?;
- let (view, doc) = current!(cx.editor);
- let pos = Selection::point(pos_at_coords(doc.text().slice(..), pos, true));
- doc.set_selection(view.id, pos);
- // does not affect opening a buffer without pos
- align_view(doc, view, Align::Center);
- }
- Ok(())
- }
-
- fn buffer_close_by_ids_impl(
- editor: &mut Editor,
- doc_ids: &[DocumentId],
- force: bool,
- ) -> anyhow::Result<()> {
- for &doc_id in doc_ids {
- editor.close_document(doc_id, force)?;
- }
-
- Ok(())
- }
-
- fn buffer_gather_paths_impl(editor: &mut Editor, args: &[Cow<str>]) -> Vec<DocumentId> {
- // No arguments implies current document
- if args.is_empty() {
- let doc_id = view!(editor).doc;
- return vec![doc_id];
- }
-
- let mut nonexistent_buffers = vec![];
- let mut document_ids = vec![];
- for arg in args {
- let doc_id = editor.documents().find_map(|doc| {
- let arg_path = Some(Path::new(arg.as_ref()));
- if doc.path().map(|p| p.as_path()) == arg_path
- || doc.relative_path().as_deref() == arg_path
- {
- Some(doc.id())
- } else {
- None
- }
- });
-
- match doc_id {
- Some(doc_id) => document_ids.push(doc_id),
- None => nonexistent_buffers.push(format!("'{}'", arg)),
- }
- }
-
- if !nonexistent_buffers.is_empty() {
- editor.set_error(format!(
- "cannot close non-existent buffers: {}",
- nonexistent_buffers.join(", ")
- ));
- }
-
- document_ids
- }
-
- fn buffer_close(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- let document_ids = buffer_gather_paths_impl(cx.editor, args);
- buffer_close_by_ids_impl(cx.editor, &document_ids, false)
- }
-
- fn force_buffer_close(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- let document_ids = buffer_gather_paths_impl(cx.editor, args);
- buffer_close_by_ids_impl(cx.editor, &document_ids, true)
- }
-
- fn buffer_gather_others_impl(editor: &mut Editor) -> Vec<DocumentId> {
- let current_document = &doc!(editor).id();
- editor
- .documents()
- .map(|doc| doc.id())
- .filter(|doc_id| doc_id != current_document)
- .collect()
- }
-
- fn buffer_close_others(
- cx: &mut compositor::Context,
- _args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- let document_ids = buffer_gather_others_impl(cx.editor);
- buffer_close_by_ids_impl(cx.editor, &document_ids, false)
- }
-
- fn force_buffer_close_others(
- cx: &mut compositor::Context,
- _args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- let document_ids = buffer_gather_others_impl(cx.editor);
- buffer_close_by_ids_impl(cx.editor, &document_ids, true)
- }
-
- fn buffer_gather_all_impl(editor: &mut Editor) -> Vec<DocumentId> {
- editor.documents().map(|doc| doc.id()).collect()
- }
-
- fn buffer_close_all(
- cx: &mut compositor::Context,
- _args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- let document_ids = buffer_gather_all_impl(cx.editor);
- buffer_close_by_ids_impl(cx.editor, &document_ids, false)
- }
-
- fn force_buffer_close_all(
- cx: &mut compositor::Context,
- _args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- let document_ids = buffer_gather_all_impl(cx.editor);
- buffer_close_by_ids_impl(cx.editor, &document_ids, true)
- }
-
- fn write_impl(cx: &mut compositor::Context, path: Option<&Cow<str>>) -> anyhow::Result<()> {
- let jobs = &mut cx.jobs;
- let doc = doc_mut!(cx.editor);
-
- if let Some(ref path) = path {
- doc.set_path(Some(path.as_ref().as_ref()))
- .context("invalid filepath")?;
- }
- if doc.path().is_none() {
- bail!("cannot write a buffer without a filename");
- }
- let fmt = doc.auto_format().map(|fmt| {
- let shared = fmt.shared();
- let callback = make_format_callback(
- doc.id(),
- doc.version(),
- Modified::SetUnmodified,
- shared.clone(),
- );
- jobs.callback(callback);
- shared
- });
- let future = doc.format_and_save(fmt);
- cx.jobs.add(Job::new(future).wait_before_exiting());
-
- if path.is_some() {
- let id = doc.id();
- let _ = cx.editor.refresh_language_server(id);
- }
- Ok(())
- }
-
- fn write(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- write_impl(cx, args.first())
- }
-
- fn new_file(
- cx: &mut compositor::Context,
- _args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- cx.editor.new_file(Action::Replace);
-
- Ok(())
- }
-
- fn format(
- cx: &mut compositor::Context,
- _args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- let doc = doc!(cx.editor);
- if let Some(format) = doc.format() {
- let callback =
- make_format_callback(doc.id(), doc.version(), Modified::LeaveModified, format);
- cx.jobs.callback(callback);
- }
-
- Ok(())
- }
- fn set_indent_style(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- use IndentStyle::*;
-
- // If no argument, report current indent style.
- if args.is_empty() {
- let style = doc!(cx.editor).indent_style;
- cx.editor.set_status(match style {
- Tabs => "tabs".to_owned(),
- Spaces(1) => "1 space".to_owned(),
- Spaces(n) if (2..=8).contains(&n) => format!("{} spaces", n),
- _ => unreachable!(), // Shouldn't happen.
- });
- return Ok(());
- }
-
- // Attempt to parse argument as an indent style.
- let style = match args.get(0) {
- Some(arg) if "tabs".starts_with(&arg.to_lowercase()) => Some(Tabs),
- Some(Cow::Borrowed("0")) => Some(Tabs),
- Some(arg) => arg
- .parse::<u8>()
- .ok()
- .filter(|n| (1..=8).contains(n))
- .map(Spaces),
- _ => None,
- };
-
- let style = style.context("invalid indent style")?;
- let doc = doc_mut!(cx.editor);
- doc.indent_style = style;
-
- Ok(())
- }
-
- /// Sets or reports the current document's line ending setting.
- fn set_line_ending(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- use LineEnding::*;
-
- // If no argument, report current line ending setting.
- if args.is_empty() {
- let line_ending = doc!(cx.editor).line_ending;
- cx.editor.set_status(match line_ending {
- Crlf => "crlf",
- LF => "line feed",
- FF => "form feed",
- CR => "carriage return",
- Nel => "next line",
-
- // These should never be a document's default line ending.
- VT | LS | PS => "error",
- });
-
- return Ok(());
- }
-
- let arg = args
- .get(0)
- .context("argument missing")?
- .to_ascii_lowercase();
-
- // Attempt to parse argument as a line ending.
- let line_ending = match arg {
- // We check for CR first because it shares a common prefix with CRLF.
- arg if arg.starts_with("cr") => CR,
- arg if arg.starts_with("crlf") => Crlf,
- arg if arg.starts_with("lf") => LF,
- arg if arg.starts_with("ff") => FF,
- arg if arg.starts_with("nel") => Nel,
- _ => bail!("invalid line ending"),
- };
-
- doc_mut!(cx.editor).line_ending = line_ending;
- Ok(())
- }
-
- fn earlier(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- let uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?;
-
- let (view, doc) = current!(cx.editor);
- let success = doc.earlier(view.id, uk);
- if !success {
- cx.editor.set_status("Already at oldest change");
- }
-
- Ok(())
- }
-
- fn later(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- let uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?;
- let (view, doc) = current!(cx.editor);
- let success = doc.later(view.id, uk);
- if !success {
- cx.editor.set_status("Already at newest change");
- }
-
- Ok(())
- }
-
- fn write_quit(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- event: PromptEvent,
- ) -> anyhow::Result<()> {
- write_impl(cx, args.first())?;
- quit(cx, &[], event)
- }
-
- fn force_write_quit(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- event: PromptEvent,
- ) -> anyhow::Result<()> {
- write_impl(cx, args.first())?;
- force_quit(cx, &[], event)
- }
-
- /// Results an error if there are modified buffers remaining and sets editor error,
- /// otherwise returns `Ok(())`
- pub(super) fn buffers_remaining_impl(editor: &mut Editor) -> anyhow::Result<()> {
- let modified: Vec<_> = editor
- .documents()
- .filter(|doc| doc.is_modified())
- .map(|doc| {
- doc.relative_path()
- .map(|path| path.to_string_lossy().to_string())
- .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into())
- })
- .collect();
- if !modified.is_empty() {
- bail!(
- "{} unsaved buffer(s) remaining: {:?}",
- modified.len(),
- modified
- );
- }
- Ok(())
- }
-
- fn write_all_impl(
- cx: &mut compositor::Context,
- _args: &[Cow<str>],
- _event: PromptEvent,
- quit: bool,
- force: bool,
- ) -> anyhow::Result<()> {
- let mut errors = String::new();
- let jobs = &mut cx.jobs;
- // save all documents
- for doc in &mut cx.editor.documents.values_mut() {
- if doc.path().is_none() {
- errors.push_str("cannot write a buffer without a filename\n");
- continue;
- }
-
- if !doc.is_modified() {
- continue;
- }
-
- let fmt = doc.auto_format().map(|fmt| {
- let shared = fmt.shared();
- let callback = make_format_callback(
- doc.id(),
- doc.version(),
- Modified::SetUnmodified,
- shared.clone(),
- );
- jobs.callback(callback);
- shared
- });
- let future = doc.format_and_save(fmt);
- jobs.add(Job::new(future).wait_before_exiting());
- }
-
- if quit {
- if !force {
- buffers_remaining_impl(cx.editor)?;
- }
-
- // close all views
- let views: Vec<_> = cx.editor.tree.views().map(|(view, _)| view.id).collect();
- for view_id in views {
- cx.editor.close(view_id);
- }
- }
-
- bail!(errors)
- }
-
- fn write_all(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- event: PromptEvent,
- ) -> anyhow::Result<()> {
- write_all_impl(cx, args, event, false, false)
- }
-
- fn write_all_quit(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- event: PromptEvent,
- ) -> anyhow::Result<()> {
- write_all_impl(cx, args, event, true, false)
- }
-
- fn force_write_all_quit(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- event: PromptEvent,
- ) -> anyhow::Result<()> {
- write_all_impl(cx, args, event, true, true)
- }
-
- fn quit_all_impl(editor: &mut Editor, force: bool) -> anyhow::Result<()> {
- if !force {
- buffers_remaining_impl(editor)?;
- }
-
- // close all views
- let views: Vec<_> = editor.tree.views().map(|(view, _)| view.id).collect();
- for view_id in views {
- editor.close(view_id);
- }
-
- Ok(())
- }
-
- fn quit_all(
- cx: &mut compositor::Context,
- _args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- quit_all_impl(cx.editor, false)
- }
-
- fn force_quit_all(
- cx: &mut compositor::Context,
- _args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- quit_all_impl(cx.editor, true)
- }
-
- fn cquit(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- let exit_code = args
- .first()
- .and_then(|code| code.parse::<i32>().ok())
- .unwrap_or(1);
- cx.editor.exit_code = exit_code;
-
- quit_all_impl(cx.editor, false)
- }
-
- fn force_cquit(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- let exit_code = args
- .first()
- .and_then(|code| code.parse::<i32>().ok())
- .unwrap_or(1);
- cx.editor.exit_code = exit_code;
-
- quit_all_impl(cx.editor, true)
- }
-
- fn theme(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- let theme = args.first().context("Theme not provided")?;
- let theme = cx
- .editor
- .theme_loader
- .load(theme)
- .with_context(|| format!("Failed setting theme {}", theme))?;
- let true_color = cx.editor.config.true_color || crate::true_color();
- if !(true_color || theme.is_16_color()) {
- bail!("Unsupported theme: theme requires true color support");
- }
- cx.editor.set_theme(theme);
- Ok(())
- }
-
- fn yank_main_selection_to_clipboard(
- cx: &mut compositor::Context,
- _args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Clipboard)
- }
-
- fn yank_joined_to_clipboard(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- 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_to_clipboard_impl(cx.editor, separator, ClipboardType::Clipboard)
- }
-
- fn yank_main_selection_to_primary_clipboard(
- cx: &mut compositor::Context,
- _args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Selection)
- }
-
- fn yank_joined_to_primary_clipboard(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- 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_to_clipboard_impl(cx.editor, separator, ClipboardType::Selection)
- }
-
- fn paste_clipboard_after(
- cx: &mut compositor::Context,
- _args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Clipboard, 1)
- }
-
- fn paste_clipboard_before(
- cx: &mut compositor::Context,
- _args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Clipboard, 1)
- }
-
- fn paste_primary_clipboard_after(
- cx: &mut compositor::Context,
- _args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Selection, 1)
- }
-
- fn paste_primary_clipboard_before(
- cx: &mut compositor::Context,
- _args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Selection, 1)
- }
-
- fn replace_selections_with_clipboard_impl(
- cx: &mut compositor::Context,
- clipboard_type: ClipboardType,
- ) -> anyhow::Result<()> {
- let (view, doc) = current!(cx.editor);
-
- match cx.editor.clipboard_provider.get_contents(clipboard_type) {
- Ok(contents) => {
- let selection = doc.selection(view.id);
- let transaction =
- Transaction::change_by_selection(doc.text(), selection, |range| {
- (range.from(), range.to(), Some(contents.as_str().into()))
- });
-
- doc.apply(&transaction, view.id);
- doc.append_changes_to_history(view.id);
- Ok(())
- }
- Err(e) => Err(e.context("Couldn't get system clipboard contents")),
- }
- }
-
- fn replace_selections_with_clipboard(
- cx: &mut compositor::Context,
- _args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- replace_selections_with_clipboard_impl(cx, ClipboardType::Clipboard)
- }
-
- fn replace_selections_with_primary_clipboard(
- cx: &mut compositor::Context,
- _args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- replace_selections_with_clipboard_impl(cx, ClipboardType::Selection)
- }
-
- fn show_clipboard_provider(
- cx: &mut compositor::Context,
- _args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- cx.editor
- .set_status(cx.editor.clipboard_provider.name().to_string());
- Ok(())
- }
-
- fn change_current_directory(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- let dir = helix_core::path::expand_tilde(
- args.first()
- .context("target directory not provided")?
- .as_ref()
- .as_ref(),
- );
-
- if let Err(e) = std::env::set_current_dir(dir) {
- bail!("Couldn't change the current working directory: {}", e);
- }
-
- let cwd = std::env::current_dir().context("Couldn't get the new working directory")?;
- cx.editor.set_status(format!(
- "Current working directory is now {}",
- cwd.display()
- ));
- Ok(())
- }
-
- fn show_current_directory(
- cx: &mut compositor::Context,
- _args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- let cwd = std::env::current_dir().context("Couldn't get the new working directory")?;
- cx.editor
- .set_status(format!("Current working directory is {}", cwd.display()));
- Ok(())
- }
-
- /// Sets the [`Document`]'s encoding..
- fn set_encoding(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- let doc = doc_mut!(cx.editor);
- if let Some(label) = args.first() {
- doc.set_encoding(label)
- } else {
- let encoding = doc.encoding().name().to_owned();
- cx.editor.set_status(encoding);
- Ok(())
- }
- }
-
- /// Reload the [`Document`] from its source file.
- fn reload(
- cx: &mut compositor::Context,
- _args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- let (view, doc) = current!(cx.editor);
- doc.reload(view.id)
- }
-
- fn tree_sitter_scopes(
- cx: &mut compositor::Context,
- _args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- let (view, doc) = current!(cx.editor);
- let text = doc.text().slice(..);
-
- let pos = doc.selection(view.id).primary().cursor(text);
- let scopes = indent::get_scopes(doc.syntax(), text, pos);
- cx.editor.set_status(format!("scopes: {:?}", &scopes));
- Ok(())
- }
-
- fn vsplit(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- let id = view!(cx.editor).doc;
-
- if args.is_empty() {
- cx.editor.switch(id, Action::VerticalSplit);
- } else {
- for arg in args {
- cx.editor
- .open(PathBuf::from(arg.as_ref()), Action::VerticalSplit)?;
- }
- }
-
- Ok(())
- }
-
- fn hsplit(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- let id = view!(cx.editor).doc;
-
- if args.is_empty() {
- cx.editor.switch(id, Action::HorizontalSplit);
- } else {
- for arg in args {
- cx.editor
- .open(PathBuf::from(arg.as_ref()), Action::HorizontalSplit)?;
- }
- }
-
- Ok(())
- }
-
- fn debug_eval(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- if let Some(debugger) = cx.editor.debugger.as_mut() {
- let (frame, thread_id) = match (debugger.active_frame, debugger.thread_id) {
- (Some(frame), Some(thread_id)) => (frame, thread_id),
- _ => {
- bail!("Cannot find current stack frame to access variables")
- }
- };
-
- // 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)))?;
- cx.editor.set_status(response.result);
- }
- Ok(())
- }
-
- fn debug_start(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- 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))
- }
-
- fn debug_remote(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- 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))
- }
-
- fn tutor(
- cx: &mut compositor::Context,
- _args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- let path = helix_core::runtime_dir().join("tutor.txt");
- cx.editor.open(path, Action::Replace)?;
- // Unset path to prevent accidentally saving to the original tutor file.
- doc_mut!(cx.editor).set_path(None)?;
- Ok(())
- }
-
- pub(super) fn goto_line_number(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- ensure!(!args.is_empty(), "Line number required");
-
- let line = args[0].parse::<usize>()?;
-
- goto_line_impl(cx.editor, NonZeroUsize::new(line));
-
- let (view, doc) = current!(cx.editor);
-
- view.ensure_cursor_in_view(doc, line);
- Ok(())
- }
-
- fn setting(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- let runtime_config = &mut cx.editor.config;
-
- if args.len() != 2 {
- anyhow::bail!("Bad arguments. Usage: `:set key field`");
- }
-
- let (key, arg) = (&args[0].to_lowercase(), &args[1]);
-
- match key.as_ref() {
- "scrolloff" => runtime_config.scrolloff = arg.parse()?,
- "scroll-lines" => runtime_config.scroll_lines = arg.parse()?,
- "mouse" => runtime_config.mouse = arg.parse()?,
- "line-number" => runtime_config.line_number = arg.parse()?,
- "middle-click_paste" => runtime_config.middle_click_paste = arg.parse()?,
- "auto-pairs" => runtime_config.auto_pairs = arg.parse()?,
- "auto-completion" => runtime_config.auto_completion = arg.parse()?,
- "completion-trigger-len" => runtime_config.completion_trigger_len = arg.parse()?,
- "auto-info" => runtime_config.auto_info = arg.parse()?,
- "true-color" => runtime_config.true_color = arg.parse()?,
- "search.smart-case" => runtime_config.search.smart_case = arg.parse()?,
- "search.wrap-around" => runtime_config.search.wrap_around = arg.parse()?,
- _ => anyhow::bail!("Unknown key `{}`.", args[0]),
- }
-
- Ok(())
- }
-
- fn sort(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- sort_impl(cx, args, false)
- }
-
- fn sort_reverse(
- cx: &mut compositor::Context,
- args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- sort_impl(cx, args, true)
- }
-
- fn sort_impl(
- cx: &mut compositor::Context,
- _args: &[Cow<str>],
- reverse: bool,
- ) -> anyhow::Result<()> {
- let (view, doc) = current!(cx.editor);
- let text = doc.text().slice(..);
-
- let selection = doc.selection(view.id);
-
- let mut fragments: Vec<_> = selection
- .fragments(text)
- .map(|fragment| Tendril::from(fragment.as_ref()))
- .collect();
-
- fragments.sort_by(match reverse {
- true => |a: &Tendril, b: &Tendril| b.cmp(a),
- false => |a: &Tendril, b: &Tendril| a.cmp(b),
- });
-
- let transaction = Transaction::change(
- doc.text(),
- selection
- .into_iter()
- .zip(fragments)
- .map(|(s, fragment)| (s.from(), s.to(), Some(fragment))),
- );
-
- doc.apply(&transaction, view.id);
- doc.append_changes_to_history(view.id);
-
- Ok(())
- }
-
- fn tree_sitter_subtree(
- cx: &mut compositor::Context,
- _args: &[Cow<str>],
- _event: PromptEvent,
- ) -> anyhow::Result<()> {
- let (view, doc) = current!(cx.editor);
-
- if let Some(syntax) = doc.syntax() {
- let primary_selection = doc.selection(view.id).primary();
- let text = doc.text();
- let from = text.char_to_byte(primary_selection.from());
- let to = text.char_to_byte(primary_selection.to());
- if let Some(selected_node) = syntax
- .tree()
- .root_node()
- .descendant_for_byte_range(from, to)
- {
- let contents = format!("```tsq\n{}\n```", selected_node.to_sexp());
-
- let callback = async move {
- let call: job::Callback =
- Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
- let contents = ui::Markdown::new(contents, editor.syn_loader.clone());
- let popup = Popup::new("hover", contents).auto_close(true);
- compositor.replace_or_push("hover", popup);
- });
- Ok(call)
- };
-
- cx.jobs.callback(callback);
- }
- }
-
- Ok(())
- }
-
- pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
- TypableCommand {
- name: "quit",
- aliases: &["q"],
- doc: "Close the current view.",
- fun: quit,
- completer: None,
- },
- TypableCommand {
- name: "quit!",
- aliases: &["q!"],
- doc: "Close the current view forcefully (ignoring unsaved changes).",
- fun: force_quit,
- completer: None,
- },
- TypableCommand {
- name: "open",
- aliases: &["o"],
- doc: "Open a file from disk into the current view.",
- fun: open,
- completer: Some(completers::filename),
- },
- TypableCommand {
- name: "buffer-close",
- aliases: &["bc", "bclose"],
- doc: "Close the current buffer.",
- fun: buffer_close,
- completer: Some(completers::buffer),
- },
- TypableCommand {
- name: "buffer-close!",
- aliases: &["bc!", "bclose!"],
- doc: "Close the current buffer forcefully (ignoring unsaved changes).",
- fun: force_buffer_close,
- completer: Some(completers::buffer),
- },
- TypableCommand {
- name: "buffer-close-others",
- aliases: &["bco", "bcloseother"],
- doc: "Close all buffers but the currently focused one.",
- fun: buffer_close_others,
- completer: None,
- },
- TypableCommand {
- name: "buffer-close-others!",
- aliases: &["bco!", "bcloseother!"],
- doc: "Close all buffers but the currently focused one.",
- fun: force_buffer_close_others,
- completer: None,
- },
- TypableCommand {
- name: "buffer-close-all",
- aliases: &["bca", "bcloseall"],
- doc: "Close all buffers, without quiting.",
- fun: buffer_close_all,
- completer: None,
- },
- TypableCommand {
- name: "buffer-close-all!",
- aliases: &["bca!", "bcloseall!"],
- doc: "Close all buffers forcefully (ignoring unsaved changes), without quiting.",
- fun: force_buffer_close_all,
- completer: None,
- },
- TypableCommand {
- name: "write",
- aliases: &["w"],
- doc: "Write changes to disk. Accepts an optional path (:write some/path.txt)",
- fun: write,
- completer: Some(completers::filename),
- },
- TypableCommand {
- name: "new",
- aliases: &["n"],
- doc: "Create a new scratch buffer.",
- fun: new_file,
- completer: Some(completers::filename),
- },
- TypableCommand {
- name: "format",
- aliases: &["fmt"],
- doc: "Format the file using the LSP formatter.",
- fun: format,
- completer: None,
- },
- TypableCommand {
- name: "indent-style",
- aliases: &[],
- doc: "Set the indentation style for editing. ('t' for tabs or 1-8 for number of spaces.)",
- fun: set_indent_style,
- completer: None,
- },
- TypableCommand {
- name: "line-ending",
- aliases: &[],
- doc: "Set the document's default line ending. Options: crlf, lf, cr, ff, nel.",
- fun: set_line_ending,
- completer: None,
- },
- 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,
- completer: None,
- },
- 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,
- completer: None,
- },
- 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,
- completer: Some(completers::filename),
- },
- 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,
- completer: Some(completers::filename),
- },
- TypableCommand {
- name: "write-all",
- aliases: &["wa"],
- doc: "Write changes from all views to disk.",
- fun: write_all,
- completer: None,
- },
- TypableCommand {
- name: "write-quit-all",
- aliases: &["wqa", "xa"],
- doc: "Write changes from all views to disk and close all views.",
- fun: write_all_quit,
- completer: None,
- },
- TypableCommand {
- name: "write-quit-all!",
- aliases: &["wqa!", "xa!"],
- doc: "Write changes from all views to disk and close all views forcefully (ignoring unsaved changes).",
- fun: force_write_all_quit,
- completer: None,
- },
- TypableCommand {
- name: "quit-all",
- aliases: &["qa"],
- doc: "Close all views.",
- fun: quit_all,
- completer: None,
- },
- TypableCommand {
- name: "quit-all!",
- aliases: &["qa!"],
- doc: "Close all views forcefully (ignoring unsaved changes).",
- fun: force_quit_all,
- completer: None,
- },
- TypableCommand {
- name: "cquit",
- aliases: &["cq"],
- doc: "Quit with exit code (default 1). Accepts an optional integer exit code (:cq 2).",
- fun: cquit,
- completer: None,
- },
- TypableCommand {
- name: "cquit!",
- aliases: &["cq!"],
- doc: "Quit with exit code (default 1) forcefully (ignoring unsaved changes). Accepts an optional integer exit code (:cq! 2).",
- fun: force_cquit,
- completer: None,
- },
- TypableCommand {
- name: "theme",
- aliases: &[],
- doc: "Change the editor theme.",
- fun: theme,
- completer: Some(completers::theme),
- },
- TypableCommand {
- name: "clipboard-yank",
- aliases: &[],
- doc: "Yank main selection into system clipboard.",
- fun: yank_main_selection_to_clipboard,
- completer: None,
- },
- 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,
- completer: None,
- },
- TypableCommand {
- name: "primary-clipboard-yank",
- aliases: &[],
- doc: "Yank main selection into system primary clipboard.",
- fun: yank_main_selection_to_primary_clipboard,
- completer: None,
- },
- 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,
- completer: None,
- },
- TypableCommand {
- name: "clipboard-paste-after",
- aliases: &[],
- doc: "Paste system clipboard after selections.",
- fun: paste_clipboard_after,
- completer: None,
- },
- TypableCommand {
- name: "clipboard-paste-before",
- aliases: &[],
- doc: "Paste system clipboard before selections.",
- fun: paste_clipboard_before,
- completer: None,
- },
- TypableCommand {
- name: "clipboard-paste-replace",
- aliases: &[],
- doc: "Replace selections with content of system clipboard.",
- fun: replace_selections_with_clipboard,
- completer: None,
- },
- TypableCommand {
- name: "primary-clipboard-paste-after",
- aliases: &[],
- doc: "Paste primary clipboard after selections.",
- fun: paste_primary_clipboard_after,
- completer: None,
- },
- TypableCommand {
- name: "primary-clipboard-paste-before",
- aliases: &[],
- doc: "Paste primary clipboard before selections.",
- fun: paste_primary_clipboard_before,
- completer: None,
- },
- TypableCommand {
- name: "primary-clipboard-paste-replace",
- aliases: &[],
- doc: "Replace selections with content of system primary clipboard.",
- fun: replace_selections_with_primary_clipboard,
- completer: None,
- },
- TypableCommand {
- name: "show-clipboard-provider",
- aliases: &[],
- doc: "Show clipboard provider name in status bar.",
- fun: show_clipboard_provider,
- completer: None,
- },
- TypableCommand {
- name: "change-current-directory",
- aliases: &["cd"],
- doc: "Change the current working directory.",
- fun: change_current_directory,
- completer: Some(completers::directory),
- },
- TypableCommand {
- name: "show-directory",
- aliases: &["pwd"],
- doc: "Show the current working directory.",
- fun: show_current_directory,
- completer: None,
- },
- TypableCommand {
- name: "encoding",
- aliases: &[],
- doc: "Set encoding based on `https://encoding.spec.whatwg.org`",
- fun: set_encoding,
- completer: None,
- },
- TypableCommand {
- name: "reload",
- aliases: &[],
- doc: "Discard changes and reload from the source file.",
- fun: reload,
- completer: None,
- },
- TypableCommand {
- name: "tree-sitter-scopes",
- aliases: &[],
- doc: "Display tree sitter scopes, primarily for theming and development.",
- fun: tree_sitter_scopes,
- completer: None,
- },
- TypableCommand {
- name: "debug-start",
- aliases: &["dbg"],
- doc: "Start a debug session from a given template with given parameters.",
- fun: debug_start,
- completer: None,
- },
- 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,
- completer: None,
- },
- TypableCommand {
- name: "debug-eval",
- aliases: &[],
- doc: "Evaluate expression in current debug context.",
- fun: debug_eval,
- completer: None,
- },
- TypableCommand {
- name: "vsplit",
- aliases: &["vs"],
- doc: "Open the file in a vertical split.",
- fun: vsplit,
- completer: Some(completers::filename),
- },
- TypableCommand {
- name: "hsplit",
- aliases: &["hs", "sp"],
- doc: "Open the file in a horizontal split.",
- fun: hsplit,
- completer: Some(completers::filename),
- },
- TypableCommand {
- name: "tutor",
- aliases: &[],
- doc: "Open the tutorial.",
- fun: tutor,
- completer: None,
- },
- TypableCommand {
- name: "goto",
- aliases: &["g"],
- doc: "Go to line number.",
- fun: goto_line_number,
- completer: None,
- },
- TypableCommand {
- name: "set-option",
- aliases: &["set"],
- doc: "Set a config option at runtime",
- fun: setting,
- completer: Some(completers::setting),
- },
- TypableCommand {
- name: "sort",
- aliases: &[],
- doc: "Sort ranges in selection.",
- fun: sort,
- completer: None,
- },
- TypableCommand {
- name: "rsort",
- aliases: &[],
- doc: "Sort ranges in selection in reverse order.",
- fun: sort_reverse,
- completer: None,
- },
- TypableCommand {
- name: "tree-sitter-subtree",
- aliases: &["ts-subtree"],
- doc: "Display tree sitter subtree under cursor, primarily for debugging queries.",
- fun: tree_sitter_subtree,
- completer: None,
- },
- ];
-
- pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> =
- Lazy::new(|| {
- TYPABLE_COMMAND_LIST
- .iter()
- .flat_map(|cmd| {
- std::iter::once((cmd.name, cmd))
- .chain(cmd.aliases.iter().map(move |&alias| (alias, cmd)))
- })
- .collect()
- });
-}
-
-fn command_mode(cx: &mut Context) {
- let mut prompt = Prompt::new(
- ":".into(),
- Some(':'),
- |editor: &Editor, input: &str| {
- static FUZZY_MATCHER: Lazy<fuzzy_matcher::skim::SkimMatcherV2> =
- Lazy::new(fuzzy_matcher::skim::SkimMatcherV2::default);
-
- // we use .this over split_whitespace() because we care about empty segments
- let parts = input.split(' ').collect::<Vec<&str>>();
-
- // simple heuristic: if there's no just one part, complete command name.
- // if there's a space, per command completion kicks in.
- if parts.len() <= 1 {
- let mut matches: Vec<_> = cmd::TYPABLE_COMMAND_LIST
- .iter()
- .filter_map(|command| {
- FUZZY_MATCHER
- .fuzzy_match(command.name, input)
- .map(|score| (command.name, score))
- })
- .collect();
-
- matches.sort_unstable_by_key(|(_file, score)| std::cmp::Reverse(*score));
- matches
- .into_iter()
- .map(|(name, _)| (0.., name.into()))
- .collect()
- } else {
- let part = parts.last().unwrap();
-
- if let Some(cmd::TypableCommand {
- completer: Some(completer),
- ..
- }) = cmd::TYPABLE_COMMAND_MAP.get(parts[0])
- {
- completer(editor, part)
- .into_iter()
- .map(|(range, file)| {
- // offset ranges to input
- let offset = input.len() - part.len();
- let range = (range.start + offset)..;
- (range, file)
- })
- .collect()
- } else {
- Vec::new()
- }
- }
- }, // completion
- move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
- if event != PromptEvent::Validate {
- return;
- }
-
- let parts = input.split_whitespace().collect::<Vec<&str>>();
- if parts.is_empty() {
- return;
- }
-
- // 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) = cmd::goto_line_number(cx, &[Cow::from(parts[0])], event) {
- cx.editor.set_error(format!("{}", e));
- }
- return;
- }
-
- // Handle typable commands
- if let Some(cmd) = cmd::TYPABLE_COMMAND_MAP.get(parts[0]) {
- let args = if cfg!(unix) {
- shellwords::shellwords(input)
- } else {
- // Windows doesn't support POSIX, so fallback for now
- parts
- .into_iter()
- .map(|part| part.into())
- .collect::<Vec<_>>()
- };
-
- if let Err(e) = (cmd.fun)(cx, &args[1..], event) {
- cx.editor.set_error(format!("{}", e));
- }
- } else {
- 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();
-
- if let Some(cmd::TypableCommand { doc, aliases, .. }) = cmd::TYPABLE_COMMAND_MAP.get(part) {
- if aliases.is_empty() {
- return Some((*doc).into());
- }
- return Some(format!("{}\nAliases: {}", doc, aliases.join(", ")).into());
- }
-
- None
- });
-
- // Calculate initial completion
- prompt.recalculate_completion(cx.editor);
- cx.push_layer(Box::new(prompt));
-}
-
fn file_picker(cx: &mut Context) {
// We don't specify language markers, root will be the root of the current git repo
let root = find_root(None, &[]).unwrap_or_else(|| PathBuf::from("./"));
@@ -3567,15 +2102,13 @@ pub fn command_palette(cx: &mut Context) {
compositor.find::<ui::EditorView>().unwrap().keymaps[&doc.mode].reverse_map();
let mut commands: Vec<MappableCommand> = MappableCommand::STATIC_COMMAND_LIST.into();
- commands.extend(
- cmd::TYPABLE_COMMAND_LIST
- .iter()
- .map(|cmd| MappableCommand::Typable {
- name: cmd.name.to_owned(),
- doc: cmd.doc.to_owned(),
- args: Vec::new(),
- }),
- );
+ commands.extend(typed::TYPABLE_COMMAND_LIST.iter().map(|cmd| {
+ MappableCommand::Typable {
+ name: cmd.name.to_owned(),
+ doc: cmd.doc.to_owned(),
+ args: Vec::new(),
+ }
+ }));
// formats key bindings, multiple bindings are comma separated,
// individual key presses are joined with `+`
@@ -5260,7 +3793,7 @@ fn vsplit(cx: &mut Context) {
fn wclose(cx: &mut Context) {
if cx.editor.tree.views().count() == 1 {
- if let Err(err) = cmd::buffers_remaining_impl(cx.editor) {
+ if let Err(err) = typed::buffers_remaining_impl(cx.editor) {
cx.editor.set_error(err.to_string());
return;
}
diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs
new file mode 100644
index 00000000..4cc996d6
--- /dev/null
+++ b/helix-term/src/commands/typed.rs
@@ -0,0 +1,1465 @@
+use super::*;
+
+use helix_view::editor::Action;
+use ui::completers::{self, Completer};
+
+#[derive(Clone)]
+pub struct TypableCommand {
+ pub name: &'static str,
+ 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 completer: Option<Completer>,
+}
+
+fn quit(
+ cx: &mut compositor::Context,
+ _args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ // last view and we have unsaved changes
+ if cx.editor.tree.views().count() == 1 {
+ buffers_remaining_impl(cx.editor)?
+ }
+
+ cx.editor.close(view!(cx.editor).id);
+
+ Ok(())
+}
+
+fn force_quit(
+ cx: &mut compositor::Context,
+ _args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ cx.editor.close(view!(cx.editor).id);
+
+ Ok(())
+}
+
+fn open(
+ cx: &mut compositor::Context,
+ args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ ensure!(!args.is_empty(), "wrong argument count");
+ for arg in args {
+ let (path, pos) = args::parse_file(arg);
+ let _ = cx.editor.open(path, Action::Replace)?;
+ let (view, doc) = current!(cx.editor);
+ let pos = Selection::point(pos_at_coords(doc.text().slice(..), pos, true));
+ doc.set_selection(view.id, pos);
+ // does not affect opening a buffer without pos
+ align_view(doc, view, Align::Center);
+ }
+ Ok(())
+}
+
+fn buffer_close_by_ids_impl(
+ editor: &mut Editor,
+ doc_ids: &[DocumentId],
+ force: bool,
+) -> anyhow::Result<()> {
+ for &doc_id in doc_ids {
+ editor.close_document(doc_id, force)?;
+ }
+
+ Ok(())
+}
+
+fn buffer_gather_paths_impl(editor: &mut Editor, args: &[Cow<str>]) -> Vec<DocumentId> {
+ // No arguments implies current document
+ if args.is_empty() {
+ let doc_id = view!(editor).doc;
+ return vec![doc_id];
+ }
+
+ let mut nonexistent_buffers = vec![];
+ let mut document_ids = vec![];
+ for arg in args {
+ let doc_id = editor.documents().find_map(|doc| {
+ let arg_path = Some(Path::new(arg.as_ref()));
+ if doc.path().map(|p| p.as_path()) == arg_path
+ || doc.relative_path().as_deref() == arg_path
+ {
+ Some(doc.id())
+ } else {
+ None
+ }
+ });
+
+ match doc_id {
+ Some(doc_id) => document_ids.push(doc_id),
+ None => nonexistent_buffers.push(format!("'{}'", arg)),
+ }
+ }
+
+ if !nonexistent_buffers.is_empty() {
+ editor.set_error(format!(
+ "cannot close non-existent buffers: {}",
+ nonexistent_buffers.join(", ")
+ ));
+ }
+
+ document_ids
+}
+
+fn buffer_close(
+ cx: &mut compositor::Context,
+ args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ let document_ids = buffer_gather_paths_impl(cx.editor, args);
+ buffer_close_by_ids_impl(cx.editor, &document_ids, false)
+}
+
+fn force_buffer_close(
+ cx: &mut compositor::Context,
+ args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ let document_ids = buffer_gather_paths_impl(cx.editor, args);
+ buffer_close_by_ids_impl(cx.editor, &document_ids, true)
+}
+
+fn buffer_gather_others_impl(editor: &mut Editor) -> Vec<DocumentId> {
+ let current_document = &doc!(editor).id();
+ editor
+ .documents()
+ .map(|doc| doc.id())
+ .filter(|doc_id| doc_id != current_document)
+ .collect()
+}
+
+fn buffer_close_others(
+ cx: &mut compositor::Context,
+ _args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ let document_ids = buffer_gather_others_impl(cx.editor);
+ buffer_close_by_ids_impl(cx.editor, &document_ids, false)
+}
+
+fn force_buffer_close_others(
+ cx: &mut compositor::Context,
+ _args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ let document_ids = buffer_gather_others_impl(cx.editor);
+ buffer_close_by_ids_impl(cx.editor, &document_ids, true)
+}
+
+fn buffer_gather_all_impl(editor: &mut Editor) -> Vec<DocumentId> {
+ editor.documents().map(|doc| doc.id()).collect()
+}
+
+fn buffer_close_all(
+ cx: &mut compositor::Context,
+ _args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ let document_ids = buffer_gather_all_impl(cx.editor);
+ buffer_close_by_ids_impl(cx.editor, &document_ids, false)
+}
+
+fn force_buffer_close_all(
+ cx: &mut compositor::Context,
+ _args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ let document_ids = buffer_gather_all_impl(cx.editor);
+ buffer_close_by_ids_impl(cx.editor, &document_ids, true)
+}
+
+fn write_impl(cx: &mut compositor::Context, path: Option<&Cow<str>>) -> anyhow::Result<()> {
+ let jobs = &mut cx.jobs;
+ let doc = doc_mut!(cx.editor);
+
+ if let Some(ref path) = path {
+ doc.set_path(Some(path.as_ref().as_ref()))
+ .context("invalid filepath")?;
+ }
+ if doc.path().is_none() {
+ bail!("cannot write a buffer without a filename");
+ }
+ let fmt = doc.auto_format().map(|fmt| {
+ let shared = fmt.shared();
+ let callback = make_format_callback(
+ doc.id(),
+ doc.version(),
+ Modified::SetUnmodified,
+ shared.clone(),
+ );
+ jobs.callback(callback);
+ shared
+ });
+ let future = doc.format_and_save(fmt);
+ cx.jobs.add(Job::new(future).wait_before_exiting());
+
+ if path.is_some() {
+ let id = doc.id();
+ let _ = cx.editor.refresh_language_server(id);
+ }
+ Ok(())
+}
+
+fn write(
+ cx: &mut compositor::Context,
+ args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ write_impl(cx, args.first())
+}
+
+fn new_file(
+ cx: &mut compositor::Context,
+ _args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ cx.editor.new_file(Action::Replace);
+
+ Ok(())
+}
+
+fn format(
+ cx: &mut compositor::Context,
+ _args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ let doc = doc!(cx.editor);
+ if let Some(format) = doc.format() {
+ let callback =
+ make_format_callback(doc.id(), doc.version(), Modified::LeaveModified, format);
+ cx.jobs.callback(callback);
+ }
+
+ Ok(())
+}
+fn set_indent_style(
+ cx: &mut compositor::Context,
+ args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ use IndentStyle::*;
+
+ // If no argument, report current indent style.
+ if args.is_empty() {
+ let style = doc!(cx.editor).indent_style;
+ cx.editor.set_status(match style {
+ Tabs => "tabs".to_owned(),
+ Spaces(1) => "1 space".to_owned(),
+ Spaces(n) if (2..=8).contains(&n) => format!("{} spaces", n),
+ _ => unreachable!(), // Shouldn't happen.
+ });
+ return Ok(());
+ }
+
+ // Attempt to parse argument as an indent style.
+ let style = match args.get(0) {
+ Some(arg) if "tabs".starts_with(&arg.to_lowercase()) => Some(Tabs),
+ Some(Cow::Borrowed("0")) => Some(Tabs),
+ Some(arg) => arg
+ .parse::<u8>()
+ .ok()
+ .filter(|n| (1..=8).contains(n))
+ .map(Spaces),
+ _ => None,
+ };
+
+ let style = style.context("invalid indent style")?;
+ let doc = doc_mut!(cx.editor);
+ doc.indent_style = style;
+
+ Ok(())
+}
+
+/// Sets or reports the current document's line ending setting.
+fn set_line_ending(
+ cx: &mut compositor::Context,
+ args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ use LineEnding::*;
+
+ // If no argument, report current line ending setting.
+ if args.is_empty() {
+ let line_ending = doc!(cx.editor).line_ending;
+ cx.editor.set_status(match line_ending {
+ Crlf => "crlf",
+ LF => "line feed",
+ FF => "form feed",
+ CR => "carriage return",
+ Nel => "next line",
+
+ // These should never be a document's default line ending.
+ VT | LS | PS => "error",
+ });
+
+ return Ok(());
+ }
+
+ let arg = args
+ .get(0)
+ .context("argument missing")?
+ .to_ascii_lowercase();
+
+ // Attempt to parse argument as a line ending.
+ let line_ending = match arg {
+ // We check for CR first because it shares a common prefix with CRLF.
+ arg if arg.starts_with("cr") => CR,
+ arg if arg.starts_with("crlf") => Crlf,
+ arg if arg.starts_with("lf") => LF,
+ arg if arg.starts_with("ff") => FF,
+ arg if arg.starts_with("nel") => Nel,
+ _ => bail!("invalid line ending"),
+ };
+
+ doc_mut!(cx.editor).line_ending = line_ending;
+ Ok(())
+}
+
+fn earlier(
+ cx: &mut compositor::Context,
+ args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ let uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?;
+
+ let (view, doc) = current!(cx.editor);
+ let success = doc.earlier(view.id, uk);
+ if !success {
+ cx.editor.set_status("Already at oldest change");
+ }
+
+ Ok(())
+}
+
+fn later(
+ cx: &mut compositor::Context,
+ args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ let uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?;
+ let (view, doc) = current!(cx.editor);
+ let success = doc.later(view.id, uk);
+ if !success {
+ cx.editor.set_status("Already at newest change");
+ }
+
+ Ok(())
+}
+
+fn write_quit(
+ cx: &mut compositor::Context,
+ args: &[Cow<str>],
+ event: PromptEvent,
+) -> anyhow::Result<()> {
+ write_impl(cx, args.first())?;
+ quit(cx, &[], event)
+}
+
+fn force_write_quit(
+ cx: &mut compositor::Context,
+ args: &[Cow<str>],
+ event: PromptEvent,
+) -> anyhow::Result<()> {
+ write_impl(cx, args.first())?;
+ force_quit(cx, &[], event)
+}
+
+/// Results an error if there are modified buffers remaining and sets editor error,
+/// otherwise returns `Ok(())`
+pub(super) fn buffers_remaining_impl(editor: &mut Editor) -> anyhow::Result<()> {
+ let modified: Vec<_> = editor
+ .documents()
+ .filter(|doc| doc.is_modified())
+ .map(|doc| {
+ doc.relative_path()
+ .map(|path| path.to_string_lossy().to_string())
+ .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into())
+ })
+ .collect();
+ if !modified.is_empty() {
+ bail!(
+ "{} unsaved buffer(s) remaining: {:?}",
+ modified.len(),
+ modified
+ );
+ }
+ Ok(())
+}
+
+fn write_all_impl(
+ cx: &mut compositor::Context,
+ _args: &[Cow<str>],
+ _event: PromptEvent,
+ quit: bool,
+ force: bool,
+) -> anyhow::Result<()> {
+ let mut errors = String::new();
+ let jobs = &mut cx.jobs;
+ // save all documents
+ for doc in &mut cx.editor.documents.values_mut() {
+ if doc.path().is_none() {
+ errors.push_str("cannot write a buffer without a filename\n");
+ continue;
+ }
+
+ if !doc.is_modified() {
+ continue;
+ }
+
+ let fmt = doc.auto_format().map(|fmt| {
+ let shared = fmt.shared();
+ let callback = make_format_callback(
+ doc.id(),
+ doc.version(),
+ Modified::SetUnmodified,
+ shared.clone(),
+ );
+ jobs.callback(callback);
+ shared
+ });
+ let future = doc.format_and_save(fmt);
+ jobs.add(Job::new(future).wait_before_exiting());
+ }
+
+ if quit {
+ if !force {
+ buffers_remaining_impl(cx.editor)?;
+ }
+
+ // close all views
+ let views: Vec<_> = cx.editor.tree.views().map(|(view, _)| view.id).collect();
+ for view_id in views {
+ cx.editor.close(view_id);
+ }
+ }
+
+ bail!(errors)
+}
+
+fn write_all(
+ cx: &mut compositor::Context,
+ args: &[Cow<str>],
+ event: PromptEvent,
+) -> anyhow::Result<()> {
+ write_all_impl(cx, args, event, false, false)
+}
+
+fn write_all_quit(
+ cx: &mut compositor::Context,
+ args: &[Cow<str>],
+ event: PromptEvent,
+) -> anyhow::Result<()> {
+ write_all_impl(cx, args, event, true, false)
+}
+
+fn force_write_all_quit(
+ cx: &mut compositor::Context,
+ args: &[Cow<str>],
+ event: PromptEvent,
+) -> anyhow::Result<()> {
+ write_all_impl(cx, args, event, true, true)
+}
+
+fn quit_all_impl(editor: &mut Editor, force: bool) -> anyhow::Result<()> {
+ if !force {
+ buffers_remaining_impl(editor)?;
+ }
+
+ // close all views
+ let views: Vec<_> = editor.tree.views().map(|(view, _)| view.id).collect();
+ for view_id in views {
+ editor.close(view_id);
+ }
+
+ Ok(())
+}
+
+fn quit_all(
+ cx: &mut compositor::Context,
+ _args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ quit_all_impl(cx.editor, false)
+}
+
+fn force_quit_all(
+ cx: &mut compositor::Context,
+ _args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ quit_all_impl(cx.editor, true)
+}
+
+fn cquit(
+ cx: &mut compositor::Context,
+ args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ let exit_code = args
+ .first()
+ .and_then(|code| code.parse::<i32>().ok())
+ .unwrap_or(1);
+ cx.editor.exit_code = exit_code;
+
+ quit_all_impl(cx.editor, false)
+}
+
+fn force_cquit(
+ cx: &mut compositor::Context,
+ args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ let exit_code = args
+ .first()
+ .and_then(|code| code.parse::<i32>().ok())
+ .unwrap_or(1);
+ cx.editor.exit_code = exit_code;
+
+ quit_all_impl(cx.editor, true)
+}
+
+fn theme(
+ cx: &mut compositor::Context,
+ args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ let theme = args.first().context("Theme not provided")?;
+ let theme = cx
+ .editor
+ .theme_loader
+ .load(theme)
+ .with_context(|| format!("Failed setting theme {}", theme))?;
+ let true_color = cx.editor.config.true_color || crate::true_color();
+ if !(true_color || theme.is_16_color()) {
+ bail!("Unsupported theme: theme requires true color support");
+ }
+ cx.editor.set_theme(theme);
+ Ok(())
+}
+
+fn yank_main_selection_to_clipboard(
+ cx: &mut compositor::Context,
+ _args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Clipboard)
+}
+
+fn yank_joined_to_clipboard(
+ cx: &mut compositor::Context,
+ args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ 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_to_clipboard_impl(cx.editor, separator, ClipboardType::Clipboard)
+}
+
+fn yank_main_selection_to_primary_clipboard(
+ cx: &mut compositor::Context,
+ _args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Selection)
+}
+
+fn yank_joined_to_primary_clipboard(
+ cx: &mut compositor::Context,
+ args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ 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_to_clipboard_impl(cx.editor, separator, ClipboardType::Selection)
+}
+
+fn paste_clipboard_after(
+ cx: &mut compositor::Context,
+ _args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Clipboard, 1)
+}
+
+fn paste_clipboard_before(
+ cx: &mut compositor::Context,
+ _args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Clipboard, 1)
+}
+
+fn paste_primary_clipboard_after(
+ cx: &mut compositor::Context,
+ _args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Selection, 1)
+}
+
+fn paste_primary_clipboard_before(
+ cx: &mut compositor::Context,
+ _args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Selection, 1)
+}
+
+fn replace_selections_with_clipboard_impl(
+ cx: &mut compositor::Context,
+ clipboard_type: ClipboardType,
+) -> anyhow::Result<()> {
+ let (view, doc) = current!(cx.editor);
+
+ match cx.editor.clipboard_provider.get_contents(clipboard_type) {
+ Ok(contents) => {
+ let selection = doc.selection(view.id);
+ let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
+ (range.from(), range.to(), Some(contents.as_str().into()))
+ });
+
+ doc.apply(&transaction, view.id);
+ doc.append_changes_to_history(view.id);
+ Ok(())
+ }
+ Err(e) => Err(e.context("Couldn't get system clipboard contents")),
+ }
+}
+
+fn replace_selections_with_clipboard(
+ cx: &mut compositor::Context,
+ _args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ replace_selections_with_clipboard_impl(cx, ClipboardType::Clipboard)
+}
+
+fn replace_selections_with_primary_clipboard(
+ cx: &mut compositor::Context,
+ _args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ replace_selections_with_clipboard_impl(cx, ClipboardType::Selection)
+}
+
+fn show_clipboard_provider(
+ cx: &mut compositor::Context,
+ _args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ cx.editor
+ .set_status(cx.editor.clipboard_provider.name().to_string());
+ Ok(())
+}
+
+fn change_current_directory(
+ cx: &mut compositor::Context,
+ args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ let dir = helix_core::path::expand_tilde(
+ args.first()
+ .context("target directory not provided")?
+ .as_ref()
+ .as_ref(),
+ );
+
+ if let Err(e) = std::env::set_current_dir(dir) {
+ bail!("Couldn't change the current working directory: {}", e);
+ }
+
+ let cwd = std::env::current_dir().context("Couldn't get the new working directory")?;
+ cx.editor.set_status(format!(
+ "Current working directory is now {}",
+ cwd.display()
+ ));
+ Ok(())
+}
+
+fn show_current_directory(
+ cx: &mut compositor::Context,
+ _args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ let cwd = std::env::current_dir().context("Couldn't get the new working directory")?;
+ cx.editor
+ .set_status(format!("Current working directory is {}", cwd.display()));
+ Ok(())
+}
+
+/// Sets the [`Document`]'s encoding..
+fn set_encoding(
+ cx: &mut compositor::Context,
+ args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ let doc = doc_mut!(cx.editor);
+ if let Some(label) = args.first() {
+ doc.set_encoding(label)
+ } else {
+ let encoding = doc.encoding().name().to_owned();
+ cx.editor.set_status(encoding);
+ Ok(())
+ }
+}
+
+/// Reload the [`Document`] from its source file.
+fn reload(
+ cx: &mut compositor::Context,
+ _args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ let (view, doc) = current!(cx.editor);
+ doc.reload(view.id)
+}
+
+fn tree_sitter_scopes(
+ cx: &mut compositor::Context,
+ _args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ let (view, doc) = current!(cx.editor);
+ let text = doc.text().slice(..);
+
+ let pos = doc.selection(view.id).primary().cursor(text);
+ let scopes = indent::get_scopes(doc.syntax(), text, pos);
+ cx.editor.set_status(format!("scopes: {:?}", &scopes));
+ Ok(())
+}
+
+fn vsplit(
+ cx: &mut compositor::Context,
+ args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ let id = view!(cx.editor).doc;
+
+ if args.is_empty() {
+ cx.editor.switch(id, Action::VerticalSplit);
+ } else {
+ for arg in args {
+ cx.editor
+ .open(PathBuf::from(arg.as_ref()), Action::VerticalSplit)?;
+ }
+ }
+
+ Ok(())
+}
+
+fn hsplit(
+ cx: &mut compositor::Context,
+ args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ let id = view!(cx.editor).doc;
+
+ if args.is_empty() {
+ cx.editor.switch(id, Action::HorizontalSplit);
+ } else {
+ for arg in args {
+ cx.editor
+ .open(PathBuf::from(arg.as_ref()), Action::HorizontalSplit)?;
+ }
+ }
+
+ Ok(())
+}
+
+fn debug_eval(
+ cx: &mut compositor::Context,
+ args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ if let Some(debugger) = cx.editor.debugger.as_mut() {
+ let (frame, thread_id) = match (debugger.active_frame, debugger.thread_id) {
+ (Some(frame), Some(thread_id)) => (frame, thread_id),
+ _ => {
+ bail!("Cannot find current stack frame to access variables")
+ }
+ };
+
+ // 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)))?;
+ cx.editor.set_status(response.result);
+ }
+ Ok(())
+}
+
+fn debug_start(
+ cx: &mut compositor::Context,
+ args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ 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))
+}
+
+fn debug_remote(
+ cx: &mut compositor::Context,
+ args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ 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))
+}
+
+fn tutor(
+ cx: &mut compositor::Context,
+ _args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ let path = helix_core::runtime_dir().join("tutor.txt");
+ cx.editor.open(path, Action::Replace)?;
+ // Unset path to prevent accidentally saving to the original tutor file.
+ doc_mut!(cx.editor).set_path(None)?;
+ Ok(())
+}
+
+pub(super) fn goto_line_number(
+ cx: &mut compositor::Context,
+ args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ ensure!(!args.is_empty(), "Line number required");
+
+ let line = args[0].parse::<usize>()?;
+
+ goto_line_impl(cx.editor, NonZeroUsize::new(line));
+
+ let (view, doc) = current!(cx.editor);
+
+ view.ensure_cursor_in_view(doc, line);
+ Ok(())
+}
+
+fn setting(
+ cx: &mut compositor::Context,
+ args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ let runtime_config = &mut cx.editor.config;
+
+ if args.len() != 2 {
+ anyhow::bail!("Bad arguments. Usage: `:set key field`");
+ }
+
+ let (key, arg) = (&args[0].to_lowercase(), &args[1]);
+
+ match key.as_ref() {
+ "scrolloff" => runtime_config.scrolloff = arg.parse()?,
+ "scroll-lines" => runtime_config.scroll_lines = arg.parse()?,
+ "mouse" => runtime_config.mouse = arg.parse()?,
+ "line-number" => runtime_config.line_number = arg.parse()?,
+ "middle-click_paste" => runtime_config.middle_click_paste = arg.parse()?,
+ "auto-pairs" => runtime_config.auto_pairs = arg.parse()?,
+ "auto-completion" => runtime_config.auto_completion = arg.parse()?,
+ "completion-trigger-len" => runtime_config.completion_trigger_len = arg.parse()?,
+ "auto-info" => runtime_config.auto_info = arg.parse()?,
+ "true-color" => runtime_config.true_color = arg.parse()?,
+ "search.smart-case" => runtime_config.search.smart_case = arg.parse()?,
+ "search.wrap-around" => runtime_config.search.wrap_around = arg.parse()?,
+ _ => anyhow::bail!("Unknown key `{}`.", args[0]),
+ }
+
+ Ok(())
+}
+
+fn sort(
+ cx: &mut compositor::Context,
+ args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ sort_impl(cx, args, false)
+}
+
+fn sort_reverse(
+ cx: &mut compositor::Context,
+ args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ sort_impl(cx, args, true)
+}
+
+fn sort_impl(
+ cx: &mut compositor::Context,
+ _args: &[Cow<str>],
+ reverse: bool,
+) -> anyhow::Result<()> {
+ let (view, doc) = current!(cx.editor);
+ let text = doc.text().slice(..);
+
+ let selection = doc.selection(view.id);
+
+ let mut fragments: Vec<_> = selection
+ .fragments(text)
+ .map(|fragment| Tendril::from(fragment.as_ref()))
+ .collect();
+
+ fragments.sort_by(match reverse {
+ true => |a: &Tendril, b: &Tendril| b.cmp(a),
+ false => |a: &Tendril, b: &Tendril| a.cmp(b),
+ });
+
+ let transaction = Transaction::change(
+ doc.text(),
+ selection
+ .into_iter()
+ .zip(fragments)
+ .map(|(s, fragment)| (s.from(), s.to(), Some(fragment))),
+ );
+
+ doc.apply(&transaction, view.id);
+ doc.append_changes_to_history(view.id);
+
+ Ok(())
+}
+
+fn tree_sitter_subtree(
+ cx: &mut compositor::Context,
+ _args: &[Cow<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ let (view, doc) = current!(cx.editor);
+
+ if let Some(syntax) = doc.syntax() {
+ let primary_selection = doc.selection(view.id).primary();
+ let text = doc.text();
+ let from = text.char_to_byte(primary_selection.from());
+ let to = text.char_to_byte(primary_selection.to());
+ if let Some(selected_node) = syntax
+ .tree()
+ .root_node()
+ .descendant_for_byte_range(from, to)
+ {
+ let contents = format!("```tsq\n{}\n```", selected_node.to_sexp());
+
+ let callback = async move {
+ let call: job::Callback =
+ Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
+ let contents = ui::Markdown::new(contents, editor.syn_loader.clone());
+ let popup = Popup::new("hover", contents).auto_close(true);
+ compositor.replace_or_push("hover", popup);
+ });
+ Ok(call)
+ };
+
+ cx.jobs.callback(callback);
+ }
+ }
+
+ Ok(())
+}
+
+pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
+ TypableCommand {
+ name: "quit",
+ aliases: &["q"],
+ doc: "Close the current view.",
+ fun: quit,
+ completer: None,
+ },
+ TypableCommand {
+ name: "quit!",
+ aliases: &["q!"],
+ doc: "Close the current view forcefully (ignoring unsaved changes).",
+ fun: force_quit,
+ completer: None,
+ },
+ TypableCommand {
+ name: "open",
+ aliases: &["o"],
+ doc: "Open a file from disk into the current view.",
+ fun: open,
+ completer: Some(completers::filename),
+ },
+ TypableCommand {
+ name: "buffer-close",
+ aliases: &["bc", "bclose"],
+ doc: "Close the current buffer.",
+ fun: buffer_close,
+ completer: Some(completers::buffer),
+ },
+ TypableCommand {
+ name: "buffer-close!",
+ aliases: &["bc!", "bclose!"],
+ doc: "Close the current buffer forcefully (ignoring unsaved changes).",
+ fun: force_buffer_close,
+ completer: Some(completers::buffer),
+ },
+ TypableCommand {
+ name: "buffer-close-others",
+ aliases: &["bco", "bcloseother"],
+ doc: "Close all buffers but the currently focused one.",
+ fun: buffer_close_others,
+ completer: None,
+ },
+ TypableCommand {
+ name: "buffer-close-others!",
+ aliases: &["bco!", "bcloseother!"],
+ doc: "Close all buffers but the currently focused one.",
+ fun: force_buffer_close_others,
+ completer: None,
+ },
+ TypableCommand {
+ name: "buffer-close-all",
+ aliases: &["bca", "bcloseall"],
+ doc: "Close all buffers, without quiting.",
+ fun: buffer_close_all,
+ completer: None,
+ },
+ TypableCommand {
+ name: "buffer-close-all!",
+ aliases: &["bca!", "bcloseall!"],
+ doc: "Close all buffers forcefully (ignoring unsaved changes), without quiting.",
+ fun: force_buffer_close_all,
+ completer: None,
+ },
+ TypableCommand {
+ name: "write",
+ aliases: &["w"],
+ doc: "Write changes to disk. Accepts an optional path (:write some/path.txt)",
+ fun: write,
+ completer: Some(completers::filename),
+ },
+ TypableCommand {
+ name: "new",
+ aliases: &["n"],
+ doc: "Create a new scratch buffer.",
+ fun: new_file,
+ completer: Some(completers::filename),
+ },
+ TypableCommand {
+ name: "format",
+ aliases: &["fmt"],
+ doc: "Format the file using the LSP formatter.",
+ fun: format,
+ completer: None,
+ },
+ TypableCommand {
+ name: "indent-style",
+ aliases: &[],
+ doc: "Set the indentation style for editing. ('t' for tabs or 1-8 for number of spaces.)",
+ fun: set_indent_style,
+ completer: None,
+ },
+ TypableCommand {
+ name: "line-ending",
+ aliases: &[],
+ doc: "Set the document's default line ending. Options: crlf, lf, cr, ff, nel.",
+ fun: set_line_ending,
+ completer: None,
+ },
+ 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,
+ completer: None,
+ },
+ 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,
+ completer: None,
+ },
+ 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,
+ completer: Some(completers::filename),
+ },
+ 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,
+ completer: Some(completers::filename),
+ },
+ TypableCommand {
+ name: "write-all",
+ aliases: &["wa"],
+ doc: "Write changes from all views to disk.",
+ fun: write_all,
+ completer: None,
+ },
+ TypableCommand {
+ name: "write-quit-all",
+ aliases: &["wqa", "xa"],
+ doc: "Write changes from all views to disk and close all views.",
+ fun: write_all_quit,
+ completer: None,
+ },
+ TypableCommand {
+ name: "write-quit-all!",
+ aliases: &["wqa!", "xa!"],
+ doc: "Write changes from all views to disk and close all views forcefully (ignoring unsaved changes).",
+ fun: force_write_all_quit,
+ completer: None,
+ },
+ TypableCommand {
+ name: "quit-all",
+ aliases: &["qa"],
+ doc: "Close all views.",
+ fun: quit_all,
+ completer: None,
+ },
+ TypableCommand {
+ name: "quit-all!",
+ aliases: &["qa!"],
+ doc: "Close all views forcefully (ignoring unsaved changes).",
+ fun: force_quit_all,
+ completer: None,
+ },
+ TypableCommand {
+ name: "cquit",
+ aliases: &["cq"],
+ doc: "Quit with exit code (default 1). Accepts an optional integer exit code (:cq 2).",
+ fun: cquit,
+ completer: None,
+ },
+ TypableCommand {
+ name: "cquit!",
+ aliases: &["cq!"],
+ doc: "Quit with exit code (default 1) forcefully (ignoring unsaved changes). Accepts an optional integer exit code (:cq! 2).",
+ fun: force_cquit,
+ completer: None,
+ },
+ TypableCommand {
+ name: "theme",
+ aliases: &[],
+ doc: "Change the editor theme.",
+ fun: theme,
+ completer: Some(completers::theme),
+ },
+ TypableCommand {
+ name: "clipboard-yank",
+ aliases: &[],
+ doc: "Yank main selection into system clipboard.",
+ fun: yank_main_selection_to_clipboard,
+ completer: None,
+ },
+ 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,
+ completer: None,
+ },
+ TypableCommand {
+ name: "primary-clipboard-yank",
+ aliases: &[],
+ doc: "Yank main selection into system primary clipboard.",
+ fun: yank_main_selection_to_primary_clipboard,
+ completer: None,
+ },
+ 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,
+ completer: None,
+ },
+ TypableCommand {
+ name: "clipboard-paste-after",
+ aliases: &[],
+ doc: "Paste system clipboard after selections.",
+ fun: paste_clipboard_after,
+ completer: None,
+ },
+ TypableCommand {
+ name: "clipboard-paste-before",
+ aliases: &[],
+ doc: "Paste system clipboard before selections.",
+ fun: paste_clipboard_before,
+ completer: None,
+ },
+ TypableCommand {
+ name: "clipboard-paste-replace",
+ aliases: &[],
+ doc: "Replace selections with content of system clipboard.",
+ fun: replace_selections_with_clipboard,
+ completer: None,
+ },
+ TypableCommand {
+ name: "primary-clipboard-paste-after",
+ aliases: &[],
+ doc: "Paste primary clipboard after selections.",
+ fun: paste_primary_clipboard_after,
+ completer: None,
+ },
+ TypableCommand {
+ name: "primary-clipboard-paste-before",
+ aliases: &[],
+ doc: "Paste primary clipboard before selections.",
+ fun: paste_primary_clipboard_before,
+ completer: None,
+ },
+ TypableCommand {
+ name: "primary-clipboard-paste-replace",
+ aliases: &[],
+ doc: "Replace selections with content of system primary clipboard.",
+ fun: replace_selections_with_primary_clipboard,
+ completer: None,
+ },
+ TypableCommand {
+ name: "show-clipboard-provider",
+ aliases: &[],
+ doc: "Show clipboard provider name in status bar.",
+ fun: show_clipboard_provider,
+ completer: None,
+ },
+ TypableCommand {
+ name: "change-current-directory",
+ aliases: &["cd"],
+ doc: "Change the current working directory.",
+ fun: change_current_directory,
+ completer: Some(completers::directory),
+ },
+ TypableCommand {
+ name: "show-directory",
+ aliases: &["pwd"],
+ doc: "Show the current working directory.",
+ fun: show_current_directory,
+ completer: None,
+ },
+ TypableCommand {
+ name: "encoding",
+ aliases: &[],
+ doc: "Set encoding based on `https://encoding.spec.whatwg.org`",
+ fun: set_encoding,
+ completer: None,
+ },
+ TypableCommand {
+ name: "reload",
+ aliases: &[],
+ doc: "Discard changes and reload from the source file.",
+ fun: reload,
+ completer: None,
+ },
+ TypableCommand {
+ name: "tree-sitter-scopes",
+ aliases: &[],
+ doc: "Display tree sitter scopes, primarily for theming and development.",
+ fun: tree_sitter_scopes,
+ completer: None,
+ },
+ TypableCommand {
+ name: "debug-start",
+ aliases: &["dbg"],
+ doc: "Start a debug session from a given template with given parameters.",
+ fun: debug_start,
+ completer: None,
+ },
+ 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,
+ completer: None,
+ },
+ TypableCommand {
+ name: "debug-eval",
+ aliases: &[],
+ doc: "Evaluate expression in current debug context.",
+ fun: debug_eval,
+ completer: None,
+ },
+ TypableCommand {
+ name: "vsplit",
+ aliases: &["vs"],
+ doc: "Open the file in a vertical split.",
+ fun: vsplit,
+ completer: Some(completers::filename),
+ },
+ TypableCommand {
+ name: "hsplit",
+ aliases: &["hs", "sp"],
+ doc: "Open the file in a horizontal split.",
+ fun: hsplit,
+ completer: Some(completers::filename),
+ },
+ TypableCommand {
+ name: "tutor",
+ aliases: &[],
+ doc: "Open the tutorial.",
+ fun: tutor,
+ completer: None,
+ },
+ TypableCommand {
+ name: "goto",
+ aliases: &["g"],
+ doc: "Go to line number.",
+ fun: goto_line_number,
+ completer: None,
+ },
+ TypableCommand {
+ name: "set-option",
+ aliases: &["set"],
+ doc: "Set a config option at runtime",
+ fun: setting,
+ completer: Some(completers::setting),
+ },
+ TypableCommand {
+ name: "sort",
+ aliases: &[],
+ doc: "Sort ranges in selection.",
+ fun: sort,
+ completer: None,
+ },
+ TypableCommand {
+ name: "rsort",
+ aliases: &[],
+ doc: "Sort ranges in selection in reverse order.",
+ fun: sort_reverse,
+ completer: None,
+ },
+ TypableCommand {
+ name: "tree-sitter-subtree",
+ aliases: &["ts-subtree"],
+ doc: "Display tree sitter subtree under cursor, primarily for debugging queries.",
+ fun: tree_sitter_subtree,
+ completer: None,
+ },
+ ];
+
+pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> =
+ Lazy::new(|| {
+ TYPABLE_COMMAND_LIST
+ .iter()
+ .flat_map(|cmd| {
+ std::iter::once((cmd.name, cmd))
+ .chain(cmd.aliases.iter().map(move |&alias| (alias, cmd)))
+ })
+ .collect()
+ });
+
+pub fn command_mode(cx: &mut Context) {
+ let mut prompt = Prompt::new(
+ ":".into(),
+ Some(':'),
+ |editor: &Editor, input: &str| {
+ static FUZZY_MATCHER: Lazy<fuzzy_matcher::skim::SkimMatcherV2> =
+ Lazy::new(fuzzy_matcher::skim::SkimMatcherV2::default);
+
+ // we use .this over split_whitespace() because we care about empty segments
+ let parts = input.split(' ').collect::<Vec<&str>>();
+
+ // simple heuristic: if there's no just one part, complete command name.
+ // if there's a space, per command completion kicks in.
+ if parts.len() <= 1 {
+ let mut matches: Vec<_> = typed::TYPABLE_COMMAND_LIST
+ .iter()
+ .filter_map(|command| {
+ FUZZY_MATCHER
+ .fuzzy_match(command.name, input)
+ .map(|score| (command.name, score))
+ })
+ .collect();
+
+ matches.sort_unstable_by_key(|(_file, score)| std::cmp::Reverse(*score));
+ matches
+ .into_iter()
+ .map(|(name, _)| (0.., name.into()))
+ .collect()
+ } else {
+ let part = parts.last().unwrap();
+
+ if let Some(typed::TypableCommand {
+ completer: Some(completer),
+ ..
+ }) = typed::TYPABLE_COMMAND_MAP.get(parts[0])
+ {
+ completer(editor, part)
+ .into_iter()
+ .map(|(range, file)| {
+ // offset ranges to input
+ let offset = input.len() - part.len();
+ let range = (range.start + offset)..;
+ (range, file)
+ })
+ .collect()
+ } else {
+ Vec::new()
+ }
+ }
+ }, // completion
+ move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
+ if event != PromptEvent::Validate {
+ return;
+ }
+
+ let parts = input.split_whitespace().collect::<Vec<&str>>();
+ if parts.is_empty() {
+ return;
+ }
+
+ // 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;
+ }
+
+ // Handle typable commands
+ if let Some(cmd) = typed::TYPABLE_COMMAND_MAP.get(parts[0]) {
+ let args = if cfg!(unix) {
+ shellwords::shellwords(input)
+ } else {
+ // Windows doesn't support POSIX, so fallback for now
+ parts
+ .into_iter()
+ .map(|part| part.into())
+ .collect::<Vec<_>>()
+ };
+
+ if let Err(e) = (cmd.fun)(cx, &args[1..], event) {
+ cx.editor.set_error(format!("{}", e));
+ }
+ } else {
+ 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();
+
+ if let Some(typed::TypableCommand { doc, aliases, .. }) =
+ typed::TYPABLE_COMMAND_MAP.get(part)
+ {
+ if aliases.is_empty() {
+ return Some((*doc).into());
+ }
+ return Some(format!("{}\nAliases: {}", doc, aliases.join(", ")).into());
+ }
+
+ None
+ });
+
+ // Calculate initial completion
+ prompt.recalculate_completion(cx.editor);
+ cx.push_layer(Box::new(prompt));
+}
diff --git a/xtask/src/main.rs b/xtask/src/main.rs
index d24a29cc..ad0eb16f 100644
--- a/xtask/src/main.rs
+++ b/xtask/src/main.rs
@@ -107,7 +107,7 @@ pub mod md_gen {
use crate::path;
use std::fs;
- use helix_term::commands::cmd::TYPABLE_COMMAND_LIST;
+ use helix_term::commands::TYPABLE_COMMAND_LIST;
pub const TYPABLE_COMMANDS_MD_OUTPUT: &str = "typable-cmd.md";
pub const LANG_SUPPORT_MD_OUTPUT: &str = "lang-support.md";