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 vim.update_state(|state| state.pre_count = count)
451 }
452 });
453}
454
455// Motion handling is specified here:
456// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
457impl Motion {
458 pub fn linewise(&self) -> bool {
459 use Motion::*;
460 match self {
461 Down { .. }
462 | Up { .. }
463 | StartOfDocument
464 | EndOfDocument
465 | CurrentLine
466 | NextLineStart
467 | StartOfLineDownward
468 | StartOfParagraph
469 | WindowTop
470 | WindowMiddle
471 | WindowBottom
472 | EndOfParagraph => true,
473 EndOfLine { .. }
474 | Matching
475 | FindForward { .. }
476 | Left
477 | Backspace
478 | Right
479 | Space
480 | StartOfLine { .. }
481 | EndOfLineDownward
482 | GoToColumn
483 | NextWordStart { .. }
484 | NextWordEnd { .. }
485 | PreviousWordStart { .. }
486 | PreviousWordEnd { .. }
487 | NextSubwordStart { .. }
488 | NextSubwordEnd { .. }
489 | PreviousSubwordStart { .. }
490 | PreviousSubwordEnd { .. }
491 | FirstNonWhitespace { .. }
492 | FindBackward { .. }
493 | RepeatFind { .. }
494 | RepeatFindReversed { .. }
495 | ZedSearchResult { .. } => false,
496 }
497 }
498
499 pub fn infallible(&self) -> bool {
500 use Motion::*;
501 match self {
502 StartOfDocument | EndOfDocument | CurrentLine => true,
503 Down { .. }
504 | Up { .. }
505 | EndOfLine { .. }
506 | Matching
507 | FindForward { .. }
508 | RepeatFind { .. }
509 | Left
510 | Backspace
511 | Right
512 | Space
513 | StartOfLine { .. }
514 | StartOfParagraph
515 | EndOfParagraph
516 | StartOfLineDownward
517 | EndOfLineDownward
518 | GoToColumn
519 | NextWordStart { .. }
520 | NextWordEnd { .. }
521 | PreviousWordStart { .. }
522 | PreviousWordEnd { .. }
523 | NextSubwordStart { .. }
524 | NextSubwordEnd { .. }
525 | PreviousSubwordStart { .. }
526 | PreviousSubwordEnd { .. }
527 | FirstNonWhitespace { .. }
528 | FindBackward { .. }
529 | RepeatFindReversed { .. }
530 | WindowTop
531 | WindowMiddle
532 | WindowBottom
533 | NextLineStart
534 | ZedSearchResult { .. } => false,
535 }
536 }
537
538 pub fn inclusive(&self) -> bool {
539 use Motion::*;
540 match self {
541 Down { .. }
542 | Up { .. }
543 | StartOfDocument
544 | EndOfDocument
545 | CurrentLine
546 | EndOfLine { .. }
547 | EndOfLineDownward
548 | Matching
549 | FindForward { .. }
550 | WindowTop
551 | WindowMiddle
552 | WindowBottom
553 | NextWordEnd { .. }
554 | PreviousWordEnd { .. }
555 | NextSubwordEnd { .. }
556 | PreviousSubwordEnd { .. }
557 | NextLineStart => true,
558 Left
559 | Backspace
560 | Right
561 | Space
562 | StartOfLine { .. }
563 | StartOfLineDownward
564 | StartOfParagraph
565 | EndOfParagraph
566 | GoToColumn
567 | NextWordStart { .. }
568 | PreviousWordStart { .. }
569 | NextSubwordStart { .. }
570 | PreviousSubwordStart { .. }
571 | FirstNonWhitespace { .. }
572 | FindBackward { .. }
573 | ZedSearchResult { .. } => false,
574 RepeatFind { last_find: motion } | RepeatFindReversed { last_find: motion } => {
575 motion.inclusive()
576 }
577 }
578 }
579
580 pub fn move_point(
581 &self,
582 map: &DisplaySnapshot,
583 point: DisplayPoint,
584 goal: SelectionGoal,
585 maybe_times: Option<usize>,
586 text_layout_details: &TextLayoutDetails,
587 ) -> Option<(DisplayPoint, SelectionGoal)> {
588 let times = maybe_times.unwrap_or(1);
589 use Motion::*;
590 let infallible = self.infallible();
591 let (new_point, goal) = match self {
592 Left => (left(map, point, times), SelectionGoal::None),
593 Backspace => (backspace(map, point, times), SelectionGoal::None),
594 Down {
595 display_lines: false,
596 } => up_down_buffer_rows(map, point, goal, times as isize, &text_layout_details),
597 Down {
598 display_lines: true,
599 } => down_display(map, point, goal, times, &text_layout_details),
600 Up {
601 display_lines: false,
602 } => up_down_buffer_rows(map, point, goal, 0 - times as isize, &text_layout_details),
603 Up {
604 display_lines: true,
605 } => up_display(map, point, goal, times, &text_layout_details),
606 Right => (right(map, point, times), SelectionGoal::None),
607 Space => (space(map, point, times), SelectionGoal::None),
608 NextWordStart { ignore_punctuation } => (
609 next_word_start(map, point, *ignore_punctuation, times),
610 SelectionGoal::None,
611 ),
612 NextWordEnd { ignore_punctuation } => (
613 next_word_end(map, point, *ignore_punctuation, times, true),
614 SelectionGoal::None,
615 ),
616 PreviousWordStart { ignore_punctuation } => (
617 previous_word_start(map, point, *ignore_punctuation, times),
618 SelectionGoal::None,
619 ),
620 PreviousWordEnd { ignore_punctuation } => (
621 previous_word_end(map, point, *ignore_punctuation, times),
622 SelectionGoal::None,
623 ),
624 NextSubwordStart { ignore_punctuation } => (
625 next_subword_start(map, point, *ignore_punctuation, times),
626 SelectionGoal::None,
627 ),
628 NextSubwordEnd { ignore_punctuation } => (
629 next_subword_end(map, point, *ignore_punctuation, times, true),
630 SelectionGoal::None,
631 ),
632 PreviousSubwordStart { ignore_punctuation } => (
633 previous_subword_start(map, point, *ignore_punctuation, times),
634 SelectionGoal::None,
635 ),
636 PreviousSubwordEnd { ignore_punctuation } => (
637 previous_subword_end(map, point, *ignore_punctuation, times),
638 SelectionGoal::None,
639 ),
640 FirstNonWhitespace { display_lines } => (
641 first_non_whitespace(map, *display_lines, point),
642 SelectionGoal::None,
643 ),
644 StartOfLine { display_lines } => (
645 start_of_line(map, *display_lines, point),
646 SelectionGoal::None,
647 ),
648 EndOfLine { display_lines } => (
649 end_of_line(map, *display_lines, point, times),
650 SelectionGoal::None,
651 ),
652 StartOfParagraph => (
653 movement::start_of_paragraph(map, point, times),
654 SelectionGoal::None,
655 ),
656 EndOfParagraph => (
657 map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
658 SelectionGoal::None,
659 ),
660 CurrentLine => (next_line_end(map, point, times), SelectionGoal::None),
661 StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
662 EndOfDocument => (
663 end_of_document(map, point, maybe_times),
664 SelectionGoal::None,
665 ),
666 Matching => (matching(map, point), SelectionGoal::None),
667 // t f
668 FindForward {
669 before,
670 char,
671 mode,
672 smartcase,
673 } => {
674 return find_forward(map, point, *before, *char, times, *mode, *smartcase)
675 .map(|new_point| (new_point, SelectionGoal::None))
676 }
677 // T F
678 FindBackward {
679 after,
680 char,
681 mode,
682 smartcase,
683 } => (
684 find_backward(map, point, *after, *char, times, *mode, *smartcase),
685 SelectionGoal::None,
686 ),
687 // ; -- repeat the last find done with t, f, T, F
688 RepeatFind { last_find } => match **last_find {
689 Motion::FindForward {
690 before,
691 char,
692 mode,
693 smartcase,
694 } => {
695 let mut new_point =
696 find_forward(map, point, before, char, times, mode, smartcase);
697 if new_point == Some(point) {
698 new_point =
699 find_forward(map, point, before, char, times + 1, mode, smartcase);
700 }
701
702 return new_point.map(|new_point| (new_point, SelectionGoal::None));
703 }
704
705 Motion::FindBackward {
706 after,
707 char,
708 mode,
709 smartcase,
710 } => {
711 let mut new_point =
712 find_backward(map, point, after, char, times, mode, smartcase);
713 if new_point == point {
714 new_point =
715 find_backward(map, point, after, char, times + 1, mode, smartcase);
716 }
717
718 (new_point, SelectionGoal::None)
719 }
720 _ => return None,
721 },
722 // , -- repeat the last find done with t, f, T, F, in opposite direction
723 RepeatFindReversed { last_find } => match **last_find {
724 Motion::FindForward {
725 before,
726 char,
727 mode,
728 smartcase,
729 } => {
730 let mut new_point =
731 find_backward(map, point, before, char, times, mode, smartcase);
732 if new_point == point {
733 new_point =
734 find_backward(map, point, before, char, times + 1, mode, smartcase);
735 }
736
737 (new_point, SelectionGoal::None)
738 }
739
740 Motion::FindBackward {
741 after,
742 char,
743 mode,
744 smartcase,
745 } => {
746 let mut new_point =
747 find_forward(map, point, after, char, times, mode, smartcase);
748 if new_point == Some(point) {
749 new_point =
750 find_forward(map, point, after, char, times + 1, mode, smartcase);
751 }
752
753 return new_point.map(|new_point| (new_point, SelectionGoal::None));
754 }
755 _ => return None,
756 },
757 NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
758 StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None),
759 EndOfLineDownward => (last_non_whitespace(map, point, times), SelectionGoal::None),
760 GoToColumn => (go_to_column(map, point, times), SelectionGoal::None),
761 WindowTop => window_top(map, point, &text_layout_details, times - 1),
762 WindowMiddle => window_middle(map, point, &text_layout_details),
763 WindowBottom => window_bottom(map, point, &text_layout_details, times - 1),
764 ZedSearchResult { new_selections, .. } => {
765 // There will be only one selection, as
766 // Search::SelectNextMatch selects a single match.
767 if let Some(new_selection) = new_selections.first() {
768 (
769 new_selection.start.to_display_point(map),
770 SelectionGoal::None,
771 )
772 } else {
773 return None;
774 }
775 }
776 };
777
778 (new_point != point || infallible).then_some((new_point, goal))
779 }
780
781 // Get the range value after self is applied to the specified selection.
782 pub fn range(
783 &self,
784 map: &DisplaySnapshot,
785 selection: Selection<DisplayPoint>,
786 times: Option<usize>,
787 expand_to_surrounding_newline: bool,
788 text_layout_details: &TextLayoutDetails,
789 ) -> Option<Range<DisplayPoint>> {
790 if let Motion::ZedSearchResult {
791 prior_selections,
792 new_selections,
793 } = self
794 {
795 if let Some((prior_selection, new_selection)) =
796 prior_selections.first().zip(new_selections.first())
797 {
798 let start = prior_selection
799 .start
800 .to_display_point(map)
801 .min(new_selection.start.to_display_point(map));
802 let end = new_selection
803 .end
804 .to_display_point(map)
805 .max(prior_selection.end.to_display_point(map));
806
807 if start < end {
808 return Some(start..end);
809 } else {
810 return Some(end..start);
811 }
812 } else {
813 return None;
814 }
815 }
816
817 if let Some((new_head, goal)) = self.move_point(
818 map,
819 selection.head(),
820 selection.goal,
821 times,
822 &text_layout_details,
823 ) {
824 let mut selection = selection.clone();
825 selection.set_head(new_head, goal);
826
827 if self.linewise() {
828 selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
829
830 if expand_to_surrounding_newline {
831 if selection.end.row() < map.max_point().row() {
832 *selection.end.row_mut() += 1;
833 *selection.end.column_mut() = 0;
834 selection.end = map.clip_point(selection.end, Bias::Right);
835 // Don't reset the end here
836 return Some(selection.start..selection.end);
837 } else if selection.start.row() > 0 {
838 *selection.start.row_mut() -= 1;
839 *selection.start.column_mut() = map.line_len(selection.start.row());
840 selection.start = map.clip_point(selection.start, Bias::Left);
841 }
842 }
843
844 selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
845 } else {
846 // Another special case: When using the "w" motion in combination with an
847 // operator and the last word moved over is at the end of a line, the end of
848 // that word becomes the end of the operated text, not the first word in the
849 // next line.
850 if let Motion::NextWordStart {
851 ignore_punctuation: _,
852 } = self
853 {
854 let start_row = selection.start.to_point(&map).row;
855 if selection.end.to_point(&map).row > start_row {
856 selection.end =
857 Point::new(start_row, map.buffer_snapshot.line_len(start_row))
858 .to_display_point(&map)
859 }
860 }
861
862 // If the motion is exclusive and the end of the motion is in column 1, the
863 // end of the motion is moved to the end of the previous line and the motion
864 // becomes inclusive. Example: "}" moves to the first line after a paragraph,
865 // but "d}" will not include that line.
866 let mut inclusive = self.inclusive();
867 if !inclusive
868 && self != &Motion::Backspace
869 && selection.end.row() > selection.start.row()
870 && selection.end.column() == 0
871 {
872 inclusive = true;
873 *selection.end.row_mut() -= 1;
874 *selection.end.column_mut() = 0;
875 selection.end = map.clip_point(
876 map.next_line_boundary(selection.end.to_point(map)).1,
877 Bias::Left,
878 );
879 }
880
881 if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
882 *selection.end.column_mut() += 1;
883 }
884 }
885 Some(selection.start..selection.end)
886 } else {
887 None
888 }
889 }
890
891 // Expands a selection using self for an operator
892 pub fn expand_selection(
893 &self,
894 map: &DisplaySnapshot,
895 selection: &mut Selection<DisplayPoint>,
896 times: Option<usize>,
897 expand_to_surrounding_newline: bool,
898 text_layout_details: &TextLayoutDetails,
899 ) -> bool {
900 if let Some(range) = self.range(
901 map,
902 selection.clone(),
903 times,
904 expand_to_surrounding_newline,
905 text_layout_details,
906 ) {
907 selection.start = range.start;
908 selection.end = range.end;
909 true
910 } else {
911 false
912 }
913 }
914}
915
916fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
917 for _ in 0..times {
918 point = movement::saturating_left(map, point);
919 if point.column() == 0 {
920 break;
921 }
922 }
923 point
924}
925
926pub(crate) fn backspace(
927 map: &DisplaySnapshot,
928 mut point: DisplayPoint,
929 times: usize,
930) -> DisplayPoint {
931 for _ in 0..times {
932 point = movement::left(map, point);
933 if point.is_zero() {
934 break;
935 }
936 }
937 point
938}
939
940fn space(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
941 for _ in 0..times {
942 point = wrapping_right(map, point);
943 if point == map.max_point() {
944 break;
945 }
946 }
947 point
948}
949
950fn wrapping_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
951 let max_column = map.line_len(point.row()).saturating_sub(1);
952 if point.column() < max_column {
953 *point.column_mut() += 1;
954 } else if point.row() < map.max_point().row() {
955 *point.row_mut() += 1;
956 *point.column_mut() = 0;
957 }
958 point
959}
960
961pub(crate) fn start_of_relative_buffer_row(
962 map: &DisplaySnapshot,
963 point: DisplayPoint,
964 times: isize,
965) -> DisplayPoint {
966 let start = map.display_point_to_fold_point(point, Bias::Left);
967 let target = start.row() as isize + times;
968 let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
969
970 map.clip_point(
971 map.fold_point_to_display_point(
972 map.fold_snapshot
973 .clip_point(FoldPoint::new(new_row, 0), Bias::Right),
974 ),
975 Bias::Right,
976 )
977}
978
979fn up_down_buffer_rows(
980 map: &DisplaySnapshot,
981 point: DisplayPoint,
982 mut goal: SelectionGoal,
983 times: isize,
984 text_layout_details: &TextLayoutDetails,
985) -> (DisplayPoint, SelectionGoal) {
986 let start = map.display_point_to_fold_point(point, Bias::Left);
987 let begin_folded_line = map.fold_point_to_display_point(
988 map.fold_snapshot
989 .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
990 );
991 let select_nth_wrapped_row = point.row() - begin_folded_line.row();
992
993 let (goal_wrap, goal_x) = match goal {
994 SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
995 SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
996 SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
997 _ => {
998 let x = map.x_for_display_point(point, text_layout_details);
999 goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x.0));
1000 (select_nth_wrapped_row, x.0)
1001 }
1002 };
1003
1004 let target = start.row() as isize + times;
1005 let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
1006
1007 let mut begin_folded_line = map.fold_point_to_display_point(
1008 map.fold_snapshot
1009 .clip_point(FoldPoint::new(new_row, 0), Bias::Left),
1010 );
1011
1012 let mut i = 0;
1013 while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
1014 let next_folded_line = DisplayPoint::new(begin_folded_line.row() + 1, 0);
1015 if map
1016 .display_point_to_fold_point(next_folded_line, Bias::Right)
1017 .row()
1018 == new_row
1019 {
1020 i += 1;
1021 begin_folded_line = next_folded_line;
1022 } else {
1023 break;
1024 }
1025 }
1026
1027 let new_col = if i == goal_wrap {
1028 map.display_column_for_x(begin_folded_line.row(), px(goal_x), text_layout_details)
1029 } else {
1030 map.line_len(begin_folded_line.row())
1031 };
1032
1033 (
1034 map.clip_point(
1035 DisplayPoint::new(begin_folded_line.row(), new_col),
1036 Bias::Left,
1037 ),
1038 goal,
1039 )
1040}
1041
1042fn down_display(
1043 map: &DisplaySnapshot,
1044 mut point: DisplayPoint,
1045 mut goal: SelectionGoal,
1046 times: usize,
1047 text_layout_details: &TextLayoutDetails,
1048) -> (DisplayPoint, SelectionGoal) {
1049 for _ in 0..times {
1050 (point, goal) = movement::down(map, point, goal, true, text_layout_details);
1051 }
1052
1053 (point, goal)
1054}
1055
1056fn up_display(
1057 map: &DisplaySnapshot,
1058 mut point: DisplayPoint,
1059 mut goal: SelectionGoal,
1060 times: usize,
1061 text_layout_details: &TextLayoutDetails,
1062) -> (DisplayPoint, SelectionGoal) {
1063 for _ in 0..times {
1064 (point, goal) = movement::up(map, point, goal, true, &text_layout_details);
1065 }
1066
1067 (point, goal)
1068}
1069
1070pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
1071 for _ in 0..times {
1072 let new_point = movement::saturating_right(map, point);
1073 if point == new_point {
1074 break;
1075 }
1076 point = new_point;
1077 }
1078 point
1079}
1080
1081pub(crate) fn next_char(
1082 map: &DisplaySnapshot,
1083 point: DisplayPoint,
1084 allow_cross_newline: bool,
1085) -> DisplayPoint {
1086 let mut new_point = point;
1087 let mut max_column = map.line_len(new_point.row());
1088 if !allow_cross_newline {
1089 max_column -= 1;
1090 }
1091 if new_point.column() < max_column {
1092 *new_point.column_mut() += 1;
1093 } else if new_point < map.max_point() && allow_cross_newline {
1094 *new_point.row_mut() += 1;
1095 *new_point.column_mut() = 0;
1096 }
1097 new_point
1098}
1099
1100pub(crate) fn next_word_start(
1101 map: &DisplaySnapshot,
1102 mut point: DisplayPoint,
1103 ignore_punctuation: bool,
1104 times: usize,
1105) -> DisplayPoint {
1106 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1107 for _ in 0..times {
1108 let mut crossed_newline = false;
1109 let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1110 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1111 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1112 let at_newline = right == '\n';
1113
1114 let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
1115 || at_newline && crossed_newline
1116 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1117
1118 crossed_newline |= at_newline;
1119 found
1120 });
1121 if point == new_point {
1122 break;
1123 }
1124 point = new_point;
1125 }
1126 point
1127}
1128
1129pub(crate) fn next_word_end(
1130 map: &DisplaySnapshot,
1131 mut point: DisplayPoint,
1132 ignore_punctuation: bool,
1133 times: usize,
1134 allow_cross_newline: bool,
1135) -> DisplayPoint {
1136 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1137 for _ in 0..times {
1138 let new_point = next_char(map, point, allow_cross_newline);
1139 let mut need_next_char = false;
1140 let new_point = movement::find_boundary_exclusive(
1141 map,
1142 new_point,
1143 FindRange::MultiLine,
1144 |left, right| {
1145 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1146 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1147 let at_newline = right == '\n';
1148
1149 if !allow_cross_newline && at_newline {
1150 need_next_char = true;
1151 return true;
1152 }
1153
1154 left_kind != right_kind && left_kind != CharKind::Whitespace
1155 },
1156 );
1157 let new_point = if need_next_char {
1158 next_char(map, new_point, true)
1159 } else {
1160 new_point
1161 };
1162 let new_point = map.clip_point(new_point, Bias::Left);
1163 if point == new_point {
1164 break;
1165 }
1166 point = new_point;
1167 }
1168 point
1169}
1170
1171fn previous_word_start(
1172 map: &DisplaySnapshot,
1173 mut point: DisplayPoint,
1174 ignore_punctuation: bool,
1175 times: usize,
1176) -> DisplayPoint {
1177 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1178 for _ in 0..times {
1179 // This works even though find_preceding_boundary is called for every character in the line containing
1180 // cursor because the newline is checked only once.
1181 let new_point = movement::find_preceding_boundary_display_point(
1182 map,
1183 point,
1184 FindRange::MultiLine,
1185 |left, right| {
1186 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1187 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1188
1189 (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
1190 },
1191 );
1192 if point == new_point {
1193 break;
1194 }
1195 point = new_point;
1196 }
1197 point
1198}
1199
1200fn previous_word_end(
1201 map: &DisplaySnapshot,
1202 point: DisplayPoint,
1203 ignore_punctuation: bool,
1204 times: usize,
1205) -> DisplayPoint {
1206 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1207 let mut point = point.to_point(map);
1208
1209 if point.column < map.buffer_snapshot.line_len(point.row) {
1210 point.column += 1;
1211 }
1212 for _ in 0..times {
1213 let new_point = movement::find_preceding_boundary_point(
1214 &map.buffer_snapshot,
1215 point,
1216 FindRange::MultiLine,
1217 |left, right| {
1218 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1219 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1220 match (left_kind, right_kind) {
1221 (CharKind::Punctuation, CharKind::Whitespace)
1222 | (CharKind::Punctuation, CharKind::Word)
1223 | (CharKind::Word, CharKind::Whitespace)
1224 | (CharKind::Word, CharKind::Punctuation) => true,
1225 (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1226 _ => false,
1227 }
1228 },
1229 );
1230 if new_point == point {
1231 break;
1232 }
1233 point = new_point;
1234 }
1235 movement::saturating_left(map, point.to_display_point(map))
1236}
1237
1238fn next_subword_start(
1239 map: &DisplaySnapshot,
1240 mut point: DisplayPoint,
1241 ignore_punctuation: bool,
1242 times: usize,
1243) -> DisplayPoint {
1244 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1245 for _ in 0..times {
1246 let mut crossed_newline = false;
1247 let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1248 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1249 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1250 let at_newline = right == '\n';
1251
1252 let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1253 let is_subword_start =
1254 left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1255
1256 let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1257 || at_newline && crossed_newline
1258 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1259
1260 crossed_newline |= at_newline;
1261 found
1262 });
1263 if point == new_point {
1264 break;
1265 }
1266 point = new_point;
1267 }
1268 point
1269}
1270
1271pub(crate) fn next_subword_end(
1272 map: &DisplaySnapshot,
1273 mut point: DisplayPoint,
1274 ignore_punctuation: bool,
1275 times: usize,
1276 allow_cross_newline: bool,
1277) -> DisplayPoint {
1278 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1279 for _ in 0..times {
1280 let new_point = next_char(map, point, allow_cross_newline);
1281
1282 let mut crossed_newline = false;
1283 let mut need_backtrack = false;
1284 let new_point =
1285 movement::find_boundary(map, new_point, FindRange::MultiLine, |left, right| {
1286 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1287 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1288 let at_newline = right == '\n';
1289
1290 if !allow_cross_newline && at_newline {
1291 return true;
1292 }
1293
1294 let is_word_end = (left_kind != right_kind) && !right.is_alphanumeric();
1295 let is_subword_end =
1296 left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1297
1298 let found = !left.is_whitespace() && !at_newline && (is_word_end || is_subword_end);
1299
1300 if found && (is_word_end || is_subword_end) {
1301 need_backtrack = true;
1302 }
1303
1304 crossed_newline |= at_newline;
1305 found
1306 });
1307 let mut new_point = map.clip_point(new_point, Bias::Left);
1308 if need_backtrack {
1309 *new_point.column_mut() -= 1;
1310 }
1311 if point == new_point {
1312 break;
1313 }
1314 point = new_point;
1315 }
1316 point
1317}
1318
1319fn previous_subword_start(
1320 map: &DisplaySnapshot,
1321 mut point: DisplayPoint,
1322 ignore_punctuation: bool,
1323 times: usize,
1324) -> DisplayPoint {
1325 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1326 for _ in 0..times {
1327 let mut crossed_newline = false;
1328 // This works even though find_preceding_boundary is called for every character in the line containing
1329 // cursor because the newline is checked only once.
1330 let new_point = movement::find_preceding_boundary_display_point(
1331 map,
1332 point,
1333 FindRange::MultiLine,
1334 |left, right| {
1335 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1336 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1337 let at_newline = right == '\n';
1338
1339 let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1340 let is_subword_start =
1341 left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1342
1343 let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1344 || at_newline && crossed_newline
1345 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1346
1347 crossed_newline |= at_newline;
1348
1349 found
1350 },
1351 );
1352 if point == new_point {
1353 break;
1354 }
1355 point = new_point;
1356 }
1357 point
1358}
1359
1360fn previous_subword_end(
1361 map: &DisplaySnapshot,
1362 point: DisplayPoint,
1363 ignore_punctuation: bool,
1364 times: usize,
1365) -> DisplayPoint {
1366 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1367 let mut point = point.to_point(map);
1368
1369 if point.column < map.buffer_snapshot.line_len(point.row) {
1370 point.column += 1;
1371 }
1372 for _ in 0..times {
1373 let new_point = movement::find_preceding_boundary_point(
1374 &map.buffer_snapshot,
1375 point,
1376 FindRange::MultiLine,
1377 |left, right| {
1378 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1379 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1380
1381 let is_subword_end =
1382 left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1383
1384 if is_subword_end {
1385 return true;
1386 }
1387
1388 match (left_kind, right_kind) {
1389 (CharKind::Word, CharKind::Whitespace)
1390 | (CharKind::Word, CharKind::Punctuation) => true,
1391 (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1392 _ => false,
1393 }
1394 },
1395 );
1396 if new_point == point {
1397 break;
1398 }
1399 point = new_point;
1400 }
1401 movement::saturating_left(map, point.to_display_point(map))
1402}
1403
1404pub(crate) fn first_non_whitespace(
1405 map: &DisplaySnapshot,
1406 display_lines: bool,
1407 from: DisplayPoint,
1408) -> DisplayPoint {
1409 let mut start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left);
1410 let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
1411 for (ch, offset) in map.buffer_chars_at(start_offset) {
1412 if ch == '\n' {
1413 return from;
1414 }
1415
1416 start_offset = offset;
1417
1418 if char_kind(&scope, ch) != CharKind::Whitespace {
1419 break;
1420 }
1421 }
1422
1423 start_offset.to_display_point(map)
1424}
1425
1426pub(crate) fn last_non_whitespace(
1427 map: &DisplaySnapshot,
1428 from: DisplayPoint,
1429 count: usize,
1430) -> DisplayPoint {
1431 let mut end_of_line = end_of_line(map, false, from, count).to_offset(map, Bias::Left);
1432 let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
1433 for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) {
1434 if ch == '\n' {
1435 break;
1436 }
1437 end_of_line = offset;
1438 if char_kind(&scope, ch) != CharKind::Whitespace || ch == '\n' {
1439 break;
1440 }
1441 }
1442
1443 end_of_line.to_display_point(map)
1444}
1445
1446pub(crate) fn start_of_line(
1447 map: &DisplaySnapshot,
1448 display_lines: bool,
1449 point: DisplayPoint,
1450) -> DisplayPoint {
1451 if display_lines {
1452 map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
1453 } else {
1454 map.prev_line_boundary(point.to_point(map)).1
1455 }
1456}
1457
1458pub(crate) fn end_of_line(
1459 map: &DisplaySnapshot,
1460 display_lines: bool,
1461 mut point: DisplayPoint,
1462 times: usize,
1463) -> DisplayPoint {
1464 if times > 1 {
1465 point = start_of_relative_buffer_row(map, point, times as isize - 1);
1466 }
1467 if display_lines {
1468 map.clip_point(
1469 DisplayPoint::new(point.row(), map.line_len(point.row())),
1470 Bias::Left,
1471 )
1472 } else {
1473 map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
1474 }
1475}
1476
1477fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
1478 let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
1479 *new_point.column_mut() = point.column();
1480 map.clip_point(new_point, Bias::Left)
1481}
1482
1483fn end_of_document(
1484 map: &DisplaySnapshot,
1485 point: DisplayPoint,
1486 line: Option<usize>,
1487) -> DisplayPoint {
1488 let new_row = if let Some(line) = line {
1489 (line - 1) as u32
1490 } else {
1491 map.max_buffer_row()
1492 };
1493
1494 let new_point = Point::new(new_row, point.column());
1495 map.clip_point(new_point.to_display_point(map), Bias::Left)
1496}
1497
1498fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
1499 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
1500 let display_point = map.clip_at_line_end(display_point);
1501 let point = display_point.to_point(map);
1502 let offset = point.to_offset(&map.buffer_snapshot);
1503
1504 // Ensure the range is contained by the current line.
1505 let mut line_end = map.next_line_boundary(point).0;
1506 if line_end == point {
1507 line_end = map.max_point().to_point(map);
1508 }
1509
1510 let line_range = map.prev_line_boundary(point).0..line_end;
1511 let visible_line_range =
1512 line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
1513 let ranges = map
1514 .buffer_snapshot
1515 .bracket_ranges(visible_line_range.clone());
1516 if let Some(ranges) = ranges {
1517 let line_range = line_range.start.to_offset(&map.buffer_snapshot)
1518 ..line_range.end.to_offset(&map.buffer_snapshot);
1519 let mut closest_pair_destination = None;
1520 let mut closest_distance = usize::MAX;
1521
1522 for (open_range, close_range) in ranges {
1523 if open_range.start >= offset && line_range.contains(&open_range.start) {
1524 let distance = open_range.start - offset;
1525 if distance < closest_distance {
1526 closest_pair_destination = Some(close_range.start);
1527 closest_distance = distance;
1528 continue;
1529 }
1530 }
1531
1532 if close_range.start >= offset && line_range.contains(&close_range.start) {
1533 let distance = close_range.start - offset;
1534 if distance < closest_distance {
1535 closest_pair_destination = Some(open_range.start);
1536 closest_distance = distance;
1537 continue;
1538 }
1539 }
1540
1541 continue;
1542 }
1543
1544 closest_pair_destination
1545 .map(|destination| destination.to_display_point(map))
1546 .unwrap_or(display_point)
1547 } else {
1548 display_point
1549 }
1550}
1551
1552fn find_forward(
1553 map: &DisplaySnapshot,
1554 from: DisplayPoint,
1555 before: bool,
1556 target: char,
1557 times: usize,
1558 mode: FindRange,
1559 smartcase: bool,
1560) -> Option<DisplayPoint> {
1561 let mut to = from;
1562 let mut found = false;
1563
1564 for _ in 0..times {
1565 found = false;
1566 let new_to = find_boundary(map, to, mode, |_, right| {
1567 found = is_character_match(target, right, smartcase);
1568 found
1569 });
1570 if to == new_to {
1571 break;
1572 }
1573 to = new_to;
1574 }
1575
1576 if found {
1577 if before && to.column() > 0 {
1578 *to.column_mut() -= 1;
1579 Some(map.clip_point(to, Bias::Left))
1580 } else {
1581 Some(to)
1582 }
1583 } else {
1584 None
1585 }
1586}
1587
1588fn find_backward(
1589 map: &DisplaySnapshot,
1590 from: DisplayPoint,
1591 after: bool,
1592 target: char,
1593 times: usize,
1594 mode: FindRange,
1595 smartcase: bool,
1596) -> DisplayPoint {
1597 let mut to = from;
1598
1599 for _ in 0..times {
1600 let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
1601 is_character_match(target, right, smartcase)
1602 });
1603 if to == new_to {
1604 break;
1605 }
1606 to = new_to;
1607 }
1608
1609 let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
1610 if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
1611 if after {
1612 *to.column_mut() += 1;
1613 map.clip_point(to, Bias::Right)
1614 } else {
1615 to
1616 }
1617 } else {
1618 from
1619 }
1620}
1621
1622fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
1623 if smartcase {
1624 if target.is_uppercase() {
1625 target == other
1626 } else {
1627 target == other.to_ascii_lowercase()
1628 }
1629 } else {
1630 target == other
1631 }
1632}
1633
1634fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1635 let correct_line = start_of_relative_buffer_row(map, point, times as isize);
1636 first_non_whitespace(map, false, correct_line)
1637}
1638
1639fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1640 let correct_line = start_of_relative_buffer_row(map, point, 0);
1641 right(map, correct_line, times.saturating_sub(1))
1642}
1643
1644pub(crate) fn next_line_end(
1645 map: &DisplaySnapshot,
1646 mut point: DisplayPoint,
1647 times: usize,
1648) -> DisplayPoint {
1649 if times > 1 {
1650 point = start_of_relative_buffer_row(map, point, times as isize - 1);
1651 }
1652 end_of_line(map, false, point, 1)
1653}
1654
1655fn window_top(
1656 map: &DisplaySnapshot,
1657 point: DisplayPoint,
1658 text_layout_details: &TextLayoutDetails,
1659 mut times: usize,
1660) -> (DisplayPoint, SelectionGoal) {
1661 let first_visible_line = text_layout_details
1662 .scroll_anchor
1663 .anchor
1664 .to_display_point(map);
1665
1666 if first_visible_line.row() != 0 && text_layout_details.vertical_scroll_margin as usize > times
1667 {
1668 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1669 }
1670
1671 if let Some(visible_rows) = text_layout_details.visible_rows {
1672 let bottom_row = first_visible_line.row() + visible_rows as u32;
1673 let new_row = (first_visible_line.row() + (times as u32))
1674 .min(bottom_row)
1675 .min(map.max_point().row());
1676 let new_col = point.column().min(map.line_len(first_visible_line.row()));
1677
1678 let new_point = DisplayPoint::new(new_row, new_col);
1679 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1680 } else {
1681 let new_row = (first_visible_line.row() + (times as u32)).min(map.max_point().row());
1682 let new_col = point.column().min(map.line_len(first_visible_line.row()));
1683
1684 let new_point = DisplayPoint::new(new_row, new_col);
1685 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1686 }
1687}
1688
1689fn window_middle(
1690 map: &DisplaySnapshot,
1691 point: DisplayPoint,
1692 text_layout_details: &TextLayoutDetails,
1693) -> (DisplayPoint, SelectionGoal) {
1694 if let Some(visible_rows) = text_layout_details.visible_rows {
1695 let first_visible_line = text_layout_details
1696 .scroll_anchor
1697 .anchor
1698 .to_display_point(map);
1699
1700 let max_visible_rows =
1701 (visible_rows as u32).min(map.max_point().row() - first_visible_line.row());
1702
1703 let new_row =
1704 (first_visible_line.row() + (max_visible_rows / 2)).min(map.max_point().row());
1705 let new_col = point.column().min(map.line_len(new_row));
1706 let new_point = DisplayPoint::new(new_row, new_col);
1707 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1708 } else {
1709 (point, SelectionGoal::None)
1710 }
1711}
1712
1713fn window_bottom(
1714 map: &DisplaySnapshot,
1715 point: DisplayPoint,
1716 text_layout_details: &TextLayoutDetails,
1717 mut times: usize,
1718) -> (DisplayPoint, SelectionGoal) {
1719 if let Some(visible_rows) = text_layout_details.visible_rows {
1720 let first_visible_line = text_layout_details
1721 .scroll_anchor
1722 .anchor
1723 .to_display_point(map);
1724 let bottom_row = first_visible_line.row()
1725 + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
1726 if bottom_row < map.max_point().row()
1727 && text_layout_details.vertical_scroll_margin as usize > times
1728 {
1729 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1730 }
1731 let bottom_row_capped = bottom_row.min(map.max_point().row());
1732 let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row() {
1733 first_visible_line.row()
1734 } else {
1735 bottom_row_capped.saturating_sub(times as u32)
1736 };
1737 let new_col = point.column().min(map.line_len(new_row));
1738 let new_point = DisplayPoint::new(new_row, new_col);
1739 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1740 } else {
1741 (point, SelectionGoal::None)
1742 }
1743}
1744
1745#[cfg(test)]
1746mod test {
1747
1748 use crate::test::NeovimBackedTestContext;
1749 use indoc::indoc;
1750
1751 #[gpui::test]
1752 async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
1753 let mut cx = NeovimBackedTestContext::new(cx).await;
1754
1755 let initial_state = indoc! {r"ˇabc
1756 def
1757
1758 paragraph
1759 the second
1760
1761
1762
1763 third and
1764 final"};
1765
1766 // goes down once
1767 cx.set_shared_state(initial_state).await;
1768 cx.simulate_shared_keystrokes(["}"]).await;
1769 cx.assert_shared_state(indoc! {r"abc
1770 def
1771 ˇ
1772 paragraph
1773 the second
1774
1775
1776
1777 third and
1778 final"})
1779 .await;
1780
1781 // goes up once
1782 cx.simulate_shared_keystrokes(["{"]).await;
1783 cx.assert_shared_state(initial_state).await;
1784
1785 // goes down twice
1786 cx.simulate_shared_keystrokes(["2", "}"]).await;
1787 cx.assert_shared_state(indoc! {r"abc
1788 def
1789
1790 paragraph
1791 the second
1792 ˇ
1793
1794
1795 third and
1796 final"})
1797 .await;
1798
1799 // goes down over multiple blanks
1800 cx.simulate_shared_keystrokes(["}"]).await;
1801 cx.assert_shared_state(indoc! {r"abc
1802 def
1803
1804 paragraph
1805 the second
1806
1807
1808
1809 third and
1810 finaˇl"})
1811 .await;
1812
1813 // goes up twice
1814 cx.simulate_shared_keystrokes(["2", "{"]).await;
1815 cx.assert_shared_state(indoc! {r"abc
1816 def
1817 ˇ
1818 paragraph
1819 the second
1820
1821
1822
1823 third and
1824 final"})
1825 .await
1826 }
1827
1828 #[gpui::test]
1829 async fn test_matching(cx: &mut gpui::TestAppContext) {
1830 let mut cx = NeovimBackedTestContext::new(cx).await;
1831
1832 cx.set_shared_state(indoc! {r"func ˇ(a string) {
1833 do(something(with<Types>.and_arrays[0, 2]))
1834 }"})
1835 .await;
1836 cx.simulate_shared_keystrokes(["%"]).await;
1837 cx.assert_shared_state(indoc! {r"func (a stringˇ) {
1838 do(something(with<Types>.and_arrays[0, 2]))
1839 }"})
1840 .await;
1841
1842 // test it works on the last character of the line
1843 cx.set_shared_state(indoc! {r"func (a string) ˇ{
1844 do(something(with<Types>.and_arrays[0, 2]))
1845 }"})
1846 .await;
1847 cx.simulate_shared_keystrokes(["%"]).await;
1848 cx.assert_shared_state(indoc! {r"func (a string) {
1849 do(something(with<Types>.and_arrays[0, 2]))
1850 ˇ}"})
1851 .await;
1852
1853 // test it works on immediate nesting
1854 cx.set_shared_state("ˇ{()}").await;
1855 cx.simulate_shared_keystrokes(["%"]).await;
1856 cx.assert_shared_state("{()ˇ}").await;
1857 cx.simulate_shared_keystrokes(["%"]).await;
1858 cx.assert_shared_state("ˇ{()}").await;
1859
1860 // test it works on immediate nesting inside braces
1861 cx.set_shared_state("{\n ˇ{()}\n}").await;
1862 cx.simulate_shared_keystrokes(["%"]).await;
1863 cx.assert_shared_state("{\n {()ˇ}\n}").await;
1864
1865 // test it jumps to the next paren on a line
1866 cx.set_shared_state("func ˇboop() {\n}").await;
1867 cx.simulate_shared_keystrokes(["%"]).await;
1868 cx.assert_shared_state("func boop(ˇ) {\n}").await;
1869 }
1870
1871 #[gpui::test]
1872 async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
1873 let mut cx = NeovimBackedTestContext::new(cx).await;
1874
1875 // f and F
1876 cx.set_shared_state("ˇone two three four").await;
1877 cx.simulate_shared_keystrokes(["f", "o"]).await;
1878 cx.assert_shared_state("one twˇo three four").await;
1879 cx.simulate_shared_keystrokes([","]).await;
1880 cx.assert_shared_state("ˇone two three four").await;
1881 cx.simulate_shared_keystrokes(["2", ";"]).await;
1882 cx.assert_shared_state("one two three fˇour").await;
1883 cx.simulate_shared_keystrokes(["shift-f", "e"]).await;
1884 cx.assert_shared_state("one two threˇe four").await;
1885 cx.simulate_shared_keystrokes(["2", ";"]).await;
1886 cx.assert_shared_state("onˇe two three four").await;
1887 cx.simulate_shared_keystrokes([","]).await;
1888 cx.assert_shared_state("one two thrˇee four").await;
1889
1890 // t and T
1891 cx.set_shared_state("ˇone two three four").await;
1892 cx.simulate_shared_keystrokes(["t", "o"]).await;
1893 cx.assert_shared_state("one tˇwo three four").await;
1894 cx.simulate_shared_keystrokes([","]).await;
1895 cx.assert_shared_state("oˇne two three four").await;
1896 cx.simulate_shared_keystrokes(["2", ";"]).await;
1897 cx.assert_shared_state("one two three ˇfour").await;
1898 cx.simulate_shared_keystrokes(["shift-t", "e"]).await;
1899 cx.assert_shared_state("one two threeˇ four").await;
1900 cx.simulate_shared_keystrokes(["3", ";"]).await;
1901 cx.assert_shared_state("oneˇ two three four").await;
1902 cx.simulate_shared_keystrokes([","]).await;
1903 cx.assert_shared_state("one two thˇree four").await;
1904 }
1905
1906 #[gpui::test]
1907 async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
1908 let mut cx = NeovimBackedTestContext::new(cx).await;
1909 let initial_state = indoc! {r"something(ˇfoo)"};
1910 cx.set_shared_state(initial_state).await;
1911 cx.simulate_shared_keystrokes(["}"]).await;
1912 cx.assert_shared_state(indoc! {r"something(fooˇ)"}).await;
1913 }
1914
1915 #[gpui::test]
1916 async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
1917 let mut cx = NeovimBackedTestContext::new(cx).await;
1918 cx.set_shared_state("ˇone\n two\nthree").await;
1919 cx.simulate_shared_keystrokes(["enter"]).await;
1920 cx.assert_shared_state("one\n ˇtwo\nthree").await;
1921 }
1922
1923 #[gpui::test]
1924 async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
1925 let mut cx = NeovimBackedTestContext::new(cx).await;
1926 cx.set_shared_state("ˇ one \n two \nthree").await;
1927 cx.simulate_shared_keystrokes(["g", "_"]).await;
1928 cx.assert_shared_state(" onˇe \n two \nthree").await;
1929 cx.simulate_shared_keystrokes(["2", "g", "_"]).await;
1930 cx.assert_shared_state(" one \n twˇo \nthree").await;
1931 }
1932
1933 #[gpui::test]
1934 async fn test_window_top(cx: &mut gpui::TestAppContext) {
1935 let mut cx = NeovimBackedTestContext::new(cx).await;
1936 let initial_state = indoc! {r"abc
1937 def
1938 paragraph
1939 the second
1940 third ˇand
1941 final"};
1942
1943 cx.set_shared_state(initial_state).await;
1944 cx.simulate_shared_keystrokes(["shift-h"]).await;
1945 cx.assert_shared_state(indoc! {r"abˇc
1946 def
1947 paragraph
1948 the second
1949 third and
1950 final"})
1951 .await;
1952
1953 // clip point
1954 cx.set_shared_state(indoc! {r"
1955 1 2 3
1956 4 5 6
1957 7 8 ˇ9
1958 "})
1959 .await;
1960 cx.simulate_shared_keystrokes(["shift-h"]).await;
1961 cx.assert_shared_state(indoc! {r"
1962 1 2 ˇ3
1963 4 5 6
1964 7 8 9
1965 "})
1966 .await;
1967
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 .await;
1987 cx.simulate_shared_keystrokes(["9", "shift-h"]).await;
1988 cx.assert_shared_state(indoc! {r"
1989 1 2 3
1990 4 5 6
1991 7 8 ˇ9"})
1992 .await;
1993 }
1994
1995 #[gpui::test]
1996 async fn test_window_middle(cx: &mut gpui::TestAppContext) {
1997 let mut cx = NeovimBackedTestContext::new(cx).await;
1998 let initial_state = indoc! {r"abˇc
1999 def
2000 paragraph
2001 the second
2002 third and
2003 final"};
2004
2005 cx.set_shared_state(initial_state).await;
2006 cx.simulate_shared_keystrokes(["shift-m"]).await;
2007 cx.assert_shared_state(indoc! {r"abc
2008 def
2009 paˇragraph
2010 the second
2011 third and
2012 final"})
2013 .await;
2014
2015 cx.set_shared_state(indoc! {r"
2016 1 2 3
2017 4 5 6
2018 7 8 ˇ9
2019 "})
2020 .await;
2021 cx.simulate_shared_keystrokes(["shift-m"]).await;
2022 cx.assert_shared_state(indoc! {r"
2023 1 2 3
2024 4 5 ˇ6
2025 7 8 9
2026 "})
2027 .await;
2028 cx.set_shared_state(indoc! {r"
2029 1 2 3
2030 4 5 6
2031 ˇ7 8 9
2032 "})
2033 .await;
2034 cx.simulate_shared_keystrokes(["shift-m"]).await;
2035 cx.assert_shared_state(indoc! {r"
2036 1 2 3
2037 ˇ4 5 6
2038 7 8 9
2039 "})
2040 .await;
2041 cx.set_shared_state(indoc! {r"
2042 ˇ1 2 3
2043 4 5 6
2044 7 8 9
2045 "})
2046 .await;
2047 cx.simulate_shared_keystrokes(["shift-m"]).await;
2048 cx.assert_shared_state(indoc! {r"
2049 1 2 3
2050 ˇ4 5 6
2051 7 8 9
2052 "})
2053 .await;
2054 cx.set_shared_state(indoc! {r"
2055 1 2 3
2056 ˇ4 5 6
2057 7 8 9
2058 "})
2059 .await;
2060 cx.simulate_shared_keystrokes(["shift-m"]).await;
2061 cx.assert_shared_state(indoc! {r"
2062 1 2 3
2063 ˇ4 5 6
2064 7 8 9
2065 "})
2066 .await;
2067 cx.set_shared_state(indoc! {r"
2068 1 2 3
2069 4 5 ˇ6
2070 7 8 9
2071 "})
2072 .await;
2073 cx.simulate_shared_keystrokes(["shift-m"]).await;
2074 cx.assert_shared_state(indoc! {r"
2075 1 2 3
2076 4 5 ˇ6
2077 7 8 9
2078 "})
2079 .await;
2080 }
2081
2082 #[gpui::test]
2083 async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
2084 let mut cx = NeovimBackedTestContext::new(cx).await;
2085 let initial_state = indoc! {r"abc
2086 deˇf
2087 paragraph
2088 the second
2089 third and
2090 final"};
2091
2092 cx.set_shared_state(initial_state).await;
2093 cx.simulate_shared_keystrokes(["shift-l"]).await;
2094 cx.assert_shared_state(indoc! {r"abc
2095 def
2096 paragraph
2097 the second
2098 third and
2099 fiˇnal"})
2100 .await;
2101
2102 cx.set_shared_state(indoc! {r"
2103 1 2 3
2104 4 5 ˇ6
2105 7 8 9
2106 "})
2107 .await;
2108 cx.simulate_shared_keystrokes(["shift-l"]).await;
2109 cx.assert_shared_state(indoc! {r"
2110 1 2 3
2111 4 5 6
2112 7 8 9
2113 ˇ"})
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(["9", "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
2173 #[gpui::test]
2174 async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
2175 let mut cx = NeovimBackedTestContext::new(cx).await;
2176 cx.set_shared_state(indoc! {r"
2177 456 5ˇ67 678
2178 "})
2179 .await;
2180 cx.simulate_shared_keystrokes(["g", "e"]).await;
2181 cx.assert_shared_state(indoc! {r"
2182 45ˇ6 567 678
2183 "})
2184 .await;
2185
2186 // Test times
2187 cx.set_shared_state(indoc! {r"
2188 123 234 345
2189 456 5ˇ67 678
2190 "})
2191 .await;
2192 cx.simulate_shared_keystrokes(["4", "g", "e"]).await;
2193 cx.assert_shared_state(indoc! {r"
2194 12ˇ3 234 345
2195 456 567 678
2196 "})
2197 .await;
2198
2199 // With punctuation
2200 cx.set_shared_state(indoc! {r"
2201 123 234 345
2202 4;5.6 5ˇ67 678
2203 789 890 901
2204 "})
2205 .await;
2206 cx.simulate_shared_keystrokes(["g", "e"]).await;
2207 cx.assert_shared_state(indoc! {r"
2208 123 234 345
2209 4;5.ˇ6 567 678
2210 789 890 901
2211 "})
2212 .await;
2213
2214 // With punctuation and count
2215 cx.set_shared_state(indoc! {r"
2216 123 234 345
2217 4;5.6 5ˇ67 678
2218 789 890 901
2219 "})
2220 .await;
2221 cx.simulate_shared_keystrokes(["5", "g", "e"]).await;
2222 cx.assert_shared_state(indoc! {r"
2223 123 234 345
2224 ˇ4;5.6 567 678
2225 789 890 901
2226 "})
2227 .await;
2228
2229 // newlines
2230 cx.set_shared_state(indoc! {r"
2231 123 234 345
2232
2233 78ˇ9 890 901
2234 "})
2235 .await;
2236 cx.simulate_shared_keystrokes(["g", "e"]).await;
2237 cx.assert_shared_state(indoc! {r"
2238 123 234 345
2239 ˇ
2240 789 890 901
2241 "})
2242 .await;
2243 cx.simulate_shared_keystrokes(["g", "e"]).await;
2244 cx.assert_shared_state(indoc! {r"
2245 123 234 34ˇ5
2246
2247 789 890 901
2248 "})
2249 .await;
2250
2251 // With punctuation
2252 cx.set_shared_state(indoc! {r"
2253 123 234 345
2254 4;5.ˇ6 567 678
2255 789 890 901
2256 "})
2257 .await;
2258 cx.simulate_shared_keystrokes(["g", "shift-e"]).await;
2259 cx.assert_shared_state(indoc! {r"
2260 123 234 34ˇ5
2261 4;5.6 567 678
2262 789 890 901
2263 "})
2264 .await;
2265 }
2266
2267 #[gpui::test]
2268 async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
2269 let mut cx = NeovimBackedTestContext::new(cx).await;
2270
2271 cx.set_shared_state(indoc! {"
2272 fn aˇ() {
2273 return
2274 }
2275 "})
2276 .await;
2277 cx.simulate_shared_keystrokes(["v", "$", "%"]).await;
2278 cx.assert_shared_state(indoc! {"
2279 fn a«() {
2280 return
2281 }ˇ»
2282 "})
2283 .await;
2284 }
2285}