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