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