1use editor::{
2 display_map::{DisplaySnapshot, FoldPoint, ToDisplayPoint},
3 movement::{
4 self, find_boundary, find_preceding_boundary_display_point, FindRange, TextLayoutDetails,
5 },
6 Bias, DisplayPoint, ToOffset,
7};
8use gpui::{actions, impl_actions, px, ViewContext, WindowContext};
9use language::{char_kind, CharKind, Point, Selection, SelectionGoal};
10use serde::Deserialize;
11use workspace::Workspace;
12
13use crate::{
14 normal::normal_motion,
15 state::{Mode, Operator},
16 utils::coerce_punctuation,
17 visual::visual_motion,
18 Vim,
19};
20
21#[derive(Clone, Debug, PartialEq, Eq)]
22pub enum Motion {
23 Left,
24 Backspace,
25 Down {
26 display_lines: bool,
27 },
28 Up {
29 display_lines: bool,
30 },
31 Right,
32 Space,
33 NextWordStart {
34 ignore_punctuation: bool,
35 },
36 NextWordEnd {
37 ignore_punctuation: bool,
38 },
39 PreviousWordStart {
40 ignore_punctuation: bool,
41 },
42 PreviousWordEnd {
43 ignore_punctuation: bool,
44 },
45 FirstNonWhitespace {
46 display_lines: bool,
47 },
48 CurrentLine,
49 StartOfLine {
50 display_lines: bool,
51 },
52 EndOfLine {
53 display_lines: bool,
54 },
55 StartOfParagraph,
56 EndOfParagraph,
57 StartOfDocument,
58 EndOfDocument,
59 Matching,
60 FindForward {
61 before: bool,
62 char: char,
63 mode: FindRange,
64 },
65 FindBackward {
66 after: bool,
67 char: char,
68 mode: FindRange,
69 },
70 RepeatFind {
71 last_find: Box<Motion>,
72 },
73 RepeatFindReversed {
74 last_find: Box<Motion>,
75 },
76 NextLineStart,
77 StartOfLineDownward,
78 EndOfLineDownward,
79 GoToColumn,
80 WindowTop,
81 WindowMiddle,
82 WindowBottom,
83}
84
85#[derive(Clone, Deserialize, PartialEq)]
86#[serde(rename_all = "camelCase")]
87struct NextWordStart {
88 #[serde(default)]
89 ignore_punctuation: bool,
90}
91
92#[derive(Clone, Deserialize, PartialEq)]
93#[serde(rename_all = "camelCase")]
94struct NextWordEnd {
95 #[serde(default)]
96 ignore_punctuation: bool,
97}
98
99#[derive(Clone, Deserialize, PartialEq)]
100#[serde(rename_all = "camelCase")]
101struct PreviousWordStart {
102 #[serde(default)]
103 ignore_punctuation: bool,
104}
105
106#[derive(Clone, Deserialize, PartialEq)]
107#[serde(rename_all = "camelCase")]
108struct PreviousWordEnd {
109 #[serde(default)]
110 ignore_punctuation: bool,
111}
112
113#[derive(Clone, Deserialize, PartialEq)]
114#[serde(rename_all = "camelCase")]
115pub(crate) struct Up {
116 #[serde(default)]
117 pub(crate) display_lines: bool,
118}
119
120#[derive(Clone, Deserialize, PartialEq)]
121#[serde(rename_all = "camelCase")]
122pub(crate) struct Down {
123 #[serde(default)]
124 pub(crate) display_lines: bool,
125}
126
127#[derive(Clone, Deserialize, PartialEq)]
128#[serde(rename_all = "camelCase")]
129struct FirstNonWhitespace {
130 #[serde(default)]
131 display_lines: bool,
132}
133
134#[derive(Clone, Deserialize, PartialEq)]
135#[serde(rename_all = "camelCase")]
136struct EndOfLine {
137 #[serde(default)]
138 display_lines: bool,
139}
140
141#[derive(Clone, Deserialize, PartialEq)]
142#[serde(rename_all = "camelCase")]
143pub struct StartOfLine {
144 #[serde(default)]
145 pub(crate) display_lines: bool,
146}
147
148impl_actions!(
149 vim,
150 [
151 StartOfLine,
152 EndOfLine,
153 FirstNonWhitespace,
154 Down,
155 Up,
156 PreviousWordStart,
157 PreviousWordEnd,
158 NextWordEnd,
159 NextWordStart
160 ]
161);
162
163actions!(
164 vim,
165 [
166 Left,
167 Backspace,
168 Right,
169 Space,
170 CurrentLine,
171 StartOfParagraph,
172 EndOfParagraph,
173 StartOfDocument,
174 EndOfDocument,
175 Matching,
176 NextLineStart,
177 StartOfLineDownward,
178 EndOfLineDownward,
179 GoToColumn,
180 RepeatFind,
181 RepeatFindReversed,
182 WindowTop,
183 WindowMiddle,
184 WindowBottom,
185 ]
186);
187
188pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
189 workspace.register_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx));
190 workspace
191 .register_action(|_: &mut Workspace, _: &Backspace, cx: _| motion(Motion::Backspace, cx));
192 workspace.register_action(|_: &mut Workspace, action: &Down, cx: _| {
193 motion(
194 Motion::Down {
195 display_lines: action.display_lines,
196 },
197 cx,
198 )
199 });
200 workspace.register_action(|_: &mut Workspace, action: &Up, cx: _| {
201 motion(
202 Motion::Up {
203 display_lines: action.display_lines,
204 },
205 cx,
206 )
207 });
208 workspace.register_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx));
209 workspace.register_action(|_: &mut Workspace, _: &Space, cx: _| motion(Motion::Space, cx));
210 workspace.register_action(|_: &mut Workspace, action: &FirstNonWhitespace, cx: _| {
211 motion(
212 Motion::FirstNonWhitespace {
213 display_lines: action.display_lines,
214 },
215 cx,
216 )
217 });
218 workspace.register_action(|_: &mut Workspace, action: &StartOfLine, cx: _| {
219 motion(
220 Motion::StartOfLine {
221 display_lines: action.display_lines,
222 },
223 cx,
224 )
225 });
226 workspace.register_action(|_: &mut Workspace, action: &EndOfLine, cx: _| {
227 motion(
228 Motion::EndOfLine {
229 display_lines: action.display_lines,
230 },
231 cx,
232 )
233 });
234 workspace.register_action(|_: &mut Workspace, _: &CurrentLine, cx: _| {
235 motion(Motion::CurrentLine, cx)
236 });
237 workspace.register_action(|_: &mut Workspace, _: &StartOfParagraph, cx: _| {
238 motion(Motion::StartOfParagraph, cx)
239 });
240 workspace.register_action(|_: &mut Workspace, _: &EndOfParagraph, cx: _| {
241 motion(Motion::EndOfParagraph, cx)
242 });
243 workspace.register_action(|_: &mut Workspace, _: &StartOfDocument, cx: _| {
244 motion(Motion::StartOfDocument, cx)
245 });
246 workspace.register_action(|_: &mut Workspace, _: &EndOfDocument, cx: _| {
247 motion(Motion::EndOfDocument, cx)
248 });
249 workspace
250 .register_action(|_: &mut Workspace, _: &Matching, cx: _| motion(Motion::Matching, cx));
251
252 workspace.register_action(
253 |_: &mut Workspace, &NextWordStart { ignore_punctuation }: &NextWordStart, cx: _| {
254 motion(Motion::NextWordStart { ignore_punctuation }, cx)
255 },
256 );
257 workspace.register_action(
258 |_: &mut Workspace, &NextWordEnd { ignore_punctuation }: &NextWordEnd, cx: _| {
259 motion(Motion::NextWordEnd { ignore_punctuation }, cx)
260 },
261 );
262 workspace.register_action(
263 |_: &mut Workspace,
264 &PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
265 cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
266 );
267 workspace.register_action(|_: &mut Workspace, &NextLineStart, cx: _| {
268 motion(Motion::NextLineStart, cx)
269 });
270 workspace.register_action(|_: &mut Workspace, &StartOfLineDownward, cx: _| {
271 motion(Motion::StartOfLineDownward, cx)
272 });
273 workspace.register_action(|_: &mut Workspace, &EndOfLineDownward, cx: _| {
274 motion(Motion::EndOfLineDownward, cx)
275 });
276 workspace
277 .register_action(|_: &mut Workspace, &GoToColumn, cx: _| motion(Motion::GoToColumn, cx));
278
279 workspace.register_action(|_: &mut Workspace, _: &RepeatFind, cx: _| {
280 if let Some(last_find) = Vim::read(cx)
281 .workspace_state
282 .last_find
283 .clone()
284 .map(Box::new)
285 {
286 motion(Motion::RepeatFind { last_find }, cx);
287 }
288 });
289
290 workspace.register_action(|_: &mut Workspace, _: &RepeatFindReversed, cx: _| {
291 if let Some(last_find) = Vim::read(cx)
292 .workspace_state
293 .last_find
294 .clone()
295 .map(Box::new)
296 {
297 motion(Motion::RepeatFindReversed { last_find }, cx);
298 }
299 });
300 workspace.register_action(|_: &mut Workspace, &WindowTop, cx: _| motion(Motion::WindowTop, cx));
301 workspace.register_action(|_: &mut Workspace, &WindowMiddle, cx: _| {
302 motion(Motion::WindowMiddle, cx)
303 });
304 workspace.register_action(|_: &mut Workspace, &WindowBottom, cx: _| {
305 motion(Motion::WindowBottom, cx)
306 });
307 workspace.register_action(
308 |_: &mut Workspace, &PreviousWordEnd { ignore_punctuation }, cx: _| {
309 motion(Motion::PreviousWordEnd { ignore_punctuation }, cx)
310 },
311 );
312}
313
314pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
315 if let Some(Operator::FindForward { .. }) | Some(Operator::FindBackward { .. }) =
316 Vim::read(cx).active_operator()
317 {
318 Vim::update(cx, |vim, cx| vim.pop_operator(cx));
319 }
320
321 let count = Vim::update(cx, |vim, cx| vim.take_count(cx));
322 let operator = Vim::read(cx).active_operator();
323 match Vim::read(cx).state().mode {
324 Mode::Normal => normal_motion(motion, operator, count, cx),
325 Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_motion(motion, count, cx),
326 Mode::Insert => {
327 // Shouldn't execute a motion in insert mode. Ignoring
328 }
329 }
330 Vim::update(cx, |vim, cx| vim.clear_operator(cx));
331}
332
333// Motion handling is specified here:
334// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
335impl Motion {
336 pub fn linewise(&self) -> bool {
337 use Motion::*;
338 match self {
339 Down { .. }
340 | Up { .. }
341 | StartOfDocument
342 | EndOfDocument
343 | CurrentLine
344 | NextLineStart
345 | StartOfLineDownward
346 | StartOfParagraph
347 | WindowTop
348 | WindowMiddle
349 | WindowBottom
350 | EndOfParagraph => true,
351 EndOfLine { .. }
352 | NextWordEnd { .. }
353 | Matching
354 | FindForward { .. }
355 | Left
356 | Backspace
357 | Right
358 | Space
359 | StartOfLine { .. }
360 | EndOfLineDownward
361 | GoToColumn
362 | NextWordStart { .. }
363 | PreviousWordStart { .. }
364 | PreviousWordEnd { .. }
365 | FirstNonWhitespace { .. }
366 | FindBackward { .. }
367 | RepeatFind { .. }
368 | RepeatFindReversed { .. } => false,
369 }
370 }
371
372 pub fn infallible(&self) -> bool {
373 use Motion::*;
374 match self {
375 StartOfDocument | EndOfDocument | CurrentLine => true,
376 Down { .. }
377 | Up { .. }
378 | EndOfLine { .. }
379 | NextWordEnd { .. }
380 | Matching
381 | FindForward { .. }
382 | RepeatFind { .. }
383 | Left
384 | Backspace
385 | Right
386 | Space
387 | StartOfLine { .. }
388 | StartOfParagraph
389 | EndOfParagraph
390 | StartOfLineDownward
391 | EndOfLineDownward
392 | GoToColumn
393 | NextWordStart { .. }
394 | PreviousWordStart { .. }
395 | FirstNonWhitespace { .. }
396 | FindBackward { .. }
397 | RepeatFindReversed { .. }
398 | WindowTop
399 | WindowMiddle
400 | WindowBottom
401 | PreviousWordEnd { .. }
402 | NextLineStart => false,
403 }
404 }
405
406 pub fn inclusive(&self) -> bool {
407 use Motion::*;
408 match self {
409 Down { .. }
410 | Up { .. }
411 | StartOfDocument
412 | EndOfDocument
413 | CurrentLine
414 | EndOfLine { .. }
415 | EndOfLineDownward
416 | NextWordEnd { .. }
417 | Matching
418 | FindForward { .. }
419 | WindowTop
420 | WindowMiddle
421 | WindowBottom
422 | PreviousWordEnd { .. }
423 | NextLineStart => true,
424 Left
425 | Backspace
426 | Right
427 | Space
428 | StartOfLine { .. }
429 | StartOfLineDownward
430 | StartOfParagraph
431 | EndOfParagraph
432 | GoToColumn
433 | NextWordStart { .. }
434 | PreviousWordStart { .. }
435 | FirstNonWhitespace { .. }
436 | FindBackward { .. } => false,
437 RepeatFind { last_find: motion } | RepeatFindReversed { last_find: motion } => {
438 motion.inclusive()
439 }
440 }
441 }
442
443 pub fn move_point(
444 &self,
445 map: &DisplaySnapshot,
446 point: DisplayPoint,
447 goal: SelectionGoal,
448 maybe_times: Option<usize>,
449 text_layout_details: &TextLayoutDetails,
450 ) -> Option<(DisplayPoint, SelectionGoal)> {
451 let times = maybe_times.unwrap_or(1);
452 use Motion::*;
453 let infallible = self.infallible();
454 let (new_point, goal) = match self {
455 Left => (left(map, point, times), SelectionGoal::None),
456 Backspace => (backspace(map, point, times), SelectionGoal::None),
457 Down {
458 display_lines: false,
459 } => up_down_buffer_rows(map, point, goal, times as isize, &text_layout_details),
460 Down {
461 display_lines: true,
462 } => down_display(map, point, goal, times, &text_layout_details),
463 Up {
464 display_lines: false,
465 } => up_down_buffer_rows(map, point, goal, 0 - times as isize, &text_layout_details),
466 Up {
467 display_lines: true,
468 } => up_display(map, point, goal, times, &text_layout_details),
469 Right => (right(map, point, times), SelectionGoal::None),
470 Space => (space(map, point, times), SelectionGoal::None),
471 NextWordStart { ignore_punctuation } => (
472 next_word_start(map, point, *ignore_punctuation, times),
473 SelectionGoal::None,
474 ),
475 NextWordEnd { ignore_punctuation } => (
476 next_word_end(map, point, *ignore_punctuation, times),
477 SelectionGoal::None,
478 ),
479 PreviousWordStart { ignore_punctuation } => (
480 previous_word_start(map, point, *ignore_punctuation, times),
481 SelectionGoal::None,
482 ),
483 PreviousWordEnd { ignore_punctuation } => (
484 previous_word_end(map, point, *ignore_punctuation, times),
485 SelectionGoal::None,
486 ),
487 FirstNonWhitespace { display_lines } => (
488 first_non_whitespace(map, *display_lines, point),
489 SelectionGoal::None,
490 ),
491 StartOfLine { display_lines } => (
492 start_of_line(map, *display_lines, point),
493 SelectionGoal::None,
494 ),
495 EndOfLine { display_lines } => {
496 (end_of_line(map, *display_lines, point), SelectionGoal::None)
497 }
498 StartOfParagraph => (
499 movement::start_of_paragraph(map, point, times),
500 SelectionGoal::None,
501 ),
502 EndOfParagraph => (
503 map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
504 SelectionGoal::None,
505 ),
506 CurrentLine => (next_line_end(map, point, times), SelectionGoal::None),
507 StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
508 EndOfDocument => (
509 end_of_document(map, point, maybe_times),
510 SelectionGoal::None,
511 ),
512 Matching => (matching(map, point), SelectionGoal::None),
513 // t f
514 FindForward { before, char, mode } => {
515 return find_forward(map, point, *before, *char, times, *mode)
516 .map(|new_point| (new_point, SelectionGoal::None))
517 }
518 // T F
519 FindBackward { after, char, mode } => (
520 find_backward(map, point, *after, *char, times, *mode),
521 SelectionGoal::None,
522 ),
523 // ; -- repeat the last find done with t, f, T, F
524 RepeatFind { last_find } => match **last_find {
525 Motion::FindForward { before, char, mode } => {
526 let mut new_point = find_forward(map, point, before, char, times, mode);
527 if new_point == Some(point) {
528 new_point = find_forward(map, point, before, char, times + 1, mode);
529 }
530
531 return new_point.map(|new_point| (new_point, SelectionGoal::None));
532 }
533
534 Motion::FindBackward { after, char, mode } => {
535 let mut new_point = find_backward(map, point, after, char, times, mode);
536 if new_point == point {
537 new_point = find_backward(map, point, after, char, times + 1, mode);
538 }
539
540 (new_point, SelectionGoal::None)
541 }
542 _ => return None,
543 },
544 // , -- repeat the last find done with t, f, T, F, in opposite direction
545 RepeatFindReversed { last_find } => match **last_find {
546 Motion::FindForward { before, char, mode } => {
547 let mut new_point = find_backward(map, point, before, char, times, mode);
548 if new_point == point {
549 new_point = find_backward(map, point, before, char, times + 1, mode);
550 }
551
552 (new_point, SelectionGoal::None)
553 }
554
555 Motion::FindBackward { after, char, mode } => {
556 let mut new_point = find_forward(map, point, after, char, times, mode);
557 if new_point == Some(point) {
558 new_point = find_forward(map, point, after, char, times + 1, mode);
559 }
560
561 return new_point.map(|new_point| (new_point, SelectionGoal::None));
562 }
563 _ => return None,
564 },
565 NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
566 StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None),
567 EndOfLineDownward => (next_line_end(map, point, times), SelectionGoal::None),
568 GoToColumn => (go_to_column(map, point, times), SelectionGoal::None),
569 WindowTop => window_top(map, point, &text_layout_details, times - 1),
570 WindowMiddle => window_middle(map, point, &text_layout_details),
571 WindowBottom => window_bottom(map, point, &text_layout_details, times - 1),
572 };
573
574 (new_point != point || infallible).then_some((new_point, goal))
575 }
576
577 // Expands a selection using self motion for an operator
578 pub fn expand_selection(
579 &self,
580 map: &DisplaySnapshot,
581 selection: &mut Selection<DisplayPoint>,
582 times: Option<usize>,
583 expand_to_surrounding_newline: bool,
584 text_layout_details: &TextLayoutDetails,
585 ) -> bool {
586 if let Some((new_head, goal)) = self.move_point(
587 map,
588 selection.head(),
589 selection.goal,
590 times,
591 &text_layout_details,
592 ) {
593 selection.set_head(new_head, goal);
594
595 if self.linewise() {
596 selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
597
598 if expand_to_surrounding_newline {
599 if selection.end.row() < map.max_point().row() {
600 *selection.end.row_mut() += 1;
601 *selection.end.column_mut() = 0;
602 selection.end = map.clip_point(selection.end, Bias::Right);
603 // Don't reset the end here
604 return true;
605 } else if selection.start.row() > 0 {
606 *selection.start.row_mut() -= 1;
607 *selection.start.column_mut() = map.line_len(selection.start.row());
608 selection.start = map.clip_point(selection.start, Bias::Left);
609 }
610 }
611
612 (_, selection.end) = map.next_line_boundary(selection.end.to_point(map));
613 } else {
614 // Another special case: When using the "w" motion in combination with an
615 // operator and the last word moved over is at the end of a line, the end of
616 // that word becomes the end of the operated text, not the first word in the
617 // next line.
618 if let Motion::NextWordStart {
619 ignore_punctuation: _,
620 } = self
621 {
622 let start_row = selection.start.to_point(&map).row;
623 if selection.end.to_point(&map).row > start_row {
624 selection.end =
625 Point::new(start_row, map.buffer_snapshot.line_len(start_row))
626 .to_display_point(&map)
627 }
628 }
629
630 // If the motion is exclusive and the end of the motion is in column 1, the
631 // end of the motion is moved to the end of the previous line and the motion
632 // becomes inclusive. Example: "}" moves to the first line after a paragraph,
633 // but "d}" will not include that line.
634 let mut inclusive = self.inclusive();
635 if !inclusive
636 && self != &Motion::Backspace
637 && selection.end.row() > selection.start.row()
638 && selection.end.column() == 0
639 {
640 inclusive = true;
641 *selection.end.row_mut() -= 1;
642 *selection.end.column_mut() = 0;
643 selection.end = map.clip_point(
644 map.next_line_boundary(selection.end.to_point(map)).1,
645 Bias::Left,
646 );
647 }
648
649 if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
650 *selection.end.column_mut() += 1;
651 }
652 }
653 true
654 } else {
655 false
656 }
657 }
658}
659
660fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
661 for _ in 0..times {
662 point = movement::saturating_left(map, point);
663 if point.column() == 0 {
664 break;
665 }
666 }
667 point
668}
669
670fn backspace(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
671 for _ in 0..times {
672 point = movement::left(map, point);
673 if point.is_zero() {
674 break;
675 }
676 }
677 point
678}
679
680fn space(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
681 for _ in 0..times {
682 point = wrapping_right(map, point);
683 if point == map.max_point() {
684 break;
685 }
686 }
687 point
688}
689
690fn wrapping_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
691 let max_column = map.line_len(point.row()).saturating_sub(1);
692 if point.column() < max_column {
693 *point.column_mut() += 1;
694 } else if point.row() < map.max_point().row() {
695 *point.row_mut() += 1;
696 *point.column_mut() = 0;
697 }
698 point
699}
700
701pub(crate) fn start_of_relative_buffer_row(
702 map: &DisplaySnapshot,
703 point: DisplayPoint,
704 times: isize,
705) -> DisplayPoint {
706 let start = map.display_point_to_fold_point(point, Bias::Left);
707 let target = start.row() as isize + times;
708 let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
709
710 map.clip_point(
711 map.fold_point_to_display_point(
712 map.fold_snapshot
713 .clip_point(FoldPoint::new(new_row, 0), Bias::Right),
714 ),
715 Bias::Right,
716 )
717}
718
719fn up_down_buffer_rows(
720 map: &DisplaySnapshot,
721 point: DisplayPoint,
722 mut goal: SelectionGoal,
723 times: isize,
724 text_layout_details: &TextLayoutDetails,
725) -> (DisplayPoint, SelectionGoal) {
726 let start = map.display_point_to_fold_point(point, Bias::Left);
727 let begin_folded_line = map.fold_point_to_display_point(
728 map.fold_snapshot
729 .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
730 );
731 let select_nth_wrapped_row = point.row() - begin_folded_line.row();
732
733 let (goal_wrap, goal_x) = match goal {
734 SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
735 SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
736 SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
737 _ => {
738 let x = map.x_for_display_point(point, text_layout_details);
739 goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x.0));
740 (select_nth_wrapped_row, x.0)
741 }
742 };
743
744 let target = start.row() as isize + times;
745 let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
746
747 let mut begin_folded_line = map.fold_point_to_display_point(
748 map.fold_snapshot
749 .clip_point(FoldPoint::new(new_row, 0), Bias::Left),
750 );
751
752 let mut i = 0;
753 while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
754 let next_folded_line = DisplayPoint::new(begin_folded_line.row() + 1, 0);
755 if map
756 .display_point_to_fold_point(next_folded_line, Bias::Right)
757 .row()
758 == new_row
759 {
760 i += 1;
761 begin_folded_line = next_folded_line;
762 } else {
763 break;
764 }
765 }
766
767 let new_col = if i == goal_wrap {
768 map.display_column_for_x(begin_folded_line.row(), px(goal_x), text_layout_details)
769 } else {
770 map.line_len(begin_folded_line.row())
771 };
772
773 (
774 map.clip_point(
775 DisplayPoint::new(begin_folded_line.row(), new_col),
776 Bias::Left,
777 ),
778 goal,
779 )
780}
781
782fn down_display(
783 map: &DisplaySnapshot,
784 mut point: DisplayPoint,
785 mut goal: SelectionGoal,
786 times: usize,
787 text_layout_details: &TextLayoutDetails,
788) -> (DisplayPoint, SelectionGoal) {
789 for _ in 0..times {
790 (point, goal) = movement::down(map, point, goal, true, text_layout_details);
791 }
792
793 (point, goal)
794}
795
796fn up_display(
797 map: &DisplaySnapshot,
798 mut point: DisplayPoint,
799 mut goal: SelectionGoal,
800 times: usize,
801 text_layout_details: &TextLayoutDetails,
802) -> (DisplayPoint, SelectionGoal) {
803 for _ in 0..times {
804 (point, goal) = movement::up(map, point, goal, true, &text_layout_details);
805 }
806
807 (point, goal)
808}
809
810pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
811 for _ in 0..times {
812 let new_point = movement::saturating_right(map, point);
813 if point == new_point {
814 break;
815 }
816 point = new_point;
817 }
818 point
819}
820
821pub(crate) fn next_word_start(
822 map: &DisplaySnapshot,
823 mut point: DisplayPoint,
824 ignore_punctuation: bool,
825 times: usize,
826) -> DisplayPoint {
827 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
828 for _ in 0..times {
829 let mut crossed_newline = false;
830 let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
831 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
832 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
833 let at_newline = right == '\n';
834
835 let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
836 || at_newline && crossed_newline
837 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
838
839 crossed_newline |= at_newline;
840 found
841 });
842 if point == new_point {
843 break;
844 }
845 point = new_point;
846 }
847 point
848}
849
850fn next_word_end(
851 map: &DisplaySnapshot,
852 mut point: DisplayPoint,
853 ignore_punctuation: bool,
854 times: usize,
855) -> DisplayPoint {
856 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
857 for _ in 0..times {
858 let mut new_point = point;
859 if new_point.column() < map.line_len(new_point.row()) {
860 *new_point.column_mut() += 1;
861 } else if new_point < map.max_point() {
862 *new_point.row_mut() += 1;
863 *new_point.column_mut() = 0;
864 }
865
866 let new_point = movement::find_boundary_exclusive(
867 map,
868 new_point,
869 FindRange::MultiLine,
870 |left, right| {
871 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
872 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
873
874 left_kind != right_kind && left_kind != CharKind::Whitespace
875 },
876 );
877 let new_point = map.clip_point(new_point, Bias::Left);
878 if point == new_point {
879 break;
880 }
881 point = new_point;
882 }
883 point
884}
885
886fn previous_word_start(
887 map: &DisplaySnapshot,
888 mut point: DisplayPoint,
889 ignore_punctuation: bool,
890 times: usize,
891) -> DisplayPoint {
892 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
893 for _ in 0..times {
894 // This works even though find_preceding_boundary is called for every character in the line containing
895 // cursor because the newline is checked only once.
896 let new_point = movement::find_preceding_boundary_display_point(
897 map,
898 point,
899 FindRange::MultiLine,
900 |left, right| {
901 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
902 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
903
904 (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
905 },
906 );
907 if point == new_point {
908 break;
909 }
910 point = new_point;
911 }
912 point
913}
914
915pub(crate) fn first_non_whitespace(
916 map: &DisplaySnapshot,
917 display_lines: bool,
918 from: DisplayPoint,
919) -> DisplayPoint {
920 let mut last_point = start_of_line(map, display_lines, from);
921 let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
922 for (ch, point) in map.chars_at(last_point) {
923 if ch == '\n' {
924 return from;
925 }
926
927 last_point = point;
928
929 if char_kind(&scope, ch) != CharKind::Whitespace {
930 break;
931 }
932 }
933
934 map.clip_point(last_point, Bias::Left)
935}
936
937pub(crate) fn start_of_line(
938 map: &DisplaySnapshot,
939 display_lines: bool,
940 point: DisplayPoint,
941) -> DisplayPoint {
942 if display_lines {
943 map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
944 } else {
945 map.prev_line_boundary(point.to_point(map)).1
946 }
947}
948
949pub(crate) fn end_of_line(
950 map: &DisplaySnapshot,
951 display_lines: bool,
952 point: DisplayPoint,
953) -> DisplayPoint {
954 if display_lines {
955 map.clip_point(
956 DisplayPoint::new(point.row(), map.line_len(point.row())),
957 Bias::Left,
958 )
959 } else {
960 map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
961 }
962}
963
964fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
965 let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
966 *new_point.column_mut() = point.column();
967 map.clip_point(new_point, Bias::Left)
968}
969
970fn end_of_document(
971 map: &DisplaySnapshot,
972 point: DisplayPoint,
973 line: Option<usize>,
974) -> DisplayPoint {
975 let new_row = if let Some(line) = line {
976 (line - 1) as u32
977 } else {
978 map.max_buffer_row()
979 };
980
981 let new_point = Point::new(new_row, point.column());
982 map.clip_point(new_point.to_display_point(map), Bias::Left)
983}
984
985fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
986 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
987 let point = display_point.to_point(map);
988 let offset = point.to_offset(&map.buffer_snapshot);
989
990 // Ensure the range is contained by the current line.
991 let mut line_end = map.next_line_boundary(point).0;
992 if line_end == point {
993 line_end = map.max_point().to_point(map);
994 }
995
996 let line_range = map.prev_line_boundary(point).0..line_end;
997 let visible_line_range =
998 line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
999 let ranges = map
1000 .buffer_snapshot
1001 .bracket_ranges(visible_line_range.clone());
1002 if let Some(ranges) = ranges {
1003 let line_range = line_range.start.to_offset(&map.buffer_snapshot)
1004 ..line_range.end.to_offset(&map.buffer_snapshot);
1005 let mut closest_pair_destination = None;
1006 let mut closest_distance = usize::MAX;
1007
1008 for (open_range, close_range) in ranges {
1009 if open_range.start >= offset && line_range.contains(&open_range.start) {
1010 let distance = open_range.start - offset;
1011 if distance < closest_distance {
1012 closest_pair_destination = Some(close_range.start);
1013 closest_distance = distance;
1014 continue;
1015 }
1016 }
1017
1018 if close_range.start >= offset && line_range.contains(&close_range.start) {
1019 let distance = close_range.start - offset;
1020 if distance < closest_distance {
1021 closest_pair_destination = Some(open_range.start);
1022 closest_distance = distance;
1023 continue;
1024 }
1025 }
1026
1027 continue;
1028 }
1029
1030 closest_pair_destination
1031 .map(|destination| destination.to_display_point(map))
1032 .unwrap_or(display_point)
1033 } else {
1034 display_point
1035 }
1036}
1037
1038fn find_forward(
1039 map: &DisplaySnapshot,
1040 from: DisplayPoint,
1041 before: bool,
1042 target: char,
1043 times: usize,
1044 mode: FindRange,
1045) -> Option<DisplayPoint> {
1046 let mut to = from;
1047 let mut found = false;
1048
1049 for _ in 0..times {
1050 found = false;
1051 let new_to = find_boundary(map, to, mode, |_, right| {
1052 found = right == target;
1053 found
1054 });
1055 if to == new_to {
1056 break;
1057 }
1058 to = new_to;
1059 }
1060
1061 if found {
1062 if before && to.column() > 0 {
1063 *to.column_mut() -= 1;
1064 Some(map.clip_point(to, Bias::Left))
1065 } else {
1066 Some(to)
1067 }
1068 } else {
1069 None
1070 }
1071}
1072
1073fn find_backward(
1074 map: &DisplaySnapshot,
1075 from: DisplayPoint,
1076 after: bool,
1077 target: char,
1078 times: usize,
1079 mode: FindRange,
1080) -> DisplayPoint {
1081 let mut to = from;
1082
1083 for _ in 0..times {
1084 let new_to =
1085 find_preceding_boundary_display_point(map, to, mode, |_, right| right == target);
1086 if to == new_to {
1087 break;
1088 }
1089 to = new_to;
1090 }
1091
1092 if map.buffer_snapshot.chars_at(to.to_point(map)).next() == Some(target) {
1093 if after {
1094 *to.column_mut() += 1;
1095 map.clip_point(to, Bias::Right)
1096 } else {
1097 to
1098 }
1099 } else {
1100 from
1101 }
1102}
1103
1104fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1105 let correct_line = start_of_relative_buffer_row(map, point, times as isize);
1106 first_non_whitespace(map, false, correct_line)
1107}
1108
1109fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1110 let correct_line = start_of_relative_buffer_row(map, point, 0);
1111 right(map, correct_line, times.saturating_sub(1))
1112}
1113
1114pub(crate) fn next_line_end(
1115 map: &DisplaySnapshot,
1116 mut point: DisplayPoint,
1117 times: usize,
1118) -> DisplayPoint {
1119 if times > 1 {
1120 point = start_of_relative_buffer_row(map, point, times as isize - 1);
1121 }
1122 end_of_line(map, false, point)
1123}
1124
1125fn window_top(
1126 map: &DisplaySnapshot,
1127 point: DisplayPoint,
1128 text_layout_details: &TextLayoutDetails,
1129 mut times: usize,
1130) -> (DisplayPoint, SelectionGoal) {
1131 let first_visible_line = text_layout_details
1132 .scroll_anchor
1133 .anchor
1134 .to_display_point(map);
1135
1136 if first_visible_line.row() != 0 && text_layout_details.vertical_scroll_margin as usize > times
1137 {
1138 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1139 }
1140
1141 if let Some(visible_rows) = text_layout_details.visible_rows {
1142 let bottom_row = first_visible_line.row() + visible_rows as u32;
1143 let new_row = (first_visible_line.row() + (times as u32))
1144 .min(bottom_row)
1145 .min(map.max_point().row());
1146 let new_col = point.column().min(map.line_len(first_visible_line.row()));
1147
1148 let new_point = DisplayPoint::new(new_row, new_col);
1149 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1150 } else {
1151 let new_row = (first_visible_line.row() + (times as u32)).min(map.max_point().row());
1152 let new_col = point.column().min(map.line_len(first_visible_line.row()));
1153
1154 let new_point = DisplayPoint::new(new_row, new_col);
1155 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1156 }
1157}
1158
1159fn window_middle(
1160 map: &DisplaySnapshot,
1161 point: DisplayPoint,
1162 text_layout_details: &TextLayoutDetails,
1163) -> (DisplayPoint, SelectionGoal) {
1164 if let Some(visible_rows) = text_layout_details.visible_rows {
1165 let first_visible_line = text_layout_details
1166 .scroll_anchor
1167 .anchor
1168 .to_display_point(map);
1169
1170 let max_visible_rows =
1171 (visible_rows as u32).min(map.max_point().row() - first_visible_line.row());
1172
1173 let new_row =
1174 (first_visible_line.row() + (max_visible_rows / 2) as u32).min(map.max_point().row());
1175 let new_col = point.column().min(map.line_len(new_row));
1176 let new_point = DisplayPoint::new(new_row, new_col);
1177 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1178 } else {
1179 (point, SelectionGoal::None)
1180 }
1181}
1182
1183fn window_bottom(
1184 map: &DisplaySnapshot,
1185 point: DisplayPoint,
1186 text_layout_details: &TextLayoutDetails,
1187 mut times: usize,
1188) -> (DisplayPoint, SelectionGoal) {
1189 if let Some(visible_rows) = text_layout_details.visible_rows {
1190 let first_visible_line = text_layout_details
1191 .scroll_anchor
1192 .anchor
1193 .to_display_point(map);
1194 let bottom_row = first_visible_line.row()
1195 + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
1196 if bottom_row < map.max_point().row()
1197 && text_layout_details.vertical_scroll_margin as usize > times
1198 {
1199 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1200 }
1201 let bottom_row_capped = bottom_row.min(map.max_point().row());
1202 let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row() {
1203 first_visible_line.row()
1204 } else {
1205 bottom_row_capped.saturating_sub(times as u32)
1206 };
1207 let new_col = point.column().min(map.line_len(new_row));
1208 let new_point = DisplayPoint::new(new_row, new_col);
1209 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1210 } else {
1211 (point, SelectionGoal::None)
1212 }
1213}
1214
1215fn previous_word_end(
1216 map: &DisplaySnapshot,
1217 point: DisplayPoint,
1218 ignore_punctuation: bool,
1219 times: usize,
1220) -> DisplayPoint {
1221 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1222 let mut point = point.to_point(map);
1223
1224 if point.column < map.buffer_snapshot.line_len(point.row) {
1225 point.column += 1;
1226 }
1227 for _ in 0..times {
1228 let new_point = movement::find_preceding_boundary_point(
1229 &map.buffer_snapshot,
1230 point,
1231 FindRange::MultiLine,
1232 |left, right| {
1233 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1234 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1235 match (left_kind, right_kind) {
1236 (CharKind::Punctuation, CharKind::Whitespace)
1237 | (CharKind::Punctuation, CharKind::Word)
1238 | (CharKind::Word, CharKind::Whitespace)
1239 | (CharKind::Word, CharKind::Punctuation) => true,
1240 (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1241 _ => false,
1242 }
1243 },
1244 );
1245 if new_point == point {
1246 break;
1247 }
1248 point = new_point;
1249 }
1250 movement::saturating_left(map, point.to_display_point(map))
1251}
1252
1253#[cfg(test)]
1254mod test {
1255
1256 use crate::test::NeovimBackedTestContext;
1257 use indoc::indoc;
1258
1259 #[gpui::test]
1260 async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
1261 let mut cx = NeovimBackedTestContext::new(cx).await;
1262
1263 let initial_state = indoc! {r"ˇabc
1264 def
1265
1266 paragraph
1267 the second
1268
1269
1270
1271 third and
1272 final"};
1273
1274 // goes down once
1275 cx.set_shared_state(initial_state).await;
1276 cx.simulate_shared_keystrokes(["}"]).await;
1277 cx.assert_shared_state(indoc! {r"abc
1278 def
1279 ˇ
1280 paragraph
1281 the second
1282
1283
1284
1285 third and
1286 final"})
1287 .await;
1288
1289 // goes up once
1290 cx.simulate_shared_keystrokes(["{"]).await;
1291 cx.assert_shared_state(initial_state).await;
1292
1293 // goes down twice
1294 cx.simulate_shared_keystrokes(["2", "}"]).await;
1295 cx.assert_shared_state(indoc! {r"abc
1296 def
1297
1298 paragraph
1299 the second
1300 ˇ
1301
1302
1303 third and
1304 final"})
1305 .await;
1306
1307 // goes down over multiple blanks
1308 cx.simulate_shared_keystrokes(["}"]).await;
1309 cx.assert_shared_state(indoc! {r"abc
1310 def
1311
1312 paragraph
1313 the second
1314
1315
1316
1317 third and
1318 finaˇl"})
1319 .await;
1320
1321 // goes up twice
1322 cx.simulate_shared_keystrokes(["2", "{"]).await;
1323 cx.assert_shared_state(indoc! {r"abc
1324 def
1325 ˇ
1326 paragraph
1327 the second
1328
1329
1330
1331 third and
1332 final"})
1333 .await
1334 }
1335
1336 #[gpui::test]
1337 async fn test_matching(cx: &mut gpui::TestAppContext) {
1338 let mut cx = NeovimBackedTestContext::new(cx).await;
1339
1340 cx.set_shared_state(indoc! {r"func ˇ(a string) {
1341 do(something(with<Types>.and_arrays[0, 2]))
1342 }"})
1343 .await;
1344 cx.simulate_shared_keystrokes(["%"]).await;
1345 cx.assert_shared_state(indoc! {r"func (a stringˇ) {
1346 do(something(with<Types>.and_arrays[0, 2]))
1347 }"})
1348 .await;
1349
1350 // test it works on the last character of the line
1351 cx.set_shared_state(indoc! {r"func (a string) ˇ{
1352 do(something(with<Types>.and_arrays[0, 2]))
1353 }"})
1354 .await;
1355 cx.simulate_shared_keystrokes(["%"]).await;
1356 cx.assert_shared_state(indoc! {r"func (a string) {
1357 do(something(with<Types>.and_arrays[0, 2]))
1358 ˇ}"})
1359 .await;
1360
1361 // test it works on immediate nesting
1362 cx.set_shared_state("ˇ{()}").await;
1363 cx.simulate_shared_keystrokes(["%"]).await;
1364 cx.assert_shared_state("{()ˇ}").await;
1365 cx.simulate_shared_keystrokes(["%"]).await;
1366 cx.assert_shared_state("ˇ{()}").await;
1367
1368 // test it works on immediate nesting inside braces
1369 cx.set_shared_state("{\n ˇ{()}\n}").await;
1370 cx.simulate_shared_keystrokes(["%"]).await;
1371 cx.assert_shared_state("{\n {()ˇ}\n}").await;
1372
1373 // test it jumps to the next paren on a line
1374 cx.set_shared_state("func ˇboop() {\n}").await;
1375 cx.simulate_shared_keystrokes(["%"]).await;
1376 cx.assert_shared_state("func boop(ˇ) {\n}").await;
1377 }
1378
1379 #[gpui::test]
1380 async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
1381 let mut cx = NeovimBackedTestContext::new(cx).await;
1382
1383 // f and F
1384 cx.set_shared_state("ˇone two three four").await;
1385 cx.simulate_shared_keystrokes(["f", "o"]).await;
1386 cx.assert_shared_state("one twˇo three four").await;
1387 cx.simulate_shared_keystrokes([","]).await;
1388 cx.assert_shared_state("ˇone two three four").await;
1389 cx.simulate_shared_keystrokes(["2", ";"]).await;
1390 cx.assert_shared_state("one two three fˇour").await;
1391 cx.simulate_shared_keystrokes(["shift-f", "e"]).await;
1392 cx.assert_shared_state("one two threˇe four").await;
1393 cx.simulate_shared_keystrokes(["2", ";"]).await;
1394 cx.assert_shared_state("onˇe two three four").await;
1395 cx.simulate_shared_keystrokes([","]).await;
1396 cx.assert_shared_state("one two thrˇee four").await;
1397
1398 // t and T
1399 cx.set_shared_state("ˇone two three four").await;
1400 cx.simulate_shared_keystrokes(["t", "o"]).await;
1401 cx.assert_shared_state("one tˇwo three four").await;
1402 cx.simulate_shared_keystrokes([","]).await;
1403 cx.assert_shared_state("oˇne two three four").await;
1404 cx.simulate_shared_keystrokes(["2", ";"]).await;
1405 cx.assert_shared_state("one two three ˇfour").await;
1406 cx.simulate_shared_keystrokes(["shift-t", "e"]).await;
1407 cx.assert_shared_state("one two threeˇ four").await;
1408 cx.simulate_shared_keystrokes(["3", ";"]).await;
1409 cx.assert_shared_state("oneˇ two three four").await;
1410 cx.simulate_shared_keystrokes([","]).await;
1411 cx.assert_shared_state("one two thˇree four").await;
1412 }
1413
1414 #[gpui::test]
1415 async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
1416 let mut cx = NeovimBackedTestContext::new(cx).await;
1417 let initial_state = indoc! {r"something(ˇfoo)"};
1418 cx.set_shared_state(initial_state).await;
1419 cx.simulate_shared_keystrokes(["}"]).await;
1420 cx.assert_shared_state(indoc! {r"something(fooˇ)"}).await;
1421 }
1422
1423 #[gpui::test]
1424 async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
1425 let mut cx = NeovimBackedTestContext::new(cx).await;
1426 cx.set_shared_state("ˇone\n two\nthree").await;
1427 cx.simulate_shared_keystrokes(["enter"]).await;
1428 cx.assert_shared_state("one\n ˇtwo\nthree").await;
1429 }
1430
1431 #[gpui::test]
1432 async fn test_window_top(cx: &mut gpui::TestAppContext) {
1433 let mut cx = NeovimBackedTestContext::new(cx).await;
1434 let initial_state = indoc! {r"abc
1435 def
1436 paragraph
1437 the second
1438 third ˇand
1439 final"};
1440
1441 cx.set_shared_state(initial_state).await;
1442 cx.simulate_shared_keystrokes(["shift-h"]).await;
1443 cx.assert_shared_state(indoc! {r"abˇc
1444 def
1445 paragraph
1446 the second
1447 third and
1448 final"})
1449 .await;
1450
1451 // clip point
1452 cx.set_shared_state(indoc! {r"
1453 1 2 3
1454 4 5 6
1455 7 8 ˇ9
1456 "})
1457 .await;
1458 cx.simulate_shared_keystrokes(["shift-h"]).await;
1459 cx.assert_shared_state(indoc! {r"
1460 1 2 ˇ3
1461 4 5 6
1462 7 8 9
1463 "})
1464 .await;
1465
1466 cx.set_shared_state(indoc! {r"
1467 1 2 3
1468 4 5 6
1469 ˇ7 8 9
1470 "})
1471 .await;
1472 cx.simulate_shared_keystrokes(["shift-h"]).await;
1473 cx.assert_shared_state(indoc! {r"
1474 ˇ1 2 3
1475 4 5 6
1476 7 8 9
1477 "})
1478 .await;
1479
1480 cx.set_shared_state(indoc! {r"
1481 1 2 3
1482 4 5 ˇ6
1483 7 8 9"})
1484 .await;
1485 cx.simulate_shared_keystrokes(["9", "shift-h"]).await;
1486 cx.assert_shared_state(indoc! {r"
1487 1 2 3
1488 4 5 6
1489 7 8 ˇ9"})
1490 .await;
1491 }
1492
1493 #[gpui::test]
1494 async fn test_window_middle(cx: &mut gpui::TestAppContext) {
1495 let mut cx = NeovimBackedTestContext::new(cx).await;
1496 let initial_state = indoc! {r"abˇc
1497 def
1498 paragraph
1499 the second
1500 third and
1501 final"};
1502
1503 cx.set_shared_state(initial_state).await;
1504 cx.simulate_shared_keystrokes(["shift-m"]).await;
1505 cx.assert_shared_state(indoc! {r"abc
1506 def
1507 paˇragraph
1508 the second
1509 third and
1510 final"})
1511 .await;
1512
1513 cx.set_shared_state(indoc! {r"
1514 1 2 3
1515 4 5 6
1516 7 8 ˇ9
1517 "})
1518 .await;
1519 cx.simulate_shared_keystrokes(["shift-m"]).await;
1520 cx.assert_shared_state(indoc! {r"
1521 1 2 3
1522 4 5 ˇ6
1523 7 8 9
1524 "})
1525 .await;
1526 cx.set_shared_state(indoc! {r"
1527 1 2 3
1528 4 5 6
1529 ˇ7 8 9
1530 "})
1531 .await;
1532 cx.simulate_shared_keystrokes(["shift-m"]).await;
1533 cx.assert_shared_state(indoc! {r"
1534 1 2 3
1535 ˇ4 5 6
1536 7 8 9
1537 "})
1538 .await;
1539 cx.set_shared_state(indoc! {r"
1540 ˇ1 2 3
1541 4 5 6
1542 7 8 9
1543 "})
1544 .await;
1545 cx.simulate_shared_keystrokes(["shift-m"]).await;
1546 cx.assert_shared_state(indoc! {r"
1547 1 2 3
1548 ˇ4 5 6
1549 7 8 9
1550 "})
1551 .await;
1552 cx.set_shared_state(indoc! {r"
1553 1 2 3
1554 ˇ4 5 6
1555 7 8 9
1556 "})
1557 .await;
1558 cx.simulate_shared_keystrokes(["shift-m"]).await;
1559 cx.assert_shared_state(indoc! {r"
1560 1 2 3
1561 ˇ4 5 6
1562 7 8 9
1563 "})
1564 .await;
1565 cx.set_shared_state(indoc! {r"
1566 1 2 3
1567 4 5 ˇ6
1568 7 8 9
1569 "})
1570 .await;
1571 cx.simulate_shared_keystrokes(["shift-m"]).await;
1572 cx.assert_shared_state(indoc! {r"
1573 1 2 3
1574 4 5 ˇ6
1575 7 8 9
1576 "})
1577 .await;
1578 }
1579
1580 #[gpui::test]
1581 async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
1582 let mut cx = NeovimBackedTestContext::new(cx).await;
1583 let initial_state = indoc! {r"abc
1584 deˇf
1585 paragraph
1586 the second
1587 third and
1588 final"};
1589
1590 cx.set_shared_state(initial_state).await;
1591 cx.simulate_shared_keystrokes(["shift-l"]).await;
1592 cx.assert_shared_state(indoc! {r"abc
1593 def
1594 paragraph
1595 the second
1596 third and
1597 fiˇnal"})
1598 .await;
1599
1600 cx.set_shared_state(indoc! {r"
1601 1 2 3
1602 4 5 ˇ6
1603 7 8 9
1604 "})
1605 .await;
1606 cx.simulate_shared_keystrokes(["shift-l"]).await;
1607 cx.assert_shared_state(indoc! {r"
1608 1 2 3
1609 4 5 6
1610 7 8 9
1611 ˇ"})
1612 .await;
1613
1614 cx.set_shared_state(indoc! {r"
1615 1 2 3
1616 ˇ4 5 6
1617 7 8 9
1618 "})
1619 .await;
1620 cx.simulate_shared_keystrokes(["shift-l"]).await;
1621 cx.assert_shared_state(indoc! {r"
1622 1 2 3
1623 4 5 6
1624 7 8 9
1625 ˇ"})
1626 .await;
1627
1628 cx.set_shared_state(indoc! {r"
1629 1 2 ˇ3
1630 4 5 6
1631 7 8 9
1632 "})
1633 .await;
1634 cx.simulate_shared_keystrokes(["shift-l"]).await;
1635 cx.assert_shared_state(indoc! {r"
1636 1 2 3
1637 4 5 6
1638 7 8 9
1639 ˇ"})
1640 .await;
1641
1642 cx.set_shared_state(indoc! {r"
1643 ˇ1 2 3
1644 4 5 6
1645 7 8 9
1646 "})
1647 .await;
1648 cx.simulate_shared_keystrokes(["shift-l"]).await;
1649 cx.assert_shared_state(indoc! {r"
1650 1 2 3
1651 4 5 6
1652 7 8 9
1653 ˇ"})
1654 .await;
1655
1656 cx.set_shared_state(indoc! {r"
1657 1 2 3
1658 4 5 ˇ6
1659 7 8 9
1660 "})
1661 .await;
1662 cx.simulate_shared_keystrokes(["9", "shift-l"]).await;
1663 cx.assert_shared_state(indoc! {r"
1664 1 2 ˇ3
1665 4 5 6
1666 7 8 9
1667 "})
1668 .await;
1669 }
1670
1671 #[gpui::test]
1672 async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
1673 let mut cx = NeovimBackedTestContext::new(cx).await;
1674 cx.set_shared_state(indoc! {r"
1675 456 5ˇ67 678
1676 "})
1677 .await;
1678 cx.simulate_shared_keystrokes(["g", "e"]).await;
1679 cx.assert_shared_state(indoc! {r"
1680 45ˇ6 567 678
1681 "})
1682 .await;
1683
1684 // Test times
1685 cx.set_shared_state(indoc! {r"
1686 123 234 345
1687 456 5ˇ67 678
1688 "})
1689 .await;
1690 cx.simulate_shared_keystrokes(["4", "g", "e"]).await;
1691 cx.assert_shared_state(indoc! {r"
1692 12ˇ3 234 345
1693 456 567 678
1694 "})
1695 .await;
1696
1697 // With punctuation
1698 cx.set_shared_state(indoc! {r"
1699 123 234 345
1700 4;5.6 5ˇ67 678
1701 789 890 901
1702 "})
1703 .await;
1704 cx.simulate_shared_keystrokes(["g", "e"]).await;
1705 cx.assert_shared_state(indoc! {r"
1706 123 234 345
1707 4;5.ˇ6 567 678
1708 789 890 901
1709 "})
1710 .await;
1711
1712 // With punctuation and count
1713 cx.set_shared_state(indoc! {r"
1714 123 234 345
1715 4;5.6 5ˇ67 678
1716 789 890 901
1717 "})
1718 .await;
1719 cx.simulate_shared_keystrokes(["5", "g", "e"]).await;
1720 cx.assert_shared_state(indoc! {r"
1721 123 234 345
1722 ˇ4;5.6 567 678
1723 789 890 901
1724 "})
1725 .await;
1726
1727 // newlines
1728 cx.set_shared_state(indoc! {r"
1729 123 234 345
1730
1731 78ˇ9 890 901
1732 "})
1733 .await;
1734 cx.simulate_shared_keystrokes(["g", "e"]).await;
1735 cx.assert_shared_state(indoc! {r"
1736 123 234 345
1737 ˇ
1738 789 890 901
1739 "})
1740 .await;
1741 cx.simulate_shared_keystrokes(["g", "e"]).await;
1742 cx.assert_shared_state(indoc! {r"
1743 123 234 34ˇ5
1744
1745 789 890 901
1746 "})
1747 .await;
1748
1749 // With punctuation
1750 cx.set_shared_state(indoc! {r"
1751 123 234 345
1752 4;5.ˇ6 567 678
1753 789 890 901
1754 "})
1755 .await;
1756 cx.simulate_shared_keystrokes(["g", "shift-e"]).await;
1757 cx.assert_shared_state(indoc! {r"
1758 123 234 34ˇ5
1759 4;5.6 567 678
1760 789 890 901
1761 "})
1762 .await;
1763 }
1764}