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