@@ -1,8 +1,10 @@
1pub(crate) mod dap;
1pub(crate) mod dap;
2pub(crate) mod lsp;
2pub(crate) mod lsp;
4pub use dap::*;
5pub use dap::*;
5pub use lsp::*;
6pub use lsp::*;
7use helix_core::{
9use helix_core::{
8 comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes,
10 comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes,
@@ -179,7 +181,7 @@ impl MappableCommand {
179 match &self {
181 match &self {
180 Self::Typable { name, args, doc: _ } => {
182 Self::Typable { name, args, doc: _ } => {
181 let args: Vec<Cow<str>> = args.iter().map(Cow::from).collect();
183 let args: Vec<Cow<str>> = args.iter().map(Cow::from).collect();
182− if let Some(command) = cmd::TYPABLE_COMMAND_MAP.get(name.as_str()) {
184+ if let Some(command) = typed::TYPABLE_COMMAND_MAP.get(name.as_str()) {
183 let mut cx = compositor::Context {
185 let mut cx = compositor::Context {
184 editor: cx.editor,
186 editor: cx.editor,
185 jobs: cx.jobs,
187 jobs: cx.jobs,
@@ -459,7 +461,7 @@ impl std::str::FromStr for MappableCommand {
459 let args = typable_command
461 let args = typable_command
460 .map(|s| s.to_owned())
462 .map(|s| s.to_owned())
461 .collect::<Vec<String>>();
463 .collect::<Vec<String>>();
462− cmd::TYPABLE_COMMAND_MAP
464+ typed::TYPABLE_COMMAND_MAP
463 .get(name)
465 .get(name)
464 .map(|cmd| MappableCommand::Typable {
466 .map(|cmd| MappableCommand::Typable {
465 name: cmd.name.to_owned(),
467 name: cmd.name.to_owned(),
@@ -2017,1473 +2019,6 @@ fn append_mode(cx: &mut Context) {
2017 doc.set_selection(view.id, selection);
2019 doc.set_selection(view.id, selection);
2023− use helix_view::editor::Action;
2024− use ui::completers::{self, Completer};
2027− pub struct TypableCommand {
2028− pub name: &'static str,
2029− pub aliases: &'static [&'static str],
2030− pub doc: &'static str,
2031− // params, flags, helper, completer
2032− pub fun: fn(&mut compositor::Context, &[Cow<str>], PromptEvent) -> anyhow::Result<()>,
2033− pub completer: Option<Completer>,
2037− cx: &mut compositor::Context,
2038− _args: &[Cow<str>],
2039− _event: PromptEvent,
2040− ) -> anyhow::Result<()> {
2041− // last view and we have unsaved changes
2042− if cx.editor.tree.views().count() == 1 {
2043− buffers_remaining_impl(cx.editor)?
2046− cx.editor.close(view!(cx.editor).id);
2052− cx: &mut compositor::Context,
2053− _args: &[Cow<str>],
2054− _event: PromptEvent,
2055− ) -> anyhow::Result<()> {
2056− cx.editor.close(view!(cx.editor).id);
2062− cx: &mut compositor::Context,
2064− _event: PromptEvent,
2065− ) -> anyhow::Result<()> {
2066− ensure!(!args.is_empty(), "wrong argument count");
2068− let (path, pos) = args::parse_file(arg);
2069− let _ = cx.editor.open(path, Action::Replace)?;
2070− let (view, doc) = current!(cx.editor);
2071− let pos = Selection::point(pos_at_coords(doc.text().slice(..), pos, true));
2072− doc.set_selection(view.id, pos);
2073− // does not affect opening a buffer without pos
2074− align_view(doc, view, Align::Center);
2079− fn buffer_close_by_ids_impl(
2080− editor: &mut Editor,
2081− doc_ids: &[DocumentId],
2083− ) -> anyhow::Result<()> {
2084− for &doc_id in doc_ids {
2085− editor.close_document(doc_id, force)?;
2091− fn buffer_gather_paths_impl(editor: &mut Editor, args: &[Cow<str>]) -> Vec<DocumentId> {
2092− // No arguments implies current document
2093− if args.is_empty() {
2094− let doc_id = view!(editor).doc;
2095− return vec![doc_id];
2098− let mut nonexistent_buffers = vec![];
2099− let mut document_ids = vec![];
2101− let doc_id = editor.documents().find_map(|doc| {
2102− let arg_path = Some(Path::new(arg.as_ref()));
2103− if doc.path().map(|p| p.as_path()) == arg_path
2104− || doc.relative_path().as_deref() == arg_path
2113− Some(doc_id) => document_ids.push(doc_id),
2114− None => nonexistent_buffers.push(format!("'{}'", arg)),
2118− if !nonexistent_buffers.is_empty() {
2119− editor.set_error(format!(
2120− "cannot close non-existent buffers: {}",
2121− nonexistent_buffers.join(", ")
2129− cx: &mut compositor::Context,
2131− _event: PromptEvent,
2132− ) -> anyhow::Result<()> {
2133− let document_ids = buffer_gather_paths_impl(cx.editor, args);
2134− buffer_close_by_ids_impl(cx.editor, &document_ids, false)
2137− fn force_buffer_close(
2138− cx: &mut compositor::Context,
2140− _event: PromptEvent,
2141− ) -> anyhow::Result<()> {
2142− let document_ids = buffer_gather_paths_impl(cx.editor, args);
2143− buffer_close_by_ids_impl(cx.editor, &document_ids, true)
2146− fn buffer_gather_others_impl(editor: &mut Editor) -> Vec<DocumentId> {
2147− let current_document = &doc!(editor).id();
2150− .map(|doc| doc.id())
2151− .filter(|doc_id| doc_id != current_document)
2155− fn buffer_close_others(
2156− cx: &mut compositor::Context,
2157− _args: &[Cow<str>],
2158− _event: PromptEvent,
2159− ) -> anyhow::Result<()> {
2160− let document_ids = buffer_gather_others_impl(cx.editor);
2161− buffer_close_by_ids_impl(cx.editor, &document_ids, false)
2164− fn force_buffer_close_others(
2165− cx: &mut compositor::Context,
2166− _args: &[Cow<str>],
2167− _event: PromptEvent,
2168− ) -> anyhow::Result<()> {
2169− let document_ids = buffer_gather_others_impl(cx.editor);
2170− buffer_close_by_ids_impl(cx.editor, &document_ids, true)
2173− fn buffer_gather_all_impl(editor: &mut Editor) -> Vec<DocumentId> {
2174− editor.documents().map(|doc| doc.id()).collect()
2177− fn buffer_close_all(
2178− cx: &mut compositor::Context,
2179− _args: &[Cow<str>],
2180− _event: PromptEvent,
2181− ) -> anyhow::Result<()> {
2182− let document_ids = buffer_gather_all_impl(cx.editor);
2183− buffer_close_by_ids_impl(cx.editor, &document_ids, false)
2186− fn force_buffer_close_all(
2187− cx: &mut compositor::Context,
2188− _args: &[Cow<str>],
2189− _event: PromptEvent,
2190− ) -> anyhow::Result<()> {
2191− let document_ids = buffer_gather_all_impl(cx.editor);
2192− buffer_close_by_ids_impl(cx.editor, &document_ids, true)
2195− fn write_impl(cx: &mut compositor::Context, path: Option<&Cow<str>>) -> anyhow::Result<()> {
2196− let jobs = &mut cx.jobs;
2197− let doc = doc_mut!(cx.editor);
2199− if let Some(ref path) = path {
2200− doc.set_path(Some(path.as_ref().as_ref()))
2201− .context("invalid filepath")?;
2203− if doc.path().is_none() {
2204− bail!("cannot write a buffer without a filename");
2206− let fmt = doc.auto_format().map(|fmt| {
2207− let shared = fmt.shared();
2208− let callback = make_format_callback(
2211− Modified::SetUnmodified,
2214− jobs.callback(callback);
2217− let future = doc.format_and_save(fmt);
2218− cx.jobs.add(Job::new(future).wait_before_exiting());
2220− if path.is_some() {
2222− let _ = cx.editor.refresh_language_server(id);
2228− cx: &mut compositor::Context,
2230− _event: PromptEvent,
2231− ) -> anyhow::Result<()> {
2232− write_impl(cx, args.first())
2236− cx: &mut compositor::Context,
2237− _args: &[Cow<str>],
2238− _event: PromptEvent,
2239− ) -> anyhow::Result<()> {
2240− cx.editor.new_file(Action::Replace);
2246− cx: &mut compositor::Context,
2247− _args: &[Cow<str>],
2248− _event: PromptEvent,
2249− ) -> anyhow::Result<()> {
2250− let doc = doc!(cx.editor);
2251− if let Some(format) = doc.format() {
2253− make_format_callback(doc.id(), doc.version(), Modified::LeaveModified, format);
2254− cx.jobs.callback(callback);
2259− fn set_indent_style(
2260− cx: &mut compositor::Context,
2262− _event: PromptEvent,
2263− ) -> anyhow::Result<()> {
2264− use IndentStyle::*;
2266− // If no argument, report current indent style.
2267− if args.is_empty() {
2268− let style = doc!(cx.editor).indent_style;
2269− cx.editor.set_status(match style {
2270− Tabs => "tabs".to_owned(),
2271− Spaces(1) => "1 space".to_owned(),
2272− Spaces(n) if (2..=8).contains(&n) => format!("{} spaces", n),
2273− _ => unreachable!(), // Shouldn't happen.
2278− // Attempt to parse argument as an indent style.
2279− let style = match args.get(0) {
2280− Some(arg) if "tabs".starts_with(&arg.to_lowercase()) => Some(Tabs),
2281− Some(Cow::Borrowed("0")) => Some(Tabs),
2285− .filter(|n| (1..=8).contains(n))
2290− let style = style.context("invalid indent style")?;
2291− let doc = doc_mut!(cx.editor);
2292− doc.indent_style = style;
2297− /// Sets or reports the current document's line ending setting.
2298− fn set_line_ending(
2299− cx: &mut compositor::Context,
2301− _event: PromptEvent,
2302− ) -> anyhow::Result<()> {
2305− // If no argument, report current line ending setting.
2306− if args.is_empty() {
2307− let line_ending = doc!(cx.editor).line_ending;
2308− cx.editor.set_status(match line_ending {
2312− CR => "carriage return",
2313− Nel => "next line",
2315− // These should never be a document's default line ending.
2316− VT | LS | PS => "error",
2324− .context("argument missing")?
2325− .to_ascii_lowercase();
2327− // Attempt to parse argument as a line ending.
2328− let line_ending = match arg {
2329− // We check for CR first because it shares a common prefix with CRLF.
2330− arg if arg.starts_with("cr") => CR,
2331− arg if arg.starts_with("crlf") => Crlf,
2332− arg if arg.starts_with("lf") => LF,
2333− arg if arg.starts_with("ff") => FF,
2334− arg if arg.starts_with("nel") => Nel,
2335− _ => bail!("invalid line ending"),
2338− doc_mut!(cx.editor).line_ending = line_ending;
2343− cx: &mut compositor::Context,
2345− _event: PromptEvent,
2346− ) -> anyhow::Result<()> {
2347− let uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?;
2349− let (view, doc) = current!(cx.editor);
2350− let success = doc.earlier(view.id, uk);
2352− cx.editor.set_status("Already at oldest change");
2359− cx: &mut compositor::Context,
2361− _event: PromptEvent,
2362− ) -> anyhow::Result<()> {
2363− let uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?;
2364− let (view, doc) = current!(cx.editor);
2365− let success = doc.later(view.id, uk);
2367− cx.editor.set_status("Already at newest change");
2374− cx: &mut compositor::Context,
2376− event: PromptEvent,
2377− ) -> anyhow::Result<()> {
2378− write_impl(cx, args.first())?;
2379− quit(cx, &[], event)
2382− fn force_write_quit(
2383− cx: &mut compositor::Context,
2385− event: PromptEvent,
2386− ) -> anyhow::Result<()> {
2387− write_impl(cx, args.first())?;
2388− force_quit(cx, &[], event)
2391− /// Results an error if there are modified buffers remaining and sets editor error,
2392− /// otherwise returns `Ok(())`
2393− pub(super) fn buffers_remaining_impl(editor: &mut Editor) -> anyhow::Result<()> {
2394− let modified: Vec<_> = editor
2396− .filter(|doc| doc.is_modified())
2398− doc.relative_path()
2399− .map(|path| path.to_string_lossy().to_string())
2400− .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into())
2403− if !modified.is_empty() {
2405− "{} unsaved buffer(s) remaining: {:?}",
2414− cx: &mut compositor::Context,
2415− _args: &[Cow<str>],
2416− _event: PromptEvent,
2419− ) -> anyhow::Result<()> {
2420− let mut errors = String::new();
2421− let jobs = &mut cx.jobs;
2422− // save all documents
2423− for doc in &mut cx.editor.documents.values_mut() {
2424− if doc.path().is_none() {
2425− errors.push_str("cannot write a buffer without a filename\n");
2429− if !doc.is_modified() {
2433− let fmt = doc.auto_format().map(|fmt| {
2434− let shared = fmt.shared();
2435− let callback = make_format_callback(
2438− Modified::SetUnmodified,
2441− jobs.callback(callback);
2444− let future = doc.format_and_save(fmt);
2445− jobs.add(Job::new(future).wait_before_exiting());
2450− buffers_remaining_impl(cx.editor)?;
2454− let views: Vec<_> = cx.editor.tree.views().map(|(view, _)| view.id).collect();
2455− for view_id in views {
2456− cx.editor.close(view_id);
2464− cx: &mut compositor::Context,
2466− event: PromptEvent,
2467− ) -> anyhow::Result<()> {
2468− write_all_impl(cx, args, event, false, false)
2472− cx: &mut compositor::Context,
2474− event: PromptEvent,
2475− ) -> anyhow::Result<()> {
2476− write_all_impl(cx, args, event, true, false)
2479− fn force_write_all_quit(
2480− cx: &mut compositor::Context,
2482− event: PromptEvent,
2483− ) -> anyhow::Result<()> {
2484− write_all_impl(cx, args, event, true, true)
2487− fn quit_all_impl(editor: &mut Editor, force: bool) -> anyhow::Result<()> {
2489− buffers_remaining_impl(editor)?;
2493− let views: Vec<_> = editor.tree.views().map(|(view, _)| view.id).collect();
2494− for view_id in views {
2495− editor.close(view_id);
2502− cx: &mut compositor::Context,
2503− _args: &[Cow<str>],
2504− _event: PromptEvent,
2505− ) -> anyhow::Result<()> {
2506− quit_all_impl(cx.editor, false)
2510− cx: &mut compositor::Context,
2511− _args: &[Cow<str>],
2512− _event: PromptEvent,
2513− ) -> anyhow::Result<()> {
2514− quit_all_impl(cx.editor, true)
2518− cx: &mut compositor::Context,
2520− _event: PromptEvent,
2521− ) -> anyhow::Result<()> {
2522− let exit_code = args
2524− .and_then(|code| code.parse::<i32>().ok())
2526− cx.editor.exit_code = exit_code;
2528− quit_all_impl(cx.editor, false)
2532− cx: &mut compositor::Context,
2534− _event: PromptEvent,
2535− ) -> anyhow::Result<()> {
2536− let exit_code = args
2538− .and_then(|code| code.parse::<i32>().ok())
2540− cx.editor.exit_code = exit_code;
2542− quit_all_impl(cx.editor, true)
2546− cx: &mut compositor::Context,
2548− _event: PromptEvent,
2549− ) -> anyhow::Result<()> {
2550− let theme = args.first().context("Theme not provided")?;
2555− .with_context(|| format!("Failed setting theme {}", theme))?;
2556− let true_color = cx.editor.config.true_color || crate::true_color();
2557− if !(true_color || theme.is_16_color()) {
2558− bail!("Unsupported theme: theme requires true color support");
2560− cx.editor.set_theme(theme);
2564− fn yank_main_selection_to_clipboard(
2565− cx: &mut compositor::Context,
2566− _args: &[Cow<str>],
2567− _event: PromptEvent,
2568− ) -> anyhow::Result<()> {
2569− yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Clipboard)
2572− fn yank_joined_to_clipboard(
2573− cx: &mut compositor::Context,
2575− _event: PromptEvent,
2576− ) -> anyhow::Result<()> {
2577− let doc = doc!(cx.editor);
2578− let default_sep = Cow::Borrowed(doc.line_ending.as_str());
2579− let separator = args.first().unwrap_or(&default_sep);
2580− yank_joined_to_clipboard_impl(cx.editor, separator, ClipboardType::Clipboard)
2583− fn yank_main_selection_to_primary_clipboard(
2584− cx: &mut compositor::Context,
2585− _args: &[Cow<str>],
2586− _event: PromptEvent,
2587− ) -> anyhow::Result<()> {
2588− yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Selection)
2591− fn yank_joined_to_primary_clipboard(
2592− cx: &mut compositor::Context,
2594− _event: PromptEvent,
2595− ) -> anyhow::Result<()> {
2596− let doc = doc!(cx.editor);
2597− let default_sep = Cow::Borrowed(doc.line_ending.as_str());
2598− let separator = args.first().unwrap_or(&default_sep);
2599− yank_joined_to_clipboard_impl(cx.editor, separator, ClipboardType::Selection)
2602− fn paste_clipboard_after(
2603− cx: &mut compositor::Context,
2604− _args: &[Cow<str>],
2605− _event: PromptEvent,
2606− ) -> anyhow::Result<()> {
2607− paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Clipboard, 1)
2610− fn paste_clipboard_before(
2611− cx: &mut compositor::Context,
2612− _args: &[Cow<str>],
2613− _event: PromptEvent,
2614− ) -> anyhow::Result<()> {
2615− paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Clipboard, 1)
2618− fn paste_primary_clipboard_after(
2619− cx: &mut compositor::Context,
2620− _args: &[Cow<str>],
2621− _event: PromptEvent,
2622− ) -> anyhow::Result<()> {
2623− paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Selection, 1)
2626− fn paste_primary_clipboard_before(
2627− cx: &mut compositor::Context,
2628− _args: &[Cow<str>],
2629− _event: PromptEvent,
2630− ) -> anyhow::Result<()> {
2631− paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Selection, 1)
2634− fn replace_selections_with_clipboard_impl(
2635− cx: &mut compositor::Context,
2636− clipboard_type: ClipboardType,
2637− ) -> anyhow::Result<()> {
2638− let (view, doc) = current!(cx.editor);
2640− match cx.editor.clipboard_provider.get_contents(clipboard_type) {
2642− let selection = doc.selection(view.id);
2644− Transaction::change_by_selection(doc.text(), selection, |range| {
2645− (range.from(), range.to(), Some(contents.as_str().into()))
2648− doc.apply(&transaction, view.id);
2649− doc.append_changes_to_history(view.id);
2652− Err(e) => Err(e.context("Couldn't get system clipboard contents")),
2656− fn replace_selections_with_clipboard(
2657− cx: &mut compositor::Context,
2658− _args: &[Cow<str>],
2659− _event: PromptEvent,
2660− ) -> anyhow::Result<()> {
2661− replace_selections_with_clipboard_impl(cx, ClipboardType::Clipboard)
2664− fn replace_selections_with_primary_clipboard(
2665− cx: &mut compositor::Context,
2666− _args: &[Cow<str>],
2667− _event: PromptEvent,
2668− ) -> anyhow::Result<()> {
2669− replace_selections_with_clipboard_impl(cx, ClipboardType::Selection)
2672− fn show_clipboard_provider(
2673− cx: &mut compositor::Context,
2674− _args: &[Cow<str>],
2675− _event: PromptEvent,
2676− ) -> anyhow::Result<()> {
2678− .set_status(cx.editor.clipboard_provider.name().to_string());
2682− fn change_current_directory(
2683− cx: &mut compositor::Context,
2685− _event: PromptEvent,
2686− ) -> anyhow::Result<()> {
2687− let dir = helix_core::path::expand_tilde(
2689− .context("target directory not provided")?
2694− if let Err(e) = std::env::set_current_dir(dir) {
2695− bail!("Couldn't change the current working directory: {}", e);
2698− let cwd = std::env::current_dir().context("Couldn't get the new working directory")?;
2699− cx.editor.set_status(format!(
2700− "Current working directory is now {}",
2706− fn show_current_directory(
2707− cx: &mut compositor::Context,
2708− _args: &[Cow<str>],
2709− _event: PromptEvent,
2710− ) -> anyhow::Result<()> {
2711− let cwd = std::env::current_dir().context("Couldn't get the new working directory")?;
2713− .set_status(format!("Current working directory is {}", cwd.display()));
2717− /// Sets the [`Document`]'s encoding..
2719− cx: &mut compositor::Context,
2721− _event: PromptEvent,
2722− ) -> anyhow::Result<()> {
2723− let doc = doc_mut!(cx.editor);
2724− if let Some(label) = args.first() {
2725− doc.set_encoding(label)
2727− let encoding = doc.encoding().name().to_owned();
2728− cx.editor.set_status(encoding);
2733− /// Reload the [`Document`] from its source file.
2735− cx: &mut compositor::Context,
2736− _args: &[Cow<str>],
2737− _event: PromptEvent,
2738− ) -> anyhow::Result<()> {
2739− let (view, doc) = current!(cx.editor);
2740− doc.reload(view.id)
2743− fn tree_sitter_scopes(
2744− cx: &mut compositor::Context,
2745− _args: &[Cow<str>],
2746− _event: PromptEvent,
2747− ) -> anyhow::Result<()> {
2748− let (view, doc) = current!(cx.editor);
2749− let text = doc.text().slice(..);
2751− let pos = doc.selection(view.id).primary().cursor(text);
2752− let scopes = indent::get_scopes(doc.syntax(), text, pos);
2753− cx.editor.set_status(format!("scopes: {:?}", &scopes));
2758− cx: &mut compositor::Context,
2760− _event: PromptEvent,
2761− ) -> anyhow::Result<()> {
2762− let id = view!(cx.editor).doc;
2764− if args.is_empty() {
2765− cx.editor.switch(id, Action::VerticalSplit);
2769− .open(PathBuf::from(arg.as_ref()), Action::VerticalSplit)?;
2777− cx: &mut compositor::Context,
2779− _event: PromptEvent,
2780− ) -> anyhow::Result<()> {
2781− let id = view!(cx.editor).doc;
2783− if args.is_empty() {
2784− cx.editor.switch(id, Action::HorizontalSplit);
2788− .open(PathBuf::from(arg.as_ref()), Action::HorizontalSplit)?;
2796− cx: &mut compositor::Context,
2798− _event: PromptEvent,
2799− ) -> anyhow::Result<()> {
2800− if let Some(debugger) = cx.editor.debugger.as_mut() {
2801− let (frame, thread_id) = match (debugger.active_frame, debugger.thread_id) {
2802− (Some(frame), Some(thread_id)) => (frame, thread_id),
2804− bail!("Cannot find current stack frame to access variables")
2808− // TODO: support no frame_id
2810− let frame_id = debugger.stack_frames[&thread_id][frame].id;
2811− let response = helix_lsp::block_on(debugger.eval(args.join(" "), Some(frame_id)))?;
2812− cx.editor.set_status(response.result);
2818− cx: &mut compositor::Context,
2820− _event: PromptEvent,
2821− ) -> anyhow::Result<()> {
2822− let mut args = args.to_owned();
2823− let name = match args.len() {
2825− _ => Some(args.remove(0)),
2827− dap_start_impl(cx, name.as_deref(), None, Some(args))
2831− cx: &mut compositor::Context,
2833− _event: PromptEvent,
2834− ) -> anyhow::Result<()> {
2835− let mut args = args.to_owned();
2836− let address = match args.len() {
2838− _ => Some(args.remove(0).parse()?),
2840− let name = match args.len() {
2842− _ => Some(args.remove(0)),
2844− dap_start_impl(cx, name.as_deref(), address, Some(args))
2848− cx: &mut compositor::Context,
2849− _args: &[Cow<str>],
2850− _event: PromptEvent,
2851− ) -> anyhow::Result<()> {
2852− let path = helix_core::runtime_dir().join("tutor.txt");
2853− cx.editor.open(path, Action::Replace)?;
2854− // Unset path to prevent accidentally saving to the original tutor file.
2855− doc_mut!(cx.editor).set_path(None)?;
2859− pub(super) fn goto_line_number(
2860− cx: &mut compositor::Context,
2862− _event: PromptEvent,
2863− ) -> anyhow::Result<()> {
2864− ensure!(!args.is_empty(), "Line number required");
2866− let line = args[0].parse::<usize>()?;
2868− goto_line_impl(cx.editor, NonZeroUsize::new(line));
2870− let (view, doc) = current!(cx.editor);
2872− view.ensure_cursor_in_view(doc, line);
2877− cx: &mut compositor::Context,
2879− _event: PromptEvent,
2880− ) -> anyhow::Result<()> {
2881− let runtime_config = &mut cx.editor.config;
2883− if args.len() != 2 {
2884− anyhow::bail!("Bad arguments. Usage: `:set key field`");
2887− let (key, arg) = (&args[0].to_lowercase(), &args[1]);
2889− match key.as_ref() {
2890− "scrolloff" => runtime_config.scrolloff = arg.parse()?,
2891− "scroll-lines" => runtime_config.scroll_lines = arg.parse()?,
2892− "mouse" => runtime_config.mouse = arg.parse()?,
2893− "line-number" => runtime_config.line_number = arg.parse()?,
2894− "middle-click_paste" => runtime_config.middle_click_paste = arg.parse()?,
2895− "auto-pairs" => runtime_config.auto_pairs = arg.parse()?,
2896− "auto-completion" => runtime_config.auto_completion = arg.parse()?,
2897− "completion-trigger-len" => runtime_config.completion_trigger_len = arg.parse()?,
2898− "auto-info" => runtime_config.auto_info = arg.parse()?,
2899− "true-color" => runtime_config.true_color = arg.parse()?,
2900− "search.smart-case" => runtime_config.search.smart_case = arg.parse()?,
2901− "search.wrap-around" => runtime_config.search.wrap_around = arg.parse()?,
2902− _ => anyhow::bail!("Unknown key `{}`.", args[0]),
2909− cx: &mut compositor::Context,
2911− _event: PromptEvent,
2912− ) -> anyhow::Result<()> {
2913− sort_impl(cx, args, false)
2917− cx: &mut compositor::Context,
2919− _event: PromptEvent,
2920− ) -> anyhow::Result<()> {
2921− sort_impl(cx, args, true)
2925− cx: &mut compositor::Context,
2926− _args: &[Cow<str>],
2928− ) -> anyhow::Result<()> {
2929− let (view, doc) = current!(cx.editor);
2930− let text = doc.text().slice(..);
2932− let selection = doc.selection(view.id);
2934− let mut fragments: Vec<_> = selection
2936− .map(|fragment| Tendril::from(fragment.as_ref()))
2939− fragments.sort_by(match reverse {
2940− true => |a: &Tendril, b: &Tendril| b.cmp(a),
2941− false => |a: &Tendril, b: &Tendril| a.cmp(b),
2944− let transaction = Transaction::change(
2949− .map(|(s, fragment)| (s.from(), s.to(), Some(fragment))),
2952− doc.apply(&transaction, view.id);
2953− doc.append_changes_to_history(view.id);
2958− fn tree_sitter_subtree(
2959− cx: &mut compositor::Context,
2960− _args: &[Cow<str>],
2961− _event: PromptEvent,
2962− ) -> anyhow::Result<()> {
2963− let (view, doc) = current!(cx.editor);
2965− if let Some(syntax) = doc.syntax() {
2966− let primary_selection = doc.selection(view.id).primary();
2967− let text = doc.text();
2968− let from = text.char_to_byte(primary_selection.from());
2969− let to = text.char_to_byte(primary_selection.to());
2970− if let Some(selected_node) = syntax
2973− .descendant_for_byte_range(from, to)
2975− let contents = format!("```tsq\n{}\n```", selected_node.to_sexp());
2977− let callback = async move {
2978− let call: job::Callback =
2979− Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
2980− let contents = ui::Markdown::new(contents, editor.syn_loader.clone());
2981− let popup = Popup::new("hover", contents).auto_close(true);
2982− compositor.replace_or_push("hover", popup);
2987− cx.jobs.callback(callback);
2994− pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
2998− doc: "Close the current view.",
3005− doc: "Close the current view forcefully (ignoring unsaved changes).",
3012− doc: "Open a file from disk into the current view.",
3014− completer: Some(completers::filename),
3017− name: "buffer-close",
3018− aliases: &["bc", "bclose"],
3019− doc: "Close the current buffer.",
3021− completer: Some(completers::buffer),
3024− name: "buffer-close!",
3025− aliases: &["bc!", "bclose!"],
3026− doc: "Close the current buffer forcefully (ignoring unsaved changes).",
3027− fun: force_buffer_close,
3028− completer: Some(completers::buffer),
3031− name: "buffer-close-others",
3032− aliases: &["bco", "bcloseother"],
3033− doc: "Close all buffers but the currently focused one.",
3034− fun: buffer_close_others,
3038− name: "buffer-close-others!",
3039− aliases: &["bco!", "bcloseother!"],
3040− doc: "Close all buffers but the currently focused one.",
3041− fun: force_buffer_close_others,
3045− name: "buffer-close-all",
3046− aliases: &["bca", "bcloseall"],
3047− doc: "Close all buffers, without quiting.",
3048− fun: buffer_close_all,
3052− name: "buffer-close-all!",
3053− aliases: &["bca!", "bcloseall!"],
3054− doc: "Close all buffers forcefully (ignoring unsaved changes), without quiting.",
3055− fun: force_buffer_close_all,
3061− doc: "Write changes to disk. Accepts an optional path (:write some/path.txt)",
3063− completer: Some(completers::filename),
3068− doc: "Create a new scratch buffer.",
3070− completer: Some(completers::filename),
3075− doc: "Format the file using the LSP formatter.",
3080− name: "indent-style",
3082− doc: "Set the indentation style for editing. ('t' for tabs or 1-8 for number of spaces.)",
3083− fun: set_indent_style,
3087− name: "line-ending",
3089− doc: "Set the document's default line ending. Options: crlf, lf, cr, ff, nel.",
3090− fun: set_line_ending,
3096− doc: "Jump back to an earlier point in edit history. Accepts a number of steps or a time span.",
3103− doc: "Jump to a later point in edit history. Accepts a number of steps or a time span.",
3108− name: "write-quit",
3109− aliases: &["wq", "x"],
3110− doc: "Write changes to disk and close the current view. Accepts an optional path (:wq some/path.txt)",
3112− completer: Some(completers::filename),
3115− name: "write-quit!",
3116− aliases: &["wq!", "x!"],
3117− doc: "Write changes to disk and close the current view forcefully. Accepts an optional path (:wq! some/path.txt)",
3118− fun: force_write_quit,
3119− completer: Some(completers::filename),
3124− doc: "Write changes from all views to disk.",
3129− name: "write-quit-all",
3130− aliases: &["wqa", "xa"],
3131− doc: "Write changes from all views to disk and close all views.",
3132− fun: write_all_quit,
3136− name: "write-quit-all!",
3137− aliases: &["wqa!", "xa!"],
3138− doc: "Write changes from all views to disk and close all views forcefully (ignoring unsaved changes).",
3139− fun: force_write_all_quit,
3145− doc: "Close all views.",
3152− doc: "Close all views forcefully (ignoring unsaved changes).",
3153− fun: force_quit_all,
3159− doc: "Quit with exit code (default 1). Accepts an optional integer exit code (:cq 2).",
3166− doc: "Quit with exit code (default 1) forcefully (ignoring unsaved changes). Accepts an optional integer exit code (:cq! 2).",
3173− doc: "Change the editor theme.",
3175− completer: Some(completers::theme),
3178− name: "clipboard-yank",
3180− doc: "Yank main selection into system clipboard.",
3181− fun: yank_main_selection_to_clipboard,
3185− name: "clipboard-yank-join",
3187− 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.
3188− fun: yank_joined_to_clipboard,
3192− name: "primary-clipboard-yank",
3194− doc: "Yank main selection into system primary clipboard.",
3195− fun: yank_main_selection_to_primary_clipboard,
3199− name: "primary-clipboard-yank-join",
3201− 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.
3202− fun: yank_joined_to_primary_clipboard,
3206− name: "clipboard-paste-after",
3208− doc: "Paste system clipboard after selections.",
3209− fun: paste_clipboard_after,
3213− name: "clipboard-paste-before",
3215− doc: "Paste system clipboard before selections.",
3216− fun: paste_clipboard_before,
3220− name: "clipboard-paste-replace",
3222− doc: "Replace selections with content of system clipboard.",
3223− fun: replace_selections_with_clipboard,
3227− name: "primary-clipboard-paste-after",
3229− doc: "Paste primary clipboard after selections.",
3230− fun: paste_primary_clipboard_after,
3234− name: "primary-clipboard-paste-before",
3236− doc: "Paste primary clipboard before selections.",
3237− fun: paste_primary_clipboard_before,
3241− name: "primary-clipboard-paste-replace",
3243− doc: "Replace selections with content of system primary clipboard.",
3244− fun: replace_selections_with_primary_clipboard,
3248− name: "show-clipboard-provider",
3250− doc: "Show clipboard provider name in status bar.",
3251− fun: show_clipboard_provider,
3255− name: "change-current-directory",
3257− doc: "Change the current working directory.",
3258− fun: change_current_directory,
3259− completer: Some(completers::directory),
3262− name: "show-directory",
3264− doc: "Show the current working directory.",
3265− fun: show_current_directory,
3271− doc: "Set encoding based on `https://encoding.spec.whatwg.org`",
3278− doc: "Discard changes and reload from the source file.",
3283− name: "tree-sitter-scopes",
3285− doc: "Display tree sitter scopes, primarily for theming and development.",
3286− fun: tree_sitter_scopes,
3290− name: "debug-start",
3292− doc: "Start a debug session from a given template with given parameters.",
3297− name: "debug-remote",
3298− aliases: &["dbg-tcp"],
3299− doc: "Connect to a debug adapter by TCP address and start a debugging session from a given template with given parameters.",
3304− name: "debug-eval",
3306− doc: "Evaluate expression in current debug context.",
3313− doc: "Open the file in a vertical split.",
3315− completer: Some(completers::filename),
3319− aliases: &["hs", "sp"],
3320− doc: "Open the file in a horizontal split.",
3322− completer: Some(completers::filename),
3327− doc: "Open the tutorial.",
3334− doc: "Go to line number.",
3335− fun: goto_line_number,
3339− name: "set-option",
3341− doc: "Set a config option at runtime",
3343− completer: Some(completers::setting),
3348− doc: "Sort ranges in selection.",
3355− doc: "Sort ranges in selection in reverse order.",
3360− name: "tree-sitter-subtree",
3361− aliases: &["ts-subtree"],
3362− doc: "Display tree sitter subtree under cursor, primarily for debugging queries.",
3363− fun: tree_sitter_subtree,
3368− pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> =
3370− TYPABLE_COMMAND_LIST
3373− std::iter::once((cmd.name, cmd))
3374− .chain(cmd.aliases.iter().map(move |&alias| (alias, cmd)))
3380−fn command_mode(cx: &mut Context) {
3381− let mut prompt = Prompt::new(
3384− |editor: &Editor, input: &str| {
3385− static FUZZY_MATCHER: Lazy<fuzzy_matcher::skim::SkimMatcherV2> =
3386− Lazy::new(fuzzy_matcher::skim::SkimMatcherV2::default);
3388− // we use .this over split_whitespace() because we care about empty segments
3389− let parts = input.split(' ').collect::<Vec<&str>>();
3391− // simple heuristic: if there's no just one part, complete command name.
3392− // if there's a space, per command completion kicks in.
3393− if parts.len() <= 1 {
3394− let mut matches: Vec<_> = cmd::TYPABLE_COMMAND_LIST
3396− .filter_map(|command| {
3398− .fuzzy_match(command.name, input)
3399− .map(|score| (command.name, score))
3403− matches.sort_unstable_by_key(|(_file, score)| std::cmp::Reverse(*score));
3406− .map(|(name, _)| (0.., name.into()))
3409− let part = parts.last().unwrap();
3411− if let Some(cmd::TypableCommand {
3412− completer: Some(completer),
3414− }) = cmd::TYPABLE_COMMAND_MAP.get(parts[0])
3416− completer(editor, part)
3418− .map(|(range, file)| {
3419− // offset ranges to input
3420− let offset = input.len() - part.len();
3421− let range = (range.start + offset)..;
3430− move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
3431− if event != PromptEvent::Validate {
3435− let parts = input.split_whitespace().collect::<Vec<&str>>();
3436− if parts.is_empty() {
3440− // If command is numeric, interpret as line number and go there.
3441− if parts.len() == 1 && parts[0].parse::<usize>().ok().is_some() {
3442− if let Err(e) = cmd::goto_line_number(cx, &[Cow::from(parts[0])], event) {
3443− cx.editor.set_error(format!("{}", e));
3448− // Handle typable commands
3449− if let Some(cmd) = cmd::TYPABLE_COMMAND_MAP.get(parts[0]) {
3450− let args = if cfg!(unix) {
3451− shellwords::shellwords(input)
3453− // Windows doesn't support POSIX, so fallback for now
3456− .map(|part| part.into())
3457− .collect::<Vec<_>>()
3460− if let Err(e) = (cmd.fun)(cx, &args[1..], event) {
3461− cx.editor.set_error(format!("{}", e));
3465− .set_error(format!("no such command: '{}'", parts[0]));
3469− prompt.doc_fn = Box::new(|input: &str| {
3470− let part = input.split(' ').next().unwrap_or_default();
3472− if let Some(cmd::TypableCommand { doc, aliases, .. }) = cmd::TYPABLE_COMMAND_MAP.get(part) {
3473− if aliases.is_empty() {
3474− return Some((*doc).into());
3476− return Some(format!("{}\nAliases: {}", doc, aliases.join(", ")).into());
3482− // Calculate initial completion
3483− prompt.recalculate_completion(cx.editor);
3484− cx.push_layer(Box::new(prompt));
3487fn file_picker(cx: &mut Context) {
2022fn file_picker(cx: &mut Context) {
3488 // We don't specify language markers, root will be the root of the current git repo
2023 // We don't specify language markers, root will be the root of the current git repo
3489 let root = find_root(None, &[]).unwrap_or_else(|| PathBuf::from("./"));
2024 let root = find_root(None, &[]).unwrap_or_else(|| PathBuf::from("./"));
@@ -3567,15 +2102,13 @@ pub fn command_palette(cx: &mut Context) {
3567 compositor.find::<ui::EditorView>().unwrap().keymaps[&doc.mode].reverse_map();
2102 compositor.find::<ui::EditorView>().unwrap().keymaps[&doc.mode].reverse_map();
3569 let mut commands: Vec<MappableCommand> = MappableCommand::STATIC_COMMAND_LIST.into();
2104 let mut commands: Vec<MappableCommand> = MappableCommand::STATIC_COMMAND_LIST.into();
3571− cmd::TYPABLE_COMMAND_LIST
2105+ commands.extend(typed::TYPABLE_COMMAND_LIST.iter().map(|cmd| {
3573− .map(|cmd| MappableCommand::Typable {
2106+ MappableCommand::Typable {
3574− name: cmd.name.to_owned(),
2107+ name: cmd.name.to_owned(),
3575− doc: cmd.doc.to_owned(),
2108+ doc: cmd.doc.to_owned(),
3576− args: Vec::new(),
2109+ args: Vec::new(),
3580 // formats key bindings, multiple bindings are comma separated,
2113 // formats key bindings, multiple bindings are comma separated,
3581 // individual key presses are joined with `+`
2114 // individual key presses are joined with `+`
@@ -5260,7 +3793,7 @@ fn vsplit(cx: &mut Context) {
5261fn wclose(cx: &mut Context) {
3794fn wclose(cx: &mut Context) {
5262 if cx.editor.tree.views().count() == 1 {
3795 if cx.editor.tree.views().count() == 1 {
5263− if let Err(err) = cmd::buffers_remaining_impl(cx.editor) {
3796+ if let Err(err) = typed::buffers_remaining_impl(cx.editor) {
5264 cx.editor.set_error(err.to_string());
3797 cx.editor.set_error(err.to_string());