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