1use editor::{
2 display_map::{DisplayRow, DisplaySnapshot, FoldPoint, ToDisplayPoint},
3 movement::{
4 self, find_boundary, find_preceding_boundary_display_point, FindRange, TextLayoutDetails,
5 },
6 scroll::Autoscroll,
7 Anchor, Bias, DisplayPoint, RowExt, ToOffset,
8};
9use gpui::{actions, impl_actions, px, ViewContext, WindowContext};
10use language::{char_kind, CharKind, Point, Selection, SelectionGoal};
11use multi_buffer::MultiBufferRow;
12use serde::Deserialize;
13use std::ops::Range;
14use workspace::Workspace;
15
16use crate::{
17 normal::{mark, normal_motion},
18 state::{Mode, Operator},
19 surrounds::SurroundsType,
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 > 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 = MultiBufferRow(selection.start.to_point(&map).row);
864 if selection.end.to_point(&map).row > start_row.0 {
865 selection.end =
866 Point::new(start_row.0, 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().0 - begin_folded_line.row().0;
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().next_row(), 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(MultiBufferRow(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(MultiBufferRow(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
1443 // NOTE: depending on clip_at_line_end we may already be one char back from the end.
1444 if let Some((ch, _)) = map.buffer_chars_at(end_of_line).next() {
1445 if char_kind(&scope, ch) != CharKind::Whitespace {
1446 return end_of_line.to_display_point(map);
1447 }
1448 }
1449
1450 for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) {
1451 if ch == '\n' {
1452 break;
1453 }
1454 end_of_line = offset;
1455 if char_kind(&scope, ch) != CharKind::Whitespace || ch == '\n' {
1456 break;
1457 }
1458 }
1459
1460 end_of_line.to_display_point(map)
1461}
1462
1463pub(crate) fn start_of_line(
1464 map: &DisplaySnapshot,
1465 display_lines: bool,
1466 point: DisplayPoint,
1467) -> DisplayPoint {
1468 if display_lines {
1469 map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
1470 } else {
1471 map.prev_line_boundary(point.to_point(map)).1
1472 }
1473}
1474
1475pub(crate) fn end_of_line(
1476 map: &DisplaySnapshot,
1477 display_lines: bool,
1478 mut point: DisplayPoint,
1479 times: usize,
1480) -> DisplayPoint {
1481 if times > 1 {
1482 point = start_of_relative_buffer_row(map, point, times as isize - 1);
1483 }
1484 if display_lines {
1485 map.clip_point(
1486 DisplayPoint::new(point.row(), map.line_len(point.row())),
1487 Bias::Left,
1488 )
1489 } else {
1490 map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
1491 }
1492}
1493
1494fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
1495 let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
1496 *new_point.column_mut() = point.column();
1497 map.clip_point(new_point, Bias::Left)
1498}
1499
1500fn end_of_document(
1501 map: &DisplaySnapshot,
1502 point: DisplayPoint,
1503 line: Option<usize>,
1504) -> DisplayPoint {
1505 let new_row = if let Some(line) = line {
1506 (line - 1) as u32
1507 } else {
1508 map.max_buffer_row().0
1509 };
1510
1511 let new_point = Point::new(new_row, point.column());
1512 map.clip_point(new_point.to_display_point(map), Bias::Left)
1513}
1514
1515fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
1516 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
1517 let display_point = map.clip_at_line_end(display_point);
1518 let point = display_point.to_point(map);
1519 let offset = point.to_offset(&map.buffer_snapshot);
1520
1521 // Ensure the range is contained by the current line.
1522 let mut line_end = map.next_line_boundary(point).0;
1523 if line_end == point {
1524 line_end = map.max_point().to_point(map);
1525 }
1526
1527 let line_range = map.prev_line_boundary(point).0..line_end;
1528 let visible_line_range =
1529 line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
1530 let ranges = map
1531 .buffer_snapshot
1532 .bracket_ranges(visible_line_range.clone());
1533 if let Some(ranges) = ranges {
1534 let line_range = line_range.start.to_offset(&map.buffer_snapshot)
1535 ..line_range.end.to_offset(&map.buffer_snapshot);
1536 let mut closest_pair_destination = None;
1537 let mut closest_distance = usize::MAX;
1538
1539 for (open_range, close_range) in ranges {
1540 if open_range.start >= offset && line_range.contains(&open_range.start) {
1541 let distance = open_range.start - offset;
1542 if distance < closest_distance {
1543 closest_pair_destination = Some(close_range.start);
1544 closest_distance = distance;
1545 continue;
1546 }
1547 }
1548
1549 if close_range.start >= offset && line_range.contains(&close_range.start) {
1550 let distance = close_range.start - offset;
1551 if distance < closest_distance {
1552 closest_pair_destination = Some(open_range.start);
1553 closest_distance = distance;
1554 continue;
1555 }
1556 }
1557
1558 continue;
1559 }
1560
1561 closest_pair_destination
1562 .map(|destination| destination.to_display_point(map))
1563 .unwrap_or(display_point)
1564 } else {
1565 display_point
1566 }
1567}
1568
1569fn find_forward(
1570 map: &DisplaySnapshot,
1571 from: DisplayPoint,
1572 before: bool,
1573 target: char,
1574 times: usize,
1575 mode: FindRange,
1576 smartcase: bool,
1577) -> Option<DisplayPoint> {
1578 let mut to = from;
1579 let mut found = false;
1580
1581 for _ in 0..times {
1582 found = false;
1583 let new_to = find_boundary(map, to, mode, |_, right| {
1584 found = is_character_match(target, right, smartcase);
1585 found
1586 });
1587 if to == new_to {
1588 break;
1589 }
1590 to = new_to;
1591 }
1592
1593 if found {
1594 if before && to.column() > 0 {
1595 *to.column_mut() -= 1;
1596 Some(map.clip_point(to, Bias::Left))
1597 } else {
1598 Some(to)
1599 }
1600 } else {
1601 None
1602 }
1603}
1604
1605fn find_backward(
1606 map: &DisplaySnapshot,
1607 from: DisplayPoint,
1608 after: bool,
1609 target: char,
1610 times: usize,
1611 mode: FindRange,
1612 smartcase: bool,
1613) -> DisplayPoint {
1614 let mut to = from;
1615
1616 for _ in 0..times {
1617 let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
1618 is_character_match(target, right, smartcase)
1619 });
1620 if to == new_to {
1621 break;
1622 }
1623 to = new_to;
1624 }
1625
1626 let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
1627 if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
1628 if after {
1629 *to.column_mut() += 1;
1630 map.clip_point(to, Bias::Right)
1631 } else {
1632 to
1633 }
1634 } else {
1635 from
1636 }
1637}
1638
1639fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
1640 if smartcase {
1641 if target.is_uppercase() {
1642 target == other
1643 } else {
1644 target == other.to_ascii_lowercase()
1645 }
1646 } else {
1647 target == other
1648 }
1649}
1650
1651fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1652 let correct_line = start_of_relative_buffer_row(map, point, times as isize);
1653 first_non_whitespace(map, false, correct_line)
1654}
1655
1656fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1657 let correct_line = start_of_relative_buffer_row(map, point, 0);
1658 right(map, correct_line, times.saturating_sub(1))
1659}
1660
1661pub(crate) fn next_line_end(
1662 map: &DisplaySnapshot,
1663 mut point: DisplayPoint,
1664 times: usize,
1665) -> DisplayPoint {
1666 if times > 1 {
1667 point = start_of_relative_buffer_row(map, point, times as isize - 1);
1668 }
1669 end_of_line(map, false, point, 1)
1670}
1671
1672fn window_top(
1673 map: &DisplaySnapshot,
1674 point: DisplayPoint,
1675 text_layout_details: &TextLayoutDetails,
1676 mut times: usize,
1677) -> (DisplayPoint, SelectionGoal) {
1678 let first_visible_line = text_layout_details
1679 .scroll_anchor
1680 .anchor
1681 .to_display_point(map);
1682
1683 if first_visible_line.row() != DisplayRow(0)
1684 && text_layout_details.vertical_scroll_margin as usize > times
1685 {
1686 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1687 }
1688
1689 if let Some(visible_rows) = text_layout_details.visible_rows {
1690 let bottom_row = first_visible_line.row().0 + visible_rows as u32;
1691 let new_row = (first_visible_line.row().0 + (times as u32))
1692 .min(bottom_row)
1693 .min(map.max_point().row().0);
1694 let new_col = point.column().min(map.line_len(first_visible_line.row()));
1695
1696 let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
1697 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1698 } else {
1699 let new_row =
1700 DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
1701 let new_col = point.column().min(map.line_len(first_visible_line.row()));
1702
1703 let new_point = DisplayPoint::new(new_row, new_col);
1704 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1705 }
1706}
1707
1708fn window_middle(
1709 map: &DisplaySnapshot,
1710 point: DisplayPoint,
1711 text_layout_details: &TextLayoutDetails,
1712) -> (DisplayPoint, SelectionGoal) {
1713 if let Some(visible_rows) = text_layout_details.visible_rows {
1714 let first_visible_line = text_layout_details
1715 .scroll_anchor
1716 .anchor
1717 .to_display_point(map);
1718
1719 let max_visible_rows =
1720 (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
1721
1722 let new_row =
1723 (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
1724 let new_row = DisplayRow(new_row);
1725 let new_col = point.column().min(map.line_len(new_row));
1726 let new_point = DisplayPoint::new(new_row, new_col);
1727 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1728 } else {
1729 (point, SelectionGoal::None)
1730 }
1731}
1732
1733fn window_bottom(
1734 map: &DisplaySnapshot,
1735 point: DisplayPoint,
1736 text_layout_details: &TextLayoutDetails,
1737 mut times: usize,
1738) -> (DisplayPoint, SelectionGoal) {
1739 if let Some(visible_rows) = text_layout_details.visible_rows {
1740 let first_visible_line = text_layout_details
1741 .scroll_anchor
1742 .anchor
1743 .to_display_point(map);
1744 let bottom_row = first_visible_line.row().0
1745 + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
1746 if bottom_row < map.max_point().row().0
1747 && text_layout_details.vertical_scroll_margin as usize > times
1748 {
1749 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1750 }
1751 let bottom_row_capped = bottom_row.min(map.max_point().row().0);
1752 let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
1753 {
1754 first_visible_line.row()
1755 } else {
1756 DisplayRow(bottom_row_capped.saturating_sub(times as u32))
1757 };
1758 let new_col = point.column().min(map.line_len(new_row));
1759 let new_point = DisplayPoint::new(new_row, new_col);
1760 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1761 } else {
1762 (point, SelectionGoal::None)
1763 }
1764}
1765
1766pub fn coerce_punctuation(kind: CharKind, treat_punctuation_as_word: bool) -> CharKind {
1767 if treat_punctuation_as_word && kind == CharKind::Punctuation {
1768 CharKind::Word
1769 } else {
1770 kind
1771 }
1772}
1773
1774#[cfg(test)]
1775mod test {
1776
1777 use crate::test::NeovimBackedTestContext;
1778 use indoc::indoc;
1779
1780 #[gpui::test]
1781 async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
1782 let mut cx = NeovimBackedTestContext::new(cx).await;
1783
1784 let initial_state = indoc! {r"ˇabc
1785 def
1786
1787 paragraph
1788 the second
1789
1790
1791
1792 third and
1793 final"};
1794
1795 // goes down once
1796 cx.set_shared_state(initial_state).await;
1797 cx.simulate_shared_keystrokes("}").await;
1798 cx.shared_state().await.assert_eq(indoc! {r"abc
1799 def
1800 ˇ
1801 paragraph
1802 the second
1803
1804
1805
1806 third and
1807 final"});
1808
1809 // goes up once
1810 cx.simulate_shared_keystrokes("{").await;
1811 cx.shared_state().await.assert_eq(initial_state);
1812
1813 // goes down twice
1814 cx.simulate_shared_keystrokes("2 }").await;
1815 cx.shared_state().await.assert_eq(indoc! {r"abc
1816 def
1817
1818 paragraph
1819 the second
1820 ˇ
1821
1822
1823 third and
1824 final"});
1825
1826 // goes down over multiple blanks
1827 cx.simulate_shared_keystrokes("}").await;
1828 cx.shared_state().await.assert_eq(indoc! {r"abc
1829 def
1830
1831 paragraph
1832 the second
1833
1834
1835
1836 third and
1837 finaˇl"});
1838
1839 // goes up twice
1840 cx.simulate_shared_keystrokes("2 {").await;
1841 cx.shared_state().await.assert_eq(indoc! {r"abc
1842 def
1843 ˇ
1844 paragraph
1845 the second
1846
1847
1848
1849 third and
1850 final"});
1851 }
1852
1853 #[gpui::test]
1854 async fn test_matching(cx: &mut gpui::TestAppContext) {
1855 let mut cx = NeovimBackedTestContext::new(cx).await;
1856
1857 cx.set_shared_state(indoc! {r"func ˇ(a string) {
1858 do(something(with<Types>.and_arrays[0, 2]))
1859 }"})
1860 .await;
1861 cx.simulate_shared_keystrokes("%").await;
1862 cx.shared_state()
1863 .await
1864 .assert_eq(indoc! {r"func (a stringˇ) {
1865 do(something(with<Types>.and_arrays[0, 2]))
1866 }"});
1867
1868 // test it works on the last character of the line
1869 cx.set_shared_state(indoc! {r"func (a string) ˇ{
1870 do(something(with<Types>.and_arrays[0, 2]))
1871 }"})
1872 .await;
1873 cx.simulate_shared_keystrokes("%").await;
1874 cx.shared_state()
1875 .await
1876 .assert_eq(indoc! {r"func (a string) {
1877 do(something(with<Types>.and_arrays[0, 2]))
1878 ˇ}"});
1879
1880 // test it works on immediate nesting
1881 cx.set_shared_state("ˇ{()}").await;
1882 cx.simulate_shared_keystrokes("%").await;
1883 cx.shared_state().await.assert_eq("{()ˇ}");
1884 cx.simulate_shared_keystrokes("%").await;
1885 cx.shared_state().await.assert_eq("ˇ{()}");
1886
1887 // test it works on immediate nesting inside braces
1888 cx.set_shared_state("{\n ˇ{()}\n}").await;
1889 cx.simulate_shared_keystrokes("%").await;
1890 cx.shared_state().await.assert_eq("{\n {()ˇ}\n}");
1891
1892 // test it jumps to the next paren on a line
1893 cx.set_shared_state("func ˇboop() {\n}").await;
1894 cx.simulate_shared_keystrokes("%").await;
1895 cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
1896 }
1897
1898 #[gpui::test]
1899 async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
1900 let mut cx = NeovimBackedTestContext::new(cx).await;
1901
1902 // f and F
1903 cx.set_shared_state("ˇone two three four").await;
1904 cx.simulate_shared_keystrokes("f o").await;
1905 cx.shared_state().await.assert_eq("one twˇo three four");
1906 cx.simulate_shared_keystrokes(",").await;
1907 cx.shared_state().await.assert_eq("ˇone two three four");
1908 cx.simulate_shared_keystrokes("2 ;").await;
1909 cx.shared_state().await.assert_eq("one two three fˇour");
1910 cx.simulate_shared_keystrokes("shift-f e").await;
1911 cx.shared_state().await.assert_eq("one two threˇe four");
1912 cx.simulate_shared_keystrokes("2 ;").await;
1913 cx.shared_state().await.assert_eq("onˇe two three four");
1914 cx.simulate_shared_keystrokes(",").await;
1915 cx.shared_state().await.assert_eq("one two thrˇee four");
1916
1917 // t and T
1918 cx.set_shared_state("ˇone two three four").await;
1919 cx.simulate_shared_keystrokes("t o").await;
1920 cx.shared_state().await.assert_eq("one tˇwo three four");
1921 cx.simulate_shared_keystrokes(",").await;
1922 cx.shared_state().await.assert_eq("oˇne two three four");
1923 cx.simulate_shared_keystrokes("2 ;").await;
1924 cx.shared_state().await.assert_eq("one two three ˇfour");
1925 cx.simulate_shared_keystrokes("shift-t e").await;
1926 cx.shared_state().await.assert_eq("one two threeˇ four");
1927 cx.simulate_shared_keystrokes("3 ;").await;
1928 cx.shared_state().await.assert_eq("oneˇ two three four");
1929 cx.simulate_shared_keystrokes(",").await;
1930 cx.shared_state().await.assert_eq("one two thˇree four");
1931 }
1932
1933 #[gpui::test]
1934 async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
1935 let mut cx = NeovimBackedTestContext::new(cx).await;
1936 let initial_state = indoc! {r"something(ˇfoo)"};
1937 cx.set_shared_state(initial_state).await;
1938 cx.simulate_shared_keystrokes("}").await;
1939 cx.shared_state().await.assert_eq("something(fooˇ)");
1940 }
1941
1942 #[gpui::test]
1943 async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
1944 let mut cx = NeovimBackedTestContext::new(cx).await;
1945 cx.set_shared_state("ˇone\n two\nthree").await;
1946 cx.simulate_shared_keystrokes("enter").await;
1947 cx.shared_state().await.assert_eq("one\n ˇtwo\nthree");
1948 }
1949
1950 #[gpui::test]
1951 async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
1952 let mut cx = NeovimBackedTestContext::new(cx).await;
1953 cx.set_shared_state("ˇ one\n two \nthree").await;
1954 cx.simulate_shared_keystrokes("g _").await;
1955 cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
1956
1957 cx.set_shared_state("ˇ one \n two \nthree").await;
1958 cx.simulate_shared_keystrokes("g _").await;
1959 cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
1960 cx.simulate_shared_keystrokes("2 g _").await;
1961 cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
1962 }
1963
1964 #[gpui::test]
1965 async fn test_window_top(cx: &mut gpui::TestAppContext) {
1966 let mut cx = NeovimBackedTestContext::new(cx).await;
1967 let initial_state = indoc! {r"abc
1968 def
1969 paragraph
1970 the second
1971 third ˇand
1972 final"};
1973
1974 cx.set_shared_state(initial_state).await;
1975 cx.simulate_shared_keystrokes("shift-h").await;
1976 cx.shared_state().await.assert_eq(indoc! {r"abˇc
1977 def
1978 paragraph
1979 the second
1980 third and
1981 final"});
1982
1983 // clip point
1984 cx.set_shared_state(indoc! {r"
1985 1 2 3
1986 4 5 6
1987 7 8 ˇ9
1988 "})
1989 .await;
1990 cx.simulate_shared_keystrokes("shift-h").await;
1991 cx.shared_state().await.assert_eq(indoc! {"
1992 1 2 ˇ3
1993 4 5 6
1994 7 8 9
1995 "});
1996
1997 cx.set_shared_state(indoc! {r"
1998 1 2 3
1999 4 5 6
2000 ˇ7 8 9
2001 "})
2002 .await;
2003 cx.simulate_shared_keystrokes("shift-h").await;
2004 cx.shared_state().await.assert_eq(indoc! {"
2005 ˇ1 2 3
2006 4 5 6
2007 7 8 9
2008 "});
2009
2010 cx.set_shared_state(indoc! {r"
2011 1 2 3
2012 4 5 ˇ6
2013 7 8 9"})
2014 .await;
2015 cx.simulate_shared_keystrokes("9 shift-h").await;
2016 cx.shared_state().await.assert_eq(indoc! {"
2017 1 2 3
2018 4 5 6
2019 7 8 ˇ9"});
2020 }
2021
2022 #[gpui::test]
2023 async fn test_window_middle(cx: &mut gpui::TestAppContext) {
2024 let mut cx = NeovimBackedTestContext::new(cx).await;
2025 let initial_state = indoc! {r"abˇc
2026 def
2027 paragraph
2028 the second
2029 third and
2030 final"};
2031
2032 cx.set_shared_state(initial_state).await;
2033 cx.simulate_shared_keystrokes("shift-m").await;
2034 cx.shared_state().await.assert_eq(indoc! {r"abc
2035 def
2036 paˇragraph
2037 the second
2038 third and
2039 final"});
2040
2041 cx.set_shared_state(indoc! {r"
2042 1 2 3
2043 4 5 6
2044 7 8 ˇ9
2045 "})
2046 .await;
2047 cx.simulate_shared_keystrokes("shift-m").await;
2048 cx.shared_state().await.assert_eq(indoc! {"
2049 1 2 3
2050 4 5 ˇ6
2051 7 8 9
2052 "});
2053 cx.set_shared_state(indoc! {r"
2054 1 2 3
2055 4 5 6
2056 ˇ7 8 9
2057 "})
2058 .await;
2059 cx.simulate_shared_keystrokes("shift-m").await;
2060 cx.shared_state().await.assert_eq(indoc! {"
2061 1 2 3
2062 ˇ4 5 6
2063 7 8 9
2064 "});
2065 cx.set_shared_state(indoc! {r"
2066 ˇ1 2 3
2067 4 5 6
2068 7 8 9
2069 "})
2070 .await;
2071 cx.simulate_shared_keystrokes("shift-m").await;
2072 cx.shared_state().await.assert_eq(indoc! {"
2073 1 2 3
2074 ˇ4 5 6
2075 7 8 9
2076 "});
2077 cx.set_shared_state(indoc! {r"
2078 1 2 3
2079 ˇ4 5 6
2080 7 8 9
2081 "})
2082 .await;
2083 cx.simulate_shared_keystrokes("shift-m").await;
2084 cx.shared_state().await.assert_eq(indoc! {"
2085 1 2 3
2086 ˇ4 5 6
2087 7 8 9
2088 "});
2089 cx.set_shared_state(indoc! {r"
2090 1 2 3
2091 4 5 ˇ6
2092 7 8 9
2093 "})
2094 .await;
2095 cx.simulate_shared_keystrokes("shift-m").await;
2096 cx.shared_state().await.assert_eq(indoc! {"
2097 1 2 3
2098 4 5 ˇ6
2099 7 8 9
2100 "});
2101 }
2102
2103 #[gpui::test]
2104 async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
2105 let mut cx = NeovimBackedTestContext::new(cx).await;
2106 let initial_state = indoc! {r"abc
2107 deˇf
2108 paragraph
2109 the second
2110 third and
2111 final"};
2112
2113 cx.set_shared_state(initial_state).await;
2114 cx.simulate_shared_keystrokes("shift-l").await;
2115 cx.shared_state().await.assert_eq(indoc! {r"abc
2116 def
2117 paragraph
2118 the second
2119 third and
2120 fiˇnal"});
2121
2122 cx.set_shared_state(indoc! {r"
2123 1 2 3
2124 4 5 ˇ6
2125 7 8 9
2126 "})
2127 .await;
2128 cx.simulate_shared_keystrokes("shift-l").await;
2129 cx.shared_state().await.assert_eq(indoc! {"
2130 1 2 3
2131 4 5 6
2132 7 8 9
2133 ˇ"});
2134
2135 cx.set_shared_state(indoc! {r"
2136 1 2 3
2137 ˇ4 5 6
2138 7 8 9
2139 "})
2140 .await;
2141 cx.simulate_shared_keystrokes("shift-l").await;
2142 cx.shared_state().await.assert_eq(indoc! {"
2143 1 2 3
2144 4 5 6
2145 7 8 9
2146 ˇ"});
2147
2148 cx.set_shared_state(indoc! {r"
2149 1 2 ˇ3
2150 4 5 6
2151 7 8 9
2152 "})
2153 .await;
2154 cx.simulate_shared_keystrokes("shift-l").await;
2155 cx.shared_state().await.assert_eq(indoc! {"
2156 1 2 3
2157 4 5 6
2158 7 8 9
2159 ˇ"});
2160
2161 cx.set_shared_state(indoc! {r"
2162 ˇ1 2 3
2163 4 5 6
2164 7 8 9
2165 "})
2166 .await;
2167 cx.simulate_shared_keystrokes("shift-l").await;
2168 cx.shared_state().await.assert_eq(indoc! {"
2169 1 2 3
2170 4 5 6
2171 7 8 9
2172 ˇ"});
2173
2174 cx.set_shared_state(indoc! {r"
2175 1 2 3
2176 4 5 ˇ6
2177 7 8 9
2178 "})
2179 .await;
2180 cx.simulate_shared_keystrokes("9 shift-l").await;
2181 cx.shared_state().await.assert_eq(indoc! {"
2182 1 2 ˇ3
2183 4 5 6
2184 7 8 9
2185 "});
2186 }
2187
2188 #[gpui::test]
2189 async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
2190 let mut cx = NeovimBackedTestContext::new(cx).await;
2191 cx.set_shared_state(indoc! {r"
2192 456 5ˇ67 678
2193 "})
2194 .await;
2195 cx.simulate_shared_keystrokes("g e").await;
2196 cx.shared_state().await.assert_eq(indoc! {"
2197 45ˇ6 567 678
2198 "});
2199
2200 // Test times
2201 cx.set_shared_state(indoc! {r"
2202 123 234 345
2203 456 5ˇ67 678
2204 "})
2205 .await;
2206 cx.simulate_shared_keystrokes("4 g e").await;
2207 cx.shared_state().await.assert_eq(indoc! {"
2208 12ˇ3 234 345
2209 456 567 678
2210 "});
2211
2212 // With punctuation
2213 cx.set_shared_state(indoc! {r"
2214 123 234 345
2215 4;5.6 5ˇ67 678
2216 789 890 901
2217 "})
2218 .await;
2219 cx.simulate_shared_keystrokes("g e").await;
2220 cx.shared_state().await.assert_eq(indoc! {"
2221 123 234 345
2222 4;5.ˇ6 567 678
2223 789 890 901
2224 "});
2225
2226 // With punctuation and count
2227 cx.set_shared_state(indoc! {r"
2228 123 234 345
2229 4;5.6 5ˇ67 678
2230 789 890 901
2231 "})
2232 .await;
2233 cx.simulate_shared_keystrokes("5 g e").await;
2234 cx.shared_state().await.assert_eq(indoc! {"
2235 123 234 345
2236 ˇ4;5.6 567 678
2237 789 890 901
2238 "});
2239
2240 // newlines
2241 cx.set_shared_state(indoc! {r"
2242 123 234 345
2243
2244 78ˇ9 890 901
2245 "})
2246 .await;
2247 cx.simulate_shared_keystrokes("g e").await;
2248 cx.shared_state().await.assert_eq(indoc! {"
2249 123 234 345
2250 ˇ
2251 789 890 901
2252 "});
2253 cx.simulate_shared_keystrokes("g e").await;
2254 cx.shared_state().await.assert_eq(indoc! {"
2255 123 234 34ˇ5
2256
2257 789 890 901
2258 "});
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.shared_state().await.assert_eq(indoc! {"
2269 123 234 34ˇ5
2270 4;5.6 567 678
2271 789 890 901
2272 "});
2273 }
2274
2275 #[gpui::test]
2276 async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
2277 let mut cx = NeovimBackedTestContext::new(cx).await;
2278
2279 cx.set_shared_state(indoc! {"
2280 fn aˇ() {
2281 return
2282 }
2283 "})
2284 .await;
2285 cx.simulate_shared_keystrokes("v $ %").await;
2286 cx.shared_state().await.assert_eq(indoc! {"
2287 fn a«() {
2288 return
2289 }ˇ»
2290 "});
2291 }
2292}