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, times),
497 SelectionGoal::None,
498 ),
499 StartOfParagraph => (
500 movement::start_of_paragraph(map, point, times),
501 SelectionGoal::None,
502 ),
503 EndOfParagraph => (
504 map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
505 SelectionGoal::None,
506 ),
507 CurrentLine => (next_line_end(map, point, times), SelectionGoal::None),
508 StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
509 EndOfDocument => (
510 end_of_document(map, point, maybe_times),
511 SelectionGoal::None,
512 ),
513 Matching => (matching(map, point), SelectionGoal::None),
514 // t f
515 FindForward { before, char, mode } => {
516 return find_forward(map, point, *before, *char, times, *mode)
517 .map(|new_point| (new_point, SelectionGoal::None))
518 }
519 // T F
520 FindBackward { after, char, mode } => (
521 find_backward(map, point, *after, *char, times, *mode),
522 SelectionGoal::None,
523 ),
524 // ; -- repeat the last find done with t, f, T, F
525 RepeatFind { last_find } => match **last_find {
526 Motion::FindForward { before, char, mode } => {
527 let mut new_point = find_forward(map, point, before, char, times, mode);
528 if new_point == Some(point) {
529 new_point = find_forward(map, point, before, char, times + 1, mode);
530 }
531
532 return new_point.map(|new_point| (new_point, SelectionGoal::None));
533 }
534
535 Motion::FindBackward { after, char, mode } => {
536 let mut new_point = find_backward(map, point, after, char, times, mode);
537 if new_point == point {
538 new_point = find_backward(map, point, after, char, times + 1, mode);
539 }
540
541 (new_point, SelectionGoal::None)
542 }
543 _ => return None,
544 },
545 // , -- repeat the last find done with t, f, T, F, in opposite direction
546 RepeatFindReversed { last_find } => match **last_find {
547 Motion::FindForward { before, char, mode } => {
548 let mut new_point = find_backward(map, point, before, char, times, mode);
549 if new_point == point {
550 new_point = find_backward(map, point, before, char, times + 1, mode);
551 }
552
553 (new_point, SelectionGoal::None)
554 }
555
556 Motion::FindBackward { after, char, mode } => {
557 let mut new_point = find_forward(map, point, after, char, times, mode);
558 if new_point == Some(point) {
559 new_point = find_forward(map, point, after, char, times + 1, mode);
560 }
561
562 return new_point.map(|new_point| (new_point, SelectionGoal::None));
563 }
564 _ => return None,
565 },
566 NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
567 StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None),
568 EndOfLineDownward => (next_line_end(map, point, times), SelectionGoal::None),
569 GoToColumn => (go_to_column(map, point, times), SelectionGoal::None),
570 WindowTop => window_top(map, point, &text_layout_details, times - 1),
571 WindowMiddle => window_middle(map, point, &text_layout_details),
572 WindowBottom => window_bottom(map, point, &text_layout_details, times - 1),
573 };
574
575 (new_point != point || infallible).then_some((new_point, goal))
576 }
577
578 // Expands a selection using self motion for an operator
579 pub fn expand_selection(
580 &self,
581 map: &DisplaySnapshot,
582 selection: &mut Selection<DisplayPoint>,
583 times: Option<usize>,
584 expand_to_surrounding_newline: bool,
585 text_layout_details: &TextLayoutDetails,
586 ) -> bool {
587 if let Some((new_head, goal)) = self.move_point(
588 map,
589 selection.head(),
590 selection.goal,
591 times,
592 &text_layout_details,
593 ) {
594 selection.set_head(new_head, goal);
595
596 if self.linewise() {
597 selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
598
599 if expand_to_surrounding_newline {
600 if selection.end.row() < map.max_point().row() {
601 *selection.end.row_mut() += 1;
602 *selection.end.column_mut() = 0;
603 selection.end = map.clip_point(selection.end, Bias::Right);
604 // Don't reset the end here
605 return true;
606 } else if selection.start.row() > 0 {
607 *selection.start.row_mut() -= 1;
608 *selection.start.column_mut() = map.line_len(selection.start.row());
609 selection.start = map.clip_point(selection.start, Bias::Left);
610 }
611 }
612
613 (_, selection.end) = map.next_line_boundary(selection.end.to_point(map));
614 } else {
615 // Another special case: When using the "w" motion in combination with an
616 // operator and the last word moved over is at the end of a line, the end of
617 // that word becomes the end of the operated text, not the first word in the
618 // next line.
619 if let Motion::NextWordStart {
620 ignore_punctuation: _,
621 } = self
622 {
623 let start_row = selection.start.to_point(&map).row;
624 if selection.end.to_point(&map).row > start_row {
625 selection.end =
626 Point::new(start_row, map.buffer_snapshot.line_len(start_row))
627 .to_display_point(&map)
628 }
629 }
630
631 // If the motion is exclusive and the end of the motion is in column 1, the
632 // end of the motion is moved to the end of the previous line and the motion
633 // becomes inclusive. Example: "}" moves to the first line after a paragraph,
634 // but "d}" will not include that line.
635 let mut inclusive = self.inclusive();
636 if !inclusive
637 && self != &Motion::Backspace
638 && selection.end.row() > selection.start.row()
639 && selection.end.column() == 0
640 {
641 inclusive = true;
642 *selection.end.row_mut() -= 1;
643 *selection.end.column_mut() = 0;
644 selection.end = map.clip_point(
645 map.next_line_boundary(selection.end.to_point(map)).1,
646 Bias::Left,
647 );
648 }
649
650 if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
651 *selection.end.column_mut() += 1;
652 }
653 }
654 true
655 } else {
656 false
657 }
658 }
659}
660
661fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
662 for _ in 0..times {
663 point = movement::saturating_left(map, point);
664 if point.column() == 0 {
665 break;
666 }
667 }
668 point
669}
670
671fn backspace(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
672 for _ in 0..times {
673 point = movement::left(map, point);
674 if point.is_zero() {
675 break;
676 }
677 }
678 point
679}
680
681fn space(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
682 for _ in 0..times {
683 point = wrapping_right(map, point);
684 if point == map.max_point() {
685 break;
686 }
687 }
688 point
689}
690
691fn wrapping_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
692 let max_column = map.line_len(point.row()).saturating_sub(1);
693 if point.column() < max_column {
694 *point.column_mut() += 1;
695 } else if point.row() < map.max_point().row() {
696 *point.row_mut() += 1;
697 *point.column_mut() = 0;
698 }
699 point
700}
701
702pub(crate) fn start_of_relative_buffer_row(
703 map: &DisplaySnapshot,
704 point: DisplayPoint,
705 times: isize,
706) -> DisplayPoint {
707 let start = map.display_point_to_fold_point(point, Bias::Left);
708 let target = start.row() as isize + times;
709 let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
710
711 map.clip_point(
712 map.fold_point_to_display_point(
713 map.fold_snapshot
714 .clip_point(FoldPoint::new(new_row, 0), Bias::Right),
715 ),
716 Bias::Right,
717 )
718}
719
720fn up_down_buffer_rows(
721 map: &DisplaySnapshot,
722 point: DisplayPoint,
723 mut goal: SelectionGoal,
724 times: isize,
725 text_layout_details: &TextLayoutDetails,
726) -> (DisplayPoint, SelectionGoal) {
727 let start = map.display_point_to_fold_point(point, Bias::Left);
728 let begin_folded_line = map.fold_point_to_display_point(
729 map.fold_snapshot
730 .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
731 );
732 let select_nth_wrapped_row = point.row() - begin_folded_line.row();
733
734 let (goal_wrap, goal_x) = match goal {
735 SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
736 SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
737 SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
738 _ => {
739 let x = map.x_for_display_point(point, text_layout_details);
740 goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x.0));
741 (select_nth_wrapped_row, x.0)
742 }
743 };
744
745 let target = start.row() as isize + times;
746 let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
747
748 let mut begin_folded_line = map.fold_point_to_display_point(
749 map.fold_snapshot
750 .clip_point(FoldPoint::new(new_row, 0), Bias::Left),
751 );
752
753 let mut i = 0;
754 while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
755 let next_folded_line = DisplayPoint::new(begin_folded_line.row() + 1, 0);
756 if map
757 .display_point_to_fold_point(next_folded_line, Bias::Right)
758 .row()
759 == new_row
760 {
761 i += 1;
762 begin_folded_line = next_folded_line;
763 } else {
764 break;
765 }
766 }
767
768 let new_col = if i == goal_wrap {
769 map.display_column_for_x(begin_folded_line.row(), px(goal_x), text_layout_details)
770 } else {
771 map.line_len(begin_folded_line.row())
772 };
773
774 (
775 map.clip_point(
776 DisplayPoint::new(begin_folded_line.row(), new_col),
777 Bias::Left,
778 ),
779 goal,
780 )
781}
782
783fn down_display(
784 map: &DisplaySnapshot,
785 mut point: DisplayPoint,
786 mut goal: SelectionGoal,
787 times: usize,
788 text_layout_details: &TextLayoutDetails,
789) -> (DisplayPoint, SelectionGoal) {
790 for _ in 0..times {
791 (point, goal) = movement::down(map, point, goal, true, text_layout_details);
792 }
793
794 (point, goal)
795}
796
797fn up_display(
798 map: &DisplaySnapshot,
799 mut point: DisplayPoint,
800 mut goal: SelectionGoal,
801 times: usize,
802 text_layout_details: &TextLayoutDetails,
803) -> (DisplayPoint, SelectionGoal) {
804 for _ in 0..times {
805 (point, goal) = movement::up(map, point, goal, true, &text_layout_details);
806 }
807
808 (point, goal)
809}
810
811pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
812 for _ in 0..times {
813 let new_point = movement::saturating_right(map, point);
814 if point == new_point {
815 break;
816 }
817 point = new_point;
818 }
819 point
820}
821
822pub(crate) fn next_word_start(
823 map: &DisplaySnapshot,
824 mut point: DisplayPoint,
825 ignore_punctuation: bool,
826 times: usize,
827) -> DisplayPoint {
828 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
829 for _ in 0..times {
830 let mut crossed_newline = false;
831 let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
832 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
833 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
834 let at_newline = right == '\n';
835
836 let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
837 || at_newline && crossed_newline
838 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
839
840 crossed_newline |= at_newline;
841 found
842 });
843 if point == new_point {
844 break;
845 }
846 point = new_point;
847 }
848 point
849}
850
851fn next_word_end(
852 map: &DisplaySnapshot,
853 mut point: DisplayPoint,
854 ignore_punctuation: bool,
855 times: usize,
856) -> DisplayPoint {
857 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
858 for _ in 0..times {
859 let mut new_point = point;
860 if new_point.column() < map.line_len(new_point.row()) {
861 *new_point.column_mut() += 1;
862 } else if new_point < map.max_point() {
863 *new_point.row_mut() += 1;
864 *new_point.column_mut() = 0;
865 }
866
867 let new_point = movement::find_boundary_exclusive(
868 map,
869 new_point,
870 FindRange::MultiLine,
871 |left, right| {
872 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
873 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
874
875 left_kind != right_kind && left_kind != CharKind::Whitespace
876 },
877 );
878 let new_point = map.clip_point(new_point, Bias::Left);
879 if point == new_point {
880 break;
881 }
882 point = new_point;
883 }
884 point
885}
886
887fn previous_word_start(
888 map: &DisplaySnapshot,
889 mut point: DisplayPoint,
890 ignore_punctuation: bool,
891 times: usize,
892) -> DisplayPoint {
893 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
894 for _ in 0..times {
895 // This works even though find_preceding_boundary is called for every character in the line containing
896 // cursor because the newline is checked only once.
897 let new_point = movement::find_preceding_boundary_display_point(
898 map,
899 point,
900 FindRange::MultiLine,
901 |left, right| {
902 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
903 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
904
905 (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
906 },
907 );
908 if point == new_point {
909 break;
910 }
911 point = new_point;
912 }
913 point
914}
915
916pub(crate) fn first_non_whitespace(
917 map: &DisplaySnapshot,
918 display_lines: bool,
919 from: DisplayPoint,
920) -> DisplayPoint {
921 let mut last_point = start_of_line(map, display_lines, from);
922 let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
923 for (ch, point) in map.chars_at(last_point) {
924 if ch == '\n' {
925 return from;
926 }
927
928 last_point = point;
929
930 if char_kind(&scope, ch) != CharKind::Whitespace {
931 break;
932 }
933 }
934
935 map.clip_point(last_point, Bias::Left)
936}
937
938pub(crate) fn start_of_line(
939 map: &DisplaySnapshot,
940 display_lines: bool,
941 point: DisplayPoint,
942) -> DisplayPoint {
943 if display_lines {
944 map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
945 } else {
946 map.prev_line_boundary(point.to_point(map)).1
947 }
948}
949
950pub(crate) fn end_of_line(
951 map: &DisplaySnapshot,
952 display_lines: bool,
953 mut point: DisplayPoint,
954 times: usize,
955) -> DisplayPoint {
956 if times > 1 {
957 point = start_of_relative_buffer_row(map, point, times as isize - 1);
958 }
959 if display_lines {
960 map.clip_point(
961 DisplayPoint::new(point.row(), map.line_len(point.row())),
962 Bias::Left,
963 )
964 } else {
965 map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
966 }
967}
968
969fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
970 let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
971 *new_point.column_mut() = point.column();
972 map.clip_point(new_point, Bias::Left)
973}
974
975fn end_of_document(
976 map: &DisplaySnapshot,
977 point: DisplayPoint,
978 line: Option<usize>,
979) -> DisplayPoint {
980 let new_row = if let Some(line) = line {
981 (line - 1) as u32
982 } else {
983 map.max_buffer_row()
984 };
985
986 let new_point = Point::new(new_row, point.column());
987 map.clip_point(new_point.to_display_point(map), Bias::Left)
988}
989
990fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
991 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
992 let point = display_point.to_point(map);
993 let offset = point.to_offset(&map.buffer_snapshot);
994
995 // Ensure the range is contained by the current line.
996 let mut line_end = map.next_line_boundary(point).0;
997 if line_end == point {
998 line_end = map.max_point().to_point(map);
999 }
1000
1001 let line_range = map.prev_line_boundary(point).0..line_end;
1002 let visible_line_range =
1003 line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
1004 let ranges = map
1005 .buffer_snapshot
1006 .bracket_ranges(visible_line_range.clone());
1007 if let Some(ranges) = ranges {
1008 let line_range = line_range.start.to_offset(&map.buffer_snapshot)
1009 ..line_range.end.to_offset(&map.buffer_snapshot);
1010 let mut closest_pair_destination = None;
1011 let mut closest_distance = usize::MAX;
1012
1013 for (open_range, close_range) in ranges {
1014 if open_range.start >= offset && line_range.contains(&open_range.start) {
1015 let distance = open_range.start - offset;
1016 if distance < closest_distance {
1017 closest_pair_destination = Some(close_range.start);
1018 closest_distance = distance;
1019 continue;
1020 }
1021 }
1022
1023 if close_range.start >= offset && line_range.contains(&close_range.start) {
1024 let distance = close_range.start - offset;
1025 if distance < closest_distance {
1026 closest_pair_destination = Some(open_range.start);
1027 closest_distance = distance;
1028 continue;
1029 }
1030 }
1031
1032 continue;
1033 }
1034
1035 closest_pair_destination
1036 .map(|destination| destination.to_display_point(map))
1037 .unwrap_or(display_point)
1038 } else {
1039 display_point
1040 }
1041}
1042
1043fn find_forward(
1044 map: &DisplaySnapshot,
1045 from: DisplayPoint,
1046 before: bool,
1047 target: char,
1048 times: usize,
1049 mode: FindRange,
1050) -> Option<DisplayPoint> {
1051 let mut to = from;
1052 let mut found = false;
1053
1054 for _ in 0..times {
1055 found = false;
1056 let new_to = find_boundary(map, to, mode, |_, right| {
1057 found = right == target;
1058 found
1059 });
1060 if to == new_to {
1061 break;
1062 }
1063 to = new_to;
1064 }
1065
1066 if found {
1067 if before && to.column() > 0 {
1068 *to.column_mut() -= 1;
1069 Some(map.clip_point(to, Bias::Left))
1070 } else {
1071 Some(to)
1072 }
1073 } else {
1074 None
1075 }
1076}
1077
1078fn find_backward(
1079 map: &DisplaySnapshot,
1080 from: DisplayPoint,
1081 after: bool,
1082 target: char,
1083 times: usize,
1084 mode: FindRange,
1085) -> DisplayPoint {
1086 let mut to = from;
1087
1088 for _ in 0..times {
1089 let new_to =
1090 find_preceding_boundary_display_point(map, to, mode, |_, right| right == target);
1091 if to == new_to {
1092 break;
1093 }
1094 to = new_to;
1095 }
1096
1097 if map.buffer_snapshot.chars_at(to.to_point(map)).next() == Some(target) {
1098 if after {
1099 *to.column_mut() += 1;
1100 map.clip_point(to, Bias::Right)
1101 } else {
1102 to
1103 }
1104 } else {
1105 from
1106 }
1107}
1108
1109fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1110 let correct_line = start_of_relative_buffer_row(map, point, times as isize);
1111 first_non_whitespace(map, false, correct_line)
1112}
1113
1114fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1115 let correct_line = start_of_relative_buffer_row(map, point, 0);
1116 right(map, correct_line, times.saturating_sub(1))
1117}
1118
1119pub(crate) fn next_line_end(
1120 map: &DisplaySnapshot,
1121 mut point: DisplayPoint,
1122 times: usize,
1123) -> DisplayPoint {
1124 if times > 1 {
1125 point = start_of_relative_buffer_row(map, point, times as isize - 1);
1126 }
1127 end_of_line(map, false, point, 1)
1128}
1129
1130fn window_top(
1131 map: &DisplaySnapshot,
1132 point: DisplayPoint,
1133 text_layout_details: &TextLayoutDetails,
1134 mut times: usize,
1135) -> (DisplayPoint, SelectionGoal) {
1136 let first_visible_line = text_layout_details
1137 .scroll_anchor
1138 .anchor
1139 .to_display_point(map);
1140
1141 if first_visible_line.row() != 0 && text_layout_details.vertical_scroll_margin as usize > times
1142 {
1143 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1144 }
1145
1146 if let Some(visible_rows) = text_layout_details.visible_rows {
1147 let bottom_row = first_visible_line.row() + visible_rows as u32;
1148 let new_row = (first_visible_line.row() + (times as u32))
1149 .min(bottom_row)
1150 .min(map.max_point().row());
1151 let new_col = point.column().min(map.line_len(first_visible_line.row()));
1152
1153 let new_point = DisplayPoint::new(new_row, new_col);
1154 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1155 } else {
1156 let new_row = (first_visible_line.row() + (times as u32)).min(map.max_point().row());
1157 let new_col = point.column().min(map.line_len(first_visible_line.row()));
1158
1159 let new_point = DisplayPoint::new(new_row, new_col);
1160 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1161 }
1162}
1163
1164fn window_middle(
1165 map: &DisplaySnapshot,
1166 point: DisplayPoint,
1167 text_layout_details: &TextLayoutDetails,
1168) -> (DisplayPoint, SelectionGoal) {
1169 if let Some(visible_rows) = text_layout_details.visible_rows {
1170 let first_visible_line = text_layout_details
1171 .scroll_anchor
1172 .anchor
1173 .to_display_point(map);
1174
1175 let max_visible_rows =
1176 (visible_rows as u32).min(map.max_point().row() - first_visible_line.row());
1177
1178 let new_row =
1179 (first_visible_line.row() + (max_visible_rows / 2)).min(map.max_point().row());
1180 let new_col = point.column().min(map.line_len(new_row));
1181 let new_point = DisplayPoint::new(new_row, new_col);
1182 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1183 } else {
1184 (point, SelectionGoal::None)
1185 }
1186}
1187
1188fn window_bottom(
1189 map: &DisplaySnapshot,
1190 point: DisplayPoint,
1191 text_layout_details: &TextLayoutDetails,
1192 mut times: usize,
1193) -> (DisplayPoint, SelectionGoal) {
1194 if let Some(visible_rows) = text_layout_details.visible_rows {
1195 let first_visible_line = text_layout_details
1196 .scroll_anchor
1197 .anchor
1198 .to_display_point(map);
1199 let bottom_row = first_visible_line.row()
1200 + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
1201 if bottom_row < map.max_point().row()
1202 && text_layout_details.vertical_scroll_margin as usize > times
1203 {
1204 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1205 }
1206 let bottom_row_capped = bottom_row.min(map.max_point().row());
1207 let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row() {
1208 first_visible_line.row()
1209 } else {
1210 bottom_row_capped.saturating_sub(times as u32)
1211 };
1212 let new_col = point.column().min(map.line_len(new_row));
1213 let new_point = DisplayPoint::new(new_row, new_col);
1214 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1215 } else {
1216 (point, SelectionGoal::None)
1217 }
1218}
1219
1220fn previous_word_end(
1221 map: &DisplaySnapshot,
1222 point: DisplayPoint,
1223 ignore_punctuation: bool,
1224 times: usize,
1225) -> DisplayPoint {
1226 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1227 let mut point = point.to_point(map);
1228
1229 if point.column < map.buffer_snapshot.line_len(point.row) {
1230 point.column += 1;
1231 }
1232 for _ in 0..times {
1233 let new_point = movement::find_preceding_boundary_point(
1234 &map.buffer_snapshot,
1235 point,
1236 FindRange::MultiLine,
1237 |left, right| {
1238 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1239 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1240 match (left_kind, right_kind) {
1241 (CharKind::Punctuation, CharKind::Whitespace)
1242 | (CharKind::Punctuation, CharKind::Word)
1243 | (CharKind::Word, CharKind::Whitespace)
1244 | (CharKind::Word, CharKind::Punctuation) => true,
1245 (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1246 _ => false,
1247 }
1248 },
1249 );
1250 if new_point == point {
1251 break;
1252 }
1253 point = new_point;
1254 }
1255 movement::saturating_left(map, point.to_display_point(map))
1256}
1257
1258#[cfg(test)]
1259mod test {
1260
1261 use crate::test::NeovimBackedTestContext;
1262 use indoc::indoc;
1263
1264 #[gpui::test]
1265 async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
1266 let mut cx = NeovimBackedTestContext::new(cx).await;
1267
1268 let initial_state = indoc! {r"ˇabc
1269 def
1270
1271 paragraph
1272 the second
1273
1274
1275
1276 third and
1277 final"};
1278
1279 // goes down once
1280 cx.set_shared_state(initial_state).await;
1281 cx.simulate_shared_keystrokes(["}"]).await;
1282 cx.assert_shared_state(indoc! {r"abc
1283 def
1284 ˇ
1285 paragraph
1286 the second
1287
1288
1289
1290 third and
1291 final"})
1292 .await;
1293
1294 // goes up once
1295 cx.simulate_shared_keystrokes(["{"]).await;
1296 cx.assert_shared_state(initial_state).await;
1297
1298 // goes down twice
1299 cx.simulate_shared_keystrokes(["2", "}"]).await;
1300 cx.assert_shared_state(indoc! {r"abc
1301 def
1302
1303 paragraph
1304 the second
1305 ˇ
1306
1307
1308 third and
1309 final"})
1310 .await;
1311
1312 // goes down over multiple blanks
1313 cx.simulate_shared_keystrokes(["}"]).await;
1314 cx.assert_shared_state(indoc! {r"abc
1315 def
1316
1317 paragraph
1318 the second
1319
1320
1321
1322 third and
1323 finaˇl"})
1324 .await;
1325
1326 // goes up twice
1327 cx.simulate_shared_keystrokes(["2", "{"]).await;
1328 cx.assert_shared_state(indoc! {r"abc
1329 def
1330 ˇ
1331 paragraph
1332 the second
1333
1334
1335
1336 third and
1337 final"})
1338 .await
1339 }
1340
1341 #[gpui::test]
1342 async fn test_matching(cx: &mut gpui::TestAppContext) {
1343 let mut cx = NeovimBackedTestContext::new(cx).await;
1344
1345 cx.set_shared_state(indoc! {r"func ˇ(a string) {
1346 do(something(with<Types>.and_arrays[0, 2]))
1347 }"})
1348 .await;
1349 cx.simulate_shared_keystrokes(["%"]).await;
1350 cx.assert_shared_state(indoc! {r"func (a stringˇ) {
1351 do(something(with<Types>.and_arrays[0, 2]))
1352 }"})
1353 .await;
1354
1355 // test it works on the last character of the line
1356 cx.set_shared_state(indoc! {r"func (a string) ˇ{
1357 do(something(with<Types>.and_arrays[0, 2]))
1358 }"})
1359 .await;
1360 cx.simulate_shared_keystrokes(["%"]).await;
1361 cx.assert_shared_state(indoc! {r"func (a string) {
1362 do(something(with<Types>.and_arrays[0, 2]))
1363 ˇ}"})
1364 .await;
1365
1366 // test it works on immediate nesting
1367 cx.set_shared_state("ˇ{()}").await;
1368 cx.simulate_shared_keystrokes(["%"]).await;
1369 cx.assert_shared_state("{()ˇ}").await;
1370 cx.simulate_shared_keystrokes(["%"]).await;
1371 cx.assert_shared_state("ˇ{()}").await;
1372
1373 // test it works on immediate nesting inside braces
1374 cx.set_shared_state("{\n ˇ{()}\n}").await;
1375 cx.simulate_shared_keystrokes(["%"]).await;
1376 cx.assert_shared_state("{\n {()ˇ}\n}").await;
1377
1378 // test it jumps to the next paren on a line
1379 cx.set_shared_state("func ˇboop() {\n}").await;
1380 cx.simulate_shared_keystrokes(["%"]).await;
1381 cx.assert_shared_state("func boop(ˇ) {\n}").await;
1382 }
1383
1384 #[gpui::test]
1385 async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
1386 let mut cx = NeovimBackedTestContext::new(cx).await;
1387
1388 // f and F
1389 cx.set_shared_state("ˇone two three four").await;
1390 cx.simulate_shared_keystrokes(["f", "o"]).await;
1391 cx.assert_shared_state("one twˇo three four").await;
1392 cx.simulate_shared_keystrokes([","]).await;
1393 cx.assert_shared_state("ˇone two three four").await;
1394 cx.simulate_shared_keystrokes(["2", ";"]).await;
1395 cx.assert_shared_state("one two three fˇour").await;
1396 cx.simulate_shared_keystrokes(["shift-f", "e"]).await;
1397 cx.assert_shared_state("one two threˇe four").await;
1398 cx.simulate_shared_keystrokes(["2", ";"]).await;
1399 cx.assert_shared_state("onˇe two three four").await;
1400 cx.simulate_shared_keystrokes([","]).await;
1401 cx.assert_shared_state("one two thrˇee four").await;
1402
1403 // t and T
1404 cx.set_shared_state("ˇone two three four").await;
1405 cx.simulate_shared_keystrokes(["t", "o"]).await;
1406 cx.assert_shared_state("one tˇwo three four").await;
1407 cx.simulate_shared_keystrokes([","]).await;
1408 cx.assert_shared_state("oˇne two three four").await;
1409 cx.simulate_shared_keystrokes(["2", ";"]).await;
1410 cx.assert_shared_state("one two three ˇfour").await;
1411 cx.simulate_shared_keystrokes(["shift-t", "e"]).await;
1412 cx.assert_shared_state("one two threeˇ four").await;
1413 cx.simulate_shared_keystrokes(["3", ";"]).await;
1414 cx.assert_shared_state("oneˇ two three four").await;
1415 cx.simulate_shared_keystrokes([","]).await;
1416 cx.assert_shared_state("one two thˇree four").await;
1417 }
1418
1419 #[gpui::test]
1420 async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
1421 let mut cx = NeovimBackedTestContext::new(cx).await;
1422 let initial_state = indoc! {r"something(ˇfoo)"};
1423 cx.set_shared_state(initial_state).await;
1424 cx.simulate_shared_keystrokes(["}"]).await;
1425 cx.assert_shared_state(indoc! {r"something(fooˇ)"}).await;
1426 }
1427
1428 #[gpui::test]
1429 async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
1430 let mut cx = NeovimBackedTestContext::new(cx).await;
1431 cx.set_shared_state("ˇone\n two\nthree").await;
1432 cx.simulate_shared_keystrokes(["enter"]).await;
1433 cx.assert_shared_state("one\n ˇtwo\nthree").await;
1434 }
1435
1436 #[gpui::test]
1437 async fn test_window_top(cx: &mut gpui::TestAppContext) {
1438 let mut cx = NeovimBackedTestContext::new(cx).await;
1439 let initial_state = indoc! {r"abc
1440 def
1441 paragraph
1442 the second
1443 third ˇand
1444 final"};
1445
1446 cx.set_shared_state(initial_state).await;
1447 cx.simulate_shared_keystrokes(["shift-h"]).await;
1448 cx.assert_shared_state(indoc! {r"abˇc
1449 def
1450 paragraph
1451 the second
1452 third and
1453 final"})
1454 .await;
1455
1456 // clip point
1457 cx.set_shared_state(indoc! {r"
1458 1 2 3
1459 4 5 6
1460 7 8 ˇ9
1461 "})
1462 .await;
1463 cx.simulate_shared_keystrokes(["shift-h"]).await;
1464 cx.assert_shared_state(indoc! {r"
1465 1 2 ˇ3
1466 4 5 6
1467 7 8 9
1468 "})
1469 .await;
1470
1471 cx.set_shared_state(indoc! {r"
1472 1 2 3
1473 4 5 6
1474 ˇ7 8 9
1475 "})
1476 .await;
1477 cx.simulate_shared_keystrokes(["shift-h"]).await;
1478 cx.assert_shared_state(indoc! {r"
1479 ˇ1 2 3
1480 4 5 6
1481 7 8 9
1482 "})
1483 .await;
1484
1485 cx.set_shared_state(indoc! {r"
1486 1 2 3
1487 4 5 ˇ6
1488 7 8 9"})
1489 .await;
1490 cx.simulate_shared_keystrokes(["9", "shift-h"]).await;
1491 cx.assert_shared_state(indoc! {r"
1492 1 2 3
1493 4 5 6
1494 7 8 ˇ9"})
1495 .await;
1496 }
1497
1498 #[gpui::test]
1499 async fn test_window_middle(cx: &mut gpui::TestAppContext) {
1500 let mut cx = NeovimBackedTestContext::new(cx).await;
1501 let initial_state = indoc! {r"abˇc
1502 def
1503 paragraph
1504 the second
1505 third and
1506 final"};
1507
1508 cx.set_shared_state(initial_state).await;
1509 cx.simulate_shared_keystrokes(["shift-m"]).await;
1510 cx.assert_shared_state(indoc! {r"abc
1511 def
1512 paˇragraph
1513 the second
1514 third and
1515 final"})
1516 .await;
1517
1518 cx.set_shared_state(indoc! {r"
1519 1 2 3
1520 4 5 6
1521 7 8 ˇ9
1522 "})
1523 .await;
1524 cx.simulate_shared_keystrokes(["shift-m"]).await;
1525 cx.assert_shared_state(indoc! {r"
1526 1 2 3
1527 4 5 ˇ6
1528 7 8 9
1529 "})
1530 .await;
1531 cx.set_shared_state(indoc! {r"
1532 1 2 3
1533 4 5 6
1534 ˇ7 8 9
1535 "})
1536 .await;
1537 cx.simulate_shared_keystrokes(["shift-m"]).await;
1538 cx.assert_shared_state(indoc! {r"
1539 1 2 3
1540 ˇ4 5 6
1541 7 8 9
1542 "})
1543 .await;
1544 cx.set_shared_state(indoc! {r"
1545 ˇ1 2 3
1546 4 5 6
1547 7 8 9
1548 "})
1549 .await;
1550 cx.simulate_shared_keystrokes(["shift-m"]).await;
1551 cx.assert_shared_state(indoc! {r"
1552 1 2 3
1553 ˇ4 5 6
1554 7 8 9
1555 "})
1556 .await;
1557 cx.set_shared_state(indoc! {r"
1558 1 2 3
1559 ˇ4 5 6
1560 7 8 9
1561 "})
1562 .await;
1563 cx.simulate_shared_keystrokes(["shift-m"]).await;
1564 cx.assert_shared_state(indoc! {r"
1565 1 2 3
1566 ˇ4 5 6
1567 7 8 9
1568 "})
1569 .await;
1570 cx.set_shared_state(indoc! {r"
1571 1 2 3
1572 4 5 ˇ6
1573 7 8 9
1574 "})
1575 .await;
1576 cx.simulate_shared_keystrokes(["shift-m"]).await;
1577 cx.assert_shared_state(indoc! {r"
1578 1 2 3
1579 4 5 ˇ6
1580 7 8 9
1581 "})
1582 .await;
1583 }
1584
1585 #[gpui::test]
1586 async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
1587 let mut cx = NeovimBackedTestContext::new(cx).await;
1588 let initial_state = indoc! {r"abc
1589 deˇf
1590 paragraph
1591 the second
1592 third and
1593 final"};
1594
1595 cx.set_shared_state(initial_state).await;
1596 cx.simulate_shared_keystrokes(["shift-l"]).await;
1597 cx.assert_shared_state(indoc! {r"abc
1598 def
1599 paragraph
1600 the second
1601 third and
1602 fiˇnal"})
1603 .await;
1604
1605 cx.set_shared_state(indoc! {r"
1606 1 2 3
1607 4 5 ˇ6
1608 7 8 9
1609 "})
1610 .await;
1611 cx.simulate_shared_keystrokes(["shift-l"]).await;
1612 cx.assert_shared_state(indoc! {r"
1613 1 2 3
1614 4 5 6
1615 7 8 9
1616 ˇ"})
1617 .await;
1618
1619 cx.set_shared_state(indoc! {r"
1620 1 2 3
1621 ˇ4 5 6
1622 7 8 9
1623 "})
1624 .await;
1625 cx.simulate_shared_keystrokes(["shift-l"]).await;
1626 cx.assert_shared_state(indoc! {r"
1627 1 2 3
1628 4 5 6
1629 7 8 9
1630 ˇ"})
1631 .await;
1632
1633 cx.set_shared_state(indoc! {r"
1634 1 2 ˇ3
1635 4 5 6
1636 7 8 9
1637 "})
1638 .await;
1639 cx.simulate_shared_keystrokes(["shift-l"]).await;
1640 cx.assert_shared_state(indoc! {r"
1641 1 2 3
1642 4 5 6
1643 7 8 9
1644 ˇ"})
1645 .await;
1646
1647 cx.set_shared_state(indoc! {r"
1648 ˇ1 2 3
1649 4 5 6
1650 7 8 9
1651 "})
1652 .await;
1653 cx.simulate_shared_keystrokes(["shift-l"]).await;
1654 cx.assert_shared_state(indoc! {r"
1655 1 2 3
1656 4 5 6
1657 7 8 9
1658 ˇ"})
1659 .await;
1660
1661 cx.set_shared_state(indoc! {r"
1662 1 2 3
1663 4 5 ˇ6
1664 7 8 9
1665 "})
1666 .await;
1667 cx.simulate_shared_keystrokes(["9", "shift-l"]).await;
1668 cx.assert_shared_state(indoc! {r"
1669 1 2 ˇ3
1670 4 5 6
1671 7 8 9
1672 "})
1673 .await;
1674 }
1675
1676 #[gpui::test]
1677 async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
1678 let mut cx = NeovimBackedTestContext::new(cx).await;
1679 cx.set_shared_state(indoc! {r"
1680 456 5ˇ67 678
1681 "})
1682 .await;
1683 cx.simulate_shared_keystrokes(["g", "e"]).await;
1684 cx.assert_shared_state(indoc! {r"
1685 45ˇ6 567 678
1686 "})
1687 .await;
1688
1689 // Test times
1690 cx.set_shared_state(indoc! {r"
1691 123 234 345
1692 456 5ˇ67 678
1693 "})
1694 .await;
1695 cx.simulate_shared_keystrokes(["4", "g", "e"]).await;
1696 cx.assert_shared_state(indoc! {r"
1697 12ˇ3 234 345
1698 456 567 678
1699 "})
1700 .await;
1701
1702 // With punctuation
1703 cx.set_shared_state(indoc! {r"
1704 123 234 345
1705 4;5.6 5ˇ67 678
1706 789 890 901
1707 "})
1708 .await;
1709 cx.simulate_shared_keystrokes(["g", "e"]).await;
1710 cx.assert_shared_state(indoc! {r"
1711 123 234 345
1712 4;5.ˇ6 567 678
1713 789 890 901
1714 "})
1715 .await;
1716
1717 // With punctuation and count
1718 cx.set_shared_state(indoc! {r"
1719 123 234 345
1720 4;5.6 5ˇ67 678
1721 789 890 901
1722 "})
1723 .await;
1724 cx.simulate_shared_keystrokes(["5", "g", "e"]).await;
1725 cx.assert_shared_state(indoc! {r"
1726 123 234 345
1727 ˇ4;5.6 567 678
1728 789 890 901
1729 "})
1730 .await;
1731
1732 // newlines
1733 cx.set_shared_state(indoc! {r"
1734 123 234 345
1735
1736 78ˇ9 890 901
1737 "})
1738 .await;
1739 cx.simulate_shared_keystrokes(["g", "e"]).await;
1740 cx.assert_shared_state(indoc! {r"
1741 123 234 345
1742 ˇ
1743 789 890 901
1744 "})
1745 .await;
1746 cx.simulate_shared_keystrokes(["g", "e"]).await;
1747 cx.assert_shared_state(indoc! {r"
1748 123 234 34ˇ5
1749
1750 789 890 901
1751 "})
1752 .await;
1753
1754 // With punctuation
1755 cx.set_shared_state(indoc! {r"
1756 123 234 345
1757 4;5.ˇ6 567 678
1758 789 890 901
1759 "})
1760 .await;
1761 cx.simulate_shared_keystrokes(["g", "shift-e"]).await;
1762 cx.assert_shared_state(indoc! {r"
1763 123 234 34ˇ5
1764 4;5.6 567 678
1765 789 890 901
1766 "})
1767 .await;
1768 }
1769}