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.row() < map.max_buffer_row() {
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)).min(bottom_row);
1114 let new_col = point.column().min(map.line_len(first_visible_line.row()));
1115
1116 let new_point = DisplayPoint::new(new_row, new_col);
1117 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1118 } else {
1119 let new_row = first_visible_line.row() + (times as u32);
1120 let new_col = point.column().min(map.line_len(first_visible_line.row()));
1121
1122 let new_point = DisplayPoint::new(new_row, new_col);
1123 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1124 }
1125}
1126
1127fn window_middle(
1128 map: &DisplaySnapshot,
1129 point: DisplayPoint,
1130 text_layout_details: &TextLayoutDetails,
1131) -> (DisplayPoint, SelectionGoal) {
1132 if let Some(visible_rows) = text_layout_details.visible_rows {
1133 let first_visible_line = text_layout_details
1134 .scroll_anchor
1135 .anchor
1136 .to_display_point(map);
1137 let max_rows = (visible_rows as u32).min(map.max_buffer_row());
1138 let new_row = first_visible_line.row() + (max_rows.div_euclid(2));
1139 let new_col = point.column().min(map.line_len(new_row));
1140 let new_point = DisplayPoint::new(new_row, new_col);
1141 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1142 } else {
1143 (point, SelectionGoal::None)
1144 }
1145}
1146
1147fn window_bottom(
1148 map: &DisplaySnapshot,
1149 point: DisplayPoint,
1150 text_layout_details: &TextLayoutDetails,
1151 mut times: usize,
1152) -> (DisplayPoint, SelectionGoal) {
1153 if let Some(visible_rows) = text_layout_details.visible_rows {
1154 let first_visible_line = text_layout_details
1155 .scroll_anchor
1156 .anchor
1157 .to_display_point(map);
1158 let bottom_row = first_visible_line.row()
1159 + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
1160 if bottom_row < map.max_buffer_row()
1161 && text_layout_details.vertical_scroll_margin as usize > times
1162 {
1163 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1164 }
1165 let bottom_row_capped = bottom_row.min(map.max_buffer_row());
1166 let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row() {
1167 first_visible_line.row()
1168 } else {
1169 bottom_row_capped.saturating_sub(times as u32)
1170 };
1171 let new_col = point.column().min(map.line_len(new_row));
1172 let new_point = DisplayPoint::new(new_row, new_col);
1173 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1174 } else {
1175 (point, SelectionGoal::None)
1176 }
1177}
1178
1179fn previous_word_end(
1180 map: &DisplaySnapshot,
1181 point: DisplayPoint,
1182 ignore_punctuation: bool,
1183 times: usize,
1184) -> DisplayPoint {
1185 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1186 let mut point = point.to_point(map);
1187
1188 if point.column < map.buffer_snapshot.line_len(point.row) {
1189 point.column += 1;
1190 }
1191 for _ in 0..times {
1192 let new_point = movement::find_preceding_boundary_point(
1193 &map.buffer_snapshot,
1194 point,
1195 FindRange::MultiLine,
1196 |left, right| {
1197 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1198 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1199 match (left_kind, right_kind) {
1200 (CharKind::Punctuation, CharKind::Whitespace)
1201 | (CharKind::Punctuation, CharKind::Word)
1202 | (CharKind::Word, CharKind::Whitespace)
1203 | (CharKind::Word, CharKind::Punctuation) => true,
1204 (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1205 _ => false,
1206 }
1207 },
1208 );
1209 if new_point == point {
1210 break;
1211 }
1212 point = new_point;
1213 }
1214 movement::saturating_left(map, point.to_display_point(map))
1215}
1216
1217#[cfg(test)]
1218mod test {
1219
1220 use crate::test::NeovimBackedTestContext;
1221 use indoc::indoc;
1222
1223 #[gpui::test]
1224 async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
1225 let mut cx = NeovimBackedTestContext::new(cx).await;
1226
1227 let initial_state = indoc! {r"ˇabc
1228 def
1229
1230 paragraph
1231 the second
1232
1233
1234
1235 third and
1236 final"};
1237
1238 // goes down once
1239 cx.set_shared_state(initial_state).await;
1240 cx.simulate_shared_keystrokes(["}"]).await;
1241 cx.assert_shared_state(indoc! {r"abc
1242 def
1243 ˇ
1244 paragraph
1245 the second
1246
1247
1248
1249 third and
1250 final"})
1251 .await;
1252
1253 // goes up once
1254 cx.simulate_shared_keystrokes(["{"]).await;
1255 cx.assert_shared_state(initial_state).await;
1256
1257 // goes down twice
1258 cx.simulate_shared_keystrokes(["2", "}"]).await;
1259 cx.assert_shared_state(indoc! {r"abc
1260 def
1261
1262 paragraph
1263 the second
1264 ˇ
1265
1266
1267 third and
1268 final"})
1269 .await;
1270
1271 // goes down over multiple blanks
1272 cx.simulate_shared_keystrokes(["}"]).await;
1273 cx.assert_shared_state(indoc! {r"abc
1274 def
1275
1276 paragraph
1277 the second
1278
1279
1280
1281 third and
1282 finaˇl"})
1283 .await;
1284
1285 // goes up twice
1286 cx.simulate_shared_keystrokes(["2", "{"]).await;
1287 cx.assert_shared_state(indoc! {r"abc
1288 def
1289 ˇ
1290 paragraph
1291 the second
1292
1293
1294
1295 third and
1296 final"})
1297 .await
1298 }
1299
1300 #[gpui::test]
1301 async fn test_matching(cx: &mut gpui::TestAppContext) {
1302 let mut cx = NeovimBackedTestContext::new(cx).await;
1303
1304 cx.set_shared_state(indoc! {r"func ˇ(a string) {
1305 do(something(with<Types>.and_arrays[0, 2]))
1306 }"})
1307 .await;
1308 cx.simulate_shared_keystrokes(["%"]).await;
1309 cx.assert_shared_state(indoc! {r"func (a stringˇ) {
1310 do(something(with<Types>.and_arrays[0, 2]))
1311 }"})
1312 .await;
1313
1314 // test it works on the last character of the line
1315 cx.set_shared_state(indoc! {r"func (a string) ˇ{
1316 do(something(with<Types>.and_arrays[0, 2]))
1317 }"})
1318 .await;
1319 cx.simulate_shared_keystrokes(["%"]).await;
1320 cx.assert_shared_state(indoc! {r"func (a string) {
1321 do(something(with<Types>.and_arrays[0, 2]))
1322 ˇ}"})
1323 .await;
1324
1325 // test it works on immediate nesting
1326 cx.set_shared_state("ˇ{()}").await;
1327 cx.simulate_shared_keystrokes(["%"]).await;
1328 cx.assert_shared_state("{()ˇ}").await;
1329 cx.simulate_shared_keystrokes(["%"]).await;
1330 cx.assert_shared_state("ˇ{()}").await;
1331
1332 // test it works on immediate nesting inside braces
1333 cx.set_shared_state("{\n ˇ{()}\n}").await;
1334 cx.simulate_shared_keystrokes(["%"]).await;
1335 cx.assert_shared_state("{\n {()ˇ}\n}").await;
1336
1337 // test it jumps to the next paren on a line
1338 cx.set_shared_state("func ˇboop() {\n}").await;
1339 cx.simulate_shared_keystrokes(["%"]).await;
1340 cx.assert_shared_state("func boop(ˇ) {\n}").await;
1341 }
1342
1343 #[gpui::test]
1344 async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
1345 let mut cx = NeovimBackedTestContext::new(cx).await;
1346
1347 // f and F
1348 cx.set_shared_state("ˇone two three four").await;
1349 cx.simulate_shared_keystrokes(["f", "o"]).await;
1350 cx.assert_shared_state("one twˇo three four").await;
1351 cx.simulate_shared_keystrokes([","]).await;
1352 cx.assert_shared_state("ˇone two three four").await;
1353 cx.simulate_shared_keystrokes(["2", ";"]).await;
1354 cx.assert_shared_state("one two three fˇour").await;
1355 cx.simulate_shared_keystrokes(["shift-f", "e"]).await;
1356 cx.assert_shared_state("one two threˇe four").await;
1357 cx.simulate_shared_keystrokes(["2", ";"]).await;
1358 cx.assert_shared_state("onˇe two three four").await;
1359 cx.simulate_shared_keystrokes([","]).await;
1360 cx.assert_shared_state("one two thrˇee four").await;
1361
1362 // t and T
1363 cx.set_shared_state("ˇone two three four").await;
1364 cx.simulate_shared_keystrokes(["t", "o"]).await;
1365 cx.assert_shared_state("one tˇwo three four").await;
1366 cx.simulate_shared_keystrokes([","]).await;
1367 cx.assert_shared_state("oˇne two three four").await;
1368 cx.simulate_shared_keystrokes(["2", ";"]).await;
1369 cx.assert_shared_state("one two three ˇfour").await;
1370 cx.simulate_shared_keystrokes(["shift-t", "e"]).await;
1371 cx.assert_shared_state("one two threeˇ four").await;
1372 cx.simulate_shared_keystrokes(["3", ";"]).await;
1373 cx.assert_shared_state("oneˇ two three four").await;
1374 cx.simulate_shared_keystrokes([","]).await;
1375 cx.assert_shared_state("one two thˇree four").await;
1376 }
1377
1378 #[gpui::test]
1379 async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
1380 let mut cx = NeovimBackedTestContext::new(cx).await;
1381 let initial_state = indoc! {r"something(ˇfoo)"};
1382 cx.set_shared_state(initial_state).await;
1383 cx.simulate_shared_keystrokes(["}"]).await;
1384 cx.assert_shared_state(indoc! {r"something(fooˇ)"}).await;
1385 }
1386
1387 #[gpui::test]
1388 async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
1389 let mut cx = NeovimBackedTestContext::new(cx).await;
1390 cx.set_shared_state("ˇone\n two\nthree").await;
1391 cx.simulate_shared_keystrokes(["enter"]).await;
1392 cx.assert_shared_state("one\n ˇtwo\nthree").await;
1393 }
1394
1395 #[gpui::test]
1396 async fn test_window_top(cx: &mut gpui::TestAppContext) {
1397 let mut cx = NeovimBackedTestContext::new(cx).await;
1398 let initial_state = indoc! {r"abc
1399 def
1400 paragraph
1401 the second
1402 third ˇand
1403 final"};
1404
1405 cx.set_shared_state(initial_state).await;
1406 cx.simulate_shared_keystrokes(["shift-h"]).await;
1407 cx.assert_shared_state(indoc! {r"abˇc
1408 def
1409 paragraph
1410 the second
1411 third and
1412 final"})
1413 .await;
1414
1415 // clip point
1416 cx.set_shared_state(indoc! {r"
1417 1 2 3
1418 4 5 6
1419 7 8 ˇ9
1420 "})
1421 .await;
1422 cx.simulate_shared_keystrokes(["shift-h"]).await;
1423 cx.assert_shared_state(indoc! {r"
1424 1 2 ˇ3
1425 4 5 6
1426 7 8 9
1427 "})
1428 .await;
1429
1430 cx.set_shared_state(indoc! {r"
1431 1 2 3
1432 4 5 6
1433 ˇ7 8 9
1434 "})
1435 .await;
1436 cx.simulate_shared_keystrokes(["shift-h"]).await;
1437 cx.assert_shared_state(indoc! {r"
1438 ˇ1 2 3
1439 4 5 6
1440 7 8 9
1441 "})
1442 .await;
1443
1444 cx.set_shared_state(indoc! {r"
1445 1 2 3
1446 4 5 ˇ6
1447 7 8 9"})
1448 .await;
1449 cx.simulate_shared_keystrokes(["9", "shift-h"]).await;
1450 cx.assert_shared_state(indoc! {r"
1451 1 2 3
1452 4 5 6
1453 7 8 ˇ9"})
1454 .await;
1455 }
1456
1457 #[gpui::test]
1458 async fn test_window_middle(cx: &mut gpui::TestAppContext) {
1459 let mut cx = NeovimBackedTestContext::new(cx).await;
1460 let initial_state = indoc! {r"abˇc
1461 def
1462 paragraph
1463 the second
1464 third and
1465 final"};
1466
1467 cx.set_shared_state(initial_state).await;
1468 cx.simulate_shared_keystrokes(["shift-m"]).await;
1469 cx.assert_shared_state(indoc! {r"abc
1470 def
1471 paˇragraph
1472 the second
1473 third and
1474 final"})
1475 .await;
1476
1477 cx.set_shared_state(indoc! {r"
1478 1 2 3
1479 4 5 6
1480 7 8 ˇ9
1481 "})
1482 .await;
1483 cx.simulate_shared_keystrokes(["shift-m"]).await;
1484 cx.assert_shared_state(indoc! {r"
1485 1 2 3
1486 4 5 ˇ6
1487 7 8 9
1488 "})
1489 .await;
1490 cx.set_shared_state(indoc! {r"
1491 1 2 3
1492 4 5 6
1493 ˇ7 8 9
1494 "})
1495 .await;
1496 cx.simulate_shared_keystrokes(["shift-m"]).await;
1497 cx.assert_shared_state(indoc! {r"
1498 1 2 3
1499 ˇ4 5 6
1500 7 8 9
1501 "})
1502 .await;
1503 cx.set_shared_state(indoc! {r"
1504 ˇ1 2 3
1505 4 5 6
1506 7 8 9
1507 "})
1508 .await;
1509 cx.simulate_shared_keystrokes(["shift-m"]).await;
1510 cx.assert_shared_state(indoc! {r"
1511 1 2 3
1512 ˇ4 5 6
1513 7 8 9
1514 "})
1515 .await;
1516 cx.set_shared_state(indoc! {r"
1517 1 2 3
1518 ˇ4 5 6
1519 7 8 9
1520 "})
1521 .await;
1522 cx.simulate_shared_keystrokes(["shift-m"]).await;
1523 cx.assert_shared_state(indoc! {r"
1524 1 2 3
1525 ˇ4 5 6
1526 7 8 9
1527 "})
1528 .await;
1529 cx.set_shared_state(indoc! {r"
1530 1 2 3
1531 4 5 ˇ6
1532 7 8 9
1533 "})
1534 .await;
1535 cx.simulate_shared_keystrokes(["shift-m"]).await;
1536 cx.assert_shared_state(indoc! {r"
1537 1 2 3
1538 4 5 ˇ6
1539 7 8 9
1540 "})
1541 .await;
1542 }
1543
1544 #[gpui::test]
1545 async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
1546 let mut cx = NeovimBackedTestContext::new(cx).await;
1547 let initial_state = indoc! {r"abc
1548 deˇf
1549 paragraph
1550 the second
1551 third and
1552 final"};
1553
1554 cx.set_shared_state(initial_state).await;
1555 cx.simulate_shared_keystrokes(["shift-l"]).await;
1556 cx.assert_shared_state(indoc! {r"abc
1557 def
1558 paragraph
1559 the second
1560 third and
1561 fiˇnal"})
1562 .await;
1563
1564 cx.set_shared_state(indoc! {r"
1565 1 2 3
1566 4 5 ˇ6
1567 7 8 9
1568 "})
1569 .await;
1570 cx.simulate_shared_keystrokes(["shift-l"]).await;
1571 cx.assert_shared_state(indoc! {r"
1572 1 2 3
1573 4 5 6
1574 7 8 9
1575 ˇ"})
1576 .await;
1577
1578 cx.set_shared_state(indoc! {r"
1579 1 2 3
1580 ˇ4 5 6
1581 7 8 9
1582 "})
1583 .await;
1584 cx.simulate_shared_keystrokes(["shift-l"]).await;
1585 cx.assert_shared_state(indoc! {r"
1586 1 2 3
1587 4 5 6
1588 7 8 9
1589 ˇ"})
1590 .await;
1591
1592 cx.set_shared_state(indoc! {r"
1593 1 2 ˇ3
1594 4 5 6
1595 7 8 9
1596 "})
1597 .await;
1598 cx.simulate_shared_keystrokes(["shift-l"]).await;
1599 cx.assert_shared_state(indoc! {r"
1600 1 2 3
1601 4 5 6
1602 7 8 9
1603 ˇ"})
1604 .await;
1605
1606 cx.set_shared_state(indoc! {r"
1607 ˇ1 2 3
1608 4 5 6
1609 7 8 9
1610 "})
1611 .await;
1612 cx.simulate_shared_keystrokes(["shift-l"]).await;
1613 cx.assert_shared_state(indoc! {r"
1614 1 2 3
1615 4 5 6
1616 7 8 9
1617 ˇ"})
1618 .await;
1619
1620 cx.set_shared_state(indoc! {r"
1621 1 2 3
1622 4 5 ˇ6
1623 7 8 9
1624 "})
1625 .await;
1626 cx.simulate_shared_keystrokes(["9", "shift-l"]).await;
1627 cx.assert_shared_state(indoc! {r"
1628 1 2 ˇ3
1629 4 5 6
1630 7 8 9
1631 "})
1632 .await;
1633 }
1634
1635 #[gpui::test]
1636 async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
1637 let mut cx = NeovimBackedTestContext::new(cx).await;
1638 cx.set_shared_state(indoc! {r"
1639 456 5ˇ67 678
1640 "})
1641 .await;
1642 cx.simulate_shared_keystrokes(["g", "e"]).await;
1643 cx.assert_shared_state(indoc! {r"
1644 45ˇ6 567 678
1645 "})
1646 .await;
1647
1648 // Test times
1649 cx.set_shared_state(indoc! {r"
1650 123 234 345
1651 456 5ˇ67 678
1652 "})
1653 .await;
1654 cx.simulate_shared_keystrokes(["4", "g", "e"]).await;
1655 cx.assert_shared_state(indoc! {r"
1656 12ˇ3 234 345
1657 456 567 678
1658 "})
1659 .await;
1660
1661 // With punctuation
1662 cx.set_shared_state(indoc! {r"
1663 123 234 345
1664 4;5.6 5ˇ67 678
1665 789 890 901
1666 "})
1667 .await;
1668 cx.simulate_shared_keystrokes(["g", "e"]).await;
1669 cx.assert_shared_state(indoc! {r"
1670 123 234 345
1671 4;5.ˇ6 567 678
1672 789 890 901
1673 "})
1674 .await;
1675
1676 // With punctuation and count
1677 cx.set_shared_state(indoc! {r"
1678 123 234 345
1679 4;5.6 5ˇ67 678
1680 789 890 901
1681 "})
1682 .await;
1683 cx.simulate_shared_keystrokes(["5", "g", "e"]).await;
1684 cx.assert_shared_state(indoc! {r"
1685 123 234 345
1686 ˇ4;5.6 567 678
1687 789 890 901
1688 "})
1689 .await;
1690
1691 // newlines
1692 cx.set_shared_state(indoc! {r"
1693 123 234 345
1694
1695 78ˇ9 890 901
1696 "})
1697 .await;
1698 cx.simulate_shared_keystrokes(["g", "e"]).await;
1699 cx.assert_shared_state(indoc! {r"
1700 123 234 345
1701 ˇ
1702 789 890 901
1703 "})
1704 .await;
1705 cx.simulate_shared_keystrokes(["g", "e"]).await;
1706 cx.assert_shared_state(indoc! {r"
1707 123 234 34ˇ5
1708
1709 789 890 901
1710 "})
1711 .await;
1712
1713 // With punctuation
1714 cx.set_shared_state(indoc! {r"
1715 123 234 345
1716 4;5.ˇ6 567 678
1717 789 890 901
1718 "})
1719 .await;
1720 cx.simulate_shared_keystrokes(["g", "shift-e"]).await;
1721 cx.assert_shared_state(indoc! {r"
1722 123 234 34ˇ5
1723 4;5.6 567 678
1724 789 890 901
1725 "})
1726 .await;
1727 }
1728}