on master help sign in
Split out typable commands into a separate file
commit19247ff0ec448a07b1f57b646738a606f73f61b5 authorBlaž Hrastnik <blaz@mxxn.io> dateFri 4 Mar 2022, 13:42 (5 years ago) parent9bfb0ca
3 files changed+1478−1480
helix-term/src/commands.rs+12−1479 helix-term/src/commands/typed.rs+1465 xtask/src/main.rs+1−1
helix-term/src/commands.rs+12−1479
@@ -1,8 +1,10 @@
11pub(crate) mod dap;
22pub(crate) mod lsp;
3+pub(crate) mod typed;
34
45pub use dap::*;
56pub use lsp::*;
7+pub use typed::*;
68
79use helix_core::{
810 comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes,
@@ -179,7 +181,7 @@ impl MappableCommand {
179181 match &self {
180182 Self::Typable { name, args, doc: _ } => {
181183 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()) {
183185 let mut cx = compositor::Context {
184186 editor: cx.editor,
185187 jobs: cx.jobs,
@@ -459,7 +461,7 @@ impl std::str::FromStr for MappableCommand {
459461 let args = typable_command
460462 .map(|s| s.to_owned())
461463 .collect::<Vec<String>>();
462− cmd::TYPABLE_COMMAND_MAP
464+ typed::TYPABLE_COMMAND_MAP
463465 .get(name)
464466 .map(|cmd| MappableCommand::Typable {
465467 name: cmd.name.to_owned(),
@@ -2017,1473 +2019,6 @@ fn append_mode(cx: &mut Context) {
20172019 doc.set_selection(view.id, selection);
20182020}
20192021
2020−pub mod cmd {
2021− use super::*;
2022−
2023− use helix_view::editor::Action;
2024− use ui::completers::{self, Completer};
2025−
2026− #[derive(Clone)]
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>,
2034− }
2035−
2036− fn quit(
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)?
2044− }
2045−
2046− cx.editor.close(view!(cx.editor).id);
2047−
2048− Ok(())
2049− }
2050−
2051− fn force_quit(
2052− cx: &mut compositor::Context,
2053− _args: &[Cow<str>],
2054− _event: PromptEvent,
2055− ) -> anyhow::Result<()> {
2056− cx.editor.close(view!(cx.editor).id);
2057−
2058− Ok(())
2059− }
2060−
2061− fn open(
2062− cx: &mut compositor::Context,
2063− args: &[Cow<str>],
2064− _event: PromptEvent,
2065− ) -> anyhow::Result<()> {
2066− ensure!(!args.is_empty(), "wrong argument count");
2067− for arg in args {
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);
2075− }
2076− Ok(())
2077− }
2078−
2079− fn buffer_close_by_ids_impl(
2080− editor: &mut Editor,
2081− doc_ids: &[DocumentId],
2082− force: bool,
2083− ) -> anyhow::Result<()> {
2084− for &doc_id in doc_ids {
2085− editor.close_document(doc_id, force)?;
2086− }
2087−
2088− Ok(())
2089− }
2090−
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];
2096− }
2097−
2098− let mut nonexistent_buffers = vec![];
2099− let mut document_ids = vec![];
2100− for arg in args {
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
2105− {
2106− Some(doc.id())
2107− } else {
2108− None
2109− }
2110− });
2111−
2112− match doc_id {
2113− Some(doc_id) => document_ids.push(doc_id),
2114− None => nonexistent_buffers.push(format!("'{}'", arg)),
2115− }
2116− }
2117−
2118− if !nonexistent_buffers.is_empty() {
2119− editor.set_error(format!(
2120− "cannot close non-existent buffers: {}",
2121− nonexistent_buffers.join(", ")
2122− ));
2123− }
2124−
2125− document_ids
2126− }
2127−
2128− fn buffer_close(
2129− cx: &mut compositor::Context,
2130− args: &[Cow<str>],
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)
2135− }
2136−
2137− fn force_buffer_close(
2138− cx: &mut compositor::Context,
2139− args: &[Cow<str>],
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)
2144− }
2145−
2146− fn buffer_gather_others_impl(editor: &mut Editor) -> Vec<DocumentId> {
2147− let current_document = &doc!(editor).id();
2148− editor
2149− .documents()
2150− .map(|doc| doc.id())
2151− .filter(|doc_id| doc_id != current_document)
2152− .collect()
2153− }
2154−
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)
2162− }
2163−
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)
2171− }
2172−
2173− fn buffer_gather_all_impl(editor: &mut Editor) -> Vec<DocumentId> {
2174− editor.documents().map(|doc| doc.id()).collect()
2175− }
2176−
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)
2184− }
2185−
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)
2193− }
2194−
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);
2198−
2199− if let Some(ref path) = path {
2200− doc.set_path(Some(path.as_ref().as_ref()))
2201− .context("invalid filepath")?;
2202− }
2203− if doc.path().is_none() {
2204− bail!("cannot write a buffer without a filename");
2205− }
2206− let fmt = doc.auto_format().map(|fmt| {
2207− let shared = fmt.shared();
2208− let callback = make_format_callback(
2209− doc.id(),
2210− doc.version(),
2211− Modified::SetUnmodified,
2212− shared.clone(),
2213− );
2214− jobs.callback(callback);
2215− shared
2216− });
2217− let future = doc.format_and_save(fmt);
2218− cx.jobs.add(Job::new(future).wait_before_exiting());
2219−
2220− if path.is_some() {
2221− let id = doc.id();
2222− let _ = cx.editor.refresh_language_server(id);
2223− }
2224− Ok(())
2225− }
2226−
2227− fn write(
2228− cx: &mut compositor::Context,
2229− args: &[Cow<str>],
2230− _event: PromptEvent,
2231− ) -> anyhow::Result<()> {
2232− write_impl(cx, args.first())
2233− }
2234−
2235− fn new_file(
2236− cx: &mut compositor::Context,
2237− _args: &[Cow<str>],
2238− _event: PromptEvent,
2239− ) -> anyhow::Result<()> {
2240− cx.editor.new_file(Action::Replace);
2241−
2242− Ok(())
2243− }
2244−
2245− fn format(
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() {
2252− let callback =
2253− make_format_callback(doc.id(), doc.version(), Modified::LeaveModified, format);
2254− cx.jobs.callback(callback);
2255− }
2256−
2257− Ok(())
2258− }
2259− fn set_indent_style(
2260− cx: &mut compositor::Context,
2261− args: &[Cow<str>],
2262− _event: PromptEvent,
2263− ) -> anyhow::Result<()> {
2264− use IndentStyle::*;
2265−
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.
2274− });
2275− return Ok(());
2276− }
2277−
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),
2282− Some(arg) => arg
2283− .parse::<u8>()
2284− .ok()
2285− .filter(|n| (1..=8).contains(n))
2286− .map(Spaces),
2287− _ => None,
2288− };
2289−
2290− let style = style.context("invalid indent style")?;
2291− let doc = doc_mut!(cx.editor);
2292− doc.indent_style = style;
2293−
2294− Ok(())
2295− }
2296−
2297− /// Sets or reports the current document's line ending setting.
2298− fn set_line_ending(
2299− cx: &mut compositor::Context,
2300− args: &[Cow<str>],
2301− _event: PromptEvent,
2302− ) -> anyhow::Result<()> {
2303− use LineEnding::*;
2304−
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 {
2309− Crlf => "crlf",
2310− LF => "line feed",
2311− FF => "form feed",
2312− CR => "carriage return",
2313− Nel => "next line",
2314−
2315− // These should never be a document's default line ending.
2316− VT | LS | PS => "error",
2317− });
2318−
2319− return Ok(());
2320− }
2321−
2322− let arg = args
2323− .get(0)
2324− .context("argument missing")?
2325− .to_ascii_lowercase();
2326−
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"),
2336− };
2337−
2338− doc_mut!(cx.editor).line_ending = line_ending;
2339− Ok(())
2340− }
2341−
2342− fn earlier(
2343− cx: &mut compositor::Context,
2344− args: &[Cow<str>],
2345− _event: PromptEvent,
2346− ) -> anyhow::Result<()> {
2347− let uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?;
2348−
2349− let (view, doc) = current!(cx.editor);
2350− let success = doc.earlier(view.id, uk);
2351− if !success {
2352− cx.editor.set_status("Already at oldest change");
2353− }
2354−
2355− Ok(())
2356− }
2357−
2358− fn later(
2359− cx: &mut compositor::Context,
2360− args: &[Cow<str>],
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);
2366− if !success {
2367− cx.editor.set_status("Already at newest change");
2368− }
2369−
2370− Ok(())
2371− }
2372−
2373− fn write_quit(
2374− cx: &mut compositor::Context,
2375− args: &[Cow<str>],
2376− event: PromptEvent,
2377− ) -> anyhow::Result<()> {
2378− write_impl(cx, args.first())?;
2379− quit(cx, &[], event)
2380− }
2381−
2382− fn force_write_quit(
2383− cx: &mut compositor::Context,
2384− args: &[Cow<str>],
2385− event: PromptEvent,
2386− ) -> anyhow::Result<()> {
2387− write_impl(cx, args.first())?;
2388− force_quit(cx, &[], event)
2389− }
2390−
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
2395− .documents()
2396− .filter(|doc| doc.is_modified())
2397− .map(|doc| {
2398− doc.relative_path()
2399− .map(|path| path.to_string_lossy().to_string())
2400− .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into())
2401− })
2402− .collect();
2403− if !modified.is_empty() {
2404− bail!(
2405− "{} unsaved buffer(s) remaining: {:?}",
2406− modified.len(),
2407− modified
2408− );
2409− }
2410− Ok(())
2411− }
2412−
2413− fn write_all_impl(
2414− cx: &mut compositor::Context,
2415− _args: &[Cow<str>],
2416− _event: PromptEvent,
2417− quit: bool,
2418− force: bool,
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");
2426− continue;
2427− }
2428−
2429− if !doc.is_modified() {
2430− continue;
2431− }
2432−
2433− let fmt = doc.auto_format().map(|fmt| {
2434− let shared = fmt.shared();
2435− let callback = make_format_callback(
2436− doc.id(),
2437− doc.version(),
2438− Modified::SetUnmodified,
2439− shared.clone(),
2440− );
2441− jobs.callback(callback);
2442− shared
2443− });
2444− let future = doc.format_and_save(fmt);
2445− jobs.add(Job::new(future).wait_before_exiting());
2446− }
2447−
2448− if quit {
2449− if !force {
2450− buffers_remaining_impl(cx.editor)?;
2451− }
2452−
2453− // close all views
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);
2457− }
2458− }
2459−
2460− bail!(errors)
2461− }
2462−
2463− fn write_all(
2464− cx: &mut compositor::Context,
2465− args: &[Cow<str>],
2466− event: PromptEvent,
2467− ) -> anyhow::Result<()> {
2468− write_all_impl(cx, args, event, false, false)
2469− }
2470−
2471− fn write_all_quit(
2472− cx: &mut compositor::Context,
2473− args: &[Cow<str>],
2474− event: PromptEvent,
2475− ) -> anyhow::Result<()> {
2476− write_all_impl(cx, args, event, true, false)
2477− }
2478−
2479− fn force_write_all_quit(
2480− cx: &mut compositor::Context,
2481− args: &[Cow<str>],
2482− event: PromptEvent,
2483− ) -> anyhow::Result<()> {
2484− write_all_impl(cx, args, event, true, true)
2485− }
2486−
2487− fn quit_all_impl(editor: &mut Editor, force: bool) -> anyhow::Result<()> {
2488− if !force {
2489− buffers_remaining_impl(editor)?;
2490− }
2491−
2492− // close all views
2493− let views: Vec<_> = editor.tree.views().map(|(view, _)| view.id).collect();
2494− for view_id in views {
2495− editor.close(view_id);
2496− }
2497−
2498− Ok(())
2499− }
2500−
2501− fn quit_all(
2502− cx: &mut compositor::Context,
2503− _args: &[Cow<str>],
2504− _event: PromptEvent,
2505− ) -> anyhow::Result<()> {
2506− quit_all_impl(cx.editor, false)
2507− }
2508−
2509− fn force_quit_all(
2510− cx: &mut compositor::Context,
2511− _args: &[Cow<str>],
2512− _event: PromptEvent,
2513− ) -> anyhow::Result<()> {
2514− quit_all_impl(cx.editor, true)
2515− }
2516−
2517− fn cquit(
2518− cx: &mut compositor::Context,
2519− args: &[Cow<str>],
2520− _event: PromptEvent,
2521− ) -> anyhow::Result<()> {
2522− let exit_code = args
2523− .first()
2524− .and_then(|code| code.parse::<i32>().ok())
2525− .unwrap_or(1);
2526− cx.editor.exit_code = exit_code;
2527−
2528− quit_all_impl(cx.editor, false)
2529− }
2530−
2531− fn force_cquit(
2532− cx: &mut compositor::Context,
2533− args: &[Cow<str>],
2534− _event: PromptEvent,
2535− ) -> anyhow::Result<()> {
2536− let exit_code = args
2537− .first()
2538− .and_then(|code| code.parse::<i32>().ok())
2539− .unwrap_or(1);
2540− cx.editor.exit_code = exit_code;
2541−
2542− quit_all_impl(cx.editor, true)
2543− }
2544−
2545− fn theme(
2546− cx: &mut compositor::Context,
2547− args: &[Cow<str>],
2548− _event: PromptEvent,
2549− ) -> anyhow::Result<()> {
2550− let theme = args.first().context("Theme not provided")?;
2551− let theme = cx
2552− .editor
2553− .theme_loader
2554− .load(theme)
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");
2559− }
2560− cx.editor.set_theme(theme);
2561− Ok(())
2562− }
2563−
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)
2570− }
2571−
2572− fn yank_joined_to_clipboard(
2573− cx: &mut compositor::Context,
2574− args: &[Cow<str>],
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)
2581− }
2582−
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)
2589− }
2590−
2591− fn yank_joined_to_primary_clipboard(
2592− cx: &mut compositor::Context,
2593− args: &[Cow<str>],
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)
2600− }
2601−
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)
2608− }
2609−
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)
2616− }
2617−
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)
2624− }
2625−
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)
2632− }
2633−
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);
2639−
2640− match cx.editor.clipboard_provider.get_contents(clipboard_type) {
2641− Ok(contents) => {
2642− let selection = doc.selection(view.id);
2643− let transaction =
2644− Transaction::change_by_selection(doc.text(), selection, |range| {
2645− (range.from(), range.to(), Some(contents.as_str().into()))
2646− });
2647−
2648− doc.apply(&transaction, view.id);
2649− doc.append_changes_to_history(view.id);
2650− Ok(())
2651− }
2652− Err(e) => Err(e.context("Couldn't get system clipboard contents")),
2653− }
2654− }
2655−
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)
2662− }
2663−
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)
2670− }
2671−
2672− fn show_clipboard_provider(
2673− cx: &mut compositor::Context,
2674− _args: &[Cow<str>],
2675− _event: PromptEvent,
2676− ) -> anyhow::Result<()> {
2677− cx.editor
2678− .set_status(cx.editor.clipboard_provider.name().to_string());
2679− Ok(())
2680− }
2681−
2682− fn change_current_directory(
2683− cx: &mut compositor::Context,
2684− args: &[Cow<str>],
2685− _event: PromptEvent,
2686− ) -> anyhow::Result<()> {
2687− let dir = helix_core::path::expand_tilde(
2688− args.first()
2689− .context("target directory not provided")?
2690− .as_ref()
2691− .as_ref(),
2692− );
2693−
2694− if let Err(e) = std::env::set_current_dir(dir) {
2695− bail!("Couldn't change the current working directory: {}", e);
2696− }
2697−
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 {}",
2701− cwd.display()
2702− ));
2703− Ok(())
2704− }
2705−
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")?;
2712− cx.editor
2713− .set_status(format!("Current working directory is {}", cwd.display()));
2714− Ok(())
2715− }
2716−
2717− /// Sets the [`Document`]'s encoding..
2718− fn set_encoding(
2719− cx: &mut compositor::Context,
2720− args: &[Cow<str>],
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)
2726− } else {
2727− let encoding = doc.encoding().name().to_owned();
2728− cx.editor.set_status(encoding);
2729− Ok(())
2730− }
2731− }
2732−
2733− /// Reload the [`Document`] from its source file.
2734− fn reload(
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)
2741− }
2742−
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(..);
2750−
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));
2754− Ok(())
2755− }
2756−
2757− fn vsplit(
2758− cx: &mut compositor::Context,
2759− args: &[Cow<str>],
2760− _event: PromptEvent,
2761− ) -> anyhow::Result<()> {
2762− let id = view!(cx.editor).doc;
2763−
2764− if args.is_empty() {
2765− cx.editor.switch(id, Action::VerticalSplit);
2766− } else {
2767− for arg in args {
2768− cx.editor
2769− .open(PathBuf::from(arg.as_ref()), Action::VerticalSplit)?;
2770− }
2771− }
2772−
2773− Ok(())
2774− }
2775−
2776− fn hsplit(
2777− cx: &mut compositor::Context,
2778− args: &[Cow<str>],
2779− _event: PromptEvent,
2780− ) -> anyhow::Result<()> {
2781− let id = view!(cx.editor).doc;
2782−
2783− if args.is_empty() {
2784− cx.editor.switch(id, Action::HorizontalSplit);
2785− } else {
2786− for arg in args {
2787− cx.editor
2788− .open(PathBuf::from(arg.as_ref()), Action::HorizontalSplit)?;
2789− }
2790− }
2791−
2792− Ok(())
2793− }
2794−
2795− fn debug_eval(
2796− cx: &mut compositor::Context,
2797− args: &[Cow<str>],
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),
2803− _ => {
2804− bail!("Cannot find current stack frame to access variables")
2805− }
2806− };
2807−
2808− // TODO: support no frame_id
2809−
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);
2813− }
2814− Ok(())
2815− }
2816−
2817− fn debug_start(
2818− cx: &mut compositor::Context,
2819− args: &[Cow<str>],
2820− _event: PromptEvent,
2821− ) -> anyhow::Result<()> {
2822− let mut args = args.to_owned();
2823− let name = match args.len() {
2824− 0 => None,
2825− _ => Some(args.remove(0)),
2826− };
2827− dap_start_impl(cx, name.as_deref(), None, Some(args))
2828− }
2829−
2830− fn debug_remote(
2831− cx: &mut compositor::Context,
2832− args: &[Cow<str>],
2833− _event: PromptEvent,
2834− ) -> anyhow::Result<()> {
2835− let mut args = args.to_owned();
2836− let address = match args.len() {
2837− 0 => None,
2838− _ => Some(args.remove(0).parse()?),
2839− };
2840− let name = match args.len() {
2841− 0 => None,
2842− _ => Some(args.remove(0)),
2843− };
2844− dap_start_impl(cx, name.as_deref(), address, Some(args))
2845− }
2846−
2847− fn tutor(
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)?;
2856− Ok(())
2857− }
2858−
2859− pub(super) fn goto_line_number(
2860− cx: &mut compositor::Context,
2861− args: &[Cow<str>],
2862− _event: PromptEvent,
2863− ) -> anyhow::Result<()> {
2864− ensure!(!args.is_empty(), "Line number required");
2865−
2866− let line = args[0].parse::<usize>()?;
2867−
2868− goto_line_impl(cx.editor, NonZeroUsize::new(line));
2869−
2870− let (view, doc) = current!(cx.editor);
2871−
2872− view.ensure_cursor_in_view(doc, line);
2873− Ok(())
2874− }
2875−
2876− fn setting(
2877− cx: &mut compositor::Context,
2878− args: &[Cow<str>],
2879− _event: PromptEvent,
2880− ) -> anyhow::Result<()> {
2881− let runtime_config = &mut cx.editor.config;
2882−
2883− if args.len() != 2 {
2884− anyhow::bail!("Bad arguments. Usage: `:set key field`");
2885− }
2886−
2887− let (key, arg) = (&args[0].to_lowercase(), &args[1]);
2888−
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]),
2903− }
2904−
2905− Ok(())
2906− }
2907−
2908− fn sort(
2909− cx: &mut compositor::Context,
2910− args: &[Cow<str>],
2911− _event: PromptEvent,
2912− ) -> anyhow::Result<()> {
2913− sort_impl(cx, args, false)
2914− }
2915−
2916− fn sort_reverse(
2917− cx: &mut compositor::Context,
2918− args: &[Cow<str>],
2919− _event: PromptEvent,
2920− ) -> anyhow::Result<()> {
2921− sort_impl(cx, args, true)
2922− }
2923−
2924− fn sort_impl(
2925− cx: &mut compositor::Context,
2926− _args: &[Cow<str>],
2927− reverse: bool,
2928− ) -> anyhow::Result<()> {
2929− let (view, doc) = current!(cx.editor);
2930− let text = doc.text().slice(..);
2931−
2932− let selection = doc.selection(view.id);
2933−
2934− let mut fragments: Vec<_> = selection
2935− .fragments(text)
2936− .map(|fragment| Tendril::from(fragment.as_ref()))
2937− .collect();
2938−
2939− fragments.sort_by(match reverse {
2940− true => |a: &Tendril, b: &Tendril| b.cmp(a),
2941− false => |a: &Tendril, b: &Tendril| a.cmp(b),
2942− });
2943−
2944− let transaction = Transaction::change(
2945− doc.text(),
2946− selection
2947− .into_iter()
2948− .zip(fragments)
2949− .map(|(s, fragment)| (s.from(), s.to(), Some(fragment))),
2950− );
2951−
2952− doc.apply(&transaction, view.id);
2953− doc.append_changes_to_history(view.id);
2954−
2955− Ok(())
2956− }
2957−
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);
2964−
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
2971− .tree()
2972− .root_node()
2973− .descendant_for_byte_range(from, to)
2974− {
2975− let contents = format!("```tsq\n{}\n```", selected_node.to_sexp());
2976−
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);
2983− });
2984− Ok(call)
2985− };
2986−
2987− cx.jobs.callback(callback);
2988− }
2989− }
2990−
2991− Ok(())
2992− }
2993−
2994− pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
2995− TypableCommand {
2996− name: "quit",
2997− aliases: &["q"],
2998− doc: "Close the current view.",
2999− fun: quit,
3000− completer: None,
3001− },
3002− TypableCommand {
3003− name: "quit!",
3004− aliases: &["q!"],
3005− doc: "Close the current view forcefully (ignoring unsaved changes).",
3006− fun: force_quit,
3007− completer: None,
3008− },
3009− TypableCommand {
3010− name: "open",
3011− aliases: &["o"],
3012− doc: "Open a file from disk into the current view.",
3013− fun: open,
3014− completer: Some(completers::filename),
3015− },
3016− TypableCommand {
3017− name: "buffer-close",
3018− aliases: &["bc", "bclose"],
3019− doc: "Close the current buffer.",
3020− fun: buffer_close,
3021− completer: Some(completers::buffer),
3022− },
3023− TypableCommand {
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),
3029− },
3030− TypableCommand {
3031− name: "buffer-close-others",
3032− aliases: &["bco", "bcloseother"],
3033− doc: "Close all buffers but the currently focused one.",
3034− fun: buffer_close_others,
3035− completer: None,
3036− },
3037− TypableCommand {
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,
3042− completer: None,
3043− },
3044− TypableCommand {
3045− name: "buffer-close-all",
3046− aliases: &["bca", "bcloseall"],
3047− doc: "Close all buffers, without quiting.",
3048− fun: buffer_close_all,
3049− completer: None,
3050− },
3051− TypableCommand {
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,
3056− completer: None,
3057− },
3058− TypableCommand {
3059− name: "write",
3060− aliases: &["w"],
3061− doc: "Write changes to disk. Accepts an optional path (:write some/path.txt)",
3062− fun: write,
3063− completer: Some(completers::filename),
3064− },
3065− TypableCommand {
3066− name: "new",
3067− aliases: &["n"],
3068− doc: "Create a new scratch buffer.",
3069− fun: new_file,
3070− completer: Some(completers::filename),
3071− },
3072− TypableCommand {
3073− name: "format",
3074− aliases: &["fmt"],
3075− doc: "Format the file using the LSP formatter.",
3076− fun: format,
3077− completer: None,
3078− },
3079− TypableCommand {
3080− name: "indent-style",
3081− aliases: &[],
3082− doc: "Set the indentation style for editing. ('t' for tabs or 1-8 for number of spaces.)",
3083− fun: set_indent_style,
3084− completer: None,
3085− },
3086− TypableCommand {
3087− name: "line-ending",
3088− aliases: &[],
3089− doc: "Set the document's default line ending. Options: crlf, lf, cr, ff, nel.",
3090− fun: set_line_ending,
3091− completer: None,
3092− },
3093− TypableCommand {
3094− name: "earlier",
3095− aliases: &["ear"],
3096− doc: "Jump back to an earlier point in edit history. Accepts a number of steps or a time span.",
3097− fun: earlier,
3098− completer: None,
3099− },
3100− TypableCommand {
3101− name: "later",
3102− aliases: &["lat"],
3103− doc: "Jump to a later point in edit history. Accepts a number of steps or a time span.",
3104− fun: later,
3105− completer: None,
3106− },
3107− TypableCommand {
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)",
3111− fun: write_quit,
3112− completer: Some(completers::filename),
3113− },
3114− TypableCommand {
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),
3120− },
3121− TypableCommand {
3122− name: "write-all",
3123− aliases: &["wa"],
3124− doc: "Write changes from all views to disk.",
3125− fun: write_all,
3126− completer: None,
3127− },
3128− TypableCommand {
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,
3133− completer: None,
3134− },
3135− TypableCommand {
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,
3140− completer: None,
3141− },
3142− TypableCommand {
3143− name: "quit-all",
3144− aliases: &["qa"],
3145− doc: "Close all views.",
3146− fun: quit_all,
3147− completer: None,
3148− },
3149− TypableCommand {
3150− name: "quit-all!",
3151− aliases: &["qa!"],
3152− doc: "Close all views forcefully (ignoring unsaved changes).",
3153− fun: force_quit_all,
3154− completer: None,
3155− },
3156− TypableCommand {
3157− name: "cquit",
3158− aliases: &["cq"],
3159− doc: "Quit with exit code (default 1). Accepts an optional integer exit code (:cq 2).",
3160− fun: cquit,
3161− completer: None,
3162− },
3163− TypableCommand {
3164− name: "cquit!",
3165− aliases: &["cq!"],
3166− doc: "Quit with exit code (default 1) forcefully (ignoring unsaved changes). Accepts an optional integer exit code (:cq! 2).",
3167− fun: force_cquit,
3168− completer: None,
3169− },
3170− TypableCommand {
3171− name: "theme",
3172− aliases: &[],
3173− doc: "Change the editor theme.",
3174− fun: theme,
3175− completer: Some(completers::theme),
3176− },
3177− TypableCommand {
3178− name: "clipboard-yank",
3179− aliases: &[],
3180− doc: "Yank main selection into system clipboard.",
3181− fun: yank_main_selection_to_clipboard,
3182− completer: None,
3183− },
3184− TypableCommand {
3185− name: "clipboard-yank-join",
3186− aliases: &[],
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,
3189− completer: None,
3190− },
3191− TypableCommand {
3192− name: "primary-clipboard-yank",
3193− aliases: &[],
3194− doc: "Yank main selection into system primary clipboard.",
3195− fun: yank_main_selection_to_primary_clipboard,
3196− completer: None,
3197− },
3198− TypableCommand {
3199− name: "primary-clipboard-yank-join",
3200− aliases: &[],
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,
3203− completer: None,
3204− },
3205− TypableCommand {
3206− name: "clipboard-paste-after",
3207− aliases: &[],
3208− doc: "Paste system clipboard after selections.",
3209− fun: paste_clipboard_after,
3210− completer: None,
3211− },
3212− TypableCommand {
3213− name: "clipboard-paste-before",
3214− aliases: &[],
3215− doc: "Paste system clipboard before selections.",
3216− fun: paste_clipboard_before,
3217− completer: None,
3218− },
3219− TypableCommand {
3220− name: "clipboard-paste-replace",
3221− aliases: &[],
3222− doc: "Replace selections with content of system clipboard.",
3223− fun: replace_selections_with_clipboard,
3224− completer: None,
3225− },
3226− TypableCommand {
3227− name: "primary-clipboard-paste-after",
3228− aliases: &[],
3229− doc: "Paste primary clipboard after selections.",
3230− fun: paste_primary_clipboard_after,
3231− completer: None,
3232− },
3233− TypableCommand {
3234− name: "primary-clipboard-paste-before",
3235− aliases: &[],
3236− doc: "Paste primary clipboard before selections.",
3237− fun: paste_primary_clipboard_before,
3238− completer: None,
3239− },
3240− TypableCommand {
3241− name: "primary-clipboard-paste-replace",
3242− aliases: &[],
3243− doc: "Replace selections with content of system primary clipboard.",
3244− fun: replace_selections_with_primary_clipboard,
3245− completer: None,
3246− },
3247− TypableCommand {
3248− name: "show-clipboard-provider",
3249− aliases: &[],
3250− doc: "Show clipboard provider name in status bar.",
3251− fun: show_clipboard_provider,
3252− completer: None,
3253− },
3254− TypableCommand {
3255− name: "change-current-directory",
3256− aliases: &["cd"],
3257− doc: "Change the current working directory.",
3258− fun: change_current_directory,
3259− completer: Some(completers::directory),
3260− },
3261− TypableCommand {
3262− name: "show-directory",
3263− aliases: &["pwd"],
3264− doc: "Show the current working directory.",
3265− fun: show_current_directory,
3266− completer: None,
3267− },
3268− TypableCommand {
3269− name: "encoding",
3270− aliases: &[],
3271− doc: "Set encoding based on `https://encoding.spec.whatwg.org`",
3272− fun: set_encoding,
3273− completer: None,
3274− },
3275− TypableCommand {
3276− name: "reload",
3277− aliases: &[],
3278− doc: "Discard changes and reload from the source file.",
3279− fun: reload,
3280− completer: None,
3281− },
3282− TypableCommand {
3283− name: "tree-sitter-scopes",
3284− aliases: &[],
3285− doc: "Display tree sitter scopes, primarily for theming and development.",
3286− fun: tree_sitter_scopes,
3287− completer: None,
3288− },
3289− TypableCommand {
3290− name: "debug-start",
3291− aliases: &["dbg"],
3292− doc: "Start a debug session from a given template with given parameters.",
3293− fun: debug_start,
3294− completer: None,
3295− },
3296− TypableCommand {
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.",
3300− fun: debug_remote,
3301− completer: None,
3302− },
3303− TypableCommand {
3304− name: "debug-eval",
3305− aliases: &[],
3306− doc: "Evaluate expression in current debug context.",
3307− fun: debug_eval,
3308− completer: None,
3309− },
3310− TypableCommand {
3311− name: "vsplit",
3312− aliases: &["vs"],
3313− doc: "Open the file in a vertical split.",
3314− fun: vsplit,
3315− completer: Some(completers::filename),
3316− },
3317− TypableCommand {
3318− name: "hsplit",
3319− aliases: &["hs", "sp"],
3320− doc: "Open the file in a horizontal split.",
3321− fun: hsplit,
3322− completer: Some(completers::filename),
3323− },
3324− TypableCommand {
3325− name: "tutor",
3326− aliases: &[],
3327− doc: "Open the tutorial.",
3328− fun: tutor,
3329− completer: None,
3330− },
3331− TypableCommand {
3332− name: "goto",
3333− aliases: &["g"],
3334− doc: "Go to line number.",
3335− fun: goto_line_number,
3336− completer: None,
3337− },
3338− TypableCommand {
3339− name: "set-option",
3340− aliases: &["set"],
3341− doc: "Set a config option at runtime",
3342− fun: setting,
3343− completer: Some(completers::setting),
3344− },
3345− TypableCommand {
3346− name: "sort",
3347− aliases: &[],
3348− doc: "Sort ranges in selection.",
3349− fun: sort,
3350− completer: None,
3351− },
3352− TypableCommand {
3353− name: "rsort",
3354− aliases: &[],
3355− doc: "Sort ranges in selection in reverse order.",
3356− fun: sort_reverse,
3357− completer: None,
3358− },
3359− TypableCommand {
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,
3364− completer: None,
3365− },
3366− ];
3367−
3368− pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> =
3369− Lazy::new(|| {
3370− TYPABLE_COMMAND_LIST
3371− .iter()
3372− .flat_map(|cmd| {
3373− std::iter::once((cmd.name, cmd))
3374− .chain(cmd.aliases.iter().map(move |&alias| (alias, cmd)))
3375− })
3376− .collect()
3377− });
3378−}
3379−
3380−fn command_mode(cx: &mut Context) {
3381− let mut prompt = Prompt::new(
3382− ":".into(),
3383− Some(':'),
3384− |editor: &Editor, input: &str| {
3385− static FUZZY_MATCHER: Lazy<fuzzy_matcher::skim::SkimMatcherV2> =
3386− Lazy::new(fuzzy_matcher::skim::SkimMatcherV2::default);
3387−
3388− // we use .this over split_whitespace() because we care about empty segments
3389− let parts = input.split(' ').collect::<Vec<&str>>();
3390−
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
3395− .iter()
3396− .filter_map(|command| {
3397− FUZZY_MATCHER
3398− .fuzzy_match(command.name, input)
3399− .map(|score| (command.name, score))
3400− })
3401− .collect();
3402−
3403− matches.sort_unstable_by_key(|(_file, score)| std::cmp::Reverse(*score));
3404− matches
3405− .into_iter()
3406− .map(|(name, _)| (0.., name.into()))
3407− .collect()
3408− } else {
3409− let part = parts.last().unwrap();
3410−
3411− if let Some(cmd::TypableCommand {
3412− completer: Some(completer),
3413− ..
3414− }) = cmd::TYPABLE_COMMAND_MAP.get(parts[0])
3415− {
3416− completer(editor, part)
3417− .into_iter()
3418− .map(|(range, file)| {
3419− // offset ranges to input
3420− let offset = input.len() - part.len();
3421− let range = (range.start + offset)..;
3422− (range, file)
3423− })
3424− .collect()
3425− } else {
3426− Vec::new()
3427− }
3428− }
3429− }, // completion
3430− move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
3431− if event != PromptEvent::Validate {
3432− return;
3433− }
3434−
3435− let parts = input.split_whitespace().collect::<Vec<&str>>();
3436− if parts.is_empty() {
3437− return;
3438− }
3439−
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));
3444− }
3445− return;
3446− }
3447−
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)
3452− } else {
3453− // Windows doesn't support POSIX, so fallback for now
3454− parts
3455− .into_iter()
3456− .map(|part| part.into())
3457− .collect::<Vec<_>>()
3458− };
3459−
3460− if let Err(e) = (cmd.fun)(cx, &args[1..], event) {
3461− cx.editor.set_error(format!("{}", e));
3462− }
3463− } else {
3464− cx.editor
3465− .set_error(format!("no such command: '{}'", parts[0]));
3466− };
3467− },
3468− );
3469− prompt.doc_fn = Box::new(|input: &str| {
3470− let part = input.split(' ').next().unwrap_or_default();
3471−
3472− if let Some(cmd::TypableCommand { doc, aliases, .. }) = cmd::TYPABLE_COMMAND_MAP.get(part) {
3473− if aliases.is_empty() {
3474− return Some((*doc).into());
3475− }
3476− return Some(format!("{}\nAliases: {}", doc, aliases.join(", ")).into());
3477− }
3478−
3479− None
3480− });
3481−
3482− // Calculate initial completion
3483− prompt.recalculate_completion(cx.editor);
3484− cx.push_layer(Box::new(prompt));
3485−}
3486−
34872022fn file_picker(cx: &mut Context) {
34882023 // We don't specify language markers, root will be the root of the current git repo
34892024 let root = find_root(None, &[]).unwrap_or_else(|| PathBuf::from("./"));
@@ -3567,15 +2102,13 @@ pub fn command_palette(cx: &mut Context) {
35672102 compositor.find::<ui::EditorView>().unwrap().keymaps[&doc.mode].reverse_map();
35682103
35692104 let mut commands: Vec<MappableCommand> = MappableCommand::STATIC_COMMAND_LIST.into();
3570− commands.extend(
3571− cmd::TYPABLE_COMMAND_LIST
3572− .iter()
3573− .map(|cmd| MappableCommand::Typable {
3574− name: cmd.name.to_owned(),
3575− doc: cmd.doc.to_owned(),
3576− args: Vec::new(),
3577− }),
3578− );
2105+ commands.extend(typed::TYPABLE_COMMAND_LIST.iter().map(|cmd| {
2106+ MappableCommand::Typable {
2107+ name: cmd.name.to_owned(),
2108+ doc: cmd.doc.to_owned(),
2109+ args: Vec::new(),
2110+ }
2111+ }));
35792112
35802113 // formats key bindings, multiple bindings are comma separated,
35812114 // individual key presses are joined with `+`
@@ -5260,7 +3793,7 @@ fn vsplit(cx: &mut Context) {
52603793
52613794fn wclose(cx: &mut Context) {
52623795 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) {
52643797 cx.editor.set_error(err.to_string());
52653798 return;
52663799 }
helix-term/src/commands/typed.rsnew file+1465
@@ -0,0 +1,1465 @@
1+use super::*;
2+
3+use helix_view::editor::Action;
4+use ui::completers::{self, Completer};
5+
6+#[derive(Clone)]
7+pub struct TypableCommand {
8+ pub name: &'static str,
9+ pub aliases: &'static [&'static str],
10+ pub doc: &'static str,
11+ // params, flags, helper, completer
12+ pub fun: fn(&mut compositor::Context, &[Cow<str>], PromptEvent) -> anyhow::Result<()>,
13+ pub completer: Option<Completer>,
14+}
15+
16+fn quit(
17+ cx: &mut compositor::Context,
18+ _args: &[Cow<str>],
19+ _event: PromptEvent,
20+) -> anyhow::Result<()> {
21+ // last view and we have unsaved changes
22+ if cx.editor.tree.views().count() == 1 {
23+ buffers_remaining_impl(cx.editor)?
24+ }
25+
26+ cx.editor.close(view!(cx.editor).id);
27+
28+ Ok(())
29+}
30+
31+fn force_quit(
32+ cx: &mut compositor::Context,
33+ _args: &[Cow<str>],
34+ _event: PromptEvent,
35+) -> anyhow::Result<()> {
36+ cx.editor.close(view!(cx.editor).id);
37+
38+ Ok(())
39+}
40+
41+fn open(
42+ cx: &mut compositor::Context,
43+ args: &[Cow<str>],
44+ _event: PromptEvent,
45+) -> anyhow::Result<()> {
46+ ensure!(!args.is_empty(), "wrong argument count");
47+ for arg in args {
48+ let (path, pos) = args::parse_file(arg);
49+ let _ = cx.editor.open(path, Action::Replace)?;
50+ let (view, doc) = current!(cx.editor);
51+ let pos = Selection::point(pos_at_coords(doc.text().slice(..), pos, true));
52+ doc.set_selection(view.id, pos);
53+ // does not affect opening a buffer without pos
54+ align_view(doc, view, Align::Center);
55+ }
56+ Ok(())
57+}
58+
59+fn buffer_close_by_ids_impl(
60+ editor: &mut Editor,
61+ doc_ids: &[DocumentId],
62+ force: bool,
63+) -> anyhow::Result<()> {
64+ for &doc_id in doc_ids {
65+ editor.close_document(doc_id, force)?;
66+ }
67+
68+ Ok(())
69+}
70+
71+fn buffer_gather_paths_impl(editor: &mut Editor, args: &[Cow<str>]) -> Vec<DocumentId> {
72+ // No arguments implies current document
73+ if args.is_empty() {
74+ let doc_id = view!(editor).doc;
75+ return vec![doc_id];
76+ }
77+
78+ let mut nonexistent_buffers = vec![];
79+ let mut document_ids = vec![];
80+ for arg in args {
81+ let doc_id = editor.documents().find_map(|doc| {
82+ let arg_path = Some(Path::new(arg.as_ref()));
83+ if doc.path().map(|p| p.as_path()) == arg_path
84+ || doc.relative_path().as_deref() == arg_path
85+ {
86+ Some(doc.id())
87+ } else {
88+ None
89+ }
90+ });
91+
92+ match doc_id {
93+ Some(doc_id) => document_ids.push(doc_id),
94+ None => nonexistent_buffers.push(format!("'{}'", arg)),
95+ }
96+ }
97+
98+ if !nonexistent_buffers.is_empty() {
99+ editor.set_error(format!(
100+ "cannot close non-existent buffers: {}",
101+ nonexistent_buffers.join(", ")
102+ ));
103+ }
104+
105+ document_ids
106+}
107+
108+fn buffer_close(
109+ cx: &mut compositor::Context,
110+ args: &[Cow<str>],
111+ _event: PromptEvent,
112+) -> anyhow::Result<()> {
113+ let document_ids = buffer_gather_paths_impl(cx.editor, args);
114+ buffer_close_by_ids_impl(cx.editor, &document_ids, false)
115+}
116+
117+fn force_buffer_close(
118+ cx: &mut compositor::Context,
119+ args: &[Cow<str>],
120+ _event: PromptEvent,
121+) -> anyhow::Result<()> {
122+ let document_ids = buffer_gather_paths_impl(cx.editor, args);
123+ buffer_close_by_ids_impl(cx.editor, &document_ids, true)
124+}
125+
126+fn buffer_gather_others_impl(editor: &mut Editor) -> Vec<DocumentId> {
127+ let current_document = &doc!(editor).id();
128+ editor
129+ .documents()
130+ .map(|doc| doc.id())
131+ .filter(|doc_id| doc_id != current_document)
132+ .collect()
133+}
134+
135+fn buffer_close_others(
136+ cx: &mut compositor::Context,
137+ _args: &[Cow<str>],
138+ _event: PromptEvent,
139+) -> anyhow::Result<()> {
140+ let document_ids = buffer_gather_others_impl(cx.editor);
141+ buffer_close_by_ids_impl(cx.editor, &document_ids, false)
142+}
143+
144+fn force_buffer_close_others(
145+ cx: &mut compositor::Context,
146+ _args: &[Cow<str>],
147+ _event: PromptEvent,
148+) -> anyhow::Result<()> {
149+ let document_ids = buffer_gather_others_impl(cx.editor);
150+ buffer_close_by_ids_impl(cx.editor, &document_ids, true)
151+}
152+
153+fn buffer_gather_all_impl(editor: &mut Editor) -> Vec<DocumentId> {
154+ editor.documents().map(|doc| doc.id()).collect()
155+}
156+
157+fn buffer_close_all(
158+ cx: &mut compositor::Context,
159+ _args: &[Cow<str>],
160+ _event: PromptEvent,
161+) -> anyhow::Result<()> {
162+ let document_ids = buffer_gather_all_impl(cx.editor);
163+ buffer_close_by_ids_impl(cx.editor, &document_ids, false)
164+}
165+
166+fn force_buffer_close_all(
167+ cx: &mut compositor::Context,
168+ _args: &[Cow<str>],
169+ _event: PromptEvent,
170+) -> anyhow::Result<()> {
171+ let document_ids = buffer_gather_all_impl(cx.editor);
172+ buffer_close_by_ids_impl(cx.editor, &document_ids, true)
173+}
174+
175+fn write_impl(cx: &mut compositor::Context, path: Option<&Cow<str>>) -> anyhow::Result<()> {
176+ let jobs = &mut cx.jobs;
177+ let doc = doc_mut!(cx.editor);
178+
179+ if let Some(ref path) = path {
180+ doc.set_path(Some(path.as_ref().as_ref()))
181+ .context("invalid filepath")?;
182+ }
183+ if doc.path().is_none() {
184+ bail!("cannot write a buffer without a filename");
185+ }
186+ let fmt = doc.auto_format().map(|fmt| {
187+ let shared = fmt.shared();
188+ let callback = make_format_callback(
189+ doc.id(),
190+ doc.version(),
191+ Modified::SetUnmodified,
192+ shared.clone(),
193+ );
194+ jobs.callback(callback);
195+ shared
196+ });
197+ let future = doc.format_and_save(fmt);
198+ cx.jobs.add(Job::new(future).wait_before_exiting());
199+
200+ if path.is_some() {
201+ let id = doc.id();
202+ let _ = cx.editor.refresh_language_server(id);
203+ }
204+ Ok(())
205+}
206+
207+fn write(
208+ cx: &mut compositor::Context,
209+ args: &[Cow<str>],
210+ _event: PromptEvent,
211+) -> anyhow::Result<()> {
212+ write_impl(cx, args.first())
213+}
214+
215+fn new_file(
216+ cx: &mut compositor::Context,
217+ _args: &[Cow<str>],
218+ _event: PromptEvent,
219+) -> anyhow::Result<()> {
220+ cx.editor.new_file(Action::Replace);
221+
222+ Ok(())
223+}
224+
225+fn format(
226+ cx: &mut compositor::Context,
227+ _args: &[Cow<str>],
228+ _event: PromptEvent,
229+) -> anyhow::Result<()> {
230+ let doc = doc!(cx.editor);
231+ if let Some(format) = doc.format() {
232+ let callback =
233+ make_format_callback(doc.id(), doc.version(), Modified::LeaveModified, format);
234+ cx.jobs.callback(callback);
235+ }
236+
237+ Ok(())
238+}
239+fn set_indent_style(
240+ cx: &mut compositor::Context,
241+ args: &[Cow<str>],
242+ _event: PromptEvent,
243+) -> anyhow::Result<()> {
244+ use IndentStyle::*;
245+
246+ // If no argument, report current indent style.
247+ if args.is_empty() {
248+ let style = doc!(cx.editor).indent_style;
249+ cx.editor.set_status(match style {
250+ Tabs => "tabs".to_owned(),
251+ Spaces(1) => "1 space".to_owned(),
252+ Spaces(n) if (2..=8).contains(&n) => format!("{} spaces", n),
253+ _ => unreachable!(), // Shouldn't happen.
254+ });
255+ return Ok(());
256+ }
257+
258+ // Attempt to parse argument as an indent style.
259+ let style = match args.get(0) {
260+ Some(arg) if "tabs".starts_with(&arg.to_lowercase()) => Some(Tabs),
261+ Some(Cow::Borrowed("0")) => Some(Tabs),
262+ Some(arg) => arg
263+ .parse::<u8>()
264+ .ok()
265+ .filter(|n| (1..=8).contains(n))
266+ .map(Spaces),
267+ _ => None,
268+ };
269+
270+ let style = style.context("invalid indent style")?;
271+ let doc = doc_mut!(cx.editor);
272+ doc.indent_style = style;
273+
274+ Ok(())
275+}
276+
277+/// Sets or reports the current document's line ending setting.
278+fn set_line_ending(
279+ cx: &mut compositor::Context,
280+ args: &[Cow<str>],
281+ _event: PromptEvent,
282+) -> anyhow::Result<()> {
283+ use LineEnding::*;
284+
285+ // If no argument, report current line ending setting.
286+ if args.is_empty() {
287+ let line_ending = doc!(cx.editor).line_ending;
288+ cx.editor.set_status(match line_ending {
289+ Crlf => "crlf",
290+ LF => "line feed",
291+ FF => "form feed",
292+ CR => "carriage return",
293+ Nel => "next line",
294+
295+ // These should never be a document's default line ending.
296+ VT | LS | PS => "error",
297+ });
298+
299+ return Ok(());
300+ }
301+
302+ let arg = args
303+ .get(0)
304+ .context("argument missing")?
305+ .to_ascii_lowercase();
306+
307+ // Attempt to parse argument as a line ending.
308+ let line_ending = match arg {
309+ // We check for CR first because it shares a common prefix with CRLF.
310+ arg if arg.starts_with("cr") => CR,
311+ arg if arg.starts_with("crlf") => Crlf,
312+ arg if arg.starts_with("lf") => LF,
313+ arg if arg.starts_with("ff") => FF,
314+ arg if arg.starts_with("nel") => Nel,
315+ _ => bail!("invalid line ending"),
316+ };
317+
318+ doc_mut!(cx.editor).line_ending = line_ending;
319+ Ok(())
320+}
321+
322+fn earlier(
323+ cx: &mut compositor::Context,
324+ args: &[Cow<str>],
325+ _event: PromptEvent,
326+) -> anyhow::Result<()> {
327+ let uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?;
328+
329+ let (view, doc) = current!(cx.editor);
330+ let success = doc.earlier(view.id, uk);
331+ if !success {
332+ cx.editor.set_status("Already at oldest change");
333+ }
334+
335+ Ok(())
336+}
337+
338+fn later(
339+ cx: &mut compositor::Context,
340+ args: &[Cow<str>],
341+ _event: PromptEvent,
342+) -> anyhow::Result<()> {
343+ let uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?;
344+ let (view, doc) = current!(cx.editor);
345+ let success = doc.later(view.id, uk);
346+ if !success {
347+ cx.editor.set_status("Already at newest change");
348+ }
349+
350+ Ok(())
351+}
352+
353+fn write_quit(
354+ cx: &mut compositor::Context,
355+ args: &[Cow<str>],
356+ event: PromptEvent,
357+) -> anyhow::Result<()> {
358+ write_impl(cx, args.first())?;
359+ quit(cx, &[], event)
360+}
361+
362+fn force_write_quit(
363+ cx: &mut compositor::Context,
364+ args: &[Cow<str>],
365+ event: PromptEvent,
366+) -> anyhow::Result<()> {
367+ write_impl(cx, args.first())?;
368+ force_quit(cx, &[], event)
369+}
370+
371+/// Results an error if there are modified buffers remaining and sets editor error,
372+/// otherwise returns `Ok(())`
373+pub(super) fn buffers_remaining_impl(editor: &mut Editor) -> anyhow::Result<()> {
374+ let modified: Vec<_> = editor
375+ .documents()
376+ .filter(|doc| doc.is_modified())
377+ .map(|doc| {
378+ doc.relative_path()
379+ .map(|path| path.to_string_lossy().to_string())
380+ .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into())
381+ })
382+ .collect();
383+ if !modified.is_empty() {
384+ bail!(
385+ "{} unsaved buffer(s) remaining: {:?}",
386+ modified.len(),
387+ modified
388+ );
389+ }
390+ Ok(())
391+}
392+
393+fn write_all_impl(
394+ cx: &mut compositor::Context,
395+ _args: &[Cow<str>],
396+ _event: PromptEvent,
397+ quit: bool,
398+ force: bool,
399+) -> anyhow::Result<()> {
400+ let mut errors = String::new();
401+ let jobs = &mut cx.jobs;
402+ // save all documents
403+ for doc in &mut cx.editor.documents.values_mut() {
404+ if doc.path().is_none() {
405+ errors.push_str("cannot write a buffer without a filename\n");
406+ continue;
407+ }
408+
409+ if !doc.is_modified() {
410+ continue;
411+ }
412+
413+ let fmt = doc.auto_format().map(|fmt| {
414+ let shared = fmt.shared();
415+ let callback = make_format_callback(
416+ doc.id(),
417+ doc.version(),
418+ Modified::SetUnmodified,
419+ shared.clone(),
420+ );
421+ jobs.callback(callback);
422+ shared
423+ });
424+ let future = doc.format_and_save(fmt);
425+ jobs.add(Job::new(future).wait_before_exiting());
426+ }
427+
428+ if quit {
429+ if !force {
430+ buffers_remaining_impl(cx.editor)?;
431+ }
432+
433+ // close all views
434+ let views: Vec<_> = cx.editor.tree.views().map(|(view, _)| view.id).collect();
435+ for view_id in views {
436+ cx.editor.close(view_id);
437+ }
438+ }
439+
440+ bail!(errors)
441+}
442+
443+fn write_all(
444+ cx: &mut compositor::Context,
445+ args: &[Cow<str>],
446+ event: PromptEvent,
447+) -> anyhow::Result<()> {
448+ write_all_impl(cx, args, event, false, false)
449+}
450+
451+fn write_all_quit(
452+ cx: &mut compositor::Context,
453+ args: &[Cow<str>],
454+ event: PromptEvent,
455+) -> anyhow::Result<()> {
456+ write_all_impl(cx, args, event, true, false)
457+}
458+
459+fn force_write_all_quit(
460+ cx: &mut compositor::Context,
461+ args: &[Cow<str>],
462+ event: PromptEvent,
463+) -> anyhow::Result<()> {
464+ write_all_impl(cx, args, event, true, true)
465+}
466+
467+fn quit_all_impl(editor: &mut Editor, force: bool) -> anyhow::Result<()> {
468+ if !force {
469+ buffers_remaining_impl(editor)?;
470+ }
471+
472+ // close all views
473+ let views: Vec<_> = editor.tree.views().map(|(view, _)| view.id).collect();
474+ for view_id in views {
475+ editor.close(view_id);
476+ }
477+
478+ Ok(())
479+}
480+
481+fn quit_all(
482+ cx: &mut compositor::Context,
483+ _args: &[Cow<str>],
484+ _event: PromptEvent,
485+) -> anyhow::Result<()> {
486+ quit_all_impl(cx.editor, false)
487+}
488+
489+fn force_quit_all(
490+ cx: &mut compositor::Context,
491+ _args: &[Cow<str>],
492+ _event: PromptEvent,
493+) -> anyhow::Result<()> {
494+ quit_all_impl(cx.editor, true)
495+}
496+
497+fn cquit(
498+ cx: &mut compositor::Context,
499+ args: &[Cow<str>],
500+ _event: PromptEvent,
501+) -> anyhow::Result<()> {
502+ let exit_code = args
503+ .first()
504+ .and_then(|code| code.parse::<i32>().ok())
505+ .unwrap_or(1);
506+ cx.editor.exit_code = exit_code;
507+
508+ quit_all_impl(cx.editor, false)
509+}
510+
511+fn force_cquit(
512+ cx: &mut compositor::Context,
513+ args: &[Cow<str>],
514+ _event: PromptEvent,
515+) -> anyhow::Result<()> {
516+ let exit_code = args
517+ .first()
518+ .and_then(|code| code.parse::<i32>().ok())
519+ .unwrap_or(1);
520+ cx.editor.exit_code = exit_code;
521+
522+ quit_all_impl(cx.editor, true)
523+}
524+
525+fn theme(
526+ cx: &mut compositor::Context,
527+ args: &[Cow<str>],
528+ _event: PromptEvent,
529+) -> anyhow::Result<()> {
530+ let theme = args.first().context("Theme not provided")?;
531+ let theme = cx
532+ .editor
533+ .theme_loader
534+ .load(theme)
535+ .with_context(|| format!("Failed setting theme {}", theme))?;
536+ let true_color = cx.editor.config.true_color || crate::true_color();
537+ if !(true_color || theme.is_16_color()) {
538+ bail!("Unsupported theme: theme requires true color support");
539+ }
540+ cx.editor.set_theme(theme);
541+ Ok(())
542+}
543+
544+fn yank_main_selection_to_clipboard(
545+ cx: &mut compositor::Context,
546+ _args: &[Cow<str>],
547+ _event: PromptEvent,
548+) -> anyhow::Result<()> {
549+ yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Clipboard)
550+}
551+
552+fn yank_joined_to_clipboard(
553+ cx: &mut compositor::Context,
554+ args: &[Cow<str>],
555+ _event: PromptEvent,
556+) -> anyhow::Result<()> {
557+ let doc = doc!(cx.editor);
558+ let default_sep = Cow::Borrowed(doc.line_ending.as_str());
559+ let separator = args.first().unwrap_or(&default_sep);
560+ yank_joined_to_clipboard_impl(cx.editor, separator, ClipboardType::Clipboard)
561+}
562+
563+fn yank_main_selection_to_primary_clipboard(
564+ cx: &mut compositor::Context,
565+ _args: &[Cow<str>],
566+ _event: PromptEvent,
567+) -> anyhow::Result<()> {
568+ yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Selection)
569+}
570+
571+fn yank_joined_to_primary_clipboard(
572+ cx: &mut compositor::Context,
573+ args: &[Cow<str>],
574+ _event: PromptEvent,
575+) -> anyhow::Result<()> {
576+ let doc = doc!(cx.editor);
577+ let default_sep = Cow::Borrowed(doc.line_ending.as_str());
578+ let separator = args.first().unwrap_or(&default_sep);
579+ yank_joined_to_clipboard_impl(cx.editor, separator, ClipboardType::Selection)
580+}
581+
582+fn paste_clipboard_after(
583+ cx: &mut compositor::Context,
584+ _args: &[Cow<str>],
585+ _event: PromptEvent,
586+) -> anyhow::Result<()> {
587+ paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Clipboard, 1)
588+}
589+
590+fn paste_clipboard_before(
591+ cx: &mut compositor::Context,
592+ _args: &[Cow<str>],
593+ _event: PromptEvent,
594+) -> anyhow::Result<()> {
595+ paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Clipboard, 1)
596+}
597+
598+fn paste_primary_clipboard_after(
599+ cx: &mut compositor::Context,
600+ _args: &[Cow<str>],
601+ _event: PromptEvent,
602+) -> anyhow::Result<()> {
603+ paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Selection, 1)
604+}
605+
606+fn paste_primary_clipboard_before(
607+ cx: &mut compositor::Context,
608+ _args: &[Cow<str>],
609+ _event: PromptEvent,
610+) -> anyhow::Result<()> {
611+ paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Selection, 1)
612+}
613+
614+fn replace_selections_with_clipboard_impl(
615+ cx: &mut compositor::Context,
616+ clipboard_type: ClipboardType,
617+) -> anyhow::Result<()> {
618+ let (view, doc) = current!(cx.editor);
619+
620+ match cx.editor.clipboard_provider.get_contents(clipboard_type) {
621+ Ok(contents) => {
622+ let selection = doc.selection(view.id);
623+ let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
624+ (range.from(), range.to(), Some(contents.as_str().into()))
625+ });
626+
627+ doc.apply(&transaction, view.id);
628+ doc.append_changes_to_history(view.id);
629+ Ok(())
630+ }
631+ Err(e) => Err(e.context("Couldn't get system clipboard contents")),
632+ }
633+}
634+
635+fn replace_selections_with_clipboard(
636+ cx: &mut compositor::Context,
637+ _args: &[Cow<str>],
638+ _event: PromptEvent,
639+) -> anyhow::Result<()> {
640+ replace_selections_with_clipboard_impl(cx, ClipboardType::Clipboard)
641+}
642+
643+fn replace_selections_with_primary_clipboard(
644+ cx: &mut compositor::Context,
645+ _args: &[Cow<str>],
646+ _event: PromptEvent,
647+) -> anyhow::Result<()> {
648+ replace_selections_with_clipboard_impl(cx, ClipboardType::Selection)
649+}
650+
651+fn show_clipboard_provider(
652+ cx: &mut compositor::Context,
653+ _args: &[Cow<str>],
654+ _event: PromptEvent,
655+) -> anyhow::Result<()> {
656+ cx.editor
657+ .set_status(cx.editor.clipboard_provider.name().to_string());
658+ Ok(())
659+}
660+
661+fn change_current_directory(
662+ cx: &mut compositor::Context,
663+ args: &[Cow<str>],
664+ _event: PromptEvent,
665+) -> anyhow::Result<()> {
666+ let dir = helix_core::path::expand_tilde(
667+ args.first()
668+ .context("target directory not provided")?
669+ .as_ref()
670+ .as_ref(),
671+ );
672+
673+ if let Err(e) = std::env::set_current_dir(dir) {
674+ bail!("Couldn't change the current working directory: {}", e);
675+ }
676+
677+ let cwd = std::env::current_dir().context("Couldn't get the new working directory")?;
678+ cx.editor.set_status(format!(
679+ "Current working directory is now {}",
680+ cwd.display()
681+ ));
682+ Ok(())
683+}
684+
685+fn show_current_directory(
686+ cx: &mut compositor::Context,
687+ _args: &[Cow<str>],
688+ _event: PromptEvent,
689+) -> anyhow::Result<()> {
690+ let cwd = std::env::current_dir().context("Couldn't get the new working directory")?;
691+ cx.editor
692+ .set_status(format!("Current working directory is {}", cwd.display()));
693+ Ok(())
694+}
695+
696+/// Sets the [`Document`]'s encoding..
697+fn set_encoding(
698+ cx: &mut compositor::Context,
699+ args: &[Cow<str>],
700+ _event: PromptEvent,
701+) -> anyhow::Result<()> {
702+ let doc = doc_mut!(cx.editor);
703+ if let Some(label) = args.first() {
704+ doc.set_encoding(label)
705+ } else {
706+ let encoding = doc.encoding().name().to_owned();
707+ cx.editor.set_status(encoding);
708+ Ok(())
709+ }
710+}
711+
712+/// Reload the [`Document`] from its source file.
713+fn reload(
714+ cx: &mut compositor::Context,
715+ _args: &[Cow<str>],
716+ _event: PromptEvent,
717+) -> anyhow::Result<()> {
718+ let (view, doc) = current!(cx.editor);
719+ doc.reload(view.id)
720+}
721+
722+fn tree_sitter_scopes(
723+ cx: &mut compositor::Context,
724+ _args: &[Cow<str>],
725+ _event: PromptEvent,
726+) -> anyhow::Result<()> {
727+ let (view, doc) = current!(cx.editor);
728+ let text = doc.text().slice(..);
729+
730+ let pos = doc.selection(view.id).primary().cursor(text);
731+ let scopes = indent::get_scopes(doc.syntax(), text, pos);
732+ cx.editor.set_status(format!("scopes: {:?}", &scopes));
733+ Ok(())
734+}
735+
736+fn vsplit(
737+ cx: &mut compositor::Context,
738+ args: &[Cow<str>],
739+ _event: PromptEvent,
740+) -> anyhow::Result<()> {
741+ let id = view!(cx.editor).doc;
742+
743+ if args.is_empty() {
744+ cx.editor.switch(id, Action::VerticalSplit);
745+ } else {
746+ for arg in args {
747+ cx.editor
748+ .open(PathBuf::from(arg.as_ref()), Action::VerticalSplit)?;
749+ }
750+ }
751+
752+ Ok(())
753+}
754+
755+fn hsplit(
756+ cx: &mut compositor::Context,
757+ args: &[Cow<str>],
758+ _event: PromptEvent,
759+) -> anyhow::Result<()> {
760+ let id = view!(cx.editor).doc;
761+
762+ if args.is_empty() {
763+ cx.editor.switch(id, Action::HorizontalSplit);
764+ } else {
765+ for arg in args {
766+ cx.editor
767+ .open(PathBuf::from(arg.as_ref()), Action::HorizontalSplit)?;
768+ }
769+ }
770+
771+ Ok(())
772+}
773+
774+fn debug_eval(
775+ cx: &mut compositor::Context,
776+ args: &[Cow<str>],
777+ _event: PromptEvent,
778+) -> anyhow::Result<()> {
779+ if let Some(debugger) = cx.editor.debugger.as_mut() {
780+ let (frame, thread_id) = match (debugger.active_frame, debugger.thread_id) {
781+ (Some(frame), Some(thread_id)) => (frame, thread_id),
782+ _ => {
783+ bail!("Cannot find current stack frame to access variables")
784+ }
785+ };
786+
787+ // TODO: support no frame_id
788+
789+ let frame_id = debugger.stack_frames[&thread_id][frame].id;
790+ let response = helix_lsp::block_on(debugger.eval(args.join(" "), Some(frame_id)))?;
791+ cx.editor.set_status(response.result);
792+ }
793+ Ok(())
794+}
795+
796+fn debug_start(
797+ cx: &mut compositor::Context,
798+ args: &[Cow<str>],
799+ _event: PromptEvent,
800+) -> anyhow::Result<()> {
801+ let mut args = args.to_owned();
802+ let name = match args.len() {
803+ 0 => None,
804+ _ => Some(args.remove(0)),
805+ };
806+ dap_start_impl(cx, name.as_deref(), None, Some(args))
807+}
808+
809+fn debug_remote(
810+ cx: &mut compositor::Context,
811+ args: &[Cow<str>],
812+ _event: PromptEvent,
813+) -> anyhow::Result<()> {
814+ let mut args = args.to_owned();
815+ let address = match args.len() {
816+ 0 => None,
817+ _ => Some(args.remove(0).parse()?),
818+ };
819+ let name = match args.len() {
820+ 0 => None,
821+ _ => Some(args.remove(0)),
822+ };
823+ dap_start_impl(cx, name.as_deref(), address, Some(args))
824+}
825+
826+fn tutor(
827+ cx: &mut compositor::Context,
828+ _args: &[Cow<str>],
829+ _event: PromptEvent,
830+) -> anyhow::Result<()> {
831+ let path = helix_core::runtime_dir().join("tutor.txt");
832+ cx.editor.open(path, Action::Replace)?;
833+ // Unset path to prevent accidentally saving to the original tutor file.
834+ doc_mut!(cx.editor).set_path(None)?;
835+ Ok(())
836+}
837+
838+pub(super) fn goto_line_number(
839+ cx: &mut compositor::Context,
840+ args: &[Cow<str>],
841+ _event: PromptEvent,
842+) -> anyhow::Result<()> {
843+ ensure!(!args.is_empty(), "Line number required");
844+
845+ let line = args[0].parse::<usize>()?;
846+
847+ goto_line_impl(cx.editor, NonZeroUsize::new(line));
848+
849+ let (view, doc) = current!(cx.editor);
850+
851+ view.ensure_cursor_in_view(doc, line);
852+ Ok(())
853+}
854+
855+fn setting(
856+ cx: &mut compositor::Context,
857+ args: &[Cow<str>],
858+ _event: PromptEvent,
859+) -> anyhow::Result<()> {
860+ let runtime_config = &mut cx.editor.config;
861+
862+ if args.len() != 2 {
863+ anyhow::bail!("Bad arguments. Usage: `:set key field`");
864+ }
865+
866+ let (key, arg) = (&args[0].to_lowercase(), &args[1]);
867+
868+ match key.as_ref() {
869+ "scrolloff" => runtime_config.scrolloff = arg.parse()?,
870+ "scroll-lines" => runtime_config.scroll_lines = arg.parse()?,
871+ "mouse" => runtime_config.mouse = arg.parse()?,
872+ "line-number" => runtime_config.line_number = arg.parse()?,
873+ "middle-click_paste" => runtime_config.middle_click_paste = arg.parse()?,
874+ "auto-pairs" => runtime_config.auto_pairs = arg.parse()?,
875+ "auto-completion" => runtime_config.auto_completion = arg.parse()?,
876+ "completion-trigger-len" => runtime_config.completion_trigger_len = arg.parse()?,
877+ "auto-info" => runtime_config.auto_info = arg.parse()?,
878+ "true-color" => runtime_config.true_color = arg.parse()?,
879+ "search.smart-case" => runtime_config.search.smart_case = arg.parse()?,
880+ "search.wrap-around" => runtime_config.search.wrap_around = arg.parse()?,
881+ _ => anyhow::bail!("Unknown key `{}`.", args[0]),
882+ }
883+
884+ Ok(())
885+}
886+
887+fn sort(
888+ cx: &mut compositor::Context,
889+ args: &[Cow<str>],
890+ _event: PromptEvent,
891+) -> anyhow::Result<()> {
892+ sort_impl(cx, args, false)
893+}
894+
895+fn sort_reverse(
896+ cx: &mut compositor::Context,
897+ args: &[Cow<str>],
898+ _event: PromptEvent,
899+) -> anyhow::Result<()> {
900+ sort_impl(cx, args, true)
901+}
902+
903+fn sort_impl(
904+ cx: &mut compositor::Context,
905+ _args: &[Cow<str>],
906+ reverse: bool,
907+) -> anyhow::Result<()> {
908+ let (view, doc) = current!(cx.editor);
909+ let text = doc.text().slice(..);
910+
911+ let selection = doc.selection(view.id);
912+
913+ let mut fragments: Vec<_> = selection
914+ .fragments(text)
915+ .map(|fragment| Tendril::from(fragment.as_ref()))
916+ .collect();
917+
918+ fragments.sort_by(match reverse {
919+ true => |a: &Tendril, b: &Tendril| b.cmp(a),
920+ false => |a: &Tendril, b: &Tendril| a.cmp(b),
921+ });
922+
923+ let transaction = Transaction::change(
924+ doc.text(),
925+ selection
926+ .into_iter()
927+ .zip(fragments)
928+ .map(|(s, fragment)| (s.from(), s.to(), Some(fragment))),
929+ );
930+
931+ doc.apply(&transaction, view.id);
932+ doc.append_changes_to_history(view.id);
933+
934+ Ok(())
935+}
936+
937+fn tree_sitter_subtree(
938+ cx: &mut compositor::Context,
939+ _args: &[Cow<str>],
940+ _event: PromptEvent,
941+) -> anyhow::Result<()> {
942+ let (view, doc) = current!(cx.editor);
943+
944+ if let Some(syntax) = doc.syntax() {
945+ let primary_selection = doc.selection(view.id).primary();
946+ let text = doc.text();
947+ let from = text.char_to_byte(primary_selection.from());
948+ let to = text.char_to_byte(primary_selection.to());
949+ if let Some(selected_node) = syntax
950+ .tree()
951+ .root_node()
952+ .descendant_for_byte_range(from, to)
953+ {
954+ let contents = format!("```tsq\n{}\n```", selected_node.to_sexp());
955+
956+ let callback = async move {
957+ let call: job::Callback =
958+ Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
959+ let contents = ui::Markdown::new(contents, editor.syn_loader.clone());
960+ let popup = Popup::new("hover", contents).auto_close(true);
961+ compositor.replace_or_push("hover", popup);
962+ });
963+ Ok(call)
964+ };
965+
966+ cx.jobs.callback(callback);
967+ }
968+ }
969+
970+ Ok(())
971+}
972+
973+pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
974+ TypableCommand {
975+ name: "quit",
976+ aliases: &["q"],
977+ doc: "Close the current view.",
978+ fun: quit,
979+ completer: None,
980+ },
981+ TypableCommand {
982+ name: "quit!",
983+ aliases: &["q!"],
984+ doc: "Close the current view forcefully (ignoring unsaved changes).",
985+ fun: force_quit,
986+ completer: None,
987+ },
988+ TypableCommand {
989+ name: "open",
990+ aliases: &["o"],
991+ doc: "Open a file from disk into the current view.",
992+ fun: open,
993+ completer: Some(completers::filename),
994+ },
995+ TypableCommand {
996+ name: "buffer-close",
997+ aliases: &["bc", "bclose"],
998+ doc: "Close the current buffer.",
999+ fun: buffer_close,
1000+ completer: Some(completers::buffer),
1001+ },
1002+ TypableCommand {
1003+ name: "buffer-close!",
1004+ aliases: &["bc!", "bclose!"],
1005+ doc: "Close the current buffer forcefully (ignoring unsaved changes).",
1006+ fun: force_buffer_close,
1007+ completer: Some(completers::buffer),
1008+ },
1009+ TypableCommand {
1010+ name: "buffer-close-others",
1011+ aliases: &["bco", "bcloseother"],
1012+ doc: "Close all buffers but the currently focused one.",
1013+ fun: buffer_close_others,
1014+ completer: None,
1015+ },
1016+ TypableCommand {
1017+ name: "buffer-close-others!",
1018+ aliases: &["bco!", "bcloseother!"],
1019+ doc: "Close all buffers but the currently focused one.",
1020+ fun: force_buffer_close_others,
1021+ completer: None,
1022+ },
1023+ TypableCommand {
1024+ name: "buffer-close-all",
1025+ aliases: &["bca", "bcloseall"],
1026+ doc: "Close all buffers, without quiting.",
1027+ fun: buffer_close_all,
1028+ completer: None,
1029+ },
1030+ TypableCommand {
1031+ name: "buffer-close-all!",
1032+ aliases: &["bca!", "bcloseall!"],
1033+ doc: "Close all buffers forcefully (ignoring unsaved changes), without quiting.",
1034+ fun: force_buffer_close_all,
1035+ completer: None,
1036+ },
1037+ TypableCommand {
1038+ name: "write",
1039+ aliases: &["w"],
1040+ doc: "Write changes to disk. Accepts an optional path (:write some/path.txt)",
1041+ fun: write,
1042+ completer: Some(completers::filename),
1043+ },
1044+ TypableCommand {
1045+ name: "new",
1046+ aliases: &["n"],
1047+ doc: "Create a new scratch buffer.",
1048+ fun: new_file,
1049+ completer: Some(completers::filename),
1050+ },
1051+ TypableCommand {
1052+ name: "format",
1053+ aliases: &["fmt"],
1054+ doc: "Format the file using the LSP formatter.",
1055+ fun: format,
1056+ completer: None,
1057+ },
1058+ TypableCommand {
1059+ name: "indent-style",
1060+ aliases: &[],
1061+ doc: "Set the indentation style for editing. ('t' for tabs or 1-8 for number of spaces.)",
1062+ fun: set_indent_style,
1063+ completer: None,
1064+ },
1065+ TypableCommand {
1066+ name: "line-ending",
1067+ aliases: &[],
1068+ doc: "Set the document's default line ending. Options: crlf, lf, cr, ff, nel.",
1069+ fun: set_line_ending,
1070+ completer: None,
1071+ },
1072+ TypableCommand {
1073+ name: "earlier",
1074+ aliases: &["ear"],
1075+ doc: "Jump back to an earlier point in edit history. Accepts a number of steps or a time span.",
1076+ fun: earlier,
1077+ completer: None,
1078+ },
1079+ TypableCommand {
1080+ name: "later",
1081+ aliases: &["lat"],
1082+ doc: "Jump to a later point in edit history. Accepts a number of steps or a time span.",
1083+ fun: later,
1084+ completer: None,
1085+ },
1086+ TypableCommand {
1087+ name: "write-quit",
1088+ aliases: &["wq", "x"],
1089+ doc: "Write changes to disk and close the current view. Accepts an optional path (:wq some/path.txt)",
1090+ fun: write_quit,
1091+ completer: Some(completers::filename),
1092+ },
1093+ TypableCommand {
1094+ name: "write-quit!",
1095+ aliases: &["wq!", "x!"],
1096+ doc: "Write changes to disk and close the current view forcefully. Accepts an optional path (:wq! some/path.txt)",
1097+ fun: force_write_quit,
1098+ completer: Some(completers::filename),
1099+ },
1100+ TypableCommand {
1101+ name: "write-all",
1102+ aliases: &["wa"],
1103+ doc: "Write changes from all views to disk.",
1104+ fun: write_all,
1105+ completer: None,
1106+ },
1107+ TypableCommand {
1108+ name: "write-quit-all",
1109+ aliases: &["wqa", "xa"],
1110+ doc: "Write changes from all views to disk and close all views.",
1111+ fun: write_all_quit,
1112+ completer: None,
1113+ },
1114+ TypableCommand {
1115+ name: "write-quit-all!",
1116+ aliases: &["wqa!", "xa!"],
1117+ doc: "Write changes from all views to disk and close all views forcefully (ignoring unsaved changes).",
1118+ fun: force_write_all_quit,
1119+ completer: None,
1120+ },
1121+ TypableCommand {
1122+ name: "quit-all",
1123+ aliases: &["qa"],
1124+ doc: "Close all views.",
1125+ fun: quit_all,
1126+ completer: None,
1127+ },
1128+ TypableCommand {
1129+ name: "quit-all!",
1130+ aliases: &["qa!"],
1131+ doc: "Close all views forcefully (ignoring unsaved changes).",
1132+ fun: force_quit_all,
1133+ completer: None,
1134+ },
1135+ TypableCommand {
1136+ name: "cquit",
1137+ aliases: &["cq"],
1138+ doc: "Quit with exit code (default 1). Accepts an optional integer exit code (:cq 2).",
1139+ fun: cquit,
1140+ completer: None,
1141+ },
1142+ TypableCommand {
1143+ name: "cquit!",
1144+ aliases: &["cq!"],
1145+ doc: "Quit with exit code (default 1) forcefully (ignoring unsaved changes). Accepts an optional integer exit code (:cq! 2).",
1146+ fun: force_cquit,
1147+ completer: None,
1148+ },
1149+ TypableCommand {
1150+ name: "theme",
1151+ aliases: &[],
1152+ doc: "Change the editor theme.",
1153+ fun: theme,
1154+ completer: Some(completers::theme),
1155+ },
1156+ TypableCommand {
1157+ name: "clipboard-yank",
1158+ aliases: &[],
1159+ doc: "Yank main selection into system clipboard.",
1160+ fun: yank_main_selection_to_clipboard,
1161+ completer: None,
1162+ },
1163+ TypableCommand {
1164+ name: "clipboard-yank-join",
1165+ aliases: &[],
1166+ 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.
1167+ fun: yank_joined_to_clipboard,
1168+ completer: None,
1169+ },
1170+ TypableCommand {
1171+ name: "primary-clipboard-yank",
1172+ aliases: &[],
1173+ doc: "Yank main selection into system primary clipboard.",
1174+ fun: yank_main_selection_to_primary_clipboard,
1175+ completer: None,
1176+ },
1177+ TypableCommand {
1178+ name: "primary-clipboard-yank-join",
1179+ aliases: &[],
1180+ 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.
1181+ fun: yank_joined_to_primary_clipboard,
1182+ completer: None,
1183+ },
1184+ TypableCommand {
1185+ name: "clipboard-paste-after",
1186+ aliases: &[],
1187+ doc: "Paste system clipboard after selections.",
1188+ fun: paste_clipboard_after,
1189+ completer: None,
1190+ },
1191+ TypableCommand {
1192+ name: "clipboard-paste-before",
1193+ aliases: &[],
1194+ doc: "Paste system clipboard before selections.",
1195+ fun: paste_clipboard_before,
1196+ completer: None,
1197+ },
1198+ TypableCommand {
1199+ name: "clipboard-paste-replace",
1200+ aliases: &[],
1201+ doc: "Replace selections with content of system clipboard.",
1202+ fun: replace_selections_with_clipboard,
1203+ completer: None,
1204+ },
1205+ TypableCommand {
1206+ name: "primary-clipboard-paste-after",
1207+ aliases: &[],
1208+ doc: "Paste primary clipboard after selections.",
1209+ fun: paste_primary_clipboard_after,
1210+ completer: None,
1211+ },
1212+ TypableCommand {
1213+ name: "primary-clipboard-paste-before",
1214+ aliases: &[],
1215+ doc: "Paste primary clipboard before selections.",
1216+ fun: paste_primary_clipboard_before,
1217+ completer: None,
1218+ },
1219+ TypableCommand {
1220+ name: "primary-clipboard-paste-replace",
1221+ aliases: &[],
1222+ doc: "Replace selections with content of system primary clipboard.",
1223+ fun: replace_selections_with_primary_clipboard,
1224+ completer: None,
1225+ },
1226+ TypableCommand {
1227+ name: "show-clipboard-provider",
1228+ aliases: &[],
1229+ doc: "Show clipboard provider name in status bar.",
1230+ fun: show_clipboard_provider,
1231+ completer: None,
1232+ },
1233+ TypableCommand {
1234+ name: "change-current-directory",
1235+ aliases: &["cd"],
1236+ doc: "Change the current working directory.",
1237+ fun: change_current_directory,
1238+ completer: Some(completers::directory),
1239+ },
1240+ TypableCommand {
1241+ name: "show-directory",
1242+ aliases: &["pwd"],
1243+ doc: "Show the current working directory.",
1244+ fun: show_current_directory,
1245+ completer: None,
1246+ },
1247+ TypableCommand {
1248+ name: "encoding",
1249+ aliases: &[],
1250+ doc: "Set encoding based on `https://encoding.spec.whatwg.org`",
1251+ fun: set_encoding,
1252+ completer: None,
1253+ },
1254+ TypableCommand {
1255+ name: "reload",
1256+ aliases: &[],
1257+ doc: "Discard changes and reload from the source file.",
1258+ fun: reload,
1259+ completer: None,
1260+ },
1261+ TypableCommand {
1262+ name: "tree-sitter-scopes",
1263+ aliases: &[],
1264+ doc: "Display tree sitter scopes, primarily for theming and development.",
1265+ fun: tree_sitter_scopes,
1266+ completer: None,
1267+ },
1268+ TypableCommand {
1269+ name: "debug-start",
1270+ aliases: &["dbg"],
1271+ doc: "Start a debug session from a given template with given parameters.",
1272+ fun: debug_start,
1273+ completer: None,
1274+ },
1275+ TypableCommand {
1276+ name: "debug-remote",
1277+ aliases: &["dbg-tcp"],
1278+ doc: "Connect to a debug adapter by TCP address and start a debugging session from a given template with given parameters.",
1279+ fun: debug_remote,
1280+ completer: None,
1281+ },
1282+ TypableCommand {
1283+ name: "debug-eval",
1284+ aliases: &[],
1285+ doc: "Evaluate expression in current debug context.",
1286+ fun: debug_eval,
1287+ completer: None,
1288+ },
1289+ TypableCommand {
1290+ name: "vsplit",
1291+ aliases: &["vs"],
1292+ doc: "Open the file in a vertical split.",
1293+ fun: vsplit,
1294+ completer: Some(completers::filename),
1295+ },
1296+ TypableCommand {
1297+ name: "hsplit",
1298+ aliases: &["hs", "sp"],
1299+ doc: "Open the file in a horizontal split.",
1300+ fun: hsplit,
1301+ completer: Some(completers::filename),
1302+ },
1303+ TypableCommand {
1304+ name: "tutor",
1305+ aliases: &[],
1306+ doc: "Open the tutorial.",
1307+ fun: tutor,
1308+ completer: None,
1309+ },
1310+ TypableCommand {
1311+ name: "goto",
1312+ aliases: &["g"],
1313+ doc: "Go to line number.",
1314+ fun: goto_line_number,
1315+ completer: None,
1316+ },
1317+ TypableCommand {
1318+ name: "set-option",
1319+ aliases: &["set"],
1320+ doc: "Set a config option at runtime",
1321+ fun: setting,
1322+ completer: Some(completers::setting),
1323+ },
1324+ TypableCommand {
1325+ name: "sort",
1326+ aliases: &[],
1327+ doc: "Sort ranges in selection.",
1328+ fun: sort,
1329+ completer: None,
1330+ },
1331+ TypableCommand {
1332+ name: "rsort",
1333+ aliases: &[],
1334+ doc: "Sort ranges in selection in reverse order.",
1335+ fun: sort_reverse,
1336+ completer: None,
1337+ },
1338+ TypableCommand {
1339+ name: "tree-sitter-subtree",
1340+ aliases: &["ts-subtree"],
1341+ doc: "Display tree sitter subtree under cursor, primarily for debugging queries.",
1342+ fun: tree_sitter_subtree,
1343+ completer: None,
1344+ },
1345+ ];
1346+
1347+pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> =
1348+ Lazy::new(|| {
1349+ TYPABLE_COMMAND_LIST
1350+ .iter()
1351+ .flat_map(|cmd| {
1352+ std::iter::once((cmd.name, cmd))
1353+ .chain(cmd.aliases.iter().map(move |&alias| (alias, cmd)))
1354+ })
1355+ .collect()
1356+ });
1357+
1358+pub fn command_mode(cx: &mut Context) {
1359+ let mut prompt = Prompt::new(
1360+ ":".into(),
1361+ Some(':'),
1362+ |editor: &Editor, input: &str| {
1363+ static FUZZY_MATCHER: Lazy<fuzzy_matcher::skim::SkimMatcherV2> =
1364+ Lazy::new(fuzzy_matcher::skim::SkimMatcherV2::default);
1365+
1366+ // we use .this over split_whitespace() because we care about empty segments
1367+ let parts = input.split(' ').collect::<Vec<&str>>();
1368+
1369+ // simple heuristic: if there's no just one part, complete command name.
1370+ // if there's a space, per command completion kicks in.
1371+ if parts.len() <= 1 {
1372+ let mut matches: Vec<_> = typed::TYPABLE_COMMAND_LIST
1373+ .iter()
1374+ .filter_map(|command| {
1375+ FUZZY_MATCHER
1376+ .fuzzy_match(command.name, input)
1377+ .map(|score| (command.name, score))
1378+ })
1379+ .collect();
1380+
1381+ matches.sort_unstable_by_key(|(_file, score)| std::cmp::Reverse(*score));
1382+ matches
1383+ .into_iter()
1384+ .map(|(name, _)| (0.., name.into()))
1385+ .collect()
1386+ } else {
1387+ let part = parts.last().unwrap();
1388+
1389+ if let Some(typed::TypableCommand {
1390+ completer: Some(completer),
1391+ ..
1392+ }) = typed::TYPABLE_COMMAND_MAP.get(parts[0])
1393+ {
1394+ completer(editor, part)
1395+ .into_iter()
1396+ .map(|(range, file)| {
1397+ // offset ranges to input
1398+ let offset = input.len() - part.len();
1399+ let range = (range.start + offset)..;
1400+ (range, file)
1401+ })
1402+ .collect()
1403+ } else {
1404+ Vec::new()
1405+ }
1406+ }
1407+ }, // completion
1408+ move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
1409+ if event != PromptEvent::Validate {
1410+ return;
1411+ }
1412+
1413+ let parts = input.split_whitespace().collect::<Vec<&str>>();
1414+ if parts.is_empty() {
1415+ return;
1416+ }
1417+
1418+ // If command is numeric, interpret as line number and go there.
1419+ if parts.len() == 1 && parts[0].parse::<usize>().ok().is_some() {
1420+ if let Err(e) = typed::goto_line_number(cx, &[Cow::from(parts[0])], event) {
1421+ cx.editor.set_error(format!("{}", e));
1422+ }
1423+ return;
1424+ }
1425+
1426+ // Handle typable commands
1427+ if let Some(cmd) = typed::TYPABLE_COMMAND_MAP.get(parts[0]) {
1428+ let args = if cfg!(unix) {
1429+ shellwords::shellwords(input)
1430+ } else {
1431+ // Windows doesn't support POSIX, so fallback for now
1432+ parts
1433+ .into_iter()
1434+ .map(|part| part.into())
1435+ .collect::<Vec<_>>()
1436+ };
1437+
1438+ if let Err(e) = (cmd.fun)(cx, &args[1..], event) {
1439+ cx.editor.set_error(format!("{}", e));
1440+ }
1441+ } else {
1442+ cx.editor
1443+ .set_error(format!("no such command: '{}'", parts[0]));
1444+ };
1445+ },
1446+ );
1447+ prompt.doc_fn = Box::new(|input: &str| {
1448+ let part = input.split(' ').next().unwrap_or_default();
1449+
1450+ if let Some(typed::TypableCommand { doc, aliases, .. }) =
1451+ typed::TYPABLE_COMMAND_MAP.get(part)
1452+ {
1453+ if aliases.is_empty() {
1454+ return Some((*doc).into());
1455+ }
1456+ return Some(format!("{}\nAliases: {}", doc, aliases.join(", ")).into());
1457+ }
1458+
1459+ None
1460+ });
1461+
1462+ // Calculate initial completion
1463+ prompt.recalculate_completion(cx.editor);
1464+ cx.push_layer(Box::new(prompt));
1465+}
xtask/src/main.rs+1−1
@@ -107,7 +107,7 @@ pub mod md_gen {
107107 use crate::path;
108108 use std::fs;
109109
110− use helix_term::commands::cmd::TYPABLE_COMMAND_LIST;
110+ use helix_term::commands::TYPABLE_COMMAND_LIST;
111111
112112 pub const TYPABLE_COMMANDS_MD_OUTPUT: &str = "typable-cmd.md";
113113 pub const LANG_SUPPORT_MD_OUTPUT: &str = "lang-support.md";