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