1pub use complete::{
12 ArgSources, CompletionKind, Completions, collect_host_registry_names, collect_registry_names,
13 complete, complete_arg, complete_command_from_names, first_word_end, longest_common_prefix,
14};
15pub use effect::ExEffect;
16pub use expand::{ExpandContext, expand_args, expand_filename};
17pub use range::{LineRange, parse_range};
18pub use registry::{ArgKind, ExCommand, HostCmd, HostRegistry, Registry};
19
20mod builtins;
21mod complete;
22mod effect;
23pub mod expand;
24mod folds;
25mod global;
26mod listings;
27mod parse;
28mod range;
29mod registry;
30mod setopt;
31mod shell;
32
33pub use setopt::all_setting_names;
34
35pub fn try_dispatch<H: hjkl_engine::Host>(
46 reg: &Registry<H>,
47 editor: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
48 input: &str,
49) -> Option<ExEffect> {
50 let input = input.trim();
51 if input.is_empty() {
52 return None;
53 }
54
55 if input.starts_with('/') || input.starts_with('?') {
59 return Some(handle_search_address(editor, input));
60 }
61
62 let (range, cmd_str) = match parse_range(input, editor) {
64 Ok(pair) => pair,
65 Err(e) => return Some(ExEffect::Error(e)),
66 };
67
68 if let Some(rest) = cmd_str.strip_prefix('!') {
71 let shell_cmd = rest.trim();
72 return Some(shell::shell_filter_handler(editor, shell_cmd, range));
73 }
74
75 if cmd_str == "&&" || cmd_str.starts_with("&& ") {
80 return Some(builtins::repeat_substitute_handler(editor, true, range));
81 }
82 if cmd_str == "&" || cmd_str.starts_with("& ") {
83 return Some(builtins::repeat_substitute_handler(editor, false, range));
84 }
85
86 let (name, args) = parse::split_name_args(cmd_str);
87 if name.is_empty() {
88 return handle_bare_line_number(editor, cmd_str, range);
90 }
91 let cmd = reg.resolve(name)?;
92 (cmd.run)(editor, args, range)
94}
95
96fn handle_search_address<H: hjkl_engine::Host>(
102 editor: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
103 input: &str,
104) -> ExEffect {
105 let forward = input.starts_with('/');
106 let delim = if forward { '/' } else { '?' };
107 let body = &input[1..];
108 let pat_str: String = match body.strip_suffix(delim).unwrap_or(body) {
109 "" => match editor.last_search() {
110 Some(p) if !p.is_empty() => p.to_string(),
111 _ => return ExEffect::Error("no previous search pattern".into()),
112 },
113 s => s.to_string(),
114 };
115 let s = editor.settings();
116 use hjkl_engine::search::{CaseMode, resolve_case_mode};
117 let base = CaseMode::from_options(s.ignore_case, s.smartcase);
118 let (stripped, mode) = resolve_case_mode(&pat_str, base);
119 let compile_src = if mode == CaseMode::Insensitive {
120 format!("(?i){stripped}")
121 } else {
122 stripped
123 };
124 match regex::Regex::new(&compile_src) {
125 Ok(re) => {
126 editor.set_search_pattern(Some(re));
127 if forward {
128 editor.search_advance_forward(false);
129 } else {
130 editor.search_advance_backward(true);
131 }
132 editor.ensure_cursor_in_scrolloff();
133 editor.set_last_search(Some(pat_str), forward);
134 ExEffect::Ok
135 }
136 Err(e) => ExEffect::Error(format!("bad search pattern: {e}")),
137 }
138}
139
140pub fn try_dispatch_host<Ctx>(
148 reg: &HostRegistry<Ctx>,
149 ctx: &mut Ctx,
150 input: &str,
151) -> Option<ExEffect> {
152 let input = input.trim();
153 if input.is_empty() {
154 return None;
155 }
156 let (name, args) = parse::split_name_args(input);
157 if name.is_empty() {
158 return None;
159 }
160 let cmd = reg.resolve(name)?;
161 cmd.run(ctx, args)
162}
163
164fn handle_bare_line_number<H: hjkl_engine::Host>(
170 editor: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
171 cmd_str: &str,
172 range: Option<LineRange>,
173) -> Option<ExEffect> {
174 if let Ok(line) = cmd_str.trim().parse::<usize>()
175 && range.is_none()
176 {
177 editor.goto_line(line);
178 return Some(ExEffect::Ok);
179 }
180 if let Some(r) = range
181 && cmd_str.trim().is_empty()
182 {
183 editor.goto_line(r.start_one_based());
184 return Some(ExEffect::Ok);
185 }
186 None
187}
188
189pub fn default_registry<H: hjkl_engine::Host>() -> Registry<H> {
191 let mut reg = Registry::new();
192 builtins::register_builtins(&mut reg);
193 reg
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199 use hjkl_engine::{DefaultHost, Editor, Options};
200
201 fn make_editor() -> Editor<hjkl_buffer::Buffer, DefaultHost> {
202 let buf = hjkl_buffer::Buffer::new();
203 let host = DefaultHost::new();
204 Editor::new(buf, host, Options::default())
205 }
206
207 fn make_editor_with_lines(lines: &[&str]) -> Editor<hjkl_buffer::Buffer, DefaultHost> {
208 let content = lines.join("\n");
209 let buf = hjkl_buffer::Buffer::from_str(&content);
210 let host = DefaultHost::new();
211 Editor::new(buf, host, Options::default())
212 }
213
214 fn buf_line(editor: &Editor<hjkl_buffer::Buffer, DefaultHost>, row: usize) -> String {
215 hjkl_buffer::rope_line_str(&editor.buffer().rope(), row)
216 }
217
218 fn buf_lines(editor: &Editor<hjkl_buffer::Buffer, DefaultHost>) -> Vec<String> {
219 let rope = editor.buffer().rope();
220 (0..rope.len_lines())
221 .map(|i| hjkl_buffer::rope_line_str(&rope, i))
222 .collect()
223 }
224
225 #[test]
228 fn dispatch_q_returns_quit() {
229 let reg = default_registry::<DefaultHost>();
230 let mut editor = make_editor();
231 let result = try_dispatch(®, &mut editor, "q");
232 assert_eq!(
233 result,
234 Some(ExEffect::Quit {
235 force: false,
236 save: false
237 })
238 );
239 }
240
241 #[test]
242 fn dispatch_quit_returns_quit() {
243 let reg = default_registry::<DefaultHost>();
244 let mut editor = make_editor();
245 let result = try_dispatch(®, &mut editor, "quit");
246 assert_eq!(
247 result,
248 Some(ExEffect::Quit {
249 force: false,
250 save: false
251 })
252 );
253 }
254
255 #[test]
256 fn dispatch_q_bang_returns_force_quit() {
257 let reg = default_registry::<DefaultHost>();
258 let mut editor = make_editor();
259 let result = try_dispatch(®, &mut editor, "q!");
260 assert_eq!(
261 result,
262 Some(ExEffect::Quit {
263 force: true,
264 save: false
265 })
266 );
267 }
268
269 #[test]
270 fn dispatch_nonexistent_returns_none() {
271 let reg = default_registry::<DefaultHost>();
272 let mut editor = make_editor();
273 let result = try_dispatch(®, &mut editor, "nonexistent");
274 assert_eq!(result, None);
275 }
276
277 #[test]
278 fn dispatch_empty_returns_none() {
279 let reg = default_registry::<DefaultHost>();
280 let mut editor = make_editor();
281 let result = try_dispatch(®, &mut editor, "");
282 assert_eq!(result, None);
283 }
284
285 #[test]
286 fn dispatch_whitespace_only_returns_none() {
287 let reg = default_registry::<DefaultHost>();
288 let mut editor = make_editor();
289 let result = try_dispatch(®, &mut editor, " ");
290 assert_eq!(result, None);
291 }
292
293 #[test]
296 fn dispatch_w_returns_save() {
297 let reg = default_registry::<DefaultHost>();
298 let mut editor = make_editor();
299 assert_eq!(try_dispatch(®, &mut editor, "w"), Some(ExEffect::Save));
300 }
301
302 #[test]
303 fn dispatch_write_returns_save() {
304 let reg = default_registry::<DefaultHost>();
305 let mut editor = make_editor();
306 assert_eq!(
307 try_dispatch(®, &mut editor, "write"),
308 Some(ExEffect::Save)
309 );
310 }
311
312 #[test]
313 fn dispatch_w_with_path_returns_save_as_phase_2b() {
314 let reg = default_registry::<DefaultHost>();
316 let mut editor = make_editor();
317 let result = try_dispatch(®, &mut editor, "w /tmp/foo.txt");
318 assert_eq!(result, Some(ExEffect::SaveAs("/tmp/foo.txt".into())));
319 }
320
321 #[test]
322 fn dispatch_wa_returns_save() {
323 let reg = default_registry::<DefaultHost>();
324 let mut editor = make_editor();
325 assert_eq!(try_dispatch(®, &mut editor, "wa"), Some(ExEffect::Save));
326 }
327
328 #[test]
329 fn dispatch_wall_returns_save() {
330 let reg = default_registry::<DefaultHost>();
331 let mut editor = make_editor();
332 assert_eq!(
333 try_dispatch(®, &mut editor, "wall"),
334 Some(ExEffect::Save)
335 );
336 }
337
338 #[test]
341 fn dispatch_wq_returns_quit_save() {
342 let reg = default_registry::<DefaultHost>();
343 let mut editor = make_editor();
344 assert_eq!(
345 try_dispatch(®, &mut editor, "wq"),
346 Some(ExEffect::Quit {
347 force: false,
348 save: true
349 })
350 );
351 }
352
353 #[test]
354 fn dispatch_x_returns_quit_save() {
355 let reg = default_registry::<DefaultHost>();
356 let mut editor = make_editor();
357 assert_eq!(
358 try_dispatch(®, &mut editor, "x"),
359 Some(ExEffect::Quit {
360 force: false,
361 save: true
362 })
363 );
364 }
365
366 #[test]
367 fn dispatch_wq_bang_returns_force_quit_save() {
368 let reg = default_registry::<DefaultHost>();
369 let mut editor = make_editor();
370 assert_eq!(
371 try_dispatch(®, &mut editor, "wq!"),
372 Some(ExEffect::Quit {
373 force: true,
374 save: true
375 })
376 );
377 }
378
379 #[test]
380 fn dispatch_x_bang_returns_force_quit_save() {
381 let reg = default_registry::<DefaultHost>();
382 let mut editor = make_editor();
383 assert_eq!(
384 try_dispatch(®, &mut editor, "x!"),
385 Some(ExEffect::Quit {
386 force: true,
387 save: true
388 })
389 );
390 }
391
392 #[test]
395 fn dispatch_wqa_returns_quit_save() {
396 let reg = default_registry::<DefaultHost>();
397 let mut editor = make_editor();
398 assert_eq!(
399 try_dispatch(®, &mut editor, "wqa"),
400 Some(ExEffect::Quit {
401 force: false,
402 save: true
403 })
404 );
405 }
406
407 #[test]
408 fn dispatch_wqall_returns_quit_save() {
409 let reg = default_registry::<DefaultHost>();
410 let mut editor = make_editor();
411 assert_eq!(
412 try_dispatch(®, &mut editor, "wqall"),
413 Some(ExEffect::Quit {
414 force: false,
415 save: true
416 })
417 );
418 }
419
420 #[test]
421 fn dispatch_wqa_bang_returns_quit_save() {
422 let reg = default_registry::<DefaultHost>();
423 let mut editor = make_editor();
424 assert_eq!(
425 try_dispatch(®, &mut editor, "wqa!"),
426 Some(ExEffect::Quit {
427 force: false,
428 save: true
429 })
430 );
431 }
432
433 #[test]
434 fn dispatch_wqall_bang_returns_quit_save() {
435 let reg = default_registry::<DefaultHost>();
436 let mut editor = make_editor();
437 assert_eq!(
438 try_dispatch(®, &mut editor, "wqall!"),
439 Some(ExEffect::Quit {
440 force: false,
441 save: true
442 })
443 );
444 }
445
446 #[test]
449 fn dispatch_qa_returns_quit_no_save() {
450 let reg = default_registry::<DefaultHost>();
451 let mut editor = make_editor();
452 assert_eq!(
453 try_dispatch(®, &mut editor, "qa"),
454 Some(ExEffect::Quit {
455 force: false,
456 save: false
457 })
458 );
459 }
460
461 #[test]
462 fn dispatch_qall_returns_quit_no_save() {
463 let reg = default_registry::<DefaultHost>();
464 let mut editor = make_editor();
465 assert_eq!(
466 try_dispatch(®, &mut editor, "qall"),
467 Some(ExEffect::Quit {
468 force: false,
469 save: false
470 })
471 );
472 }
473
474 #[test]
475 fn dispatch_qa_bang_returns_force_quit_no_save() {
476 let reg = default_registry::<DefaultHost>();
477 let mut editor = make_editor();
478 assert_eq!(
479 try_dispatch(®, &mut editor, "qa!"),
480 Some(ExEffect::Quit {
481 force: true,
482 save: false
483 })
484 );
485 }
486
487 #[test]
488 fn dispatch_qall_bang_returns_force_quit_no_save() {
489 let reg = default_registry::<DefaultHost>();
490 let mut editor = make_editor();
491 assert_eq!(
492 try_dispatch(®, &mut editor, "qall!"),
493 Some(ExEffect::Quit {
494 force: true,
495 save: false
496 })
497 );
498 }
499
500 #[test]
503 fn dispatch_noh_clears_search_and_returns_ok() {
504 let reg = default_registry::<DefaultHost>();
505 let mut editor = make_editor();
506 assert_eq!(try_dispatch(®, &mut editor, "noh"), Some(ExEffect::Ok));
507 }
508
509 #[test]
510 fn dispatch_nohl_returns_ok() {
511 let reg = default_registry::<DefaultHost>();
512 let mut editor = make_editor();
513 assert_eq!(try_dispatch(®, &mut editor, "nohl"), Some(ExEffect::Ok));
514 }
515
516 #[test]
517 fn dispatch_nohlsearch_returns_ok() {
518 let reg = default_registry::<DefaultHost>();
519 let mut editor = make_editor();
520 assert_eq!(
521 try_dispatch(®, &mut editor, "nohlsearch"),
522 Some(ExEffect::Ok)
523 );
524 }
525
526 #[test]
529 fn dispatch_u_returns_ok() {
530 let reg = default_registry::<DefaultHost>();
531 let mut editor = make_editor();
532 assert_eq!(try_dispatch(®, &mut editor, "u"), Some(ExEffect::Ok));
533 }
534
535 #[test]
536 fn dispatch_undo_returns_ok() {
537 let reg = default_registry::<DefaultHost>();
538 let mut editor = make_editor();
539 assert_eq!(try_dispatch(®, &mut editor, "undo"), Some(ExEffect::Ok));
540 }
541
542 #[test]
543 fn dispatch_redo_returns_ok() {
544 let reg = default_registry::<DefaultHost>();
545 let mut editor = make_editor();
546 assert_eq!(try_dispatch(®, &mut editor, "redo"), Some(ExEffect::Ok));
547 }
548
549 #[test]
551 fn dispatch_red_returns_ok() {
552 let reg = default_registry::<DefaultHost>();
553 let mut editor = make_editor();
554 assert_eq!(try_dispatch(®, &mut editor, "red"), Some(ExEffect::Ok));
555 }
556
557 #[test]
560 fn dispatch_re_resolves_to_read_no_args() {
561 let reg = default_registry::<DefaultHost>();
562 let mut editor = make_editor();
563 assert_eq!(try_dispatch(®, &mut editor, "re"), None);
565 }
566
567 #[test]
570 fn dispatch_write_with_path_returns_save_as() {
571 let reg = default_registry::<DefaultHost>();
572 let mut editor = make_editor();
573 assert_eq!(
574 try_dispatch(®, &mut editor, "write foo.txt"),
575 Some(ExEffect::SaveAs("foo.txt".into()))
576 );
577 }
578
579 #[test]
582 fn dispatch_e_with_path_returns_edit_file() {
583 let reg = default_registry::<DefaultHost>();
584 let mut editor = make_editor();
585 assert_eq!(
586 try_dispatch(®, &mut editor, "e foo.txt"),
587 Some(ExEffect::EditFile {
588 path: "foo.txt".into(),
589 force: false
590 })
591 );
592 }
593
594 #[test]
595 fn dispatch_edit_with_path_returns_edit_file() {
596 let reg = default_registry::<DefaultHost>();
597 let mut editor = make_editor();
598 assert_eq!(
599 try_dispatch(®, &mut editor, "edit src/main.rs"),
600 Some(ExEffect::EditFile {
601 path: "src/main.rs".into(),
602 force: false
603 })
604 );
605 }
606
607 #[test]
608 fn dispatch_e_no_args_returns_edit_file_empty_path() {
609 let reg = default_registry::<DefaultHost>();
610 let mut editor = make_editor();
611 assert_eq!(
613 try_dispatch(®, &mut editor, "e"),
614 Some(ExEffect::EditFile {
615 path: "".into(),
616 force: false
617 })
618 );
619 }
620
621 #[test]
622 fn dispatch_e_bang_with_path_returns_edit_file_force() {
623 let reg = default_registry::<DefaultHost>();
624 let mut editor = make_editor();
625 assert_eq!(
626 try_dispatch(®, &mut editor, "e! foo.txt"),
627 Some(ExEffect::EditFile {
628 path: "foo.txt".into(),
629 force: true
630 })
631 );
632 }
633
634 #[test]
641 fn dispatch_r_with_path_inserts_content_phase8a() {
642 let reg = default_registry::<DefaultHost>();
643 let mut editor = make_editor();
644 let tmp = tempfile::NamedTempFile::new().unwrap();
645 std::fs::write(tmp.path(), "hello\n").unwrap();
646 let path = tmp.path().to_string_lossy().to_string();
647 let result = try_dispatch(®, &mut editor, &format!("r {path}"));
648 assert_eq!(result, Some(ExEffect::Ok), "got: {result:?}");
649 let lines = buf_lines(&editor);
650 assert!(lines.contains(&"hello".to_string()), "lines: {lines:?}");
651 }
652
653 #[test]
654 fn dispatch_read_with_path_inserts_content_phase8a() {
655 let reg = default_registry::<DefaultHost>();
656 let mut editor = make_editor();
657 let tmp = tempfile::NamedTempFile::new().unwrap();
658 std::fs::write(tmp.path(), "world\n").unwrap();
659 let path = tmp.path().to_string_lossy().to_string();
660 let result = try_dispatch(®, &mut editor, &format!("read {path}"));
661 assert_eq!(result, Some(ExEffect::Ok), "got: {result:?}");
662 let lines = buf_lines(&editor);
663 assert!(lines.contains(&"world".to_string()), "lines: {lines:?}");
664 }
665
666 #[test]
667 fn dispatch_r_no_args_returns_none() {
668 let reg = default_registry::<DefaultHost>();
669 let mut editor = make_editor();
670 assert_eq!(try_dispatch(®, &mut editor, "r"), None);
671 }
672
673 #[test]
676 fn dispatch_bd_returns_buffer_delete() {
677 let reg = default_registry::<DefaultHost>();
678 let mut editor = make_editor();
679 assert_eq!(
680 try_dispatch(®, &mut editor, "bd"),
681 Some(ExEffect::BufferDelete {
682 force: false,
683 wipe: false
684 })
685 );
686 }
687
688 #[test]
689 fn dispatch_bdelete_returns_buffer_delete() {
690 let reg = default_registry::<DefaultHost>();
691 let mut editor = make_editor();
692 assert_eq!(
693 try_dispatch(®, &mut editor, "bdelete"),
694 Some(ExEffect::BufferDelete {
695 force: false,
696 wipe: false
697 })
698 );
699 }
700
701 #[test]
702 fn dispatch_bd_bang_returns_buffer_delete_force() {
703 let reg = default_registry::<DefaultHost>();
704 let mut editor = make_editor();
705 assert_eq!(
706 try_dispatch(®, &mut editor, "bd!"),
707 Some(ExEffect::BufferDelete {
708 force: true,
709 wipe: false
710 })
711 );
712 }
713
714 #[test]
715 fn dispatch_bdelete_bang_returns_buffer_delete_force() {
716 let reg = default_registry::<DefaultHost>();
717 let mut editor = make_editor();
718 assert_eq!(
719 try_dispatch(®, &mut editor, "bdelete!"),
720 Some(ExEffect::BufferDelete {
721 force: true,
722 wipe: false
723 })
724 );
725 }
726
727 #[test]
730 fn dispatch_bw_returns_buffer_wipeout() {
731 let reg = default_registry::<DefaultHost>();
732 let mut editor = make_editor();
733 assert_eq!(
734 try_dispatch(®, &mut editor, "bw"),
735 Some(ExEffect::BufferDelete {
736 force: false,
737 wipe: true
738 })
739 );
740 }
741
742 #[test]
743 fn dispatch_bwipeout_returns_buffer_wipeout() {
744 let reg = default_registry::<DefaultHost>();
745 let mut editor = make_editor();
746 assert_eq!(
747 try_dispatch(®, &mut editor, "bwipeout"),
748 Some(ExEffect::BufferDelete {
749 force: false,
750 wipe: true
751 })
752 );
753 }
754
755 #[test]
756 fn dispatch_bw_bang_returns_buffer_wipeout_force() {
757 let reg = default_registry::<DefaultHost>();
758 let mut editor = make_editor();
759 assert_eq!(
760 try_dispatch(®, &mut editor, "bw!"),
761 Some(ExEffect::BufferDelete {
762 force: true,
763 wipe: true
764 })
765 );
766 }
767
768 #[test]
769 fn dispatch_bwipeout_bang_returns_buffer_wipeout_force() {
770 let reg = default_registry::<DefaultHost>();
771 let mut editor = make_editor();
772 assert_eq!(
773 try_dispatch(®, &mut editor, "bwipeout!"),
774 Some(ExEffect::BufferDelete {
775 force: true,
776 wipe: true
777 })
778 );
779 }
780
781 #[test]
785 fn dispatch_r_resolves_to_read_not_redo() {
786 let reg = default_registry::<DefaultHost>();
787 let mut editor = make_editor();
788 let result = try_dispatch(®, &mut editor, "r /nonexistent_test_path");
790 assert!(
791 matches!(result, Some(ExEffect::Error(_))),
792 ":r of nonexistent file should be Error, got: {result:?}"
793 );
794 }
795
796 #[test]
799 fn dispatch_reg_returns_info_titled_registers() {
800 let reg = default_registry::<DefaultHost>();
801 let mut editor = make_editor();
802 let result = try_dispatch(®, &mut editor, "reg");
803 match result {
804 Some(ExEffect::InfoTitled { title, content }) => {
805 assert_eq!(title, "registers");
806 assert!(content.starts_with("--- Registers ---"), "got: {content}");
807 }
808 other => panic!("expected Some(InfoTitled), got {other:?}"),
809 }
810 }
811
812 #[test]
813 fn dispatch_registers_returns_info_titled_registers() {
814 let reg = default_registry::<DefaultHost>();
815 let mut editor = make_editor();
816 let result = try_dispatch(®, &mut editor, "registers");
817 match result {
818 Some(ExEffect::InfoTitled { title, content }) => {
819 assert_eq!(title, "registers");
820 assert!(content.starts_with("--- Registers ---"), "got: {content}");
821 }
822 other => panic!("expected Some(InfoTitled), got {other:?}"),
823 }
824 }
825
826 #[test]
829 fn dispatch_marks_returns_info_titled_marks() {
830 let reg = default_registry::<DefaultHost>();
831 let mut editor = make_editor();
832 let result = try_dispatch(®, &mut editor, "marks");
833 match result {
834 Some(ExEffect::InfoTitled { title, content }) => {
835 assert_eq!(title, "marks");
836 assert!(content.starts_with("--- Marks ---"), "got: {content}");
837 }
838 other => panic!("expected Some(InfoTitled), got {other:?}"),
839 }
840 }
841
842 #[test]
845 fn dispatch_jumps_returns_info_titled_jumps_empty() {
846 let reg = default_registry::<DefaultHost>();
847 let mut editor = make_editor();
848 let result = try_dispatch(®, &mut editor, "jumps");
849 match result {
850 Some(ExEffect::InfoTitled { title, content }) => {
851 assert_eq!(title, "jumps");
852 assert!(content.starts_with("(no jumps"), "got: {content}");
853 }
854 other => panic!("expected Some(InfoTitled), got {other:?}"),
855 }
856 }
857
858 #[test]
861 fn dispatch_changes_returns_info_titled_changes_empty() {
862 let reg = default_registry::<DefaultHost>();
863 let mut editor = make_editor();
864 let result = try_dispatch(®, &mut editor, "changes");
865 match result {
866 Some(ExEffect::InfoTitled { title, content }) => {
867 assert_eq!(title, "changes");
868 assert!(content.starts_with("(no changes"), "got: {content}");
869 }
870 other => panic!("expected Some(InfoTitled), got {other:?}"),
871 }
872 }
873
874 #[test]
877 fn dispatch_m_returns_none_below_min_prefix() {
878 let reg = default_registry::<DefaultHost>();
879 let mut editor = make_editor();
880 assert_eq!(try_dispatch(®, &mut editor, "m"), None);
882 }
883
884 #[test]
885 fn dispatch_mark_returns_none_below_min_prefix() {
886 let reg = default_registry::<DefaultHost>();
887 let mut editor = make_editor();
888 assert_eq!(try_dispatch(®, &mut editor, "mark"), None);
890 }
891
892 #[test]
893 fn dispatch_marks_full_name_returns_some() {
894 let reg = default_registry::<DefaultHost>();
895 let mut editor = make_editor();
896 assert!(try_dispatch(®, &mut editor, "marks").is_some());
897 }
898
899 #[test]
904 fn dispatch_reg_via_alias_returns_info_titled() {
905 let reg = default_registry::<DefaultHost>();
906 let mut editor = make_editor();
907 assert!(matches!(
908 try_dispatch(®, &mut editor, "reg"),
909 Some(ExEffect::InfoTitled { .. })
910 ));
911 }
912
913 #[test]
914 fn dispatch_re_still_resolves_to_read_no_args() {
915 let reg = default_registry::<DefaultHost>();
917 let mut editor = make_editor();
918 assert_eq!(try_dispatch(®, &mut editor, "re"), None);
919 }
920
921 #[test]
924 fn dispatch_bare_number_jumps_to_line() {
925 let reg = default_registry::<DefaultHost>();
927 let mut editor = make_editor_with_lines(&["a", "b", "c", "d", "e"]);
928 let result = try_dispatch(®, &mut editor, "5");
929 assert_eq!(result, Some(ExEffect::Ok));
930 assert_eq!(editor.cursor().0, 4);
931 }
932
933 #[test]
934 fn dispatch_bare_range_jumps_to_range_start() {
935 let reg = default_registry::<DefaultHost>();
937 let mut editor = make_editor_with_lines(&["a", "b", "c", "d", "e"]);
938 let result = try_dispatch(®, &mut editor, "1,5");
939 assert_eq!(result, Some(ExEffect::Ok));
940 assert_eq!(editor.cursor().0, 0);
941 }
942
943 #[test]
946 fn dispatch_d_no_range_deletes_cursor_line() {
947 let reg = default_registry::<DefaultHost>();
949 let mut editor = make_editor_with_lines(&["first", "second", "third"]);
950 let result = try_dispatch(®, &mut editor, "d");
951 assert_eq!(result, Some(ExEffect::Ok));
952 assert_eq!(buf_line(&editor, 0), "second");
954 assert_eq!(editor.buffer().row_count(), 2);
955 }
956
957 #[test]
958 fn dispatch_1d_deletes_line_1() {
959 let reg = default_registry::<DefaultHost>();
961 let mut editor = make_editor_with_lines(&["first", "second", "third"]);
962 let result = try_dispatch(®, &mut editor, "1d");
963 assert_eq!(result, Some(ExEffect::Ok));
964 assert_eq!(buf_line(&editor, 0), "second");
965 assert_eq!(editor.buffer().row_count(), 2);
966 }
967
968 #[test]
969 fn dispatch_1_2d_deletes_lines_1_and_2() {
970 let reg = default_registry::<DefaultHost>();
972 let mut editor = make_editor_with_lines(&["first", "second", "third"]);
973 let result = try_dispatch(®, &mut editor, "1,2d");
974 assert_eq!(result, Some(ExEffect::Ok));
975 assert_eq!(buf_line(&editor, 0), "third");
976 assert_eq!(editor.buffer().row_count(), 1);
977 }
978
979 #[test]
982 fn dispatch_sort_sorts_whole_buffer() {
983 let reg = default_registry::<DefaultHost>();
984 let mut editor = make_editor_with_lines(&["banana", "apple", "cherry"]);
985 let result = try_dispatch(®, &mut editor, "sort");
986 assert_eq!(result, Some(ExEffect::Ok));
987 let lines = buf_lines(&editor);
988 assert_eq!(lines, vec!["apple", "banana", "cherry"]);
989 }
990
991 #[test]
992 fn dispatch_1_3sort_sorts_range_only() {
993 let reg = default_registry::<DefaultHost>();
995 let mut editor = make_editor_with_lines(&["cherry", "apple", "banana", "zebra", "mango"]);
996 let result = try_dispatch(®, &mut editor, "1,3sort");
997 assert_eq!(result, Some(ExEffect::Ok));
998 let lines = buf_lines(&editor);
999 assert_eq!(lines[0], "apple");
1000 assert_eq!(lines[1], "banana");
1001 assert_eq!(lines[2], "cherry");
1002 assert_eq!(lines[3], "zebra");
1003 assert_eq!(lines[4], "mango");
1004 }
1005
1006 #[test]
1009 fn substitute_single_occurrence_on_cursor_line() {
1010 let reg = default_registry::<DefaultHost>();
1012 let mut editor = make_editor_with_lines(&["foo"]);
1013 let result = try_dispatch(®, &mut editor, "s/foo/bar/");
1014 assert_eq!(
1015 result,
1016 Some(ExEffect::Substituted {
1017 count: 1,
1018 lines_changed: 1
1019 })
1020 );
1021 assert_eq!(buf_line(&editor, 0), "bar");
1022 }
1023
1024 #[test]
1025 fn substitute_global_flag_replaces_all_occurrences() {
1026 let reg = default_registry::<DefaultHost>();
1028 let mut editor = make_editor_with_lines(&["foo foo foo"]);
1029 let result = try_dispatch(®, &mut editor, "s/foo/bar/g");
1030 assert_eq!(
1031 result,
1032 Some(ExEffect::Substituted {
1033 count: 3,
1034 lines_changed: 1
1035 })
1036 );
1037 assert_eq!(buf_line(&editor, 0), "bar bar bar");
1038 }
1039
1040 #[test]
1041 fn substitute_percent_range_applies_to_all_lines() {
1042 let reg = default_registry::<DefaultHost>();
1044 let mut editor = make_editor_with_lines(&["foo", "foo bar", "baz"]);
1045 let result = try_dispatch(®, &mut editor, "%s/foo/bar/g");
1046 assert_eq!(
1047 result,
1048 Some(ExEffect::Substituted {
1049 count: 2,
1050 lines_changed: 2
1051 })
1052 );
1053 assert_eq!(buf_line(&editor, 0), "bar");
1054 assert_eq!(buf_line(&editor, 1), "bar bar");
1055 assert_eq!(buf_line(&editor, 2), "baz");
1056 }
1057
1058 #[test]
1059 fn substitute_explicit_range_applied_correctly() {
1060 let reg = default_registry::<DefaultHost>();
1062 let mut editor = make_editor_with_lines(&["x", "x", "x"]);
1063 let result = try_dispatch(®, &mut editor, "1,2s/x/y/");
1064 assert_eq!(
1065 result,
1066 Some(ExEffect::Substituted {
1067 count: 2,
1068 lines_changed: 2
1069 })
1070 );
1071 assert_eq!(buf_line(&editor, 0), "y");
1072 assert_eq!(buf_line(&editor, 1), "y");
1073 assert_eq!(buf_line(&editor, 2), "x"); }
1075
1076 #[test]
1077 fn substitute_bad_regex_returns_error() {
1078 let reg = default_registry::<DefaultHost>();
1080 let mut editor = make_editor_with_lines(&["foo"]);
1081 let result = try_dispatch(®, &mut editor, "s/[bad/foo/");
1082 assert!(
1083 matches!(result, Some(ExEffect::Error(_))),
1084 "expected Some(Error(_)), got {result:?}"
1085 );
1086 }
1087
1088 #[test]
1089 fn substitute_no_body_returns_error() {
1090 let reg = default_registry::<DefaultHost>();
1092 let mut editor = make_editor_with_lines(&["foo"]);
1093 let result = try_dispatch(®, &mut editor, "s");
1094 assert!(
1095 matches!(result, Some(ExEffect::Error(_))),
1096 "expected Some(Error(_)), got {result:?}"
1097 );
1098 }
1099
1100 #[test]
1101 fn substitute_empty_pattern_no_prior_search_returns_error() {
1102 let reg = default_registry::<DefaultHost>();
1104 let mut editor = make_editor_with_lines(&["foo"]);
1105 let result = try_dispatch(®, &mut editor, "s//bar/");
1106 assert!(
1107 matches!(result, Some(ExEffect::Error(_))),
1108 "expected Some(Error(_)), got {result:?}"
1109 );
1110 }
1111
1112 #[test]
1115 fn dispatch_set_bare_returns_info() {
1116 let reg = default_registry::<DefaultHost>();
1117 let mut editor = make_editor();
1118 let result = try_dispatch(®, &mut editor, "set");
1119 assert!(
1120 matches!(result, Some(ExEffect::Info(_))),
1121 "expected Some(Info(_)), got {result:?}"
1122 );
1123 }
1124
1125 #[test]
1126 fn dispatch_se_prefix_returns_info() {
1127 let reg = default_registry::<DefaultHost>();
1129 let mut editor = make_editor();
1130 let result = try_dispatch(®, &mut editor, "se");
1131 assert!(
1132 matches!(result, Some(ExEffect::Info(_))),
1133 "expected Some(Info(_)), got {result:?}"
1134 );
1135 }
1136
1137 #[test]
1138 fn dispatch_set_number_enables_number() {
1139 let reg = default_registry::<DefaultHost>();
1140 let mut editor = make_editor();
1141 let result = try_dispatch(®, &mut editor, "set number");
1142 assert_eq!(result, Some(ExEffect::Ok));
1143 assert!(editor.settings().number);
1144 }
1145
1146 #[test]
1147 fn dispatch_set_nonumber_disables_number() {
1148 let reg = default_registry::<DefaultHost>();
1149 let mut editor = make_editor();
1150 editor.settings_mut().number = true;
1151 let result = try_dispatch(®, &mut editor, "set nonumber");
1152 assert_eq!(result, Some(ExEffect::Ok));
1153 assert!(!editor.settings().number);
1154 }
1155
1156 #[test]
1157 fn dispatch_set_tabstop_eq_4() {
1158 let reg = default_registry::<DefaultHost>();
1159 let mut editor = make_editor();
1160 let result = try_dispatch(®, &mut editor, "set tabstop=4");
1161 assert_eq!(result, Some(ExEffect::Ok));
1162 assert_eq!(editor.settings().tabstop, 4);
1163 }
1164
1165 struct TestCtx {
1168 counter: i32,
1169 }
1170
1171 struct PingCmd;
1172 impl HostCmd<TestCtx> for PingCmd {
1173 fn name(&self) -> &'static str {
1174 "ping"
1175 }
1176 fn aliases(&self) -> &'static [&'static str] {
1177 &["pn"]
1178 }
1179 fn min_prefix(&self) -> usize {
1180 2
1181 }
1182 fn run(&self, ctx: &mut TestCtx, _args: &str) -> Option<ExEffect> {
1183 ctx.counter += 1;
1184 Some(ExEffect::Ok)
1185 }
1186 }
1187
1188 struct EchoCmd;
1189 impl HostCmd<TestCtx> for EchoCmd {
1190 fn name(&self) -> &'static str {
1191 "echo"
1192 }
1193 fn min_prefix(&self) -> usize {
1194 4
1195 }
1196 fn run(&self, _ctx: &mut TestCtx, args: &str) -> Option<ExEffect> {
1197 if args.is_empty() {
1198 None
1199 } else {
1200 Some(ExEffect::Info(args.to_string()))
1201 }
1202 }
1203 }
1204
1205 fn make_host_registry() -> HostRegistry<TestCtx> {
1206 let mut reg = HostRegistry::new();
1207 reg.add(Box::new(PingCmd));
1208 reg.add(Box::new(EchoCmd));
1209 reg
1210 }
1211
1212 #[test]
1213 fn try_dispatch_host_claims_exact_name() {
1214 let reg = make_host_registry();
1215 let mut ctx = TestCtx { counter: 0 };
1216 let result = try_dispatch_host(®, &mut ctx, "ping");
1217 assert_eq!(result, Some(ExEffect::Ok));
1218 assert_eq!(ctx.counter, 1);
1219 }
1220
1221 #[test]
1222 fn try_dispatch_host_claims_alias() {
1223 let reg = make_host_registry();
1224 let mut ctx = TestCtx { counter: 0 };
1225 let result = try_dispatch_host(®, &mut ctx, "pn");
1226 assert_eq!(result, Some(ExEffect::Ok));
1227 assert_eq!(ctx.counter, 1);
1228 }
1229
1230 #[test]
1231 fn try_dispatch_host_claims_prefix() {
1232 let reg = make_host_registry();
1233 let mut ctx = TestCtx { counter: 0 };
1234 let result = try_dispatch_host(®, &mut ctx, "pi");
1236 assert_eq!(result, Some(ExEffect::Ok));
1237 }
1238
1239 #[test]
1240 fn try_dispatch_host_returns_none_on_miss() {
1241 let reg = make_host_registry();
1242 let mut ctx = TestCtx { counter: 0 };
1243 let result = try_dispatch_host(®, &mut ctx, "unknown");
1244 assert!(result.is_none());
1245 assert_eq!(ctx.counter, 0);
1246 }
1247
1248 #[test]
1249 fn try_dispatch_host_returns_none_on_empty_input() {
1250 let reg = make_host_registry();
1251 let mut ctx = TestCtx { counter: 0 };
1252 assert!(try_dispatch_host(®, &mut ctx, "").is_none());
1253 assert!(try_dispatch_host(®, &mut ctx, " ").is_none());
1254 }
1255
1256 #[test]
1257 fn try_dispatch_host_passes_args() {
1258 let reg = make_host_registry();
1259 let mut ctx = TestCtx { counter: 0 };
1260 let result = try_dispatch_host(®, &mut ctx, "echo hello world");
1261 assert_eq!(result, Some(ExEffect::Info("hello world".to_string())));
1262 }
1263
1264 #[test]
1265 fn try_dispatch_host_defers_when_command_returns_none() {
1266 let reg = make_host_registry();
1267 let mut ctx = TestCtx { counter: 0 };
1268 let result = try_dispatch_host(®, &mut ctx, "echo");
1270 assert!(result.is_none());
1271 }
1272
1273 fn noop_handler(
1276 _editor: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, DefaultHost>,
1277 _args: &str,
1278 _range: Option<crate::range::LineRange>,
1279 ) -> Option<ExEffect> {
1280 Some(ExEffect::Ok)
1281 }
1282
1283 #[test]
1284 fn collect_registry_names_includes_aliases() {
1285 let mut reg = crate::Registry::<DefaultHost>::new();
1286 reg.add(crate::ExCommand {
1287 name: "test",
1288 aliases: &["t1", "t2"],
1289 arg_kind: crate::ArgKind::None,
1290 min_prefix: 1,
1291 run: noop_handler,
1292 });
1293 let names = collect_registry_names(®);
1294 assert!(names.contains(&"test".to_string()));
1295 assert!(names.contains(&"t1".to_string()));
1296 assert!(names.contains(&"t2".to_string()));
1297 }
1298
1299 #[test]
1300 fn default_registry_includes_quit_and_q_bang() {
1301 let reg = default_registry::<DefaultHost>();
1302 let names = collect_registry_names(®);
1303 assert!(
1304 names.contains(&"quit".to_string()),
1305 "missing 'quit': {names:?}"
1306 );
1307 assert!(names.contains(&"q!".to_string()), "missing 'q!': {names:?}");
1308 }
1309
1310 #[test]
1311 fn complete_through_default_registry() {
1312 let reg = default_registry::<DefaultHost>();
1313 let names = collect_registry_names(®);
1314 let result = complete_command_from_names("qu", 2, &names);
1315 assert_eq!(result.kind, CompletionKind::Command);
1316 assert!(
1317 result.candidates.contains(&"quit".to_string()),
1318 "missing 'quit': {:?}",
1319 result.candidates
1320 );
1321 assert!(
1322 result.candidates.contains(&"quit!".to_string()),
1323 "missing 'quit!': {:?}",
1324 result.candidates
1325 );
1326 }
1327
1328 #[test]
1331 fn dispatch_foldindent_on_indented_buffer_returns_info() {
1332 let reg = default_registry::<DefaultHost>();
1333 let mut editor =
1334 make_editor_with_lines(&["fn foo() {", " let x = 1;", " let y = 2;", "}"]);
1335 let result = try_dispatch(®, &mut editor, "foldindent");
1336 match result {
1337 Some(ExEffect::Info(msg)) => {
1338 assert!(msg.contains("fold"), "got: {msg}");
1339 }
1340 other => panic!("expected Some(Info(_)), got {other:?}"),
1341 }
1342 }
1343
1344 #[test]
1345 fn dispatch_foldi_prefix_resolves_to_foldindent() {
1346 let reg = default_registry::<DefaultHost>();
1347 let mut editor = make_editor_with_lines(&["fn foo() {", " x;", "}"]);
1348 let result = try_dispatch(®, &mut editor, "foldi");
1350 assert!(result.is_some());
1351 }
1352
1353 #[test]
1354 fn dispatch_foldsyntax_no_ranges_returns_info() {
1355 let reg = default_registry::<DefaultHost>();
1356 let mut editor = make_editor_with_lines(&["fn foo() {", " bar();", "}"]);
1357 let result = try_dispatch(®, &mut editor, "foldsyntax");
1358 assert_eq!(
1359 result,
1360 Some(ExEffect::Info("no syntax block ranges available".into()))
1361 );
1362 }
1363
1364 #[test]
1367 fn dispatch_r_with_path_inserts_content() {
1368 let reg = default_registry::<DefaultHost>();
1369 let mut editor = make_editor_with_lines(&["line1", "line2"]);
1370 let tmp = tempfile::NamedTempFile::new().unwrap();
1372 std::fs::write(tmp.path(), "inserted\n").unwrap();
1373 let path = tmp.path().to_string_lossy().to_string();
1374 let result = try_dispatch(®, &mut editor, &format!("r {path}"));
1375 assert_eq!(result, Some(ExEffect::Ok), "got: {result:?}");
1376 let lines = buf_lines(&editor);
1377 assert!(lines.contains(&"inserted".to_string()), "lines: {lines:?}");
1378 }
1379
1380 #[cfg(unix)]
1381 #[test]
1382 fn dispatch_r_shell_cmd_inserts_output() {
1383 let reg = default_registry::<DefaultHost>();
1384 let mut editor = make_editor_with_lines(&["line1"]);
1385 let result = try_dispatch(®, &mut editor, "r !echo hello");
1386 assert_eq!(result, Some(ExEffect::Ok), "got: {result:?}");
1387 let lines = buf_lines(&editor);
1388 assert!(lines.contains(&"hello".to_string()), "lines: {lines:?}");
1389 }
1390
1391 #[test]
1392 fn dispatch_r_missing_file_returns_error() {
1393 let reg = default_registry::<DefaultHost>();
1394 let mut editor = make_editor_with_lines(&["line1"]);
1395 let result = try_dispatch(®, &mut editor, "r /nonexistent/path/xyz.txt");
1396 assert!(
1397 matches!(result, Some(ExEffect::Error(_))),
1398 "got: {result:?}"
1399 );
1400 }
1401
1402 #[test]
1405 fn dispatch_shell_empty_cmd_returns_error() {
1406 let reg = default_registry::<DefaultHost>();
1407 let mut editor = make_editor_with_lines(&["hello"]);
1408 let result = try_dispatch(®, &mut editor, "!");
1409 assert!(
1410 matches!(result, Some(ExEffect::Error(_))),
1411 "got: {result:?}"
1412 );
1413 }
1414
1415 #[cfg(unix)]
1416 #[test]
1417 fn dispatch_shell_no_range_returns_info() {
1418 let reg = default_registry::<DefaultHost>();
1419 let mut editor = make_editor_with_lines(&["hello"]);
1420 let result = try_dispatch(®, &mut editor, "!echo hello");
1421 match result {
1422 Some(ExEffect::Info(msg)) => assert!(msg.contains("hello"), "got: {msg}"),
1423 other => panic!("expected Some(Info(_)), got {other:?}"),
1424 }
1425 }
1426
1427 #[cfg(unix)]
1428 #[test]
1429 fn dispatch_shell_range_filter() {
1430 let reg = default_registry::<DefaultHost>();
1431 let mut editor = make_editor_with_lines(&["banana", "apple", "cherry"]);
1432 let result = try_dispatch(®, &mut editor, "1,3!sort");
1433 assert_eq!(result, Some(ExEffect::Ok), "got: {result:?}");
1434 let lines = buf_lines(&editor);
1435 assert_eq!(lines[0], "apple");
1436 assert_eq!(lines[1], "banana");
1437 assert_eq!(lines[2], "cherry");
1438 }
1439
1440 #[test]
1443 fn dispatch_g_d_deletes_matching_lines() {
1444 let reg = default_registry::<DefaultHost>();
1445 let mut editor = make_editor_with_lines(&["foo", "bar", "foo"]);
1446 let result = try_dispatch(®, &mut editor, "g/foo/d");
1447 assert!(
1448 matches!(result, Some(ExEffect::Substituted { count: 2, .. })),
1449 "got: {result:?}"
1450 );
1451 let lines = buf_lines(&editor);
1452 assert!(!lines.contains(&"foo".to_string()), "lines: {lines:?}");
1453 }
1454
1455 #[test]
1456 fn dispatch_v_d_deletes_non_matching_lines() {
1457 let reg = default_registry::<DefaultHost>();
1458 let mut editor = make_editor_with_lines(&["foo", "bar", "baz"]);
1459 let result = try_dispatch(®, &mut editor, "v/foo/d");
1460 assert!(
1461 matches!(result, Some(ExEffect::Substituted { .. })),
1462 "got: {result:?}"
1463 );
1464 let lines = buf_lines(&editor);
1465 assert!(!lines.contains(&"bar".to_string()));
1466 assert!(!lines.contains(&"baz".to_string()));
1467 }
1468
1469 #[test]
1470 fn dispatch_global_full_name_works() {
1471 let reg = default_registry::<DefaultHost>();
1472 let mut editor = make_editor_with_lines(&["foo", "bar"]);
1473 let result = try_dispatch(®, &mut editor, "global/foo/d");
1474 assert!(matches!(result, Some(ExEffect::Substituted { .. })));
1475 }
1476
1477 #[test]
1478 fn dispatch_vglobal_full_name_works() {
1479 let reg = default_registry::<DefaultHost>();
1480 let mut editor = make_editor_with_lines(&["foo", "bar"]);
1481 let result = try_dispatch(®, &mut editor, "vglobal/foo/d");
1482 assert!(matches!(result, Some(ExEffect::Substituted { .. })));
1483 }
1484
1485 #[test]
1488 fn dispatch_search_forward_jumps_to_line() {
1489 let reg = default_registry::<DefaultHost>();
1490 let mut editor = make_editor_with_lines(&["apple", "banana", "cherry"]);
1491 let result = try_dispatch(®, &mut editor, "/banana");
1492 assert_eq!(result, Some(ExEffect::Ok), "got: {result:?}");
1493 assert_eq!(editor.cursor().0, 1, "cursor should be on row 1 (banana)");
1494 }
1495
1496 #[test]
1497 fn dispatch_search_backward_jumps_to_line() {
1498 let reg = default_registry::<DefaultHost>();
1499 let mut editor = make_editor_with_lines(&["apple", "banana", "cherry"]);
1500 editor.goto_line(3);
1502 let result = try_dispatch(®, &mut editor, "?apple");
1503 assert_eq!(result, Some(ExEffect::Ok), "got: {result:?}");
1504 assert_eq!(editor.cursor().0, 0, "cursor should be on row 0 (apple)");
1505 }
1506
1507 #[test]
1508 fn dispatch_search_bad_pattern_returns_error() {
1509 let reg = default_registry::<DefaultHost>();
1510 let mut editor = make_editor_with_lines(&["foo"]);
1511 let result = try_dispatch(®, &mut editor, "/[bad");
1512 assert!(
1513 matches!(result, Some(ExEffect::Error(_))),
1514 "got: {result:?}"
1515 );
1516 }
1517
1518 #[test]
1519 fn dispatch_search_empty_no_prior_returns_error() {
1520 let reg = default_registry::<DefaultHost>();
1521 let mut editor = make_editor_with_lines(&["foo"]);
1522 let result = try_dispatch(®, &mut editor, "/");
1523 assert!(
1524 matches!(result, Some(ExEffect::Error(_))),
1525 "got: {result:?}"
1526 );
1527 }
1528
1529 #[test]
1532 fn dispatch_amp_no_prior_sub_returns_error() {
1533 let reg = default_registry::<DefaultHost>();
1534 let mut editor = make_editor_with_lines(&["foo"]);
1535 let result = try_dispatch(®, &mut editor, "&");
1536 assert!(
1537 matches!(result, Some(ExEffect::Error(_))),
1538 "expected Error, got {result:?}"
1539 );
1540 }
1541
1542 #[test]
1543 fn dispatch_amp_repeats_last_sub_on_current_line() {
1544 let reg = default_registry::<DefaultHost>();
1545 let mut editor = make_editor_with_lines(&["foo", "foo"]);
1546 let r1 = try_dispatch(®, &mut editor, "s/foo/bar/");
1547 assert!(
1548 matches!(r1, Some(ExEffect::Substituted { count: 1, .. })),
1549 "got: {r1:?}"
1550 );
1551 assert_eq!(buf_line(&editor, 0), "bar");
1552 editor.goto_line(2);
1553 let r2 = try_dispatch(®, &mut editor, "&");
1554 assert!(
1555 matches!(r2, Some(ExEffect::Substituted { count: 1, .. })),
1556 "expected Substituted(1), got {r2:?}"
1557 );
1558 assert_eq!(buf_line(&editor, 1), "bar");
1559 }
1560
1561 #[test]
1562 fn dispatch_amp_amp_keeps_global_flag() {
1563 let reg = default_registry::<DefaultHost>();
1564 let mut editor = make_editor_with_lines(&["x x x", "x x x"]);
1565 try_dispatch(®, &mut editor, "s/x/y/g").unwrap();
1566 assert_eq!(buf_line(&editor, 0), "y y y");
1567 editor.goto_line(2);
1568 let result = try_dispatch(®, &mut editor, "&&");
1569 assert!(
1570 matches!(result, Some(ExEffect::Substituted { count: 3, .. })),
1571 "expected Substituted(3), got {result:?}"
1572 );
1573 assert_eq!(buf_line(&editor, 1), "y y y");
1574 }
1575
1576 #[test]
1577 fn dispatch_amp_drops_global_flag() {
1578 let reg = default_registry::<DefaultHost>();
1579 let mut editor = make_editor_with_lines(&["x x x", "x x x"]);
1580 try_dispatch(®, &mut editor, "s/x/y/g").unwrap();
1581 assert_eq!(buf_line(&editor, 0), "y y y");
1582 editor.goto_line(2);
1583 let result = try_dispatch(®, &mut editor, "&");
1584 assert!(
1585 matches!(result, Some(ExEffect::Substituted { count: 1, .. })),
1586 "expected Substituted(1) (first only), got {result:?}"
1587 );
1588 assert_eq!(buf_line(&editor, 1), "y x x");
1589 }
1590
1591 #[test]
1592 fn dispatch_percent_amp_repeats_on_whole_buffer() {
1593 let reg = default_registry::<DefaultHost>();
1594 let mut editor = make_editor_with_lines(&["foo", "foo", "bar"]);
1595 try_dispatch(®, &mut editor, "s/foo/baz/").unwrap();
1596 assert_eq!(buf_line(&editor, 0), "baz");
1597 let result = try_dispatch(®, &mut editor, "%&");
1598 assert!(
1599 matches!(result, Some(ExEffect::Substituted { count: 1, .. })),
1600 "expected Substituted(1), got {result:?}"
1601 );
1602 assert_eq!(buf_line(&editor, 1), "baz");
1603 assert_eq!(buf_line(&editor, 2), "bar");
1604 }
1605}