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