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