diff options
Diffstat (limited to 'helix-term/src/commands.rs')
-rw-r--r-- | helix-term/src/commands.rs | 1491 |
1 files changed, 12 insertions, 1479 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; } |