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