1use editor::{
2 display_map::{DisplaySnapshot, FoldPoint, ToDisplayPoint},
3 movement::{
4 self, find_boundary, find_preceding_boundary_display_point, FindRange, TextLayoutDetails,
5 },
6 scroll::Autoscroll,
7 Anchor, Bias, DisplayPoint, ToOffset,
8};
9use gpui::{actions, impl_actions, px, ViewContext, WindowContext};
10use language::{char_kind, CharKind, Point, Selection, SelectionGoal};
11use serde::Deserialize;
12use std::ops::Range;
13use workspace::Workspace;
14
15use crate::{
16 normal::{mark, normal_motion},
17 state::{Mode, Operator},
18 surrounds::SurroundsType,
19 utils::coerce_punctuation,
20 visual::visual_motion,
21 Vim,
22};
23
24#[derive(Clone, Debug, PartialEq, Eq)]
25pub enum Motion {
26 Left,
27 Backspace,
28 Down {
29 display_lines: bool,
30 },
31 Up {
32 display_lines: bool,
33 },
34 Right,
35 Space,
36 NextWordStart {
37 ignore_punctuation: bool,
38 },
39 NextWordEnd {
40 ignore_punctuation: bool,
41 },
42 PreviousWordStart {
43 ignore_punctuation: bool,
44 },
45 PreviousWordEnd {
46 ignore_punctuation: bool,
47 },
48 NextSubwordStart {
49 ignore_punctuation: bool,
50 },
51 NextSubwordEnd {
52 ignore_punctuation: bool,
53 },
54 PreviousSubwordStart {
55 ignore_punctuation: bool,
56 },
57 PreviousSubwordEnd {
58 ignore_punctuation: bool,
59 },
60 FirstNonWhitespace {
61 display_lines: bool,
62 },
63 CurrentLine,
64 StartOfLine {
65 display_lines: bool,
66 },
67 EndOfLine {
68 display_lines: bool,
69 },
70 StartOfParagraph,
71 EndOfParagraph,
72 StartOfDocument,
73 EndOfDocument,
74 Matching,
75 FindForward {
76 before: bool,
77 char: char,
78 mode: FindRange,
79 smartcase: bool,
80 },
81 FindBackward {
82 after: bool,
83 char: char,
84 mode: FindRange,
85 smartcase: bool,
86 },
87 RepeatFind {
88 last_find: Box<Motion>,
89 },
90 RepeatFindReversed {
91 last_find: Box<Motion>,
92 },
93 NextLineStart,
94 StartOfLineDownward,
95 EndOfLineDownward,
96 GoToColumn,
97 WindowTop,
98 WindowMiddle,
99 WindowBottom,
100
101 // we don't have a good way to run a search syncronously, so
102 // we handle search motions by running the search async and then
103 // calling back into motion with this
104 ZedSearchResult {
105 prior_selections: Vec<Range<Anchor>>,
106 new_selections: Vec<Range<Anchor>>,
107 },
108 Jump {
109 anchor: Anchor,
110 line: bool,
111 },
112}
113
114#[derive(Clone, Deserialize, PartialEq)]
115#[serde(rename_all = "camelCase")]
116struct NextWordStart {
117 #[serde(default)]
118 ignore_punctuation: bool,
119}
120
121#[derive(Clone, Deserialize, PartialEq)]
122#[serde(rename_all = "camelCase")]
123struct NextWordEnd {
124 #[serde(default)]
125 ignore_punctuation: bool,
126}
127
128#[derive(Clone, Deserialize, PartialEq)]
129#[serde(rename_all = "camelCase")]
130struct PreviousWordStart {
131 #[serde(default)]
132 ignore_punctuation: bool,
133}
134
135#[derive(Clone, Deserialize, PartialEq)]
136#[serde(rename_all = "camelCase")]
137struct PreviousWordEnd {
138 #[serde(default)]
139 ignore_punctuation: bool,
140}
141
142#[derive(Clone, Deserialize, PartialEq)]
143#[serde(rename_all = "camelCase")]
144pub(crate) struct NextSubwordStart {
145 #[serde(default)]
146 pub(crate) ignore_punctuation: bool,
147}
148
149#[derive(Clone, Deserialize, PartialEq)]
150#[serde(rename_all = "camelCase")]
151pub(crate) struct NextSubwordEnd {
152 #[serde(default)]
153 pub(crate) ignore_punctuation: bool,
154}
155
156#[derive(Clone, Deserialize, PartialEq)]
157#[serde(rename_all = "camelCase")]
158pub(crate) struct PreviousSubwordStart {
159 #[serde(default)]
160 pub(crate) ignore_punctuation: bool,
161}
162
163#[derive(Clone, Deserialize, PartialEq)]
164#[serde(rename_all = "camelCase")]
165pub(crate) struct PreviousSubwordEnd {
166 #[serde(default)]
167 pub(crate) ignore_punctuation: bool,
168}
169
170#[derive(Clone, Deserialize, PartialEq)]
171#[serde(rename_all = "camelCase")]
172pub(crate) struct Up {
173 #[serde(default)]
174 pub(crate) display_lines: bool,
175}
176
177#[derive(Clone, Deserialize, PartialEq)]
178#[serde(rename_all = "camelCase")]
179pub(crate) struct Down {
180 #[serde(default)]
181 pub(crate) display_lines: bool,
182}
183
184#[derive(Clone, Deserialize, PartialEq)]
185#[serde(rename_all = "camelCase")]
186struct FirstNonWhitespace {
187 #[serde(default)]
188 display_lines: bool,
189}
190
191#[derive(Clone, Deserialize, PartialEq)]
192#[serde(rename_all = "camelCase")]
193struct EndOfLine {
194 #[serde(default)]
195 display_lines: bool,
196}
197
198#[derive(Clone, Deserialize, PartialEq)]
199#[serde(rename_all = "camelCase")]
200pub struct StartOfLine {
201 #[serde(default)]
202 pub(crate) display_lines: bool,
203}
204
205impl_actions!(
206 vim,
207 [
208 StartOfLine,
209 EndOfLine,
210 FirstNonWhitespace,
211 Down,
212 Up,
213 NextWordStart,
214 NextWordEnd,
215 PreviousWordStart,
216 PreviousWordEnd,
217 NextSubwordStart,
218 NextSubwordEnd,
219 PreviousSubwordStart,
220 PreviousSubwordEnd,
221 ]
222);
223
224actions!(
225 vim,
226 [
227 Left,
228 Backspace,
229 Right,
230 Space,
231 CurrentLine,
232 StartOfParagraph,
233 EndOfParagraph,
234 StartOfDocument,
235 EndOfDocument,
236 Matching,
237 NextLineStart,
238 StartOfLineDownward,
239 EndOfLineDownward,
240 GoToColumn,
241 RepeatFind,
242 RepeatFindReversed,
243 WindowTop,
244 WindowMiddle,
245 WindowBottom,
246 ]
247);
248
249pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
250 workspace.register_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx));
251 workspace
252 .register_action(|_: &mut Workspace, _: &Backspace, cx: _| motion(Motion::Backspace, cx));
253 workspace.register_action(|_: &mut Workspace, action: &Down, cx: _| {
254 motion(
255 Motion::Down {
256 display_lines: action.display_lines,
257 },
258 cx,
259 )
260 });
261 workspace.register_action(|_: &mut Workspace, action: &Up, cx: _| {
262 motion(
263 Motion::Up {
264 display_lines: action.display_lines,
265 },
266 cx,
267 )
268 });
269 workspace.register_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx));
270 workspace.register_action(|_: &mut Workspace, _: &Space, cx: _| motion(Motion::Space, cx));
271 workspace.register_action(|_: &mut Workspace, action: &FirstNonWhitespace, cx: _| {
272 motion(
273 Motion::FirstNonWhitespace {
274 display_lines: action.display_lines,
275 },
276 cx,
277 )
278 });
279 workspace.register_action(|_: &mut Workspace, action: &StartOfLine, cx: _| {
280 motion(
281 Motion::StartOfLine {
282 display_lines: action.display_lines,
283 },
284 cx,
285 )
286 });
287 workspace.register_action(|_: &mut Workspace, action: &EndOfLine, cx: _| {
288 motion(
289 Motion::EndOfLine {
290 display_lines: action.display_lines,
291 },
292 cx,
293 )
294 });
295 workspace.register_action(|_: &mut Workspace, _: &CurrentLine, cx: _| {
296 motion(Motion::CurrentLine, cx)
297 });
298 workspace.register_action(|_: &mut Workspace, _: &StartOfParagraph, cx: _| {
299 motion(Motion::StartOfParagraph, cx)
300 });
301 workspace.register_action(|_: &mut Workspace, _: &EndOfParagraph, cx: _| {
302 motion(Motion::EndOfParagraph, cx)
303 });
304 workspace.register_action(|_: &mut Workspace, _: &StartOfDocument, cx: _| {
305 motion(Motion::StartOfDocument, cx)
306 });
307 workspace.register_action(|_: &mut Workspace, _: &EndOfDocument, cx: _| {
308 motion(Motion::EndOfDocument, cx)
309 });
310 workspace
311 .register_action(|_: &mut Workspace, _: &Matching, cx: _| motion(Motion::Matching, cx));
312
313 workspace.register_action(
314 |_: &mut Workspace, &NextWordStart { ignore_punctuation }: &NextWordStart, cx: _| {
315 motion(Motion::NextWordStart { ignore_punctuation }, cx)
316 },
317 );
318 workspace.register_action(
319 |_: &mut Workspace, &NextWordEnd { ignore_punctuation }: &NextWordEnd, cx: _| {
320 motion(Motion::NextWordEnd { ignore_punctuation }, cx)
321 },
322 );
323 workspace.register_action(
324 |_: &mut Workspace,
325 &PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
326 cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
327 );
328 workspace.register_action(
329 |_: &mut Workspace, &PreviousWordEnd { ignore_punctuation }, cx: _| {
330 motion(Motion::PreviousWordEnd { ignore_punctuation }, cx)
331 },
332 );
333 workspace.register_action(
334 |_: &mut Workspace, &NextSubwordStart { ignore_punctuation }: &NextSubwordStart, cx: _| {
335 motion(Motion::NextSubwordStart { ignore_punctuation }, cx)
336 },
337 );
338 workspace.register_action(
339 |_: &mut Workspace, &NextSubwordEnd { ignore_punctuation }: &NextSubwordEnd, cx: _| {
340 motion(Motion::NextSubwordEnd { ignore_punctuation }, cx)
341 },
342 );
343 workspace.register_action(
344 |_: &mut Workspace,
345 &PreviousSubwordStart { ignore_punctuation }: &PreviousSubwordStart,
346 cx: _| { motion(Motion::PreviousSubwordStart { ignore_punctuation }, cx) },
347 );
348 workspace.register_action(
349 |_: &mut Workspace, &PreviousSubwordEnd { ignore_punctuation }, cx: _| {
350 motion(Motion::PreviousSubwordEnd { ignore_punctuation }, cx)
351 },
352 );
353 workspace.register_action(|_: &mut Workspace, &NextLineStart, cx: _| {
354 motion(Motion::NextLineStart, cx)
355 });
356 workspace.register_action(|_: &mut Workspace, &StartOfLineDownward, cx: _| {
357 motion(Motion::StartOfLineDownward, cx)
358 });
359 workspace.register_action(|_: &mut Workspace, &EndOfLineDownward, cx: _| {
360 motion(Motion::EndOfLineDownward, cx)
361 });
362 workspace
363 .register_action(|_: &mut Workspace, &GoToColumn, cx: _| motion(Motion::GoToColumn, cx));
364
365 workspace.register_action(|_: &mut Workspace, _: &RepeatFind, cx: _| {
366 if let Some(last_find) = Vim::read(cx)
367 .workspace_state
368 .last_find
369 .clone()
370 .map(Box::new)
371 {
372 motion(Motion::RepeatFind { last_find }, cx);
373 }
374 });
375
376 workspace.register_action(|_: &mut Workspace, _: &RepeatFindReversed, cx: _| {
377 if let Some(last_find) = Vim::read(cx)
378 .workspace_state
379 .last_find
380 .clone()
381 .map(Box::new)
382 {
383 motion(Motion::RepeatFindReversed { last_find }, cx);
384 }
385 });
386 workspace.register_action(|_: &mut Workspace, &WindowTop, cx: _| motion(Motion::WindowTop, cx));
387 workspace.register_action(|_: &mut Workspace, &WindowMiddle, cx: _| {
388 motion(Motion::WindowMiddle, cx)
389 });
390 workspace.register_action(|_: &mut Workspace, &WindowBottom, cx: _| {
391 motion(Motion::WindowBottom, cx)
392 });
393}
394
395pub(crate) fn search_motion(m: Motion, cx: &mut WindowContext) {
396 if let Motion::ZedSearchResult {
397 prior_selections, ..
398 } = &m
399 {
400 match Vim::read(cx).state().mode {
401 Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
402 if !prior_selections.is_empty() {
403 Vim::update(cx, |vim, cx| {
404 vim.update_active_editor(cx, |_, editor, cx| {
405 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
406 s.select_ranges(prior_selections.iter().cloned())
407 })
408 });
409 });
410 }
411 }
412 Mode::Normal | Mode::Replace | Mode::Insert => {
413 if Vim::read(cx).active_operator().is_none() {
414 return;
415 }
416 }
417 }
418 }
419
420 motion(m, cx)
421}
422
423pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
424 if let Some(Operator::FindForward { .. }) | Some(Operator::FindBackward { .. }) =
425 Vim::read(cx).active_operator()
426 {
427 Vim::update(cx, |vim, cx| vim.pop_operator(cx));
428 }
429
430 let count = Vim::update(cx, |vim, cx| vim.take_count(cx));
431 let active_operator = Vim::read(cx).active_operator();
432 let mut waiting_operator: Option<Operator> = None;
433 match Vim::read(cx).state().mode {
434 Mode::Normal | Mode::Replace => {
435 if active_operator == Some(Operator::AddSurrounds { target: None }) {
436 waiting_operator = Some(Operator::AddSurrounds {
437 target: Some(SurroundsType::Motion(motion)),
438 });
439 } else {
440 normal_motion(motion.clone(), active_operator.clone(), count, cx)
441 }
442 }
443 Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
444 visual_motion(motion.clone(), count, cx)
445 }
446 Mode::Insert => {
447 // Shouldn't execute a motion in insert mode. Ignoring
448 }
449 }
450 Vim::update(cx, |vim, cx| {
451 vim.clear_operator(cx);
452 if let Some(operator) = waiting_operator {
453 vim.push_operator(operator, cx);
454 vim.update_state(|state| state.pre_count = count)
455 }
456 });
457}
458
459// Motion handling is specified here:
460// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
461impl Motion {
462 pub fn linewise(&self) -> bool {
463 use Motion::*;
464 match self {
465 Down { .. }
466 | Up { .. }
467 | StartOfDocument
468 | EndOfDocument
469 | CurrentLine
470 | NextLineStart
471 | StartOfLineDownward
472 | StartOfParagraph
473 | WindowTop
474 | WindowMiddle
475 | WindowBottom
476 | Jump { line: true, .. }
477 | EndOfParagraph => true,
478 EndOfLine { .. }
479 | Matching
480 | FindForward { .. }
481 | Left
482 | Backspace
483 | Right
484 | Space
485 | StartOfLine { .. }
486 | EndOfLineDownward
487 | GoToColumn
488 | NextWordStart { .. }
489 | NextWordEnd { .. }
490 | PreviousWordStart { .. }
491 | PreviousWordEnd { .. }
492 | NextSubwordStart { .. }
493 | NextSubwordEnd { .. }
494 | PreviousSubwordStart { .. }
495 | PreviousSubwordEnd { .. }
496 | FirstNonWhitespace { .. }
497 | FindBackward { .. }
498 | RepeatFind { .. }
499 | RepeatFindReversed { .. }
500 | Jump { line: false, .. }
501 | ZedSearchResult { .. } => false,
502 }
503 }
504
505 pub fn infallible(&self) -> bool {
506 use Motion::*;
507 match self {
508 StartOfDocument | EndOfDocument | CurrentLine => true,
509 Down { .. }
510 | Up { .. }
511 | EndOfLine { .. }
512 | Matching
513 | FindForward { .. }
514 | RepeatFind { .. }
515 | Left
516 | Backspace
517 | Right
518 | Space
519 | StartOfLine { .. }
520 | StartOfParagraph
521 | EndOfParagraph
522 | StartOfLineDownward
523 | EndOfLineDownward
524 | GoToColumn
525 | NextWordStart { .. }
526 | NextWordEnd { .. }
527 | PreviousWordStart { .. }
528 | PreviousWordEnd { .. }
529 | NextSubwordStart { .. }
530 | NextSubwordEnd { .. }
531 | PreviousSubwordStart { .. }
532 | PreviousSubwordEnd { .. }
533 | FirstNonWhitespace { .. }
534 | FindBackward { .. }
535 | RepeatFindReversed { .. }
536 | WindowTop
537 | WindowMiddle
538 | WindowBottom
539 | NextLineStart
540 | ZedSearchResult { .. }
541 | Jump { .. } => false,
542 }
543 }
544
545 pub fn inclusive(&self) -> bool {
546 use Motion::*;
547 match self {
548 Down { .. }
549 | Up { .. }
550 | StartOfDocument
551 | EndOfDocument
552 | CurrentLine
553 | EndOfLine { .. }
554 | EndOfLineDownward
555 | Matching
556 | FindForward { .. }
557 | WindowTop
558 | WindowMiddle
559 | WindowBottom
560 | NextWordEnd { .. }
561 | PreviousWordEnd { .. }
562 | NextSubwordEnd { .. }
563 | PreviousSubwordEnd { .. }
564 | NextLineStart => true,
565 Left
566 | Backspace
567 | Right
568 | Space
569 | StartOfLine { .. }
570 | StartOfLineDownward
571 | StartOfParagraph
572 | EndOfParagraph
573 | GoToColumn
574 | NextWordStart { .. }
575 | PreviousWordStart { .. }
576 | NextSubwordStart { .. }
577 | PreviousSubwordStart { .. }
578 | FirstNonWhitespace { .. }
579 | FindBackward { .. }
580 | Jump { .. }
581 | ZedSearchResult { .. } => false,
582 RepeatFind { last_find: motion } | RepeatFindReversed { last_find: motion } => {
583 motion.inclusive()
584 }
585 }
586 }
587
588 pub fn move_point(
589 &self,
590 map: &DisplaySnapshot,
591 point: DisplayPoint,
592 goal: SelectionGoal,
593 maybe_times: Option<usize>,
594 text_layout_details: &TextLayoutDetails,
595 ) -> Option<(DisplayPoint, SelectionGoal)> {
596 let times = maybe_times.unwrap_or(1);
597 use Motion::*;
598 let infallible = self.infallible();
599 let (new_point, goal) = match self {
600 Left => (left(map, point, times), SelectionGoal::None),
601 Backspace => (backspace(map, point, times), SelectionGoal::None),
602 Down {
603 display_lines: false,
604 } => up_down_buffer_rows(map, point, goal, times as isize, &text_layout_details),
605 Down {
606 display_lines: true,
607 } => down_display(map, point, goal, times, &text_layout_details),
608 Up {
609 display_lines: false,
610 } => up_down_buffer_rows(map, point, goal, 0 - times as isize, &text_layout_details),
611 Up {
612 display_lines: true,
613 } => up_display(map, point, goal, times, &text_layout_details),
614 Right => (right(map, point, times), SelectionGoal::None),
615 Space => (space(map, point, times), SelectionGoal::None),
616 NextWordStart { ignore_punctuation } => (
617 next_word_start(map, point, *ignore_punctuation, times),
618 SelectionGoal::None,
619 ),
620 NextWordEnd { ignore_punctuation } => (
621 next_word_end(map, point, *ignore_punctuation, times, true),
622 SelectionGoal::None,
623 ),
624 PreviousWordStart { ignore_punctuation } => (
625 previous_word_start(map, point, *ignore_punctuation, times),
626 SelectionGoal::None,
627 ),
628 PreviousWordEnd { ignore_punctuation } => (
629 previous_word_end(map, point, *ignore_punctuation, times),
630 SelectionGoal::None,
631 ),
632 NextSubwordStart { ignore_punctuation } => (
633 next_subword_start(map, point, *ignore_punctuation, times),
634 SelectionGoal::None,
635 ),
636 NextSubwordEnd { ignore_punctuation } => (
637 next_subword_end(map, point, *ignore_punctuation, times, true),
638 SelectionGoal::None,
639 ),
640 PreviousSubwordStart { ignore_punctuation } => (
641 previous_subword_start(map, point, *ignore_punctuation, times),
642 SelectionGoal::None,
643 ),
644 PreviousSubwordEnd { ignore_punctuation } => (
645 previous_subword_end(map, point, *ignore_punctuation, times),
646 SelectionGoal::None,
647 ),
648 FirstNonWhitespace { display_lines } => (
649 first_non_whitespace(map, *display_lines, point),
650 SelectionGoal::None,
651 ),
652 StartOfLine { display_lines } => (
653 start_of_line(map, *display_lines, point),
654 SelectionGoal::None,
655 ),
656 EndOfLine { display_lines } => (
657 end_of_line(map, *display_lines, point, times),
658 SelectionGoal::None,
659 ),
660 StartOfParagraph => (
661 movement::start_of_paragraph(map, point, times),
662 SelectionGoal::None,
663 ),
664 EndOfParagraph => (
665 map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
666 SelectionGoal::None,
667 ),
668 CurrentLine => (next_line_end(map, point, times), SelectionGoal::None),
669 StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
670 EndOfDocument => (
671 end_of_document(map, point, maybe_times),
672 SelectionGoal::None,
673 ),
674 Matching => (matching(map, point), SelectionGoal::None),
675 // t f
676 FindForward {
677 before,
678 char,
679 mode,
680 smartcase,
681 } => {
682 return find_forward(map, point, *before, *char, times, *mode, *smartcase)
683 .map(|new_point| (new_point, SelectionGoal::None))
684 }
685 // T F
686 FindBackward {
687 after,
688 char,
689 mode,
690 smartcase,
691 } => (
692 find_backward(map, point, *after, *char, times, *mode, *smartcase),
693 SelectionGoal::None,
694 ),
695 // ; -- repeat the last find done with t, f, T, F
696 RepeatFind { last_find } => match **last_find {
697 Motion::FindForward {
698 before,
699 char,
700 mode,
701 smartcase,
702 } => {
703 let mut new_point =
704 find_forward(map, point, before, char, times, mode, smartcase);
705 if new_point == Some(point) {
706 new_point =
707 find_forward(map, point, before, char, times + 1, mode, smartcase);
708 }
709
710 return new_point.map(|new_point| (new_point, SelectionGoal::None));
711 }
712
713 Motion::FindBackward {
714 after,
715 char,
716 mode,
717 smartcase,
718 } => {
719 let mut new_point =
720 find_backward(map, point, after, char, times, mode, smartcase);
721 if new_point == point {
722 new_point =
723 find_backward(map, point, after, char, times + 1, mode, smartcase);
724 }
725
726 (new_point, SelectionGoal::None)
727 }
728 _ => return None,
729 },
730 // , -- repeat the last find done with t, f, T, F, in opposite direction
731 RepeatFindReversed { last_find } => match **last_find {
732 Motion::FindForward {
733 before,
734 char,
735 mode,
736 smartcase,
737 } => {
738 let mut new_point =
739 find_backward(map, point, before, char, times, mode, smartcase);
740 if new_point == point {
741 new_point =
742 find_backward(map, point, before, char, times + 1, mode, smartcase);
743 }
744
745 (new_point, SelectionGoal::None)
746 }
747
748 Motion::FindBackward {
749 after,
750 char,
751 mode,
752 smartcase,
753 } => {
754 let mut new_point =
755 find_forward(map, point, after, char, times, mode, smartcase);
756 if new_point == Some(point) {
757 new_point =
758 find_forward(map, point, after, char, times + 1, mode, smartcase);
759 }
760
761 return new_point.map(|new_point| (new_point, SelectionGoal::None));
762 }
763 _ => return None,
764 },
765 NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
766 StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None),
767 EndOfLineDownward => (last_non_whitespace(map, point, times), SelectionGoal::None),
768 GoToColumn => (go_to_column(map, point, times), SelectionGoal::None),
769 WindowTop => window_top(map, point, &text_layout_details, times - 1),
770 WindowMiddle => window_middle(map, point, &text_layout_details),
771 WindowBottom => window_bottom(map, point, &text_layout_details, times - 1),
772 Jump { line, anchor } => mark::jump_motion(map, *anchor, *line),
773 ZedSearchResult { new_selections, .. } => {
774 // There will be only one selection, as
775 // Search::SelectNextMatch selects a single match.
776 if let Some(new_selection) = new_selections.first() {
777 (
778 new_selection.start.to_display_point(map),
779 SelectionGoal::None,
780 )
781 } else {
782 return None;
783 }
784 }
785 };
786
787 (new_point != point || infallible).then_some((new_point, goal))
788 }
789
790 // Get the range value after self is applied to the specified selection.
791 pub fn range(
792 &self,
793 map: &DisplaySnapshot,
794 selection: Selection<DisplayPoint>,
795 times: Option<usize>,
796 expand_to_surrounding_newline: bool,
797 text_layout_details: &TextLayoutDetails,
798 ) -> Option<Range<DisplayPoint>> {
799 if let Motion::ZedSearchResult {
800 prior_selections,
801 new_selections,
802 } = self
803 {
804 if let Some((prior_selection, new_selection)) =
805 prior_selections.first().zip(new_selections.first())
806 {
807 let start = prior_selection
808 .start
809 .to_display_point(map)
810 .min(new_selection.start.to_display_point(map));
811 let end = new_selection
812 .end
813 .to_display_point(map)
814 .max(prior_selection.end.to_display_point(map));
815
816 if start < end {
817 return Some(start..end);
818 } else {
819 return Some(end..start);
820 }
821 } else {
822 return None;
823 }
824 }
825
826 if let Some((new_head, goal)) = self.move_point(
827 map,
828 selection.head(),
829 selection.goal,
830 times,
831 &text_layout_details,
832 ) {
833 let mut selection = selection.clone();
834 selection.set_head(new_head, goal);
835
836 if self.linewise() {
837 selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
838
839 if expand_to_surrounding_newline {
840 if selection.end.row() < map.max_point().row() {
841 *selection.end.row_mut() += 1;
842 *selection.end.column_mut() = 0;
843 selection.end = map.clip_point(selection.end, Bias::Right);
844 // Don't reset the end here
845 return Some(selection.start..selection.end);
846 } else if selection.start.row() > 0 {
847 *selection.start.row_mut() -= 1;
848 *selection.start.column_mut() = map.line_len(selection.start.row());
849 selection.start = map.clip_point(selection.start, Bias::Left);
850 }
851 }
852
853 selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
854 } else {
855 // Another special case: When using the "w" motion in combination with an
856 // operator and the last word moved over is at the end of a line, the end of
857 // that word becomes the end of the operated text, not the first word in the
858 // next line.
859 if let Motion::NextWordStart {
860 ignore_punctuation: _,
861 } = self
862 {
863 let start_row = selection.start.to_point(&map).row;
864 if selection.end.to_point(&map).row > start_row {
865 selection.end =
866 Point::new(start_row, map.buffer_snapshot.line_len(start_row))
867 .to_display_point(&map)
868 }
869 }
870
871 // If the motion is exclusive and the end of the motion is in column 1, the
872 // end of the motion is moved to the end of the previous line and the motion
873 // becomes inclusive. Example: "}" moves to the first line after a paragraph,
874 // but "d}" will not include that line.
875 let mut inclusive = self.inclusive();
876 if !inclusive
877 && self != &Motion::Backspace
878 && selection.end.row() > selection.start.row()
879 && selection.end.column() == 0
880 {
881 inclusive = true;
882 *selection.end.row_mut() -= 1;
883 *selection.end.column_mut() = 0;
884 selection.end = map.clip_point(
885 map.next_line_boundary(selection.end.to_point(map)).1,
886 Bias::Left,
887 );
888 }
889
890 if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
891 *selection.end.column_mut() += 1;
892 }
893 }
894 Some(selection.start..selection.end)
895 } else {
896 None
897 }
898 }
899
900 // Expands a selection using self for an operator
901 pub fn expand_selection(
902 &self,
903 map: &DisplaySnapshot,
904 selection: &mut Selection<DisplayPoint>,
905 times: Option<usize>,
906 expand_to_surrounding_newline: bool,
907 text_layout_details: &TextLayoutDetails,
908 ) -> bool {
909 if let Some(range) = self.range(
910 map,
911 selection.clone(),
912 times,
913 expand_to_surrounding_newline,
914 text_layout_details,
915 ) {
916 selection.start = range.start;
917 selection.end = range.end;
918 true
919 } else {
920 false
921 }
922 }
923}
924
925fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
926 for _ in 0..times {
927 point = movement::saturating_left(map, point);
928 if point.column() == 0 {
929 break;
930 }
931 }
932 point
933}
934
935pub(crate) fn backspace(
936 map: &DisplaySnapshot,
937 mut point: DisplayPoint,
938 times: usize,
939) -> DisplayPoint {
940 for _ in 0..times {
941 point = movement::left(map, point);
942 if point.is_zero() {
943 break;
944 }
945 }
946 point
947}
948
949fn space(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
950 for _ in 0..times {
951 point = wrapping_right(map, point);
952 if point == map.max_point() {
953 break;
954 }
955 }
956 point
957}
958
959fn wrapping_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
960 let max_column = map.line_len(point.row()).saturating_sub(1);
961 if point.column() < max_column {
962 *point.column_mut() += 1;
963 } else if point.row() < map.max_point().row() {
964 *point.row_mut() += 1;
965 *point.column_mut() = 0;
966 }
967 point
968}
969
970pub(crate) fn start_of_relative_buffer_row(
971 map: &DisplaySnapshot,
972 point: DisplayPoint,
973 times: isize,
974) -> DisplayPoint {
975 let start = map.display_point_to_fold_point(point, Bias::Left);
976 let target = start.row() as isize + times;
977 let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
978
979 map.clip_point(
980 map.fold_point_to_display_point(
981 map.fold_snapshot
982 .clip_point(FoldPoint::new(new_row, 0), Bias::Right),
983 ),
984 Bias::Right,
985 )
986}
987
988fn up_down_buffer_rows(
989 map: &DisplaySnapshot,
990 point: DisplayPoint,
991 mut goal: SelectionGoal,
992 times: isize,
993 text_layout_details: &TextLayoutDetails,
994) -> (DisplayPoint, SelectionGoal) {
995 let start = map.display_point_to_fold_point(point, Bias::Left);
996 let begin_folded_line = map.fold_point_to_display_point(
997 map.fold_snapshot
998 .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
999 );
1000 let select_nth_wrapped_row = point.row() - begin_folded_line.row();
1001
1002 let (goal_wrap, goal_x) = match goal {
1003 SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
1004 SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
1005 SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
1006 _ => {
1007 let x = map.x_for_display_point(point, text_layout_details);
1008 goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x.0));
1009 (select_nth_wrapped_row, x.0)
1010 }
1011 };
1012
1013 let target = start.row() as isize + times;
1014 let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
1015
1016 let mut begin_folded_line = map.fold_point_to_display_point(
1017 map.fold_snapshot
1018 .clip_point(FoldPoint::new(new_row, 0), Bias::Left),
1019 );
1020
1021 let mut i = 0;
1022 while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
1023 let next_folded_line = DisplayPoint::new(begin_folded_line.row() + 1, 0);
1024 if map
1025 .display_point_to_fold_point(next_folded_line, Bias::Right)
1026 .row()
1027 == new_row
1028 {
1029 i += 1;
1030 begin_folded_line = next_folded_line;
1031 } else {
1032 break;
1033 }
1034 }
1035
1036 let new_col = if i == goal_wrap {
1037 map.display_column_for_x(begin_folded_line.row(), px(goal_x), text_layout_details)
1038 } else {
1039 map.line_len(begin_folded_line.row())
1040 };
1041
1042 (
1043 map.clip_point(
1044 DisplayPoint::new(begin_folded_line.row(), new_col),
1045 Bias::Left,
1046 ),
1047 goal,
1048 )
1049}
1050
1051fn down_display(
1052 map: &DisplaySnapshot,
1053 mut point: DisplayPoint,
1054 mut goal: SelectionGoal,
1055 times: usize,
1056 text_layout_details: &TextLayoutDetails,
1057) -> (DisplayPoint, SelectionGoal) {
1058 for _ in 0..times {
1059 (point, goal) = movement::down(map, point, goal, true, text_layout_details);
1060 }
1061
1062 (point, goal)
1063}
1064
1065fn up_display(
1066 map: &DisplaySnapshot,
1067 mut point: DisplayPoint,
1068 mut goal: SelectionGoal,
1069 times: usize,
1070 text_layout_details: &TextLayoutDetails,
1071) -> (DisplayPoint, SelectionGoal) {
1072 for _ in 0..times {
1073 (point, goal) = movement::up(map, point, goal, true, &text_layout_details);
1074 }
1075
1076 (point, goal)
1077}
1078
1079pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
1080 for _ in 0..times {
1081 let new_point = movement::saturating_right(map, point);
1082 if point == new_point {
1083 break;
1084 }
1085 point = new_point;
1086 }
1087 point
1088}
1089
1090pub(crate) fn next_char(
1091 map: &DisplaySnapshot,
1092 point: DisplayPoint,
1093 allow_cross_newline: bool,
1094) -> DisplayPoint {
1095 let mut new_point = point;
1096 let mut max_column = map.line_len(new_point.row());
1097 if !allow_cross_newline {
1098 max_column -= 1;
1099 }
1100 if new_point.column() < max_column {
1101 *new_point.column_mut() += 1;
1102 } else if new_point < map.max_point() && allow_cross_newline {
1103 *new_point.row_mut() += 1;
1104 *new_point.column_mut() = 0;
1105 }
1106 map.clip_ignoring_line_ends(new_point, Bias::Right)
1107}
1108
1109pub(crate) fn next_word_start(
1110 map: &DisplaySnapshot,
1111 mut point: DisplayPoint,
1112 ignore_punctuation: bool,
1113 times: usize,
1114) -> DisplayPoint {
1115 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1116 for _ in 0..times {
1117 let mut crossed_newline = false;
1118 let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1119 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1120 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1121 let at_newline = right == '\n';
1122
1123 let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
1124 || at_newline && crossed_newline
1125 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1126
1127 crossed_newline |= at_newline;
1128 found
1129 });
1130 if point == new_point {
1131 break;
1132 }
1133 point = new_point;
1134 }
1135 point
1136}
1137
1138pub(crate) fn next_word_end(
1139 map: &DisplaySnapshot,
1140 mut point: DisplayPoint,
1141 ignore_punctuation: bool,
1142 times: usize,
1143 allow_cross_newline: bool,
1144) -> DisplayPoint {
1145 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1146 for _ in 0..times {
1147 let new_point = next_char(map, point, allow_cross_newline);
1148 let mut need_next_char = false;
1149 let new_point = movement::find_boundary_exclusive(
1150 map,
1151 new_point,
1152 FindRange::MultiLine,
1153 |left, right| {
1154 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1155 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1156 let at_newline = right == '\n';
1157
1158 if !allow_cross_newline && at_newline {
1159 need_next_char = true;
1160 return true;
1161 }
1162
1163 left_kind != right_kind && left_kind != CharKind::Whitespace
1164 },
1165 );
1166 let new_point = if need_next_char {
1167 next_char(map, new_point, true)
1168 } else {
1169 new_point
1170 };
1171 let new_point = map.clip_point(new_point, Bias::Left);
1172 if point == new_point {
1173 break;
1174 }
1175 point = new_point;
1176 }
1177 point
1178}
1179
1180fn previous_word_start(
1181 map: &DisplaySnapshot,
1182 mut point: DisplayPoint,
1183 ignore_punctuation: bool,
1184 times: usize,
1185) -> DisplayPoint {
1186 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1187 for _ in 0..times {
1188 // This works even though find_preceding_boundary is called for every character in the line containing
1189 // cursor because the newline is checked only once.
1190 let new_point = movement::find_preceding_boundary_display_point(
1191 map,
1192 point,
1193 FindRange::MultiLine,
1194 |left, right| {
1195 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1196 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1197
1198 (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
1199 },
1200 );
1201 if point == new_point {
1202 break;
1203 }
1204 point = new_point;
1205 }
1206 point
1207}
1208
1209fn previous_word_end(
1210 map: &DisplaySnapshot,
1211 point: DisplayPoint,
1212 ignore_punctuation: bool,
1213 times: usize,
1214) -> DisplayPoint {
1215 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1216 let mut point = point.to_point(map);
1217
1218 if point.column < map.buffer_snapshot.line_len(point.row) {
1219 point.column += 1;
1220 }
1221 for _ in 0..times {
1222 let new_point = movement::find_preceding_boundary_point(
1223 &map.buffer_snapshot,
1224 point,
1225 FindRange::MultiLine,
1226 |left, right| {
1227 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1228 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1229 match (left_kind, right_kind) {
1230 (CharKind::Punctuation, CharKind::Whitespace)
1231 | (CharKind::Punctuation, CharKind::Word)
1232 | (CharKind::Word, CharKind::Whitespace)
1233 | (CharKind::Word, CharKind::Punctuation) => true,
1234 (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1235 _ => false,
1236 }
1237 },
1238 );
1239 if new_point == point {
1240 break;
1241 }
1242 point = new_point;
1243 }
1244 movement::saturating_left(map, point.to_display_point(map))
1245}
1246
1247fn next_subword_start(
1248 map: &DisplaySnapshot,
1249 mut point: DisplayPoint,
1250 ignore_punctuation: bool,
1251 times: usize,
1252) -> DisplayPoint {
1253 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1254 for _ in 0..times {
1255 let mut crossed_newline = false;
1256 let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1257 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1258 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1259 let at_newline = right == '\n';
1260
1261 let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1262 let is_subword_start =
1263 left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1264
1265 let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1266 || at_newline && crossed_newline
1267 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1268
1269 crossed_newline |= at_newline;
1270 found
1271 });
1272 if point == new_point {
1273 break;
1274 }
1275 point = new_point;
1276 }
1277 point
1278}
1279
1280pub(crate) fn next_subword_end(
1281 map: &DisplaySnapshot,
1282 mut point: DisplayPoint,
1283 ignore_punctuation: bool,
1284 times: usize,
1285 allow_cross_newline: bool,
1286) -> DisplayPoint {
1287 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1288 for _ in 0..times {
1289 let new_point = next_char(map, point, allow_cross_newline);
1290
1291 let mut crossed_newline = false;
1292 let mut need_backtrack = false;
1293 let new_point =
1294 movement::find_boundary(map, new_point, FindRange::MultiLine, |left, right| {
1295 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1296 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1297 let at_newline = right == '\n';
1298
1299 if !allow_cross_newline && at_newline {
1300 return true;
1301 }
1302
1303 let is_word_end = (left_kind != right_kind) && !right.is_alphanumeric();
1304 let is_subword_end =
1305 left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1306
1307 let found = !left.is_whitespace() && !at_newline && (is_word_end || is_subword_end);
1308
1309 if found && (is_word_end || is_subword_end) {
1310 need_backtrack = true;
1311 }
1312
1313 crossed_newline |= at_newline;
1314 found
1315 });
1316 let mut new_point = map.clip_point(new_point, Bias::Left);
1317 if need_backtrack {
1318 *new_point.column_mut() -= 1;
1319 }
1320 if point == new_point {
1321 break;
1322 }
1323 point = new_point;
1324 }
1325 point
1326}
1327
1328fn previous_subword_start(
1329 map: &DisplaySnapshot,
1330 mut point: DisplayPoint,
1331 ignore_punctuation: bool,
1332 times: usize,
1333) -> DisplayPoint {
1334 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1335 for _ in 0..times {
1336 let mut crossed_newline = false;
1337 // This works even though find_preceding_boundary is called for every character in the line containing
1338 // cursor because the newline is checked only once.
1339 let new_point = movement::find_preceding_boundary_display_point(
1340 map,
1341 point,
1342 FindRange::MultiLine,
1343 |left, right| {
1344 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1345 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1346 let at_newline = right == '\n';
1347
1348 let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1349 let is_subword_start =
1350 left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1351
1352 let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1353 || at_newline && crossed_newline
1354 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1355
1356 crossed_newline |= at_newline;
1357
1358 found
1359 },
1360 );
1361 if point == new_point {
1362 break;
1363 }
1364 point = new_point;
1365 }
1366 point
1367}
1368
1369fn previous_subword_end(
1370 map: &DisplaySnapshot,
1371 point: DisplayPoint,
1372 ignore_punctuation: bool,
1373 times: usize,
1374) -> DisplayPoint {
1375 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1376 let mut point = point.to_point(map);
1377
1378 if point.column < map.buffer_snapshot.line_len(point.row) {
1379 point.column += 1;
1380 }
1381 for _ in 0..times {
1382 let new_point = movement::find_preceding_boundary_point(
1383 &map.buffer_snapshot,
1384 point,
1385 FindRange::MultiLine,
1386 |left, right| {
1387 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1388 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1389
1390 let is_subword_end =
1391 left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1392
1393 if is_subword_end {
1394 return true;
1395 }
1396
1397 match (left_kind, right_kind) {
1398 (CharKind::Word, CharKind::Whitespace)
1399 | (CharKind::Word, CharKind::Punctuation) => true,
1400 (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1401 _ => false,
1402 }
1403 },
1404 );
1405 if new_point == point {
1406 break;
1407 }
1408 point = new_point;
1409 }
1410 movement::saturating_left(map, point.to_display_point(map))
1411}
1412
1413pub(crate) fn first_non_whitespace(
1414 map: &DisplaySnapshot,
1415 display_lines: bool,
1416 from: DisplayPoint,
1417) -> DisplayPoint {
1418 let mut start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left);
1419 let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
1420 for (ch, offset) in map.buffer_chars_at(start_offset) {
1421 if ch == '\n' {
1422 return from;
1423 }
1424
1425 start_offset = offset;
1426
1427 if char_kind(&scope, ch) != CharKind::Whitespace {
1428 break;
1429 }
1430 }
1431
1432 start_offset.to_display_point(map)
1433}
1434
1435pub(crate) fn last_non_whitespace(
1436 map: &DisplaySnapshot,
1437 from: DisplayPoint,
1438 count: usize,
1439) -> DisplayPoint {
1440 let mut end_of_line = end_of_line(map, false, from, count).to_offset(map, Bias::Left);
1441 let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
1442 for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) {
1443 if ch == '\n' {
1444 break;
1445 }
1446 end_of_line = offset;
1447 if char_kind(&scope, ch) != CharKind::Whitespace || ch == '\n' {
1448 break;
1449 }
1450 }
1451
1452 end_of_line.to_display_point(map)
1453}
1454
1455pub(crate) fn start_of_line(
1456 map: &DisplaySnapshot,
1457 display_lines: bool,
1458 point: DisplayPoint,
1459) -> DisplayPoint {
1460 if display_lines {
1461 map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
1462 } else {
1463 map.prev_line_boundary(point.to_point(map)).1
1464 }
1465}
1466
1467pub(crate) fn end_of_line(
1468 map: &DisplaySnapshot,
1469 display_lines: bool,
1470 mut point: DisplayPoint,
1471 times: usize,
1472) -> DisplayPoint {
1473 if times > 1 {
1474 point = start_of_relative_buffer_row(map, point, times as isize - 1);
1475 }
1476 if display_lines {
1477 map.clip_point(
1478 DisplayPoint::new(point.row(), map.line_len(point.row())),
1479 Bias::Left,
1480 )
1481 } else {
1482 map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
1483 }
1484}
1485
1486fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
1487 let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
1488 *new_point.column_mut() = point.column();
1489 map.clip_point(new_point, Bias::Left)
1490}
1491
1492fn end_of_document(
1493 map: &DisplaySnapshot,
1494 point: DisplayPoint,
1495 line: Option<usize>,
1496) -> DisplayPoint {
1497 let new_row = if let Some(line) = line {
1498 (line - 1) as u32
1499 } else {
1500 map.max_buffer_row()
1501 };
1502
1503 let new_point = Point::new(new_row, point.column());
1504 map.clip_point(new_point.to_display_point(map), Bias::Left)
1505}
1506
1507fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
1508 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
1509 let display_point = map.clip_at_line_end(display_point);
1510 let point = display_point.to_point(map);
1511 let offset = point.to_offset(&map.buffer_snapshot);
1512
1513 // Ensure the range is contained by the current line.
1514 let mut line_end = map.next_line_boundary(point).0;
1515 if line_end == point {
1516 line_end = map.max_point().to_point(map);
1517 }
1518
1519 let line_range = map.prev_line_boundary(point).0..line_end;
1520 let visible_line_range =
1521 line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
1522 let ranges = map
1523 .buffer_snapshot
1524 .bracket_ranges(visible_line_range.clone());
1525 if let Some(ranges) = ranges {
1526 let line_range = line_range.start.to_offset(&map.buffer_snapshot)
1527 ..line_range.end.to_offset(&map.buffer_snapshot);
1528 let mut closest_pair_destination = None;
1529 let mut closest_distance = usize::MAX;
1530
1531 for (open_range, close_range) in ranges {
1532 if open_range.start >= offset && line_range.contains(&open_range.start) {
1533 let distance = open_range.start - offset;
1534 if distance < closest_distance {
1535 closest_pair_destination = Some(close_range.start);
1536 closest_distance = distance;
1537 continue;
1538 }
1539 }
1540
1541 if close_range.start >= offset && line_range.contains(&close_range.start) {
1542 let distance = close_range.start - offset;
1543 if distance < closest_distance {
1544 closest_pair_destination = Some(open_range.start);
1545 closest_distance = distance;
1546 continue;
1547 }
1548 }
1549
1550 continue;
1551 }
1552
1553 closest_pair_destination
1554 .map(|destination| destination.to_display_point(map))
1555 .unwrap_or(display_point)
1556 } else {
1557 display_point
1558 }
1559}
1560
1561fn find_forward(
1562 map: &DisplaySnapshot,
1563 from: DisplayPoint,
1564 before: bool,
1565 target: char,
1566 times: usize,
1567 mode: FindRange,
1568 smartcase: bool,
1569) -> Option<DisplayPoint> {
1570 let mut to = from;
1571 let mut found = false;
1572
1573 for _ in 0..times {
1574 found = false;
1575 let new_to = find_boundary(map, to, mode, |_, right| {
1576 found = is_character_match(target, right, smartcase);
1577 found
1578 });
1579 if to == new_to {
1580 break;
1581 }
1582 to = new_to;
1583 }
1584
1585 if found {
1586 if before && to.column() > 0 {
1587 *to.column_mut() -= 1;
1588 Some(map.clip_point(to, Bias::Left))
1589 } else {
1590 Some(to)
1591 }
1592 } else {
1593 None
1594 }
1595}
1596
1597fn find_backward(
1598 map: &DisplaySnapshot,
1599 from: DisplayPoint,
1600 after: bool,
1601 target: char,
1602 times: usize,
1603 mode: FindRange,
1604 smartcase: bool,
1605) -> DisplayPoint {
1606 let mut to = from;
1607
1608 for _ in 0..times {
1609 let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
1610 is_character_match(target, right, smartcase)
1611 });
1612 if to == new_to {
1613 break;
1614 }
1615 to = new_to;
1616 }
1617
1618 let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
1619 if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
1620 if after {
1621 *to.column_mut() += 1;
1622 map.clip_point(to, Bias::Right)
1623 } else {
1624 to
1625 }
1626 } else {
1627 from
1628 }
1629}
1630
1631fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
1632 if smartcase {
1633 if target.is_uppercase() {
1634 target == other
1635 } else {
1636 target == other.to_ascii_lowercase()
1637 }
1638 } else {
1639 target == other
1640 }
1641}
1642
1643fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1644 let correct_line = start_of_relative_buffer_row(map, point, times as isize);
1645 first_non_whitespace(map, false, correct_line)
1646}
1647
1648fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1649 let correct_line = start_of_relative_buffer_row(map, point, 0);
1650 right(map, correct_line, times.saturating_sub(1))
1651}
1652
1653pub(crate) fn next_line_end(
1654 map: &DisplaySnapshot,
1655 mut point: DisplayPoint,
1656 times: usize,
1657) -> DisplayPoint {
1658 if times > 1 {
1659 point = start_of_relative_buffer_row(map, point, times as isize - 1);
1660 }
1661 end_of_line(map, false, point, 1)
1662}
1663
1664fn window_top(
1665 map: &DisplaySnapshot,
1666 point: DisplayPoint,
1667 text_layout_details: &TextLayoutDetails,
1668 mut times: usize,
1669) -> (DisplayPoint, SelectionGoal) {
1670 let first_visible_line = text_layout_details
1671 .scroll_anchor
1672 .anchor
1673 .to_display_point(map);
1674
1675 if first_visible_line.row() != 0 && text_layout_details.vertical_scroll_margin as usize > times
1676 {
1677 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1678 }
1679
1680 if let Some(visible_rows) = text_layout_details.visible_rows {
1681 let bottom_row = first_visible_line.row() + visible_rows as u32;
1682 let new_row = (first_visible_line.row() + (times as u32))
1683 .min(bottom_row)
1684 .min(map.max_point().row());
1685 let new_col = point.column().min(map.line_len(first_visible_line.row()));
1686
1687 let new_point = DisplayPoint::new(new_row, new_col);
1688 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1689 } else {
1690 let new_row = (first_visible_line.row() + (times as u32)).min(map.max_point().row());
1691 let new_col = point.column().min(map.line_len(first_visible_line.row()));
1692
1693 let new_point = DisplayPoint::new(new_row, new_col);
1694 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1695 }
1696}
1697
1698fn window_middle(
1699 map: &DisplaySnapshot,
1700 point: DisplayPoint,
1701 text_layout_details: &TextLayoutDetails,
1702) -> (DisplayPoint, SelectionGoal) {
1703 if let Some(visible_rows) = text_layout_details.visible_rows {
1704 let first_visible_line = text_layout_details
1705 .scroll_anchor
1706 .anchor
1707 .to_display_point(map);
1708
1709 let max_visible_rows =
1710 (visible_rows as u32).min(map.max_point().row() - first_visible_line.row());
1711
1712 let new_row =
1713 (first_visible_line.row() + (max_visible_rows / 2)).min(map.max_point().row());
1714 let new_col = point.column().min(map.line_len(new_row));
1715 let new_point = DisplayPoint::new(new_row, new_col);
1716 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1717 } else {
1718 (point, SelectionGoal::None)
1719 }
1720}
1721
1722fn window_bottom(
1723 map: &DisplaySnapshot,
1724 point: DisplayPoint,
1725 text_layout_details: &TextLayoutDetails,
1726 mut times: usize,
1727) -> (DisplayPoint, SelectionGoal) {
1728 if let Some(visible_rows) = text_layout_details.visible_rows {
1729 let first_visible_line = text_layout_details
1730 .scroll_anchor
1731 .anchor
1732 .to_display_point(map);
1733 let bottom_row = first_visible_line.row()
1734 + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
1735 if bottom_row < map.max_point().row()
1736 && text_layout_details.vertical_scroll_margin as usize > times
1737 {
1738 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1739 }
1740 let bottom_row_capped = bottom_row.min(map.max_point().row());
1741 let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row() {
1742 first_visible_line.row()
1743 } else {
1744 bottom_row_capped.saturating_sub(times as u32)
1745 };
1746 let new_col = point.column().min(map.line_len(new_row));
1747 let new_point = DisplayPoint::new(new_row, new_col);
1748 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1749 } else {
1750 (point, SelectionGoal::None)
1751 }
1752}
1753
1754#[cfg(test)]
1755mod test {
1756
1757 use crate::test::NeovimBackedTestContext;
1758 use indoc::indoc;
1759
1760 #[gpui::test]
1761 async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
1762 let mut cx = NeovimBackedTestContext::new(cx).await;
1763
1764 let initial_state = indoc! {r"ˇabc
1765 def
1766
1767 paragraph
1768 the second
1769
1770
1771
1772 third and
1773 final"};
1774
1775 // goes down once
1776 cx.set_shared_state(initial_state).await;
1777 cx.simulate_shared_keystrokes(["}"]).await;
1778 cx.assert_shared_state(indoc! {r"abc
1779 def
1780 ˇ
1781 paragraph
1782 the second
1783
1784
1785
1786 third and
1787 final"})
1788 .await;
1789
1790 // goes up once
1791 cx.simulate_shared_keystrokes(["{"]).await;
1792 cx.assert_shared_state(initial_state).await;
1793
1794 // goes down twice
1795 cx.simulate_shared_keystrokes(["2", "}"]).await;
1796 cx.assert_shared_state(indoc! {r"abc
1797 def
1798
1799 paragraph
1800 the second
1801 ˇ
1802
1803
1804 third and
1805 final"})
1806 .await;
1807
1808 // goes down over multiple blanks
1809 cx.simulate_shared_keystrokes(["}"]).await;
1810 cx.assert_shared_state(indoc! {r"abc
1811 def
1812
1813 paragraph
1814 the second
1815
1816
1817
1818 third and
1819 finaˇl"})
1820 .await;
1821
1822 // goes up twice
1823 cx.simulate_shared_keystrokes(["2", "{"]).await;
1824 cx.assert_shared_state(indoc! {r"abc
1825 def
1826 ˇ
1827 paragraph
1828 the second
1829
1830
1831
1832 third and
1833 final"})
1834 .await
1835 }
1836
1837 #[gpui::test]
1838 async fn test_matching(cx: &mut gpui::TestAppContext) {
1839 let mut cx = NeovimBackedTestContext::new(cx).await;
1840
1841 cx.set_shared_state(indoc! {r"func ˇ(a string) {
1842 do(something(with<Types>.and_arrays[0, 2]))
1843 }"})
1844 .await;
1845 cx.simulate_shared_keystrokes(["%"]).await;
1846 cx.assert_shared_state(indoc! {r"func (a stringˇ) {
1847 do(something(with<Types>.and_arrays[0, 2]))
1848 }"})
1849 .await;
1850
1851 // test it works on the last character of the line
1852 cx.set_shared_state(indoc! {r"func (a string) ˇ{
1853 do(something(with<Types>.and_arrays[0, 2]))
1854 }"})
1855 .await;
1856 cx.simulate_shared_keystrokes(["%"]).await;
1857 cx.assert_shared_state(indoc! {r"func (a string) {
1858 do(something(with<Types>.and_arrays[0, 2]))
1859 ˇ}"})
1860 .await;
1861
1862 // test it works on immediate nesting
1863 cx.set_shared_state("ˇ{()}").await;
1864 cx.simulate_shared_keystrokes(["%"]).await;
1865 cx.assert_shared_state("{()ˇ}").await;
1866 cx.simulate_shared_keystrokes(["%"]).await;
1867 cx.assert_shared_state("ˇ{()}").await;
1868
1869 // test it works on immediate nesting inside braces
1870 cx.set_shared_state("{\n ˇ{()}\n}").await;
1871 cx.simulate_shared_keystrokes(["%"]).await;
1872 cx.assert_shared_state("{\n {()ˇ}\n}").await;
1873
1874 // test it jumps to the next paren on a line
1875 cx.set_shared_state("func ˇboop() {\n}").await;
1876 cx.simulate_shared_keystrokes(["%"]).await;
1877 cx.assert_shared_state("func boop(ˇ) {\n}").await;
1878 }
1879
1880 #[gpui::test]
1881 async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
1882 let mut cx = NeovimBackedTestContext::new(cx).await;
1883
1884 // f and F
1885 cx.set_shared_state("ˇone two three four").await;
1886 cx.simulate_shared_keystrokes(["f", "o"]).await;
1887 cx.assert_shared_state("one twˇo three four").await;
1888 cx.simulate_shared_keystrokes([","]).await;
1889 cx.assert_shared_state("ˇone two three four").await;
1890 cx.simulate_shared_keystrokes(["2", ";"]).await;
1891 cx.assert_shared_state("one two three fˇour").await;
1892 cx.simulate_shared_keystrokes(["shift-f", "e"]).await;
1893 cx.assert_shared_state("one two threˇe four").await;
1894 cx.simulate_shared_keystrokes(["2", ";"]).await;
1895 cx.assert_shared_state("onˇe two three four").await;
1896 cx.simulate_shared_keystrokes([","]).await;
1897 cx.assert_shared_state("one two thrˇee four").await;
1898
1899 // t and T
1900 cx.set_shared_state("ˇone two three four").await;
1901 cx.simulate_shared_keystrokes(["t", "o"]).await;
1902 cx.assert_shared_state("one tˇwo three four").await;
1903 cx.simulate_shared_keystrokes([","]).await;
1904 cx.assert_shared_state("oˇne two three four").await;
1905 cx.simulate_shared_keystrokes(["2", ";"]).await;
1906 cx.assert_shared_state("one two three ˇfour").await;
1907 cx.simulate_shared_keystrokes(["shift-t", "e"]).await;
1908 cx.assert_shared_state("one two threeˇ four").await;
1909 cx.simulate_shared_keystrokes(["3", ";"]).await;
1910 cx.assert_shared_state("oneˇ two three four").await;
1911 cx.simulate_shared_keystrokes([","]).await;
1912 cx.assert_shared_state("one two thˇree four").await;
1913 }
1914
1915 #[gpui::test]
1916 async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
1917 let mut cx = NeovimBackedTestContext::new(cx).await;
1918 let initial_state = indoc! {r"something(ˇfoo)"};
1919 cx.set_shared_state(initial_state).await;
1920 cx.simulate_shared_keystrokes(["}"]).await;
1921 cx.assert_shared_state(indoc! {r"something(fooˇ)"}).await;
1922 }
1923
1924 #[gpui::test]
1925 async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
1926 let mut cx = NeovimBackedTestContext::new(cx).await;
1927 cx.set_shared_state("ˇone\n two\nthree").await;
1928 cx.simulate_shared_keystrokes(["enter"]).await;
1929 cx.assert_shared_state("one\n ˇtwo\nthree").await;
1930 }
1931
1932 #[gpui::test]
1933 async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
1934 let mut cx = NeovimBackedTestContext::new(cx).await;
1935 cx.set_shared_state("ˇ one \n two \nthree").await;
1936 cx.simulate_shared_keystrokes(["g", "_"]).await;
1937 cx.assert_shared_state(" onˇe \n two \nthree").await;
1938 cx.simulate_shared_keystrokes(["2", "g", "_"]).await;
1939 cx.assert_shared_state(" one \n twˇo \nthree").await;
1940 }
1941
1942 #[gpui::test]
1943 async fn test_window_top(cx: &mut gpui::TestAppContext) {
1944 let mut cx = NeovimBackedTestContext::new(cx).await;
1945 let initial_state = indoc! {r"abc
1946 def
1947 paragraph
1948 the second
1949 third ˇand
1950 final"};
1951
1952 cx.set_shared_state(initial_state).await;
1953 cx.simulate_shared_keystrokes(["shift-h"]).await;
1954 cx.assert_shared_state(indoc! {r"abˇc
1955 def
1956 paragraph
1957 the second
1958 third and
1959 final"})
1960 .await;
1961
1962 // clip point
1963 cx.set_shared_state(indoc! {r"
1964 1 2 3
1965 4 5 6
1966 7 8 ˇ9
1967 "})
1968 .await;
1969 cx.simulate_shared_keystrokes(["shift-h"]).await;
1970 cx.assert_shared_state(indoc! {r"
1971 1 2 ˇ3
1972 4 5 6
1973 7 8 9
1974 "})
1975 .await;
1976
1977 cx.set_shared_state(indoc! {r"
1978 1 2 3
1979 4 5 6
1980 ˇ7 8 9
1981 "})
1982 .await;
1983 cx.simulate_shared_keystrokes(["shift-h"]).await;
1984 cx.assert_shared_state(indoc! {r"
1985 ˇ1 2 3
1986 4 5 6
1987 7 8 9
1988 "})
1989 .await;
1990
1991 cx.set_shared_state(indoc! {r"
1992 1 2 3
1993 4 5 ˇ6
1994 7 8 9"})
1995 .await;
1996 cx.simulate_shared_keystrokes(["9", "shift-h"]).await;
1997 cx.assert_shared_state(indoc! {r"
1998 1 2 3
1999 4 5 6
2000 7 8 ˇ9"})
2001 .await;
2002 }
2003
2004 #[gpui::test]
2005 async fn test_window_middle(cx: &mut gpui::TestAppContext) {
2006 let mut cx = NeovimBackedTestContext::new(cx).await;
2007 let initial_state = indoc! {r"abˇc
2008 def
2009 paragraph
2010 the second
2011 third and
2012 final"};
2013
2014 cx.set_shared_state(initial_state).await;
2015 cx.simulate_shared_keystrokes(["shift-m"]).await;
2016 cx.assert_shared_state(indoc! {r"abc
2017 def
2018 paˇragraph
2019 the second
2020 third and
2021 final"})
2022 .await;
2023
2024 cx.set_shared_state(indoc! {r"
2025 1 2 3
2026 4 5 6
2027 7 8 ˇ9
2028 "})
2029 .await;
2030 cx.simulate_shared_keystrokes(["shift-m"]).await;
2031 cx.assert_shared_state(indoc! {r"
2032 1 2 3
2033 4 5 ˇ6
2034 7 8 9
2035 "})
2036 .await;
2037 cx.set_shared_state(indoc! {r"
2038 1 2 3
2039 4 5 6
2040 ˇ7 8 9
2041 "})
2042 .await;
2043 cx.simulate_shared_keystrokes(["shift-m"]).await;
2044 cx.assert_shared_state(indoc! {r"
2045 1 2 3
2046 ˇ4 5 6
2047 7 8 9
2048 "})
2049 .await;
2050 cx.set_shared_state(indoc! {r"
2051 ˇ1 2 3
2052 4 5 6
2053 7 8 9
2054 "})
2055 .await;
2056 cx.simulate_shared_keystrokes(["shift-m"]).await;
2057 cx.assert_shared_state(indoc! {r"
2058 1 2 3
2059 ˇ4 5 6
2060 7 8 9
2061 "})
2062 .await;
2063 cx.set_shared_state(indoc! {r"
2064 1 2 3
2065 ˇ4 5 6
2066 7 8 9
2067 "})
2068 .await;
2069 cx.simulate_shared_keystrokes(["shift-m"]).await;
2070 cx.assert_shared_state(indoc! {r"
2071 1 2 3
2072 ˇ4 5 6
2073 7 8 9
2074 "})
2075 .await;
2076 cx.set_shared_state(indoc! {r"
2077 1 2 3
2078 4 5 ˇ6
2079 7 8 9
2080 "})
2081 .await;
2082 cx.simulate_shared_keystrokes(["shift-m"]).await;
2083 cx.assert_shared_state(indoc! {r"
2084 1 2 3
2085 4 5 ˇ6
2086 7 8 9
2087 "})
2088 .await;
2089 }
2090
2091 #[gpui::test]
2092 async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
2093 let mut cx = NeovimBackedTestContext::new(cx).await;
2094 let initial_state = indoc! {r"abc
2095 deˇf
2096 paragraph
2097 the second
2098 third and
2099 final"};
2100
2101 cx.set_shared_state(initial_state).await;
2102 cx.simulate_shared_keystrokes(["shift-l"]).await;
2103 cx.assert_shared_state(indoc! {r"abc
2104 def
2105 paragraph
2106 the second
2107 third and
2108 fiˇnal"})
2109 .await;
2110
2111 cx.set_shared_state(indoc! {r"
2112 1 2 3
2113 4 5 ˇ6
2114 7 8 9
2115 "})
2116 .await;
2117 cx.simulate_shared_keystrokes(["shift-l"]).await;
2118 cx.assert_shared_state(indoc! {r"
2119 1 2 3
2120 4 5 6
2121 7 8 9
2122 ˇ"})
2123 .await;
2124
2125 cx.set_shared_state(indoc! {r"
2126 1 2 3
2127 ˇ4 5 6
2128 7 8 9
2129 "})
2130 .await;
2131 cx.simulate_shared_keystrokes(["shift-l"]).await;
2132 cx.assert_shared_state(indoc! {r"
2133 1 2 3
2134 4 5 6
2135 7 8 9
2136 ˇ"})
2137 .await;
2138
2139 cx.set_shared_state(indoc! {r"
2140 1 2 ˇ3
2141 4 5 6
2142 7 8 9
2143 "})
2144 .await;
2145 cx.simulate_shared_keystrokes(["shift-l"]).await;
2146 cx.assert_shared_state(indoc! {r"
2147 1 2 3
2148 4 5 6
2149 7 8 9
2150 ˇ"})
2151 .await;
2152
2153 cx.set_shared_state(indoc! {r"
2154 ˇ1 2 3
2155 4 5 6
2156 7 8 9
2157 "})
2158 .await;
2159 cx.simulate_shared_keystrokes(["shift-l"]).await;
2160 cx.assert_shared_state(indoc! {r"
2161 1 2 3
2162 4 5 6
2163 7 8 9
2164 ˇ"})
2165 .await;
2166
2167 cx.set_shared_state(indoc! {r"
2168 1 2 3
2169 4 5 ˇ6
2170 7 8 9
2171 "})
2172 .await;
2173 cx.simulate_shared_keystrokes(["9", "shift-l"]).await;
2174 cx.assert_shared_state(indoc! {r"
2175 1 2 ˇ3
2176 4 5 6
2177 7 8 9
2178 "})
2179 .await;
2180 }
2181
2182 #[gpui::test]
2183 async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
2184 let mut cx = NeovimBackedTestContext::new(cx).await;
2185 cx.set_shared_state(indoc! {r"
2186 456 5ˇ67 678
2187 "})
2188 .await;
2189 cx.simulate_shared_keystrokes(["g", "e"]).await;
2190 cx.assert_shared_state(indoc! {r"
2191 45ˇ6 567 678
2192 "})
2193 .await;
2194
2195 // Test times
2196 cx.set_shared_state(indoc! {r"
2197 123 234 345
2198 456 5ˇ67 678
2199 "})
2200 .await;
2201 cx.simulate_shared_keystrokes(["4", "g", "e"]).await;
2202 cx.assert_shared_state(indoc! {r"
2203 12ˇ3 234 345
2204 456 567 678
2205 "})
2206 .await;
2207
2208 // With punctuation
2209 cx.set_shared_state(indoc! {r"
2210 123 234 345
2211 4;5.6 5ˇ67 678
2212 789 890 901
2213 "})
2214 .await;
2215 cx.simulate_shared_keystrokes(["g", "e"]).await;
2216 cx.assert_shared_state(indoc! {r"
2217 123 234 345
2218 4;5.ˇ6 567 678
2219 789 890 901
2220 "})
2221 .await;
2222
2223 // With punctuation and count
2224 cx.set_shared_state(indoc! {r"
2225 123 234 345
2226 4;5.6 5ˇ67 678
2227 789 890 901
2228 "})
2229 .await;
2230 cx.simulate_shared_keystrokes(["5", "g", "e"]).await;
2231 cx.assert_shared_state(indoc! {r"
2232 123 234 345
2233 ˇ4;5.6 567 678
2234 789 890 901
2235 "})
2236 .await;
2237
2238 // newlines
2239 cx.set_shared_state(indoc! {r"
2240 123 234 345
2241
2242 78ˇ9 890 901
2243 "})
2244 .await;
2245 cx.simulate_shared_keystrokes(["g", "e"]).await;
2246 cx.assert_shared_state(indoc! {r"
2247 123 234 345
2248 ˇ
2249 789 890 901
2250 "})
2251 .await;
2252 cx.simulate_shared_keystrokes(["g", "e"]).await;
2253 cx.assert_shared_state(indoc! {r"
2254 123 234 34ˇ5
2255
2256 789 890 901
2257 "})
2258 .await;
2259
2260 // With punctuation
2261 cx.set_shared_state(indoc! {r"
2262 123 234 345
2263 4;5.ˇ6 567 678
2264 789 890 901
2265 "})
2266 .await;
2267 cx.simulate_shared_keystrokes(["g", "shift-e"]).await;
2268 cx.assert_shared_state(indoc! {r"
2269 123 234 34ˇ5
2270 4;5.6 567 678
2271 789 890 901
2272 "})
2273 .await;
2274 }
2275
2276 #[gpui::test]
2277 async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
2278 let mut cx = NeovimBackedTestContext::new(cx).await;
2279
2280 cx.set_shared_state(indoc! {"
2281 fn aˇ() {
2282 return
2283 }
2284 "})
2285 .await;
2286 cx.simulate_shared_keystrokes(["v", "$", "%"]).await;
2287 cx.assert_shared_state(indoc! {"
2288 fn a«() {
2289 return
2290 }ˇ»
2291 "})
2292 .await;
2293 }
2294}