1use lattice_protocol::edit::Edit;
25use lattice_protocol::position::{Position, Range as ProtoRange};
26use std::sync::Arc;
27
28use crate::cancel::CheckCancelled;
29use crate::effect::{Effect, YankKind};
30use crate::error::CommandError;
31use crate::registry::{
32 CommandRegistry, FindKind, MotionContext, MotionId, MotionResult, MotionSpec, OperatorContext,
33 OperatorId, OperatorSpec, TextObjectContext, TextObjectId, TextObjectSpec,
34};
35
36pub fn populate(registry: &mut CommandRegistry) -> Builtins {
78 let word_forward = registry.register_motion(
79 "motion:word-forward",
80 "Move to the start of the next word.",
81 MotionSpec {
82 curswant: crate::registry::CurswantEffect::default(),
83 jump: false,
84 exclusive: true,
85 apply: Arc::new(motion_word_forward),
86 args_schema: vec![],
87 },
88 );
89 let word_backward = registry.register_motion(
90 "motion:word-backward",
91 "Move to the start of the previous word (vim's `b`).",
92 MotionSpec {
93 curswant: crate::registry::CurswantEffect::default(),
94 jump: false,
95 exclusive: true,
96 apply: Arc::new(motion_word_backward),
97 args_schema: vec![],
98 },
99 );
100 let word_end = registry.register_motion(
101 "motion:word-end",
102 "Move to the last byte of the current or next word (vim's `e`). Inclusive.",
103 MotionSpec {
104 curswant: crate::registry::CurswantEffect::default(),
105 jump: false,
106 exclusive: false,
107 apply: Arc::new(motion_word_end),
108 args_schema: vec![],
109 },
110 );
111 let first_non_blank = registry.register_motion(
112 "motion:first-non-blank",
113 "Move to the first non-whitespace byte of the current line (vim's `^`).",
114 MotionSpec {
115 curswant: crate::registry::CurswantEffect::default(),
116 jump: false,
117 exclusive: true,
121 apply: Arc::new(motion_first_non_blank),
122 args_schema: vec![],
123 },
124 );
125 let find_char_forward = registry.register_motion(
126 "motion:find-char-forward",
127 "Move to the next occurrence of `args.char` on the current line (vim's `f`).",
128 MotionSpec {
129 curswant: crate::registry::CurswantEffect::default(),
130 jump: false,
131 exclusive: false,
132 apply: Arc::new(motion_find_char_forward),
133 args_schema: vec![],
134 },
135 );
136 let find_char_backward = registry.register_motion(
137 "motion:find-char-backward",
138 "Move to the previous occurrence of `args.char` on the current line (vim's `F`).",
139 MotionSpec {
140 curswant: crate::registry::CurswantEffect::default(),
141 jump: false,
142 exclusive: true,
148 apply: Arc::new(motion_find_char_backward),
149 args_schema: vec![],
150 },
151 );
152 let till_char_forward = registry.register_motion(
153 "motion:till-char-forward",
154 "Move to one byte before the next occurrence of `args.char` (vim's `t`).",
155 MotionSpec {
156 curswant: crate::registry::CurswantEffect::default(),
157 jump: false,
158 exclusive: false,
159 apply: Arc::new(motion_till_char_forward),
160 args_schema: vec![],
161 },
162 );
163 let till_char_backward = registry.register_motion(
164 "motion:till-char-backward",
165 "Move to one byte after the previous occurrence of `args.char` (vim's `T`).",
166 MotionSpec {
167 curswant: crate::registry::CurswantEffect::default(),
168 jump: false,
169 exclusive: true,
171 apply: Arc::new(motion_till_char_backward),
172 args_schema: vec![],
173 },
174 );
175 let big_word_forward = registry.register_motion(
176 "motion:big-word-forward",
177 "Move to the start of the next WORD (vim's `W` -- whitespace-delimited).",
178 MotionSpec {
179 curswant: crate::registry::CurswantEffect::default(),
180 jump: false,
181 exclusive: true,
182 apply: Arc::new(motion_big_word_forward),
183 args_schema: vec![],
184 },
185 );
186 registry.tag_word_forward_motion(word_forward);
191 registry.tag_word_forward_motion(big_word_forward);
192
193 let big_word_backward = registry.register_motion(
194 "motion:big-word-backward",
195 "Move to the start of the previous WORD (vim's `B`).",
196 MotionSpec {
197 curswant: crate::registry::CurswantEffect::default(),
198 jump: false,
199 exclusive: true,
200 apply: Arc::new(motion_big_word_backward),
201 args_schema: vec![],
202 },
203 );
204 let big_word_end = registry.register_motion(
205 "motion:big-word-end",
206 "Move to the last byte of the current or next WORD (vim's `E`).",
207 MotionSpec {
208 curswant: crate::registry::CurswantEffect::default(),
209 jump: false,
210 exclusive: false,
211 apply: Arc::new(motion_big_word_end),
212 args_schema: vec![],
213 },
214 );
215 let paragraph_forward = registry.register_motion(
216 "motion:paragraph-forward",
217 "Move to the next paragraph boundary -- the next blank line at or after the cursor (vim's `}`).",
218 MotionSpec {
219 curswant: crate::registry::CurswantEffect::default(),
220 jump: true,
221 exclusive: true,
223 apply: Arc::new(motion_paragraph_forward),
224 args_schema: vec![],
225 },
226 );
227 let paragraph_backward = registry.register_motion(
228 "motion:paragraph-backward",
229 "Move to the previous paragraph boundary (vim's `{`).",
230 MotionSpec {
231 curswant: crate::registry::CurswantEffect::default(),
232 jump: true,
233 exclusive: true,
235 apply: Arc::new(motion_paragraph_backward),
236 args_schema: vec![],
237 },
238 );
239 let sentence_forward = registry.register_motion(
240 "motion:sentence-forward",
241 "Move to the start of the next sentence (vim's `)`).",
242 MotionSpec {
243 curswant: crate::registry::CurswantEffect::default(),
244 jump: true,
245 exclusive: true,
247 apply: Arc::new(motion_sentence_forward),
248 args_schema: vec![],
249 },
250 );
251 let sentence_backward = registry.register_motion(
252 "motion:sentence-backward",
253 "Move to the start of the previous sentence (vim's `(`).",
254 MotionSpec {
255 curswant: crate::registry::CurswantEffect::default(),
256 jump: true,
257 exclusive: true,
259 apply: Arc::new(motion_sentence_backward),
260 args_schema: vec![],
261 },
262 );
263 let char_left = registry.register_motion(
264 "motion:char-left",
265 "Move one byte to the left within the current line.",
266 MotionSpec {
267 curswant: crate::registry::CurswantEffect::default(),
268 jump: false,
269 exclusive: true,
272 apply: Arc::new(motion_char_left),
273 args_schema: vec![],
274 },
275 );
276 let char_right = registry.register_motion(
277 "motion:char-right",
278 "Move one byte to the right within the current line.",
279 MotionSpec {
280 curswant: crate::registry::CurswantEffect::default(),
281 jump: false,
282 exclusive: true,
283 apply: Arc::new(motion_char_right),
284 args_schema: vec![],
285 },
286 );
287 let line_up = registry.register_motion(
288 "motion:line-up",
289 "Move one line up.",
290 MotionSpec {
291 curswant: crate::registry::CurswantEffect::Keep,
292 jump: false,
293 exclusive: true,
304 apply: Arc::new(motion_line_up),
305 args_schema: vec![],
306 },
307 );
308 let line_down = registry.register_motion(
309 "motion:line-down",
310 "Move one line down.",
311 MotionSpec {
312 curswant: crate::registry::CurswantEffect::Keep,
313 jump: false,
314 exclusive: false,
315 apply: Arc::new(motion_line_down),
316 args_schema: vec![],
317 },
318 );
319 let line_start = registry.register_motion(
320 "motion:line-start",
321 "Move to the first byte of the current line.",
322 MotionSpec {
323 curswant: crate::registry::CurswantEffect::default(),
324 jump: false,
325 exclusive: true,
327 apply: Arc::new(motion_line_start),
328 args_schema: vec![],
329 },
330 );
331 let line_end = registry.register_motion(
332 "motion:line-end",
333 "Move to the last byte of the current line.",
334 MotionSpec {
335 curswant: crate::registry::CurswantEffect::PinToEnd,
336 jump: false,
337 exclusive: false,
338 apply: Arc::new(motion_line_end),
339 args_schema: vec![],
340 },
341 );
342 let goto_first_line = registry.register_motion(
343 "motion:goto-first-line",
344 "Jump to the first line of the buffer (vim's `gg`).",
345 MotionSpec {
346 curswant: crate::registry::CurswantEffect::default(),
347 jump: true,
348 exclusive: false,
349 apply: Arc::new(motion_goto_first_line),
350 args_schema: vec![],
351 },
352 );
353 let goto_last_line = registry.register_motion(
354 "motion:goto-last-line",
355 "Jump to the last line of the buffer (vim's `G`).",
356 MotionSpec {
357 curswant: crate::registry::CurswantEffect::default(),
358 jump: true,
359 exclusive: false,
360 apply: Arc::new(motion_goto_last_line),
361 args_schema: vec![],
362 },
363 );
364
365 let delete = registry.register_operator(
366 "operator:delete",
367 "Delete the bytes covered by the target range; yank to the unnamed register.",
368 OperatorSpec {
369 repeatable: true,
370 apply: Arc::new(operator_delete),
371 args_schema: vec![],
372 blockwise_per_row: true,
373 post_motion_char: false,
374 },
375 );
376 let change = registry.register_operator(
377 "operator:change",
378 "Delete the bytes covered by the target range and enter Insert mode (vim's `c`).",
379 OperatorSpec {
380 repeatable: true,
381 apply: Arc::new(operator_change),
382 args_schema: vec![],
383 blockwise_per_row: true,
384 post_motion_char: false,
385 },
386 );
387 let yank = registry.register_operator(
388 "operator:yank",
389 "Copy the bytes covered by the target range into the named register without modifying the buffer (vim's `y`).",
390 OperatorSpec {
391 repeatable: true,
392 apply: Arc::new(operator_yank),
393 args_schema: vec![],
394 blockwise_per_row: true,
395 post_motion_char: false,
396 },
397 );
398 let indent_left = registry.register_operator(
399 "operator:indent-left",
400 "Strip leading indentation (4 spaces or one tab) from each line in the range (vim's `<`).",
401 OperatorSpec {
402 repeatable: true,
403 apply: Arc::new(operator_indent_left),
404 args_schema: vec![],
405 blockwise_per_row: false,
408 post_motion_char: false,
409 },
410 );
411 let reindent = registry.register_operator(
412 "operator:reindent",
413 "Reindent each line in the range to the depth the syntax tree implies, \
414 adjusting leading whitespace only (vim's `=`).",
415 OperatorSpec {
416 repeatable: true,
417 apply: Arc::new(operator_reindent),
418 args_schema: vec![],
419 blockwise_per_row: false,
423 post_motion_char: false,
424 },
425 );
426 let reformat = registry.register_operator(
427 "operator:reformat",
428 "Run the buffer's `format.reformat` chain (language server, or an external \
429 formatter) over the range — the operator form of `:format` (`g=`).",
430 OperatorSpec {
431 repeatable: true,
432 apply: Arc::new(operator_reformat),
433 args_schema: vec![],
434 blockwise_per_row: false,
437 post_motion_char: false,
438 },
439 );
440 let create_fold = registry.register_operator(
441 "operator:create-fold",
442 "Create a closed fold over the whole lines the {motion} or selection \
443 covers (vim's `zf`).",
444 OperatorSpec {
445 repeatable: false,
447 apply: Arc::new(operator_create_fold),
448 args_schema: vec![],
449 blockwise_per_row: false,
451 post_motion_char: false,
452 },
453 );
454 let reflow = registry.register_operator(
455 "operator:reflow",
456 "Reflow each paragraph in the range to `textwidth`, keeping indentation \
457 and any comment leader (vim's `gq` and `gw`, which are one operator here).",
458 OperatorSpec {
459 repeatable: true,
460 apply: Arc::new(operator_reflow),
461 args_schema: vec![],
462 blockwise_per_row: false,
466 post_motion_char: false,
467 },
468 );
469 let indent_right = registry.register_operator(
470 "operator:indent-right",
471 "Prepend 4 spaces to each line in the range (vim's `>`).",
472 OperatorSpec {
473 repeatable: true,
474 apply: Arc::new(operator_indent_right),
475 args_schema: vec![],
476 blockwise_per_row: false,
477 post_motion_char: false,
478 },
479 );
480 let upper = registry.register_operator(
481 "operator:upper",
482 "Uppercase ASCII letters in the range (vim's `gU`).",
483 OperatorSpec {
484 repeatable: true,
485 apply: Arc::new(operator_upper),
486 args_schema: vec![],
487 blockwise_per_row: false,
488 post_motion_char: false,
489 },
490 );
491 let lower = registry.register_operator(
492 "operator:lower",
493 "Lowercase ASCII letters in the range (vim's `gu`).",
494 OperatorSpec {
495 repeatable: true,
496 apply: Arc::new(operator_lower),
497 args_schema: vec![],
498 blockwise_per_row: false,
499 post_motion_char: false,
500 },
501 );
502 let toggle_case = registry.register_operator(
503 "operator:toggle-case",
504 "Toggle case of ASCII letters in the range (vim's `g~`).",
505 OperatorSpec {
506 repeatable: true,
507 apply: Arc::new(operator_toggle_case),
508 args_schema: vec![],
509 blockwise_per_row: false,
510 post_motion_char: false,
511 },
512 );
513 let search = registry.register_operator(
514 "operator:search",
515 "Project-search for the text spanned by the motion / text object (Lattice's `g/`).",
516 OperatorSpec {
517 repeatable: false,
518 apply: Arc::new(operator_search),
519 args_schema: vec![],
520 blockwise_per_row: false,
521 post_motion_char: false,
522 },
523 );
524 let replace_char = registry.register_operator(
525 "operator:replace-char",
526 "Overwrite each non-newline char in the range with the captured char (vim's `r{char}` and Visual `r`).",
527 OperatorSpec {
528 repeatable: true,
529 apply: Arc::new(operator_replace_char),
530 args_schema: vec![],
534 blockwise_per_row: true,
537 post_motion_char: false,
538 },
539 );
540
541 let inner_sentence = registry.register_text_object(
542 "text-object:inner-sentence",
543 "Inner sentence -- text up to the next .!? that ends a sentence (vim's `is`).",
544 TextObjectSpec {
545 apply: Arc::new(text_object_inner_sentence),
546 args_schema: vec![],
547 },
548 );
549 let around_sentence = registry.register_text_object(
550 "text-object:around-sentence",
551 "Around sentence -- inner_sentence plus trailing whitespace (vim's `as`).",
552 TextObjectSpec {
553 apply: Arc::new(text_object_around_sentence),
554 args_schema: vec![],
555 },
556 );
557
558 let inner_paragraph = registry.register_text_object(
559 "text-object:inner-paragraph",
560 "Inner paragraph -- the run of non-blank lines containing the cursor (vim's `ip`).",
561 TextObjectSpec {
562 apply: Arc::new(text_object_inner_paragraph),
563 args_schema: vec![],
564 },
565 );
566 let around_paragraph = registry.register_text_object(
567 "text-object:around-paragraph",
568 "Around paragraph -- inner_paragraph plus trailing blank lines (vim's `ap`).",
569 TextObjectSpec {
570 apply: Arc::new(text_object_around_paragraph),
571 args_schema: vec![],
572 },
573 );
574
575 let inner_word = registry.register_text_object(
576 "text-object:inner-word",
577 "Inner word -- alphanum + underscore run containing the cursor (vim's `iw`).",
578 TextObjectSpec {
579 apply: Arc::new(text_object_inner_word),
580 args_schema: vec![],
581 },
582 );
583 let around_word = registry.register_text_object(
584 "text-object:around-word",
585 "Around word -- inner_word plus trailing whitespace (vim's `aw`).",
586 TextObjectSpec {
587 apply: Arc::new(text_object_around_word),
588 args_schema: vec![],
589 },
590 );
591 let inner_quote_double = registry.register_text_object(
592 "text-object:inner-quote-double",
593 "Inner double-quoted string -- text between the surrounding `\"` chars (vim's `i\"`).",
594 TextObjectSpec {
595 apply: Arc::new(|ctx| text_object_inner_quote(ctx, '"')),
596 args_schema: vec![],
597 },
598 );
599 let around_quote_double = registry.register_text_object(
600 "text-object:around-quote-double",
601 "Around double-quoted string -- includes the surrounding `\"` chars (vim's `a\"`).",
602 TextObjectSpec {
603 apply: Arc::new(|ctx| text_object_around_quote(ctx, '"')),
604 args_schema: vec![],
605 },
606 );
607 let inner_quote_single = registry.register_text_object(
608 "text-object:inner-quote-single",
609 "Inner single-quoted string (vim's `i'`).",
610 TextObjectSpec {
611 apply: Arc::new(|ctx| text_object_inner_quote(ctx, '\'')),
612 args_schema: vec![],
613 },
614 );
615 let around_quote_single = registry.register_text_object(
616 "text-object:around-quote-single",
617 "Around single-quoted string (vim's `a'`).",
618 TextObjectSpec {
619 apply: Arc::new(|ctx| text_object_around_quote(ctx, '\'')),
620 args_schema: vec![],
621 },
622 );
623 let inner_quote_backtick = registry.register_text_object(
624 "text-object:inner-quote-backtick",
625 "Inner backtick-quoted string (vim's ``i` ``).",
626 TextObjectSpec {
627 apply: Arc::new(|ctx| text_object_inner_quote(ctx, '`')),
628 args_schema: vec![],
629 },
630 );
631 let around_quote_backtick = registry.register_text_object(
632 "text-object:around-quote-backtick",
633 "Around backtick-quoted string (vim's ``a` ``).",
634 TextObjectSpec {
635 apply: Arc::new(|ctx| text_object_around_quote(ctx, '`')),
636 args_schema: vec![],
637 },
638 );
639 let inner_paren = registry.register_text_object(
640 "text-object:inner-paren",
641 "Inside the innermost enclosing `()` pair (vim's `i(`).",
642 TextObjectSpec {
643 apply: Arc::new(|ctx| text_object_inner_brackets(ctx, '(', ')')),
644 args_schema: vec![],
645 },
646 );
647 let around_paren = registry.register_text_object(
648 "text-object:around-paren",
649 "Around the innermost enclosing `()` pair, including the brackets (vim's `a(`).",
650 TextObjectSpec {
651 apply: Arc::new(|ctx| text_object_around_brackets(ctx, '(', ')')),
652 args_schema: vec![],
653 },
654 );
655 let inner_bracket = registry.register_text_object(
656 "text-object:inner-bracket",
657 "Inside the innermost enclosing `[]` pair (vim's `i[`).",
658 TextObjectSpec {
659 apply: Arc::new(|ctx| text_object_inner_brackets(ctx, '[', ']')),
660 args_schema: vec![],
661 },
662 );
663 let around_bracket = registry.register_text_object(
664 "text-object:around-bracket",
665 "Around the innermost enclosing `[]` pair, including the brackets (vim's `a[`).",
666 TextObjectSpec {
667 apply: Arc::new(|ctx| text_object_around_brackets(ctx, '[', ']')),
668 args_schema: vec![],
669 },
670 );
671 let inner_tag = registry.register_text_object(
672 "text-object:inner-tag",
673 "Inside the innermost enclosing XML/HTML tag pair (vim's `it`).",
674 TextObjectSpec {
675 apply: Arc::new(text_object_inner_tag),
676 args_schema: vec![],
677 },
678 );
679 let around_tag = registry.register_text_object(
680 "text-object:around-tag",
681 "Around the innermost enclosing XML/HTML tag pair, including the tags (vim's `at`).",
682 TextObjectSpec {
683 apply: Arc::new(text_object_around_tag),
684 args_schema: vec![],
685 },
686 );
687
688 let inner_brace = registry.register_text_object(
689 "text-object:inner-brace",
690 "Inside the innermost enclosing `{}` pair (vim's `i{`).",
691 TextObjectSpec {
692 apply: Arc::new(|ctx| text_object_inner_brackets(ctx, '{', '}')),
693 args_schema: vec![],
694 },
695 );
696 let around_brace = registry.register_text_object(
697 "text-object:around-brace",
698 "Around the innermost enclosing `{}` pair, including the brackets (vim's `a{`).",
699 TextObjectSpec {
700 apply: Arc::new(|ctx| text_object_around_brackets(ctx, '{', '}')),
701 args_schema: vec![],
702 },
703 );
704 let inner_big_word = registry.register_text_object(
705 "text-object:inner-big-word",
706 "Inner WORD -- whitespace-delimited run containing the cursor (vim's `iW`).",
707 TextObjectSpec {
708 apply: Arc::new(text_object_inner_big_word),
709 args_schema: vec![],
710 },
711 );
712 let around_big_word = registry.register_text_object(
713 "text-object:around-big-word",
714 "Around WORD -- inner WORD plus trailing whitespace (vim's `aW`).",
715 TextObjectSpec {
716 apply: Arc::new(text_object_around_big_word),
717 args_schema: vec![],
718 },
719 );
720 let inner_angle = registry.register_text_object(
721 "text-object:inner-angle",
722 "Inside the innermost enclosing `<>` pair (vim's `i<`).",
723 TextObjectSpec {
724 apply: Arc::new(|ctx| text_object_inner_brackets(ctx, '<', '>')),
725 args_schema: vec![],
726 },
727 );
728 let around_angle = registry.register_text_object(
729 "text-object:around-angle",
730 "Around the innermost enclosing `<>` pair, including the brackets (vim's `a<`).",
731 TextObjectSpec {
732 apply: Arc::new(|ctx| text_object_around_brackets(ctx, '<', '>')),
733 args_schema: vec![],
734 },
735 );
736
737 let inner_comment = registry.register_text_object(
740 "text-object:inner-comment",
741 "Inner comment -- the comment text with the first line's leader stripped (`iC`).",
742 TextObjectSpec {
743 apply: Arc::new(text_object_inner_comment),
744 args_schema: vec![],
745 },
746 );
747 let around_comment = registry.register_text_object(
748 "text-object:around-comment",
749 "A comment -- the contiguous run of comment lines including the markers (`aC`).",
750 TextObjectSpec {
751 apply: Arc::new(text_object_around_comment),
752 args_schema: vec![],
753 },
754 );
755
756 let find_repeat = registry.register_motion(
757 "motion:find-repeat",
758 "Repeat the last `f` / `F` / `t` / `T` in the same direction (vim's `;`).",
759 MotionSpec {
760 curswant: crate::registry::CurswantEffect::default(),
761 jump: false,
764 exclusive: false,
770 apply: Arc::new(motion_find_repeat),
771 args_schema: vec![],
772 },
773 );
774 let find_repeat_reverse = registry.register_motion(
775 "motion:find-repeat-reverse",
776 "Repeat the last `f` / `F` / `t` / `T` in the opposite direction (vim's `,`).",
777 MotionSpec {
778 curswant: crate::registry::CurswantEffect::default(),
779 jump: false,
780 exclusive: false,
781 apply: Arc::new(motion_find_repeat_reverse),
782 args_schema: vec![],
783 },
784 );
785
786 let match_pair = registry.register_motion(
787 "motion:match-pair",
788 "Move to the bracket matching the first one at or after the cursor on this line (vim's `%`).",
789 MotionSpec {
790 curswant: crate::registry::CurswantEffect::default(),
791 jump: true,
794 exclusive: false,
798 apply: Arc::new(motion_match_pair),
799 args_schema: vec![],
800 },
801 );
802 let goto_next_fold = registry.register_motion(
806 "motion:goto-next-fold",
807 "Move to the start of the next fold; a closed fold counts as one (vim's `zj`).",
808 MotionSpec {
809 curswant: crate::registry::CurswantEffect::default(),
810 jump: false,
811 exclusive: true,
812 apply: Arc::new(motion_goto_next_fold),
813 args_schema: vec![],
814 },
815 );
816 let goto_prev_fold = registry.register_motion(
817 "motion:goto-prev-fold",
818 "Move to the end of the previous fold; a closed fold counts as one (vim's `zk`).",
819 MotionSpec {
820 curswant: crate::registry::CurswantEffect::default(),
821 jump: false,
822 exclusive: true,
823 apply: Arc::new(motion_goto_prev_fold),
824 args_schema: vec![],
825 },
826 );
827 let search_spec =
834 |apply: fn(&MotionContext) -> Result<MotionResult, CommandError>| MotionSpec {
835 curswant: crate::registry::CurswantEffect::default(),
836 jump: true,
837 exclusive: true,
838 apply: Arc::new(apply),
839 args_schema: vec![],
840 };
841 let search_next = registry.register_motion(
842 "motion:search-next",
843 "Repeat the last search in its own direction (vim's `n`).",
844 search_spec(motion_search_next),
845 );
846 let search_prev = registry.register_motion(
847 "motion:search-prev",
848 "Repeat the last search in the opposite direction (vim's `N`).",
849 search_spec(motion_search_prev),
850 );
851 let search_word_forward = registry.register_motion(
852 "motion:search-word-forward",
853 "Search forward for the word under the cursor (vim's `*`).",
854 search_spec(motion_search_next),
855 );
856 let search_word_backward = registry.register_motion(
857 "motion:search-word-backward",
858 "Search backward for the word under the cursor (vim's `#`).",
859 search_spec(motion_search_next),
860 );
861 let search_word_forward_partial = registry.register_motion(
866 "motion:search-word-forward-partial",
867 "Search forward for the word under the cursor, not only whole words (vim's `g*`).",
868 search_spec(motion_search_next),
869 );
870 let search_word_backward_partial = registry.register_motion(
871 "motion:search-word-backward-partial",
872 "Search backward for the word under the cursor, not only whole words (vim's `g#`).",
873 search_spec(motion_search_next),
874 );
875 let display_line_down = registry.register_motion(
885 "motion:display-line-down",
886 "Move one display row down, keeping the screen column (vim's `gj`).",
887 MotionSpec {
888 jump: false,
889 exclusive: true,
890 curswant: crate::registry::CurswantEffect::default(),
896 apply: Arc::new(motion_display_line_down),
897 args_schema: vec![],
898 },
899 );
900 let display_line_up = registry.register_motion(
901 "motion:display-line-up",
902 "Move one display row up, keeping the screen column (vim's `gk`).",
903 MotionSpec {
904 jump: false,
905 exclusive: true,
906 curswant: crate::registry::CurswantEffect::default(),
909 apply: Arc::new(motion_display_line_up),
910 args_schema: vec![],
911 },
912 );
913 let display_line_start = registry.register_motion(
914 "motion:display-line-start",
915 "Move to the first column of the display row (vim's `g0`).",
916 MotionSpec {
917 jump: false,
918 exclusive: true,
919 curswant: crate::registry::CurswantEffect::default(),
920 apply: Arc::new(motion_display_line_start),
921 args_schema: vec![],
922 },
923 );
924 let display_line_end = registry.register_motion(
925 "motion:display-line-end",
926 "Move to the last column of the display row (vim's `g$`).",
927 MotionSpec {
928 jump: false,
929 exclusive: false,
930 curswant: crate::registry::CurswantEffect::default(),
931 apply: Arc::new(motion_display_line_end),
932 args_schema: vec![],
933 },
934 );
935 let mark_line = registry.register_motion(
936 "motion:mark-line",
937 "Go to the first non-blank of the line of mark `args.char` (vim's `'`).",
938 MotionSpec {
939 curswant: crate::registry::CurswantEffect::default(),
940 jump: true,
941 exclusive: false,
942 apply: Arc::new(motion_mark_line),
943 args_schema: vec![],
944 },
945 );
946 let mark_exact = registry.register_motion(
947 "motion:mark-exact",
948 "Go to the exact position of mark `args.char` (vim's `` ` ``).",
949 MotionSpec {
950 curswant: crate::registry::CurswantEffect::default(),
951 jump: true,
952 exclusive: true,
953 apply: Arc::new(motion_mark_exact),
954 args_schema: vec![],
955 },
956 );
957 let viewport_spec =
961 |apply: fn(&MotionContext) -> Result<MotionResult, CommandError>| MotionSpec {
962 curswant: crate::registry::CurswantEffect::default(),
963 jump: true,
964 exclusive: false,
965 apply: Arc::new(apply),
966 args_schema: vec![],
967 };
968 let viewport_top = registry.register_motion(
969 "motion:viewport-top",
970 "Go to the `count`-th line from the top of the window (vim's `H`).",
971 viewport_spec(motion_viewport_top),
972 );
973 let viewport_middle = registry.register_motion(
974 "motion:viewport-middle",
975 "Go to the middle line of the window (vim's `M`).",
976 viewport_spec(motion_viewport_middle),
977 );
978 let viewport_bottom = registry.register_motion(
979 "motion:viewport-bottom",
980 "Go to the `count`-th line from the bottom of the window (vim's `L`).",
981 viewport_spec(motion_viewport_bottom),
982 );
983
984 Builtins {
985 word_forward,
986 word_backward,
987 word_end,
988 first_non_blank,
989 find_char_forward,
990 find_char_backward,
991 till_char_forward,
992 till_char_backward,
993 big_word_forward,
994 big_word_backward,
995 big_word_end,
996 paragraph_forward,
997 paragraph_backward,
998 sentence_forward,
999 sentence_backward,
1000 char_left,
1001 char_right,
1002 line_up,
1003 line_down,
1004 line_start,
1005 line_end,
1006 goto_first_line,
1007 goto_last_line,
1008 find_repeat,
1009 find_repeat_reverse,
1010 match_pair,
1011 goto_next_fold,
1012 goto_prev_fold,
1013 search_next,
1014 search_prev,
1015 search_word_forward,
1016 search_word_backward,
1017 search_word_forward_partial,
1018 search_word_backward_partial,
1019 mark_line,
1020 display_line_down,
1021 display_line_up,
1022 display_line_start,
1023 display_line_end,
1024 mark_exact,
1025 viewport_top,
1026 viewport_middle,
1027 viewport_bottom,
1028 delete,
1029 change,
1030 yank,
1031 indent_left,
1032 reindent,
1033 reflow,
1034 reformat,
1035 create_fold,
1036 indent_right,
1037 upper,
1038 lower,
1039 toggle_case,
1040 search,
1041 replace_char,
1042 inner_paragraph,
1043 around_paragraph,
1044 inner_sentence,
1045 around_sentence,
1046 inner_word,
1047 around_word,
1048 inner_quote_double,
1049 around_quote_double,
1050 inner_quote_single,
1051 around_quote_single,
1052 inner_quote_backtick,
1053 around_quote_backtick,
1054 inner_paren,
1055 around_paren,
1056 inner_bracket,
1057 around_bracket,
1058 inner_brace,
1059 around_brace,
1060 inner_tag,
1061 around_tag,
1062 inner_big_word,
1063 around_big_word,
1064 inner_angle,
1065 around_angle,
1066 inner_comment,
1067 around_comment,
1068 }
1069}
1070
1071#[derive(Debug, Clone, Copy, Default)]
1081pub struct Builtins {
1082 pub word_forward: MotionId,
1084 pub word_backward: MotionId,
1086 pub word_end: MotionId,
1088 pub first_non_blank: MotionId,
1090 pub find_char_forward: MotionId,
1092 pub find_char_backward: MotionId,
1094 pub till_char_forward: MotionId,
1096 pub till_char_backward: MotionId,
1098 pub big_word_forward: MotionId,
1100 pub big_word_backward: MotionId,
1102 pub big_word_end: MotionId,
1104 pub paragraph_forward: MotionId,
1106 pub paragraph_backward: MotionId,
1108 pub sentence_forward: MotionId,
1110 pub sentence_backward: MotionId,
1112 pub char_left: MotionId,
1114 pub char_right: MotionId,
1116 pub line_up: MotionId,
1118 pub line_down: MotionId,
1120 pub line_start: MotionId,
1122 pub line_end: MotionId,
1124 pub goto_first_line: MotionId,
1126 pub goto_last_line: MotionId,
1128 pub find_repeat: MotionId,
1132 pub find_repeat_reverse: MotionId,
1134 pub match_pair: MotionId,
1138 pub goto_next_fold: MotionId,
1140 pub goto_prev_fold: MotionId,
1142 pub search_next: MotionId,
1144 pub search_prev: MotionId,
1146 pub search_word_forward: MotionId,
1148 pub search_word_backward: MotionId,
1150 pub search_word_forward_partial: MotionId,
1153 pub search_word_backward_partial: MotionId,
1155 pub mark_line: MotionId,
1157 pub display_line_down: MotionId,
1159 pub display_line_up: MotionId,
1161 pub display_line_start: MotionId,
1163 pub display_line_end: MotionId,
1165 pub mark_exact: MotionId,
1167 pub viewport_top: MotionId,
1169 pub viewport_middle: MotionId,
1171 pub viewport_bottom: MotionId,
1173 pub delete: OperatorId,
1175 pub change: OperatorId,
1177 pub yank: OperatorId,
1179 pub indent_left: OperatorId,
1181 pub indent_right: OperatorId,
1183 pub reindent: OperatorId,
1185 pub reflow: OperatorId,
1189 pub reformat: OperatorId,
1192 pub create_fold: OperatorId,
1194 pub upper: OperatorId,
1196 pub lower: OperatorId,
1198 pub toggle_case: OperatorId,
1200 pub search: OperatorId,
1202 pub replace_char: OperatorId,
1204 pub inner_paragraph: TextObjectId,
1206 pub around_paragraph: TextObjectId,
1208 pub inner_sentence: TextObjectId,
1210 pub around_sentence: TextObjectId,
1212 pub inner_word: TextObjectId,
1214 pub around_word: TextObjectId,
1216 pub inner_quote_double: TextObjectId,
1218 pub around_quote_double: TextObjectId,
1220 pub inner_quote_single: TextObjectId,
1222 pub around_quote_single: TextObjectId,
1224 pub inner_quote_backtick: TextObjectId,
1226 pub around_quote_backtick: TextObjectId,
1228 pub inner_paren: TextObjectId,
1230 pub around_paren: TextObjectId,
1232 pub inner_bracket: TextObjectId,
1234 pub around_bracket: TextObjectId,
1236 pub inner_brace: TextObjectId,
1238 pub around_brace: TextObjectId,
1240 pub inner_tag: TextObjectId,
1242 pub around_tag: TextObjectId,
1244 pub inner_big_word: TextObjectId,
1246 pub around_big_word: TextObjectId,
1248 pub inner_angle: TextObjectId,
1250 pub around_angle: TextObjectId,
1252 pub inner_comment: TextObjectId,
1254 pub around_comment: TextObjectId,
1256}
1257
1258fn motion_word_forward(ctx: &MotionContext) -> Result<MotionResult, CommandError> {
1261 let text = ctx.buffer.as_string();
1267 let bytes = text.as_bytes();
1268 let total = bytes.len();
1269 let count = ctx.count.get().max(1);
1270 let mut idx = ctx
1271 .buffer
1272 .position_to_byte(ctx.from)
1273 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
1274
1275 for _ in 0..count {
1276 if idx >= total {
1277 break;
1278 }
1279 let start_class = word_class(bytes[idx]);
1282 if start_class != WordClass::Blank {
1283 while idx < total && word_class(bytes[idx]) == start_class {
1284 idx += 1;
1285 }
1286 }
1287 while idx < total && word_class(bytes[idx]) == WordClass::Blank {
1289 idx += 1;
1290 }
1291 }
1292
1293 let target = ctx
1296 .buffer
1297 .byte_to_position(idx.min(total))
1298 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
1299
1300 Ok(MotionResult {
1301 curswant: None,
1302 target,
1303 linewise: false,
1304 exclusive: None,
1305 notice: None,
1306 })
1307}
1308
1309fn is_word_byte(b: u8) -> bool {
1310 b.is_ascii_alphanumeric() || b == b'_'
1311}
1312
1313fn is_blank_byte(b: u8) -> bool {
1314 b == b' ' || b == b'\t'
1315}
1316
1317fn is_big_word_byte(b: u8) -> bool {
1320 !b.is_ascii_whitespace()
1321}
1322
1323#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1330enum WordClass {
1331 Blank,
1332 Word,
1333 Punct,
1334}
1335
1336fn word_class(b: u8) -> WordClass {
1337 if is_word_byte(b) {
1338 WordClass::Word
1339 } else if b.is_ascii_whitespace() {
1340 WordClass::Blank
1341 } else {
1342 WordClass::Punct
1343 }
1344}
1345
1346fn big_word_class(b: u8) -> WordClass {
1351 if b.is_ascii_whitespace() {
1352 WordClass::Blank
1353 } else {
1354 WordClass::Word
1355 }
1356}
1357
1358fn motion_word_backward(ctx: &MotionContext) -> Result<MotionResult, CommandError> {
1361 let text = ctx.buffer.as_string();
1362 let bytes = text.as_bytes();
1363 let count = ctx.count.get().max(1);
1364 let mut idx = ctx
1365 .buffer
1366 .position_to_byte(ctx.from)
1367 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
1368
1369 for _ in 0..count {
1370 if idx == 0 {
1371 break;
1372 }
1373 idx -= 1;
1378 while idx > 0 && word_class(bytes[idx]) == WordClass::Blank {
1379 idx -= 1;
1380 }
1381 let c = word_class(bytes[idx]);
1382 while idx > 0 && word_class(bytes[idx - 1]) == c {
1383 idx -= 1;
1384 }
1385 }
1386
1387 let target = ctx
1388 .buffer
1389 .byte_to_position(idx)
1390 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
1391 Ok(MotionResult {
1392 curswant: None,
1393 target,
1394 linewise: false,
1395 exclusive: None,
1396 notice: None,
1397 })
1398}
1399
1400fn motion_word_end(ctx: &MotionContext) -> Result<MotionResult, CommandError> {
1403 let text = ctx.buffer.as_string();
1404 let bytes = text.as_bytes();
1405 let total = bytes.len();
1406 let count = ctx.count.get().max(1);
1407 let mut idx = ctx
1408 .buffer
1409 .position_to_byte(ctx.from)
1410 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
1411
1412 for _ in 0..count {
1413 if idx >= total.saturating_sub(1) {
1414 break;
1415 }
1416 idx += 1;
1420 while idx < total && word_class(bytes[idx]) == WordClass::Blank {
1421 idx += 1;
1422 }
1423 if idx >= total {
1424 idx = total.saturating_sub(1);
1425 break;
1426 }
1427 let c = word_class(bytes[idx]);
1428 while idx + 1 < total && word_class(bytes[idx + 1]) == c {
1429 idx += 1;
1430 }
1431 }
1432
1433 let target = ctx
1434 .buffer
1435 .byte_to_position(idx)
1436 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
1437 Ok(MotionResult {
1438 curswant: None,
1439 target,
1440 linewise: false,
1441 exclusive: None,
1442 notice: None,
1443 })
1444}
1445
1446fn line_is_blank(text: &str, line: u32) -> bool {
1454 text.split_inclusive('\n')
1455 .nth(line as usize)
1456 .map(|l| l.trim_end_matches('\n').trim().is_empty())
1457 .unwrap_or(true)
1458}
1459
1460fn buffer_last_line(text: &str) -> u32 {
1461 let lc = text.split_inclusive('\n').count() as u32;
1462 if text.ends_with('\n') {
1463 lc.saturating_sub(2)
1464 } else {
1465 lc.saturating_sub(1)
1466 }
1467}
1468
1469fn motion_match_pair(ctx: &MotionContext) -> Result<MotionResult, CommandError> {
1488 let unmoved = Ok(MotionResult {
1489 curswant: None,
1490 target: ctx.from,
1491 linewise: false,
1492 exclusive: None,
1493 notice: None,
1494 });
1495 let text = ctx.buffer.as_string();
1496 let bytes = text.as_bytes();
1497 let Ok(cursor_byte) = ctx.buffer.position_to_byte(ctx.from) else {
1498 return unmoved;
1499 };
1500
1501 let mut idx = cursor_byte;
1503 let mut found = None;
1504 while idx < bytes.len() && bytes[idx] != b'\n' {
1505 if matches!(bytes[idx], b'(' | b')' | b'[' | b']' | b'{' | b'}') {
1506 found = Some((idx, bytes[idx]));
1507 break;
1508 }
1509 idx += 1;
1510 }
1511 let Some((start, b)) = found else {
1512 return unmoved;
1513 };
1514 let (open, close, forward) = match b {
1515 b'(' => (b'(', b')', true),
1516 b')' => (b'(', b')', false),
1517 b'[' => (b'[', b']', true),
1518 b']' => (b'[', b']', false),
1519 b'{' => (b'{', b'}', true),
1520 b'}' => (b'{', b'}', false),
1521 _ => return unmoved,
1522 };
1523
1524 let target = if forward {
1525 scan_forward_for_match(bytes, start, open, close)
1526 } else {
1527 scan_backward_for_match(bytes, start, open, close)
1528 };
1529 match target.and_then(|t| ctx.buffer.byte_to_position(t).ok()) {
1530 Some(pos) => Ok(MotionResult {
1531 curswant: None,
1532 target: pos,
1533 linewise: false,
1534 exclusive: None,
1535 notice: None,
1536 }),
1537 None => unmoved,
1538 }
1539}
1540
1541fn scan_forward_for_match(bytes: &[u8], from: usize, open: u8, close: u8) -> Option<usize> {
1545 let mut depth = 0i32;
1546 let mut i = from;
1547 loop {
1548 if i >= bytes.len() {
1549 return None;
1550 }
1551 let b = bytes[i];
1552 if b == open {
1553 depth += 1;
1554 } else if b == close {
1555 depth -= 1;
1556 if depth == 0 {
1557 return Some(i);
1558 }
1559 }
1560 i += 1;
1561 }
1562}
1563
1564fn scan_backward_for_match(bytes: &[u8], from: usize, open: u8, close: u8) -> Option<usize> {
1567 let mut depth = 0i32;
1568 let mut i = from;
1569 loop {
1570 let b = bytes[i];
1571 if b == close {
1572 depth += 1;
1573 } else if b == open {
1574 depth -= 1;
1575 if depth == 0 {
1576 return Some(i);
1577 }
1578 }
1579 if i == 0 {
1580 return None;
1581 }
1582 i -= 1;
1583 }
1584}
1585
1586fn motion_viewport(
1591 ctx: &MotionContext,
1592 pos: crate::app_effect::ViewportPos,
1593) -> Result<MotionResult, CommandError> {
1594 let viewport = ctx.viewport.ok_or(CommandError::MotionFailed)?;
1595 let line = viewport
1596 .viewport_line(pos, ctx.count.get())
1597 .ok_or(CommandError::MotionFailed)?;
1598 let line = scrolloff_adjusted(ctx, viewport, pos, line).min(last_addressable_line(ctx.buffer));
1599 Ok(MotionResult {
1600 curswant: None,
1601 target: startofline_target(ctx, line),
1603 linewise: true,
1604 exclusive: None,
1605 notice: None,
1606 })
1607}
1608
1609fn scrolloff_adjusted(
1621 ctx: &MotionContext,
1622 viewport: &dyn crate::registry::ViewportResolver,
1623 pos: crate::app_effect::ViewportPos,
1624 line: u32,
1625) -> u32 {
1626 use crate::app_effect::ViewportPos;
1627 if ctx.scrolloff == 0 || ctx.operator_pending {
1628 return line;
1629 }
1630 match pos {
1634 ViewportPos::Top => match viewport.viewport_line(ViewportPos::Top, 1) {
1635 Some(top) if top > 0 => line.max(top + ctx.scrolloff),
1637 _ => line,
1638 },
1639 ViewportPos::Bottom => match viewport.viewport_line(ViewportPos::Bottom, 1) {
1640 Some(bottom) if bottom < last_addressable_line(ctx.buffer) => {
1641 line.min(bottom.saturating_sub(ctx.scrolloff))
1642 }
1643 _ => line,
1644 },
1645 ViewportPos::Middle => line,
1646 }
1647}
1648
1649fn motion_viewport_top(ctx: &MotionContext) -> Result<MotionResult, CommandError> {
1650 motion_viewport(ctx, crate::app_effect::ViewportPos::Top)
1651}
1652
1653fn motion_viewport_middle(ctx: &MotionContext) -> Result<MotionResult, CommandError> {
1654 motion_viewport(ctx, crate::app_effect::ViewportPos::Middle)
1655}
1656
1657fn motion_viewport_bottom(ctx: &MotionContext) -> Result<MotionResult, CommandError> {
1658 motion_viewport(ctx, crate::app_effect::ViewportPos::Bottom)
1659}
1660
1661fn mark_position(ctx: &MotionContext) -> Result<Position, CommandError> {
1665 let name = match &ctx.args {
1666 crate::args::Args::Char(c) if c.is_ascii_alphanumeric() => *c,
1667 crate::args::Args::Char(_) => {
1668 return Err(CommandError::User("E78: Unknown mark".to_string()));
1669 }
1670 _ => return Err(CommandError::InvalidArgs("'/` require Args::Char")),
1671 };
1672 let Some(pos) = ctx.marks.and_then(|marks| marks.mark(name)) else {
1673 return Err(CommandError::User("E20: Mark not set".to_string()));
1674 };
1675 let line = pos.line.min(last_addressable_line(ctx.buffer));
1676 Ok(Position::new(
1677 line,
1678 pos.byte.min(line_byte_len(ctx.buffer, line)),
1679 ))
1680}
1681
1682fn motion_mark_line(ctx: &MotionContext) -> Result<MotionResult, CommandError> {
1685 let mark = mark_position(ctx)?;
1686 let line_text = ctx.buffer.line(mark.line).unwrap_or_default();
1687 let bytes = line_text.as_bytes();
1688 let mut col = 0usize;
1689 while col < bytes.len() && is_blank_byte(bytes[col]) {
1690 col += 1;
1691 }
1692 let col = (col as u32).min(line_byte_len(ctx.buffer, mark.line));
1693 Ok(MotionResult {
1694 curswant: None,
1695 target: Position::new(mark.line, col),
1696 linewise: true,
1697 exclusive: None,
1698 notice: None,
1699 })
1700}
1701
1702fn motion_mark_exact(ctx: &MotionContext) -> Result<MotionResult, CommandError> {
1704 Ok(MotionResult {
1705 curswant: None,
1706 target: mark_position(ctx)?,
1707 linewise: false,
1708 exclusive: None,
1709 notice: None,
1710 })
1711}
1712
1713fn motion_search(ctx: &MotionContext, reverse: bool) -> Result<MotionResult, CommandError> {
1719 use crate::modal::SearchDirection;
1720 let Some(last) = ctx.last_search else {
1721 return Err(CommandError::User(
1722 "E35: no previous regular expression".to_string(),
1723 ));
1724 };
1725 let direction = match (last.direction, reverse) {
1726 (SearchDirection::Forward, false) | (SearchDirection::Backward, true) => {
1727 SearchDirection::Forward
1728 }
1729 (SearchDirection::Backward, false) | (SearchDirection::Forward, true) => {
1730 SearchDirection::Backward
1731 }
1732 };
1733 let regex = fancy_regex::Regex::new(&last.pattern)
1734 .map_err(|e| CommandError::User(format!("regex: {e}")))?;
1735 let core_direction = match direction {
1736 SearchDirection::Forward => lattice_core::search::Direction::Forward,
1737 SearchDirection::Backward => lattice_core::search::Direction::Backward,
1738 };
1739 let mut at = ctx.from;
1740 let mut wrapped = false;
1741 for _ in 0..ctx.count.get().max(1) {
1742 let from = step_for_search(ctx.buffer, at, direction);
1743 match lattice_core::search::find(ctx.buffer, ®ex, from, core_direction, ctx.cancel)? {
1744 Some(hit) => {
1745 at = hit.range.start;
1746 wrapped |= hit.wrapped;
1747 }
1748 None => {
1749 return Err(CommandError::User(format!(
1750 "E486: Pattern not found: {}",
1751 last.pattern
1752 )));
1753 }
1754 }
1755 }
1756 Ok(MotionResult {
1757 curswant: None,
1758 target: at,
1759 linewise: false,
1760 exclusive: None,
1761 notice: wrapped.then_some(match direction {
1762 SearchDirection::Forward => crate::registry::MotionNotice::SearchHitBottom,
1763 SearchDirection::Backward => crate::registry::MotionNotice::SearchHitTop,
1764 }),
1765 })
1766}
1767
1768fn motion_search_next(ctx: &MotionContext) -> Result<MotionResult, CommandError> {
1769 motion_search(ctx, false)
1770}
1771
1772fn motion_search_prev(ctx: &MotionContext) -> Result<MotionResult, CommandError> {
1773 motion_search(ctx, true)
1774}
1775
1776fn step_for_search(
1782 buffer: &lattice_core::Buffer,
1783 p: Position,
1784 direction: crate::modal::SearchDirection,
1785) -> Position {
1786 let line = buffer.line(p.line).unwrap_or_default();
1787 match direction {
1788 crate::modal::SearchDirection::Forward => {
1789 if p.byte < line_byte_len(buffer, p.line) {
1790 let mut byte = p.byte as usize + 1;
1791 while byte < line.len() && !line.is_char_boundary(byte) {
1792 byte += 1;
1793 }
1794 Position::new(p.line, byte as u32)
1795 } else if p.line < last_addressable_line(buffer) {
1796 Position::new(p.line + 1, 0)
1797 } else {
1798 p
1799 }
1800 }
1801 crate::modal::SearchDirection::Backward => {
1802 if p.byte > 0 {
1803 let mut byte = (p.byte as usize - 1).min(line.len());
1804 while byte > 0 && !line.is_char_boundary(byte) {
1805 byte -= 1;
1806 }
1807 Position::new(p.line, byte as u32)
1808 } else if p.line > 0 {
1809 Position::new(p.line - 1, line_byte_len(buffer, p.line - 1))
1810 } else {
1811 p
1812 }
1813 }
1814 }
1815}
1816
1817fn motion_goto_fold(ctx: &MotionContext, forward: bool) -> Result<MotionResult, CommandError> {
1825 let mut target = ctx.from;
1826 if let Some(folds) = ctx.fold_resolver {
1827 let mut line = ctx.from.line;
1828 for _ in 0..ctx.count.get().max(1) {
1829 match folds.fold_edge(line, forward) {
1830 Some(edge) => {
1831 line = edge;
1832 target = Position::new(edge, 0);
1833 }
1834 None => break,
1835 }
1836 }
1837 }
1838 Ok(MotionResult {
1839 curswant: None,
1840 target,
1841 linewise: false,
1842 exclusive: None,
1843 notice: None,
1844 })
1845}
1846
1847fn motion_goto_next_fold(ctx: &MotionContext) -> Result<MotionResult, CommandError> {
1848 motion_goto_fold(ctx, true)
1849}
1850
1851fn motion_goto_prev_fold(ctx: &MotionContext) -> Result<MotionResult, CommandError> {
1852 motion_goto_fold(ctx, false)
1853}
1854
1855fn motion_paragraph_forward(ctx: &MotionContext) -> Result<MotionResult, CommandError> {
1856 let text = ctx.buffer.as_string();
1857 let count = ctx.count.get().max(1);
1858 let last = buffer_last_line(&text);
1859 let mut line = ctx.from.line;
1860 for _ in 0..count {
1861 if line > last {
1863 line = last;
1864 break;
1865 }
1866 line = line.saturating_add(1);
1867 while line <= last && !line_is_blank(&text, line) {
1869 line = line.saturating_add(1);
1870 }
1871 if line > last {
1872 line = last;
1873 break;
1874 }
1875 }
1876 Ok(MotionResult {
1877 curswant: None,
1878 target: Position::new(line, 0),
1879 linewise: false,
1880 exclusive: None,
1881 notice: None,
1882 })
1883}
1884
1885fn motion_paragraph_backward(ctx: &MotionContext) -> Result<MotionResult, CommandError> {
1886 let text = ctx.buffer.as_string();
1887 let count = ctx.count.get().max(1);
1888 let mut line = ctx.from.line;
1889 for _ in 0..count {
1890 if line == 0 {
1891 break;
1892 }
1893 line = line.saturating_sub(1);
1894 while line > 0 && !line_is_blank(&text, line) {
1895 line = line.saturating_sub(1);
1896 }
1897 }
1898 Ok(MotionResult {
1899 curswant: None,
1900 target: Position::new(line, 0),
1901 linewise: false,
1902 exclusive: None,
1903 notice: None,
1904 })
1905}
1906
1907fn is_sentence_end(b: u8) -> bool {
1914 matches!(b, b'.' | b'!' | b'?')
1915}
1916
1917fn next_sentence_start(bytes: &[u8], from: usize) -> Option<usize> {
1922 let mut i = from;
1923 while i + 1 < bytes.len() {
1924 if is_sentence_end(bytes[i]) && bytes[i + 1].is_ascii_whitespace() {
1925 let mut j = i + 2;
1927 while j < bytes.len() && bytes[j].is_ascii_whitespace() {
1928 j += 1;
1929 }
1930 if j < bytes.len() {
1931 return Some(j);
1932 }
1933 return None;
1934 }
1935 i += 1;
1936 }
1937 None
1938}
1939
1940fn prev_sentence_start(bytes: &[u8], from: usize) -> usize {
1944 if from == 0 {
1945 return 0;
1946 }
1947 let mut i = from.saturating_sub(1);
1949 while i > 0 {
1950 if is_sentence_end(bytes[i]) && i + 1 < bytes.len() && bytes[i + 1].is_ascii_whitespace() {
1951 let mut j = i + 2;
1953 while j < bytes.len() && bytes[j].is_ascii_whitespace() {
1954 j += 1;
1955 }
1956 if j < from {
1957 return j;
1958 }
1959 }
1960 i -= 1;
1961 }
1962 0
1963}
1964
1965fn motion_sentence_forward(ctx: &MotionContext) -> Result<MotionResult, CommandError> {
1966 let text = ctx.buffer.as_string();
1967 let bytes = text.as_bytes();
1968 let count = ctx.count.get().max(1);
1969 let mut idx = ctx
1970 .buffer
1971 .position_to_byte(ctx.from)
1972 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
1973 for _ in 0..count {
1974 match next_sentence_start(bytes, idx) {
1975 Some(next) => idx = next,
1976 None => {
1977 idx = bytes.len().saturating_sub(1);
1978 break;
1979 }
1980 }
1981 }
1982 let target = ctx
1983 .buffer
1984 .byte_to_position(idx)
1985 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
1986 Ok(MotionResult {
1987 curswant: None,
1988 target,
1989 linewise: false,
1990 exclusive: None,
1991 notice: None,
1992 })
1993}
1994
1995fn motion_sentence_backward(ctx: &MotionContext) -> Result<MotionResult, CommandError> {
1996 let text = ctx.buffer.as_string();
1997 let bytes = text.as_bytes();
1998 let count = ctx.count.get().max(1);
1999 let mut idx = ctx
2000 .buffer
2001 .position_to_byte(ctx.from)
2002 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
2003 for _ in 0..count {
2004 let next = prev_sentence_start(bytes, idx);
2005 if next == idx {
2006 break;
2007 }
2008 idx = next;
2009 }
2010 let target = ctx
2011 .buffer
2012 .byte_to_position(idx)
2013 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
2014 Ok(MotionResult {
2015 curswant: None,
2016 target,
2017 linewise: false,
2018 exclusive: None,
2019 notice: None,
2020 })
2021}
2022
2023fn text_object_inner_sentence(ctx: &TextObjectContext) -> Result<ProtoRange, CommandError> {
2024 let text = ctx.buffer.as_string();
2025 let bytes = text.as_bytes();
2026 let cursor = ctx
2027 .buffer
2028 .position_to_byte(ctx.at)
2029 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
2030 let start_byte = prev_sentence_start(bytes, cursor.saturating_add(1));
2031 let mut end_byte = cursor;
2032 while end_byte < bytes.len() {
2033 if is_sentence_end(bytes[end_byte]) {
2034 break;
2036 }
2037 end_byte += 1;
2038 }
2039 let start_pos = ctx
2040 .buffer
2041 .byte_to_position(start_byte)
2042 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
2043 let end_pos = ctx
2044 .buffer
2045 .byte_to_position(end_byte)
2046 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
2047 Ok(ProtoRange::new(start_pos, end_pos))
2048}
2049
2050fn text_object_around_sentence(ctx: &TextObjectContext) -> Result<ProtoRange, CommandError> {
2051 let inner = text_object_inner_sentence(ctx)?;
2052 let text = ctx.buffer.as_string();
2053 let bytes = text.as_bytes();
2054 let inner_end_byte = ctx
2055 .buffer
2056 .position_to_byte(inner.end)
2057 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
2058 let mut end = inner_end_byte;
2060 if end < bytes.len() && is_sentence_end(bytes[end]) {
2061 end += 1;
2062 }
2063 while end < bytes.len() && bytes[end].is_ascii_whitespace() {
2064 end += 1;
2065 }
2066 let end_pos = ctx
2067 .buffer
2068 .byte_to_position(end)
2069 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
2070 Ok(ProtoRange::new(inner.start, end_pos))
2071}
2072
2073fn motion_big_word_forward(ctx: &MotionContext) -> Result<MotionResult, CommandError> {
2076 let text = ctx.buffer.as_string();
2077 let bytes = text.as_bytes();
2078 let total = bytes.len();
2079 let count = ctx.count.get().max(1);
2080 let mut idx = ctx
2081 .buffer
2082 .position_to_byte(ctx.from)
2083 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
2084
2085 for _ in 0..count {
2086 if idx >= total {
2087 break;
2088 }
2089 while idx < total && is_big_word_byte(bytes[idx]) {
2091 idx += 1;
2092 }
2093 while idx < total && !is_big_word_byte(bytes[idx]) {
2095 idx += 1;
2096 }
2097 }
2098 if idx >= total {
2099 idx = total.saturating_sub(1);
2100 }
2101 let target = ctx
2102 .buffer
2103 .byte_to_position(idx)
2104 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
2105 Ok(MotionResult {
2106 curswant: None,
2107 target,
2108 linewise: false,
2109 exclusive: None,
2110 notice: None,
2111 })
2112}
2113
2114fn motion_big_word_backward(ctx: &MotionContext) -> Result<MotionResult, CommandError> {
2115 let text = ctx.buffer.as_string();
2116 let bytes = text.as_bytes();
2117 let count = ctx.count.get().max(1);
2118 let mut idx = ctx
2119 .buffer
2120 .position_to_byte(ctx.from)
2121 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
2122
2123 for _ in 0..count {
2124 if idx == 0 {
2125 break;
2126 }
2127 idx -= 1;
2128 while idx > 0 && !is_big_word_byte(bytes[idx]) {
2129 idx -= 1;
2130 }
2131 while idx > 0 && is_big_word_byte(bytes[idx - 1]) {
2132 idx -= 1;
2133 }
2134 }
2135 let target = ctx
2136 .buffer
2137 .byte_to_position(idx)
2138 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
2139 Ok(MotionResult {
2140 curswant: None,
2141 target,
2142 linewise: false,
2143 exclusive: None,
2144 notice: None,
2145 })
2146}
2147
2148fn motion_big_word_end(ctx: &MotionContext) -> Result<MotionResult, CommandError> {
2149 let text = ctx.buffer.as_string();
2150 let bytes = text.as_bytes();
2151 let total = bytes.len();
2152 let count = ctx.count.get().max(1);
2153 let mut idx = ctx
2154 .buffer
2155 .position_to_byte(ctx.from)
2156 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
2157
2158 for _ in 0..count {
2159 if idx >= total.saturating_sub(1) {
2160 break;
2161 }
2162 idx += 1;
2163 while idx < total && !is_big_word_byte(bytes[idx]) {
2164 idx += 1;
2165 }
2166 if idx >= total {
2167 idx = total.saturating_sub(1);
2168 break;
2169 }
2170 while idx + 1 < total && is_big_word_byte(bytes[idx + 1]) {
2171 idx += 1;
2172 }
2173 }
2174 let target = ctx
2175 .buffer
2176 .byte_to_position(idx)
2177 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
2178 Ok(MotionResult {
2179 curswant: None,
2180 target,
2181 linewise: false,
2182 exclusive: None,
2183 notice: None,
2184 })
2185}
2186
2187fn args_to_char(args: &crate::args::Args) -> Result<char, CommandError> {
2195 match args {
2196 crate::args::Args::Char(c) => Ok(*c),
2197 _ => Err(CommandError::InvalidArgs("f/F/t/T require Args::Char")),
2198 }
2199}
2200
2201fn line_text(text: &str, line: u32) -> &str {
2202 text.split_inclusive('\n')
2203 .nth(line as usize)
2204 .map(|l| l.trim_end_matches('\n'))
2205 .unwrap_or("")
2206}
2207
2208fn motion_find_char_forward(ctx: &MotionContext) -> Result<MotionResult, CommandError> {
2209 let needle = args_to_char(&ctx.args)?;
2210 let text = ctx.buffer.as_string();
2211 let line = line_text(&text, ctx.from.line);
2212 let bytes = line.as_bytes();
2213 let start = (ctx.from.byte as usize).saturating_add(1);
2214 let mut buf = [0u8; 4];
2215 let needle_bytes = needle.encode_utf8(&mut buf).as_bytes();
2216 let nlen = needle_bytes.len();
2217 let mut idx = start;
2218 while idx + nlen <= bytes.len() {
2219 if &bytes[idx..idx + nlen] == needle_bytes {
2220 return Ok(MotionResult {
2221 curswant: None,
2222 target: Position::new(ctx.from.line, idx as u32),
2223 linewise: false,
2224 exclusive: None,
2225 notice: None,
2226 });
2227 }
2228 idx += 1;
2229 }
2230 Ok(MotionResult {
2232 curswant: None,
2233 target: ctx.from,
2234 linewise: false,
2235 exclusive: None,
2236 notice: None,
2237 })
2238}
2239
2240fn motion_find_char_backward(ctx: &MotionContext) -> Result<MotionResult, CommandError> {
2241 let needle = args_to_char(&ctx.args)?;
2242 let text = ctx.buffer.as_string();
2243 let line = line_text(&text, ctx.from.line);
2244 let bytes = line.as_bytes();
2245 let mut buf = [0u8; 4];
2246 let needle_bytes = needle.encode_utf8(&mut buf).as_bytes();
2247 let nlen = needle_bytes.len();
2248 if (ctx.from.byte as usize) < nlen {
2249 return Ok(MotionResult {
2250 curswant: None,
2251 target: ctx.from,
2252 linewise: false,
2253 exclusive: None,
2254 notice: None,
2255 });
2256 }
2257 let mut idx = (ctx.from.byte as usize) - nlen;
2258 loop {
2259 if idx + nlen <= bytes.len() && &bytes[idx..idx + nlen] == needle_bytes {
2260 return Ok(MotionResult {
2261 curswant: None,
2262 target: Position::new(ctx.from.line, idx as u32),
2263 linewise: false,
2264 exclusive: None,
2265 notice: None,
2266 });
2267 }
2268 if idx == 0 {
2269 break;
2270 }
2271 idx -= 1;
2272 }
2273 Ok(MotionResult {
2274 curswant: None,
2275 target: ctx.from,
2276 linewise: false,
2277 exclusive: None,
2278 notice: None,
2279 })
2280}
2281
2282fn motion_till_char_forward(ctx: &MotionContext) -> Result<MotionResult, CommandError> {
2283 let result = motion_find_char_forward(ctx)?;
2285 if result.target == ctx.from {
2286 return Ok(result);
2287 }
2288 let target_byte = result.target.byte.saturating_sub(1);
2289 Ok(MotionResult {
2290 curswant: None,
2291 target: Position::new(result.target.line, target_byte),
2292 linewise: false,
2293 exclusive: None,
2294 notice: None,
2295 })
2296}
2297
2298fn motion_till_char_backward(ctx: &MotionContext) -> Result<MotionResult, CommandError> {
2299 let result = motion_find_char_backward(ctx)?;
2301 if result.target == ctx.from {
2302 return Ok(result);
2303 }
2304 let line_len = result
2305 .target
2306 .byte
2307 .saturating_add(args_to_char(&ctx.args)?.len_utf8() as u32);
2308 Ok(MotionResult {
2309 curswant: None,
2310 target: Position::new(result.target.line, line_len),
2311 linewise: false,
2312 exclusive: None,
2313 notice: None,
2314 })
2315}
2316
2317fn motion_find_repeat(ctx: &MotionContext) -> Result<MotionResult, CommandError> {
2333 find_repeat(ctx, false)
2334}
2335
2336fn motion_find_repeat_reverse(ctx: &MotionContext) -> Result<MotionResult, CommandError> {
2338 find_repeat(ctx, true)
2339}
2340
2341fn find_repeat(ctx: &MotionContext, reverse: bool) -> Result<MotionResult, CommandError> {
2342 let Some(last) = ctx.last_find else {
2343 return Ok(MotionResult {
2347 curswant: None,
2348 target: ctx.from,
2349 linewise: false,
2350 exclusive: None,
2351 notice: None,
2352 });
2353 };
2354 let kind = if reverse {
2355 last.kind.reversed()
2356 } else {
2357 last.kind
2358 };
2359 let sub = MotionContext {
2363 buffer: ctx.buffer,
2364 buffer_id: ctx.buffer_id,
2365 from: ctx.from,
2366 count: ctx.count,
2367 has_explicit_count: ctx.has_explicit_count,
2368 args: crate::args::Args::Char(last.target),
2369 cancel: ctx.cancel,
2370 scope_resolver: ctx.scope_resolver,
2371 path: ctx.path,
2372 syntax: ctx.syntax,
2373 last_find: ctx.last_find,
2374 fold_resolver: ctx.fold_resolver,
2375 last_search: ctx.last_search,
2376 marks: ctx.marks,
2377 viewport: ctx.viewport,
2378 nostartofline: ctx.nostartofline,
2379 curswant: ctx.curswant,
2380 display: ctx.display,
2381 scrolloff: ctx.scrolloff,
2382 operator_pending: ctx.operator_pending,
2383 };
2384 let mut result = match kind {
2385 FindKind::Forward => motion_find_char_forward(&sub)?,
2386 FindKind::Backward => motion_find_char_backward(&sub)?,
2387 FindKind::TillForward => motion_till_char_forward(&sub)?,
2388 FindKind::TillBackward => motion_till_char_backward(&sub)?,
2389 };
2390 result.exclusive = Some(matches!(kind, FindKind::Backward | FindKind::TillBackward));
2395 Ok(result)
2396}
2397
2398fn motion_first_non_blank(ctx: &MotionContext) -> Result<MotionResult, CommandError> {
2401 let text = ctx.buffer.as_string();
2402 let line_text = text
2403 .split_inclusive('\n')
2404 .nth(ctx.from.line as usize)
2405 .map(|l| l.trim_end_matches('\n'))
2406 .unwrap_or("");
2407 let bytes = line_text.as_bytes();
2408 let mut col = 0usize;
2409 while col < bytes.len() && is_blank_byte(bytes[col]) {
2410 col += 1;
2411 }
2412 Ok(MotionResult {
2413 curswant: None,
2414 target: Position::new(ctx.from.line, col as u32),
2415 linewise: false,
2416 exclusive: None,
2417 notice: None,
2418 })
2419}
2420
2421fn motion_char_left(ctx: &MotionContext) -> Result<MotionResult, CommandError> {
2424 let count = ctx.count.get().max(1);
2425 let mut pos = ctx.from;
2426 let text = ctx.buffer.as_string();
2427 let line = line_text(&text, pos.line);
2428 let mut byte = (pos.byte as usize).min(line.len());
2433 for _ in 0..count {
2434 if byte == 0 {
2435 break;
2436 }
2437 byte -= 1;
2438 while byte > 0 && !line.is_char_boundary(byte) {
2439 byte -= 1;
2440 }
2441 }
2442 pos.byte = byte as u32;
2443 Ok(MotionResult {
2444 curswant: None,
2445 target: pos,
2446 linewise: false,
2447 exclusive: None,
2448 notice: None,
2449 })
2450}
2451
2452fn motion_char_right(ctx: &MotionContext) -> Result<MotionResult, CommandError> {
2455 let count = ctx.count.get().max(1);
2456 let mut pos = ctx.from;
2457 let text = ctx.buffer.as_string();
2458 let line = line_text(&text, pos.line);
2459 let line_len = line.len();
2460 let mut byte = (pos.byte as usize).min(line_len);
2464 for _ in 0..count {
2465 if byte >= line_len {
2466 break;
2467 }
2468 byte += 1;
2469 while byte < line_len && !line.is_char_boundary(byte) {
2470 byte += 1;
2471 }
2472 }
2473 pos.byte = byte as u32;
2474 Ok(MotionResult {
2475 curswant: None,
2476 target: pos,
2477 linewise: false,
2478 exclusive: None,
2479 notice: None,
2480 })
2481}
2482
2483fn motion_line_up(ctx: &MotionContext) -> Result<MotionResult, CommandError> {
2490 if ctx.from.line == 0 {
2491 return Err(CommandError::MotionFailed);
2492 }
2493 let count = ctx.count.get().max(1);
2494 let line = ctx.from.line.saturating_sub(count);
2495 let byte = curswant_byte(ctx, line);
2496 Ok(MotionResult {
2497 curswant: None,
2498 target: Position::new(line, byte),
2499 linewise: true,
2500 exclusive: None,
2501 notice: None,
2502 })
2503}
2504
2505fn motion_line_down(ctx: &MotionContext) -> Result<MotionResult, CommandError> {
2509 let last = last_addressable_line(ctx.buffer);
2510 if ctx.from.line >= last {
2511 return Err(CommandError::MotionFailed);
2512 }
2513 let count = ctx.count.get().max(1);
2514 let line = ctx.from.line.saturating_add(count).min(last);
2515 let byte = curswant_byte(ctx, line);
2516 Ok(MotionResult {
2517 curswant: None,
2518 target: Position::new(line, byte),
2519 linewise: true,
2520 exclusive: None,
2521 notice: None,
2522 })
2523}
2524
2525fn motion_line_start(ctx: &MotionContext) -> Result<MotionResult, CommandError> {
2528 Ok(MotionResult {
2529 curswant: None,
2530 target: Position::new(ctx.from.line, 0),
2531 linewise: false,
2532 exclusive: None,
2533 notice: None,
2534 })
2535}
2536
2537fn motion_line_end(ctx: &MotionContext) -> Result<MotionResult, CommandError> {
2538 let len = line_byte_len(ctx.buffer, ctx.from.line);
2539 Ok(MotionResult {
2540 curswant: None,
2541 target: Position::new(ctx.from.line, len),
2542 linewise: false,
2543 exclusive: None,
2544 notice: None,
2545 })
2546}
2547
2548fn motion_goto_first_line(ctx: &MotionContext) -> Result<MotionResult, CommandError> {
2551 let target_line = if ctx.has_explicit_count {
2553 let last = last_addressable_line(ctx.buffer);
2554 ctx.count.get().saturating_sub(1).min(last)
2555 } else {
2556 0
2557 };
2558 Ok(MotionResult {
2559 curswant: None,
2560 target: startofline_target(ctx, target_line),
2563 linewise: true,
2564 exclusive: None,
2565 notice: None,
2566 })
2567}
2568
2569fn motion_goto_last_line(ctx: &MotionContext) -> Result<MotionResult, CommandError> {
2570 let last = last_addressable_line(ctx.buffer);
2572 let target_line = if ctx.has_explicit_count {
2573 ctx.count.get().saturating_sub(1).min(last)
2574 } else {
2575 last
2576 };
2577 Ok(MotionResult {
2578 curswant: None,
2579 target: startofline_target(ctx, target_line),
2581 linewise: true,
2582 exclusive: None,
2583 notice: None,
2584 })
2585}
2586
2587fn line_byte_len(buffer: &lattice_core::Buffer, line: u32) -> u32 {
2590 let s = buffer.as_string();
2591 s.split_inclusive('\n')
2592 .nth(line as usize)
2593 .map(|l| l.trim_end_matches('\n').len() as u32)
2594 .unwrap_or(0)
2595}
2596
2597fn clamp_into_buffer(buffer: &lattice_core::Buffer, pos: Position) -> Position {
2601 let line = pos.line.min(last_addressable_line(buffer));
2602 Position::new(line, pos.byte.min(line_byte_len(buffer, line)))
2603}
2604
2605fn display_goal(ctx: &MotionContext, wrap: u32) -> u32 {
2609 let wrap = wrap.max(1);
2610 match ctx.curswant {
2611 Some(crate::registry::Curswant::EndOfLine) => wrap - 1,
2612 Some(crate::registry::Curswant::Col(c)) => c % wrap,
2613 None => ctx.from.byte % wrap,
2614 }
2615}
2616
2617fn motion_display_line_down(ctx: &MotionContext) -> Result<MotionResult, CommandError> {
2625 let Some(display) = ctx.display else {
2626 return motion_line_down(ctx);
2627 };
2628 let wrap = display.wrap_width();
2629 if wrap == 0 {
2630 return motion_line_down(ctx);
2631 }
2632 let last = last_addressable_line(ctx.buffer);
2633 let goal = display_goal(ctx, wrap);
2634 let mut at = ctx.from;
2635 let mut aim = at.byte;
2639 for _ in 0..ctx.count.get().max(1) {
2640 let seg = at.byte / wrap;
2641 let segs = display.segments(at.line).max(1);
2642 if seg + 1 < segs {
2643 let next_start = (seg + 1) * wrap;
2645 let next_end = ((seg + 2) * wrap).min(line_byte_len(ctx.buffer, at.line));
2646 aim = next_start + goal;
2647 let byte = aim.min(next_end.saturating_sub(1).max(next_start));
2648 at = Position::new(at.line, byte);
2649 } else if at.line < last {
2650 let next = at.line + 1;
2651 let len = line_byte_len(ctx.buffer, next);
2652 aim = goal;
2653 at = Position::new(next, goal.min(len.saturating_sub(1)));
2654 } else {
2655 break;
2658 }
2659 }
2660 Ok(MotionResult {
2661 curswant: Some(crate::registry::Curswant::Col(aim)),
2662 target: at,
2663 linewise: false,
2664 exclusive: None,
2665 notice: None,
2666 })
2667}
2668
2669fn motion_display_line_up(ctx: &MotionContext) -> Result<MotionResult, CommandError> {
2671 let Some(display) = ctx.display else {
2672 return motion_line_up(ctx);
2673 };
2674 let wrap = display.wrap_width();
2675 if wrap == 0 {
2676 return motion_line_up(ctx);
2677 }
2678 let goal = display_goal(ctx, wrap);
2679 let mut at = ctx.from;
2680 let mut aim = at.byte;
2682 for _ in 0..ctx.count.get().max(1) {
2683 let seg = at.byte / wrap;
2684 if seg > 0 {
2685 let prev_start = (seg - 1) * wrap;
2686 let actual_end = (seg * wrap).min(line_byte_len(ctx.buffer, at.line));
2687 aim = prev_start + goal;
2688 let byte = aim.min(actual_end.saturating_sub(1).max(prev_start));
2689 at = Position::new(at.line, byte);
2690 } else if at.line > 0 {
2691 let prev = at.line - 1;
2692 let len = line_byte_len(ctx.buffer, prev);
2693 let last_start = (display.segments(prev).max(1) - 1) * wrap;
2694 aim = last_start + goal;
2695 let byte = aim.min(len.saturating_sub(1).max(last_start));
2696 at = Position::new(prev, byte);
2697 } else {
2698 break;
2699 }
2700 }
2701 Ok(MotionResult {
2702 curswant: Some(crate::registry::Curswant::Col(aim)),
2703 target: at,
2704 linewise: false,
2705 exclusive: None,
2706 notice: None,
2707 })
2708}
2709
2710fn motion_display_line_start(ctx: &MotionContext) -> Result<MotionResult, CommandError> {
2712 let Some(display) = ctx.display else {
2713 return motion_line_start(ctx);
2714 };
2715 let wrap = display.wrap_width();
2716 if wrap == 0 {
2717 return motion_line_start(ctx);
2718 }
2719 Ok(MotionResult {
2720 curswant: None,
2721 target: Position::new(ctx.from.line, (ctx.from.byte / wrap) * wrap),
2722 linewise: false,
2723 exclusive: None,
2724 notice: None,
2725 })
2726}
2727
2728fn motion_display_line_end(ctx: &MotionContext) -> Result<MotionResult, CommandError> {
2732 let Some(display) = ctx.display else {
2733 return motion_line_end(ctx);
2734 };
2735 let wrap = display.wrap_width();
2736 if wrap == 0 {
2737 return motion_line_end(ctx);
2738 }
2739 let len = line_byte_len(ctx.buffer, ctx.from.line);
2740 let seg_end = (((ctx.from.byte / wrap) + 1) * wrap).min(len);
2741 Ok(MotionResult {
2742 curswant: None,
2743 target: Position::new(ctx.from.line, seg_end.saturating_sub(1)),
2744 linewise: false,
2745 exclusive: None,
2746 notice: None,
2747 })
2748}
2749
2750fn curswant_byte(ctx: &MotionContext, line: u32) -> u32 {
2758 let len = line_byte_len(ctx.buffer, line);
2759 match ctx.curswant {
2760 Some(crate::registry::Curswant::EndOfLine) => len,
2761 Some(crate::registry::Curswant::Col(c)) => c.min(len),
2762 None => ctx.from.byte.min(len),
2763 }
2764}
2765
2766fn startofline_target(ctx: &MotionContext, line: u32) -> Position {
2774 if ctx.nostartofline {
2775 let len = line_byte_len(ctx.buffer, line);
2776 Position::new(line, ctx.from.byte.min(len))
2777 } else {
2778 first_non_blank_position(ctx.buffer, line)
2779 }
2780}
2781
2782fn first_non_blank_position(buffer: &lattice_core::Buffer, line: u32) -> Position {
2785 let text = buffer.line(line).unwrap_or_default();
2786 let col = text.bytes().take_while(|&b| is_blank_byte(b)).count() as u32;
2787 Position::new(line, col.min(line_byte_len(buffer, line)))
2788}
2789
2790fn last_addressable_line(buffer: &lattice_core::Buffer) -> u32 {
2796 buffer.content_line_count().saturating_sub(1)
2797}
2798
2799fn extend_linewise_range(buffer: &lattice_core::Buffer, range: ProtoRange) -> ProtoRange {
2815 let last = last_addressable_line(buffer);
2816 let end_line = range.end.line;
2817 if end_line < last {
2818 ProtoRange::new(range.start, Position::new(end_line + 1, 0))
2819 } else if range.start.line > 0 {
2820 let prev_line = range.start.line - 1;
2821 let prev_len = line_byte_len(buffer, prev_line);
2822 ProtoRange::new(Position::new(prev_line, prev_len), range.end)
2823 } else {
2824 range
2825 }
2826}
2827
2828fn text_object_inner_paragraph(ctx: &TextObjectContext) -> Result<ProtoRange, CommandError> {
2831 let text = ctx.buffer.as_string();
2832 let last_line = buffer_last_line(&text);
2833 let cursor_line = ctx.at.line.min(last_line);
2834 let blank_at_cursor = line_is_blank(&text, cursor_line);
2835 let mut start = cursor_line;
2837 while start > 0 && line_is_blank(&text, start - 1) == blank_at_cursor {
2838 start -= 1;
2839 }
2840 let mut end = cursor_line;
2842 while end < last_line && line_is_blank(&text, end + 1) == blank_at_cursor {
2843 end += 1;
2844 }
2845 let end_byte = line_byte_len(ctx.buffer, end);
2846 Ok(ProtoRange::new(
2847 Position::new(start, 0),
2848 Position::new(end, end_byte),
2849 ))
2850}
2851
2852fn text_object_around_paragraph(ctx: &TextObjectContext) -> Result<ProtoRange, CommandError> {
2853 let inner = text_object_inner_paragraph(ctx)?;
2855 let text = ctx.buffer.as_string();
2856 let last_line = buffer_last_line(&text);
2857 let mut end_line = inner.end.line;
2858 let blank_at_inner = line_is_blank(&text, end_line);
2859 if !blank_at_inner {
2860 while end_line < last_line && line_is_blank(&text, end_line + 1) {
2861 end_line += 1;
2862 }
2863 let end_byte = line_byte_len(ctx.buffer, end_line);
2864 Ok(ProtoRange::new(
2865 inner.start,
2866 Position::new(end_line, end_byte),
2867 ))
2868 } else {
2869 while end_line < last_line && !line_is_blank(&text, end_line + 1) {
2870 end_line += 1;
2871 }
2872 let end_byte = line_byte_len(ctx.buffer, end_line);
2873 Ok(ProtoRange::new(
2874 inner.start,
2875 Position::new(end_line, end_byte),
2876 ))
2877 }
2878}
2879
2880fn line_is_comment(buffer: &lattice_core::Buffer, line: u32, leader: &str) -> bool {
2894 buffer
2895 .line(line)
2896 .map(|t| t.trim_start().starts_with(leader))
2897 .unwrap_or(false)
2898}
2899
2900fn comment_content_start(buffer: &lattice_core::Buffer, line: u32, leader: &str) -> u32 {
2904 let Some(text) = buffer.line(line) else {
2905 return 0;
2906 };
2907 let indent = text.len() - text.trim_start().len();
2908 let mut off = indent + leader.len();
2909 if text.as_bytes().get(off) == Some(&b' ') {
2910 off += 1;
2911 }
2912 off.min(text.len()) as u32
2913}
2914
2915fn comment_text_object(ctx: &TextObjectContext, inner: bool) -> Result<ProtoRange, CommandError> {
2921 let Some(leader) = ctx.comment_syntax.and_then(|c| c.line.as_deref()) else {
2922 return Ok(ProtoRange::new(ctx.at, ctx.at));
2923 };
2924 let line_count = ctx.buffer.content_line_count();
2928 if leader.is_empty() || line_count == 0 {
2929 return Ok(ProtoRange::new(ctx.at, ctx.at));
2930 }
2931 let cursor_line = ctx.at.line.min(line_count - 1);
2932 if !line_is_comment(ctx.buffer, cursor_line, leader) {
2933 return Ok(ProtoRange::new(ctx.at, ctx.at));
2934 }
2935 let mut start = cursor_line;
2936 while start > 0 && line_is_comment(ctx.buffer, start - 1, leader) {
2937 start -= 1;
2938 }
2939 let mut end = cursor_line;
2940 while end + 1 < line_count && line_is_comment(ctx.buffer, end + 1, leader) {
2941 end += 1;
2942 }
2943 let start_byte = if inner {
2944 comment_content_start(ctx.buffer, start, leader)
2945 } else {
2946 0
2947 };
2948 Ok(ProtoRange::new(
2949 Position::new(start, start_byte),
2950 Position::new(end, line_byte_len(ctx.buffer, end)),
2951 ))
2952}
2953
2954fn text_object_around_comment(ctx: &TextObjectContext) -> Result<ProtoRange, CommandError> {
2955 comment_text_object(ctx, false)
2956}
2957
2958fn text_object_inner_comment(ctx: &TextObjectContext) -> Result<ProtoRange, CommandError> {
2959 comment_text_object(ctx, true)
2960}
2961
2962fn text_object_inner_word_class(
2977 ctx: &TextObjectContext,
2978 classify: fn(u8) -> WordClass,
2979) -> Result<ProtoRange, CommandError> {
2980 let text = ctx.buffer.as_string();
2981 let bytes = text.as_bytes();
2982 let cursor = ctx
2983 .buffer
2984 .position_to_byte(ctx.at)
2985 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
2986 if cursor >= bytes.len() {
2987 return Ok(ProtoRange::new(ctx.at, ctx.at));
2989 }
2990 let class = classify(bytes[cursor]);
2991 let mut start = cursor;
2992 while start > 0 && classify(bytes[start - 1]) == class {
2993 start -= 1;
2994 }
2995 let mut end = cursor;
2996 while end + 1 < bytes.len() && classify(bytes[end + 1]) == class {
2997 end += 1;
2998 }
2999 let start_pos = ctx
3001 .buffer
3002 .byte_to_position(start)
3003 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
3004 let end_pos = ctx
3005 .buffer
3006 .byte_to_position(end + 1)
3007 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
3008 Ok(ProtoRange::new(start_pos, end_pos))
3009}
3010
3011fn text_object_around_word_class(
3016 ctx: &TextObjectContext,
3017 classify: fn(u8) -> WordClass,
3018) -> Result<ProtoRange, CommandError> {
3019 let inner = text_object_inner_word_class(ctx, classify)?;
3020 let text = ctx.buffer.as_string();
3021 let bytes = text.as_bytes();
3022 let inner_end_byte = ctx
3023 .buffer
3024 .position_to_byte(inner.end)
3025 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
3026 let mut end = inner_end_byte;
3027 while end < bytes.len() && (bytes[end] == b' ' || bytes[end] == b'\t') {
3028 end += 1;
3029 }
3030 if end == inner_end_byte {
3031 let inner_start_byte = ctx
3033 .buffer
3034 .position_to_byte(inner.start)
3035 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
3036 let mut start = inner_start_byte;
3037 while start > 0 && (bytes[start - 1] == b' ' || bytes[start - 1] == b'\t') {
3038 start -= 1;
3039 }
3040 let start_pos = ctx
3041 .buffer
3042 .byte_to_position(start)
3043 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
3044 Ok(ProtoRange::new(start_pos, inner.end))
3045 } else {
3046 let end_pos = ctx
3047 .buffer
3048 .byte_to_position(end)
3049 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
3050 Ok(ProtoRange::new(inner.start, end_pos))
3051 }
3052}
3053
3054fn text_object_inner_word(ctx: &TextObjectContext) -> Result<ProtoRange, CommandError> {
3055 text_object_inner_word_class(ctx, word_class)
3056}
3057
3058fn text_object_around_word(ctx: &TextObjectContext) -> Result<ProtoRange, CommandError> {
3059 text_object_around_word_class(ctx, word_class)
3060}
3061
3062fn text_object_inner_big_word(ctx: &TextObjectContext) -> Result<ProtoRange, CommandError> {
3063 text_object_inner_word_class(ctx, big_word_class)
3064}
3065
3066fn text_object_around_big_word(ctx: &TextObjectContext) -> Result<ProtoRange, CommandError> {
3067 text_object_around_word_class(ctx, big_word_class)
3068}
3069
3070fn find_quote_pair(bytes: &[u8], cursor: usize, q: u8) -> Option<(usize, usize)> {
3071 let mut line_start = cursor;
3075 while line_start > 0 && bytes[line_start - 1] != b'\n' {
3076 line_start -= 1;
3077 }
3078 let mut line_end = cursor;
3079 while line_end < bytes.len() && bytes[line_end] != b'\n' {
3080 line_end += 1;
3081 }
3082 let mut quotes = Vec::new();
3083 for (offset, &b) in bytes[line_start..line_end].iter().enumerate() {
3084 if b == q {
3085 quotes.push(line_start + offset);
3086 }
3087 }
3088 let mut i = 0;
3090 while i + 1 < quotes.len() {
3091 let (l, r) = (quotes[i], quotes[i + 1]);
3092 if cursor >= l && cursor <= r {
3093 return Some((l, r));
3094 }
3095 i += 2;
3096 }
3097 None
3098}
3099
3100fn text_object_inner_quote(ctx: &TextObjectContext, q: char) -> Result<ProtoRange, CommandError> {
3101 let text = ctx.buffer.as_string();
3102 let bytes = text.as_bytes();
3103 let cursor = ctx
3104 .buffer
3105 .position_to_byte(ctx.at)
3106 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
3107 if !q.is_ascii() {
3108 return Err(CommandError::InvalidArgs("non-ASCII quote not supported"));
3109 }
3110 let q_byte = q as u8;
3111 let Some((l, r)) = find_quote_pair(bytes, cursor, q_byte) else {
3112 return Ok(ProtoRange::new(ctx.at, ctx.at));
3113 };
3114 let start_pos = ctx
3116 .buffer
3117 .byte_to_position(l + 1)
3118 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
3119 let end_pos = ctx
3120 .buffer
3121 .byte_to_position(r)
3122 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
3123 Ok(ProtoRange::new(start_pos, end_pos))
3124}
3125
3126fn text_object_around_quote(ctx: &TextObjectContext, q: char) -> Result<ProtoRange, CommandError> {
3127 let text = ctx.buffer.as_string();
3128 let bytes = text.as_bytes();
3129 let cursor = ctx
3130 .buffer
3131 .position_to_byte(ctx.at)
3132 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
3133 if !q.is_ascii() {
3134 return Err(CommandError::InvalidArgs("non-ASCII quote not supported"));
3135 }
3136 let q_byte = q as u8;
3137 let Some((l, r)) = find_quote_pair(bytes, cursor, q_byte) else {
3138 return Ok(ProtoRange::new(ctx.at, ctx.at));
3139 };
3140 let start_pos = ctx
3141 .buffer
3142 .byte_to_position(l)
3143 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
3144 let end_pos = ctx
3145 .buffer
3146 .byte_to_position(r + 1)
3147 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
3148 Ok(ProtoRange::new(start_pos, end_pos))
3149}
3150
3151fn find_bracket_pair(bytes: &[u8], cursor: usize, open: u8, close: u8) -> Option<(usize, usize)> {
3152 if bytes.is_empty() {
3153 return None;
3154 }
3155 let mut depth = 0i32;
3157 let mut i = cursor.min(bytes.len() - 1);
3158 let open_pos = loop {
3159 let b = bytes[i];
3160 if b == close && i != cursor {
3161 depth += 1;
3164 } else if b == open {
3165 if depth == 0 {
3166 break i;
3167 }
3168 depth -= 1;
3169 }
3170 if i == 0 {
3171 return None;
3172 }
3173 i -= 1;
3174 };
3175 let mut depth = 0i32;
3177 let mut j = open_pos + 1;
3178 while j < bytes.len() {
3179 let b = bytes[j];
3180 if b == open {
3181 depth += 1;
3182 } else if b == close {
3183 if depth == 0 {
3184 return Some((open_pos, j));
3185 }
3186 depth -= 1;
3187 }
3188 j += 1;
3189 }
3190 None
3191}
3192
3193fn text_object_inner_brackets(
3194 ctx: &TextObjectContext,
3195 open: char,
3196 close: char,
3197) -> Result<ProtoRange, CommandError> {
3198 let text = ctx.buffer.as_string();
3199 let bytes = text.as_bytes();
3200 let cursor = ctx
3201 .buffer
3202 .position_to_byte(ctx.at)
3203 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
3204 let Some((l, r)) = find_bracket_pair(bytes, cursor, open as u8, close as u8) else {
3205 return Ok(ProtoRange::new(ctx.at, ctx.at));
3206 };
3207 let start_pos = ctx
3208 .buffer
3209 .byte_to_position(l + 1)
3210 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
3211 let end_pos = ctx
3212 .buffer
3213 .byte_to_position(r)
3214 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
3215 Ok(ProtoRange::new(start_pos, end_pos))
3216}
3217
3218fn find_enclosing_tag(bytes: &[u8], cursor: usize) -> Option<(usize, usize, usize, usize)> {
3223 let mut i = cursor.min(bytes.len().saturating_sub(1));
3225 let mut depth = 0i32;
3226 let open_start;
3227 loop {
3228 if bytes[i] == b'>' && i > 0 && bytes[i - 1] != b'/' {
3229 }
3234 if bytes[i] == b'<' {
3235 if i + 1 < bytes.len() && bytes[i + 1] == b'/' {
3237 depth += 1;
3238 } else if depth == 0 {
3239 open_start = i;
3240 break;
3241 } else {
3242 depth -= 1;
3243 }
3244 }
3245 if i == 0 {
3246 return None;
3247 }
3248 i -= 1;
3249 }
3250 let mut open_end = open_start + 1;
3252 while open_end < bytes.len() && bytes[open_end] != b'>' {
3253 open_end += 1;
3254 }
3255 if open_end >= bytes.len() {
3256 return None;
3257 }
3258 let mut name_end = open_start + 1;
3260 while name_end < open_end
3261 && bytes[name_end] != b' '
3262 && bytes[name_end] != b'>'
3263 && bytes[name_end] != b'/'
3264 {
3265 name_end += 1;
3266 }
3267 let name = &bytes[open_start + 1..name_end];
3268 if name.is_empty() {
3269 return None;
3270 }
3271 let close_marker: Vec<u8> = {
3273 let mut v = Vec::with_capacity(name.len() + 3);
3274 v.push(b'<');
3275 v.push(b'/');
3276 v.extend_from_slice(name);
3277 v.push(b'>');
3278 v
3279 };
3280 let mut j = open_end + 1;
3281 while j + close_marker.len() <= bytes.len() {
3282 if &bytes[j..j + close_marker.len()] == close_marker.as_slice() {
3283 return Some((open_start, open_end + 1, j, j + close_marker.len()));
3284 }
3285 j += 1;
3286 }
3287 None
3288}
3289
3290fn text_object_inner_tag(ctx: &TextObjectContext) -> Result<ProtoRange, CommandError> {
3291 let text = ctx.buffer.as_string();
3292 let bytes = text.as_bytes();
3293 let cursor = ctx
3294 .buffer
3295 .position_to_byte(ctx.at)
3296 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
3297 let Some((_, open_end, close_start, _)) = find_enclosing_tag(bytes, cursor) else {
3298 return Ok(ProtoRange::new(ctx.at, ctx.at));
3299 };
3300 let start_pos = ctx
3301 .buffer
3302 .byte_to_position(open_end)
3303 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
3304 let end_pos = ctx
3305 .buffer
3306 .byte_to_position(close_start)
3307 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
3308 Ok(ProtoRange::new(start_pos, end_pos))
3309}
3310
3311fn text_object_around_tag(ctx: &TextObjectContext) -> Result<ProtoRange, CommandError> {
3312 let text = ctx.buffer.as_string();
3313 let bytes = text.as_bytes();
3314 let cursor = ctx
3315 .buffer
3316 .position_to_byte(ctx.at)
3317 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
3318 let Some((open_start, _, _, close_end)) = find_enclosing_tag(bytes, cursor) else {
3319 return Ok(ProtoRange::new(ctx.at, ctx.at));
3320 };
3321 let start_pos = ctx
3322 .buffer
3323 .byte_to_position(open_start)
3324 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
3325 let end_pos = ctx
3326 .buffer
3327 .byte_to_position(close_end)
3328 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
3329 Ok(ProtoRange::new(start_pos, end_pos))
3330}
3331
3332fn text_object_around_brackets(
3333 ctx: &TextObjectContext,
3334 open: char,
3335 close: char,
3336) -> Result<ProtoRange, CommandError> {
3337 let text = ctx.buffer.as_string();
3338 let bytes = text.as_bytes();
3339 let cursor = ctx
3340 .buffer
3341 .position_to_byte(ctx.at)
3342 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
3343 let Some((l, r)) = find_bracket_pair(bytes, cursor, open as u8, close as u8) else {
3344 return Ok(ProtoRange::new(ctx.at, ctx.at));
3345 };
3346 let start_pos = ctx
3347 .buffer
3348 .byte_to_position(l)
3349 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
3350 let end_pos = ctx
3351 .buffer
3352 .byte_to_position(r + 1)
3353 .map_err(|_| CommandError::InvalidArgs("position out of bounds"))?;
3354 Ok(ProtoRange::new(start_pos, end_pos))
3355}
3356
3357fn operator_delete(ctx: &mut OperatorContext) -> Result<Effect, CommandError> {
3365 if ctx.range.is_empty() {
3366 return Ok(Effect::None);
3367 }
3368 let edit_range = if ctx.linewise {
3374 extend_linewise_range(ctx.document.buffer(), ctx.range)
3375 } else {
3376 ctx.range
3377 };
3378 let yanked = if ctx.linewise {
3384 let mut s = ctx.document.buffer().slice(ctx.range)?;
3385 if !s.ends_with('\n') {
3386 s.push('\n');
3387 }
3388 s
3389 } else {
3390 ctx.document.buffer().slice(ctx.range)?
3391 };
3392 let edit = Edit::delete(edit_range);
3393 let applied = ctx.document.apply_edit(edit)?;
3394 let yank_kind = if ctx.linewise {
3395 YankKind::Linewise
3396 } else {
3397 YankKind::Charwise
3398 };
3399 let mut effects = vec![
3400 Effect::Edits(vec![applied]),
3401 Effect::Yank {
3402 register: ctx.register,
3403 content: yanked,
3404 kind: yank_kind,
3405 explicit_yank: false,
3408 },
3409 ];
3410 if ctx.linewise {
3416 let buffer = ctx.document.buffer();
3417 let line = ctx.range.start.line.min(last_addressable_line(buffer));
3418 effects.push(Effect::CursorMove(first_non_blank_position(buffer, line)));
3419 }
3420 Ok(Effect::Many(effects))
3421}
3422
3423fn operator_change(ctx: &mut OperatorContext) -> Result<Effect, CommandError> {
3430 if ctx.range.is_empty() {
3431 return Ok(Effect::EnterMode(crate::modal::ModalState::Insert));
3434 }
3435 let yanked = ctx.document.buffer().slice(ctx.range)?;
3436 let edit = Edit::delete(ctx.range);
3437 let applied = ctx.document.apply_edit(edit)?;
3438 let yank_kind = if ctx.linewise {
3439 YankKind::Linewise
3440 } else {
3441 YankKind::Charwise
3442 };
3443 Ok(Effect::Many(vec![
3444 Effect::Edits(vec![applied]),
3445 Effect::Yank {
3446 register: ctx.register,
3447 content: yanked,
3448 kind: yank_kind,
3449 explicit_yank: false,
3452 },
3453 Effect::EnterMode(crate::modal::ModalState::Insert),
3454 ]))
3455}
3456
3457fn operator_replace_char(ctx: &mut OperatorContext) -> Result<Effect, CommandError> {
3480 let replacement = match &ctx.args {
3481 crate::args::Args::Char(c) => *c,
3482 _ => return Err(CommandError::InvalidArgs("r requires Args::Char")),
3483 };
3484 if ctx.range.is_empty() {
3485 return Ok(Effect::None);
3486 }
3487 let text = ctx.document.buffer().slice(ctx.range)?;
3488 let n_chars = text.chars().filter(|c| *c != '\n').count() as u32;
3489 if n_chars < ctx.count.get().max(1) {
3493 return Ok(Effect::None);
3494 }
3495 let replaced: String = text
3496 .chars()
3497 .map(|c| if c == '\n' { '\n' } else { replacement })
3498 .collect();
3499 let applied = ctx
3500 .document
3501 .apply_edit(Edit::replace(ctx.range, replaced))?;
3502 Ok(Effect::Edits(vec![applied]))
3503}
3504
3505fn operator_yank(ctx: &mut OperatorContext) -> Result<Effect, CommandError> {
3511 if ctx.range.is_empty() {
3512 return Ok(Effect::None);
3513 }
3514 let content = if ctx.linewise {
3521 let mut s = ctx.document.buffer().slice(ctx.range)?;
3522 if !s.ends_with('\n') {
3523 s.push('\n');
3524 }
3525 s
3526 } else {
3527 ctx.document.buffer().slice(ctx.range)?
3528 };
3529 let kind = if ctx.linewise {
3530 YankKind::Linewise
3531 } else {
3532 YankKind::Charwise
3533 };
3534 Ok(Effect::Many(vec![
3538 Effect::Yank {
3539 register: ctx.register,
3540 content: content.clone(),
3541 kind,
3542 explicit_yank: true,
3545 },
3546 Effect::Yank {
3547 register: crate::register::Register::Numbered(0),
3548 content,
3549 kind,
3550 explicit_yank: false,
3553 },
3554 Effect::CursorMove(clamp_into_buffer(ctx.document.buffer(), ctx.origin)),
3559 ]))
3560}
3561
3562fn shift_indent(ctx: &mut OperatorContext, levels: i32) -> Result<Effect, CommandError> {
3585 if ctx.range.is_empty() {
3586 return Ok(Effect::None);
3587 }
3588 let first_line = ctx.range.start.line;
3589 let last_line = ctx.range.end.line;
3590 let buffer_text = ctx.document.text();
3591 let lines: Vec<&str> = buffer_text.split_inclusive('\n').collect();
3592
3593 let mut edits: Vec<Edit> = Vec::new();
3596 for line in (first_line..=last_line).rev() {
3597 let line_text = lines
3598 .get(line as usize)
3599 .map(|l| l.trim_end_matches('\n'))
3600 .unwrap_or("");
3601 let Some((old_len, rendered)) = ctx.indent.reindented_prefix(line_text, levels) else {
3607 continue;
3608 };
3609 let range = lattice_protocol::position::Range::new(
3610 Position::new(line, 0),
3611 Position::new(line, old_len as u32),
3612 );
3613 edits.push(Edit::replace(range, rendered));
3614 }
3615 if edits.is_empty() {
3616 return Ok(Effect::None);
3617 }
3618 let applied = ctx.document.apply_edit_batch(edits)?;
3619 let mut applied = applied;
3623 applied.reverse();
3624 Ok(landing_on_first_non_blank(ctx, first_line, applied))
3625}
3626
3627fn landing_on_first_non_blank(
3635 ctx: &mut OperatorContext,
3636 first_line: u32,
3637 applied: Vec<lattice_core::buffer::AppliedEdit>,
3638) -> Effect {
3639 let buffer = ctx.document.buffer();
3640 let line = first_line.min(last_addressable_line(buffer));
3641 let at = first_non_blank_position(buffer, line);
3642 Effect::Many(vec![Effect::Edits(applied), Effect::CursorMove(at)])
3643}
3644
3645fn operator_indent_right(ctx: &mut OperatorContext) -> Result<Effect, CommandError> {
3647 shift_indent(ctx, 1)
3648}
3649
3650fn operator_indent_left(ctx: &mut OperatorContext) -> Result<Effect, CommandError> {
3653 shift_indent(ctx, -1)
3654}
3655
3656fn operator_reindent(ctx: &mut OperatorContext) -> Result<Effect, CommandError> {
3674 if ctx.range.is_empty() {
3675 return Ok(Effect::None);
3676 }
3677 if !ctx.native_format.indent {
3682 return Ok(Effect::AppAction(
3683 crate::app_effect::AppEffect::FormatRange {
3684 intent: lattice_core::FormatIntent::Indent,
3685 start_line: ctx.range.start.line,
3686 end_line: ctx.range.end.line,
3687 },
3688 ));
3689 }
3690 let Some(resolver) = ctx.indent_resolver else {
3691 return Ok(Effect::None);
3698 };
3699 let first_line = ctx.range.start.line;
3700 let last_line = ctx.range.end.line;
3701 let buffer_text = ctx.document.text();
3702 let lines: Vec<&str> = buffer_text.split_inclusive('\n').collect();
3703
3704 let mut edits: Vec<Edit> = Vec::new();
3707 for line in (first_line..=last_line).rev() {
3708 ctx.cancel.check()?;
3709 let line_text = lines
3710 .get(line as usize)
3711 .map(|l| l.trim_end_matches('\n'))
3712 .unwrap_or("");
3713 if lattice_core::IndentUnit::is_blank(line_text) {
3716 continue;
3717 }
3718 let Some(levels) = resolver.levels_for_line(line) else {
3719 continue;
3720 };
3721 let target = ctx.indent.shift(0, levels);
3722 let rendered = ctx.indent.render(target);
3723 let old_len = lattice_core::IndentUnit::indent_len(line_text);
3724 if rendered.as_bytes() == &line_text.as_bytes()[..old_len] {
3725 continue;
3726 }
3727 let range = lattice_protocol::position::Range::new(
3728 Position::new(line, 0),
3729 Position::new(line, old_len as u32),
3730 );
3731 edits.push(Edit::replace(range, rendered));
3732 }
3733 if edits.is_empty() {
3734 return Ok(Effect::None);
3735 }
3736 let applied = ctx.document.apply_edit_batch(edits)?;
3737 let mut applied = applied;
3738 applied.reverse();
3739 Ok(landing_on_first_non_blank(ctx, first_line, applied))
3740}
3741
3742fn operator_reflow(ctx: &mut OperatorContext) -> Result<Effect, CommandError> {
3764 if ctx.range.is_empty() {
3765 return Ok(Effect::None);
3766 }
3767 let first_line = ctx.range.start.line;
3768
3769 let buffer_text = ctx.document.text();
3770 let all: Vec<&str> = buffer_text.split('\n').collect();
3771 let last_line = ctx.range.end.line.min(all.len().saturating_sub(1) as u32);
3772 if first_line > last_line {
3773 return Ok(Effect::None);
3774 }
3775 ctx.cancel.check()?;
3776
3777 if !ctx.native_format.reflow {
3783 return Ok(Effect::AppAction(
3784 crate::app_effect::AppEffect::FormatRange {
3785 intent: lattice_core::FormatIntent::Reflow,
3786 start_line: first_line,
3787 end_line: last_line,
3788 },
3789 ));
3790 }
3791
3792 let slice: Vec<&str> = all[first_line as usize..=last_line as usize].to_vec();
3793 let cfg = crate::reflow::ReflowConfig {
3794 textwidth: ctx.textwidth.columns(),
3795 line_comment: ctx.comment_syntax.and_then(|c| c.line.as_deref()),
3796 };
3797 let out = crate::reflow::reflow_range(&slice, cfg);
3798 ctx.cancel.check()?;
3799
3800 if out.len() == slice.len() && out.iter().zip(slice.iter()).all(|(a, b)| a == b) {
3804 return Ok(Effect::None);
3805 }
3806
3807 let end_col = all
3812 .get(last_line as usize)
3813 .map(|l| l.len() as u32)
3814 .unwrap_or(0);
3815 let range = lattice_protocol::position::Range::new(
3816 Position::new(first_line, 0),
3817 Position::new(last_line, end_col),
3818 );
3819 let applied = ctx
3820 .document
3821 .apply_edit_batch(vec![Edit::replace(range, out.join("\n"))])?;
3822 Ok(Effect::Edits(applied))
3823}
3824
3825fn operator_create_fold(ctx: &mut OperatorContext) -> Result<Effect, CommandError> {
3848 let (s, e) = (ctx.range.start, ctx.range.end);
3849 let whole_lines = ctx.linewise || ctx.document.selections().primary().visual.is_some();
3850 let (start_line, end_line) = if whole_lines {
3851 (s.line.min(e.line), s.line.max(e.line))
3852 } else {
3853 crate::range::span_to_whole_lines(s.line, s.byte, e.line, e.byte)
3854 };
3855 Ok(Effect::AppAction(
3856 crate::app_effect::AppEffect::CreateFold {
3857 start_line,
3858 end_line,
3859 },
3860 ))
3861}
3862
3863fn operator_reformat(ctx: &mut OperatorContext) -> Result<Effect, CommandError> {
3864 if ctx.range.is_empty() {
3865 return Ok(Effect::None);
3866 }
3867 Ok(Effect::AppAction(
3868 crate::app_effect::AppEffect::FormatRange {
3869 intent: lattice_core::FormatIntent::Reformat,
3870 start_line: ctx.range.start.line,
3871 end_line: ctx.range.end.line,
3872 },
3873 ))
3874}
3875
3876fn case_transform_in_range<F: Fn(u8) -> u8>(
3879 ctx: &mut OperatorContext,
3880 map: F,
3881) -> Result<Effect, CommandError> {
3882 if ctx.range.is_empty() {
3883 return Ok(Effect::None);
3884 }
3885 let original = ctx.document.buffer().slice(ctx.range)?;
3886 let transformed: String = original
3887 .as_bytes()
3888 .iter()
3889 .map(|&b| map(b) as char)
3890 .collect();
3891 if transformed == original {
3892 return Ok(Effect::None);
3893 }
3894 let edit = Edit::replace(ctx.range, &transformed);
3895 let applied = ctx.document.apply_edit(edit)?;
3896 Ok(Effect::Edits(vec![applied]))
3897}
3898
3899fn operator_upper(ctx: &mut OperatorContext) -> Result<Effect, CommandError> {
3900 case_transform_in_range(ctx, |b| b.to_ascii_uppercase())
3901}
3902
3903fn operator_lower(ctx: &mut OperatorContext) -> Result<Effect, CommandError> {
3904 case_transform_in_range(ctx, |b| b.to_ascii_lowercase())
3905}
3906
3907fn operator_toggle_case(ctx: &mut OperatorContext) -> Result<Effect, CommandError> {
3908 case_transform_in_range(ctx, |b| match b {
3909 b'a'..=b'z' => b - 32,
3910 b'A'..=b'Z' => b + 32,
3911 other => other,
3912 })
3913}
3914
3915fn operator_search(ctx: &mut OperatorContext) -> Result<Effect, CommandError> {
3923 if ctx.range.is_empty() {
3924 return Ok(Effect::None);
3925 }
3926 let query = ctx.document.buffer().slice(ctx.range)?;
3927 if query.trim().is_empty() {
3928 return Ok(Effect::None);
3929 }
3930 Ok(Effect::AppAction(
3931 crate::app_effect::AppEffect::SearchTrigger { query },
3932 ))
3933}
3934
3935#[cfg(test)]
3936mod tests {
3937 #![allow(clippy::unwrap_used, clippy::panic)]
3938 use super::*;
3939 use crate::CancellationToken;
3940 use crate::command::{CommandInvocation, Count};
3941 use crate::dispatcher::execute;
3942 use crate::effect::Effect;
3943 use crate::target::Target;
3944 use lattice_core::Document;
3945
3946 fn fixture(text: &str) -> (CommandRegistry, Builtins, Document) {
3947 let mut r = CommandRegistry::new();
3948 let b = populate(&mut r);
3949 let d = Document::from_text(text);
3950 (r, b, d)
3951 }
3952
3953 fn search_query(eff: Effect) -> String {
3956 match eff {
3957 Effect::AppAction(crate::app_effect::AppEffect::SearchTrigger { query }) => query,
3958 other => panic!("expected SearchTrigger, got {other:?}"),
3959 }
3960 }
3961
3962 fn reflow_whole(text: &str, textwidth: usize, leader: Option<&str>) -> (String, Effect) {
3968 let (registry, b, mut doc) = fixture(text);
3969 let cancel = CancellationToken::never();
3970 let cs = leader.map(|l| crate::CommentSyntax {
3971 line: Some(l.to_string()),
3972 block: None,
3973 });
3974 let env = crate::registry::GrammarEnv {
3975 textwidth: lattice_core::WrapWidth(textwidth),
3976 comment_syntax: cs.as_ref(),
3977 ..Default::default()
3978 };
3979 let inv = CommandInvocation::of(b.reflow.0).with_range(crate::Range::Whole);
3980 let eff = crate::dispatcher::execute_with_env(
3981 ®istry,
3982 &mut doc,
3983 lattice_core::BufferId(0),
3984 Position::new(0, 0),
3985 inv,
3986 &cancel,
3987 env,
3988 )
3989 .unwrap();
3990 (doc.text().to_string(), eff)
3991 }
3992
3993 #[test]
3994 fn reflow_fills_a_paragraph_to_textwidth() {
3995 let (out, _) = reflow_whole("aaa bbb ccc ddd eee fff\n", 11, None);
3996 assert_eq!(out, "aaa bbb ccc\nddd eee fff\n");
3997 }
3998
3999 #[test]
4000 fn reflow_keeps_a_comment_leader() {
4001 let (out, _) = reflow_whole("/// aaa bbb ccc ddd\n", 12, Some("//"));
4002 assert_eq!(out, "/// aaa bbb\n/// ccc ddd\n");
4003 }
4004
4005 #[test]
4010 fn reflow_that_changes_nothing_produces_no_edit_at_all() {
4011 let (out, eff) = reflow_whole("aaa bbb\n", 80, None);
4012 assert_eq!(out, "aaa bbb\n");
4013 assert!(
4014 matches!(eff, Effect::None),
4015 "an already-filled paragraph must not push an undo step, got {eff:?}"
4016 );
4017 }
4018
4019 #[test]
4020 fn reflow_of_an_empty_range_is_a_noop() {
4021 let (registry, b, mut doc) = fixture("");
4022 let cancel = CancellationToken::never();
4023 let inv = CommandInvocation::of(b.reflow.0)
4024 .with_target(Target::TextObject(b.inner_word, crate::args::Args::None));
4025 let eff = execute(
4026 ®istry,
4027 &mut doc,
4028 lattice_core::BufferId(0),
4029 Position::new(0, 0),
4030 inv,
4031 &cancel,
4032 )
4033 .unwrap();
4034 assert!(matches!(eff, Effect::None), "got {eff:?}");
4035 }
4036
4037 #[test]
4042 fn reflow_emits_one_edit_for_the_whole_range() {
4043 let (_, eff) = reflow_whole("aaa bbb ccc ddd\n", 7, None);
4044 match eff {
4045 Effect::Edits(edits) => assert_eq!(
4046 edits.len(),
4047 1,
4048 "filling joins and splits lines, so a per-line edit set would \
4049 have to encode that itself; one edit is also one undo unit \
4050 by construction"
4051 ),
4052 other => panic!("expected Edits, got {other:?}"),
4053 }
4054 }
4055
4056 #[test]
4060 fn reflow_does_not_reindent() {
4061 let (out, _) = reflow_whole(" aaa bbb ccc ddd\n", 20, None);
4062 assert_eq!(
4063 out, " aaa bbb ccc\n ddd\n",
4064 "the paragraph's indent is preserved verbatim, not normalised"
4065 );
4066 }
4067
4068 #[test]
4071 fn gq_and_gw_are_one_operator() {
4072 let (_, b, _) = fixture("x\n");
4073 assert_ne!(b.reflow.0, b.reindent.0, "reflow is not reindent");
4078 }
4079
4080 fn run_whole(
4087 text: &str,
4088 pick: impl Fn(&Builtins) -> crate::registry::OperatorId,
4089 native: crate::registry::NativeFormatIntents,
4090 ) -> Effect {
4091 let (registry, b, mut doc) = fixture(text);
4092 let op = pick(&b);
4093 let cancel = CancellationToken::never();
4094 let env = crate::registry::GrammarEnv {
4095 textwidth: lattice_core::WrapWidth(11),
4096 native_format: native,
4097 ..Default::default()
4098 };
4099 let inv = CommandInvocation::of(op.0).with_range(crate::Range::Whole);
4100 crate::dispatcher::execute_with_env(
4101 ®istry,
4102 &mut doc,
4103 lattice_core::BufferId(0),
4104 Position::new(0, 0),
4105 inv,
4106 &cancel,
4107 env,
4108 )
4109 .unwrap()
4110 }
4111
4112 #[test]
4115 fn the_default_env_formats_natively() {
4116 assert_eq!(
4117 crate::registry::NativeFormatIntents::default(),
4118 crate::registry::NativeFormatIntents {
4119 indent: true,
4120 reflow: true
4121 }
4122 );
4123 }
4124
4125 #[test]
4129 fn a_non_native_reflow_chain_hands_the_range_to_the_host() {
4130 let eff = run_whole(
4131 "aaa bbb ccc ddd\n",
4132 |b| b.reflow,
4133 crate::registry::NativeFormatIntents {
4134 indent: true,
4135 reflow: false,
4136 },
4137 );
4138 match eff {
4139 Effect::AppAction(crate::app_effect::AppEffect::FormatRange {
4140 intent,
4141 start_line,
4142 end_line,
4143 }) => {
4144 assert_eq!(intent, lattice_core::FormatIntent::Reflow);
4145 assert_eq!((start_line, end_line), (0, 0));
4148 }
4149 other => panic!("expected FormatRange, got {other:?}"),
4150 }
4151 }
4152
4153 #[test]
4157 fn a_native_reflow_chain_still_edits_in_place() {
4158 let eff = run_whole(
4159 "aaa bbb ccc ddd\n",
4160 |b| b.reflow,
4161 crate::registry::NativeFormatIntents::default(),
4162 );
4163 assert!(
4164 matches!(eff, Effect::Edits(_)),
4165 "a native chain must edit, got {eff:?}"
4166 );
4167 }
4168
4169 #[test]
4173 fn a_non_native_indent_chain_delegates_without_a_tree() {
4174 let eff = run_whole(
4175 " aaa\n",
4176 |b| b.reindent,
4177 crate::registry::NativeFormatIntents {
4178 indent: false,
4179 reflow: true,
4180 },
4181 );
4182 match eff {
4183 Effect::AppAction(crate::app_effect::AppEffect::FormatRange { intent, .. }) => {
4184 assert_eq!(intent, lattice_core::FormatIntent::Indent);
4185 }
4186 other => panic!("expected FormatRange, got {other:?}"),
4187 }
4188 }
4191
4192 #[test]
4195 fn g_equals_always_hands_the_range_to_the_host() {
4196 let eff = run_whole(
4197 "let x=1;\n",
4198 |b| b.reformat,
4199 crate::registry::NativeFormatIntents::default(),
4200 );
4201 match eff {
4202 Effect::AppAction(crate::app_effect::AppEffect::FormatRange { intent, .. }) => {
4203 assert_eq!(intent, lattice_core::FormatIntent::Reformat);
4204 }
4205 other => panic!("expected FormatRange even with a native env, got {other:?}"),
4206 }
4207 }
4208
4209 fn fold_span_for_motion(
4214 text: &str,
4215 from: Position,
4216 pick: impl Fn(&Builtins) -> crate::registry::MotionId,
4217 ) -> (u32, u32) {
4218 let (registry, b, mut doc) = fixture(text);
4219 let cancel = CancellationToken::never();
4220 let inv = CommandInvocation::of(b.create_fold.0).with_target(
4221 crate::target::Target::Motion(pick(&b), crate::args::Args::None),
4222 );
4223 let eff = crate::dispatcher::execute_with_env(
4224 ®istry,
4225 &mut doc,
4226 lattice_core::BufferId(0),
4227 from,
4228 inv,
4229 &cancel,
4230 crate::registry::GrammarEnv::default(),
4231 )
4232 .expect("zf dispatches");
4233 match eff {
4234 Effect::AppAction(crate::app_effect::AppEffect::CreateFold {
4235 start_line,
4236 end_line,
4237 }) => (start_line, end_line),
4238 other => panic!("expected CreateFold, got {other:?}"),
4239 }
4240 }
4241
4242 #[test]
4245 fn zf_j_folds_two_whole_lines() {
4246 let text = "one\ntwo\nthree\n";
4247 assert_eq!(
4248 fold_span_for_motion(text, Position::new(0, 0), |b| b.line_down),
4249 (0, 1)
4250 );
4251 assert_eq!(
4252 fold_span_for_motion(text, Position::new(0, 2), |b| b.line_down),
4253 (0, 1)
4254 );
4255 }
4256
4257 #[test]
4260 fn zf_paragraph_forward_stops_before_the_blank_line() {
4261 let text = "a\nb\n\nc\n";
4262 assert_eq!(
4263 fold_span_for_motion(text, Position::new(0, 0), |b| b.paragraph_forward),
4264 (0, 1)
4265 );
4266 }
4267
4268 #[test]
4271 fn zf_over_a_linewise_range_keeps_an_empty_last_line() {
4272 let (registry, b, mut doc) = fixture("a\n\n");
4273 let cancel = CancellationToken::never();
4274 let inv = CommandInvocation::of(b.create_fold.0).with_range(crate::Range::Whole);
4275 let eff = crate::dispatcher::execute_with_env(
4276 ®istry,
4277 &mut doc,
4278 lattice_core::BufferId(0),
4279 Position::new(0, 0),
4280 inv,
4281 &cancel,
4282 crate::registry::GrammarEnv::default(),
4283 )
4284 .expect("zf dispatches");
4285 match eff {
4286 Effect::AppAction(crate::app_effect::AppEffect::CreateFold {
4287 start_line,
4288 end_line,
4289 }) => assert_eq!((start_line, end_line), (0, 1)),
4290 other => panic!("expected CreateFold, got {other:?}"),
4291 }
4292 }
4293
4294 #[test]
4295 fn g_equals_over_an_empty_range_is_a_noop() {
4296 let (registry, b, mut doc) = fixture("");
4297 let cancel = CancellationToken::never();
4298 let inv = CommandInvocation::of(b.reformat.0)
4299 .with_target(Target::TextObject(b.inner_word, crate::args::Args::None));
4300 let eff = execute(
4301 ®istry,
4302 &mut doc,
4303 lattice_core::BufferId(0),
4304 Position::new(0, 0),
4305 inv,
4306 &cancel,
4307 )
4308 .unwrap();
4309 assert!(matches!(eff, Effect::None), "got {eff:?}");
4310 }
4311
4312 #[test]
4313 fn search_operator_greps_word_under_cursor() {
4314 let (registry, b, mut doc) = fixture("let needle = 1;\n");
4316 let cancel = CancellationToken::never();
4317 let cursor = Position::new(0, 4); let inv = CommandInvocation::of(b.search.0)
4319 .with_target(Target::TextObject(b.inner_word, crate::args::Args::None));
4320 let eff = execute(
4321 ®istry,
4322 &mut doc,
4323 lattice_core::BufferId(0),
4324 cursor,
4325 inv,
4326 &cancel,
4327 )
4328 .unwrap();
4329 assert_eq!(search_query(eff), "needle");
4330 }
4331
4332 #[test]
4333 fn search_operator_greps_quoted_string() {
4334 let (registry, b, mut doc) = fixture("let s = \"find me\";\n");
4336 let cancel = CancellationToken::never();
4337 let cursor = Position::new(0, 10); let inv = CommandInvocation::of(b.search.0).with_target(Target::TextObject(
4339 b.inner_quote_double,
4340 crate::args::Args::None,
4341 ));
4342 let eff = execute(
4343 ®istry,
4344 &mut doc,
4345 lattice_core::BufferId(0),
4346 cursor,
4347 inv,
4348 &cancel,
4349 )
4350 .unwrap();
4351 assert_eq!(search_query(eff), "find me");
4352 }
4353
4354 #[test]
4355 fn search_operator_empty_range_is_noop() {
4356 let (registry, b, mut doc) = fixture("\n");
4358 let cancel = CancellationToken::never();
4359 let cursor = Position::new(0, 0);
4360 let inv = CommandInvocation::of(b.search.0)
4361 .with_target(Target::TextObject(b.inner_word, crate::args::Args::None));
4362 let eff = execute(
4363 ®istry,
4364 &mut doc,
4365 lattice_core::BufferId(0),
4366 cursor,
4367 inv,
4368 &cancel,
4369 )
4370 .unwrap();
4371 assert!(
4372 matches!(eff, Effect::None),
4373 "empty range must be a no-op, got {eff:?}"
4374 );
4375 }
4376
4377 #[test]
4378 fn comment_object_around_keeps_markers_inner_strips_leader() {
4379 use crate::dispatcher::execute_with_env;
4380 use crate::registry::{CommentSyntax, GrammarEnv};
4381 let src = "fn f() {\n // first\n // second\n let x = 1;\n}\n";
4383 let (registry, b, mut doc) = fixture(src);
4384 let cs = CommentSyntax {
4385 line: Some("//".to_string()),
4386 block: None,
4387 };
4388 let cancel = CancellationToken::never();
4389 let cursor = Position::new(1, 8); fn yanked(eff: Effect) -> String {
4393 match eff {
4394 Effect::Yank { content, .. } => content,
4395 Effect::Many(effs) => effs
4396 .into_iter()
4397 .find_map(|e| match e {
4398 Effect::Yank { content, .. } => Some(content),
4399 _ => None,
4400 })
4401 .expect("a Yank inside Many"),
4402 other => panic!("expected a Yank, got {other:?}"),
4403 }
4404 }
4405
4406 let inv = CommandInvocation::of(b.yank.0).with_target(Target::TextObject(
4408 b.around_comment,
4409 crate::args::Args::None,
4410 ));
4411 let eff = execute_with_env(
4412 ®istry,
4413 &mut doc,
4414 lattice_core::BufferId(0),
4415 cursor,
4416 inv,
4417 &cancel,
4418 GrammarEnv {
4419 scope_resolver: None,
4420 comment_syntax: Some(&cs),
4421 syntax: None,
4422 ..Default::default()
4423 },
4424 )
4425 .unwrap();
4426 let content = yanked(eff);
4427 assert!(
4428 content.contains("// first") && content.contains("// second"),
4429 "aC yanks the comment block including markers, got {content:?}"
4430 );
4431
4432 let inv = CommandInvocation::of(b.yank.0)
4434 .with_target(Target::TextObject(b.inner_comment, crate::args::Args::None));
4435 let eff = execute_with_env(
4436 ®istry,
4437 &mut doc,
4438 lattice_core::BufferId(0),
4439 cursor,
4440 inv,
4441 &cancel,
4442 GrammarEnv {
4443 scope_resolver: None,
4444 comment_syntax: Some(&cs),
4445 syntax: None,
4446 ..Default::default()
4447 },
4448 )
4449 .unwrap();
4450 let content = yanked(eff);
4451 assert!(
4452 content.starts_with("first"),
4453 "iC strips the first line's leader (no `//`), got {content:?}"
4454 );
4455 }
4456
4457 #[test]
4458 fn comment_object_no_leader_is_a_noop() {
4459 use crate::dispatcher::execute_with_env;
4462 use crate::registry::GrammarEnv;
4463 let src = "// a comment\n";
4464 let (registry, b, mut doc) = fixture(src);
4465 let cancel = CancellationToken::never();
4466 let inv = CommandInvocation::of(b.delete.0).with_target(Target::TextObject(
4467 b.around_comment,
4468 crate::args::Args::None,
4469 ));
4470 let eff = execute_with_env(
4471 ®istry,
4472 &mut doc,
4473 lattice_core::BufferId(0),
4474 Position::new(0, 3),
4475 inv,
4476 &cancel,
4477 GrammarEnv::default(),
4478 )
4479 .unwrap();
4480 assert!(matches!(eff, Effect::None), "no leader -> no-op");
4481 assert_eq!(doc.text(), src, "document unchanged");
4482 }
4483
4484 #[test]
4485 fn populate_registers_known_builtins_by_name() {
4486 let mut r = CommandRegistry::new();
4487 let _ = populate(&mut r);
4488 assert!(r.lookup_by_name("motion:word-forward").is_some());
4489 assert!(r.lookup_by_name("operator:delete").is_some());
4490 }
4491
4492 #[test]
4493 fn pre_flipped_token_short_circuits_dispatch() {
4494 let (registry, b, mut doc) = fixture("hello world");
4497 let inv = CommandInvocation::of(b.word_forward.0);
4498 let cancel = CancellationToken::new();
4499 cancel.cancel();
4500 let result = execute(
4501 ®istry,
4502 &mut doc,
4503 lattice_core::BufferId(0),
4504 Position::ZERO,
4505 inv,
4506 &cancel,
4507 );
4508 match result {
4509 Err(CommandError::Cancelled) => {}
4510 other => panic!("expected Cancelled, got {other:?}"),
4511 }
4512 assert_eq!(doc.text(), "hello world");
4514 }
4515
4516 #[test]
4517 fn fresh_token_does_not_short_circuit() {
4518 let (registry, b, mut doc) = fixture("hello world");
4521 let inv = CommandInvocation::of(b.word_forward.0);
4522 let cancel = CancellationToken::new();
4523 let result = execute(
4524 ®istry,
4525 &mut doc,
4526 lattice_core::BufferId(0),
4527 Position::ZERO,
4528 inv,
4529 &cancel,
4530 );
4531 assert!(result.is_ok());
4532 }
4533
4534 const FOO: &str = "alpha foo one\nbeta two\ngamma foo three\ndelta four\n";
4537
4538 fn search_effect(
4539 from: Position,
4540 reverse: bool,
4541 count: Option<u32>,
4542 last: Option<&crate::registry::LastSearch>,
4543 ) -> Result<Effect, CommandError> {
4544 let (registry, b, mut doc) = fixture(FOO);
4545 let id = if reverse {
4546 b.search_prev
4547 } else {
4548 b.search_next
4549 };
4550 let mut inv = CommandInvocation::of(id.0);
4551 if let Some(n) = count {
4552 inv = inv.with_count(crate::command::Count(n));
4553 }
4554 let env = crate::registry::GrammarEnv {
4555 last_search: last,
4556 ..Default::default()
4557 };
4558 crate::dispatcher::execute_with_env(
4559 ®istry,
4560 &mut doc,
4561 lattice_core::BufferId(0),
4562 from,
4563 inv,
4564 &CancellationToken::never(),
4565 env,
4566 )
4567 }
4568
4569 fn foo_forward() -> crate::registry::LastSearch {
4570 crate::registry::LastSearch {
4571 pattern: "foo".into(),
4572 direction: crate::modal::SearchDirection::Forward,
4573 }
4574 }
4575
4576 #[test]
4578 fn n_moves_to_the_next_match_start() {
4579 let last = foo_forward();
4580 match search_effect(Position::new(0, 0), false, None, Some(&last)).unwrap() {
4581 Effect::CursorMove(pos) => assert_eq!(pos, Position::new(0, 6)),
4582 other => panic!("expected a plain CursorMove, got {other:?}"),
4583 }
4584 }
4585
4586 #[test]
4588 fn a_count_repeats_and_upper_n_reverses() {
4589 let last = foo_forward();
4590 match search_effect(Position::new(0, 0), false, Some(2), Some(&last)).unwrap() {
4591 Effect::CursorMove(pos) => assert_eq!(pos, Position::new(2, 6)),
4592 other => panic!("expected CursorMove, got {other:?}"),
4593 }
4594 match search_effect(Position::new(2, 9), true, None, Some(&last)).unwrap() {
4595 Effect::CursorMove(pos) => assert_eq!(pos, Position::new(2, 6)),
4596 other => panic!("expected CursorMove, got {other:?}"),
4597 }
4598 }
4599
4600 #[test]
4602 fn a_wrapping_n_echoes_search_hit_bottom() {
4603 let last = foo_forward();
4604 match search_effect(Position::new(2, 9), false, None, Some(&last)).unwrap() {
4605 Effect::Many(parts) => {
4606 assert!(matches!(parts[0], Effect::CursorMove(p) if p == Position::new(0, 6)));
4607 assert!(matches!(
4608 &parts[1],
4609 Effect::Echo { level: crate::effect::EchoLevel::Warn, text }
4610 if text == "search hit BOTTOM, continuing at TOP"
4611 ));
4612 }
4613 other => panic!("expected CursorMove + Echo, got {other:?}"),
4614 }
4615 }
4616
4617 #[test]
4620 fn a_failed_search_is_a_user_facing_error() {
4621 let missing = crate::registry::LastSearch {
4622 pattern: "zzz".into(),
4623 direction: crate::modal::SearchDirection::Forward,
4624 };
4625 match search_effect(Position::new(0, 0), false, None, Some(&missing)) {
4626 Err(CommandError::User(msg)) => assert_eq!(msg, "E486: Pattern not found: zzz"),
4627 other => panic!("expected E486, got {other:?}"),
4628 }
4629 match search_effect(Position::new(0, 0), false, None, None) {
4630 Err(CommandError::User(msg)) => assert_eq!(msg, "E35: no previous regular expression"),
4631 other => panic!("expected E35, got {other:?}"),
4632 }
4633 }
4634
4635 fn wrapped_fixture() -> (CommandRegistry, Builtins, Document, WrapAt80) {
4641 let long: String = std::iter::repeat_n("abcdefghij", 20).collect();
4642 let text = format!(" short one\n{long}\n short three");
4643 let (r, b, d) = fixture(&text);
4644 (r, b, d, WrapAt80)
4645 }
4646
4647 struct WrapAt80;
4648
4649 impl crate::registry::DisplayResolver for WrapAt80 {
4650 fn wrap_width(&self) -> u32 {
4651 80
4652 }
4653 fn segments(&self, line: u32) -> u32 {
4654 if line == 1 { 3 } else { 1 }
4656 }
4657 }
4658
4659 fn display_motion(
4660 pick: impl Fn(&Builtins) -> crate::registry::MotionId,
4661 from: Position,
4662 count: Option<u32>,
4663 wrapped: bool,
4664 ) -> Position {
4665 let (registry, b, mut doc, display) = wrapped_fixture();
4666 let mut inv = CommandInvocation::of(pick(&b).0);
4667 if let Some(n) = count {
4668 inv = inv.with_count(crate::command::Count(n));
4669 }
4670 let env = crate::registry::GrammarEnv {
4671 display: wrapped.then_some(&display as &dyn crate::registry::DisplayResolver),
4672 ..Default::default()
4673 };
4674 match crate::dispatcher::execute_with_env(
4675 ®istry,
4676 &mut doc,
4677 lattice_core::BufferId(0),
4678 from,
4679 inv,
4680 &CancellationToken::never(),
4681 env,
4682 )
4683 .unwrap()
4684 {
4685 Effect::CursorMove(p) => p,
4686 other => panic!("expected CursorMove, got {other:?}"),
4687 }
4688 }
4689
4690 #[test]
4693 fn gj_and_gk_move_by_display_row() {
4694 assert_eq!(
4695 display_motion(|b| b.display_line_down, Position::new(1, 4), None, true),
4696 Position::new(1, 84)
4697 );
4698 assert_eq!(
4699 display_motion(|b| b.display_line_up, Position::new(1, 84), None, true),
4700 Position::new(1, 4)
4701 );
4702 assert_eq!(
4703 display_motion(|b| b.display_line_down, Position::new(1, 4), Some(2), true),
4704 Position::new(1, 164)
4705 );
4706 }
4707
4708 fn display_motion_curswant(
4711 pick: impl Fn(&Builtins) -> crate::registry::MotionId,
4712 from: Position,
4713 curswant_in: Option<crate::registry::Curswant>,
4714 ) -> (Position, Option<crate::registry::Curswant>) {
4715 let (registry, b, mut doc, display) = wrapped_fixture();
4716 let slot = std::sync::Mutex::new(None);
4717 let env = crate::registry::GrammarEnv {
4718 display: Some(&display as &dyn crate::registry::DisplayResolver),
4719 curswant: curswant_in,
4720 curswant_out: Some(&slot),
4721 ..Default::default()
4722 };
4723 let landed = match crate::dispatcher::execute_with_env(
4724 ®istry,
4725 &mut doc,
4726 lattice_core::BufferId(0),
4727 from,
4728 CommandInvocation::of(pick(&b).0),
4729 &CancellationToken::never(),
4730 env,
4731 )
4732 .unwrap()
4733 {
4734 Effect::CursorMove(p) => p,
4735 other => panic!("expected CursorMove, got {other:?}"),
4736 };
4737 let reported = *slot.lock().unwrap();
4738 (landed, reported)
4739 }
4740
4741 #[test]
4753 fn a_clamped_gj_reports_the_aim_not_the_landing() {
4754 let (landed, reported) = display_motion_curswant(
4755 |b| b.display_line_down,
4756 Position::new(1, 159),
4757 Some(crate::registry::Curswant::Col(159)),
4758 );
4759 assert_eq!(landed, Position::new(1, 199), "the row ends at byte 199");
4760 assert_eq!(
4761 reported,
4762 Some(crate::registry::Curswant::Col(239)),
4763 "but the aim was 239, and that is what vim keeps"
4764 );
4765 }
4766
4767 #[test]
4771 fn an_unclamped_gj_reports_the_column_it_reached() {
4772 let (landed, reported) = display_motion_curswant(
4773 |b| b.display_line_down,
4774 Position::new(1, 4),
4775 Some(crate::registry::Curswant::Col(4)),
4776 );
4777 assert_eq!(landed, Position::new(1, 84));
4778 assert_eq!(reported, Some(crate::registry::Curswant::Col(84)));
4779 }
4780
4781 #[test]
4783 fn gk_reports_its_aim_too() {
4784 let (landed, reported) = display_motion_curswant(
4785 |b| b.display_line_up,
4786 Position::new(1, 84),
4787 Some(crate::registry::Curswant::Col(84)),
4788 );
4789 assert_eq!(landed, Position::new(1, 4));
4790 assert_eq!(reported, Some(crate::registry::Curswant::Col(4)));
4791 }
4792
4793 #[test]
4797 fn an_ordinary_motion_reports_no_goal() {
4798 let (_, reported) = display_motion_curswant(|b| b.line_down, Position::new(1, 4), None);
4799 assert_eq!(reported, None);
4800 }
4801
4802 #[test]
4804 fn gj_on_the_last_display_row_stays() {
4805 assert_eq!(
4806 display_motion(|b| b.display_line_down, Position::new(2, 4), None, true),
4807 Position::new(2, 4)
4808 );
4809 }
4810
4811 #[test]
4813 fn g0_and_g_dollar_bound_the_display_row() {
4814 assert_eq!(
4815 display_motion(|b| b.display_line_start, Position::new(1, 84), None, true),
4816 Position::new(1, 80)
4817 );
4818 assert_eq!(
4819 display_motion(|b| b.display_line_end, Position::new(1, 84), None, true),
4820 Position::new(1, 159)
4821 );
4822 }
4823
4824 #[test]
4827 fn without_wrap_they_degrade_to_the_line_motions() {
4828 assert_eq!(
4829 display_motion(|b| b.display_line_down, Position::new(0, 4), None, false),
4830 Position::new(1, 4),
4831 "`gj` is `j`"
4832 );
4833 assert_eq!(
4834 display_motion(|b| b.display_line_start, Position::new(1, 84), None, false),
4835 Position::new(1, 0),
4836 "`g0` is `0`"
4837 );
4838 }
4839
4840 #[test]
4845 fn the_display_motions_set_the_goal_from_where_they_land() {
4846 use crate::registry::CurswantEffect;
4847 let (registry, b, _d, _w) = wrapped_fixture();
4848 for id in [
4849 b.display_line_down,
4850 b.display_line_up,
4851 b.display_line_start,
4852 b.display_line_end,
4853 ] {
4854 assert_eq!(
4855 registry.motion_curswant(id.0),
4856 CurswantEffect::SetFromTarget
4857 );
4858 }
4859 assert_eq!(
4861 registry.motion_curswant(b.line_down.0),
4862 CurswantEffect::Keep
4863 );
4864 }
4865
4866 const RAGGED: &str = "abcdefghijkl\nab\nabcdefghijkl\n";
4869
4870 fn vertical(
4871 down: bool,
4872 from: Position,
4873 curswant: Option<crate::registry::Curswant>,
4874 ) -> Position {
4875 let (registry, b, mut doc) = fixture(RAGGED);
4876 let motion = if down { b.line_down } else { b.line_up };
4877 let env = crate::registry::GrammarEnv {
4878 curswant,
4879 ..Default::default()
4880 };
4881 match crate::dispatcher::execute_with_env(
4882 ®istry,
4883 &mut doc,
4884 lattice_core::BufferId(0),
4885 from,
4886 CommandInvocation::of(motion.0),
4887 &CancellationToken::never(),
4888 env,
4889 )
4890 .unwrap()
4891 {
4892 Effect::CursorMove(p) => p,
4893 other => panic!("expected CursorMove, got {other:?}"),
4894 }
4895 }
4896
4897 #[test]
4901 fn a_vertical_motion_aims_at_the_goal_column_not_the_cursor() {
4902 use crate::registry::Curswant;
4903 assert_eq!(
4905 vertical(true, Position::new(0, 8), Some(Curswant::Col(8))),
4906 Position::new(1, 2)
4907 );
4908 assert_eq!(
4910 vertical(true, Position::new(1, 2), Some(Curswant::Col(8))),
4911 Position::new(2, 8)
4912 );
4913 assert_eq!(
4915 vertical(false, Position::new(1, 2), Some(Curswant::Col(8))),
4916 Position::new(0, 8)
4917 );
4918 }
4919
4920 #[test]
4923 fn an_end_of_line_goal_lands_on_each_lines_end() {
4924 use crate::registry::Curswant;
4925 assert_eq!(
4926 vertical(true, Position::new(0, 12), Some(Curswant::EndOfLine)),
4927 Position::new(1, 2),
4928 "the short line's end"
4929 );
4930 assert_eq!(
4931 vertical(true, Position::new(1, 2), Some(Curswant::EndOfLine)),
4932 Position::new(2, 12),
4933 "and the long line's end, not the short one's column"
4934 );
4935 }
4936
4937 #[test]
4940 fn no_goal_yet_means_the_cursors_own_column() {
4941 assert_eq!(
4942 vertical(true, Position::new(0, 5), None),
4943 Position::new(1, 2)
4944 );
4945 assert_eq!(
4946 vertical(true, Position::new(1, 1), None),
4947 Position::new(2, 1)
4948 );
4949 }
4950
4951 #[test]
4953 fn j_k_keep_the_goal_and_dollar_pins_it() {
4954 use crate::registry::CurswantEffect;
4955 let (registry, b, _doc) = fixture(RAGGED);
4956 assert_eq!(
4957 registry.motion_curswant(b.line_down.0),
4958 CurswantEffect::Keep
4959 );
4960 assert_eq!(registry.motion_curswant(b.line_up.0), CurswantEffect::Keep);
4961 assert_eq!(
4962 registry.motion_curswant(b.line_end.0),
4963 CurswantEffect::PinToEnd
4964 );
4965 assert_eq!(
4967 registry.motion_curswant(b.word_forward.0),
4968 CurswantEffect::SetFromTarget
4969 );
4970 assert_eq!(
4971 registry.motion_curswant(b.delete.0),
4972 CurswantEffect::SetFromTarget
4973 );
4974 }
4975
4976 fn sol_effect(first_line: bool, from: Position, nostartofline: bool) -> Effect {
4982 let (registry, b, mut doc) = fixture(" one a\n two b\n three c\n");
4983 let motion = if first_line {
4984 b.goto_first_line
4985 } else {
4986 b.goto_last_line
4987 };
4988 let env = crate::registry::GrammarEnv {
4989 nostartofline,
4990 ..Default::default()
4991 };
4992 crate::dispatcher::execute_with_env(
4993 ®istry,
4994 &mut doc,
4995 lattice_core::BufferId(0),
4996 from,
4997 CommandInvocation::of(motion.0),
4998 &CancellationToken::never(),
4999 env,
5000 )
5001 .unwrap()
5002 }
5003
5004 #[test]
5007 fn gg_and_g_land_on_the_first_non_blank() {
5008 match sol_effect(true, Position::new(2, 5), false) {
5009 Effect::CursorMove(p) => assert_eq!(p, Position::new(0, 2)),
5010 other => panic!("expected CursorMove, got {other:?}"),
5011 }
5012 match sol_effect(false, Position::new(0, 5), false) {
5013 Effect::CursorMove(p) => assert_eq!(p, Position::new(2, 2)),
5014 other => panic!("expected CursorMove, got {other:?}"),
5015 }
5016 }
5017
5018 #[test]
5020 fn nostartofline_keeps_the_column_on_gg_and_g() {
5021 match sol_effect(true, Position::new(2, 5), true) {
5022 Effect::CursorMove(p) => assert_eq!(p, Position::new(0, 5)),
5023 other => panic!("expected CursorMove, got {other:?}"),
5024 }
5025 match sol_effect(false, Position::new(1, 8), true) {
5026 Effect::CursorMove(p) => assert_eq!(p, Position::new(2, 8)),
5027 other => panic!("expected CursorMove, got {other:?}"),
5028 }
5029 }
5030
5031 #[test]
5036 fn a_linewise_delete_lands_on_the_first_non_blank() {
5037 let (registry, b, mut doc) = fixture(" one a\n two b\n three c\n");
5038 let inv = CommandInvocation::of(b.delete.0).with_range(crate::range::Range::CurrentLine);
5039 let effect = execute(
5040 ®istry,
5041 &mut doc,
5042 lattice_core::BufferId(0),
5043 Position::new(0, 5),
5044 inv,
5045 &CancellationToken::never(),
5046 )
5047 .unwrap();
5048 match effect {
5049 Effect::Many(parts) => assert!(matches!(
5050 parts[parts.len() - 1],
5051 Effect::CursorMove(p) if p == Position::new(0, 4)
5052 )),
5053 other => panic!("expected Many, got {other:?}"),
5054 }
5055 }
5056
5057 #[test]
5060 fn a_yank_lands_on_the_start_of_what_it_yanked() {
5061 let (registry, b, mut doc) = fixture(" one a\n two b\n three c\n");
5062 let inv = CommandInvocation::of(b.yank.0)
5063 .with_target(Target::Motion(b.line_up, crate::args::Args::None));
5064 let effect = execute(
5065 ®istry,
5066 &mut doc,
5067 lattice_core::BufferId(0),
5068 Position::new(1, 6),
5069 inv,
5070 &CancellationToken::never(),
5071 )
5072 .unwrap();
5073 match effect {
5074 Effect::Many(parts) => assert!(matches!(
5075 parts[parts.len() - 1],
5076 Effect::CursorMove(p) if p == Position::new(0, 6)
5077 )),
5078 other => panic!("expected Many, got {other:?}"),
5079 }
5080 }
5081
5082 fn shown(first: u32, count: u32) -> crate::registry::ShownLines {
5085 crate::registry::ShownLines((first..first + count).map(|l| (l, 1.0)).collect())
5086 }
5087
5088 #[test]
5092 fn shown_lines_answer_h_m_and_l_as_vim_does() {
5093 use crate::app_effect::ViewportPos::{Bottom, Middle, Top};
5094 use crate::registry::ViewportResolver;
5095 let window = shown(5, 21);
5096 assert_eq!(window.viewport_line(Top, 1), Some(5));
5097 assert_eq!(window.viewport_line(Middle, 1), Some(15));
5098 assert_eq!(window.viewport_line(Bottom, 1), Some(25));
5099 assert_eq!(window.viewport_line(Top, 3), Some(7));
5100 assert_eq!(window.viewport_line(Bottom, 3), Some(23));
5101 assert_eq!(window.viewport_line(Top, 99), Some(25));
5102 assert_eq!(window.viewport_line(Bottom, 99), Some(5));
5103 }
5104
5105 #[test]
5108 fn middle_is_half_the_lines_shown_less_one() {
5109 use crate::app_effect::ViewportPos::Middle;
5110 use crate::registry::{ShownLines, ViewportResolver};
5111 assert_eq!(shown(0, 22).viewport_line(Middle, 1), Some(10));
5112 assert_eq!(shown(0, 23).viewport_line(Middle, 1), Some(11));
5113 assert_eq!(shown(0, 6).viewport_line(Middle, 1), Some(2));
5114 let mut rows: Vec<(u32, f32)> = (0..9).map(|l| (l, 1.0)).collect();
5115 rows[2].1 = 4.0;
5116 assert_eq!(ShownLines(rows).viewport_line(Middle, 1), Some(2));
5117 assert_eq!(ShownLines::default().viewport_line(Middle, 1), None);
5118 }
5119
5120 #[test]
5129 fn h_and_l_keep_scrolloff_from_the_windows_edges() {
5130 use crate::app_effect::ViewportPos::{Bottom, Top};
5131 let long = "x\n".repeat(100);
5132 let (_registry, _b, doc) = fixture(&long);
5133 let window = shown(44, 11);
5134 let motion = |pos, scrolloff, operator_pending| {
5135 let ctx = MotionContext {
5136 buffer: doc.buffer(),
5137 buffer_id: lattice_core::BufferId(0),
5138 from: Position::new(49, 0),
5139 count: Count::default(),
5140 has_explicit_count: false,
5141 args: crate::args::Args::None,
5142 cancel: &CancellationToken::never(),
5143 scope_resolver: None,
5144 path: None,
5145 syntax: None,
5146 last_find: None,
5147 fold_resolver: None,
5148 last_search: None,
5149 marks: None,
5150 viewport: Some(&window as &dyn crate::registry::ViewportResolver),
5151 nostartofline: false,
5152 curswant: None,
5153 display: None,
5154 scrolloff,
5155 operator_pending,
5156 };
5157 motion_viewport(&ctx, pos).unwrap().target.line
5158 };
5159
5160 assert_eq!(motion(Top, 3, false), 47, "`H` stops 3 inside the top");
5161 assert_eq!(
5162 motion(Bottom, 3, false),
5163 51,
5164 "`L` stops 3 inside the bottom"
5165 );
5166 assert_eq!(motion(Top, 0, false), 44, "`scrolloff=0` reaches the edge");
5167 assert_eq!(motion(Bottom, 0, false), 54);
5168 assert_eq!(motion(Top, 3, true), 44, "an operator target is unadjusted");
5171 assert_eq!(motion(Bottom, 3, true), 54);
5172
5173 let top_of_file = shown(0, 11);
5176 let ctx_at = |window: &crate::registry::ShownLines, pos| {
5177 let ctx = MotionContext {
5178 buffer: doc.buffer(),
5179 buffer_id: lattice_core::BufferId(0),
5180 from: Position::new(5, 0),
5181 count: Count::default(),
5182 has_explicit_count: false,
5183 args: crate::args::Args::None,
5184 cancel: &CancellationToken::never(),
5185 scope_resolver: None,
5186 path: None,
5187 syntax: None,
5188 last_find: None,
5189 fold_resolver: None,
5190 last_search: None,
5191 marks: None,
5192 viewport: Some(window as &dyn crate::registry::ViewportResolver),
5193 nostartofline: false,
5194 curswant: None,
5195 display: None,
5196 scrolloff: 3,
5197 operator_pending: false,
5198 };
5199 motion_viewport(&ctx, pos).unwrap().target.line
5200 };
5201 assert_eq!(ctx_at(&top_of_file, Top), 0, "`H` on the file's first line");
5202 let end_of_file = shown(89, 11);
5205 assert_eq!(
5206 ctx_at(&end_of_file, Bottom),
5207 99,
5208 "`L` on the file's last line"
5209 );
5210 }
5211
5212 fn viewport_effect(nostartofline: bool, with_window: bool) -> Result<Effect, CommandError> {
5213 let (registry, b, mut doc) = fixture(" one\n two\n three\n");
5214 let window = shown(1, 2);
5215 let env = crate::registry::GrammarEnv {
5216 viewport: with_window.then_some(&window as &dyn crate::registry::ViewportResolver),
5217 nostartofline,
5218 ..Default::default()
5219 };
5220 crate::dispatcher::execute_with_env(
5221 ®istry,
5222 &mut doc,
5223 lattice_core::BufferId(0),
5224 Position::new(2, 4),
5225 CommandInvocation::of(b.viewport_top.0),
5226 &CancellationToken::never(),
5227 env,
5228 )
5229 }
5230
5231 #[test]
5234 fn a_viewport_motion_follows_startofline() {
5235 match viewport_effect(false, true).unwrap() {
5236 Effect::CursorMove(pos) => assert_eq!(pos, Position::new(1, 2)),
5237 other => panic!("expected CursorMove, got {other:?}"),
5238 }
5239 match viewport_effect(true, true).unwrap() {
5240 Effect::CursorMove(pos) => assert_eq!(pos, Position::new(1, 4)),
5241 other => panic!("expected CursorMove, got {other:?}"),
5242 }
5243 assert!(matches!(
5244 viewport_effect(false, false),
5245 Err(CommandError::MotionFailed)
5246 ));
5247 }
5248
5249 const MARKS: &str = " one a\n two b\n\n four d\n five e\n six f\n";
5252
5253 fn mark_effect(
5254 exact: bool,
5255 name: char,
5256 marks: Option<&std::collections::HashMap<char, Position>>,
5257 ) -> Result<Effect, CommandError> {
5258 let (registry, b, mut doc) = fixture(MARKS);
5259 let id = if exact { b.mark_exact } else { b.mark_line };
5260 let inv = CommandInvocation::of(id.0).with_args(crate::args::Args::Char(name));
5261 let env = crate::registry::GrammarEnv {
5262 marks: marks.map(|m| m as &dyn crate::registry::MarkResolver),
5263 ..Default::default()
5264 };
5265 crate::dispatcher::execute_with_env(
5266 ®istry,
5267 &mut doc,
5268 lattice_core::BufferId(0),
5269 Position::new(0, 4),
5270 inv,
5271 &CancellationToken::never(),
5272 env,
5273 )
5274 }
5275
5276 fn mark_a_at(line: u32, byte: u32) -> std::collections::HashMap<char, Position> {
5277 std::collections::HashMap::from([('a', Position::new(line, byte))])
5278 }
5279
5280 #[test]
5283 fn a_mark_motion_targets_its_line_or_its_exact_position() {
5284 let marks = mark_a_at(4, 5);
5285 match mark_effect(false, 'a', Some(&marks)).unwrap() {
5286 Effect::CursorMove(pos) => assert_eq!(pos, Position::new(4, 2)),
5287 other => panic!("expected CursorMove, got {other:?}"),
5288 }
5289 match mark_effect(true, 'a', Some(&marks)).unwrap() {
5290 Effect::CursorMove(pos) => assert_eq!(pos, Position::new(4, 5)),
5291 other => panic!("expected CursorMove, got {other:?}"),
5292 }
5293 }
5294
5295 #[test]
5297 fn an_unset_mark_is_e20() {
5298 let marks = mark_a_at(4, 5);
5299 for result in [
5300 mark_effect(false, 'b', Some(&marks)),
5301 mark_effect(true, 'a', None),
5302 ] {
5303 match result {
5304 Err(CommandError::User(msg)) => assert_eq!(msg, "E20: Mark not set"),
5305 other => panic!("expected E20, got {other:?}"),
5306 }
5307 }
5308 }
5309
5310 #[test]
5313 fn a_mark_past_the_end_is_clamped_into_the_buffer() {
5314 let marks = mark_a_at(40, 9);
5315 match mark_effect(true, 'a', Some(&marks)).unwrap() {
5316 Effect::CursorMove(pos) => assert_eq!(pos.line, 5),
5317 other => panic!("expected CursorMove, got {other:?}"),
5318 }
5319 }
5320
5321 struct OpenFolds(Vec<(u32, u32)>);
5326
5327 impl crate::registry::FoldResolver for OpenFolds {
5328 fn fold_edge(&self, line: u32, forward: bool) -> Option<u32> {
5329 if forward {
5330 self.0.iter().map(|f| f.0).filter(|&s| s > line).min()
5331 } else {
5332 self.0.iter().map(|f| f.1).filter(|&e| e < line).max()
5333 }
5334 }
5335 }
5336
5337 fn vim_folds() -> OpenFolds {
5340 OpenFolds(vec![(3, 5), (8, 9)])
5341 }
5342
5343 fn fold_motion(
5344 from: Position,
5345 count: Option<u32>,
5346 forward: bool,
5347 folds: Option<&OpenFolds>,
5348 ) -> Position {
5349 let text: String = (1..=14).map(|n| format!("line {n}\n")).collect();
5350 let (registry, b, mut doc) = fixture(&text);
5351 let id = if forward {
5352 b.goto_next_fold
5353 } else {
5354 b.goto_prev_fold
5355 };
5356 let mut inv = CommandInvocation::of(id.0);
5357 if let Some(n) = count {
5358 inv = inv.with_count(crate::command::Count(n));
5359 }
5360 let env = crate::registry::GrammarEnv {
5361 fold_resolver: folds.map(|f| f as &dyn crate::registry::FoldResolver),
5362 ..Default::default()
5363 };
5364 let effect = crate::dispatcher::execute_with_env(
5365 ®istry,
5366 &mut doc,
5367 lattice_core::BufferId(0),
5368 from,
5369 inv,
5370 &CancellationToken::never(),
5371 env,
5372 )
5373 .expect("zj / zk dispatch");
5374 match effect {
5375 Effect::CursorMove(pos) => pos,
5376 other => panic!("expected CursorMove, got {other:?}"),
5377 }
5378 }
5379
5380 #[test]
5382 fn zj_and_zk_step_to_fold_edges_at_column_zero() {
5383 let folds = vim_folds();
5384 assert_eq!(
5385 fold_motion(Position::new(0, 2), None, true, Some(&folds)),
5386 Position::new(3, 0)
5387 );
5388 assert_eq!(
5389 fold_motion(Position::new(0, 2), Some(2), true, Some(&folds)),
5390 Position::new(8, 0)
5391 );
5392 assert_eq!(
5393 fold_motion(Position::new(11, 2), None, false, Some(&folds)),
5394 Position::new(9, 0)
5395 );
5396 }
5397
5398 #[test]
5401 fn zj_without_an_edge_stays_put() {
5402 let folds = vim_folds();
5403 assert_eq!(
5404 fold_motion(Position::new(11, 2), None, true, Some(&folds)),
5405 Position::new(11, 2)
5406 );
5407 assert_eq!(
5408 fold_motion(Position::new(0, 2), None, true, None),
5409 Position::new(0, 2)
5410 );
5411 }
5412
5413 #[test]
5415 fn a_count_past_the_last_fold_stops_at_the_last_edge() {
5416 let folds = vim_folds();
5417 assert_eq!(
5418 fold_motion(Position::new(0, 2), Some(9), true, Some(&folds)),
5419 Position::new(8, 0)
5420 );
5421 }
5422
5423 fn repro_word_motion_punct(
5424 select: impl Fn(&Builtins) -> MotionId,
5425 text: &str,
5426 from: Position,
5427 ) -> Position {
5428 let (registry, b, mut doc) = fixture(text);
5429 let inv = CommandInvocation::of(select(&b).0);
5430 let effect = execute(
5431 ®istry,
5432 &mut doc,
5433 lattice_core::BufferId(0),
5434 from,
5435 inv,
5436 &CancellationToken::never(),
5437 )
5438 .unwrap();
5439 match effect {
5440 Effect::CursorMove(pos) => pos,
5441 other => panic!("expected CursorMove, got {other:?}"),
5442 }
5443 }
5444
5445 #[test]
5446 fn word_forward_stops_at_punctuation() {
5447 assert_eq!(
5449 repro_word_motion_punct(|b| b.word_forward, "foo.bar", Position::ZERO),
5450 Position::new(0, 3),
5451 );
5452 }
5453
5454 #[test]
5455 fn word_backward_stops_at_punctuation() {
5456 assert_eq!(
5458 repro_word_motion_punct(|b| b.word_backward, "foo.bar", Position::new(0, 4)),
5459 Position::new(0, 3),
5460 );
5461 }
5462
5463 #[test]
5464 fn word_end_stops_at_punctuation() {
5465 assert_eq!(
5468 repro_word_motion_punct(|b| b.word_end, "foo.bar", Position::ZERO),
5469 Position::new(0, 2),
5470 );
5471 assert_eq!(
5472 repro_word_motion_punct(|b| b.word_end, "foo.bar", Position::new(0, 2)),
5473 Position::new(0, 3),
5474 );
5475 }
5476
5477 #[test]
5478 fn word_forward_advances_to_next_word_start() {
5479 let (registry, b, mut doc) = fixture("hello world");
5480 let inv = CommandInvocation::of(b.word_forward.0);
5481 let effect = execute(
5482 ®istry,
5483 &mut doc,
5484 lattice_core::BufferId(0),
5485 Position::ZERO,
5486 inv,
5487 &CancellationToken::never(),
5488 )
5489 .unwrap();
5490 match effect {
5491 Effect::CursorMove(pos) => {
5492 assert_eq!(pos, Position::new(0, 6));
5493 }
5494 other => panic!("expected CursorMove, got {other:?}"),
5495 }
5496 }
5497
5498 #[test]
5499 fn word_forward_with_count_advances_by_count() {
5500 let (registry, b, mut doc) = fixture("one two three four");
5501 let inv = CommandInvocation::of(b.word_forward.0).with_count(Count(2));
5502 let effect = execute(
5503 ®istry,
5504 &mut doc,
5505 lattice_core::BufferId(0),
5506 Position::ZERO,
5507 inv,
5508 &CancellationToken::never(),
5509 )
5510 .unwrap();
5511 match effect {
5512 Effect::CursorMove(pos) => {
5513 assert_eq!(pos, Position::new(0, 8));
5516 }
5517 other => panic!("expected CursorMove, got {other:?}"),
5518 }
5519 }
5520
5521 #[test]
5522 fn word_forward_across_newline() {
5523 let (registry, b, mut doc) = fixture("hello\nworld");
5524 let inv = CommandInvocation::of(b.word_forward.0);
5525 let effect = execute(
5526 ®istry,
5527 &mut doc,
5528 lattice_core::BufferId(0),
5529 Position::ZERO,
5530 inv,
5531 &CancellationToken::never(),
5532 )
5533 .unwrap();
5534 match effect {
5535 Effect::CursorMove(pos) => {
5536 assert_eq!(pos, Position::new(1, 0));
5537 }
5538 other => panic!("expected CursorMove, got {other:?}"),
5539 }
5540 }
5541
5542 #[test]
5543 fn delete_with_word_forward_target_dw_semantics() {
5544 let (registry, b, mut doc) = fixture("hello world");
5548 let inv = CommandInvocation::of(b.delete.0)
5549 .with_target(Target::Motion(b.word_forward, crate::args::Args::None));
5550 let effect = execute(
5551 ®istry,
5552 &mut doc,
5553 lattice_core::BufferId(0),
5554 Position::ZERO,
5555 inv,
5556 &CancellationToken::never(),
5557 )
5558 .unwrap();
5559 match effect {
5560 Effect::Many(parts) => {
5561 assert_eq!(parts.len(), 2);
5562 match &parts[0] {
5563 Effect::Edits(applied) => {
5564 assert_eq!(applied.len(), 1);
5565 assert_eq!(applied[0].replaced_text, "hello ");
5566 }
5567 other => panic!("expected Edits at [0], got {other:?}"),
5568 }
5569 match &parts[1] {
5570 Effect::Yank { content, kind, .. } => {
5571 assert_eq!(content, "hello ");
5572 assert_eq!(*kind, YankKind::Charwise);
5573 }
5574 other => panic!("expected Yank at [1], got {other:?}"),
5575 }
5576 }
5577 other => panic!("expected Many, got {other:?}"),
5578 }
5579 assert_eq!(doc.text(), "world");
5580 }
5581
5582 #[test]
5583 fn delete_with_explicit_whole_range_deletes_buffer_and_yanks_linewise() {
5584 let (registry, b, mut doc) = fixture("a\nb\nc");
5585 let inv = CommandInvocation::of(b.delete.0).with_range(crate::range::Range::Whole);
5586 let effect = execute(
5587 ®istry,
5588 &mut doc,
5589 lattice_core::BufferId(0),
5590 Position::ZERO,
5591 inv,
5592 &CancellationToken::never(),
5593 )
5594 .unwrap();
5595 match effect {
5596 Effect::Many(parts) => {
5597 assert!(matches!(parts[0], Effect::Edits(_)));
5598 match &parts[1] {
5599 Effect::Yank { kind, .. } => assert_eq!(*kind, YankKind::Linewise),
5600 other => panic!("expected Yank at [1], got {other:?}"),
5601 }
5602 }
5603 other => panic!("expected Many, got {other:?}"),
5604 }
5605 assert_eq!(doc.text(), "");
5606 }
5607
5608 #[test]
5609 fn delete_with_current_line_range_clears_just_that_line() {
5610 let (registry, b, mut doc) = fixture("aaa\nBBB\nccc");
5611 let cursor = Position::new(1, 0);
5612 let inv = CommandInvocation::of(b.delete.0).with_range(crate::range::Range::CurrentLine);
5613 execute(
5614 ®istry,
5615 &mut doc,
5616 lattice_core::BufferId(0),
5617 cursor,
5618 inv,
5619 &CancellationToken::never(),
5620 )
5621 .unwrap();
5622 assert_eq!(doc.text(), "aaa\nccc");
5625 }
5626
5627 #[test]
5628 fn unknown_command_id_errors() {
5629 let (registry, _, mut doc) = fixture("abc");
5630 let bogus = lattice_protocol::ids::CommandId::new(99_999);
5631 let inv = CommandInvocation::of(bogus);
5632 assert!(matches!(
5633 execute(
5634 ®istry,
5635 &mut doc,
5636 lattice_core::BufferId(0),
5637 Position::ZERO,
5638 inv,
5639 &CancellationToken::never()
5640 ),
5641 Err(CommandError::UnknownCommand)
5642 ));
5643 }
5644
5645 #[test]
5646 fn operator_without_target_or_range_errors() {
5647 let (registry, b, mut doc) = fixture("abc");
5648 let inv = CommandInvocation::of(b.delete.0);
5649 assert!(matches!(
5650 execute(
5651 ®istry,
5652 &mut doc,
5653 lattice_core::BufferId(0),
5654 Position::ZERO,
5655 inv,
5656 &CancellationToken::never()
5657 ),
5658 Err(CommandError::MissingTarget)
5659 ));
5660 }
5661
5662 #[test]
5663 fn char_left_at_origin_stays_put() {
5664 let (registry, b, mut doc) = fixture("abc");
5665 let inv = CommandInvocation::of(b.char_left.0);
5666 let effect = execute(
5667 ®istry,
5668 &mut doc,
5669 lattice_core::BufferId(0),
5670 Position::ZERO,
5671 inv,
5672 &CancellationToken::never(),
5673 )
5674 .unwrap();
5675 match effect {
5676 Effect::CursorMove(pos) => assert_eq!(pos, Position::ZERO),
5677 other => panic!("expected CursorMove, got {other:?}"),
5678 }
5679 }
5680
5681 #[test]
5682 fn char_right_advances_one_byte() {
5683 let (registry, b, mut doc) = fixture("abc");
5684 let inv = CommandInvocation::of(b.char_right.0);
5685 let effect = execute(
5686 ®istry,
5687 &mut doc,
5688 lattice_core::BufferId(0),
5689 Position::ZERO,
5690 inv,
5691 &CancellationToken::never(),
5692 )
5693 .unwrap();
5694 match effect {
5695 Effect::CursorMove(pos) => assert_eq!(pos, Position::new(0, 1)),
5696 other => panic!("expected CursorMove, got {other:?}"),
5697 }
5698 }
5699
5700 #[test]
5701 fn char_right_advances_whole_multibyte_scalar() {
5702 let (registry, b, mut doc) = fixture("│x");
5706 let inv = CommandInvocation::of(b.char_right.0);
5707 let effect = execute(
5708 ®istry,
5709 &mut doc,
5710 lattice_core::BufferId(0),
5711 Position::ZERO,
5712 inv,
5713 &CancellationToken::never(),
5714 )
5715 .unwrap();
5716 match effect {
5717 Effect::CursorMove(pos) => assert_eq!(pos, Position::new(0, 3)),
5718 other => panic!("expected CursorMove, got {other:?}"),
5719 }
5720 }
5721
5722 #[test]
5723 fn char_left_retreats_whole_multibyte_scalar() {
5724 let (registry, b, mut doc) = fixture("│x");
5727 let inv = CommandInvocation::of(b.char_left.0);
5728 let effect = execute(
5729 ®istry,
5730 &mut doc,
5731 lattice_core::BufferId(0),
5732 Position::new(0, 3),
5733 inv,
5734 &CancellationToken::never(),
5735 )
5736 .unwrap();
5737 match effect {
5738 Effect::CursorMove(pos) => assert_eq!(pos, Position::ZERO),
5739 other => panic!("expected CursorMove, got {other:?}"),
5740 }
5741 }
5742
5743 #[test]
5744 fn char_right_at_end_of_line_stays_put() {
5745 let (registry, b, mut doc) = fixture("ab");
5746 let inv = CommandInvocation::of(b.char_right.0);
5747 let effect = execute(
5748 ®istry,
5749 &mut doc,
5750 lattice_core::BufferId(0),
5751 Position::new(0, 2),
5752 inv,
5753 &CancellationToken::never(),
5754 )
5755 .unwrap();
5756 match effect {
5757 Effect::CursorMove(pos) => assert_eq!(pos, Position::new(0, 2)),
5758 other => panic!("expected CursorMove, got {other:?}"),
5759 }
5760 }
5761
5762 #[test]
5763 fn line_down_moves_one_line_and_clamps_byte() {
5764 let (registry, b, mut doc) = fixture("hello\nhi");
5765 let inv = CommandInvocation::of(b.line_down.0);
5766 let effect = execute(
5767 ®istry,
5768 &mut doc,
5769 lattice_core::BufferId(0),
5770 Position::new(0, 5),
5771 inv,
5772 &CancellationToken::never(),
5773 )
5774 .unwrap();
5775 match effect {
5776 Effect::CursorMove(pos) => assert_eq!(pos, Position::new(1, 2)),
5777 other => panic!("expected CursorMove, got {other:?}"),
5778 }
5779 }
5780
5781 #[test]
5784 fn line_up_at_top_fails() {
5785 let (registry, b, mut doc) = fixture("a\nb");
5786 let inv = CommandInvocation::of(b.line_up.0);
5787 let result = execute(
5788 ®istry,
5789 &mut doc,
5790 lattice_core::BufferId(0),
5791 Position::ZERO,
5792 inv,
5793 &CancellationToken::never(),
5794 );
5795 assert!(
5796 matches!(result, Err(CommandError::MotionFailed)),
5797 "got {result:?}"
5798 );
5799 }
5800
5801 #[test]
5803 fn line_down_on_the_last_line_fails() {
5804 let (registry, b, mut doc) = fixture("a\nb");
5805 let inv = CommandInvocation::of(b.line_down.0);
5806 let result = execute(
5807 ®istry,
5808 &mut doc,
5809 lattice_core::BufferId(0),
5810 Position::new(1, 0),
5811 inv,
5812 &CancellationToken::never(),
5813 );
5814 assert!(
5815 matches!(result, Err(CommandError::MotionFailed)),
5816 "got {result:?}"
5817 );
5818 }
5819
5820 #[test]
5821 fn line_start_resets_byte_to_zero() {
5822 let (registry, b, mut doc) = fixture("hello world");
5823 let inv = CommandInvocation::of(b.line_start.0);
5824 let effect = execute(
5825 ®istry,
5826 &mut doc,
5827 lattice_core::BufferId(0),
5828 Position::new(0, 7),
5829 inv,
5830 &CancellationToken::never(),
5831 )
5832 .unwrap();
5833 match effect {
5834 Effect::CursorMove(pos) => assert_eq!(pos, Position::new(0, 0)),
5835 other => panic!("expected CursorMove, got {other:?}"),
5836 }
5837 }
5838
5839 #[test]
5840 fn line_end_jumps_to_line_byte_length() {
5841 let (registry, b, mut doc) = fixture("hello world");
5842 let inv = CommandInvocation::of(b.line_end.0);
5843 let effect = execute(
5844 ®istry,
5845 &mut doc,
5846 lattice_core::BufferId(0),
5847 Position::ZERO,
5848 inv,
5849 &CancellationToken::never(),
5850 )
5851 .unwrap();
5852 match effect {
5853 Effect::CursorMove(pos) => assert_eq!(pos, Position::new(0, 11)),
5854 other => panic!("expected CursorMove, got {other:?}"),
5855 }
5856 }
5857
5858 #[test]
5859 fn goto_first_line_returns_to_origin() {
5860 let (registry, b, mut doc) = fixture("a\nb\nc");
5861 let inv = CommandInvocation::of(b.goto_first_line.0);
5862 let effect = execute(
5863 ®istry,
5864 &mut doc,
5865 lattice_core::BufferId(0),
5866 Position::new(2, 0),
5867 inv,
5868 &CancellationToken::never(),
5869 )
5870 .unwrap();
5871 match effect {
5872 Effect::CursorMove(pos) => assert_eq!(pos, Position::ZERO),
5873 other => panic!("expected CursorMove, got {other:?}"),
5874 }
5875 }
5876
5877 #[test]
5878 fn goto_last_line_jumps_to_last_addressable_line() {
5879 let (registry, b, mut doc) = fixture("a\nb\nc");
5880 let inv = CommandInvocation::of(b.goto_last_line.0);
5881 let effect = execute(
5882 ®istry,
5883 &mut doc,
5884 lattice_core::BufferId(0),
5885 Position::ZERO,
5886 inv,
5887 &CancellationToken::never(),
5888 )
5889 .unwrap();
5890 match effect {
5891 Effect::CursorMove(pos) => assert_eq!(pos, Position::new(2, 0)),
5892 other => panic!("expected CursorMove, got {other:?}"),
5893 }
5894 }
5895
5896 #[test]
5897 fn goto_last_line_with_count_goes_to_specific_line() {
5898 let (registry, b, mut doc) = fixture("a\nb\nc\nd\ne");
5900 let inv = CommandInvocation::of(b.goto_last_line.0).with_count(Count(3));
5901 let effect = execute(
5902 ®istry,
5903 &mut doc,
5904 lattice_core::BufferId(0),
5905 Position::ZERO,
5906 inv,
5907 &CancellationToken::never(),
5908 )
5909 .unwrap();
5910 match effect {
5911 Effect::CursorMove(pos) => assert_eq!(pos, Position::new(2, 0)),
5912 other => panic!("expected CursorMove, got {other:?}"),
5913 }
5914 }
5915
5916 #[test]
5917 fn goto_first_line_with_count_goes_to_specific_line() {
5918 let (registry, b, mut doc) = fixture("a\nb\nc\nd\ne");
5920 let inv = CommandInvocation::of(b.goto_first_line.0).with_count(Count(3));
5921 let effect = execute(
5922 ®istry,
5923 &mut doc,
5924 lattice_core::BufferId(0),
5925 Position::new(4, 0),
5926 inv,
5927 &CancellationToken::never(),
5928 )
5929 .unwrap();
5930 match effect {
5931 Effect::CursorMove(pos) => assert_eq!(pos, Position::new(2, 0)),
5932 other => panic!("expected CursorMove, got {other:?}"),
5933 }
5934 }
5935
5936 #[test]
5937 fn x_semantics_via_delete_with_char_right_target() {
5938 let (registry, b, mut doc) = fixture("hello");
5940 let inv = CommandInvocation::of(b.delete.0)
5941 .with_target(Target::Motion(b.char_right, crate::args::Args::None));
5942 let effect = execute(
5943 ®istry,
5944 &mut doc,
5945 lattice_core::BufferId(0),
5946 Position::ZERO,
5947 inv,
5948 &CancellationToken::never(),
5949 )
5950 .unwrap();
5951 match effect {
5952 Effect::Many(parts) => {
5953 assert!(matches!(parts[0], Effect::Edits(_)));
5954 assert!(matches!(parts[1], Effect::Yank { .. }));
5955 }
5956 other => panic!("expected Many, got {other:?}"),
5957 }
5958 assert_eq!(doc.text(), "ello");
5959 }
5960
5961 #[test]
5964 fn word_backward_from_mid_word_lands_at_start_of_word() {
5965 let (registry, b, mut doc) = fixture("hello world");
5966 let inv = CommandInvocation::of(b.word_backward.0);
5968 let effect = execute(
5969 ®istry,
5970 &mut doc,
5971 lattice_core::BufferId(0),
5972 Position::new(0, 8),
5973 inv,
5974 &CancellationToken::never(),
5975 )
5976 .unwrap();
5977 match effect {
5978 Effect::CursorMove(pos) => assert_eq!(pos, Position::new(0, 6)),
5979 other => panic!("expected CursorMove, got {other:?}"),
5980 }
5981 }
5982
5983 #[test]
5984 fn word_backward_from_start_of_word_lands_at_start_of_previous_word() {
5985 let (registry, b, mut doc) = fixture("one two three");
5986 let inv = CommandInvocation::of(b.word_backward.0);
5988 let effect = execute(
5989 ®istry,
5990 &mut doc,
5991 lattice_core::BufferId(0),
5992 Position::new(0, 8),
5993 inv,
5994 &CancellationToken::never(),
5995 )
5996 .unwrap();
5997 match effect {
5998 Effect::CursorMove(pos) => assert_eq!(pos, Position::new(0, 4)),
5999 other => panic!("expected CursorMove, got {other:?}"),
6000 }
6001 }
6002
6003 #[test]
6004 fn word_backward_at_origin_stays_put() {
6005 let (registry, b, mut doc) = fixture("hello");
6006 let inv = CommandInvocation::of(b.word_backward.0);
6007 let effect = execute(
6008 ®istry,
6009 &mut doc,
6010 lattice_core::BufferId(0),
6011 Position::ZERO,
6012 inv,
6013 &CancellationToken::never(),
6014 )
6015 .unwrap();
6016 match effect {
6017 Effect::CursorMove(pos) => assert_eq!(pos, Position::ZERO),
6018 other => panic!("expected CursorMove, got {other:?}"),
6019 }
6020 }
6021
6022 #[test]
6023 fn word_backward_crosses_newlines() {
6024 let (registry, b, mut doc) = fixture("foo\nbar");
6025 let inv = CommandInvocation::of(b.word_backward.0);
6027 let effect = execute(
6028 ®istry,
6029 &mut doc,
6030 lattice_core::BufferId(0),
6031 Position::new(1, 0),
6032 inv,
6033 &CancellationToken::never(),
6034 )
6035 .unwrap();
6036 match effect {
6037 Effect::CursorMove(pos) => assert_eq!(pos, Position::ZERO),
6038 other => panic!("expected CursorMove, got {other:?}"),
6039 }
6040 }
6041
6042 #[test]
6043 fn word_backward_with_count_jumps_count_words() {
6044 let (registry, b, mut doc) = fixture("one two three four");
6045 let inv = CommandInvocation::of(b.word_backward.0).with_count(Count(2));
6047 let effect = execute(
6048 ®istry,
6049 &mut doc,
6050 lattice_core::BufferId(0),
6051 Position::new(0, 14),
6052 inv,
6053 &CancellationToken::never(),
6054 )
6055 .unwrap();
6056 match effect {
6057 Effect::CursorMove(pos) => assert_eq!(pos, Position::new(0, 4)),
6058 other => panic!("expected CursorMove, got {other:?}"),
6059 }
6060 }
6061
6062 #[test]
6063 fn word_backward_lands_on_punctuation_word() {
6064 let (registry, b, mut doc) = fixture("alpha, beta");
6065 let inv = CommandInvocation::of(b.word_backward.0);
6069 let effect = execute(
6070 ®istry,
6071 &mut doc,
6072 lattice_core::BufferId(0),
6073 Position::new(0, 7),
6074 inv,
6075 &CancellationToken::never(),
6076 )
6077 .unwrap();
6078 match effect {
6079 Effect::CursorMove(pos) => assert_eq!(pos, Position::new(0, 5)),
6080 other => panic!("expected CursorMove, got {other:?}"),
6081 }
6082 }
6083
6084 #[test]
6087 fn word_end_from_start_of_word_lands_at_last_byte_of_word() {
6088 let (registry, b, mut doc) = fixture("hello world");
6089 let inv = CommandInvocation::of(b.word_end.0);
6091 let effect = execute(
6092 ®istry,
6093 &mut doc,
6094 lattice_core::BufferId(0),
6095 Position::ZERO,
6096 inv,
6097 &CancellationToken::never(),
6098 )
6099 .unwrap();
6100 match effect {
6101 Effect::CursorMove(pos) => assert_eq!(pos, Position::new(0, 4)),
6102 other => panic!("expected CursorMove, got {other:?}"),
6103 }
6104 }
6105
6106 #[test]
6107 fn word_end_from_end_of_word_lands_at_end_of_next_word() {
6108 let (registry, b, mut doc) = fixture("hello world");
6109 let inv = CommandInvocation::of(b.word_end.0);
6111 let effect = execute(
6112 ®istry,
6113 &mut doc,
6114 lattice_core::BufferId(0),
6115 Position::new(0, 4),
6116 inv,
6117 &CancellationToken::never(),
6118 )
6119 .unwrap();
6120 match effect {
6121 Effect::CursorMove(pos) => assert_eq!(pos, Position::new(0, 10)),
6122 other => panic!("expected CursorMove, got {other:?}"),
6123 }
6124 }
6125
6126 #[test]
6127 fn word_end_at_buffer_end_stays_put() {
6128 let (registry, b, mut doc) = fixture("hi");
6129 let inv = CommandInvocation::of(b.word_end.0);
6131 let effect = execute(
6132 ®istry,
6133 &mut doc,
6134 lattice_core::BufferId(0),
6135 Position::new(0, 1),
6136 inv,
6137 &CancellationToken::never(),
6138 )
6139 .unwrap();
6140 match effect {
6141 Effect::CursorMove(pos) => assert_eq!(pos, Position::new(0, 1)),
6142 other => panic!("expected CursorMove, got {other:?}"),
6143 }
6144 }
6145
6146 #[test]
6147 fn word_end_crosses_newlines() {
6148 let (registry, b, mut doc) = fixture("foo\nbar");
6149 let inv = CommandInvocation::of(b.word_end.0);
6151 let effect = execute(
6152 ®istry,
6153 &mut doc,
6154 lattice_core::BufferId(0),
6155 Position::new(0, 2),
6156 inv,
6157 &CancellationToken::never(),
6158 )
6159 .unwrap();
6160 match effect {
6161 Effect::CursorMove(pos) => assert_eq!(pos, Position::new(1, 2)),
6162 other => panic!("expected CursorMove, got {other:?}"),
6163 }
6164 }
6165
6166 #[test]
6167 fn word_end_with_count_advances_by_count() {
6168 let (registry, b, mut doc) = fixture("one two three four");
6169 let inv = CommandInvocation::of(b.word_end.0).with_count(Count(2));
6171 let effect = execute(
6172 ®istry,
6173 &mut doc,
6174 lattice_core::BufferId(0),
6175 Position::ZERO,
6176 inv,
6177 &CancellationToken::never(),
6178 )
6179 .unwrap();
6180 match effect {
6181 Effect::CursorMove(pos) => assert_eq!(pos, Position::new(0, 6)),
6182 other => panic!("expected CursorMove, got {other:?}"),
6183 }
6184 }
6185
6186 #[test]
6189 fn first_non_blank_skips_leading_spaces() {
6190 let (registry, b, mut doc) = fixture(" hello");
6191 let inv = CommandInvocation::of(b.first_non_blank.0);
6192 let effect = execute(
6193 ®istry,
6194 &mut doc,
6195 lattice_core::BufferId(0),
6196 Position::ZERO,
6197 inv,
6198 &CancellationToken::never(),
6199 )
6200 .unwrap();
6201 match effect {
6202 Effect::CursorMove(pos) => assert_eq!(pos, Position::new(0, 4)),
6203 other => panic!("expected CursorMove, got {other:?}"),
6204 }
6205 }
6206
6207 #[test]
6208 fn first_non_blank_skips_leading_tabs() {
6209 let (registry, b, mut doc) = fixture("\t\thello");
6210 let inv = CommandInvocation::of(b.first_non_blank.0);
6211 let effect = execute(
6212 ®istry,
6213 &mut doc,
6214 lattice_core::BufferId(0),
6215 Position::ZERO,
6216 inv,
6217 &CancellationToken::never(),
6218 )
6219 .unwrap();
6220 match effect {
6221 Effect::CursorMove(pos) => assert_eq!(pos, Position::new(0, 2)),
6222 other => panic!("expected CursorMove, got {other:?}"),
6223 }
6224 }
6225
6226 #[test]
6227 fn first_non_blank_on_already_non_blank_line_returns_zero() {
6228 let (registry, b, mut doc) = fixture("hello");
6229 let inv = CommandInvocation::of(b.first_non_blank.0);
6230 let effect = execute(
6231 ®istry,
6232 &mut doc,
6233 lattice_core::BufferId(0),
6234 Position::new(0, 3),
6235 inv,
6236 &CancellationToken::never(),
6237 )
6238 .unwrap();
6239 match effect {
6240 Effect::CursorMove(pos) => assert_eq!(pos, Position::ZERO),
6241 other => panic!("expected CursorMove, got {other:?}"),
6242 }
6243 }
6244
6245 #[test]
6246 fn first_non_blank_on_blank_only_line_returns_end() {
6247 let (registry, b, mut doc) = fixture(" ");
6248 let inv = CommandInvocation::of(b.first_non_blank.0);
6249 let effect = execute(
6250 ®istry,
6251 &mut doc,
6252 lattice_core::BufferId(0),
6253 Position::ZERO,
6254 inv,
6255 &CancellationToken::never(),
6256 )
6257 .unwrap();
6258 match effect {
6259 Effect::CursorMove(pos) => {
6260 assert_eq!(pos, Position::new(0, 4));
6262 }
6263 other => panic!("expected CursorMove, got {other:?}"),
6264 }
6265 }
6266
6267 #[test]
6268 fn first_non_blank_respects_current_line() {
6269 let (registry, b, mut doc) = fixture("a\n bc");
6270 let inv = CommandInvocation::of(b.first_non_blank.0);
6272 let effect = execute(
6273 ®istry,
6274 &mut doc,
6275 lattice_core::BufferId(0),
6276 Position::new(1, 4),
6277 inv,
6278 &CancellationToken::never(),
6279 )
6280 .unwrap();
6281 match effect {
6282 Effect::CursorMove(pos) => assert_eq!(pos, Position::new(1, 2)),
6283 other => panic!("expected CursorMove, got {other:?}"),
6284 }
6285 }
6286
6287 #[test]
6290 fn delete_with_word_backward_target_db_semantics() {
6291 let (registry, b, mut doc) = fixture("hello world");
6294 let inv = CommandInvocation::of(b.delete.0)
6295 .with_target(Target::Motion(b.word_backward, crate::args::Args::None));
6296 execute(
6297 ®istry,
6298 &mut doc,
6299 lattice_core::BufferId(0),
6300 Position::new(0, 11),
6301 inv,
6302 &CancellationToken::never(),
6303 )
6304 .unwrap();
6305 assert_eq!(doc.text(), "hello ");
6306 }
6307
6308 #[test]
6311 fn indent_right_current_line_prepends_four_spaces() {
6312 let (registry, b, mut doc) = fixture("hello");
6313 let inv =
6314 CommandInvocation::of(b.indent_right.0).with_range(crate::range::Range::CurrentLine);
6315 execute(
6316 ®istry,
6317 &mut doc,
6318 lattice_core::BufferId(0),
6319 Position::ZERO,
6320 inv,
6321 &CancellationToken::never(),
6322 )
6323 .unwrap();
6324 assert_eq!(doc.text(), " hello");
6325 }
6326
6327 #[test]
6328 fn indent_left_current_line_strips_four_spaces() {
6329 let (registry, b, mut doc) = fixture(" hello");
6330 let inv =
6331 CommandInvocation::of(b.indent_left.0).with_range(crate::range::Range::CurrentLine);
6332 execute(
6333 ®istry,
6334 &mut doc,
6335 lattice_core::BufferId(0),
6336 Position::ZERO,
6337 inv,
6338 &CancellationToken::never(),
6339 )
6340 .unwrap();
6341 assert_eq!(doc.text(), "hello");
6342 }
6343
6344 #[test]
6345 fn indent_left_strips_partial_indent() {
6346 let (registry, b, mut doc) = fixture(" hello");
6347 let inv =
6348 CommandInvocation::of(b.indent_left.0).with_range(crate::range::Range::CurrentLine);
6349 execute(
6350 ®istry,
6351 &mut doc,
6352 lattice_core::BufferId(0),
6353 Position::ZERO,
6354 inv,
6355 &CancellationToken::never(),
6356 )
6357 .unwrap();
6358 assert_eq!(doc.text(), "hello");
6360 }
6361
6362 #[test]
6363 fn indent_left_strips_leading_tab() {
6364 let (registry, b, mut doc) = fixture("\thello");
6365 let inv =
6366 CommandInvocation::of(b.indent_left.0).with_range(crate::range::Range::CurrentLine);
6367 execute(
6368 ®istry,
6369 &mut doc,
6370 lattice_core::BufferId(0),
6371 Position::ZERO,
6372 inv,
6373 &CancellationToken::never(),
6374 )
6375 .unwrap();
6376 assert_eq!(doc.text(), "hello");
6377 }
6378
6379 #[test]
6380 fn indent_left_no_indent_is_no_op() {
6381 let (registry, b, mut doc) = fixture("hello");
6382 let inv =
6383 CommandInvocation::of(b.indent_left.0).with_range(crate::range::Range::CurrentLine);
6384 execute(
6385 ®istry,
6386 &mut doc,
6387 lattice_core::BufferId(0),
6388 Position::ZERO,
6389 inv,
6390 &CancellationToken::never(),
6391 )
6392 .unwrap();
6393 assert_eq!(doc.text(), "hello");
6394 }
6395
6396 #[test]
6397 fn indent_right_with_whole_range_indents_every_line() {
6398 let (registry, b, mut doc) = fixture("a\nb\nc");
6399 let inv = CommandInvocation::of(b.indent_right.0).with_range(crate::range::Range::Whole);
6400 execute(
6401 ®istry,
6402 &mut doc,
6403 lattice_core::BufferId(0),
6404 Position::ZERO,
6405 inv,
6406 &CancellationToken::never(),
6407 )
6408 .unwrap();
6409 assert_eq!(doc.text(), " a\n b\n c");
6410 }
6411
6412 fn indent_shift(text: &str, unit: lattice_core::IndentUnit, right: bool) -> String {
6418 let (registry, b, mut doc) = fixture(text);
6419 let op = if right { b.indent_right } else { b.indent_left };
6420 let inv = CommandInvocation::of(op.0).with_range(crate::range::Range::Whole);
6421 crate::execute_with_env(
6422 ®istry,
6423 &mut doc,
6424 lattice_core::BufferId(0),
6425 Position::ZERO,
6426 inv,
6427 &CancellationToken::never(),
6428 crate::registry::GrammarEnv {
6429 indent: unit,
6430 ..Default::default()
6431 },
6432 )
6433 .unwrap();
6434 doc.text()
6435 }
6436
6437 #[test]
6438 fn indent_right_honours_shiftwidth() {
6439 let sw2 = lattice_core::IndentUnit::new(2, true, 4);
6440 assert_eq!(indent_shift("a", sw2, true), " a");
6441 let sw8 = lattice_core::IndentUnit::new(8, true, 4);
6442 assert_eq!(indent_shift("a", sw8, true), " a");
6443 }
6444
6445 #[test]
6446 fn indent_right_honours_noexpandtab() {
6447 let tabs = lattice_core::IndentUnit::new(4, false, 4);
6449 assert_eq!(indent_shift("a", tabs, true), "\ta");
6450 let tabs8 = lattice_core::IndentUnit::new(4, false, 8);
6452 assert_eq!(indent_shift(" a", tabs8, true), "\ta");
6453 }
6454
6455 #[test]
6456 fn a_tab_and_tabstop_spaces_dedent_identically() {
6457 let u = lattice_core::IndentUnit::new(2, true, 4);
6461 assert_eq!(indent_shift("\ta", u, false), " a");
6462 assert_eq!(indent_shift(" a", u, false), " a");
6463 }
6464
6465 #[test]
6466 fn indent_right_leaves_blank_lines_alone() {
6467 let u = lattice_core::IndentUnit::default();
6470 assert_eq!(indent_shift("a\n\nb", u, true), " a\n\n b");
6471 assert_eq!(indent_shift("a\n \nb", u, true), " a\n \n b");
6472 }
6473
6474 #[test]
6475 fn dedent_at_column_zero_emits_no_edit() {
6476 let (registry, b, mut doc) = fixture("a\nb");
6479 let inv = CommandInvocation::of(b.indent_left.0).with_range(crate::range::Range::Whole);
6480 let eff = execute(
6481 ®istry,
6482 &mut doc,
6483 lattice_core::BufferId(0),
6484 Position::ZERO,
6485 inv,
6486 &CancellationToken::never(),
6487 )
6488 .unwrap();
6489 assert!(matches!(eff, Effect::None), "expected no edit, got {eff:?}");
6490 assert_eq!(doc.text(), "a\nb");
6491 }
6492
6493 #[test]
6494 fn indent_left_with_whole_range_dedents_every_line() {
6495 let (registry, b, mut doc) = fixture(" a\n b\n c");
6496 let inv = CommandInvocation::of(b.indent_left.0).with_range(crate::range::Range::Whole);
6497 execute(
6498 ®istry,
6499 &mut doc,
6500 lattice_core::BufferId(0),
6501 Position::ZERO,
6502 inv,
6503 &CancellationToken::never(),
6504 )
6505 .unwrap();
6506 assert_eq!(doc.text(), "a\nb\nc");
6507 }
6508
6509 #[test]
6512 fn upper_uppercases_word_target() {
6513 let (registry, b, mut doc) = fixture("hello world");
6514 let inv = CommandInvocation::of(b.upper.0)
6515 .with_target(Target::Motion(b.word_forward, crate::args::Args::None));
6516 execute(
6517 ®istry,
6518 &mut doc,
6519 lattice_core::BufferId(0),
6520 Position::ZERO,
6521 inv,
6522 &CancellationToken::never(),
6523 )
6524 .unwrap();
6525 assert_eq!(doc.text(), "HELLO world");
6527 }
6528
6529 #[test]
6530 fn lower_lowercases_range() {
6531 let (registry, b, mut doc) = fixture("HELLO WORLD");
6532 let inv = CommandInvocation::of(b.lower.0).with_range(crate::range::Range::Whole);
6533 execute(
6534 ®istry,
6535 &mut doc,
6536 lattice_core::BufferId(0),
6537 Position::ZERO,
6538 inv,
6539 &CancellationToken::never(),
6540 )
6541 .unwrap();
6542 assert_eq!(doc.text(), "hello world");
6543 }
6544
6545 #[test]
6546 fn toggle_case_inverts_each_letter() {
6547 let (registry, b, mut doc) = fixture("Hello World");
6548 let inv = CommandInvocation::of(b.toggle_case.0).with_range(crate::range::Range::Whole);
6549 execute(
6550 ®istry,
6551 &mut doc,
6552 lattice_core::BufferId(0),
6553 Position::ZERO,
6554 inv,
6555 &CancellationToken::never(),
6556 )
6557 .unwrap();
6558 assert_eq!(doc.text(), "hELLO wORLD");
6559 }
6560
6561 #[test]
6562 fn upper_with_no_letters_is_no_op() {
6563 let (registry, b, mut doc) = fixture("123 !@# 456");
6564 let inv = CommandInvocation::of(b.upper.0).with_range(crate::range::Range::Whole);
6565 let effect = execute(
6566 ®istry,
6567 &mut doc,
6568 lattice_core::BufferId(0),
6569 Position::ZERO,
6570 inv,
6571 &CancellationToken::never(),
6572 )
6573 .unwrap();
6574 assert!(matches!(effect, Effect::None));
6576 assert_eq!(doc.text(), "123 !@# 456");
6577 }
6578
6579 #[test]
6580 fn case_operators_preserve_non_letter_bytes() {
6581 let (registry, b, mut doc) = fixture("foo_bar.baz");
6582 let inv = CommandInvocation::of(b.upper.0).with_range(crate::range::Range::Whole);
6583 execute(
6584 ®istry,
6585 &mut doc,
6586 lattice_core::BufferId(0),
6587 Position::ZERO,
6588 inv,
6589 &CancellationToken::never(),
6590 )
6591 .unwrap();
6592 assert_eq!(doc.text(), "FOO_BAR.BAZ");
6594 }
6595
6596 fn invoke_textobj(
6599 op: crate::registry::OperatorId,
6600 tobj: crate::registry::TextObjectId,
6601 ) -> CommandInvocation {
6602 CommandInvocation::of(op.0).with_target(Target::TextObject(tobj, crate::args::Args::None))
6603 }
6604
6605 #[test]
6606 fn iw_inner_word_covers_word_at_cursor() {
6607 let (registry, b, mut doc) = fixture("hello world");
6609 let inv = invoke_textobj(b.delete, b.inner_word);
6610 execute(
6611 ®istry,
6612 &mut doc,
6613 lattice_core::BufferId(0),
6614 Position::new(0, 2),
6615 inv,
6616 &CancellationToken::never(),
6617 )
6618 .unwrap();
6619 assert_eq!(doc.text(), " world");
6620 }
6621
6622 #[test]
6623 fn iw_at_start_of_word_works() {
6624 let (registry, b, mut doc) = fixture("hello world");
6625 let inv = invoke_textobj(b.delete, b.inner_word);
6626 execute(
6627 ®istry,
6628 &mut doc,
6629 lattice_core::BufferId(0),
6630 Position::ZERO,
6631 inv,
6632 &CancellationToken::never(),
6633 )
6634 .unwrap();
6635 assert_eq!(doc.text(), " world");
6636 }
6637
6638 #[test]
6639 fn iw_on_single_punctuation_deletes_it() {
6640 let (registry, b, mut doc) = fixture("foo.bar");
6642 let inv = invoke_textobj(b.delete, b.inner_word);
6643 execute(
6645 ®istry,
6646 &mut doc,
6647 lattice_core::BufferId(0),
6648 Position::new(0, 3),
6649 inv,
6650 &CancellationToken::never(),
6651 )
6652 .unwrap();
6653 assert_eq!(doc.text(), "foobar");
6654 }
6655
6656 #[test]
6657 fn iw_on_punctuation_covers_the_whole_run() {
6658 let (registry, b, mut doc) = fixture("foo...bar");
6660 let inv = invoke_textobj(b.delete, b.inner_word);
6661 execute(
6663 ®istry,
6664 &mut doc,
6665 lattice_core::BufferId(0),
6666 Position::new(0, 4),
6667 inv,
6668 &CancellationToken::never(),
6669 )
6670 .unwrap();
6671 assert_eq!(doc.text(), "foobar");
6672 }
6673
6674 #[test]
6675 fn iw_on_whitespace_deletes_the_whitespace_run() {
6676 let (registry, b, mut doc) = fixture("hello world");
6679 let inv = invoke_textobj(b.delete, b.inner_word);
6680 execute(
6682 ®istry,
6683 &mut doc,
6684 lattice_core::BufferId(0),
6685 Position::new(0, 5),
6686 inv,
6687 &CancellationToken::never(),
6688 )
6689 .unwrap();
6690 assert_eq!(doc.text(), "helloworld");
6691 }
6692
6693 #[test]
6694 fn aw_around_word_includes_trailing_whitespace() {
6695 let (registry, b, mut doc) = fixture("hello world");
6696 let inv = invoke_textobj(b.delete, b.around_word);
6697 execute(
6698 ®istry,
6699 &mut doc,
6700 lattice_core::BufferId(0),
6701 Position::new(0, 2),
6702 inv,
6703 &CancellationToken::never(),
6704 )
6705 .unwrap();
6706 assert_eq!(doc.text(), "world");
6708 }
6709
6710 #[test]
6711 fn aw_on_last_word_takes_leading_whitespace() {
6712 let (registry, b, mut doc) = fixture("hello world");
6713 let inv = invoke_textobj(b.delete, b.around_word);
6714 execute(
6716 ®istry,
6717 &mut doc,
6718 lattice_core::BufferId(0),
6719 Position::new(0, 6),
6720 inv,
6721 &CancellationToken::never(),
6722 )
6723 .unwrap();
6724 assert_eq!(doc.text(), "hello");
6725 }
6726
6727 #[test]
6728 #[allow(non_snake_case)]
6729 fn iW_treats_punctuation_as_part_of_big_word() {
6730 let (registry, b, mut doc) = fixture("foo.bar baz");
6733 let inv = invoke_textobj(b.delete, b.inner_big_word);
6734 execute(
6735 ®istry,
6736 &mut doc,
6737 lattice_core::BufferId(0),
6738 Position::new(0, 2),
6739 inv,
6740 &CancellationToken::never(),
6741 )
6742 .unwrap();
6743 assert_eq!(doc.text(), " baz");
6744 }
6745
6746 #[test]
6747 #[allow(non_snake_case)]
6748 fn iW_on_whitespace_deletes_the_whitespace_run() {
6749 let (registry, b, mut doc) = fixture("hello world");
6752 let inv = invoke_textobj(b.delete, b.inner_big_word);
6753 execute(
6755 ®istry,
6756 &mut doc,
6757 lattice_core::BufferId(0),
6758 Position::new(0, 5),
6759 inv,
6760 &CancellationToken::never(),
6761 )
6762 .unwrap();
6763 assert_eq!(doc.text(), "helloworld");
6764 }
6765
6766 #[test]
6767 #[allow(non_snake_case)]
6768 fn aW_around_big_word_includes_trailing_whitespace() {
6769 let (registry, b, mut doc) = fixture("foo.bar baz");
6770 let inv = invoke_textobj(b.delete, b.around_big_word);
6771 execute(
6772 ®istry,
6773 &mut doc,
6774 lattice_core::BufferId(0),
6775 Position::new(0, 0),
6776 inv,
6777 &CancellationToken::never(),
6778 )
6779 .unwrap();
6780 assert_eq!(doc.text(), "baz");
6782 }
6783
6784 #[test]
6785 fn i_angle_covers_inside_pair() {
6786 let (registry, b, mut doc) = fixture("Vec<String>");
6787 let inv = invoke_textobj(b.delete, b.inner_angle);
6788 execute(
6790 ®istry,
6791 &mut doc,
6792 lattice_core::BufferId(0),
6793 Position::new(0, 5),
6794 inv,
6795 &CancellationToken::never(),
6796 )
6797 .unwrap();
6798 assert_eq!(doc.text(), "Vec<>");
6799 }
6800
6801 #[test]
6802 fn a_angle_covers_pair_including_brackets() {
6803 let (registry, b, mut doc) = fixture("Vec<String>");
6804 let inv = invoke_textobj(b.delete, b.around_angle);
6805 execute(
6806 ®istry,
6807 &mut doc,
6808 lattice_core::BufferId(0),
6809 Position::new(0, 5),
6810 inv,
6811 &CancellationToken::never(),
6812 )
6813 .unwrap();
6814 assert_eq!(doc.text(), "Vec");
6815 }
6816
6817 #[test]
6818 fn i_double_quote_covers_quoted_content_only() {
6819 let (registry, b, mut doc) = fixture(r#"foo "bar baz" qux"#);
6820 let inv = invoke_textobj(b.delete, b.inner_quote_double);
6821 execute(
6823 ®istry,
6824 &mut doc,
6825 lattice_core::BufferId(0),
6826 Position::new(0, 6),
6827 inv,
6828 &CancellationToken::never(),
6829 )
6830 .unwrap();
6831 assert_eq!(doc.text(), r#"foo "" qux"#);
6832 }
6833
6834 #[test]
6835 fn a_double_quote_covers_quoted_content_and_quotes() {
6836 let (registry, b, mut doc) = fixture(r#"foo "bar baz" qux"#);
6837 let inv = invoke_textobj(b.delete, b.around_quote_double);
6838 execute(
6839 ®istry,
6840 &mut doc,
6841 lattice_core::BufferId(0),
6842 Position::new(0, 6),
6843 inv,
6844 &CancellationToken::never(),
6845 )
6846 .unwrap();
6847 assert_eq!(doc.text(), "foo qux");
6848 }
6849
6850 #[test]
6851 fn i_single_quote_works() {
6852 let (registry, b, mut doc) = fixture("foo 'bar' baz");
6853 let inv = invoke_textobj(b.delete, b.inner_quote_single);
6854 execute(
6855 ®istry,
6856 &mut doc,
6857 lattice_core::BufferId(0),
6858 Position::new(0, 6),
6859 inv,
6860 &CancellationToken::never(),
6861 )
6862 .unwrap();
6863 assert_eq!(doc.text(), "foo '' baz");
6864 }
6865
6866 #[test]
6867 fn i_paren_covers_inside_outermost_parens() {
6868 let (registry, b, mut doc) = fixture("call(arg1, arg2)");
6869 let inv = invoke_textobj(b.delete, b.inner_paren);
6870 execute(
6872 ®istry,
6873 &mut doc,
6874 lattice_core::BufferId(0),
6875 Position::new(0, 11),
6876 inv,
6877 &CancellationToken::never(),
6878 )
6879 .unwrap();
6880 assert_eq!(doc.text(), "call()");
6881 }
6882
6883 #[test]
6884 fn a_paren_includes_brackets() {
6885 let (registry, b, mut doc) = fixture("call(arg1, arg2)");
6886 let inv = invoke_textobj(b.delete, b.around_paren);
6887 execute(
6888 ®istry,
6889 &mut doc,
6890 lattice_core::BufferId(0),
6891 Position::new(0, 11),
6892 inv,
6893 &CancellationToken::never(),
6894 )
6895 .unwrap();
6896 assert_eq!(doc.text(), "call");
6897 }
6898
6899 #[test]
6900 fn nested_parens_picks_innermost() {
6901 let (registry, b, mut doc) = fixture("a(b(c)d)e");
6902 let inv = invoke_textobj(b.delete, b.inner_paren);
6903 execute(
6905 ®istry,
6906 &mut doc,
6907 lattice_core::BufferId(0),
6908 Position::new(0, 4),
6909 inv,
6910 &CancellationToken::never(),
6911 )
6912 .unwrap();
6913 assert_eq!(doc.text(), "a(b()d)e");
6914 }
6915
6916 #[test]
6917 fn i_bracket_works_for_square_brackets() {
6918 let (registry, b, mut doc) = fixture("arr[1, 2, 3]");
6919 let inv = invoke_textobj(b.delete, b.inner_bracket);
6920 execute(
6921 ®istry,
6922 &mut doc,
6923 lattice_core::BufferId(0),
6924 Position::new(0, 4),
6925 inv,
6926 &CancellationToken::never(),
6927 )
6928 .unwrap();
6929 assert_eq!(doc.text(), "arr[]");
6930 }
6931
6932 #[test]
6933 fn i_brace_works_for_curly_braces() {
6934 let (registry, b, mut doc) = fixture("fn body { return 42; }");
6935 let inv = invoke_textobj(b.delete, b.inner_brace);
6936 execute(
6938 ®istry,
6939 &mut doc,
6940 lattice_core::BufferId(0),
6941 Position::new(0, 12),
6942 inv,
6943 &CancellationToken::never(),
6944 )
6945 .unwrap();
6946 assert_eq!(doc.text(), "fn body {}");
6947 }
6948
6949 #[test]
6950 fn unmatched_bracket_is_no_op() {
6951 let (registry, b, mut doc) = fixture("no brackets here");
6952 let inv = invoke_textobj(b.delete, b.inner_paren);
6953 execute(
6954 ®istry,
6955 &mut doc,
6956 lattice_core::BufferId(0),
6957 Position::new(0, 5),
6958 inv,
6959 &CancellationToken::never(),
6960 )
6961 .unwrap();
6962 assert_eq!(doc.text(), "no brackets here");
6963 }
6964
6965 #[test]
6966 fn ciw_change_inner_word_enters_insert() {
6967 let (registry, b, mut doc) = fixture("hello world");
6968 let inv = CommandInvocation::of(b.change.0)
6969 .with_target(Target::TextObject(b.inner_word, crate::args::Args::None));
6970 let effect = execute(
6971 ®istry,
6972 &mut doc,
6973 lattice_core::BufferId(0),
6974 Position::new(0, 2),
6975 inv,
6976 &CancellationToken::never(),
6977 )
6978 .unwrap();
6979 match effect {
6980 Effect::Many(parts) => {
6981 assert!(matches!(
6982 parts[parts.len() - 1],
6983 Effect::EnterMode(crate::ModalState::Insert)
6984 ));
6985 }
6986 other => panic!("expected Many, got {other:?}"),
6987 }
6988 assert_eq!(doc.text(), " world");
6989 }
6990
6991 #[test]
6994 fn it_selects_inside_tag() {
6995 let (registry, b, mut doc) = fixture("<p>hello world</p>");
6996 let inv = CommandInvocation::of(b.delete.0)
6997 .with_target(Target::TextObject(b.inner_tag, crate::args::Args::None));
6998 execute(
7000 ®istry,
7001 &mut doc,
7002 lattice_core::BufferId(0),
7003 Position::new(0, 5),
7004 inv,
7005 &CancellationToken::never(),
7006 )
7007 .unwrap();
7008 assert_eq!(doc.text(), "<p></p>");
7009 }
7010
7011 #[test]
7012 fn at_selects_around_tag() {
7013 let (registry, b, mut doc) = fixture("<p>hello world</p>");
7014 let inv = CommandInvocation::of(b.delete.0)
7015 .with_target(Target::TextObject(b.around_tag, crate::args::Args::None));
7016 execute(
7017 ®istry,
7018 &mut doc,
7019 lattice_core::BufferId(0),
7020 Position::new(0, 5),
7021 inv,
7022 &CancellationToken::never(),
7023 )
7024 .unwrap();
7025 assert_eq!(doc.text(), "");
7026 }
7027
7028 #[test]
7029 fn it_with_no_enclosing_tag_is_no_op() {
7030 let (registry, b, mut doc) = fixture("plain text");
7031 let inv = CommandInvocation::of(b.delete.0)
7032 .with_target(Target::TextObject(b.inner_tag, crate::args::Args::None));
7033 execute(
7034 ®istry,
7035 &mut doc,
7036 lattice_core::BufferId(0),
7037 Position::new(0, 4),
7038 inv,
7039 &CancellationToken::never(),
7040 )
7041 .unwrap();
7042 assert_eq!(doc.text(), "plain text");
7043 }
7044
7045 #[test]
7048 fn sentence_forward_advances_after_period_space() {
7049 let (registry, b, mut doc) = fixture("First sentence. Second sentence. Third.");
7050 let inv = CommandInvocation::of(b.sentence_forward.0);
7051 let effect = execute(
7052 ®istry,
7053 &mut doc,
7054 lattice_core::BufferId(0),
7055 Position::ZERO,
7056 inv,
7057 &CancellationToken::never(),
7058 )
7059 .unwrap();
7060 match effect {
7061 Effect::CursorMove(pos) => {
7062 assert_eq!(pos, Position::new(0, 16));
7064 }
7065 other => panic!("expected CursorMove, got {other:?}"),
7066 }
7067 }
7068
7069 #[test]
7070 fn sentence_backward_returns_to_previous_start() {
7071 let (registry, b, mut doc) = fixture("First. Second.");
7072 let inv = CommandInvocation::of(b.sentence_backward.0);
7074 let effect = execute(
7075 ®istry,
7076 &mut doc,
7077 lattice_core::BufferId(0),
7078 Position::new(0, 7),
7079 inv,
7080 &CancellationToken::never(),
7081 )
7082 .unwrap();
7083 match effect {
7084 Effect::CursorMove(pos) => assert_eq!(pos, Position::ZERO),
7085 other => panic!("expected CursorMove, got {other:?}"),
7086 }
7087 }
7088
7089 #[test]
7090 fn dis_deletes_inner_sentence() {
7091 let (registry, b, mut doc) = fixture("First sentence. Second sentence.");
7092 let inv = CommandInvocation::of(b.delete.0).with_target(Target::TextObject(
7094 b.inner_sentence,
7095 crate::args::Args::None,
7096 ));
7097 execute(
7098 ®istry,
7099 &mut doc,
7100 lattice_core::BufferId(0),
7101 Position::new(0, 5),
7102 inv,
7103 &CancellationToken::never(),
7104 )
7105 .unwrap();
7106 assert!(doc.text().starts_with('.'));
7108 }
7109
7110 #[test]
7113 fn paragraph_forward_lands_on_next_blank_line() {
7114 let (registry, b, mut doc) = fixture("foo\nbar\n\nbaz");
7115 let inv = CommandInvocation::of(b.paragraph_forward.0);
7116 let effect = execute(
7117 ®istry,
7118 &mut doc,
7119 lattice_core::BufferId(0),
7120 Position::ZERO,
7121 inv,
7122 &CancellationToken::never(),
7123 )
7124 .unwrap();
7125 match effect {
7126 Effect::CursorMove(pos) => {
7127 assert_eq!(pos, Position::new(2, 0));
7129 }
7130 other => panic!("expected CursorMove, got {other:?}"),
7131 }
7132 }
7133
7134 #[test]
7135 fn paragraph_backward_lands_on_previous_blank_line() {
7136 let (registry, b, mut doc) = fixture("foo\n\nbar\nbaz");
7137 let inv = CommandInvocation::of(b.paragraph_backward.0);
7138 let effect = execute(
7139 ®istry,
7140 &mut doc,
7141 lattice_core::BufferId(0),
7142 Position::new(3, 0),
7143 inv,
7144 &CancellationToken::never(),
7145 )
7146 .unwrap();
7147 match effect {
7148 Effect::CursorMove(pos) => assert_eq!(pos, Position::new(1, 0)),
7149 other => panic!("expected CursorMove, got {other:?}"),
7150 }
7151 }
7152
7153 #[test]
7154 fn paragraph_forward_at_end_of_buffer_clamps() {
7155 let (registry, b, mut doc) = fixture("foo\nbar");
7156 let inv = CommandInvocation::of(b.paragraph_forward.0);
7157 let effect = execute(
7158 ®istry,
7159 &mut doc,
7160 lattice_core::BufferId(0),
7161 Position::ZERO,
7162 inv,
7163 &CancellationToken::never(),
7164 )
7165 .unwrap();
7166 match effect {
7167 Effect::CursorMove(pos) => {
7168 assert_eq!(pos.line, 1);
7170 }
7171 other => panic!("expected CursorMove, got {other:?}"),
7172 }
7173 }
7174
7175 #[test]
7176 fn dap_deletes_paragraph_with_blank_lines() {
7177 let (registry, b, mut doc) = fixture("foo\nbar\n\nbaz");
7178 let inv = CommandInvocation::of(b.delete.0).with_target(Target::TextObject(
7179 b.around_paragraph,
7180 crate::args::Args::None,
7181 ));
7182 execute(
7183 ®istry,
7184 &mut doc,
7185 lattice_core::BufferId(0),
7186 Position::ZERO,
7187 inv,
7188 &CancellationToken::never(),
7189 )
7190 .unwrap();
7191 assert!(doc.text().contains("baz"));
7193 }
7194
7195 #[test]
7196 fn dip_deletes_paragraph_only() {
7197 let (registry, b, mut doc) = fixture("foo\nbar\n\nbaz");
7198 let inv = CommandInvocation::of(b.delete.0).with_target(Target::TextObject(
7199 b.inner_paragraph,
7200 crate::args::Args::None,
7201 ));
7202 execute(
7203 ®istry,
7204 &mut doc,
7205 lattice_core::BufferId(0),
7206 Position::ZERO,
7207 inv,
7208 &CancellationToken::never(),
7209 )
7210 .unwrap();
7211 assert!(doc.text().starts_with('\n') || doc.text().starts_with("\nbaz"));
7213 }
7214
7215 #[test]
7218 fn big_word_forward_treats_punctuation_as_part_of_word() {
7219 let (registry, b, mut doc) = fixture("foo,bar baz");
7221 let inv = CommandInvocation::of(b.big_word_forward.0);
7222 let effect = execute(
7223 ®istry,
7224 &mut doc,
7225 lattice_core::BufferId(0),
7226 Position::ZERO,
7227 inv,
7228 &CancellationToken::never(),
7229 )
7230 .unwrap();
7231 match effect {
7232 Effect::CursorMove(pos) => {
7233 assert_eq!(pos, Position::new(0, 8));
7235 }
7236 other => panic!("expected CursorMove, got {other:?}"),
7237 }
7238 }
7239
7240 #[test]
7241 fn big_word_backward_skips_to_word_start() {
7242 let (registry, b, mut doc) = fixture("foo,bar baz");
7243 let inv = CommandInvocation::of(b.big_word_backward.0);
7245 let effect = execute(
7246 ®istry,
7247 &mut doc,
7248 lattice_core::BufferId(0),
7249 Position::new(0, 8),
7250 inv,
7251 &CancellationToken::never(),
7252 )
7253 .unwrap();
7254 match effect {
7255 Effect::CursorMove(pos) => assert_eq!(pos, Position::ZERO),
7256 other => panic!("expected CursorMove, got {other:?}"),
7257 }
7258 }
7259
7260 #[test]
7261 fn big_word_end_lands_at_last_byte_of_big_word() {
7262 let (registry, b, mut doc) = fixture("foo,bar baz");
7263 let inv = CommandInvocation::of(b.big_word_end.0);
7264 let effect = execute(
7265 ®istry,
7266 &mut doc,
7267 lattice_core::BufferId(0),
7268 Position::ZERO,
7269 inv,
7270 &CancellationToken::never(),
7271 )
7272 .unwrap();
7273 match effect {
7274 Effect::CursorMove(pos) => {
7275 assert_eq!(pos, Position::new(0, 6));
7277 }
7278 other => panic!("expected CursorMove, got {other:?}"),
7279 }
7280 }
7281
7282 fn invoke_with_char(id: crate::registry::MotionId, c: char) -> CommandInvocation {
7285 CommandInvocation::of(id.0).with_args(crate::args::Args::Char(c))
7286 }
7287
7288 #[test]
7289 fn find_char_forward_lands_on_next_occurrence_on_line() {
7290 let (registry, b, mut doc) = fixture("hello world");
7291 let inv = invoke_with_char(b.find_char_forward, 'o');
7293 let effect = execute(
7294 ®istry,
7295 &mut doc,
7296 lattice_core::BufferId(0),
7297 Position::ZERO,
7298 inv,
7299 &CancellationToken::never(),
7300 )
7301 .unwrap();
7302 match effect {
7303 Effect::CursorMove(pos) => assert_eq!(pos, Position::new(0, 4)),
7304 other => panic!("expected CursorMove, got {other:?}"),
7305 }
7306 }
7307
7308 #[test]
7309 fn find_char_forward_skips_current_byte_so_it_advances() {
7310 let (registry, b, mut doc) = fixture("oxox");
7311 let inv = invoke_with_char(b.find_char_forward, 'o');
7313 let effect = execute(
7314 ®istry,
7315 &mut doc,
7316 lattice_core::BufferId(0),
7317 Position::ZERO,
7318 inv,
7319 &CancellationToken::never(),
7320 )
7321 .unwrap();
7322 match effect {
7323 Effect::CursorMove(pos) => assert_eq!(pos, Position::new(0, 2)),
7324 other => panic!("expected CursorMove, got {other:?}"),
7325 }
7326 }
7327
7328 #[test]
7329 fn find_char_forward_no_match_is_no_op() {
7330 let (registry, b, mut doc) = fixture("hello");
7331 let inv = invoke_with_char(b.find_char_forward, 'z');
7332 let effect = execute(
7333 ®istry,
7334 &mut doc,
7335 lattice_core::BufferId(0),
7336 Position::ZERO,
7337 inv,
7338 &CancellationToken::never(),
7339 )
7340 .unwrap();
7341 match effect {
7342 Effect::CursorMove(pos) => assert_eq!(pos, Position::ZERO),
7343 other => panic!("expected CursorMove, got {other:?}"),
7344 }
7345 }
7346
7347 #[test]
7348 fn find_char_forward_does_not_cross_newlines() {
7349 let (registry, b, mut doc) = fixture("hello\nworld");
7350 let inv = invoke_with_char(b.find_char_forward, 'w');
7352 let effect = execute(
7353 ®istry,
7354 &mut doc,
7355 lattice_core::BufferId(0),
7356 Position::ZERO,
7357 inv,
7358 &CancellationToken::never(),
7359 )
7360 .unwrap();
7361 match effect {
7362 Effect::CursorMove(pos) => assert_eq!(pos, Position::ZERO),
7363 other => panic!("expected CursorMove, got {other:?}"),
7364 }
7365 }
7366
7367 #[test]
7368 fn find_char_backward_lands_on_previous_occurrence() {
7369 let (registry, b, mut doc) = fixture("hello world");
7370 let inv = invoke_with_char(b.find_char_backward, 'o');
7372 let effect = execute(
7373 ®istry,
7374 &mut doc,
7375 lattice_core::BufferId(0),
7376 Position::new(0, 8),
7377 inv,
7378 &CancellationToken::never(),
7379 )
7380 .unwrap();
7381 match effect {
7382 Effect::CursorMove(pos) => assert_eq!(pos, Position::new(0, 7)),
7383 other => panic!("expected CursorMove, got {other:?}"),
7384 }
7385 }
7386
7387 #[test]
7388 fn find_char_backward_no_match_is_no_op() {
7389 let (registry, b, mut doc) = fixture("hello");
7390 let inv = invoke_with_char(b.find_char_backward, 'z');
7391 let effect = execute(
7392 ®istry,
7393 &mut doc,
7394 lattice_core::BufferId(0),
7395 Position::new(0, 4),
7396 inv,
7397 &CancellationToken::never(),
7398 )
7399 .unwrap();
7400 match effect {
7401 Effect::CursorMove(pos) => assert_eq!(pos, Position::new(0, 4)),
7402 other => panic!("expected CursorMove, got {other:?}"),
7403 }
7404 }
7405
7406 #[test]
7407 fn till_char_forward_lands_one_byte_before_match() {
7408 let (registry, b, mut doc) = fixture("hello world");
7409 let inv = invoke_with_char(b.till_char_forward, 'w');
7411 let effect = execute(
7412 ®istry,
7413 &mut doc,
7414 lattice_core::BufferId(0),
7415 Position::ZERO,
7416 inv,
7417 &CancellationToken::never(),
7418 )
7419 .unwrap();
7420 match effect {
7421 Effect::CursorMove(pos) => assert_eq!(pos, Position::new(0, 5)),
7422 other => panic!("expected CursorMove, got {other:?}"),
7423 }
7424 }
7425
7426 #[test]
7427 fn till_char_backward_lands_one_byte_after_match() {
7428 let (registry, b, mut doc) = fixture("hello world");
7429 let inv = invoke_with_char(b.till_char_backward, 'h');
7431 let effect = execute(
7432 ®istry,
7433 &mut doc,
7434 lattice_core::BufferId(0),
7435 Position::new(0, 8),
7436 inv,
7437 &CancellationToken::never(),
7438 )
7439 .unwrap();
7440 match effect {
7441 Effect::CursorMove(pos) => assert_eq!(pos, Position::new(0, 1)),
7442 other => panic!("expected CursorMove, got {other:?}"),
7443 }
7444 }
7445
7446 #[test]
7447 fn find_char_with_no_args_errors() {
7448 let (registry, b, mut doc) = fixture("hello");
7449 let inv = CommandInvocation::of(b.find_char_forward.0);
7451 let err = execute(
7452 ®istry,
7453 &mut doc,
7454 lattice_core::BufferId(0),
7455 Position::ZERO,
7456 inv,
7457 &CancellationToken::never(),
7458 )
7459 .unwrap_err();
7460 assert!(matches!(err, CommandError::InvalidArgs(_)));
7461 }
7462
7463 #[test]
7464 fn find_char_forward_handles_unicode_target() {
7465 let (registry, b, mut doc) = fixture("café au lait");
7466 let inv = invoke_with_char(b.find_char_forward, 'é');
7468 let effect = execute(
7469 ®istry,
7470 &mut doc,
7471 lattice_core::BufferId(0),
7472 Position::ZERO,
7473 inv,
7474 &CancellationToken::never(),
7475 )
7476 .unwrap();
7477 match effect {
7478 Effect::CursorMove(pos) => assert_eq!(pos, Position::new(0, 3)),
7479 other => panic!("expected CursorMove, got {other:?}"),
7480 }
7481 }
7482
7483 #[test]
7486 fn change_with_word_forward_emits_edits_yank_and_enter_insert() {
7487 let (registry, b, mut doc) = fixture("hello world");
7488 let inv = CommandInvocation::of(b.change.0)
7489 .with_target(Target::Motion(b.word_forward, crate::args::Args::None));
7490 let effect = execute(
7491 ®istry,
7492 &mut doc,
7493 lattice_core::BufferId(0),
7494 Position::ZERO,
7495 inv,
7496 &CancellationToken::never(),
7497 )
7498 .unwrap();
7499 match effect {
7501 Effect::Many(parts) => {
7502 assert_eq!(parts.len(), 3);
7503 assert!(matches!(parts[0], Effect::Edits(_)));
7504 assert!(matches!(parts[1], Effect::Yank { .. }));
7505 assert!(matches!(
7506 parts[2],
7507 Effect::EnterMode(crate::ModalState::Insert)
7508 ));
7509 }
7510 other => panic!("expected Effect::Many, got {other:?}"),
7511 }
7512 assert_eq!(doc.text(), "world");
7513 }
7514
7515 #[test]
7516 fn change_current_line_clears_line_and_enters_insert() {
7517 let (registry, b, mut doc) = fixture("aaa\nBBB\nccc");
7518 let cursor = Position::new(1, 0);
7519 let inv = CommandInvocation::of(b.change.0).with_range(crate::range::Range::CurrentLine);
7520 let effect = execute(
7521 ®istry,
7522 &mut doc,
7523 lattice_core::BufferId(0),
7524 cursor,
7525 inv,
7526 &CancellationToken::never(),
7527 )
7528 .unwrap();
7529 match effect {
7530 Effect::Many(parts) => {
7531 assert_eq!(parts.len(), 3);
7532 match &parts[1] {
7534 Effect::Yank { kind, .. } => assert_eq!(*kind, YankKind::Linewise),
7535 other => panic!("expected Yank at [1], got {other:?}"),
7536 }
7537 assert!(matches!(
7538 parts[2],
7539 Effect::EnterMode(crate::ModalState::Insert)
7540 ));
7541 }
7542 other => panic!("expected Effect::Many, got {other:?}"),
7543 }
7544 assert_eq!(doc.text(), "aaa\n\nccc");
7545 }
7546
7547 #[test]
7548 fn change_with_line_end_target_truncates_line_and_enters_insert() {
7549 let (registry, b, mut doc) = fixture("hello world");
7551 let inv = CommandInvocation::of(b.change.0)
7552 .with_target(Target::Motion(b.line_end, crate::args::Args::None));
7553 let effect = execute(
7554 ®istry,
7555 &mut doc,
7556 lattice_core::BufferId(0),
7557 Position::new(0, 5),
7558 inv,
7559 &CancellationToken::never(),
7560 )
7561 .unwrap();
7562 match effect {
7563 Effect::Many(parts) => {
7564 assert_eq!(parts.len(), 3);
7565 assert!(matches!(
7566 parts[2],
7567 Effect::EnterMode(crate::ModalState::Insert)
7568 ));
7569 }
7570 other => panic!("expected Effect::Many, got {other:?}"),
7571 }
7572 assert_eq!(doc.text(), "hello");
7573 }
7574
7575 fn set_visual(
7579 doc: &mut Document,
7580 anchor: Position,
7581 head: Position,
7582 visual: lattice_protocol::selection::VisualMode,
7583 ) {
7584 use lattice_protocol::selection::{Selection, SelectionSet};
7585 doc.set_selections(SelectionSet::single(Selection {
7586 anchor,
7587 head,
7588 visual: Some(visual),
7589 }));
7590 }
7591
7592 #[test]
7593 fn replace_char_overwrites_single_char_under_cursor() {
7594 let (registry, b, mut doc) = fixture("hello");
7595 let inv = CommandInvocation::of(b.replace_char.0)
7596 .with_target(Target::Motion(b.char_right, crate::args::Args::None))
7597 .with_args(crate::args::Args::Char('a'));
7598 let eff = execute(
7599 ®istry,
7600 &mut doc,
7601 lattice_core::BufferId(0),
7602 Position::ZERO,
7603 inv,
7604 &CancellationToken::never(),
7605 )
7606 .unwrap();
7607 assert!(matches!(eff, Effect::Edits(_)));
7608 assert_eq!(doc.text(), "aello");
7609 }
7610
7611 #[test]
7612 fn replace_char_with_count_overwrites_count_chars() {
7613 let (registry, b, mut doc) = fixture("hello");
7614 let inv = CommandInvocation::of(b.replace_char.0)
7615 .with_target(Target::Motion(b.char_right, crate::args::Args::None))
7616 .with_args(crate::args::Args::Char('a'))
7617 .with_count(Count(3));
7618 execute(
7619 ®istry,
7620 &mut doc,
7621 lattice_core::BufferId(0),
7622 Position::ZERO,
7623 inv,
7624 &CancellationToken::never(),
7625 )
7626 .unwrap();
7627 assert_eq!(doc.text(), "aaalo");
7628 }
7629
7630 #[test]
7631 fn replace_char_count_too_large_is_a_no_op() {
7632 let (registry, b, mut doc) = fixture("ab");
7634 let inv = CommandInvocation::of(b.replace_char.0)
7635 .with_target(Target::Motion(b.char_right, crate::args::Args::None))
7636 .with_args(crate::args::Args::Char('x'))
7637 .with_count(Count(3));
7638 let eff = execute(
7639 ®istry,
7640 &mut doc,
7641 lattice_core::BufferId(0),
7642 Position::ZERO,
7643 inv,
7644 &CancellationToken::never(),
7645 )
7646 .unwrap();
7647 assert!(matches!(eff, Effect::None));
7648 assert_eq!(doc.text(), "ab");
7649 }
7650
7651 #[test]
7652 fn replace_char_on_empty_line_is_a_no_op() {
7653 let (registry, b, mut doc) = fixture("");
7654 let inv = CommandInvocation::of(b.replace_char.0)
7655 .with_target(Target::Motion(b.char_right, crate::args::Args::None))
7656 .with_args(crate::args::Args::Char('x'));
7657 let eff = execute(
7658 ®istry,
7659 &mut doc,
7660 lattice_core::BufferId(0),
7661 Position::ZERO,
7662 inv,
7663 &CancellationToken::never(),
7664 )
7665 .unwrap();
7666 assert!(matches!(eff, Effect::None));
7667 assert_eq!(doc.text(), "");
7668 }
7669
7670 #[test]
7671 fn visual_replace_char_overwrites_selection() {
7672 use lattice_protocol::selection::VisualMode;
7673 let (registry, b, mut doc) = fixture("hello world");
7674 set_visual(
7677 &mut doc,
7678 Position::new(0, 0),
7679 Position::new(0, 4),
7680 VisualMode::Charwise,
7681 );
7682 let inv = CommandInvocation::of(b.replace_char.0)
7683 .with_range(crate::range::Range::Selection)
7684 .with_args(crate::args::Args::Char('x'));
7685 execute(
7686 ®istry,
7687 &mut doc,
7688 lattice_core::BufferId(0),
7689 Position::new(0, 0),
7690 inv,
7691 &CancellationToken::never(),
7692 )
7693 .unwrap();
7694 assert_eq!(doc.text(), "xxxxx world");
7695 }
7696
7697 #[test]
7698 fn visual_replace_char_preserves_newlines_across_lines() {
7699 use lattice_protocol::selection::VisualMode;
7700 let (registry, b, mut doc) = fixture("ab\ncd");
7701 set_visual(
7704 &mut doc,
7705 Position::new(0, 1),
7706 Position::new(1, 0),
7707 VisualMode::Charwise,
7708 );
7709 let inv = CommandInvocation::of(b.replace_char.0)
7710 .with_range(crate::range::Range::Selection)
7711 .with_args(crate::args::Args::Char('X'));
7712 execute(
7713 ®istry,
7714 &mut doc,
7715 lattice_core::BufferId(0),
7716 Position::new(0, 1),
7717 inv,
7718 &CancellationToken::never(),
7719 )
7720 .unwrap();
7721 assert_eq!(doc.text(), "aX\nXd");
7723 }
7724
7725 #[test]
7726 fn visual_linewise_replace_char_overwrites_whole_lines() {
7727 use lattice_protocol::selection::VisualMode;
7728 let (registry, b, mut doc) = fixture("aaa\nbbb\nccc");
7729 set_visual(
7730 &mut doc,
7731 Position::new(0, 0),
7732 Position::new(1, 0),
7733 VisualMode::Linewise,
7734 );
7735 let inv = CommandInvocation::of(b.replace_char.0)
7736 .with_range(crate::range::Range::Selection)
7737 .with_args(crate::args::Args::Char('z'));
7738 execute(
7739 ®istry,
7740 &mut doc,
7741 lattice_core::BufferId(0),
7742 Position::new(0, 0),
7743 inv,
7744 &CancellationToken::never(),
7745 )
7746 .unwrap();
7747 assert_eq!(doc.text(), "zzz\nzzz\nccc");
7748 }
7749
7750 #[test]
7751 fn visual_blockwise_replace_char_overwrites_each_row_slice() {
7752 use lattice_protocol::selection::VisualMode;
7753 let (registry, b, mut doc) = fixture("abcd\nefgh\nijkl");
7754 set_visual(
7756 &mut doc,
7757 Position::new(0, 1),
7758 Position::new(2, 2),
7759 VisualMode::Blockwise,
7760 );
7761 let inv = CommandInvocation::of(b.replace_char.0)
7762 .with_range(crate::range::Range::Selection)
7763 .with_args(crate::args::Args::Char('*'));
7764 execute(
7765 ®istry,
7766 &mut doc,
7767 lattice_core::BufferId(0),
7768 Position::new(0, 1),
7769 inv,
7770 &CancellationToken::never(),
7771 )
7772 .unwrap();
7773 assert_eq!(doc.text(), "a**d\ne**h\ni**l");
7774 }
7775
7776 #[test]
7777 fn replace_char_without_char_arg_errors() {
7778 let (registry, b, mut doc) = fixture("hello");
7779 let inv = CommandInvocation::of(b.replace_char.0)
7780 .with_target(Target::Motion(b.char_right, crate::args::Args::None));
7781 let err = execute(
7782 ®istry,
7783 &mut doc,
7784 lattice_core::BufferId(0),
7785 Position::ZERO,
7786 inv,
7787 &CancellationToken::never(),
7788 )
7789 .expect_err("replace-char requires Args::Char");
7790 assert!(matches!(err, CommandError::InvalidArgs(_)));
7791 }
7792
7793 #[test]
7796 fn yank_with_word_forward_emits_charwise_yank_and_zero_register() {
7797 let (registry, b, mut doc) = fixture("hello world");
7798 let original_text = doc.text();
7799 let inv = CommandInvocation::of(b.yank.0)
7800 .with_target(Target::Motion(b.word_forward, crate::args::Args::None));
7801 let effect = execute(
7802 ®istry,
7803 &mut doc,
7804 lattice_core::BufferId(0),
7805 Position::ZERO,
7806 inv,
7807 &CancellationToken::never(),
7808 )
7809 .unwrap();
7810 assert_eq!(doc.text(), original_text);
7812 match effect {
7814 Effect::Many(parts) => {
7815 assert_eq!(parts.len(), 3);
7816 match &parts[0] {
7817 Effect::Yank {
7818 content,
7819 kind,
7820 register,
7821 explicit_yank,
7822 } => {
7823 assert_eq!(content, "hello ");
7824 assert_eq!(*kind, YankKind::Charwise);
7825 assert_eq!(*register, crate::register::Register::Unnamed);
7826 assert!(
7827 *explicit_yank,
7828 "the primary yank write is clipboard-eligible"
7829 );
7830 }
7831 other => panic!("expected Yank at [0], got {other:?}"),
7832 }
7833 match &parts[1] {
7834 Effect::Yank { register, .. } => {
7835 assert_eq!(*register, crate::register::Register::Numbered(0));
7836 }
7837 other => panic!("expected Yank(\"0) at [1], got {other:?}"),
7838 }
7839 assert!(matches!(parts[2], Effect::CursorMove(p) if p == Position::ZERO));
7842 }
7843 other => panic!("expected Many, got {other:?}"),
7844 }
7845 }
7846
7847 #[test]
7848 fn yank_with_current_line_range_emits_linewise_yank() {
7849 let (registry, b, mut doc) = fixture("aaa\nBBB\nccc");
7850 let inv = CommandInvocation::of(b.yank.0).with_range(crate::range::Range::CurrentLine);
7851 let effect = execute(
7852 ®istry,
7853 &mut doc,
7854 lattice_core::BufferId(0),
7855 Position::new(1, 0),
7856 inv,
7857 &CancellationToken::never(),
7858 )
7859 .unwrap();
7860 assert_eq!(doc.text(), "aaa\nBBB\nccc");
7861 match effect {
7862 Effect::Many(parts) => match &parts[0] {
7863 Effect::Yank { content, kind, .. } => {
7864 assert_eq!(content, "BBB\n");
7867 assert_eq!(*kind, YankKind::Linewise);
7868 }
7869 other => panic!("expected Yank, got {other:?}"),
7870 },
7871 other => panic!("expected Many, got {other:?}"),
7872 }
7873 }
7874
7875 #[test]
7876 fn yank_with_whole_range_emits_linewise_full_buffer() {
7877 let (registry, b, mut doc) = fixture("hello\nworld");
7878 let inv = CommandInvocation::of(b.yank.0).with_range(crate::range::Range::Whole);
7879 let effect = execute(
7880 ®istry,
7881 &mut doc,
7882 lattice_core::BufferId(0),
7883 Position::ZERO,
7884 inv,
7885 &CancellationToken::never(),
7886 )
7887 .unwrap();
7888 match effect {
7889 Effect::Many(parts) => match &parts[0] {
7890 Effect::Yank { content, kind, .. } => {
7891 assert_eq!(content, "hello\nworld\n");
7896 assert_eq!(*kind, YankKind::Linewise);
7897 }
7898 other => panic!("expected Yank, got {other:?}"),
7899 },
7900 other => panic!("expected Many, got {other:?}"),
7901 }
7902 }
7903
7904 #[test]
7905 fn yank_does_not_modify_buffer() {
7906 let (registry, b, mut doc) = fixture("immutable text");
7907 let original = doc.text();
7908 let inv = CommandInvocation::of(b.yank.0).with_range(crate::range::Range::Whole);
7909 execute(
7910 ®istry,
7911 &mut doc,
7912 lattice_core::BufferId(0),
7913 Position::ZERO,
7914 inv,
7915 &CancellationToken::never(),
7916 )
7917 .unwrap();
7918 assert_eq!(doc.text(), original);
7919 }
7920
7921 #[test]
7922 fn yank_empty_range_returns_none() {
7923 let (registry, b, mut doc) = fixture("");
7924 let inv = CommandInvocation::of(b.yank.0).with_range(crate::range::Range::Whole);
7925 let effect = execute(
7926 ®istry,
7927 &mut doc,
7928 lattice_core::BufferId(0),
7929 Position::ZERO,
7930 inv,
7931 &CancellationToken::never(),
7932 )
7933 .unwrap();
7934 assert!(matches!(effect, Effect::None));
7936 }
7937
7938 #[test]
7941 fn delete_charwise_yanks_charwise() {
7942 let (registry, b, mut doc) = fixture("hello world");
7943 let inv = CommandInvocation::of(b.delete.0)
7944 .with_target(Target::Motion(b.word_forward, crate::args::Args::None));
7945 let effect = execute(
7946 ®istry,
7947 &mut doc,
7948 lattice_core::BufferId(0),
7949 Position::ZERO,
7950 inv,
7951 &CancellationToken::never(),
7952 )
7953 .unwrap();
7954 match effect {
7955 Effect::Many(parts) => match &parts[1] {
7956 Effect::Yank { kind, content, .. } => {
7957 assert_eq!(*kind, YankKind::Charwise);
7958 assert_eq!(content, "hello ");
7959 }
7960 other => panic!("expected Yank, got {other:?}"),
7961 },
7962 other => panic!("expected Many, got {other:?}"),
7963 }
7964 }
7965
7966 #[test]
7967 fn delete_linewise_yanks_linewise() {
7968 let (registry, b, mut doc) = fixture("aaa\nBBB\nccc");
7969 let inv = CommandInvocation::of(b.delete.0).with_range(crate::range::Range::CurrentLine);
7970 let effect = execute(
7971 ®istry,
7972 &mut doc,
7973 lattice_core::BufferId(0),
7974 Position::new(1, 0),
7975 inv,
7976 &CancellationToken::never(),
7977 )
7978 .unwrap();
7979 match effect {
7980 Effect::Many(parts) => match &parts[1] {
7981 Effect::Yank { kind, .. } => assert_eq!(*kind, YankKind::Linewise),
7982 other => panic!("expected Yank, got {other:?}"),
7983 },
7984 other => panic!("expected Many, got {other:?}"),
7985 }
7986 }
7987
7988 #[test]
7989 fn delete_emits_many_with_edits_and_yank_only() {
7990 let (registry, b, mut doc) = fixture("hello world");
7992 let inv = CommandInvocation::of(b.delete.0)
7993 .with_target(Target::Motion(b.word_forward, crate::args::Args::None));
7994 let effect = execute(
7995 ®istry,
7996 &mut doc,
7997 lattice_core::BufferId(0),
7998 Position::ZERO,
7999 inv,
8000 &CancellationToken::never(),
8001 )
8002 .unwrap();
8003 match effect {
8004 Effect::Many(parts) => {
8005 assert_eq!(parts.len(), 2);
8006 assert!(!parts.iter().any(|e| matches!(e, Effect::EnterMode(_))));
8007 }
8008 other => panic!("expected Many, got {other:?}"),
8009 }
8010 }
8011
8012 #[test]
8013 fn delete_with_word_end_target_de_semantics() {
8014 let (registry, b, mut doc) = fixture("hello world");
8020 let inv = CommandInvocation::of(b.delete.0)
8021 .with_target(Target::Motion(b.word_end, crate::args::Args::None));
8022 execute(
8023 ®istry,
8024 &mut doc,
8025 lattice_core::BufferId(0),
8026 Position::ZERO,
8027 inv,
8028 &CancellationToken::never(),
8029 )
8030 .unwrap();
8031 assert_eq!(doc.text(), " world");
8032 }
8033
8034 #[test]
8035 fn change_with_word_end_target_ce_is_inclusive() {
8036 let (registry, b, mut doc) = fixture("hello world");
8040 let inv = CommandInvocation::of(b.change.0)
8041 .with_target(Target::Motion(b.word_end, crate::args::Args::None));
8042 execute(
8043 ®istry,
8044 &mut doc,
8045 lattice_core::BufferId(0),
8046 Position::ZERO,
8047 inv,
8048 &CancellationToken::never(),
8049 )
8050 .unwrap();
8051 assert_eq!(doc.text(), " world");
8052 }
8053
8054 #[test]
8055 fn delete_with_find_char_target_df_is_inclusive() {
8056 let (registry, b, mut doc) = fixture("hello");
8060 let inv = CommandInvocation::of(b.delete.0).with_target(Target::Motion(
8061 b.find_char_forward,
8062 crate::args::Args::Char('l'),
8063 ));
8064 execute(
8065 ®istry,
8066 &mut doc,
8067 lattice_core::BufferId(0),
8068 Position::ZERO,
8069 inv,
8070 &CancellationToken::never(),
8071 )
8072 .unwrap();
8073 assert_eq!(doc.text(), "lo");
8074 }
8075
8076 #[test]
8077 fn dw_on_last_word_of_line_stops_at_line_end() {
8078 let (registry, b, mut doc) = fixture("hello foo\nbar");
8083 let inv = CommandInvocation::of(b.delete.0)
8084 .with_target(Target::Motion(b.word_forward, crate::args::Args::None));
8085 execute(
8086 ®istry,
8087 &mut doc,
8088 lattice_core::BufferId(0),
8089 Position::new(0, 6),
8090 inv,
8091 &CancellationToken::never(),
8092 )
8093 .unwrap();
8094 assert_eq!(doc.text(), "hello \nbar", "dw must not cross the newline");
8095 }
8096
8097 #[test]
8098 fn delete_with_motion_target_for_motion_id_kind_mismatch_is_caught() {
8099 let (registry, b, mut doc) = fixture("abc");
8102 let bogus = MotionId(b.delete.0);
8104 let inv = CommandInvocation::of(b.delete.0)
8105 .with_target(Target::Motion(bogus, crate::args::Args::None));
8106 let err = execute(
8107 ®istry,
8108 &mut doc,
8109 lattice_core::BufferId(0),
8110 Position::ZERO,
8111 inv,
8112 &CancellationToken::never(),
8113 )
8114 .unwrap_err();
8115 assert!(matches!(err, CommandError::KindMismatch { .. }));
8116 }
8117}