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