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