1use crate::AppEffect;
73use crate::args::{ArgDefault, ArgKind, ArgSpec, ArgValue, Args};
74use crate::command::LatencyClass;
75use crate::effect::{Effect, LspRequest, QuitScope, SubstituteScope};
76use crate::error::{CommandError, GrammarResult};
77use crate::range::Range;
78use crate::registry::{CommandRegistry, ExCommandContext, ExCommandId, ExCommandSpec, SurfaceForm};
79use std::sync::Arc;
80
81#[derive(Debug, Clone, Copy)]
91pub struct ExBuiltins {
92 pub write: ExCommandId,
94 pub quit: ExCommandId,
96 pub write_quit: ExCommandId,
98 pub no_hlsearch: ExCommandId,
100 pub list_registers: ExCommandId,
102 pub list_marks: ExCommandId,
104 pub delete_line: ExCommandId,
106 pub set_option: ExCommandId,
108 pub set_local_option: ExCommandId,
110 pub set_global_option: ExCommandId,
112 pub colorscheme: ExCommandId,
116 pub edit: ExCommandId,
118 pub substitute: ExCommandId,
120 pub global: ExCommandId,
122 pub describe_command: ExCommandId,
124 pub describe_buffer: ExCommandId,
126 pub apropos: ExCommandId,
128 pub describe_key: ExCommandId,
130 pub list_keymap: ExCommandId,
132 pub buffer_next: ExCommandId,
134 pub buffer_prev: ExCommandId,
136 pub list_buffers: ExCommandId,
138 pub buffer_delete: ExCommandId,
140 pub file_tree: ExCommandId,
142 pub file_tree_close: ExCommandId,
144 pub oil: ExCommandId,
146 pub describe_option: ExCommandId,
148 pub describe_element: ExCommandId,
151 pub list_options: ExCommandId,
153 pub describe_plugin_api: ExCommandId,
157 pub list_plugin_apis: ExCommandId,
159 pub export_plugin_api: ExCommandId,
162 pub list_commands: ExCommandId,
164 pub describe_plugin: ExCommandId,
167 pub list_plugins: ExCommandId,
169 pub describe_events: ExCommandId,
171 pub describe_event: ExCommandId,
173 pub list_modes: ExCommandId,
181 pub describe_mode: ExCommandId,
183 pub describe_option_resolution: ExCommandId,
185 pub customize: ExCommandId,
187 pub tutor: ExCommandId,
189 pub tutor_next: ExCommandId,
191 pub tutor_prev: ExCommandId,
193 pub hover: ExCommandId,
195 pub hover_close: ExCommandId,
197 pub help: ExCommandId,
199 pub list_diagnostics: ExCommandId,
201 pub next_diagnostic: ExCommandId,
203 pub prev_diagnostic: ExCommandId,
205 pub lsp_log: ExCommandId,
207 pub messages: ExCommandId,
209 pub lsp_trace: ExCommandId,
211 pub lsp_status: ExCommandId,
213 pub lsp_server_log: ExCommandId,
215 pub lsp_restart: ExCommandId,
217 pub lsp_progress_cancel: ExCommandId,
219 pub lsp_expand_region: ExCommandId,
221 pub lsp_shrink_region: ExCommandId,
223 pub lsp_log_level: ExCommandId,
225 pub lsp_log_clear: ExCommandId,
227 pub lsp_symbols: ExCommandId,
229 pub lsp_workspace_symbol: ExCommandId,
231 pub lsp_incoming_calls: ExCommandId,
233 pub lsp_outgoing_calls: ExCommandId,
235 pub lsp_supertypes: ExCommandId,
237 pub lsp_subtypes: ExCommandId,
239 pub lsp_moniker: ExCommandId,
241 pub lsp_code_lens: ExCommandId,
243 pub lsp_color_presentation: ExCommandId,
245 pub format: ExCommandId,
247 pub lsp_format: ExCommandId,
249 pub lsp_format_range: ExCommandId,
251 pub lsp_signature_help: ExCommandId,
253 pub lsp_complete: ExCommandId,
255 pub lsp_rename: ExCommandId,
257 pub lsp_code_action: ExCommandId,
259 pub reload_snippets: ExCommandId,
264 pub cd: ExCommandId,
266 pub pwd: ExCommandId,
268 pub project_root: ExCommandId,
271}
272
273pub fn populate(registry: &mut CommandRegistry) -> ExBuiltins {
282 let write = registry.register_ex_command(
283 "ex:write",
284 "Write the current buffer to disk (`:w [path]`).",
285 ExCommandSpec {
286 latency_class: LatencyClass::Display,
287 accepts_bang: false,
288 accepts_range: false,
289 parse_args: Arc::new(parse_optional_path),
290 apply: Arc::new(apply_write),
291 args_schema: vec![ArgSpec {
292 name: "path".into(),
293 kind: ArgKind::String,
294 doc: "Destination path. Absent = overwrite current file.".into(),
295 prompt: "path:".into(),
296 default: ArgDefault::None,
297 completion: Some("gen:files".into()),
298 picker: None,
299 }],
300 surface_form: SurfaceForm::Keyword,
301 },
302 );
303 let quit = registry.register_ex_command(
304 "ex:quit",
305 "Quit the editor (`:q[!]`).",
306 ExCommandSpec {
307 latency_class: LatencyClass::Reflex,
308 accepts_bang: true,
309 accepts_range: false,
310 parse_args: Arc::new(parse_no_args),
311 apply: Arc::new(apply_quit),
312 args_schema: vec![],
313 surface_form: SurfaceForm::Keyword,
314 },
315 );
316 let write_quit = registry.register_ex_command(
317 "ex:write-quit",
318 "Write the current buffer and quit (`:wq[!]` / `:x[!]`).",
319 ExCommandSpec {
320 latency_class: LatencyClass::Display,
321 accepts_bang: true,
322 accepts_range: false,
323 parse_args: Arc::new(parse_no_args),
324 apply: Arc::new(apply_write_quit),
325 args_schema: vec![],
326 surface_form: SurfaceForm::Keyword,
327 },
328 );
329 let _quit_all = registry.register_ex_command(
336 "ex:quit-all",
337 "Quit the editor, closing every pane and tab (`:qa[!]`).",
338 ExCommandSpec {
339 latency_class: LatencyClass::Reflex,
340 accepts_bang: true,
341 accepts_range: false,
342 parse_args: Arc::new(parse_no_args),
343 apply: Arc::new(apply_quit_all),
344 args_schema: vec![],
345 surface_form: SurfaceForm::Keyword,
346 },
347 );
348 let _only = registry.register_ex_command(
354 "ex:only",
355 "Close every pane except the active one (`:only`).",
356 ExCommandSpec {
357 latency_class: LatencyClass::Reflex,
358 accepts_bang: false,
359 accepts_range: false,
360 parse_args: Arc::new(parse_no_args),
361 apply: Arc::new(|_| Ok(Effect::AppAction(AppEffect::OnlyPane))),
362 args_schema: vec![],
363 surface_form: SurfaceForm::Keyword,
364 },
365 );
366 let _zoom_pane = registry.register_ex_command(
371 "ex:zoom-pane",
372 "Toggle tmux-style zoom on the active pane (`:zoom-pane`).",
373 ExCommandSpec {
374 latency_class: LatencyClass::Reflex,
375 accepts_bang: false,
376 accepts_range: false,
377 parse_args: Arc::new(parse_no_args),
378 apply: Arc::new(|_| Ok(Effect::AppAction(AppEffect::ToggleZoomPane))),
379 args_schema: vec![],
380 surface_form: SurfaceForm::Keyword,
381 },
382 );
383 let _split = registry.register_ex_command(
389 "ex:split",
390 "Split the window horizontally (`:sp[lit]`).",
391 ExCommandSpec {
392 latency_class: LatencyClass::Reflex,
393 accepts_bang: false,
394 accepts_range: false,
395 parse_args: Arc::new(parse_no_args),
396 apply: Arc::new(|_| Ok(Effect::AppAction(AppEffect::SplitPaneHorizontal))),
397 args_schema: vec![],
398 surface_form: SurfaceForm::Keyword,
399 },
400 );
401 let _vsplit = registry.register_ex_command(
402 "ex:vsplit",
403 "Split the window vertically (`:vs[plit]`).",
404 ExCommandSpec {
405 latency_class: LatencyClass::Reflex,
406 accepts_bang: false,
407 accepts_range: false,
408 parse_args: Arc::new(parse_no_args),
409 apply: Arc::new(|_| Ok(Effect::AppAction(AppEffect::SplitPaneVertical))),
410 args_schema: vec![],
411 surface_form: SurfaceForm::Keyword,
412 },
413 );
414 let _close = registry.register_ex_command(
415 "ex:close",
416 "Close the active pane (`:clo[se]`).",
417 ExCommandSpec {
418 latency_class: LatencyClass::Reflex,
419 accepts_bang: false,
420 accepts_range: false,
421 parse_args: Arc::new(parse_no_args),
422 apply: Arc::new(|_| Ok(Effect::AppAction(AppEffect::ClosePane))),
423 args_schema: vec![],
424 surface_form: SurfaceForm::Keyword,
425 },
426 );
427 let _fold_cycle = registry.register_ex_command(
432 "fold-cycle",
433 "org-cycle: cycle the fold under the cursor through \
434 FOLDED → CHILDREN → SUBTREE (`z<Space>`).",
435 ExCommandSpec {
436 latency_class: LatencyClass::Reflex,
437 accepts_bang: false,
438 accepts_range: false,
439 parse_args: Arc::new(parse_no_args),
440 apply: Arc::new(|_| Ok(Effect::AppAction(AppEffect::CycleFoldAtCursor))),
441 args_schema: vec![],
442 surface_form: SurfaceForm::Keyword,
443 },
444 );
445 let _fold_cycle_global = registry.register_ex_command(
446 "fold-cycle-global",
447 "org-cycle: cycle the whole buffer through \
448 OVERVIEW → CONTENTS → SHOW-ALL (`z<Tab>`).",
449 ExCommandSpec {
450 latency_class: LatencyClass::Reflex,
451 accepts_bang: false,
452 accepts_range: false,
453 parse_args: Arc::new(parse_no_args),
454 apply: Arc::new(|_| Ok(Effect::AppAction(AppEffect::CycleFoldsGlobal))),
455 args_schema: vec![],
456 surface_form: SurfaceForm::Keyword,
457 },
458 );
459 let _fold_goto_parent = registry.register_ex_command(
460 "fold-goto-parent",
461 "Move the cursor to the parent heading, one level up the fold \
462 hierarchy (`zp`).",
463 ExCommandSpec {
464 latency_class: LatencyClass::Reflex,
465 accepts_bang: false,
466 accepts_range: false,
467 parse_args: Arc::new(parse_no_args),
468 apply: Arc::new(|_| Ok(Effect::AppAction(AppEffect::GotoParentFold))),
469 args_schema: vec![],
470 surface_form: SurfaceForm::Keyword,
471 },
472 );
473 let no_hlsearch = registry.register_ex_command(
474 "ex:nohlsearch",
475 "Clear the search-highlight overlay (`:noh[lsearch]`).",
476 ExCommandSpec {
477 latency_class: LatencyClass::Reflex,
478 accepts_bang: false,
479 accepts_range: false,
480 parse_args: Arc::new(parse_no_args),
481 apply: Arc::new(|_| Ok(Effect::ClearSearchHighlight)),
482 args_schema: vec![],
483 surface_form: SurfaceForm::Keyword,
484 },
485 );
486 let list_registers = registry.register_ex_command(
487 "ex:registers",
488 "Show every register's contents (`:reg[isters]`).",
489 ExCommandSpec {
490 latency_class: LatencyClass::Display,
491 accepts_bang: false,
492 accepts_range: false,
493 parse_args: Arc::new(parse_no_args),
494 apply: Arc::new(|_| Ok(Effect::EchoRegisters)),
495 args_schema: vec![],
496 surface_form: SurfaceForm::Keyword,
497 },
498 );
499 let list_marks = registry.register_ex_command(
500 "ex:marks",
501 "Show every set mark's name + position (`:marks`).",
502 ExCommandSpec {
503 latency_class: LatencyClass::Display,
504 accepts_bang: false,
505 accepts_range: false,
506 parse_args: Arc::new(parse_no_args),
507 apply: Arc::new(|_| Ok(Effect::EchoMarks)),
508 args_schema: vec![],
509 surface_form: SurfaceForm::Keyword,
510 },
511 );
512 let delete_line = registry.register_ex_command(
513 "ex:delete",
514 "Delete the current line including its newline (`:d[elete]`).",
515 ExCommandSpec {
516 latency_class: LatencyClass::Reflex,
517 accepts_bang: false,
518 accepts_range: false,
519 parse_args: Arc::new(parse_no_args),
520 apply: Arc::new(|_| Ok(Effect::DeleteCurrentLine)),
521 args_schema: vec![],
522 surface_form: SurfaceForm::Keyword,
523 },
524 );
525 let set_option = registry.register_ex_command(
526 "ex:set",
527 "Set a view option (`:set <option>`).",
528 ExCommandSpec {
529 latency_class: LatencyClass::Reflex,
530 accepts_bang: false,
531 accepts_range: false,
532 parse_args: Arc::new(parse_required_string),
533 apply: Arc::new(apply_set),
534 args_schema: vec![ArgSpec {
538 name: "option".into(),
539 kind: ArgKind::String,
540 doc: "Option name, `name=value`, `name?`, or `noname`.".into(),
541 prompt: "option:".into(),
542 default: ArgDefault::Required,
543 completion: Some("gen:options".into()),
544 picker: None,
545 }],
546 surface_form: SurfaceForm::Keyword,
547 },
548 );
549 let set_local_option = registry.register_ex_command(
550 "ex:setlocal",
551 "Set a buffer-local option override (`:setlocal <option>`).",
552 ExCommandSpec {
553 latency_class: LatencyClass::Reflex,
554 accepts_bang: false,
555 accepts_range: false,
556 parse_args: Arc::new(parse_required_string),
557 apply: Arc::new(apply_set_local),
558 args_schema: vec![ArgSpec {
559 name: "option".into(),
560 kind: ArgKind::String,
561 doc: "Option name, `name=value`, `noname`, or `name&` to clear.".into(),
562 prompt: "option:".into(),
563 default: ArgDefault::Required,
564 completion: Some("gen:options".into()),
565 picker: None,
566 }],
567 surface_form: SurfaceForm::Keyword,
568 },
569 );
570 let set_global_option = registry.register_ex_command(
571 "ex:setglobal",
572 "Set a global option (`:setglobal <option>`). Does not update buffer-local overrides.",
573 ExCommandSpec {
574 latency_class: LatencyClass::Reflex,
575 accepts_bang: false,
576 accepts_range: false,
577 parse_args: Arc::new(parse_required_string),
578 apply: Arc::new(apply_set_global),
579 args_schema: vec![ArgSpec {
580 name: "option".into(),
581 kind: ArgKind::String,
582 doc: "Option name, `name=value`, `noname`, or `name?` to query global value."
583 .into(),
584 prompt: "option:".into(),
585 default: ArgDefault::Required,
586 completion: Some("gen:options".into()),
587 picker: None,
588 }],
589 surface_form: SurfaceForm::Keyword,
590 },
591 );
592 let colorscheme = registry.register_ex_command(
593 "ex:colorscheme",
594 "Swap the active theme by name (`:colorscheme <name>`).",
595 ExCommandSpec {
596 latency_class: LatencyClass::Display,
597 accepts_bang: false,
598 accepts_range: false,
599 parse_args: Arc::new(parse_optional_path),
603 apply: Arc::new(apply_colorscheme),
604 args_schema: vec![ArgSpec {
605 name: "name".into(),
606 kind: ArgKind::String,
607 doc: "Theme name (`catppuccin-mocha`, `catppuccin-macchiato`). Omit to open the live-preview picker.".into(),
608 prompt: "colorscheme:".into(),
609 default: ArgDefault::None,
611 completion: Some("gen:themes".into()),
615 picker: None,
616 }],
617 surface_form: SurfaceForm::Keyword,
618 },
619 );
620 let edit = registry.register_ex_command(
621 "ex:edit",
622 "Load a file into the current document (`:e[!] [path]`).",
623 ExCommandSpec {
624 latency_class: LatencyClass::Display,
625 accepts_bang: true,
626 accepts_range: false,
627 parse_args: Arc::new(parse_optional_path),
628 apply: Arc::new(apply_edit),
629 args_schema: vec![ArgSpec {
630 name: "path".into(),
631 kind: ArgKind::String,
632 doc: "File path to open. Absent = reload current file.".into(),
633 prompt: "path:".into(),
634 default: ArgDefault::None,
635 completion: Some("gen:files".into()),
636 picker: None,
637 }],
638 surface_form: SurfaceForm::Keyword,
639 },
640 );
641 let substitute = registry.register_ex_command(
642 "ex:substitute",
643 "Replace pattern with replacement on the current line or `%` whole buffer (`:s/pat/rep/[g]`).",
644 ExCommandSpec {
645 latency_class: LatencyClass::Reflex,
646 accepts_bang: false,
647 accepts_range: true,
651 parse_args: Arc::new(parse_substitute_args_unreachable),
657 apply: Arc::new(apply_substitute),
658 args_schema: vec![
659 ArgSpec::required(
660 "pattern",
661 ArgKind::Pattern,
662 "Search pattern (literal in v1; regex post-1.0)",
663 ),
664 ArgSpec::required(
665 "replacement",
666 ArgKind::String,
667 "Replacement text (empty deletes matches)",
668 ),
669 ArgSpec {
670 name: "flags".into(),
671 kind: ArgKind::String,
672 doc: "Flags string (currently `g` honoured; others ignored)".into(),
673 prompt: "".into(),
674 default: ArgDefault::Literal(ArgValue::String(String::new())),
675 completion: None,
676 picker: None,
677 },
678 ],
679 surface_form: SurfaceForm::Delimiter {
680 hint: ":s/pattern/replacement/[flags] (or :%s/.../.../ for whole buffer)"
681 .into(),
682 },
683 },
684 );
685 let global = registry.register_ex_command(
686 "ex:global",
687 "Run a command on every line matching (`:g`) or NOT matching (`:v`) a pattern.",
688 ExCommandSpec {
689 latency_class: LatencyClass::Reflex,
690 accepts_bang: false,
691 accepts_range: false,
692 parse_args: Arc::new(parse_global_args_unreachable),
693 apply: Arc::new(apply_global),
694 args_schema: vec![
695 ArgSpec::required("pattern", ArgKind::Pattern, "Match pattern (literal in v1)"),
696 ArgSpec {
697 name: "inverted".into(),
698 kind: ArgKind::Bool,
699 doc: "True for `:v` form -- match lines NOT matching the pattern.".into(),
700 prompt: "".into(),
701 default: ArgDefault::Literal(ArgValue::Bool(false)),
702 completion: None,
703 picker: None,
704 },
705 ArgSpec::required(
706 "body",
707 ArgKind::Raw,
708 "Ex-command to run on each matching line (re-parsed per match)",
709 ),
710 ],
711 surface_form: SurfaceForm::Delimiter {
712 hint: ":g/pattern/body (or :v/pattern/body for inverted)".into(),
713 },
714 },
715 );
716 let describe_command = registry.register_ex_command(
717 "ex:describe-command",
718 "Open the help view for a named command (DESIGN.md §5.11).",
719 ExCommandSpec {
720 latency_class: LatencyClass::Display,
721 accepts_bang: false,
722 accepts_range: false,
723 parse_args: Arc::new(parse_required_string),
724 apply: Arc::new(apply_describe_command),
725 args_schema: vec![ArgSpec {
726 name: "name".into(),
727 kind: ArgKind::String,
728 doc: "Registered command name (`ex:write`, `motion:word-forward`, ...)".into(),
729 prompt: "command:".into(),
730 default: ArgDefault::Required,
731 completion: Some("gen:commands".into()),
732 picker: None,
733 }],
734 surface_form: SurfaceForm::Keyword,
735 },
736 );
737 let describe_buffer = registry.register_ex_command(
738 "ex:describe-buffer",
739 "Open the help view for the current buffer's state (DESIGN.md §5.11).",
740 ExCommandSpec {
741 latency_class: LatencyClass::Display,
742 accepts_bang: false,
743 accepts_range: false,
744 parse_args: Arc::new(parse_no_args),
745 apply: Arc::new(|_| Ok(Effect::DescribeBuffer)),
746 args_schema: vec![],
747 surface_form: SurfaceForm::Keyword,
748 },
749 );
750 let apropos = registry.register_ex_command(
751 "ex:apropos",
752 "Search every registered command's name + doc for a substring (DESIGN.md §5.11).",
753 ExCommandSpec {
754 latency_class: LatencyClass::Display,
755 accepts_bang: false,
756 accepts_range: false,
757 parse_args: Arc::new(parse_required_string),
758 apply: Arc::new(apply_apropos),
759 args_schema: vec![ArgSpec {
760 name: "pattern".into(),
761 kind: ArgKind::String,
762 doc: "Case-insensitive substring matched against name and doc".into(),
763 prompt: "apropos:".into(),
764 default: ArgDefault::Required,
765 completion: None,
766 picker: None,
767 }],
768 surface_form: SurfaceForm::Keyword,
769 },
770 );
771 let describe_key = registry.register_ex_command(
772 "ex:describe-key",
773 "Open the help view for a key chord (DESIGN.md §5.11).",
774 ExCommandSpec {
775 latency_class: LatencyClass::Display,
776 accepts_bang: false,
777 accepts_range: false,
778 parse_args: Arc::new(parse_required_string),
779 apply: Arc::new(apply_describe_key),
780 args_schema: vec![ArgSpec {
781 name: "chord".into(),
782 kind: ArgKind::Chord,
783 doc: "Chord notation (`j`, `dw`, `<C-d>`, `gg`, `<Esc>`, ...)".into(),
784 prompt: "key:".into(),
785 default: ArgDefault::Required,
786 completion: None,
787 picker: None,
788 }],
789 surface_form: SurfaceForm::Keyword,
790 },
791 );
792 let list_keymap = registry.register_ex_command(
793 "ex:keymap",
794 "Open the help view listing every default keymap binding by mode (DESIGN.md §5.11).",
795 ExCommandSpec {
796 latency_class: LatencyClass::Display,
797 accepts_bang: false,
798 accepts_range: false,
799 parse_args: Arc::new(parse_no_args),
800 apply: Arc::new(|_| Ok(Effect::ListKeymap)),
801 args_schema: vec![],
802 surface_form: SurfaceForm::Keyword,
803 },
804 );
805 let buffer_next = registry.register_ex_command(
806 "ex:bnext",
807 "Cycle to the next open document buffer (`:bn[ext]`).",
808 ExCommandSpec {
809 latency_class: LatencyClass::Reflex,
810 accepts_bang: false,
811 accepts_range: false,
812 parse_args: Arc::new(parse_no_args),
813 apply: Arc::new(|_| Ok(Effect::BufferNext)),
814 args_schema: vec![],
815 surface_form: SurfaceForm::Keyword,
816 },
817 );
818 let _tab_next = registry.register_ex_command(
823 "ex:tabnext",
824 "Switch to the next tab (`:tabn[ext]`).",
825 ExCommandSpec {
826 latency_class: LatencyClass::Reflex,
827 accepts_bang: false,
828 accepts_range: false,
829 parse_args: Arc::new(parse_no_args),
830 apply: Arc::new(|_| Ok(Effect::AppAction(AppEffect::NextTab))),
831 args_schema: vec![],
832 surface_form: SurfaceForm::Keyword,
833 },
834 );
835 let _tab_prev = registry.register_ex_command(
836 "ex:tabprev",
837 "Switch to the previous tab (`:tabp[rev]`).",
838 ExCommandSpec {
839 latency_class: LatencyClass::Reflex,
840 accepts_bang: false,
841 accepts_range: false,
842 parse_args: Arc::new(parse_no_args),
843 apply: Arc::new(|_| Ok(Effect::AppAction(AppEffect::PrevTab))),
844 args_schema: vec![],
845 surface_form: SurfaceForm::Keyword,
846 },
847 );
848 let _tab_new = registry.register_ex_command(
849 "ex:tabnew",
850 "Open a new tab (optionally with `<path>`).",
851 ExCommandSpec {
852 latency_class: LatencyClass::Reflex,
853 accepts_bang: false,
854 accepts_range: false,
855 parse_args: Arc::new(parse_optional_path),
856 apply: Arc::new(|ctx| match &ctx.args {
857 Args::String(path) if !path.is_empty() => {
858 Ok(Effect::AppAction(AppEffect::NewTabAt(path.clone())))
859 }
860 _ => Ok(Effect::AppAction(AppEffect::NewTab)),
861 }),
862 args_schema: vec![ArgSpec {
863 name: "path".into(),
864 kind: ArgKind::String,
865 doc: "Optional file path to open in the new tab".into(),
866 prompt: "".into(),
867 default: ArgDefault::None,
868 completion: Some("gen:files".into()),
869 picker: None,
870 }],
871 surface_form: SurfaceForm::Keyword,
872 },
873 );
874 let _tab_close = registry.register_ex_command(
875 "ex:tabclose",
876 "Close the active tab.",
877 ExCommandSpec {
878 latency_class: LatencyClass::Reflex,
879 accepts_bang: false,
880 accepts_range: false,
881 parse_args: Arc::new(parse_no_args),
882 apply: Arc::new(|_| Ok(Effect::AppAction(AppEffect::CloseTab))),
883 args_schema: vec![],
884 surface_form: SurfaceForm::Keyword,
885 },
886 );
887 let _terminal = registry.register_ex_command(
890 "ex:terminal",
891 "Open a PTY-backed shell buffer (optionally running `<cmd>`).",
892 ExCommandSpec {
893 latency_class: LatencyClass::Reflex,
894 accepts_bang: false,
895 accepts_range: false,
896 parse_args: Arc::new(parse_optional_path),
897 apply: Arc::new(|ctx| match &ctx.args {
898 Args::String(s) if !s.is_empty() => {
899 Ok(Effect::AppAction(AppEffect::TerminalSpawn(Some(s.clone()))))
900 }
901 _ => Ok(Effect::AppAction(AppEffect::TerminalSpawn(None))),
902 }),
903 args_schema: vec![ArgSpec {
904 name: "cmd".into(),
905 kind: ArgKind::String,
906 doc: "Optional command line (default: $SHELL or /bin/sh)".into(),
907 prompt: "".into(),
908 default: ArgDefault::None,
909 completion: None,
910 picker: None,
911 }],
912 surface_form: SurfaceForm::Keyword,
913 },
914 );
915 let _tab_terminal = registry.register_ex_command(
918 "ex:tabterminal",
919 "Open a new tab containing a PTY-backed shell buffer.",
920 ExCommandSpec {
921 latency_class: LatencyClass::Reflex,
922 accepts_bang: false,
923 accepts_range: false,
924 parse_args: Arc::new(parse_optional_path),
925 apply: Arc::new(|ctx| match &ctx.args {
926 Args::String(s) if !s.is_empty() => Ok(Effect::AppAction(
927 AppEffect::TerminalSpawnInNewTab(Some(s.clone())),
928 )),
929 _ => Ok(Effect::AppAction(AppEffect::TerminalSpawnInNewTab(None))),
930 }),
931 args_schema: vec![ArgSpec {
932 name: "cmd".into(),
933 kind: ArgKind::String,
934 doc: "Optional command line (default: $SHELL or /bin/sh)".into(),
935 prompt: "".into(),
936 default: ArgDefault::None,
937 completion: None,
938 picker: None,
939 }],
940 surface_form: SurfaceForm::Keyword,
941 },
942 );
943 let _tab_only = registry.register_ex_command(
944 "ex:tabonly",
945 "Close every tab except the active one.",
946 ExCommandSpec {
947 latency_class: LatencyClass::Reflex,
948 accepts_bang: false,
949 accepts_range: false,
950 parse_args: Arc::new(parse_no_args),
951 apply: Arc::new(|_| Ok(Effect::AppAction(AppEffect::OnlyTab))),
952 args_schema: vec![],
953 surface_form: SurfaceForm::Keyword,
954 },
955 );
956 let _tab_move = registry.register_ex_command(
961 "ex:tabmove",
962 "Move the active tab to position N (1-indexed; default last).",
963 ExCommandSpec {
964 latency_class: LatencyClass::Reflex,
965 accepts_bang: false,
966 accepts_range: false,
967 parse_args: Arc::new(parse_tabmove_arg),
968 apply: Arc::new(|ctx| {
969 let n: u32 = match &ctx.args {
970 Args::String(s) => s.parse::<u32>().unwrap_or(0),
971 _ => 0,
972 };
973 Ok(Effect::AppAction(AppEffect::MoveTab(n)))
974 }),
975 args_schema: vec![ArgSpec::required(
976 "n",
977 ArgKind::Int,
978 "Target position (1-indexed; 0 = last)",
979 )],
980 surface_form: SurfaceForm::Keyword,
981 },
982 );
983 let buffer_prev = registry.register_ex_command(
984 "ex:bprev",
985 "Cycle to the previous open document buffer (`:bp[rev]`).",
986 ExCommandSpec {
987 latency_class: LatencyClass::Reflex,
988 accepts_bang: false,
989 accepts_range: false,
990 parse_args: Arc::new(parse_no_args),
991 apply: Arc::new(|_| Ok(Effect::BufferPrev)),
992 args_schema: vec![],
993 surface_form: SurfaceForm::Keyword,
994 },
995 );
996 let list_buffers = registry.register_ex_command(
997 "ex:buffers",
998 "List every open document buffer as a static text view (`:ls`). \
999 For the fuzzy switcher, see `:buffers` / `:b`.",
1000 ExCommandSpec {
1001 latency_class: LatencyClass::Display,
1002 accepts_bang: false,
1003 accepts_range: false,
1004 parse_args: Arc::new(parse_no_args),
1005 apply: Arc::new(|_| Ok(Effect::ListBuffers)),
1006 args_schema: vec![],
1007 surface_form: SurfaceForm::Keyword,
1008 },
1009 );
1010 let _buffer_picker = registry.register_ex_command(
1011 "ex:buffer-picker",
1012 "Open the vertico-style buffer switcher (`:buffers` / `:b`). \
1013 Type to filter; `<CR>` to switch. For the static text listing, see `:ls`.",
1014 ExCommandSpec {
1015 latency_class: LatencyClass::Display,
1016 accepts_bang: false,
1017 accepts_range: false,
1018 parse_args: Arc::new(parse_no_args),
1019 apply: Arc::new(|_| Ok(Effect::OpenBufferPicker)),
1020 args_schema: vec![],
1021 surface_form: SurfaceForm::Keyword,
1022 },
1023 );
1024 let buffer_delete = registry.register_ex_command(
1025 "ex:bdelete",
1026 "Close the active document buffer (`:bd[elete][!]`). `!` discards unsaved changes.",
1027 ExCommandSpec {
1028 latency_class: LatencyClass::Reflex,
1029 accepts_bang: true,
1030 accepts_range: false,
1031 parse_args: Arc::new(parse_no_args),
1032 apply: Arc::new(|ctx| Ok(Effect::BufferDelete { force: ctx.bang })),
1033 args_schema: vec![],
1034 surface_form: SurfaceForm::Keyword,
1035 },
1036 );
1037 let _files_picker = registry.register_ex_command(
1044 "ex:files",
1045 "Open the project file picker (`:files [root]`). Absent root = the active buffer's project. Alias for `:picker files [root]`.",
1046 ExCommandSpec {
1047 latency_class: LatencyClass::Display,
1048 accepts_bang: false,
1049 accepts_range: false,
1050 parse_args: Arc::new(parse_optional_path),
1051 apply: Arc::new(|ctx| {
1052 let args: Vec<String> = match &ctx.args {
1053 Args::String(p) if !p.is_empty() => vec![p.clone()],
1054 _ => Vec::new(),
1055 };
1056 Ok(Effect::OpenPicker {
1057 source: "files".into(),
1058 args,
1059 root: None,
1060 fill_action: None,
1061 query: None,
1062 })
1063 }),
1064 args_schema: vec![ArgSpec {
1065 name: "root".into(),
1066 kind: ArgKind::String,
1067 doc: "Directory to walk. Absent = the active buffer's project root.".into(),
1068 prompt: "root:".into(),
1069 default: ArgDefault::None,
1070 completion: Some("gen:files".into()),
1071 picker: None,
1072 }],
1073 surface_form: SurfaceForm::Keyword,
1074 },
1075 );
1076 let _recent_files_picker = registry.register_ex_command(
1077 "ex:recent",
1078 "Open the recent-files picker (`:recent`). Alias for `:picker recent`.",
1079 ExCommandSpec {
1080 latency_class: LatencyClass::Display,
1081 accepts_bang: false,
1082 accepts_range: false,
1083 parse_args: Arc::new(parse_no_args),
1084 apply: Arc::new(|_| {
1085 Ok(Effect::OpenPicker {
1086 source: "recent".into(),
1087 args: Vec::new(),
1088 root: None,
1089 fill_action: None,
1090 query: None,
1091 })
1092 }),
1093 args_schema: vec![],
1094 surface_form: SurfaceForm::Keyword,
1095 },
1096 );
1097 let _history_picker = registry.register_ex_command(
1101 "ex:history",
1102 "Open a history picker (`:history [commands|searches|pane-buffers]`). \
1103 `commands`: command-line history picker (default, also `q:`). \
1104 `searches`: search-line history picker (`q/` / `q?`). \
1105 `pane-buffers`: this pane's buffer history (`<C-6>` / `<C-7>`). \
1106 `<CR>` loads the chosen entry into the `:` / `/` line (does not execute).",
1107 ExCommandSpec {
1108 latency_class: LatencyClass::Display,
1109 accepts_bang: false,
1110 accepts_range: false,
1111 parse_args: Arc::new(parse_history_args),
1112 apply: Arc::new(|ctx| {
1113 let source = if let Args::List(ref values) = ctx.args
1114 && !values.is_empty()
1115 {
1116 match values[0].as_str() {
1117 Some("searches") => "search-history",
1118 Some("pane-buffers") => "pane-buffer-history",
1120 _ => "history",
1121 }
1122 } else {
1123 "history"
1124 };
1125 Ok(Effect::OpenPicker {
1126 source: source.into(),
1127 args: Vec::new(),
1128 root: None,
1129 fill_action: None,
1130 query: None,
1131 })
1132 }),
1133 args_schema: vec![ArgSpec {
1134 name: "kind".into(),
1135 kind: ArgKind::String,
1136 prompt: "history kind (`commands`, `searches`, or `pane-buffers`)".into(),
1137 default: ArgDefault::None,
1138 doc: "`commands` (default), `searches`, or `pane-buffers`.".into(),
1139 completion: Some("gen:history-kinds".into()),
1140 picker: None,
1141 }],
1142 surface_form: SurfaceForm::Keyword,
1143 },
1144 );
1145 let _picker = registry.register_ex_command(
1146 "ex:picker",
1147 "Open a picker over the named source (`:picker <source> [args...]`). \
1148 Source ids come from the host's `PickerRegistry` -- type `<Tab>` after \
1149 `:picker ` to list them. Short aliases like `:files`, `:recent`, `:b` \
1150 dispatch through the same machinery.",
1151 ExCommandSpec {
1152 latency_class: LatencyClass::Display,
1153 accepts_bang: false,
1154 accepts_range: false,
1155 parse_args: Arc::new(parse_picker_args),
1156 apply: Arc::new(|ctx| {
1157 let list = ctx
1160 .args
1161 .as_list()
1162 .ok_or_else(|| CommandError::BadArgs("picker: expected list args".into()))?;
1163 let source = list
1164 .first()
1165 .and_then(|v| v.as_str())
1166 .ok_or_else(|| CommandError::BadArgs("picker: source missing".into()))?
1167 .to_string();
1168 let args: Vec<String> = list[1..]
1169 .iter()
1170 .filter_map(|v| v.as_str().map(String::from))
1171 .collect();
1172 Ok(Effect::OpenPicker {
1176 source,
1177 args,
1178 root: None,
1179 fill_action: None,
1180 query: None,
1181 })
1182 }),
1183 args_schema: vec![ArgSpec {
1184 name: "source".into(),
1185 kind: ArgKind::String,
1186 doc: "Picker source id (`files`, `recent`, `buffers`, ...).".into(),
1187 prompt: "source:".into(),
1188 default: ArgDefault::Required,
1189 completion: Some("gen:picker-sources".into()),
1195 picker: None,
1196 }],
1197 surface_form: SurfaceForm::Keyword,
1198 },
1199 );
1200 let file_tree = registry.register_ex_command(
1201 "ex:filetree",
1202 "Open a file-tree buffer (`:Filetree [path]`). Absent = current dir.",
1203 ExCommandSpec {
1204 latency_class: LatencyClass::Display,
1205 accepts_bang: false,
1206 accepts_range: false,
1207 parse_args: Arc::new(parse_optional_path),
1208 apply: Arc::new(|ctx| {
1209 let root = match &ctx.args {
1210 Args::String(p) if !p.is_empty() => Some(std::path::PathBuf::from(p.as_str())),
1211 _ => None,
1212 };
1213 Ok(Effect::OpenFileTree { root })
1214 }),
1215 args_schema: vec![ArgSpec {
1216 name: "root".into(),
1217 kind: ArgKind::String,
1218 doc: "Directory to open as the tree root. Absent = current dir.".into(),
1219 prompt: "root:".into(),
1220 default: ArgDefault::None,
1221 completion: Some("gen:files".into()),
1222 picker: None,
1223 }],
1224 surface_form: SurfaceForm::Keyword,
1225 },
1226 );
1227 let file_tree_close = registry.register_ex_command(
1228 "ex:filetree-close",
1229 "Dismiss the file-tree buffer (`:FiletreeClose`).",
1230 ExCommandSpec {
1231 latency_class: LatencyClass::Reflex,
1232 accepts_bang: false,
1233 accepts_range: false,
1234 parse_args: Arc::new(parse_no_args),
1235 apply: Arc::new(|_| Ok(Effect::CloseFileTree)),
1236 args_schema: vec![],
1237 surface_form: SurfaceForm::Keyword,
1238 },
1239 );
1240 let oil = registry.register_ex_command(
1241 "ex:oil",
1242 "Open an oil buffer (`:Oil [path]`). Absent = current dir.",
1243 ExCommandSpec {
1244 latency_class: LatencyClass::Display,
1245 accepts_bang: false,
1246 accepts_range: false,
1247 parse_args: Arc::new(parse_optional_path),
1248 apply: Arc::new(|ctx| {
1249 let dir = match &ctx.args {
1250 Args::String(p) if !p.is_empty() => Some(std::path::PathBuf::from(p.as_str())),
1251 _ => None,
1252 };
1253 Ok(Effect::OpenOil { dir })
1254 }),
1255 args_schema: vec![ArgSpec {
1256 name: "dir".into(),
1257 kind: ArgKind::String,
1258 doc: "Directory to open. Absent = current document's parent.".into(),
1259 prompt: "dir:".into(),
1260 default: ArgDefault::None,
1261 completion: Some("gen:files".into()),
1262 picker: None,
1263 }],
1264 surface_form: SurfaceForm::Keyword,
1265 },
1266 );
1267 let describe_option = registry.register_ex_command(
1268 "ex:describe-option",
1269 "Open the help view for a typed option (`:describe-option NAME`).",
1270 ExCommandSpec {
1271 latency_class: LatencyClass::Display,
1272 accepts_bang: false,
1273 accepts_range: false,
1274 parse_args: Arc::new(parse_required_string),
1275 apply: Arc::new(|ctx| match &ctx.args {
1276 Args::String(name) => Ok(Effect::DescribeOption {
1277 name: name.to_string(),
1278 }),
1279 _ => Err(CommandError::BadArgs("expected option name".into())),
1280 }),
1281 args_schema: vec![ArgSpec {
1282 name: "name".into(),
1283 kind: ArgKind::String,
1284 doc: "Registered option name (or alias).".into(),
1285 prompt: "option:".into(),
1286 default: ArgDefault::Required,
1287 completion: Some("gen:options".into()),
1288 picker: None,
1289 }],
1290 surface_form: SurfaceForm::Keyword,
1291 },
1292 );
1293 let describe_element = registry.register_ex_command(
1294 "ex:describe-element",
1295 "Open the help view for a theme element / face \
1296 (`:describe-element NAME`, alias `:describe-face NAME`). \
1297 Shows owner, doc, the authoring (reference-form) style spec \
1298 (palette keys + inherit parent), and the concrete resolved \
1299 style under the active theme.",
1300 ExCommandSpec {
1301 latency_class: LatencyClass::Display,
1302 accepts_bang: false,
1303 accepts_range: false,
1304 parse_args: Arc::new(parse_required_string),
1305 apply: Arc::new(|ctx| match &ctx.args {
1306 Args::String(name) => Ok(Effect::DescribeElement {
1307 name: name.to_string(),
1308 }),
1309 _ => Err(CommandError::BadArgs("expected element name".into())),
1310 }),
1311 args_schema: vec![ArgSpec {
1312 name: "name".into(),
1313 kind: ArgKind::String,
1314 doc: "Registered theme-element name (e.g. `syntax.keyword`, `diff.add.sign`)."
1318 .into(),
1319 prompt: "element:".into(),
1320 default: ArgDefault::Required,
1321 completion: Some("gen:elements".into()),
1322 picker: None,
1323 }],
1324 surface_form: SurfaceForm::Keyword,
1325 },
1326 );
1327 let list_options = registry.register_ex_command(
1328 "ex:options",
1329 "List every registered option (`:options`).",
1330 ExCommandSpec {
1331 latency_class: LatencyClass::Display,
1332 accepts_bang: false,
1333 accepts_range: false,
1334 parse_args: Arc::new(parse_no_args),
1335 apply: Arc::new(|_| Ok(Effect::ListOptions)),
1336 args_schema: vec![],
1337 surface_form: SurfaceForm::Keyword,
1338 },
1339 );
1340 let describe_plugin_api = registry.register_ex_command(
1341 "ex:describe-plugin-api",
1342 "Open the help view for the plugin API (`:describe-plugin-api [<seam>]`). \
1343 With a seam name (`host-services`, `picker-source`, ...) render that \
1344 interface's functions, direction, and capability; without, list every \
1345 seam. The catalog is derived from `wit/` at build time.",
1346 ExCommandSpec {
1347 latency_class: LatencyClass::Display,
1348 accepts_bang: false,
1349 accepts_range: false,
1350 parse_args: Arc::new(parse_optional_string),
1351 apply: Arc::new(apply_describe_plugin_api),
1352 args_schema: vec![ArgSpec {
1353 name: "seam".into(),
1354 kind: ArgKind::String,
1355 doc: "Plugin-API interface name (`host-services`, `picker-source`, \
1356 `grammar`, ...). Omit to list every seam."
1357 .into(),
1358 prompt: "seam:".into(),
1359 default: ArgDefault::None,
1360 completion: Some("gen:plugin-api-seams".into()),
1365 picker: None,
1366 }],
1367 surface_form: SurfaceForm::Keyword,
1368 },
1369 );
1370 let list_plugin_apis = registry.register_ex_command(
1371 "ex:list-plugin-apis",
1372 "List every plugin-API interface the `wit/` package exposes \
1373 (`:list-plugin-apis`). One row per seam with direction + capability + \
1374 function count.",
1375 ExCommandSpec {
1376 latency_class: LatencyClass::Display,
1377 accepts_bang: false,
1378 accepts_range: false,
1379 parse_args: Arc::new(parse_no_args),
1380 apply: Arc::new(|_| Ok(Effect::ListPluginApis)),
1381 args_schema: vec![],
1382 surface_form: SurfaceForm::Keyword,
1383 },
1384 );
1385 let export_plugin_api = registry.register_ex_command(
1386 "ex:export-plugin-api",
1387 "Export the whole plugin-API catalog to a savable buffer \
1388 (`:export-plugin-api [markdown|json]`). Opens `*plugin-api.md*` (or \
1389 `*plugin-api.json*`) under text-mode; save it with `:w <path>`. \
1390 Defaults to markdown.",
1391 ExCommandSpec {
1392 latency_class: LatencyClass::Display,
1393 accepts_bang: false,
1394 accepts_range: false,
1395 parse_args: Arc::new(parse_optional_string),
1396 apply: Arc::new(apply_export_plugin_api),
1397 args_schema: vec![ArgSpec {
1398 name: "format".into(),
1399 kind: ArgKind::String,
1400 doc: "`markdown` (default) or `json`.".into(),
1401 prompt: "format:".into(),
1402 default: ArgDefault::None,
1403 completion: Some("gen:plugin-api-formats".into()),
1404 picker: None,
1405 }],
1406 surface_form: SurfaceForm::Keyword,
1407 },
1408 );
1409 let list_commands = registry.register_ex_command(
1410 "ex:list-commands",
1411 "List every registered command grouped by source (`:list-commands`): \
1412 built-in, user config, plugin, ... Each row links to its \
1413 `:describe-command` view.",
1414 ExCommandSpec {
1415 latency_class: LatencyClass::Display,
1416 accepts_bang: false,
1417 accepts_range: false,
1418 parse_args: Arc::new(parse_no_args),
1419 apply: Arc::new(|_| Ok(Effect::ListCommands)),
1420 args_schema: vec![],
1421 surface_form: SurfaceForm::Keyword,
1422 },
1423 );
1424 let describe_plugin = registry.register_ex_command(
1425 "ex:describe-plugin",
1426 "Open the help view for a loaded plugin (`:describe-plugin <name>`): its \
1427 own documentation + contributions. (Loaded-plugin enumeration is \
1428 Phase-8-gated; today no plugins are loaded.)",
1429 ExCommandSpec {
1430 latency_class: LatencyClass::Display,
1431 accepts_bang: false,
1432 accepts_range: false,
1433 parse_args: Arc::new(parse_required_string),
1434 apply: Arc::new(|ctx| match &ctx.args {
1435 Args::String(name) => Ok(Effect::DescribePlugin { name: name.clone() }),
1436 _ => Err(CommandError::BadArgs("expected a plugin name".into())),
1437 }),
1438 args_schema: vec![ArgSpec {
1439 name: "name".into(),
1440 kind: ArgKind::String,
1441 doc: "Loaded plugin name (e.g. `git-gutter`).".into(),
1442 prompt: "plugin:".into(),
1443 default: ArgDefault::Required,
1444 completion: Some("gen:plugins".into()),
1445 picker: None,
1446 }],
1447 surface_form: SurfaceForm::Keyword,
1448 },
1449 );
1450 let list_plugins = registry.register_ex_command(
1451 "ex:list-plugins",
1452 "List every loaded plugin (`:list-plugins`): name + doc summary. Empty \
1453 until the Phase-8 plugin loader is wired in.",
1454 ExCommandSpec {
1455 latency_class: LatencyClass::Display,
1456 accepts_bang: false,
1457 accepts_range: false,
1458 parse_args: Arc::new(parse_no_args),
1459 apply: Arc::new(|_| Ok(Effect::ListPlugins)),
1460 args_schema: vec![],
1461 surface_form: SurfaceForm::Keyword,
1462 },
1463 );
1464 let describe_events = registry.register_ex_command(
1465 "ex:describe-events",
1466 "List every registered event (`:describe-events`). Walks the \
1467 distributed-slice event registry and renders one row per event \
1468 with name + source crate + doc.",
1469 ExCommandSpec {
1470 latency_class: LatencyClass::Display,
1471 accepts_bang: false,
1472 accepts_range: false,
1473 parse_args: Arc::new(parse_no_args),
1474 apply: Arc::new(|_| Ok(Effect::DescribeEvents)),
1475 args_schema: vec![],
1476 surface_form: SurfaceForm::Keyword,
1477 },
1478 );
1479 let describe_event = registry.register_ex_command(
1480 "ex:describe-event",
1481 "Open the help view for a registered event (`:describe-event NAME`).",
1482 ExCommandSpec {
1483 latency_class: LatencyClass::Display,
1484 accepts_bang: false,
1485 accepts_range: false,
1486 parse_args: Arc::new(parse_required_string),
1487 apply: Arc::new(|ctx| match &ctx.args {
1488 Args::String(name) => Ok(Effect::DescribeEvent {
1489 name: name.to_string(),
1490 }),
1491 _ => Err(CommandError::BadArgs("expected event name".into())),
1492 }),
1493 args_schema: vec![ArgSpec {
1494 name: "name".into(),
1495 kind: ArgKind::String,
1496 doc: "Registered event name (e.g. `lsp.buffer-attached`).".into(),
1497 prompt: "event:".into(),
1498 default: ArgDefault::Required,
1499 completion: Some("gen:events".into()),
1500 picker: None,
1501 }],
1502 surface_form: SurfaceForm::Keyword,
1503 },
1504 );
1505 let list_modes = registry.register_ex_command(
1506 "ex:list-modes",
1507 "List every registered mode (`:list-modes`). Groups by \
1508 kind (Major / Minor) and shows each mode's current \
1509 activation state on the active buffer.",
1510 ExCommandSpec {
1511 latency_class: LatencyClass::Display,
1512 accepts_bang: false,
1513 accepts_range: false,
1514 parse_args: Arc::new(parse_no_args),
1515 apply: Arc::new(|_| Ok(Effect::ListModes)),
1516 args_schema: vec![],
1517 surface_form: SurfaceForm::Keyword,
1518 },
1519 );
1520 let _describe_active_modes = registry.register_ex_command(
1528 "ex:describe-active-modes",
1529 "Show the mode stack live on the current buffer \
1530 (`:describe-active-modes`, `<C-h>m`): the major plus every \
1531 minor, each with the chords it contributes.",
1532 ExCommandSpec {
1533 latency_class: LatencyClass::Display,
1534 accepts_bang: false,
1535 accepts_range: false,
1536 parse_args: Arc::new(parse_no_args),
1537 apply: Arc::new(|_| Ok(Effect::DescribeActiveModes)),
1538 args_schema: vec![],
1539 surface_form: SurfaceForm::Keyword,
1540 },
1541 );
1542 let _describe_bindings = registry.register_ex_command(
1546 "ex:describe-bindings",
1547 "List the chords that can fire on the current buffer \
1548 (`:describe-bindings`, `<C-h>K`): builtin bindings live in \
1549 the current binding-mode plus every active mode's \
1550 contributions. `:keymap` lists the full default catalog.",
1551 ExCommandSpec {
1552 latency_class: LatencyClass::Display,
1553 accepts_bang: false,
1554 accepts_range: false,
1555 parse_args: Arc::new(parse_no_args),
1556 apply: Arc::new(|_| Ok(Effect::DescribeActiveBindings)),
1557 args_schema: vec![],
1558 surface_form: SurfaceForm::Keyword,
1559 },
1560 );
1561 let describe_mode = registry.register_ex_command(
1562 "ex:describe-mode",
1563 "Open the help view for a registered mode \
1564 (`:describe-mode NAME`).",
1565 ExCommandSpec {
1566 latency_class: LatencyClass::Display,
1567 accepts_bang: false,
1568 accepts_range: false,
1569 parse_args: Arc::new(parse_required_string),
1570 apply: Arc::new(|ctx| match &ctx.args {
1571 Args::String(name) => Ok(Effect::DescribeMode {
1572 name: name.to_string(),
1573 }),
1574 _ => Err(CommandError::BadArgs("expected mode name".into())),
1575 }),
1576 args_schema: vec![ArgSpec {
1577 name: "name".into(),
1578 kind: ArgKind::String,
1579 doc: "Registered mode name (e.g. `lsp-mode`, `text-mode`).".into(),
1580 prompt: "mode:".into(),
1581 default: ArgDefault::Required,
1582 completion: Some("gen:modes".into()),
1583 picker: None,
1584 }],
1585 surface_form: SurfaceForm::Keyword,
1586 },
1587 );
1588 let describe_option_resolution = registry.register_ex_command(
1589 "ex:describe-option-resolution",
1590 "Show which resolver layer provides the resolved value \
1591 for an option on the active buffer \
1592 (`:describe-option-resolution NAME`).",
1593 ExCommandSpec {
1594 latency_class: LatencyClass::Display,
1595 accepts_bang: false,
1596 accepts_range: false,
1597 parse_args: Arc::new(parse_required_string),
1598 apply: Arc::new(|ctx| match &ctx.args {
1599 Args::String(name) => Ok(Effect::DescribeOptionResolution {
1600 name: name.to_string(),
1601 }),
1602 _ => Err(CommandError::BadArgs("expected option name".into())),
1603 }),
1604 args_schema: vec![ArgSpec {
1605 name: "name".into(),
1606 kind: ArgKind::String,
1607 doc: "Registered option name (e.g. `number`, `tabstop`).".into(),
1608 prompt: "option:".into(),
1609 default: ArgDefault::Required,
1610 completion: Some("gen:options".into()),
1611 picker: None,
1612 }],
1613 surface_form: SurfaceForm::Keyword,
1614 },
1615 );
1616 let tutor = registry.register_ex_command(
1617 "ex:tutor",
1618 "Open the interactive Lattice tutor lesson \
1619 (`:tutor [N]`). With no arg, opens lesson 1. The \
1620 lesson is embedded in the binary; each invocation \
1621 copies a fresh practice file to a temp path so you \
1622 can edit / practice without losing the canonical \
1623 lesson source. Run `:tutor` again to start over.",
1624 ExCommandSpec {
1625 latency_class: LatencyClass::Display,
1626 accepts_bang: false,
1627 accepts_range: false,
1628 parse_args: Arc::new(parse_optional_path),
1629 apply: Arc::new(|ctx| match &ctx.args {
1630 Args::None => Ok(Effect::Tutor { lesson: None }),
1631 Args::String(s) => {
1632 let n: u32 = s.parse().map_err(|_| {
1633 CommandError::BadArgs(format!("expected lesson number, got `{s}`"))
1634 })?;
1635 Ok(Effect::Tutor { lesson: Some(n) })
1636 }
1637 _ => Err(CommandError::BadArgs(
1638 "expected at most one numeric argument".into(),
1639 )),
1640 }),
1641 args_schema: vec![ArgSpec {
1642 name: "lesson".into(),
1643 kind: ArgKind::String,
1644 doc: "Lesson number (default: 1).".into(),
1645 prompt: "lesson:".into(),
1646 default: ArgDefault::None,
1647 completion: None,
1648 picker: None,
1649 }],
1650 surface_form: SurfaceForm::Keyword,
1651 },
1652 );
1653 let tutor_next = registry.register_ex_command(
1654 "ex:tutor-next",
1655 "Advance to the next tutor exercise (or lesson). \
1656 Equivalent to pressing `<CR>` in tutor-mode.",
1657 ExCommandSpec {
1658 latency_class: LatencyClass::Display,
1659 accepts_bang: false,
1660 accepts_range: false,
1661 parse_args: Arc::new(parse_no_args),
1662 apply: Arc::new(|_| Ok(Effect::AppAction(AppEffect::TutorAdvance))),
1663 args_schema: vec![],
1664 surface_form: SurfaceForm::Keyword,
1665 },
1666 );
1667 let tutor_prev = registry.register_ex_command(
1668 "ex:tutor-prev",
1669 "Retreat to the previous tutor exercise.",
1670 ExCommandSpec {
1671 latency_class: LatencyClass::Display,
1672 accepts_bang: false,
1673 accepts_range: false,
1674 parse_args: Arc::new(parse_no_args),
1675 apply: Arc::new(|_| Ok(Effect::AppAction(AppEffect::TutorRetreat))),
1676 args_schema: vec![],
1677 surface_form: SurfaceForm::Keyword,
1678 },
1679 );
1680 let customize = registry.register_ex_command(
1681 "ex:customize",
1682 "Open the customize buffer (`:customize [name]`). With no \
1683 arg, opens the picker listing every registered group + \
1684 every mode with at least one customizable option. With \
1685 a `<name>-mode` arg, opens the focused view of that \
1686 mode's contributions. With a group name (no `-mode` \
1687 suffix), opens the cross-mode group view.",
1688 ExCommandSpec {
1689 latency_class: LatencyClass::Display,
1690 accepts_bang: false,
1691 accepts_range: false,
1692 parse_args: Arc::new(parse_optional_path),
1693 apply: Arc::new(|ctx| match &ctx.args {
1694 Args::None => Ok(Effect::Customize { name: None }),
1695 Args::String(name) => Ok(Effect::Customize {
1696 name: Some(name.to_string()),
1697 }),
1698 _ => Err(CommandError::BadArgs(
1699 "expected at most one argument".into(),
1700 )),
1701 }),
1702 args_schema: vec![ArgSpec {
1703 name: "name".into(),
1704 kind: ArgKind::String,
1705 doc: "Group name (e.g. `editor`, `lsp`) OR mode name \
1706 ending in `-mode` (e.g. `lsp-completion-mode`)."
1707 .into(),
1708 prompt: "customize:".into(),
1709 default: ArgDefault::None,
1710 completion: Some("gen:customize".into()),
1711 picker: None,
1712 }],
1713 surface_form: SurfaceForm::Keyword,
1714 },
1715 );
1716 let hover = registry.register_ex_command(
1717 "ex:hover",
1718 "Open a hover popup at the cursor (`:hover [markdown]`). v1 path: feed text manually; \
1719 Phase 4 LSP will source from `textDocument/hover`.",
1720 ExCommandSpec {
1721 latency_class: LatencyClass::Display,
1722 accepts_bang: false,
1723 accepts_range: false,
1724 parse_args: Arc::new(parse_optional_path),
1725 apply: Arc::new(|ctx| {
1726 let markdown = match &ctx.args {
1727 Args::String(s) if !s.is_empty() => s.to_string(),
1728 _ => "(empty hover)".to_string(),
1729 };
1730 Ok(Effect::OpenHover { markdown })
1731 }),
1732 args_schema: vec![ArgSpec {
1733 name: "markdown".into(),
1734 kind: ArgKind::String,
1735 doc: "Markdown body of the hover popup.".into(),
1736 prompt: "hover:".into(),
1737 default: ArgDefault::None,
1738 completion: None,
1739 picker: None,
1740 }],
1741 surface_form: SurfaceForm::Keyword,
1742 },
1743 );
1744 let hover_close = registry.register_ex_command(
1745 "ex:hover-close",
1746 "Dismiss the active hover popup (`:HoverClose`).",
1747 ExCommandSpec {
1748 latency_class: LatencyClass::Reflex,
1749 accepts_bang: false,
1750 accepts_range: false,
1751 parse_args: Arc::new(parse_no_args),
1752 apply: Arc::new(|_| Ok(Effect::DismissPopup)),
1753 args_schema: vec![],
1754 surface_form: SurfaceForm::Keyword,
1755 },
1756 );
1757 let messages = registry.register_ex_command(
1759 "ex:messages",
1760 "Open the `*messages*` buffer -- the emacs `*Messages*` analogue carrying every minibuffer echo / notification with timestamps (`:messages`).",
1761 ExCommandSpec {
1762 latency_class: LatencyClass::Display,
1763 accepts_bang: false,
1764 accepts_range: false,
1765 parse_args: Arc::new(parse_no_args),
1766 apply: Arc::new(|_ctx| Ok(Effect::OpenMessages)),
1767 args_schema: Vec::new(),
1768 surface_form: SurfaceForm::Keyword,
1769 },
1770 );
1771 let lsp_log = registry.register_ex_command(
1772 "ex:lsp-log",
1773 "Open the LSP subsystem log (`*lsp*`) or a per-server log (`*lsp:<server>*`) (`:lsp-log [server]`).",
1774 ExCommandSpec {
1775 latency_class: LatencyClass::Display,
1776 accepts_bang: false,
1777 accepts_range: false,
1778 parse_args: Arc::new(parse_optional_path),
1779 apply: Arc::new(|ctx| {
1780 let server_id = match &ctx.args {
1781 Args::String(s) if !s.is_empty() => Some(s.to_string()),
1782 _ => None,
1783 };
1784 Ok(Effect::OpenLspLog { server_id })
1785 }),
1786 args_schema: vec![ArgSpec {
1787 name: "server".into(),
1788 kind: ArgKind::String,
1789 doc: "Server id (e.g. `rust`, `python`). Absent = subsystem-wide log.".into(),
1790 prompt: "server:".into(),
1791 default: ArgDefault::None,
1792 completion: Some("gen:lsp-servers".into()),
1793 picker: None,
1794 }],
1795 surface_form: SurfaceForm::Keyword,
1796 },
1797 );
1798 let lsp_trace = registry.register_ex_command(
1799 "ex:lsp-trace",
1800 "Toggle JSON-RPC wire trace for a server (pure toggle; view records via `:lsp-trace-log <server>`) (`:lsp-trace <server>`).",
1801 ExCommandSpec {
1802 latency_class: LatencyClass::Display,
1803 accepts_bang: false,
1804 accepts_range: false,
1805 parse_args: Arc::new(parse_required_string),
1806 apply: Arc::new(|ctx| match &ctx.args {
1807 Args::String(s) if !s.is_empty() => Ok(Effect::ToggleLspTrace {
1808 server_id: s.to_string(),
1809 }),
1810 _ => Err(CommandError::BadArgs(
1811 ":lsp-trace requires a server id".into(),
1812 )),
1813 }),
1814 args_schema: vec![ArgSpec {
1815 name: "server".into(),
1816 kind: ArgKind::String,
1817 doc: "Server id to toggle trace on.".into(),
1818 prompt: "server:".into(),
1819 default: ArgDefault::None,
1820 completion: Some("gen:lsp-servers".into()),
1821 picker: None,
1822 }],
1823 surface_form: SurfaceForm::Keyword,
1824 },
1825 );
1826 let _lsp_trace_log = registry.register_ex_command(
1827 "ex:lsp-trace-log",
1828 "Open the JSON-RPC trace ring for an LSP server (`:lsp-trace-log [server]`). No arg = picker over every running instance; arg = pre-filter (single match short-circuits). Independent of `:lsp-trace`.",
1829 ExCommandSpec {
1830 latency_class: LatencyClass::Display,
1831 accepts_bang: false,
1832 accepts_range: false,
1833 parse_args: Arc::new(parse_optional_path),
1834 apply: Arc::new(|ctx| {
1835 let server_id = match &ctx.args {
1836 Args::String(s) if !s.is_empty() => Some(s.to_string()),
1837 _ => None,
1838 };
1839 Ok(Effect::OpenLspTraceLog { server_id })
1840 }),
1841 args_schema: vec![ArgSpec {
1842 name: "server".into(),
1843 kind: ArgKind::String,
1844 doc: "Server id (e.g. `rust`). Absent = picker over every running instance.".into(),
1845 prompt: "server:".into(),
1846 default: ArgDefault::None,
1847 completion: Some("gen:lsp-servers".into()),
1848 picker: None,
1849 }],
1850 surface_form: SurfaceForm::Keyword,
1851 },
1852 );
1853 let lsp_status = registry.register_ex_command(
1854 "ex:lsp-status",
1855 "Render every running LSP server (id, root, pid, uptime) in a help-style buffer (`:lsp-status`).",
1856 ExCommandSpec {
1857 latency_class: LatencyClass::Display,
1858 accepts_bang: false,
1859 accepts_range: false,
1860 parse_args: Arc::new(parse_no_args),
1861 apply: Arc::new(|_| Ok(Effect::LspStatus)),
1862 args_schema: vec![],
1863 surface_form: SurfaceForm::Keyword,
1864 },
1865 );
1866 let _lsp_references_view = registry.register_ex_command(
1872 "ex:lsp-references",
1873 "Open every reference to the symbol under the cursor as an editable multibuffer, \
1874 one excerpt per site. Edits propagate to the source files. `gr` keeps opening the \
1875 picker, which is the better surface for jumping to a single site.",
1876 ExCommandSpec {
1877 latency_class: LatencyClass::Display,
1878 accepts_bang: false,
1879 accepts_range: false,
1880 parse_args: Arc::new(parse_no_args),
1881 apply: Arc::new(|_| Ok(Effect::Lsp(LspRequest::ReferencesView))),
1882 args_schema: vec![],
1883 surface_form: SurfaceForm::Keyword,
1884 },
1885 );
1886
1887 let _lsp_references_to_error_list = registry.register_ex_command(
1891 "ex:lsp-references-to-error-list",
1892 "Find references to the symbol under the cursor and put them in the error list \
1893 (`:next-error` / `]qq` / `:problems`). The manual peer of \
1894 `lsp.references-to-error-list`, which is off by default.",
1895 ExCommandSpec {
1896 latency_class: LatencyClass::Display,
1897 accepts_bang: false,
1898 accepts_range: false,
1899 parse_args: Arc::new(parse_no_args),
1900 apply: Arc::new(|_| Ok(Effect::Lsp(LspRequest::ReferencesToErrorList))),
1901 args_schema: vec![],
1902 surface_form: SurfaceForm::Keyword,
1903 },
1904 );
1905
1906 let _lsp_diagnostics_to_error_list = registry.register_ex_command(
1910 "ex:lsp-diagnostics-to-error-list",
1911 "Pull the language server's currently published diagnostics into the error list \
1912 (`:problems` / `:next-error` / the picker). The manual peer of \
1913 `lsp.diagnostics-to-error-list`; surfaces what servers have published, which is \
1914 not a workspace scan.",
1915 ExCommandSpec {
1916 latency_class: LatencyClass::Display,
1917 accepts_bang: false,
1918 accepts_range: false,
1919 parse_args: Arc::new(parse_no_args),
1920 apply: Arc::new(|_| Ok(Effect::LspDiagnosticsToErrorList)),
1921 args_schema: vec![],
1922 surface_form: SurfaceForm::Keyword,
1923 },
1924 );
1925 let lsp_server_log = registry.register_ex_command(
1926 "ex:lsp-server-log",
1927 "Picker-style listing of every running LSP server actor with workspace root + buffer count + capability summary; each row links to its log + trace via `exec:` URLs (`:lsp-server-log`).",
1928 ExCommandSpec {
1929 latency_class: LatencyClass::Display,
1930 accepts_bang: false,
1931 accepts_range: false,
1932 parse_args: Arc::new(parse_no_args),
1933 apply: Arc::new(|_| Ok(Effect::LspServerLogListing)),
1934 args_schema: vec![],
1935 surface_form: SurfaceForm::Keyword,
1936 },
1937 );
1938 let lsp_restart = registry.register_ex_command(
1939 "ex:lsp-restart",
1940 "Force-restart a stuck LSP server. Wired but no-op until the supervisor restart path lands in 4.4 (`:lsp-restart <server>`).",
1941 ExCommandSpec {
1942 latency_class: LatencyClass::Display,
1943 accepts_bang: false,
1944 accepts_range: false,
1945 parse_args: Arc::new(parse_required_string),
1946 apply: Arc::new(|ctx| match &ctx.args {
1947 Args::String(s) if !s.is_empty() => Ok(Effect::LspRestart {
1948 server_id: s.to_string(),
1949 }),
1950 _ => Err(CommandError::BadArgs(
1951 ":lsp-restart requires a server id".into(),
1952 )),
1953 }),
1954 args_schema: vec![ArgSpec {
1955 name: "server".into(),
1956 kind: ArgKind::String,
1957 doc: "Server id to restart.".into(),
1958 prompt: "server:".into(),
1959 default: ArgDefault::None,
1960 completion: Some("gen:lsp-servers".into()),
1961 picker: None,
1962 }],
1963 surface_form: SurfaceForm::Keyword,
1964 },
1965 );
1966 let lsp_progress_cancel = registry.register_ex_command(
1967 "ex:lsp-progress-cancel",
1968 "Cancel cancellable LSP $/progress operations on the named server (or every server attached to the active buffer if omitted) (`:lsp-progress-cancel [server]`).",
1969 ExCommandSpec {
1970 latency_class: LatencyClass::Reflex,
1971 accepts_bang: false,
1972 accepts_range: false,
1973 parse_args: Arc::new(parse_optional_path),
1974 apply: Arc::new(|ctx| match &ctx.args {
1975 Args::String(s) if !s.trim().is_empty() => Ok(Effect::LspProgressCancel {
1976 server_id: Some(s.trim().to_string()),
1977 }),
1978 _ => Ok(Effect::LspProgressCancel { server_id: None }),
1979 }),
1980 args_schema: vec![ArgSpec {
1981 name: "server".into(),
1982 kind: ArgKind::String,
1983 doc: "Optional server id; omit to cancel on every attached server.".into(),
1984 prompt: "server:".into(),
1985 default: ArgDefault::None,
1986 completion: Some("gen:lsp-servers".into()),
1987 picker: None,
1988 }],
1989 surface_form: SurfaceForm::Keyword,
1990 },
1991 );
1992 let lsp_expand_region = registry.register_ex_command(
1993 "ex:lsp-expand-region",
1994 "Smart-expansion: walk one step outward in the LSP selectionRange chain (`:lsp-expand-region`).",
1995 ExCommandSpec {
1996 latency_class: LatencyClass::Reflex,
1997 accepts_bang: false,
1998 accepts_range: false,
1999 parse_args: Arc::new(parse_no_args),
2000 apply: Arc::new(|_| Ok(Effect::LspExpandRegion)),
2001 args_schema: vec![],
2002 surface_form: SurfaceForm::Keyword,
2003 },
2004 );
2005 let lsp_shrink_region = registry.register_ex_command(
2006 "ex:lsp-shrink-region",
2007 "Walk one step inward in the cached LSP selectionRange chain (`:lsp-shrink-region`).",
2008 ExCommandSpec {
2009 latency_class: LatencyClass::Reflex,
2010 accepts_bang: false,
2011 accepts_range: false,
2012 parse_args: Arc::new(parse_no_args),
2013 apply: Arc::new(|_| Ok(Effect::LspShrinkRegion)),
2014 args_schema: vec![],
2015 surface_form: SurfaceForm::Keyword,
2016 },
2017 );
2018 let lsp_log_level = registry.register_ex_command(
2019 "ex:lsp-log-level",
2020 "Set the subsystem-wide default min log level (or a per-server override) (`:lsp-log-level [server] <level>`).",
2021 ExCommandSpec {
2022 latency_class: LatencyClass::Reflex,
2023 accepts_bang: false,
2024 accepts_range: false,
2025 parse_args: Arc::new(parse_required_string),
2026 apply: Arc::new(|ctx| match &ctx.args {
2027 Args::String(s) if !s.is_empty() => {
2028 let trimmed = s.trim();
2029 let mut parts = trimmed.split_whitespace();
2030 let first = parts.next().unwrap_or("");
2031 let second = parts.next();
2032 let (server_id, level) = match second {
2033 Some(level) => (Some(first.to_string()), level.to_string()),
2034 None => (None, first.to_string()),
2035 };
2036 Ok(Effect::SetLspLogLevel { server_id, level })
2037 }
2038 _ => Err(CommandError::BadArgs(
2039 ":lsp-log-level requires `[server] <level>`".into(),
2040 )),
2041 }),
2042 args_schema: vec![ArgSpec {
2043 name: "spec".into(),
2044 kind: ArgKind::String,
2045 doc: "Either a level (`error`/`warn`/`info`/`debug`/`trace`) for the subsystem default, or `<server> <level>` for a per-server override.".into(),
2051 prompt: "[server] level:".into(),
2052 default: ArgDefault::None,
2053 completion: Some("gen:log-levels".into()),
2054 picker: None,
2055 }],
2056 surface_form: SurfaceForm::Keyword,
2057 },
2058 );
2059 let lsp_log_clear = registry.register_ex_command(
2060 "ex:lsp-log-clear",
2061 "Drop the records in `*lsp*` (no arg) or `*lsp:<server>*` (with arg) (`:lsp-log-clear [server]`).",
2062 ExCommandSpec {
2063 latency_class: LatencyClass::Reflex,
2064 accepts_bang: false,
2065 accepts_range: false,
2066 parse_args: Arc::new(parse_optional_path),
2067 apply: Arc::new(|ctx| {
2068 let server_id = match &ctx.args {
2069 Args::String(s) if !s.is_empty() => Some(s.to_string()),
2070 _ => None,
2071 };
2072 Ok(Effect::LspLogClear { server_id })
2073 }),
2074 args_schema: vec![ArgSpec {
2075 name: "server".into(),
2076 kind: ArgKind::String,
2077 doc: "Server id whose ring to clear. Absent = subsystem-wide.".into(),
2078 prompt: "server:".into(),
2079 default: ArgDefault::None,
2080 completion: Some("gen:lsp-servers".into()),
2081 picker: None,
2082 }],
2083 surface_form: SurfaceForm::Keyword,
2084 },
2085 );
2086
2087 let lsp_symbols = registry.register_ex_command(
2088 "ex:lsp-symbols",
2089 "Open a vertico picker over the active document's symbol outline (`:lsp-symbols`, Phase 4.2.e).",
2090 ExCommandSpec {
2091 latency_class: LatencyClass::Display,
2092 accepts_bang: false,
2093 accepts_range: false,
2094 parse_args: Arc::new(parse_no_args),
2095 apply: Arc::new(|_| Ok(Effect::LspDocumentSymbol)),
2096 args_schema: vec![],
2097 surface_form: SurfaceForm::Keyword,
2098 },
2099 );
2100 let format = registry.register_ex_command(
2106 "ex:format",
2107 "Format the active buffer: LSP if a server advertises formatting, otherwise `formatprg` \
2108 or the built-in per-language formatter table (`:format`).",
2109 ExCommandSpec {
2110 latency_class: LatencyClass::Display,
2111 accepts_bang: false,
2112 accepts_range: false,
2113 parse_args: Arc::new(parse_no_args),
2114 apply: Arc::new(|_| Ok(Effect::Format)),
2115 args_schema: vec![],
2116 surface_form: SurfaceForm::Keyword,
2117 },
2118 );
2119 let lsp_format = registry.register_ex_command(
2120 "ex:lsp-format",
2121 "Run `textDocument/formatting` on the active buffer's highest-priority LSP server; apply the returned edits as one undo unit (`:format`, Phase 4.3).",
2122 ExCommandSpec {
2123 latency_class: LatencyClass::Display,
2124 accepts_bang: false,
2125 accepts_range: false,
2126 parse_args: Arc::new(parse_no_args),
2127 apply: Arc::new(|_| Ok(Effect::LspFormat)),
2128 args_schema: vec![],
2129 surface_form: SurfaceForm::Keyword,
2130 },
2131 );
2132 let lsp_format_range = registry.register_ex_command(
2133 "ex:lsp-format-range",
2134 "Run `textDocument/rangeFormatting` over the active Visual selection or the supplied line range (`:[range]format-range`, Phase 4.3).",
2135 ExCommandSpec {
2136 latency_class: LatencyClass::Display,
2137 accepts_bang: false,
2138 accepts_range: true,
2139 parse_args: Arc::new(parse_no_args),
2140 apply: Arc::new(|_| Ok(Effect::LspFormatRange)),
2141 args_schema: vec![],
2142 surface_form: SurfaceForm::Keyword,
2143 },
2144 );
2145
2146 let lsp_code_action = registry.register_ex_command(
2147 "ex:lsp-code-action",
2148 "Open a vertico picker over LSP code actions at the cursor / selection (`:code-actions`, Phase 4.3).",
2149 ExCommandSpec {
2150 latency_class: LatencyClass::Display,
2151 accepts_bang: false,
2152 accepts_range: true,
2153 parse_args: Arc::new(parse_no_args),
2154 apply: Arc::new(|_| Ok(Effect::LspCodeAction)),
2155 args_schema: vec![],
2156 surface_form: SurfaceForm::Keyword,
2157 },
2158 );
2159
2160 let lsp_rename = registry.register_ex_command(
2161 "ex:lsp-rename",
2162 "Rename the symbol under cursor across the workspace via textDocument/rename. Empty name uses prepareRename's placeholder when advertised (`:rename [new-name]`, Phase 4.3).",
2163 ExCommandSpec {
2164 latency_class: LatencyClass::Display,
2165 accepts_bang: false,
2166 accepts_range: false,
2167 parse_args: Arc::new(parse_optional_path),
2168 apply: Arc::new(|ctx| {
2169 let new_name = match &ctx.args {
2170 Args::String(s) => s.trim().to_string(),
2171 _ => String::new(),
2172 };
2173 Ok(Effect::LspRename { new_name })
2174 }),
2175 args_schema: vec![ArgSpec {
2176 name: "new-name".into(),
2177 kind: ArgKind::String,
2178 doc: "Replacement identifier. Empty -> use the server's prepareRename placeholder.".into(),
2179 prompt: "new name:".into(),
2180 default: ArgDefault::None,
2181 completion: None,
2182 picker: None,
2183 }],
2184 surface_form: SurfaceForm::Keyword,
2185 },
2186 );
2187
2188 let lsp_complete = registry.register_ex_command(
2189 "ex:lsp-complete",
2190 "Open a vertico picker over LSP completion items at the cursor (`:complete`, Phase 4.2.g). Plain-text insert -- snippet expansion / lazy resolve land with buffer-level Insert-mode completion.",
2191 ExCommandSpec {
2192 latency_class: LatencyClass::Display,
2193 accepts_bang: false,
2194 accepts_range: false,
2195 parse_args: Arc::new(parse_no_args),
2196 apply: Arc::new(|_| Ok(Effect::LspComplete)),
2197 args_schema: vec![],
2198 surface_form: SurfaceForm::Keyword,
2199 },
2200 );
2201
2202 let lsp_signature_help = registry.register_ex_command(
2203 "ex:lsp-signature-help",
2204 "Open the LSP signature-help popup for the current cursor (`:signature-help`, Phase 4.3). Trigger-character driven in Insert mode -- typing `(` / `,` etc. fires the same request automatically.",
2205 ExCommandSpec {
2206 latency_class: LatencyClass::Display,
2207 accepts_bang: false,
2208 accepts_range: false,
2209 parse_args: Arc::new(parse_no_args),
2210 apply: Arc::new(|_| Ok(Effect::LspSignatureHelp)),
2211 args_schema: vec![],
2212 surface_form: SurfaceForm::Keyword,
2213 },
2214 );
2215
2216 let lsp_workspace_symbol = registry.register_ex_command(
2217 "ex:lsp-workspace-symbol",
2218 "Open a vertico picker over workspace symbols matching `query` (server-side substring filter; `:lsp-workspace-symbol [query]`, Phase 4.2.f).",
2219 ExCommandSpec {
2220 latency_class: LatencyClass::Display,
2221 accepts_bang: false,
2222 accepts_range: false,
2223 parse_args: Arc::new(parse_optional_path),
2224 apply: Arc::new(|ctx| {
2225 let query = match &ctx.args {
2226 Args::String(s) => s.to_string(),
2227 _ => String::new(),
2228 };
2229 Ok(Effect::LspWorkspaceSymbol { query })
2230 }),
2231 args_schema: vec![ArgSpec {
2232 name: "query".into(),
2233 kind: ArgKind::String,
2234 doc: "Server-side substring filter; empty returns the server's idea of \"every workspace symbol\".".into(),
2235 prompt: "query:".into(),
2236 default: ArgDefault::None,
2237 completion: None,
2238 picker: None,
2239 }],
2240 surface_form: SurfaceForm::Keyword,
2241 },
2242 );
2243
2244 let lsp_incoming_calls = registry.register_ex_command(
2245 "ex:lsp-incoming-calls",
2246 "Open a vertico picker over the callers of the function at the cursor (`textDocument/prepareCallHierarchy` -> `callHierarchy/incomingCalls`, Phase 4.5.a).",
2247 ExCommandSpec {
2248 latency_class: LatencyClass::Display,
2249 accepts_bang: false,
2250 accepts_range: false,
2251 parse_args: Arc::new(parse_no_args),
2252 apply: Arc::new(|_ctx| Ok(Effect::LspIncomingCalls)),
2253 args_schema: Vec::new(),
2254 surface_form: SurfaceForm::Keyword,
2255 },
2256 );
2257 let lsp_outgoing_calls = registry.register_ex_command(
2258 "ex:lsp-outgoing-calls",
2259 "Open a vertico picker over the callees of the function at the cursor (`textDocument/prepareCallHierarchy` -> `callHierarchy/outgoingCalls`, Phase 4.5.a).",
2260 ExCommandSpec {
2261 latency_class: LatencyClass::Display,
2262 accepts_bang: false,
2263 accepts_range: false,
2264 parse_args: Arc::new(parse_no_args),
2265 apply: Arc::new(|_ctx| Ok(Effect::LspOutgoingCalls)),
2266 args_schema: Vec::new(),
2267 surface_form: SurfaceForm::Keyword,
2268 },
2269 );
2270 let lsp_supertypes = registry.register_ex_command(
2271 "ex:lsp-supertypes",
2272 "Open a vertico picker over the types the type at the cursor subtypes (`textDocument/prepareTypeHierarchy` -> `typeHierarchy/supertypes`, Phase 4.5.b).",
2273 ExCommandSpec {
2274 latency_class: LatencyClass::Display,
2275 accepts_bang: false,
2276 accepts_range: false,
2277 parse_args: Arc::new(parse_no_args),
2278 apply: Arc::new(|_ctx| Ok(Effect::LspSupertypes)),
2279 args_schema: Vec::new(),
2280 surface_form: SurfaceForm::Keyword,
2281 },
2282 );
2283 let lsp_subtypes = registry.register_ex_command(
2284 "ex:lsp-subtypes",
2285 "Open a vertico picker over the subtypes of the type at the cursor (`textDocument/prepareTypeHierarchy` -> `typeHierarchy/subtypes`, Phase 4.5.b).",
2286 ExCommandSpec {
2287 latency_class: LatencyClass::Display,
2288 accepts_bang: false,
2289 accepts_range: false,
2290 parse_args: Arc::new(parse_no_args),
2291 apply: Arc::new(|_ctx| Ok(Effect::LspSubtypes)),
2292 args_schema: Vec::new(),
2293 surface_form: SurfaceForm::Keyword,
2294 },
2295 );
2296 let lsp_moniker = registry.register_ex_command(
2297 "ex:lsp-moniker",
2298 "Fire `textDocument/moniker` at the cursor and echo the result (cross-project symbol identifier, Phase 4.5.g).",
2299 ExCommandSpec {
2300 latency_class: LatencyClass::Display,
2301 accepts_bang: false,
2302 accepts_range: false,
2303 parse_args: Arc::new(parse_no_args),
2304 apply: Arc::new(|_ctx| Ok(Effect::LspMoniker)),
2305 args_schema: Vec::new(),
2306 surface_form: SurfaceForm::Keyword,
2307 },
2308 );
2309 let lsp_code_lens = registry.register_ex_command(
2310 "ex:lsp-code-lens",
2311 "Open a picker over the active buffer's cached code lenses; accept routes the chosen lens through `workspace/executeCommand` (Phase 4.5.d).",
2312 ExCommandSpec {
2313 latency_class: LatencyClass::Display,
2314 accepts_bang: false,
2315 accepts_range: false,
2316 parse_args: Arc::new(parse_no_args),
2317 apply: Arc::new(|_ctx| Ok(Effect::LspCodeLens)),
2318 args_schema: Vec::new(),
2319 surface_form: SurfaceForm::Keyword,
2320 },
2321 );
2322 let lsp_color_presentation = registry.register_ex_command(
2323 "ex:lsp-color-presentation",
2324 "At the cursor, look up the color literal in the documentColor cache and open a picker of alternative formats (`textDocument/colorPresentation`, Phase 4.5.e).",
2325 ExCommandSpec {
2326 latency_class: LatencyClass::Display,
2327 accepts_bang: false,
2328 accepts_range: false,
2329 parse_args: Arc::new(parse_no_args),
2330 apply: Arc::new(|_ctx| Ok(Effect::LspColorPresentation)),
2331 args_schema: Vec::new(),
2332 surface_form: SurfaceForm::Keyword,
2333 },
2334 );
2335
2336 let list_diagnostics = registry.register_ex_command(
2337 "ex:diagnostics",
2338 "Open a help-style buffer listing every workspace diagnostic with clickable per-entry source links (`:diagnostics`).",
2339 ExCommandSpec {
2340 latency_class: LatencyClass::Display,
2341 accepts_bang: false,
2342 accepts_range: false,
2343 parse_args: Arc::new(parse_no_args),
2344 apply: Arc::new(|_| Ok(Effect::ListDiagnostics)),
2345 args_schema: vec![],
2346 surface_form: SurfaceForm::Keyword,
2347 },
2348 );
2349 let _list_errors = registry.register_ex_command(
2350 "error-list",
2351 "Open the error list in a fuzzy picker (`:error`; vim `:clist`/`:cl`). The flat browse-and-jump surface — complements `:next-error` (step) and `:problems` (the *problems* multibuffer).",
2352 ExCommandSpec {
2353 latency_class: LatencyClass::Display,
2354 accepts_bang: false,
2355 accepts_range: false,
2356 parse_args: Arc::new(parse_no_args),
2357 apply: Arc::new(|_| Ok(Effect::ListErrors)),
2358 args_schema: vec![],
2359 surface_form: SurfaceForm::Keyword,
2360 },
2361 );
2362 let next_diagnostic = registry.register_ex_command(
2363 "ex:diag-next",
2364 "Move the cursor to the next diagnostic in the active buffer (wraps; `]d` / `:diag-next` / `:cnext`).",
2365 ExCommandSpec {
2366 latency_class: LatencyClass::Reflex,
2367 accepts_bang: false,
2368 accepts_range: false,
2369 parse_args: Arc::new(parse_no_args),
2370 apply: Arc::new(|_| Ok(Effect::NextDiagnostic)),
2371 args_schema: vec![],
2372 surface_form: SurfaceForm::Keyword,
2373 },
2374 );
2375 let prev_diagnostic = registry.register_ex_command(
2376 "ex:diag-prev",
2377 "Move the cursor to the previous diagnostic in the active buffer (wraps; `[d` / `:diag-prev` / `:cprev`).",
2378 ExCommandSpec {
2379 latency_class: LatencyClass::Reflex,
2380 accepts_bang: false,
2381 accepts_range: false,
2382 parse_args: Arc::new(parse_no_args),
2383 apply: Arc::new(|_| Ok(Effect::PrevDiagnostic)),
2384 args_schema: vec![],
2385 surface_form: SurfaceForm::Keyword,
2386 },
2387 );
2388 let _error_next = registry.register_ex_command(
2396 "next-error",
2397 "Jump to the next error / location in the error list (wraps; `:next-error`; vim `:cnext`/`:cn`/`]qq`). Echoes `no error list` when empty (diagnostics live on `]d`/`[d` / `:diag-*`).",
2398 ExCommandSpec {
2399 latency_class: LatencyClass::Reflex,
2400 accepts_bang: false,
2401 accepts_range: false,
2402 parse_args: Arc::new(parse_no_args),
2403 apply: Arc::new(|_| {
2404 Ok(Effect::AppAction(AppEffect::ErrorNav {
2405 target: crate::app_effect::ErrorTarget::Next,
2406 }))
2407 }),
2408 args_schema: vec![],
2409 surface_form: SurfaceForm::Keyword,
2410 },
2411 );
2412 let _error_prev = registry.register_ex_command(
2413 "previous-error",
2414 "Jump to the previous error / location in the error list (wraps; `:previous-error`; vim `:cprev`/`:cp`/`[qq`). Echoes `no error list` when empty (diagnostics live on `]d`/`[d` / `:diag-*`).",
2415 ExCommandSpec {
2416 latency_class: LatencyClass::Reflex,
2417 accepts_bang: false,
2418 accepts_range: false,
2419 parse_args: Arc::new(parse_no_args),
2420 apply: Arc::new(|_| {
2421 Ok(Effect::AppAction(AppEffect::ErrorNav {
2422 target: crate::app_effect::ErrorTarget::Prev,
2423 }))
2424 }),
2425 args_schema: vec![],
2426 surface_form: SurfaceForm::Keyword,
2427 },
2428 );
2429 let _error_cc = registry.register_ex_command(
2430 "error",
2431 "Jump to error / location N in the error list (1-based; `:error [N]`; vim `:cc [N]`). Bare `:error` re-visits the current entry.",
2432 ExCommandSpec {
2433 latency_class: LatencyClass::Reflex,
2434 accepts_bang: false,
2435 accepts_range: false,
2436 parse_args: Arc::new(parse_cc_arg),
2437 apply: Arc::new(|ctx| {
2438 let n: Option<usize> = match &ctx.args {
2439 Args::String(s) => s.parse::<usize>().ok(),
2440 _ => None,
2441 };
2442 Ok(Effect::AppAction(AppEffect::ErrorNav {
2443 target: crate::app_effect::ErrorTarget::Jump(n),
2444 }))
2445 }),
2446 args_schema: vec![ArgSpec::optional(
2447 "n",
2448 ArgKind::Int,
2449 "Target error entry (1-indexed; default current)",
2450 )],
2451 surface_form: SurfaceForm::Keyword,
2452 },
2453 );
2454 let _error_first = registry.register_ex_command(
2455 "first-error",
2456 "Jump to the first error / location in the error list (`:first-error`; vim `:cfirst`/`:cr`/`[Q`).",
2457 ExCommandSpec {
2458 latency_class: LatencyClass::Reflex,
2459 accepts_bang: false,
2460 accepts_range: false,
2461 parse_args: Arc::new(parse_no_args),
2462 apply: Arc::new(|_| {
2463 Ok(Effect::AppAction(AppEffect::ErrorNav {
2464 target: crate::app_effect::ErrorTarget::First,
2465 }))
2466 }),
2467 args_schema: vec![],
2468 surface_form: SurfaceForm::Keyword,
2469 },
2470 );
2471 let _error_last = registry.register_ex_command(
2472 "last-error",
2473 "Jump to the last error / location in the error list (`:last-error`; vim `:clast`/`]Q`).",
2474 ExCommandSpec {
2475 latency_class: LatencyClass::Reflex,
2476 accepts_bang: false,
2477 accepts_range: false,
2478 parse_args: Arc::new(parse_no_args),
2479 apply: Arc::new(|_| {
2480 Ok(Effect::AppAction(AppEffect::ErrorNav {
2481 target: crate::app_effect::ErrorTarget::Last,
2482 }))
2483 }),
2484 args_schema: vec![],
2485 surface_form: SurfaceForm::Keyword,
2486 },
2487 );
2488 let _error_next_file = registry.register_ex_command(
2492 "next-error-file",
2493 "Jump to the first error / location in the next file (wraps; `:next-error-file`; vim `:cnextfile`/`:cnf`/`]qf`).",
2494 ExCommandSpec {
2495 latency_class: LatencyClass::Reflex,
2496 accepts_bang: false,
2497 accepts_range: false,
2498 parse_args: Arc::new(parse_no_args),
2499 apply: Arc::new(|_| {
2500 Ok(Effect::AppAction(AppEffect::ErrorNav {
2501 target: crate::app_effect::ErrorTarget::NextFile,
2502 }))
2503 }),
2504 args_schema: vec![],
2505 surface_form: SurfaceForm::Keyword,
2506 },
2507 );
2508 let _error_prev_file = registry.register_ex_command(
2509 "previous-error-file",
2510 "Jump to the first error / location in the previous file (wraps; `:previous-error-file`; vim `:cprevfile`/`:cpf`/`[qf`).",
2511 ExCommandSpec {
2512 latency_class: LatencyClass::Reflex,
2513 accepts_bang: false,
2514 accepts_range: false,
2515 parse_args: Arc::new(parse_no_args),
2516 apply: Arc::new(|_| {
2517 Ok(Effect::AppAction(AppEffect::ErrorNav {
2518 target: crate::app_effect::ErrorTarget::PrevFile,
2519 }))
2520 }),
2521 args_schema: vec![],
2522 surface_form: SurfaceForm::Keyword,
2523 },
2524 );
2525 let reload_snippets = registry.register_ex_command(
2531 "ex:reload-snippets",
2532 "Re-read every snippet file from disk and rebuild the per-language snippet registry (`:reload-snippets`, Phase 4.2.g.4).",
2533 ExCommandSpec {
2534 latency_class: LatencyClass::Display,
2535 accepts_bang: false,
2536 accepts_range: false,
2537 parse_args: Arc::new(parse_no_args),
2538 apply: Arc::new(|_| Ok(Effect::ReloadSnippets)),
2539 args_schema: vec![],
2540 surface_form: SurfaceForm::Keyword,
2541 },
2542 );
2543 let cd = registry.register_ex_command(
2544 "ex:cd",
2545 "Change the working directory (`:cd [path]`). No arg goes to HOME.",
2546 ExCommandSpec {
2547 latency_class: LatencyClass::Reflex,
2548 accepts_bang: false,
2549 accepts_range: false,
2550 parse_args: Arc::new(parse_optional_path),
2551 apply: Arc::new(|ctx| {
2552 let path = match &ctx.args {
2553 Args::String(s) => Some(s.clone()),
2554 Args::None => None,
2555 _ => return Err(CommandError::BadArgs("unexpected argument".into())),
2556 };
2557 Ok(Effect::ChangeDir(path))
2558 }),
2559 args_schema: vec![ArgSpec {
2560 name: "path".into(),
2561 kind: ArgKind::String,
2562 doc: "Directory path. Absent = HOME.".into(),
2563 prompt: "directory:".into(),
2564 default: ArgDefault::None,
2565 completion: Some("gen:directories".into()),
2566 picker: None,
2567 }],
2568 surface_form: SurfaceForm::Keyword,
2569 },
2570 );
2571 let pwd = registry.register_ex_command(
2572 "ex:pwd",
2573 "Print the current working directory (`:pwd`).",
2574 ExCommandSpec {
2575 latency_class: LatencyClass::Reflex,
2576 accepts_bang: false,
2577 accepts_range: false,
2578 parse_args: Arc::new(parse_no_args),
2579 apply: Arc::new(|_| Ok(Effect::PrintWorkingDir)),
2580 args_schema: vec![],
2581 surface_form: SurfaceForm::Keyword,
2582 },
2583 );
2584 let project_root = registry.register_ex_command(
2585 "ex:project-root",
2586 "Print the active buffer's project root and the marker that decided it (`:project-root`).",
2587 ExCommandSpec {
2588 latency_class: LatencyClass::Reflex,
2589 accepts_bang: false,
2590 accepts_range: false,
2591 parse_args: Arc::new(parse_no_args),
2592 apply: Arc::new(|_| Ok(Effect::PrintProjectRoot)),
2593 args_schema: vec![],
2594 surface_form: SurfaceForm::Keyword,
2595 },
2596 );
2597 let help = registry.register_ex_command(
2598 "ex:help",
2599 "Open the topic index or a named help topic (`:help [topic]`).",
2600 ExCommandSpec {
2601 latency_class: LatencyClass::Display,
2602 accepts_bang: false,
2603 accepts_range: false,
2604 parse_args: Arc::new(parse_optional_path),
2605 apply: Arc::new(|ctx| {
2606 let topic = match &ctx.args {
2607 Args::String(s) if !s.is_empty() => Some(s.to_string()),
2608 _ => None,
2609 };
2610 Ok(Effect::OpenHelpTopic { topic })
2611 }),
2612 args_schema: vec![ArgSpec {
2613 name: "topic".into(),
2614 kind: ArgKind::String,
2615 doc: "Topic name (`folding`, `buffers`, ...). Absent = index.".into(),
2616 prompt: "topic:".into(),
2617 default: ArgDefault::None,
2618 completion: Some("gen:help-topics".into()),
2619 picker: None,
2620 }],
2621 surface_form: SurfaceForm::Keyword,
2622 },
2623 );
2624 ExBuiltins {
2625 write,
2626 quit,
2627 write_quit,
2628 no_hlsearch,
2629 list_registers,
2630 list_marks,
2631 delete_line,
2632 set_option,
2633 set_local_option,
2634 set_global_option,
2635 colorscheme,
2636 edit,
2637 substitute,
2638 global,
2639 describe_command,
2640 describe_buffer,
2641 apropos,
2642 describe_key,
2643 list_keymap,
2644 buffer_next,
2645 buffer_prev,
2646 list_buffers,
2647 buffer_delete,
2648 file_tree,
2649 file_tree_close,
2650 oil,
2651 describe_option,
2652 describe_element,
2653 list_options,
2654 describe_plugin_api,
2655 list_plugin_apis,
2656 export_plugin_api,
2657 list_commands,
2658 describe_plugin,
2659 list_plugins,
2660 describe_events,
2661 describe_event,
2662 list_modes,
2664 describe_mode,
2665 describe_option_resolution,
2666 customize,
2667 tutor,
2668 tutor_next,
2669 tutor_prev,
2670 hover,
2671 hover_close,
2672 help,
2673 list_diagnostics,
2674 next_diagnostic,
2675 prev_diagnostic,
2676 lsp_log,
2677 messages,
2678 lsp_trace,
2679 lsp_status,
2680 lsp_server_log,
2681 lsp_restart,
2682 lsp_progress_cancel,
2683 lsp_expand_region,
2684 lsp_shrink_region,
2685 lsp_log_level,
2686 lsp_log_clear,
2687 lsp_symbols,
2688 lsp_workspace_symbol,
2689 lsp_incoming_calls,
2690 lsp_outgoing_calls,
2691 lsp_supertypes,
2692 lsp_subtypes,
2693 lsp_moniker,
2694 lsp_code_lens,
2695 lsp_color_presentation,
2696 format,
2697 lsp_format,
2698 lsp_format_range,
2699 lsp_signature_help,
2700 lsp_complete,
2701 lsp_rename,
2702 lsp_code_action,
2703 reload_snippets,
2704 cd,
2705 pwd,
2706 project_root,
2707 }
2708}
2709
2710fn parse_no_args(rest: &str, _bang: bool) -> GrammarResult<Args> {
2713 if rest.trim().is_empty() {
2714 Ok(Args::None)
2715 } else {
2716 Err(CommandError::BadArgs(
2717 "trailing characters after command".into(),
2718 ))
2719 }
2720}
2721
2722fn parse_history_args(rest: &str, _bang: bool) -> GrammarResult<Args> {
2726 let trimmed = rest.trim();
2727 if trimmed.is_empty() {
2728 return Ok(Args::None);
2729 }
2730 let lower = trimmed.to_lowercase();
2731 match lower.as_str() {
2732 "commands" | "searches" | "pane-buffers" => Ok(Args::List(vec![ArgValue::String(lower)])),
2736 other => Err(CommandError::BadArgs(format!(
2737 "unknown history kind `{other}`; expected `commands`, `searches`, or `pane-buffers`"
2738 ))),
2739 }
2740}
2741
2742fn parse_optional_path(rest: &str, _bang: bool) -> GrammarResult<Args> {
2743 let trimmed = rest.trim();
2744 if trimmed.is_empty() {
2745 Ok(Args::None)
2746 } else {
2747 Ok(Args::String(trimmed.to_string()))
2748 }
2749}
2750
2751fn parse_tabmove_arg(rest: &str, _bang: bool) -> GrammarResult<Args> {
2756 let trimmed = rest.trim();
2757 if trimmed.is_empty() {
2758 return Ok(Args::String("0".to_string()));
2759 }
2760 trimmed.parse::<u32>().map_err(|e| {
2762 CommandError::BadArgs(format!(":tabmove arg must be a non-negative integer: {e}"))
2763 })?;
2764 Ok(Args::String(trimmed.to_string()))
2765}
2766
2767fn parse_cc_arg(rest: &str, _bang: bool) -> GrammarResult<Args> {
2772 let trimmed = rest.trim();
2773 if trimmed.is_empty() {
2774 return Ok(Args::None);
2775 }
2776 trimmed
2778 .parse::<usize>()
2779 .map_err(|e| CommandError::BadArgs(format!(":cc arg must be a positive integer: {e}")))?;
2780 Ok(Args::String(trimmed.to_string()))
2781}
2782
2783fn parse_optional_string(rest: &str, _bang: bool) -> GrammarResult<Args> {
2786 let trimmed = rest.trim();
2787 if trimmed.is_empty() {
2788 Ok(Args::None)
2789 } else {
2790 Ok(Args::String(trimmed.to_string()))
2791 }
2792}
2793
2794fn apply_describe_plugin_api(ctx: &ExCommandContext) -> GrammarResult<Effect> {
2795 let seam = match &ctx.args {
2796 Args::String(s) => Some(s.clone()),
2797 Args::None => None,
2798 _ => {
2799 return Err(CommandError::BadArgs(
2800 "expected an optional seam name".into(),
2801 ));
2802 }
2803 };
2804 Ok(Effect::DescribePluginApi { seam })
2805}
2806
2807fn apply_export_plugin_api(ctx: &ExCommandContext) -> GrammarResult<Effect> {
2808 let format = match &ctx.args {
2809 Args::String(s) => {
2810 let f = s.trim().to_ascii_lowercase();
2811 if f != "markdown" && f != "md" && f != "json" {
2814 return Err(CommandError::BadArgs(format!(
2815 "unknown format `{s}` (expected `markdown` or `json`)"
2816 )));
2817 }
2818 Some(f)
2819 }
2820 Args::None => None,
2821 _ => return Err(CommandError::BadArgs("expected an optional format".into())),
2822 };
2823 Ok(Effect::ExportPluginApi { format })
2824}
2825
2826fn parse_required_string(rest: &str, _bang: bool) -> GrammarResult<Args> {
2827 let trimmed = rest.trim();
2828 if trimmed.is_empty() {
2829 Err(CommandError::BadArgs("argument required".into()))
2830 } else {
2831 Ok(Args::String(trimmed.to_string()))
2832 }
2833}
2834
2835fn parse_picker_args(rest: &str, _bang: bool) -> GrammarResult<Args> {
2842 let trimmed = rest.trim();
2843 if trimmed.is_empty() {
2844 return Err(CommandError::BadArgs(
2845 "picker source required (e.g. `:picker files`)".into(),
2846 ));
2847 }
2848 let mut tokens = trimmed.split_whitespace();
2849 let source = tokens
2850 .next()
2851 .expect("non-empty after trim guarantees at least one token");
2852 let mut values: Vec<ArgValue> = Vec::with_capacity(1);
2853 values.push(ArgValue::String(source.to_string()));
2854 for tok in tokens {
2855 values.push(ArgValue::Raw(tok.to_string()));
2856 }
2857 Ok(Args::List(values))
2858}
2859
2860fn parse_substitute_args_unreachable(_rest: &str, _bang: bool) -> GrammarResult<Args> {
2866 Err(CommandError::BadArgs(
2867 "use the delimiter form: `:s/pattern/replacement/[flags]`".into(),
2868 ))
2869}
2870
2871fn parse_global_args_unreachable(_rest: &str, _bang: bool) -> GrammarResult<Args> {
2872 Err(CommandError::BadArgs(
2873 "use the delimiter form: `:g/pattern/body` (or `:v/...` for inverted)".into(),
2874 ))
2875}
2876
2877fn apply_write(ctx: &ExCommandContext) -> GrammarResult<Effect> {
2880 let path = match &ctx.args {
2881 Args::None => None,
2882 Args::String(s) => Some(std::path::PathBuf::from(s)),
2883 _ => {
2884 return Err(CommandError::BadArgs(
2885 "expected optional path string".into(),
2886 ));
2887 }
2888 };
2889 Ok(Effect::SaveBuffer { path })
2890}
2891
2892fn apply_quit(ctx: &ExCommandContext) -> GrammarResult<Effect> {
2893 Ok(Effect::QuitEditor {
2896 force: ctx.bang,
2897 scope: QuitScope::Pane,
2898 })
2899}
2900
2901fn apply_quit_all(ctx: &ExCommandContext) -> GrammarResult<Effect> {
2902 Ok(Effect::QuitEditor {
2907 force: ctx.bang,
2908 scope: QuitScope::All,
2909 })
2910}
2911
2912fn apply_write_quit(ctx: &ExCommandContext) -> GrammarResult<Effect> {
2913 Ok(Effect::Many(vec![
2918 Effect::SaveBuffer { path: None },
2919 Effect::QuitEditor {
2920 force: ctx.bang,
2921 scope: QuitScope::Pane,
2922 },
2923 ]))
2924}
2925
2926fn apply_set(ctx: &ExCommandContext) -> GrammarResult<Effect> {
2927 match &ctx.args {
2928 Args::String(s) => Ok(Effect::SetOption { spec: s.clone() }),
2929 _ => Err(CommandError::BadArgs("expected option string".into())),
2930 }
2931}
2932
2933fn apply_set_local(ctx: &ExCommandContext) -> GrammarResult<Effect> {
2934 match &ctx.args {
2935 Args::String(s) => Ok(Effect::SetLocalOption { spec: s.clone() }),
2936 _ => Err(CommandError::BadArgs("expected option string".into())),
2937 }
2938}
2939
2940fn apply_set_global(ctx: &ExCommandContext) -> GrammarResult<Effect> {
2941 match &ctx.args {
2942 Args::String(s) => Ok(Effect::SetGlobalOption { spec: s.clone() }),
2943 _ => Err(CommandError::BadArgs("expected option string".into())),
2944 }
2945}
2946
2947fn apply_colorscheme(ctx: &ExCommandContext) -> GrammarResult<Effect> {
2948 match &ctx.args {
2954 Args::String(s) if !s.trim().is_empty() => Ok(Effect::SetColorscheme(s.trim().to_string())),
2955 _ => Ok(Effect::SetColorscheme(String::new())),
2957 }
2958}
2959
2960fn apply_edit(ctx: &ExCommandContext) -> GrammarResult<Effect> {
2961 let path = match &ctx.args {
2962 Args::None => None,
2963 Args::String(s) => Some(std::path::PathBuf::from(s)),
2964 _ => {
2965 return Err(CommandError::BadArgs(
2966 "expected optional path string".into(),
2967 ));
2968 }
2969 };
2970 Ok(Effect::OpenBuffer {
2971 path,
2972 force: ctx.bang,
2973 })
2974}
2975
2976fn apply_substitute(ctx: &ExCommandContext) -> GrammarResult<Effect> {
2977 let list = ctx
2978 .args
2979 .as_list()
2980 .ok_or_else(|| CommandError::BadArgs("expected Args::List for :substitute".into()))?;
2981 if list.len() != 3 {
2982 return Err(CommandError::BadArgs(
2983 "expected 3 args: pattern, replacement, flags".into(),
2984 ));
2985 }
2986 let pattern = list[0]
2987 .as_str()
2988 .ok_or_else(|| CommandError::BadArgs("arg 0 (pattern) must be string-shaped".into()))?
2989 .to_string();
2990 let replacement = list[1]
2991 .as_str()
2992 .ok_or_else(|| CommandError::BadArgs("arg 1 (replacement) must be string-shaped".into()))?
2993 .to_string();
2994 let flags = list[2]
2995 .as_str()
2996 .ok_or_else(|| CommandError::BadArgs("arg 2 (flags) must be string-shaped".into()))?;
2997 let global = flags.contains('g');
2998 let scope = match ctx.range {
3002 Some(Range::Whole) => SubstituteScope::Whole,
3003 _ => SubstituteScope::CurrentLine,
3004 };
3005 Ok(Effect::Substitute {
3006 scope,
3007 pattern,
3008 replacement,
3009 global,
3010 })
3011}
3012
3013fn apply_describe_command(ctx: &ExCommandContext) -> GrammarResult<Effect> {
3014 match &ctx.args {
3015 Args::String(s) => Ok(Effect::DescribeCommand {
3016 name: s.clone(),
3017 anchor: None,
3018 }),
3019 _ => Err(CommandError::BadArgs("expected command name string".into())),
3020 }
3021}
3022
3023fn apply_apropos(ctx: &ExCommandContext) -> GrammarResult<Effect> {
3024 match &ctx.args {
3025 Args::String(s) => Ok(Effect::Apropos { pattern: s.clone() }),
3026 _ => Err(CommandError::BadArgs("expected pattern string".into())),
3027 }
3028}
3029
3030fn apply_describe_key(ctx: &ExCommandContext) -> GrammarResult<Effect> {
3031 match &ctx.args {
3032 Args::String(s) => Ok(Effect::DescribeKey { chord: s.clone() }),
3033 _ => Err(CommandError::BadArgs(
3034 "expected chord notation string".into(),
3035 )),
3036 }
3037}
3038
3039fn apply_global(ctx: &ExCommandContext) -> GrammarResult<Effect> {
3040 let list = ctx
3041 .args
3042 .as_list()
3043 .ok_or_else(|| CommandError::BadArgs("expected Args::List for :global".into()))?;
3044 if list.len() != 3 {
3045 return Err(CommandError::BadArgs(
3046 "expected 3 args: pattern, inverted, body".into(),
3047 ));
3048 }
3049 let pattern = list[0]
3050 .as_str()
3051 .ok_or_else(|| CommandError::BadArgs("arg 0 (pattern) must be string-shaped".into()))?
3052 .to_string();
3053 let inverted = list[1]
3054 .as_bool()
3055 .ok_or_else(|| CommandError::BadArgs("arg 1 (inverted) must be bool".into()))?;
3056 let body = list[2]
3057 .as_invocation()
3058 .ok_or_else(|| {
3059 CommandError::BadArgs(
3060 "arg 2 (body) must be a parsed Invocation -- parser front-end is responsible"
3061 .into(),
3062 )
3063 })?
3064 .clone();
3065 Ok(Effect::Global {
3066 pattern,
3067 inverted,
3068 body: Box::new(body),
3069 })
3070}
3071
3072#[cfg(test)]
3073mod tests {
3074 #![allow(clippy::unwrap_used, clippy::panic)]
3075 use super::*;
3076 use crate::CancellationToken;
3077 use crate::command::CommandInvocation;
3078 use crate::dispatcher::execute;
3079 use lattice_core::Document;
3080 use lattice_protocol::position::Position;
3081
3082 fn fixture() -> (CommandRegistry, ExBuiltins, Document) {
3083 let mut registry = CommandRegistry::new();
3084 let _ = crate::builtins::populate(&mut registry);
3085 let ex = populate(&mut registry);
3086 (registry, ex, Document::empty())
3087 }
3088
3089 #[test]
3090 fn write_with_no_path_emits_save_buffer_none() {
3091 let (registry, ex, mut doc) = fixture();
3092 let inv = CommandInvocation::of(ex.write.0).with_args(Args::None);
3093 let eff = execute(
3094 ®istry,
3095 &mut doc,
3096 lattice_core::BufferId(0),
3097 Position::ZERO,
3098 inv,
3099 &CancellationToken::never(),
3100 )
3101 .unwrap();
3102 match eff {
3103 Effect::SaveBuffer { path } => assert!(path.is_none()),
3104 other => panic!("unexpected effect: {other:?}"),
3105 }
3106 }
3107
3108 #[test]
3109 fn write_with_path_carries_path() {
3110 let (registry, ex, mut doc) = fixture();
3111 let inv = CommandInvocation::of(ex.write.0).with_args(Args::String("foo.txt".into()));
3112 let eff = execute(
3113 ®istry,
3114 &mut doc,
3115 lattice_core::BufferId(0),
3116 Position::ZERO,
3117 inv,
3118 &CancellationToken::never(),
3119 )
3120 .unwrap();
3121 match eff {
3122 Effect::SaveBuffer { path: Some(p) } => {
3123 assert_eq!(p, std::path::PathBuf::from("foo.txt"))
3124 }
3125 other => panic!("unexpected effect: {other:?}"),
3126 }
3127 }
3128
3129 #[test]
3130 fn quit_bang_propagates_to_force() {
3131 let (registry, ex, mut doc) = fixture();
3132 let inv = CommandInvocation::of(ex.quit.0).with_bang(true);
3133 let eff = execute(
3134 ®istry,
3135 &mut doc,
3136 lattice_core::BufferId(0),
3137 Position::ZERO,
3138 inv,
3139 &CancellationToken::never(),
3140 )
3141 .unwrap();
3142 match eff {
3143 Effect::QuitEditor { force, scope } => {
3144 assert!(force);
3145 assert_eq!(scope, QuitScope::Pane, ":q is pane-scoped");
3146 }
3147 other => panic!("unexpected effect: {other:?}"),
3148 }
3149 }
3150
3151 #[test]
3152 fn quit_no_bang_is_not_forced() {
3153 let (registry, ex, mut doc) = fixture();
3154 let inv = CommandInvocation::of(ex.quit.0);
3155 let eff = execute(
3156 ®istry,
3157 &mut doc,
3158 lattice_core::BufferId(0),
3159 Position::ZERO,
3160 inv,
3161 &CancellationToken::never(),
3162 )
3163 .unwrap();
3164 match eff {
3165 Effect::QuitEditor { force, scope } => {
3166 assert!(!force);
3167 assert_eq!(scope, QuitScope::Pane, ":q is pane-scoped");
3168 }
3169 other => panic!("unexpected effect: {other:?}"),
3170 }
3171 }
3172
3173 #[test]
3174 fn describe_active_modes_is_registered_and_takes_no_args() {
3175 let (registry, _ex, mut doc) = fixture();
3181 let id = registry
3182 .id_by_name("ex:describe-active-modes")
3183 .expect("ex:describe-active-modes must be registered");
3184 let eff = execute(
3185 ®istry,
3186 &mut doc,
3187 lattice_core::BufferId(0),
3188 Position::ZERO,
3189 CommandInvocation::of(id),
3190 &CancellationToken::never(),
3191 )
3192 .unwrap();
3193 assert!(matches!(eff, Effect::DescribeActiveModes), "got {eff:?}");
3194 }
3195
3196 #[test]
3197 fn describe_active_modes_has_an_empty_args_schema() {
3198 let (registry, _ex, _doc) = fixture();
3202 let spec = registry
3203 .lookup_by_name("ex:describe-active-modes")
3204 .expect("registered");
3205 assert!(
3206 spec.args_schema.is_empty(),
3207 "an arg schema would re-arm the prompt this command exists to remove: {:?}",
3208 spec.args_schema,
3209 );
3210 }
3211
3212 #[test]
3213 fn describe_active_modes_does_not_shadow_describe_mode() {
3214 let (registry, _ex, _doc) = fixture();
3222 assert!(
3223 registry.id_by_name("ex:describe-mode").is_some(),
3224 "ex:describe-mode must still be registered and untouched",
3225 );
3226 assert!(
3227 registry.id_by_name("ex:describe-modes").is_none(),
3228 "`:describe-modes` must not exist — it would shadow `:describe-mode` completion",
3229 );
3230 }
3231
3232 #[test]
3233 fn describe_mode_still_requires_its_argument() {
3234 assert!(matches!(
3238 parse_required_string("", false),
3239 Err(CommandError::BadArgs(_))
3240 ));
3241 let (registry, _ex, _doc) = fixture();
3242 let spec = registry
3243 .lookup_by_name("ex:describe-mode")
3244 .expect("registered");
3245 assert_eq!(spec.args_schema.len(), 1, "describe-mode keeps its one arg");
3246 }
3247
3248 #[test]
3249 fn quit_all_is_editor_scoped() {
3250 let (registry, _ex, mut doc) = fixture();
3254 let id = registry
3255 .id_by_name("ex:quit-all")
3256 .expect("ex:quit-all must be registered");
3257 for (bang, want_force) in [(false, false), (true, true)] {
3258 let mut inv = CommandInvocation::of(id);
3259 if bang {
3260 inv = inv.with_bang(true);
3261 }
3262 let eff = execute(
3263 ®istry,
3264 &mut doc,
3265 lattice_core::BufferId(0),
3266 Position::ZERO,
3267 inv,
3268 &CancellationToken::never(),
3269 )
3270 .unwrap();
3271 match eff {
3272 Effect::QuitEditor { force, scope } => {
3273 assert_eq!(force, want_force);
3274 assert_eq!(scope, QuitScope::All, ":qa is editor-scoped");
3275 }
3276 other => panic!("unexpected effect: {other:?}"),
3277 }
3278 }
3279 }
3280
3281 #[test]
3282 fn write_quit_emits_many_save_then_quit() {
3283 let (registry, ex, mut doc) = fixture();
3284 let inv = CommandInvocation::of(ex.write_quit.0).with_bang(true);
3285 let eff = execute(
3286 ®istry,
3287 &mut doc,
3288 lattice_core::BufferId(0),
3289 Position::ZERO,
3290 inv,
3291 &CancellationToken::never(),
3292 )
3293 .unwrap();
3294 match eff {
3295 Effect::Many(parts) => {
3296 assert!(matches!(parts[0], Effect::SaveBuffer { .. }));
3297 assert!(matches!(
3298 parts[1],
3299 Effect::QuitEditor {
3300 force: true,
3301 scope: QuitScope::Pane
3302 }
3303 ));
3304 }
3305 other => panic!("unexpected effect: {other:?}"),
3306 }
3307 }
3308
3309 #[test]
3310 fn pane_split_and_close_commands_emit_app_effects() {
3311 let (registry, _ex, mut doc) = fixture();
3315 for (name, want) in [
3316 ("ex:split", AppEffect::SplitPaneHorizontal),
3317 ("ex:vsplit", AppEffect::SplitPaneVertical),
3318 ("ex:close", AppEffect::ClosePane),
3319 ("ex:zoom-pane", AppEffect::ToggleZoomPane),
3322 ] {
3323 let id = registry
3324 .id_by_name(name)
3325 .unwrap_or_else(|| panic!("{name} must be registered"));
3326 let eff = execute(
3327 ®istry,
3328 &mut doc,
3329 lattice_core::BufferId(0),
3330 Position::ZERO,
3331 CommandInvocation::of(id),
3332 &CancellationToken::never(),
3333 )
3334 .unwrap();
3335 match eff {
3336 Effect::AppAction(got) => assert_eq!(got, want, "{name}"),
3337 other => panic!("{name}: unexpected effect: {other:?}"),
3338 }
3339 }
3340 }
3341
3342 #[test]
3343 fn nohlsearch_emits_clear_search_highlight() {
3344 let (registry, ex, mut doc) = fixture();
3345 let inv = CommandInvocation::of(ex.no_hlsearch.0);
3346 let eff = execute(
3347 ®istry,
3348 &mut doc,
3349 lattice_core::BufferId(0),
3350 Position::ZERO,
3351 inv,
3352 &CancellationToken::never(),
3353 )
3354 .unwrap();
3355 assert!(matches!(eff, Effect::ClearSearchHighlight));
3356 }
3357
3358 #[test]
3359 fn registers_emits_echo_registers() {
3360 let (registry, ex, mut doc) = fixture();
3361 let inv = CommandInvocation::of(ex.list_registers.0);
3362 let eff = execute(
3363 ®istry,
3364 &mut doc,
3365 lattice_core::BufferId(0),
3366 Position::ZERO,
3367 inv,
3368 &CancellationToken::never(),
3369 )
3370 .unwrap();
3371 assert!(matches!(eff, Effect::EchoRegisters));
3372 }
3373
3374 #[test]
3375 fn marks_emits_echo_marks() {
3376 let (registry, ex, mut doc) = fixture();
3377 let inv = CommandInvocation::of(ex.list_marks.0);
3378 let eff = execute(
3379 ®istry,
3380 &mut doc,
3381 lattice_core::BufferId(0),
3382 Position::ZERO,
3383 inv,
3384 &CancellationToken::never(),
3385 )
3386 .unwrap();
3387 assert!(matches!(eff, Effect::EchoMarks));
3388 }
3389
3390 #[test]
3391 fn delete_emits_delete_current_line() {
3392 let (registry, ex, mut doc) = fixture();
3393 let inv = CommandInvocation::of(ex.delete_line.0);
3394 let eff = execute(
3395 ®istry,
3396 &mut doc,
3397 lattice_core::BufferId(0),
3398 Position::ZERO,
3399 inv,
3400 &CancellationToken::never(),
3401 )
3402 .unwrap();
3403 assert!(matches!(eff, Effect::DeleteCurrentLine));
3404 }
3405
3406 #[test]
3407 fn describe_command_emits_describe_command_effect() {
3408 let (registry, ex, mut doc) = fixture();
3409 let inv =
3410 CommandInvocation::of(ex.describe_command.0).with_args(Args::String("ex:write".into()));
3411 let eff = execute(
3412 ®istry,
3413 &mut doc,
3414 lattice_core::BufferId(0),
3415 Position::ZERO,
3416 inv,
3417 &CancellationToken::never(),
3418 )
3419 .unwrap();
3420 match eff {
3421 Effect::DescribeCommand { name, anchor } => {
3422 assert_eq!(name, "ex:write");
3423 assert!(anchor.is_none());
3424 }
3425 other => panic!("unexpected effect: {other:?}"),
3426 }
3427 }
3428
3429 #[test]
3430 fn describe_plugin_api_emits_effect_with_and_without_seam() {
3431 let (registry, ex, mut doc) = fixture();
3433 let inv = CommandInvocation::of(ex.describe_plugin_api.0)
3434 .with_args(Args::String("host-services".into()));
3435 let eff = execute(
3436 ®istry,
3437 &mut doc,
3438 lattice_core::BufferId(0),
3439 Position::ZERO,
3440 inv,
3441 &CancellationToken::never(),
3442 )
3443 .unwrap();
3444 match eff {
3445 Effect::DescribePluginApi { seam } => {
3446 assert_eq!(seam.as_deref(), Some("host-services"))
3447 }
3448 other => panic!("unexpected effect: {other:?}"),
3449 }
3450 let inv = CommandInvocation::of(ex.describe_plugin_api.0);
3452 let eff = execute(
3453 ®istry,
3454 &mut doc,
3455 lattice_core::BufferId(0),
3456 Position::ZERO,
3457 inv,
3458 &CancellationToken::never(),
3459 )
3460 .unwrap();
3461 match eff {
3462 Effect::DescribePluginApi { seam } => assert!(seam.is_none()),
3463 other => panic!("unexpected effect: {other:?}"),
3464 }
3465 }
3466
3467 #[test]
3468 fn list_plugin_apis_emits_effect() {
3469 let (registry, ex, mut doc) = fixture();
3470 let inv = CommandInvocation::of(ex.list_plugin_apis.0);
3471 let eff = execute(
3472 ®istry,
3473 &mut doc,
3474 lattice_core::BufferId(0),
3475 Position::ZERO,
3476 inv,
3477 &CancellationToken::never(),
3478 )
3479 .unwrap();
3480 assert!(matches!(eff, Effect::ListPluginApis));
3481 }
3482
3483 #[test]
3484 fn export_plugin_api_validates_format_and_emits_effect() {
3485 let (registry, ex, mut doc) = fixture();
3486 let run = |registry: &_, doc: &mut _, args: Args| {
3487 execute(
3488 registry,
3489 doc,
3490 lattice_core::BufferId(0),
3491 Position::ZERO,
3492 CommandInvocation::of(ex.export_plugin_api.0).with_args(args),
3493 &CancellationToken::never(),
3494 )
3495 };
3496 match run(®istry, &mut doc, Args::String("json".into())).unwrap() {
3498 Effect::ExportPluginApi { format } => assert_eq!(format.as_deref(), Some("json")),
3499 other => panic!("unexpected: {other:?}"),
3500 }
3501 match run(®istry, &mut doc, Args::None).unwrap() {
3502 Effect::ExportPluginApi { format } => assert!(format.is_none()),
3503 other => panic!("unexpected: {other:?}"),
3504 }
3505 assert!(run(®istry, &mut doc, Args::String("yaml".into())).is_err());
3507 }
3508
3509 #[test]
3510 fn list_commands_emits_effect() {
3511 let (registry, ex, mut doc) = fixture();
3512 let eff = execute(
3513 ®istry,
3514 &mut doc,
3515 lattice_core::BufferId(0),
3516 Position::ZERO,
3517 CommandInvocation::of(ex.list_commands.0),
3518 &CancellationToken::never(),
3519 )
3520 .unwrap();
3521 assert!(matches!(eff, Effect::ListCommands));
3522 }
3523
3524 #[test]
3525 fn describe_plugin_and_list_plugins_emit_effects() {
3526 let (registry, ex, mut doc) = fixture();
3527 let run = |registry: &_, doc: &mut _, id, args: Args| {
3528 execute(
3529 registry,
3530 doc,
3531 lattice_core::BufferId(0),
3532 Position::ZERO,
3533 CommandInvocation::of(id).with_args(args),
3534 &CancellationToken::never(),
3535 )
3536 };
3537 match run(
3538 ®istry,
3539 &mut doc,
3540 ex.describe_plugin.0,
3541 Args::String("git-gutter".into()),
3542 )
3543 .unwrap()
3544 {
3545 Effect::DescribePlugin { name } => assert_eq!(name, "git-gutter"),
3546 other => panic!("unexpected: {other:?}"),
3547 }
3548 assert!(run(®istry, &mut doc, ex.describe_plugin.0, Args::None).is_err());
3550 assert!(matches!(
3551 run(®istry, &mut doc, ex.list_plugins.0, Args::None).unwrap(),
3552 Effect::ListPlugins
3553 ));
3554 }
3555
3556 #[test]
3564 fn plugin_command_lands_in_the_store_read_by_completion_and_introspection() {
3565 let mut registry = CommandRegistry::new();
3566 let spec = ExCommandSpec {
3567 latency_class: LatencyClass::Display,
3568 accepts_bang: false,
3569 accepts_range: false,
3570 parse_args: Arc::new(parse_no_args),
3571 apply: Arc::new(|_| Ok(Effect::None)),
3572 args_schema: vec![],
3573 surface_form: SurfaceForm::Keyword,
3574 };
3575 registry.register_plugin_ex_command(7, "plugin-cmd", "a plugin command", spec);
3576 assert!(
3578 registry.names().any(|n| n == "plugin-cmd"),
3579 "plugin command must be in the store `names()` iterates"
3580 );
3581 let spec = registry
3582 .lookup_by_name("plugin-cmd")
3583 .expect("plugin command is looked up like any other");
3584 assert!(
3585 matches!(spec.source.layer, crate::source::SourceLayer::Plugin(7)),
3586 "plugin command carries Plugin(id) provenance"
3587 );
3588 assert_eq!(spec.doc, "a plugin command");
3589 }
3590
3591 #[test]
3592 fn describe_buffer_emits_describe_buffer_effect() {
3593 let (registry, ex, mut doc) = fixture();
3594 let inv = CommandInvocation::of(ex.describe_buffer.0);
3595 let eff = execute(
3596 ®istry,
3597 &mut doc,
3598 lattice_core::BufferId(0),
3599 Position::ZERO,
3600 inv,
3601 &CancellationToken::never(),
3602 )
3603 .unwrap();
3604 assert!(matches!(eff, Effect::DescribeBuffer));
3605 }
3606
3607 #[test]
3608 fn apropos_emits_apropos_effect() {
3609 let (registry, ex, mut doc) = fixture();
3610 let inv = CommandInvocation::of(ex.apropos.0).with_args(Args::String("write".into()));
3611 let eff = execute(
3612 ®istry,
3613 &mut doc,
3614 lattice_core::BufferId(0),
3615 Position::ZERO,
3616 inv,
3617 &CancellationToken::never(),
3618 )
3619 .unwrap();
3620 match eff {
3621 Effect::Apropos { pattern } => assert_eq!(pattern, "write"),
3622 other => panic!("unexpected effect: {other:?}"),
3623 }
3624 }
3625
3626 #[test]
3627 fn describe_command_advertises_args_schema() {
3628 let (registry, ex, _doc) = fixture();
3630 let spec = registry.lookup(ex.describe_command.0).unwrap();
3631 assert_eq!(spec.args_schema.len(), 1);
3632 assert_eq!(spec.args_schema[0].name, "name");
3633 }
3634
3635 #[test]
3636 fn apropos_advertises_args_schema() {
3637 let (registry, ex, _doc) = fixture();
3638 let spec = registry.lookup(ex.apropos.0).unwrap();
3639 assert_eq!(spec.args_schema.len(), 1);
3640 assert_eq!(spec.args_schema[0].name, "pattern");
3641 }
3642
3643 #[test]
3644 fn set_with_string_arg_carries_spec() {
3645 let (registry, ex, mut doc) = fixture();
3646 let inv = CommandInvocation::of(ex.set_option.0).with_args(Args::String("number".into()));
3647 let eff = execute(
3648 ®istry,
3649 &mut doc,
3650 lattice_core::BufferId(0),
3651 Position::ZERO,
3652 inv,
3653 &CancellationToken::never(),
3654 )
3655 .unwrap();
3656 match eff {
3657 Effect::SetOption { spec } => assert_eq!(spec, "number"),
3658 other => panic!("unexpected effect: {other:?}"),
3659 }
3660 }
3661
3662 #[test]
3663 fn set_with_no_args_errors() {
3664 let (registry, ex, mut doc) = fixture();
3665 let inv = CommandInvocation::of(ex.set_option.0).with_args(Args::None);
3669 let err = execute(
3670 ®istry,
3671 &mut doc,
3672 lattice_core::BufferId(0),
3673 Position::ZERO,
3674 inv,
3675 &CancellationToken::never(),
3676 )
3677 .unwrap_err();
3678 assert!(matches!(err, CommandError::BadArgs(_)));
3679 }
3680
3681 #[test]
3682 fn edit_with_path_and_bang_carries_force() {
3683 let (registry, ex, mut doc) = fixture();
3684 let inv = CommandInvocation::of(ex.edit.0)
3685 .with_args(Args::String("/tmp/x".into()))
3686 .with_bang(true);
3687 let eff = execute(
3688 ®istry,
3689 &mut doc,
3690 lattice_core::BufferId(0),
3691 Position::ZERO,
3692 inv,
3693 &CancellationToken::never(),
3694 )
3695 .unwrap();
3696 match eff {
3697 Effect::OpenBuffer { path, force } => {
3698 assert_eq!(path, Some(std::path::PathBuf::from("/tmp/x")));
3699 assert!(force);
3700 }
3701 other => panic!("unexpected effect: {other:?}"),
3702 }
3703 }
3704
3705 #[test]
3706 fn parse_no_args_rejects_trailing() {
3707 assert!(matches!(
3708 parse_no_args("oops", false),
3709 Err(CommandError::BadArgs(_))
3710 ));
3711 assert!(matches!(parse_no_args("", false), Ok(Args::None)));
3712 assert!(matches!(parse_no_args(" ", false), Ok(Args::None)));
3713 }
3714
3715 #[test]
3716 fn parse_optional_path_returns_some_or_none() {
3717 assert!(matches!(parse_optional_path("", false), Ok(Args::None)));
3718 match parse_optional_path("foo.rs", false).unwrap() {
3719 Args::String(s) => assert_eq!(s, "foo.rs"),
3720 other => panic!("unexpected args: {other:?}"),
3721 }
3722 }
3723
3724 #[test]
3725 fn parse_picker_args_splits_source_then_raw_tokens() {
3726 assert!(matches!(
3728 parse_picker_args("", false),
3729 Err(CommandError::BadArgs(_))
3730 ));
3731 assert!(matches!(
3732 parse_picker_args(" ", false),
3733 Err(CommandError::BadArgs(_))
3734 ));
3735 match parse_picker_args("files", false).unwrap() {
3737 Args::List(v) => {
3738 assert_eq!(v.len(), 1);
3739 assert_eq!(v[0].as_str(), Some("files"));
3740 assert!(matches!(v[0], ArgValue::String(_)));
3741 }
3742 other => panic!("unexpected args: {other:?}"),
3743 }
3744 match parse_picker_args("files /tmp/a /tmp/b", false).unwrap() {
3747 Args::List(v) => {
3748 assert_eq!(v.len(), 3);
3749 assert_eq!(v[0].as_str(), Some("files"));
3750 assert!(matches!(v[0], ArgValue::String(_)));
3751 assert_eq!(v[1].as_str(), Some("/tmp/a"));
3752 assert!(matches!(v[1], ArgValue::Raw(_)));
3753 assert_eq!(v[2].as_str(), Some("/tmp/b"));
3754 assert!(matches!(v[2], ArgValue::Raw(_)));
3755 }
3756 other => panic!("unexpected args: {other:?}"),
3757 }
3758 }
3759
3760 #[test]
3761 fn picker_command_applies_into_open_picker_effect() {
3762 let (registry, _ex, mut doc) = fixture();
3763 let picker_id = registry
3764 .id_by_name("ex:picker")
3765 .expect("ex:picker must be registered");
3766 let args = parse_picker_args("files /tmp/a", false).unwrap();
3767 let inv = CommandInvocation::of(picker_id).with_args(args);
3768 let eff = execute(
3769 ®istry,
3770 &mut doc,
3771 lattice_core::BufferId(0),
3772 Position::ZERO,
3773 inv,
3774 &CancellationToken::never(),
3775 )
3776 .unwrap();
3777 match eff {
3778 Effect::OpenPicker { source, args, .. } => {
3779 assert_eq!(source, "files");
3780 assert_eq!(args, vec!["/tmp/a".to_string()]);
3781 }
3782 other => panic!("expected Effect::OpenPicker, got {other:?}"),
3783 }
3784 }
3785
3786 #[test]
3787 fn parse_required_string_demands_arg() {
3788 assert!(matches!(
3789 parse_required_string("", false),
3790 Err(CommandError::BadArgs(_))
3791 ));
3792 match parse_required_string("number", false).unwrap() {
3793 Args::String(s) => assert_eq!(s, "number"),
3794 other => panic!("unexpected args: {other:?}"),
3795 }
3796 }
3797
3798 #[test]
3799 fn option_describing_commands_complete_against_gen_options() {
3800 let (registry, ex, _) = fixture();
3806 for id in [ex.describe_option, ex.describe_option_resolution] {
3807 let spec = registry.ex_command_spec(id.0).unwrap();
3808 assert_eq!(spec.args_schema.len(), 1);
3809 assert_eq!(
3810 spec.args_schema[0].completion.as_deref(),
3811 Some("gen:options")
3812 );
3813 }
3814 }
3815
3816 #[test]
3817 fn introspection_commands_advertise_completion_sources() {
3818 let (registry, ex, _) = fixture();
3824 let cases: &[(crate::ExCommandId, &str)] = &[
3825 (ex.describe_command, "gen:commands"),
3826 (ex.describe_event, "gen:events"),
3827 (ex.describe_mode, "gen:modes"),
3828 (ex.describe_option, "gen:options"),
3829 (ex.describe_option_resolution, "gen:options"),
3830 (ex.customize, "gen:customize"),
3831 ];
3832 for (id, expected) in cases {
3833 let cmd = registry.lookup(id.0).unwrap();
3834 let spec = registry.ex_command_spec(id.0).unwrap();
3835 assert_eq!(
3836 spec.args_schema[0].completion.as_deref(),
3837 Some(*expected),
3838 "{} should complete against {expected}",
3839 cmd.name
3840 );
3841 }
3842 }
3843
3844 #[test]
3845 fn lsp_admin_commands_complete_against_server_ids() {
3846 let (registry, ex, _) = fixture();
3851 let cases: &[crate::ExCommandId] =
3852 &[ex.lsp_log, ex.lsp_trace, ex.lsp_restart, ex.lsp_log_clear];
3853 for id in cases {
3854 let cmd = registry.lookup(id.0).unwrap();
3855 let spec = registry.ex_command_spec(id.0).unwrap();
3856 assert_eq!(
3857 spec.args_schema[0].completion.as_deref(),
3858 Some("gen:lsp-servers"),
3859 "{} should complete against gen:lsp-servers",
3860 cmd.name
3861 );
3862 }
3863 let cmd = registry.lookup(ex.lsp_log_level.0).unwrap();
3868 let spec = registry.ex_command_spec(ex.lsp_log_level.0).unwrap();
3869 assert_eq!(
3870 spec.args_schema[0].completion.as_deref(),
3871 Some("gen:log-levels"),
3872 "{} should complete against gen:log-levels",
3873 cmd.name
3874 );
3875 }
3876}