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 }
621 point
622}
623
624fn space(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
625 for _ in 0..times {
626 point = wrapping_right(map, point);
627 }
628 point
629}
630
631fn wrapping_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
632 let max_column = map.line_len(point.row()).saturating_sub(1);
633 if point.column() < max_column {
634 *point.column_mut() += 1;
635 } else if point.row() < map.max_point().row() {
636 *point.row_mut() += 1;
637 *point.column_mut() = 0;
638 }
639 point
640}
641
642pub(crate) fn start_of_relative_buffer_row(
643 map: &DisplaySnapshot,
644 point: DisplayPoint,
645 times: isize,
646) -> DisplayPoint {
647 let start = map.display_point_to_fold_point(point, Bias::Left);
648 let target = start.row() as isize + times;
649 let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
650
651 map.clip_point(
652 map.fold_point_to_display_point(
653 map.fold_snapshot
654 .clip_point(FoldPoint::new(new_row, 0), Bias::Right),
655 ),
656 Bias::Right,
657 )
658}
659
660fn up_down_buffer_rows(
661 map: &DisplaySnapshot,
662 point: DisplayPoint,
663 mut goal: SelectionGoal,
664 times: isize,
665 text_layout_details: &TextLayoutDetails,
666) -> (DisplayPoint, SelectionGoal) {
667 let start = map.display_point_to_fold_point(point, Bias::Left);
668 let begin_folded_line = map.fold_point_to_display_point(
669 map.fold_snapshot
670 .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
671 );
672 let select_nth_wrapped_row = point.row() - begin_folded_line.row();
673
674 let (goal_wrap, goal_x) = match goal {
675 SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
676 SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
677 SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
678 _ => {
679 let x = map.x_for_display_point(point, text_layout_details);
680 goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x.0));
681 (select_nth_wrapped_row, x.0)
682 }
683 };
684
685 let target = start.row() as isize + times;
686 let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
687
688 let mut begin_folded_line = map.fold_point_to_display_point(
689 map.fold_snapshot
690 .clip_point(FoldPoint::new(new_row, 0), Bias::Left),
691 );
692
693 let mut i = 0;
694 while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
695 let next_folded_line = DisplayPoint::new(begin_folded_line.row() + 1, 0);
696 if map
697 .display_point_to_fold_point(next_folded_line, Bias::Right)
698 .row()
699 == new_row
700 {
701 i += 1;
702 begin_folded_line = next_folded_line;
703 } else {
704 break;
705 }
706 }
707
708 let new_col = if i == goal_wrap {
709 map.display_column_for_x(begin_folded_line.row(), px(goal_x), text_layout_details)
710 } else {
711 map.line_len(begin_folded_line.row())
712 };
713
714 (
715 map.clip_point(
716 DisplayPoint::new(begin_folded_line.row(), new_col),
717 Bias::Left,
718 ),
719 goal,
720 )
721}
722
723fn down_display(
724 map: &DisplaySnapshot,
725 mut point: DisplayPoint,
726 mut goal: SelectionGoal,
727 times: usize,
728 text_layout_details: &TextLayoutDetails,
729) -> (DisplayPoint, SelectionGoal) {
730 for _ in 0..times {
731 (point, goal) = movement::down(map, point, goal, true, text_layout_details);
732 }
733
734 (point, goal)
735}
736
737fn up_display(
738 map: &DisplaySnapshot,
739 mut point: DisplayPoint,
740 mut goal: SelectionGoal,
741 times: usize,
742 text_layout_details: &TextLayoutDetails,
743) -> (DisplayPoint, SelectionGoal) {
744 for _ in 0..times {
745 (point, goal) = movement::up(map, point, goal, true, &text_layout_details);
746 }
747
748 (point, goal)
749}
750
751pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
752 for _ in 0..times {
753 let new_point = movement::saturating_right(map, point);
754 if point == new_point {
755 break;
756 }
757 point = new_point;
758 }
759 point
760}
761
762pub(crate) fn next_word_start(
763 map: &DisplaySnapshot,
764 mut point: DisplayPoint,
765 ignore_punctuation: bool,
766 times: usize,
767) -> DisplayPoint {
768 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
769 for _ in 0..times {
770 let mut crossed_newline = false;
771 point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
772 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
773 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
774 let at_newline = right == '\n';
775
776 let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
777 || at_newline && crossed_newline
778 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
779
780 crossed_newline |= at_newline;
781 found
782 })
783 }
784 point
785}
786
787fn next_word_end(
788 map: &DisplaySnapshot,
789 mut point: DisplayPoint,
790 ignore_punctuation: bool,
791 times: usize,
792) -> DisplayPoint {
793 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
794 for _ in 0..times {
795 if point.column() < map.line_len(point.row()) {
796 *point.column_mut() += 1;
797 } else if point.row() < map.max_buffer_row() {
798 *point.row_mut() += 1;
799 *point.column_mut() = 0;
800 }
801 point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
802 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
803 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
804
805 left_kind != right_kind && left_kind != CharKind::Whitespace
806 });
807
808 // find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
809 // we have backtracked already
810 if !map
811 .chars_at(point)
812 .nth(1)
813 .map(|(c, _)| c == '\n')
814 .unwrap_or(true)
815 {
816 *point.column_mut() = point.column().saturating_sub(1);
817 }
818 point = map.clip_point(point, Bias::Left);
819 }
820 point
821}
822
823fn previous_word_start(
824 map: &DisplaySnapshot,
825 mut point: DisplayPoint,
826 ignore_punctuation: bool,
827 times: usize,
828) -> DisplayPoint {
829 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
830 for _ in 0..times {
831 // This works even though find_preceding_boundary is called for every character in the line containing
832 // cursor because the newline is checked only once.
833 point =
834 movement::find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
835 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
836 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
837
838 (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
839 });
840 }
841 point
842}
843
844pub(crate) fn first_non_whitespace(
845 map: &DisplaySnapshot,
846 display_lines: bool,
847 from: DisplayPoint,
848) -> DisplayPoint {
849 let mut last_point = start_of_line(map, display_lines, from);
850 let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
851 for (ch, point) in map.chars_at(last_point) {
852 if ch == '\n' {
853 return from;
854 }
855
856 last_point = point;
857
858 if char_kind(&scope, ch) != CharKind::Whitespace {
859 break;
860 }
861 }
862
863 map.clip_point(last_point, Bias::Left)
864}
865
866pub(crate) fn start_of_line(
867 map: &DisplaySnapshot,
868 display_lines: bool,
869 point: DisplayPoint,
870) -> DisplayPoint {
871 if display_lines {
872 map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
873 } else {
874 map.prev_line_boundary(point.to_point(map)).1
875 }
876}
877
878pub(crate) fn end_of_line(
879 map: &DisplaySnapshot,
880 display_lines: bool,
881 point: DisplayPoint,
882) -> DisplayPoint {
883 if display_lines {
884 map.clip_point(
885 DisplayPoint::new(point.row(), map.line_len(point.row())),
886 Bias::Left,
887 )
888 } else {
889 map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
890 }
891}
892
893fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
894 let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
895 *new_point.column_mut() = point.column();
896 map.clip_point(new_point, Bias::Left)
897}
898
899fn end_of_document(
900 map: &DisplaySnapshot,
901 point: DisplayPoint,
902 line: Option<usize>,
903) -> DisplayPoint {
904 let new_row = if let Some(line) = line {
905 (line - 1) as u32
906 } else {
907 map.max_buffer_row()
908 };
909
910 let new_point = Point::new(new_row, point.column());
911 map.clip_point(new_point.to_display_point(map), Bias::Left)
912}
913
914fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
915 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
916 let point = display_point.to_point(map);
917 let offset = point.to_offset(&map.buffer_snapshot);
918
919 // Ensure the range is contained by the current line.
920 let mut line_end = map.next_line_boundary(point).0;
921 if line_end == point {
922 line_end = map.max_point().to_point(map);
923 }
924
925 let line_range = map.prev_line_boundary(point).0..line_end;
926 let visible_line_range =
927 line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
928 let ranges = map
929 .buffer_snapshot
930 .bracket_ranges(visible_line_range.clone());
931 if let Some(ranges) = ranges {
932 let line_range = line_range.start.to_offset(&map.buffer_snapshot)
933 ..line_range.end.to_offset(&map.buffer_snapshot);
934 let mut closest_pair_destination = None;
935 let mut closest_distance = usize::MAX;
936
937 for (open_range, close_range) in ranges {
938 if open_range.start >= offset && line_range.contains(&open_range.start) {
939 let distance = open_range.start - offset;
940 if distance < closest_distance {
941 closest_pair_destination = Some(close_range.start);
942 closest_distance = distance;
943 continue;
944 }
945 }
946
947 if close_range.start >= offset && line_range.contains(&close_range.start) {
948 let distance = close_range.start - offset;
949 if distance < closest_distance {
950 closest_pair_destination = Some(open_range.start);
951 closest_distance = distance;
952 continue;
953 }
954 }
955
956 continue;
957 }
958
959 closest_pair_destination
960 .map(|destination| destination.to_display_point(map))
961 .unwrap_or(display_point)
962 } else {
963 display_point
964 }
965}
966
967fn find_forward(
968 map: &DisplaySnapshot,
969 from: DisplayPoint,
970 before: bool,
971 target: char,
972 times: usize,
973) -> Option<DisplayPoint> {
974 let mut to = from;
975 let mut found = false;
976
977 for _ in 0..times {
978 found = false;
979 to = find_boundary(map, to, FindRange::SingleLine, |_, right| {
980 found = right == target;
981 found
982 });
983 }
984
985 if found {
986 if before && to.column() > 0 {
987 *to.column_mut() -= 1;
988 Some(map.clip_point(to, Bias::Left))
989 } else {
990 Some(to)
991 }
992 } else {
993 None
994 }
995}
996
997fn find_backward(
998 map: &DisplaySnapshot,
999 from: DisplayPoint,
1000 after: bool,
1001 target: char,
1002 times: usize,
1003) -> DisplayPoint {
1004 let mut to = from;
1005
1006 for _ in 0..times {
1007 to = find_preceding_boundary(map, to, FindRange::SingleLine, |_, right| right == target);
1008 }
1009
1010 if map.buffer_snapshot.chars_at(to.to_point(map)).next() == Some(target) {
1011 if after {
1012 *to.column_mut() += 1;
1013 map.clip_point(to, Bias::Right)
1014 } else {
1015 to
1016 }
1017 } else {
1018 from
1019 }
1020}
1021
1022fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1023 let correct_line = start_of_relative_buffer_row(map, point, times as isize);
1024 first_non_whitespace(map, false, correct_line)
1025}
1026
1027fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1028 let correct_line = start_of_relative_buffer_row(map, point, 0);
1029 right(map, correct_line, times.saturating_sub(1))
1030}
1031
1032pub(crate) fn next_line_end(
1033 map: &DisplaySnapshot,
1034 mut point: DisplayPoint,
1035 times: usize,
1036) -> DisplayPoint {
1037 if times > 1 {
1038 point = start_of_relative_buffer_row(map, point, times as isize - 1);
1039 }
1040 end_of_line(map, false, point)
1041}
1042
1043fn window_top(
1044 map: &DisplaySnapshot,
1045 point: DisplayPoint,
1046 text_layout_details: &TextLayoutDetails,
1047 times: usize,
1048) -> (DisplayPoint, SelectionGoal) {
1049 let first_visible_line = text_layout_details.anchor.to_display_point(map);
1050
1051 if let Some(visible_rows) = text_layout_details.visible_rows {
1052 let bottom_row = first_visible_line.row() + visible_rows as u32;
1053 let new_row = (first_visible_line.row() + (times as u32)).min(bottom_row);
1054 let new_col = point.column().min(map.line_len(first_visible_line.row()));
1055
1056 let new_point = DisplayPoint::new(new_row, new_col);
1057 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1058 } else {
1059 let new_row = first_visible_line.row() + (times as u32);
1060 let new_col = point.column().min(map.line_len(first_visible_line.row()));
1061
1062 let new_point = DisplayPoint::new(new_row, new_col);
1063 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1064 }
1065}
1066
1067fn window_middle(
1068 map: &DisplaySnapshot,
1069 point: DisplayPoint,
1070 text_layout_details: &TextLayoutDetails,
1071) -> (DisplayPoint, SelectionGoal) {
1072 if let Some(visible_rows) = text_layout_details.visible_rows {
1073 let first_visible_line = text_layout_details.anchor.to_display_point(map);
1074 let max_rows = (visible_rows as u32).min(map.max_buffer_row());
1075 let new_row = first_visible_line.row() + (max_rows.div_euclid(2));
1076 let new_col = point.column().min(map.line_len(new_row));
1077 let new_point = DisplayPoint::new(new_row, new_col);
1078 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1079 } else {
1080 (point, SelectionGoal::None)
1081 }
1082}
1083
1084fn window_bottom(
1085 map: &DisplaySnapshot,
1086 point: DisplayPoint,
1087 text_layout_details: &TextLayoutDetails,
1088 times: usize,
1089) -> (DisplayPoint, SelectionGoal) {
1090 if let Some(visible_rows) = text_layout_details.visible_rows {
1091 let first_visible_line = text_layout_details.anchor.to_display_point(map);
1092 let bottom_row = first_visible_line.row() + (visible_rows) as u32;
1093 let bottom_row_capped = bottom_row.min(map.max_buffer_row());
1094 let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row() {
1095 first_visible_line.row()
1096 } else {
1097 bottom_row_capped.saturating_sub(times as u32)
1098 };
1099 let new_col = point.column().min(map.line_len(new_row));
1100 let new_point = DisplayPoint::new(new_row, new_col);
1101 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1102 } else {
1103 (point, SelectionGoal::None)
1104 }
1105}
1106
1107#[cfg(test)]
1108mod test {
1109
1110 use crate::test::NeovimBackedTestContext;
1111 use indoc::indoc;
1112
1113 #[gpui::test]
1114 async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
1115 let mut cx = NeovimBackedTestContext::new(cx).await;
1116
1117 let initial_state = indoc! {r"ˇabc
1118 def
1119
1120 paragraph
1121 the second
1122
1123
1124
1125 third and
1126 final"};
1127
1128 // goes down once
1129 cx.set_shared_state(initial_state).await;
1130 cx.simulate_shared_keystrokes(["}"]).await;
1131 cx.assert_shared_state(indoc! {r"abc
1132 def
1133 ˇ
1134 paragraph
1135 the second
1136
1137
1138
1139 third and
1140 final"})
1141 .await;
1142
1143 // goes up once
1144 cx.simulate_shared_keystrokes(["{"]).await;
1145 cx.assert_shared_state(initial_state).await;
1146
1147 // goes down twice
1148 cx.simulate_shared_keystrokes(["2", "}"]).await;
1149 cx.assert_shared_state(indoc! {r"abc
1150 def
1151
1152 paragraph
1153 the second
1154 ˇ
1155
1156
1157 third and
1158 final"})
1159 .await;
1160
1161 // goes down over multiple blanks
1162 cx.simulate_shared_keystrokes(["}"]).await;
1163 cx.assert_shared_state(indoc! {r"abc
1164 def
1165
1166 paragraph
1167 the second
1168
1169
1170
1171 third and
1172 finaˇl"})
1173 .await;
1174
1175 // goes up twice
1176 cx.simulate_shared_keystrokes(["2", "{"]).await;
1177 cx.assert_shared_state(indoc! {r"abc
1178 def
1179 ˇ
1180 paragraph
1181 the second
1182
1183
1184
1185 third and
1186 final"})
1187 .await
1188 }
1189
1190 #[gpui::test]
1191 async fn test_matching(cx: &mut gpui::TestAppContext) {
1192 let mut cx = NeovimBackedTestContext::new(cx).await;
1193
1194 cx.set_shared_state(indoc! {r"func ˇ(a string) {
1195 do(something(with<Types>.and_arrays[0, 2]))
1196 }"})
1197 .await;
1198 cx.simulate_shared_keystrokes(["%"]).await;
1199 cx.assert_shared_state(indoc! {r"func (a stringˇ) {
1200 do(something(with<Types>.and_arrays[0, 2]))
1201 }"})
1202 .await;
1203
1204 // test it works on the last character of the line
1205 cx.set_shared_state(indoc! {r"func (a string) ˇ{
1206 do(something(with<Types>.and_arrays[0, 2]))
1207 }"})
1208 .await;
1209 cx.simulate_shared_keystrokes(["%"]).await;
1210 cx.assert_shared_state(indoc! {r"func (a string) {
1211 do(something(with<Types>.and_arrays[0, 2]))
1212 ˇ}"})
1213 .await;
1214
1215 // test it works on immediate nesting
1216 cx.set_shared_state("ˇ{()}").await;
1217 cx.simulate_shared_keystrokes(["%"]).await;
1218 cx.assert_shared_state("{()ˇ}").await;
1219 cx.simulate_shared_keystrokes(["%"]).await;
1220 cx.assert_shared_state("ˇ{()}").await;
1221
1222 // test it works on immediate nesting inside braces
1223 cx.set_shared_state("{\n ˇ{()}\n}").await;
1224 cx.simulate_shared_keystrokes(["%"]).await;
1225 cx.assert_shared_state("{\n {()ˇ}\n}").await;
1226
1227 // test it jumps to the next paren on a line
1228 cx.set_shared_state("func ˇboop() {\n}").await;
1229 cx.simulate_shared_keystrokes(["%"]).await;
1230 cx.assert_shared_state("func boop(ˇ) {\n}").await;
1231 }
1232
1233 #[gpui::test]
1234 async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
1235 let mut cx = NeovimBackedTestContext::new(cx).await;
1236
1237 // f and F
1238 cx.set_shared_state("ˇone two three four").await;
1239 cx.simulate_shared_keystrokes(["f", "o"]).await;
1240 cx.assert_shared_state("one twˇo three four").await;
1241 cx.simulate_shared_keystrokes([","]).await;
1242 cx.assert_shared_state("ˇone two three four").await;
1243 cx.simulate_shared_keystrokes(["2", ";"]).await;
1244 cx.assert_shared_state("one two three fˇour").await;
1245 cx.simulate_shared_keystrokes(["shift-f", "e"]).await;
1246 cx.assert_shared_state("one two threˇe four").await;
1247 cx.simulate_shared_keystrokes(["2", ";"]).await;
1248 cx.assert_shared_state("onˇe two three four").await;
1249 cx.simulate_shared_keystrokes([","]).await;
1250 cx.assert_shared_state("one two thrˇee four").await;
1251
1252 // t and T
1253 cx.set_shared_state("ˇone two three four").await;
1254 cx.simulate_shared_keystrokes(["t", "o"]).await;
1255 cx.assert_shared_state("one tˇwo three four").await;
1256 cx.simulate_shared_keystrokes([","]).await;
1257 cx.assert_shared_state("oˇne two three four").await;
1258 cx.simulate_shared_keystrokes(["2", ";"]).await;
1259 cx.assert_shared_state("one two three ˇfour").await;
1260 cx.simulate_shared_keystrokes(["shift-t", "e"]).await;
1261 cx.assert_shared_state("one two threeˇ four").await;
1262 cx.simulate_shared_keystrokes(["3", ";"]).await;
1263 cx.assert_shared_state("oneˇ two three four").await;
1264 cx.simulate_shared_keystrokes([","]).await;
1265 cx.assert_shared_state("one two thˇree four").await;
1266 }
1267
1268 #[gpui::test]
1269 async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
1270 let mut cx = NeovimBackedTestContext::new(cx).await;
1271 cx.set_shared_state("ˇone\n two\nthree").await;
1272 cx.simulate_shared_keystrokes(["enter"]).await;
1273 cx.assert_shared_state("one\n ˇtwo\nthree").await;
1274 }
1275
1276 #[gpui::test]
1277 async fn test_window_top(cx: &mut gpui::TestAppContext) {
1278 let mut cx = NeovimBackedTestContext::new(cx).await;
1279 let initial_state = indoc! {r"abc
1280 def
1281 paragraph
1282 the second
1283 third ˇand
1284 final"};
1285
1286 cx.set_shared_state(initial_state).await;
1287 cx.simulate_shared_keystrokes(["shift-h"]).await;
1288 cx.assert_shared_state(indoc! {r"abˇc
1289 def
1290 paragraph
1291 the second
1292 third and
1293 final"})
1294 .await;
1295
1296 // clip point
1297 cx.set_shared_state(indoc! {r"
1298 1 2 3
1299 4 5 6
1300 7 8 ˇ9
1301 "})
1302 .await;
1303 cx.simulate_shared_keystrokes(["shift-h"]).await;
1304 cx.assert_shared_state(indoc! {r"
1305 1 2 ˇ3
1306 4 5 6
1307 7 8 9
1308 "})
1309 .await;
1310
1311 cx.set_shared_state(indoc! {r"
1312 1 2 3
1313 4 5 6
1314 ˇ7 8 9
1315 "})
1316 .await;
1317 cx.simulate_shared_keystrokes(["shift-h"]).await;
1318 cx.assert_shared_state(indoc! {r"
1319 ˇ1 2 3
1320 4 5 6
1321 7 8 9
1322 "})
1323 .await;
1324
1325 cx.set_shared_state(indoc! {r"
1326 1 2 3
1327 4 5 ˇ6
1328 7 8 9"})
1329 .await;
1330 cx.simulate_shared_keystrokes(["9", "shift-h"]).await;
1331 cx.assert_shared_state(indoc! {r"
1332 1 2 3
1333 4 5 6
1334 7 8 ˇ9"})
1335 .await;
1336 }
1337
1338 #[gpui::test]
1339 async fn test_window_middle(cx: &mut gpui::TestAppContext) {
1340 let mut cx = NeovimBackedTestContext::new(cx).await;
1341 let initial_state = indoc! {r"abˇc
1342 def
1343 paragraph
1344 the second
1345 third and
1346 final"};
1347
1348 cx.set_shared_state(initial_state).await;
1349 cx.simulate_shared_keystrokes(["shift-m"]).await;
1350 cx.assert_shared_state(indoc! {r"abc
1351 def
1352 paˇragraph
1353 the second
1354 third and
1355 final"})
1356 .await;
1357
1358 cx.set_shared_state(indoc! {r"
1359 1 2 3
1360 4 5 6
1361 7 8 ˇ9
1362 "})
1363 .await;
1364 cx.simulate_shared_keystrokes(["shift-m"]).await;
1365 cx.assert_shared_state(indoc! {r"
1366 1 2 3
1367 4 5 ˇ6
1368 7 8 9
1369 "})
1370 .await;
1371 cx.set_shared_state(indoc! {r"
1372 1 2 3
1373 4 5 6
1374 ˇ7 8 9
1375 "})
1376 .await;
1377 cx.simulate_shared_keystrokes(["shift-m"]).await;
1378 cx.assert_shared_state(indoc! {r"
1379 1 2 3
1380 ˇ4 5 6
1381 7 8 9
1382 "})
1383 .await;
1384 cx.set_shared_state(indoc! {r"
1385 ˇ1 2 3
1386 4 5 6
1387 7 8 9
1388 "})
1389 .await;
1390 cx.simulate_shared_keystrokes(["shift-m"]).await;
1391 cx.assert_shared_state(indoc! {r"
1392 1 2 3
1393 ˇ4 5 6
1394 7 8 9
1395 "})
1396 .await;
1397 cx.set_shared_state(indoc! {r"
1398 1 2 3
1399 ˇ4 5 6
1400 7 8 9
1401 "})
1402 .await;
1403 cx.simulate_shared_keystrokes(["shift-m"]).await;
1404 cx.assert_shared_state(indoc! {r"
1405 1 2 3
1406 ˇ4 5 6
1407 7 8 9
1408 "})
1409 .await;
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 }
1424
1425 #[gpui::test]
1426 async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
1427 let mut cx = NeovimBackedTestContext::new(cx).await;
1428 let initial_state = indoc! {r"abc
1429 deˇf
1430 paragraph
1431 the second
1432 third and
1433 final"};
1434
1435 cx.set_shared_state(initial_state).await;
1436 cx.simulate_shared_keystrokes(["shift-l"]).await;
1437 cx.assert_shared_state(indoc! {r"abc
1438 def
1439 paragraph
1440 the second
1441 third and
1442 fiˇnal"})
1443 .await;
1444
1445 cx.set_shared_state(indoc! {r"
1446 1 2 3
1447 4 5 ˇ6
1448 7 8 9
1449 "})
1450 .await;
1451 cx.simulate_shared_keystrokes(["shift-l"]).await;
1452 cx.assert_shared_state(indoc! {r"
1453 1 2 3
1454 4 5 6
1455 7 8 9
1456 ˇ"})
1457 .await;
1458
1459 cx.set_shared_state(indoc! {r"
1460 1 2 3
1461 ˇ4 5 6
1462 7 8 9
1463 "})
1464 .await;
1465 cx.simulate_shared_keystrokes(["shift-l"]).await;
1466 cx.assert_shared_state(indoc! {r"
1467 1 2 3
1468 4 5 6
1469 7 8 9
1470 ˇ"})
1471 .await;
1472
1473 cx.set_shared_state(indoc! {r"
1474 1 2 ˇ3
1475 4 5 6
1476 7 8 9
1477 "})
1478 .await;
1479 cx.simulate_shared_keystrokes(["shift-l"]).await;
1480 cx.assert_shared_state(indoc! {r"
1481 1 2 3
1482 4 5 6
1483 7 8 9
1484 ˇ"})
1485 .await;
1486
1487 cx.set_shared_state(indoc! {r"
1488 ˇ1 2 3
1489 4 5 6
1490 7 8 9
1491 "})
1492 .await;
1493 cx.simulate_shared_keystrokes(["shift-l"]).await;
1494 cx.assert_shared_state(indoc! {r"
1495 1 2 3
1496 4 5 6
1497 7 8 9
1498 ˇ"})
1499 .await;
1500
1501 cx.set_shared_state(indoc! {r"
1502 1 2 3
1503 4 5 ˇ6
1504 7 8 9
1505 "})
1506 .await;
1507 cx.simulate_shared_keystrokes(["9", "shift-l"]).await;
1508 cx.assert_shared_state(indoc! {r"
1509 1 2 ˇ3
1510 4 5 6
1511 7 8 9
1512 "})
1513 .await;
1514 }
1515}