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