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.row() < map.max_buffer_row() {
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)).min(bottom_row);
1085 let new_col = point.column().min(map.line_len(first_visible_line.row()));
1086
1087 let new_point = DisplayPoint::new(new_row, new_col);
1088 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1089 } else {
1090 let new_row = first_visible_line.row() + (times as u32);
1091 let new_col = point.column().min(map.line_len(first_visible_line.row()));
1092
1093 let new_point = DisplayPoint::new(new_row, new_col);
1094 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1095 }
1096}
1097
1098fn window_middle(
1099 map: &DisplaySnapshot,
1100 point: DisplayPoint,
1101 text_layout_details: &TextLayoutDetails,
1102) -> (DisplayPoint, SelectionGoal) {
1103 if let Some(visible_rows) = text_layout_details.visible_rows {
1104 let first_visible_line = text_layout_details
1105 .scroll_anchor
1106 .anchor
1107 .to_display_point(map);
1108 let max_rows = (visible_rows as u32).min(map.max_buffer_row());
1109 let new_row = first_visible_line.row() + (max_rows.div_euclid(2));
1110 let new_col = point.column().min(map.line_len(new_row));
1111 let new_point = DisplayPoint::new(new_row, new_col);
1112 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1113 } else {
1114 (point, SelectionGoal::None)
1115 }
1116}
1117
1118fn window_bottom(
1119 map: &DisplaySnapshot,
1120 point: DisplayPoint,
1121 text_layout_details: &TextLayoutDetails,
1122 mut times: usize,
1123) -> (DisplayPoint, SelectionGoal) {
1124 if let Some(visible_rows) = text_layout_details.visible_rows {
1125 let first_visible_line = text_layout_details
1126 .scroll_anchor
1127 .anchor
1128 .to_display_point(map);
1129 let bottom_row = first_visible_line.row()
1130 + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
1131 if bottom_row < map.max_buffer_row()
1132 && text_layout_details.vertical_scroll_margin as usize > times
1133 {
1134 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1135 }
1136 let bottom_row_capped = bottom_row.min(map.max_buffer_row());
1137 let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row() {
1138 first_visible_line.row()
1139 } else {
1140 bottom_row_capped.saturating_sub(times as u32)
1141 };
1142 let new_col = point.column().min(map.line_len(new_row));
1143 let new_point = DisplayPoint::new(new_row, new_col);
1144 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1145 } else {
1146 (point, SelectionGoal::None)
1147 }
1148}
1149
1150#[cfg(test)]
1151mod test {
1152
1153 use crate::test::NeovimBackedTestContext;
1154 use indoc::indoc;
1155
1156 #[gpui::test]
1157 async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
1158 let mut cx = NeovimBackedTestContext::new(cx).await;
1159
1160 let initial_state = indoc! {r"ˇabc
1161 def
1162
1163 paragraph
1164 the second
1165
1166
1167
1168 third and
1169 final"};
1170
1171 // goes down once
1172 cx.set_shared_state(initial_state).await;
1173 cx.simulate_shared_keystrokes(["}"]).await;
1174 cx.assert_shared_state(indoc! {r"abc
1175 def
1176 ˇ
1177 paragraph
1178 the second
1179
1180
1181
1182 third and
1183 final"})
1184 .await;
1185
1186 // goes up once
1187 cx.simulate_shared_keystrokes(["{"]).await;
1188 cx.assert_shared_state(initial_state).await;
1189
1190 // goes down twice
1191 cx.simulate_shared_keystrokes(["2", "}"]).await;
1192 cx.assert_shared_state(indoc! {r"abc
1193 def
1194
1195 paragraph
1196 the second
1197 ˇ
1198
1199
1200 third and
1201 final"})
1202 .await;
1203
1204 // goes down over multiple blanks
1205 cx.simulate_shared_keystrokes(["}"]).await;
1206 cx.assert_shared_state(indoc! {r"abc
1207 def
1208
1209 paragraph
1210 the second
1211
1212
1213
1214 third and
1215 finaˇl"})
1216 .await;
1217
1218 // goes up twice
1219 cx.simulate_shared_keystrokes(["2", "{"]).await;
1220 cx.assert_shared_state(indoc! {r"abc
1221 def
1222 ˇ
1223 paragraph
1224 the second
1225
1226
1227
1228 third and
1229 final"})
1230 .await
1231 }
1232
1233 #[gpui::test]
1234 async fn test_matching(cx: &mut gpui::TestAppContext) {
1235 let mut cx = NeovimBackedTestContext::new(cx).await;
1236
1237 cx.set_shared_state(indoc! {r"func ˇ(a string) {
1238 do(something(with<Types>.and_arrays[0, 2]))
1239 }"})
1240 .await;
1241 cx.simulate_shared_keystrokes(["%"]).await;
1242 cx.assert_shared_state(indoc! {r"func (a stringˇ) {
1243 do(something(with<Types>.and_arrays[0, 2]))
1244 }"})
1245 .await;
1246
1247 // test it works on the last character of the line
1248 cx.set_shared_state(indoc! {r"func (a string) ˇ{
1249 do(something(with<Types>.and_arrays[0, 2]))
1250 }"})
1251 .await;
1252 cx.simulate_shared_keystrokes(["%"]).await;
1253 cx.assert_shared_state(indoc! {r"func (a string) {
1254 do(something(with<Types>.and_arrays[0, 2]))
1255 ˇ}"})
1256 .await;
1257
1258 // test it works on immediate nesting
1259 cx.set_shared_state("ˇ{()}").await;
1260 cx.simulate_shared_keystrokes(["%"]).await;
1261 cx.assert_shared_state("{()ˇ}").await;
1262 cx.simulate_shared_keystrokes(["%"]).await;
1263 cx.assert_shared_state("ˇ{()}").await;
1264
1265 // test it works on immediate nesting inside braces
1266 cx.set_shared_state("{\n ˇ{()}\n}").await;
1267 cx.simulate_shared_keystrokes(["%"]).await;
1268 cx.assert_shared_state("{\n {()ˇ}\n}").await;
1269
1270 // test it jumps to the next paren on a line
1271 cx.set_shared_state("func ˇboop() {\n}").await;
1272 cx.simulate_shared_keystrokes(["%"]).await;
1273 cx.assert_shared_state("func boop(ˇ) {\n}").await;
1274 }
1275
1276 #[gpui::test]
1277 async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
1278 let mut cx = NeovimBackedTestContext::new(cx).await;
1279
1280 // f and F
1281 cx.set_shared_state("ˇone two three four").await;
1282 cx.simulate_shared_keystrokes(["f", "o"]).await;
1283 cx.assert_shared_state("one twˇo three four").await;
1284 cx.simulate_shared_keystrokes([","]).await;
1285 cx.assert_shared_state("ˇone two three four").await;
1286 cx.simulate_shared_keystrokes(["2", ";"]).await;
1287 cx.assert_shared_state("one two three fˇour").await;
1288 cx.simulate_shared_keystrokes(["shift-f", "e"]).await;
1289 cx.assert_shared_state("one two threˇe four").await;
1290 cx.simulate_shared_keystrokes(["2", ";"]).await;
1291 cx.assert_shared_state("onˇe two three four").await;
1292 cx.simulate_shared_keystrokes([","]).await;
1293 cx.assert_shared_state("one two thrˇee four").await;
1294
1295 // t and T
1296 cx.set_shared_state("ˇone two three four").await;
1297 cx.simulate_shared_keystrokes(["t", "o"]).await;
1298 cx.assert_shared_state("one tˇwo three four").await;
1299 cx.simulate_shared_keystrokes([","]).await;
1300 cx.assert_shared_state("oˇne two three four").await;
1301 cx.simulate_shared_keystrokes(["2", ";"]).await;
1302 cx.assert_shared_state("one two three ˇfour").await;
1303 cx.simulate_shared_keystrokes(["shift-t", "e"]).await;
1304 cx.assert_shared_state("one two threeˇ four").await;
1305 cx.simulate_shared_keystrokes(["3", ";"]).await;
1306 cx.assert_shared_state("oneˇ two three four").await;
1307 cx.simulate_shared_keystrokes([","]).await;
1308 cx.assert_shared_state("one two thˇree four").await;
1309 }
1310
1311 #[gpui::test]
1312 async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
1313 let mut cx = NeovimBackedTestContext::new(cx).await;
1314 let initial_state = indoc! {r"something(ˇfoo)"};
1315 cx.set_shared_state(initial_state).await;
1316 cx.simulate_shared_keystrokes(["}"]).await;
1317 cx.assert_shared_state(indoc! {r"something(fooˇ)"}).await;
1318 }
1319
1320 #[gpui::test]
1321 async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
1322 let mut cx = NeovimBackedTestContext::new(cx).await;
1323 cx.set_shared_state("ˇone\n two\nthree").await;
1324 cx.simulate_shared_keystrokes(["enter"]).await;
1325 cx.assert_shared_state("one\n ˇtwo\nthree").await;
1326 }
1327
1328 #[gpui::test]
1329 async fn test_window_top(cx: &mut gpui::TestAppContext) {
1330 let mut cx = NeovimBackedTestContext::new(cx).await;
1331 let initial_state = indoc! {r"abc
1332 def
1333 paragraph
1334 the second
1335 third ˇand
1336 final"};
1337
1338 cx.set_shared_state(initial_state).await;
1339 cx.simulate_shared_keystrokes(["shift-h"]).await;
1340 cx.assert_shared_state(indoc! {r"abˇc
1341 def
1342 paragraph
1343 the second
1344 third and
1345 final"})
1346 .await;
1347
1348 // clip point
1349 cx.set_shared_state(indoc! {r"
1350 1 2 3
1351 4 5 6
1352 7 8 ˇ9
1353 "})
1354 .await;
1355 cx.simulate_shared_keystrokes(["shift-h"]).await;
1356 cx.assert_shared_state(indoc! {r"
1357 1 2 ˇ3
1358 4 5 6
1359 7 8 9
1360 "})
1361 .await;
1362
1363 cx.set_shared_state(indoc! {r"
1364 1 2 3
1365 4 5 6
1366 ˇ7 8 9
1367 "})
1368 .await;
1369 cx.simulate_shared_keystrokes(["shift-h"]).await;
1370 cx.assert_shared_state(indoc! {r"
1371 ˇ1 2 3
1372 4 5 6
1373 7 8 9
1374 "})
1375 .await;
1376
1377 cx.set_shared_state(indoc! {r"
1378 1 2 3
1379 4 5 ˇ6
1380 7 8 9"})
1381 .await;
1382 cx.simulate_shared_keystrokes(["9", "shift-h"]).await;
1383 cx.assert_shared_state(indoc! {r"
1384 1 2 3
1385 4 5 6
1386 7 8 ˇ9"})
1387 .await;
1388 }
1389
1390 #[gpui::test]
1391 async fn test_window_middle(cx: &mut gpui::TestAppContext) {
1392 let mut cx = NeovimBackedTestContext::new(cx).await;
1393 let initial_state = indoc! {r"abˇc
1394 def
1395 paragraph
1396 the second
1397 third and
1398 final"};
1399
1400 cx.set_shared_state(initial_state).await;
1401 cx.simulate_shared_keystrokes(["shift-m"]).await;
1402 cx.assert_shared_state(indoc! {r"abc
1403 def
1404 paˇragraph
1405 the second
1406 third and
1407 final"})
1408 .await;
1409
1410 cx.set_shared_state(indoc! {r"
1411 1 2 3
1412 4 5 6
1413 7 8 ˇ9
1414 "})
1415 .await;
1416 cx.simulate_shared_keystrokes(["shift-m"]).await;
1417 cx.assert_shared_state(indoc! {r"
1418 1 2 3
1419 4 5 ˇ6
1420 7 8 9
1421 "})
1422 .await;
1423 cx.set_shared_state(indoc! {r"
1424 1 2 3
1425 4 5 6
1426 ˇ7 8 9
1427 "})
1428 .await;
1429 cx.simulate_shared_keystrokes(["shift-m"]).await;
1430 cx.assert_shared_state(indoc! {r"
1431 1 2 3
1432 ˇ4 5 6
1433 7 8 9
1434 "})
1435 .await;
1436 cx.set_shared_state(indoc! {r"
1437 ˇ1 2 3
1438 4 5 6
1439 7 8 9
1440 "})
1441 .await;
1442 cx.simulate_shared_keystrokes(["shift-m"]).await;
1443 cx.assert_shared_state(indoc! {r"
1444 1 2 3
1445 ˇ4 5 6
1446 7 8 9
1447 "})
1448 .await;
1449 cx.set_shared_state(indoc! {r"
1450 1 2 3
1451 ˇ4 5 6
1452 7 8 9
1453 "})
1454 .await;
1455 cx.simulate_shared_keystrokes(["shift-m"]).await;
1456 cx.assert_shared_state(indoc! {r"
1457 1 2 3
1458 ˇ4 5 6
1459 7 8 9
1460 "})
1461 .await;
1462 cx.set_shared_state(indoc! {r"
1463 1 2 3
1464 4 5 ˇ6
1465 7 8 9
1466 "})
1467 .await;
1468 cx.simulate_shared_keystrokes(["shift-m"]).await;
1469 cx.assert_shared_state(indoc! {r"
1470 1 2 3
1471 4 5 ˇ6
1472 7 8 9
1473 "})
1474 .await;
1475 }
1476
1477 #[gpui::test]
1478 async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
1479 let mut cx = NeovimBackedTestContext::new(cx).await;
1480 let initial_state = indoc! {r"abc
1481 deˇf
1482 paragraph
1483 the second
1484 third and
1485 final"};
1486
1487 cx.set_shared_state(initial_state).await;
1488 cx.simulate_shared_keystrokes(["shift-l"]).await;
1489 cx.assert_shared_state(indoc! {r"abc
1490 def
1491 paragraph
1492 the second
1493 third and
1494 fiˇnal"})
1495 .await;
1496
1497 cx.set_shared_state(indoc! {r"
1498 1 2 3
1499 4 5 ˇ6
1500 7 8 9
1501 "})
1502 .await;
1503 cx.simulate_shared_keystrokes(["shift-l"]).await;
1504 cx.assert_shared_state(indoc! {r"
1505 1 2 3
1506 4 5 6
1507 7 8 9
1508 ˇ"})
1509 .await;
1510
1511 cx.set_shared_state(indoc! {r"
1512 1 2 3
1513 ˇ4 5 6
1514 7 8 9
1515 "})
1516 .await;
1517 cx.simulate_shared_keystrokes(["shift-l"]).await;
1518 cx.assert_shared_state(indoc! {r"
1519 1 2 3
1520 4 5 6
1521 7 8 9
1522 ˇ"})
1523 .await;
1524
1525 cx.set_shared_state(indoc! {r"
1526 1 2 ˇ3
1527 4 5 6
1528 7 8 9
1529 "})
1530 .await;
1531 cx.simulate_shared_keystrokes(["shift-l"]).await;
1532 cx.assert_shared_state(indoc! {r"
1533 1 2 3
1534 4 5 6
1535 7 8 9
1536 ˇ"})
1537 .await;
1538
1539 cx.set_shared_state(indoc! {r"
1540 ˇ1 2 3
1541 4 5 6
1542 7 8 9
1543 "})
1544 .await;
1545 cx.simulate_shared_keystrokes(["shift-l"]).await;
1546 cx.assert_shared_state(indoc! {r"
1547 1 2 3
1548 4 5 6
1549 7 8 9
1550 ˇ"})
1551 .await;
1552
1553 cx.set_shared_state(indoc! {r"
1554 1 2 3
1555 4 5 ˇ6
1556 7 8 9
1557 "})
1558 .await;
1559 cx.simulate_shared_keystrokes(["9", "shift-l"]).await;
1560 cx.assert_shared_state(indoc! {r"
1561 1 2 ˇ3
1562 4 5 6
1563 7 8 9
1564 "})
1565 .await;
1566 }
1567}