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
802 point =
803 movement::find_boundary_exclusive(map, point, FindRange::MultiLine, |left, right| {
804 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
805 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
806
807 left_kind != right_kind && left_kind != CharKind::Whitespace
808 });
809 point = map.clip_point(point, Bias::Left);
810 }
811 point
812}
813
814fn previous_word_start(
815 map: &DisplaySnapshot,
816 mut point: DisplayPoint,
817 ignore_punctuation: bool,
818 times: usize,
819) -> DisplayPoint {
820 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
821 for _ in 0..times {
822 // This works even though find_preceding_boundary is called for every character in the line containing
823 // cursor because the newline is checked only once.
824 point =
825 movement::find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
826 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
827 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
828
829 (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
830 });
831 }
832 point
833}
834
835pub(crate) fn first_non_whitespace(
836 map: &DisplaySnapshot,
837 display_lines: bool,
838 from: DisplayPoint,
839) -> DisplayPoint {
840 let mut last_point = start_of_line(map, display_lines, from);
841 let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
842 for (ch, point) in map.chars_at(last_point) {
843 if ch == '\n' {
844 return from;
845 }
846
847 last_point = point;
848
849 if char_kind(&scope, ch) != CharKind::Whitespace {
850 break;
851 }
852 }
853
854 map.clip_point(last_point, Bias::Left)
855}
856
857pub(crate) fn start_of_line(
858 map: &DisplaySnapshot,
859 display_lines: bool,
860 point: DisplayPoint,
861) -> DisplayPoint {
862 if display_lines {
863 map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
864 } else {
865 map.prev_line_boundary(point.to_point(map)).1
866 }
867}
868
869pub(crate) fn end_of_line(
870 map: &DisplaySnapshot,
871 display_lines: bool,
872 point: DisplayPoint,
873) -> DisplayPoint {
874 if display_lines {
875 map.clip_point(
876 DisplayPoint::new(point.row(), map.line_len(point.row())),
877 Bias::Left,
878 )
879 } else {
880 map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
881 }
882}
883
884fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
885 let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
886 *new_point.column_mut() = point.column();
887 map.clip_point(new_point, Bias::Left)
888}
889
890fn end_of_document(
891 map: &DisplaySnapshot,
892 point: DisplayPoint,
893 line: Option<usize>,
894) -> DisplayPoint {
895 let new_row = if let Some(line) = line {
896 (line - 1) as u32
897 } else {
898 map.max_buffer_row()
899 };
900
901 let new_point = Point::new(new_row, point.column());
902 map.clip_point(new_point.to_display_point(map), Bias::Left)
903}
904
905fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
906 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
907 let point = display_point.to_point(map);
908 let offset = point.to_offset(&map.buffer_snapshot);
909
910 // Ensure the range is contained by the current line.
911 let mut line_end = map.next_line_boundary(point).0;
912 if line_end == point {
913 line_end = map.max_point().to_point(map);
914 }
915
916 let line_range = map.prev_line_boundary(point).0..line_end;
917 let visible_line_range =
918 line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
919 let ranges = map
920 .buffer_snapshot
921 .bracket_ranges(visible_line_range.clone());
922 if let Some(ranges) = ranges {
923 let line_range = line_range.start.to_offset(&map.buffer_snapshot)
924 ..line_range.end.to_offset(&map.buffer_snapshot);
925 let mut closest_pair_destination = None;
926 let mut closest_distance = usize::MAX;
927
928 for (open_range, close_range) in ranges {
929 if open_range.start >= offset && line_range.contains(&open_range.start) {
930 let distance = open_range.start - offset;
931 if distance < closest_distance {
932 closest_pair_destination = Some(close_range.start);
933 closest_distance = distance;
934 continue;
935 }
936 }
937
938 if close_range.start >= offset && line_range.contains(&close_range.start) {
939 let distance = close_range.start - offset;
940 if distance < closest_distance {
941 closest_pair_destination = Some(open_range.start);
942 closest_distance = distance;
943 continue;
944 }
945 }
946
947 continue;
948 }
949
950 closest_pair_destination
951 .map(|destination| destination.to_display_point(map))
952 .unwrap_or(display_point)
953 } else {
954 display_point
955 }
956}
957
958fn find_forward(
959 map: &DisplaySnapshot,
960 from: DisplayPoint,
961 before: bool,
962 target: char,
963 times: usize,
964) -> Option<DisplayPoint> {
965 let mut to = from;
966 let mut found = false;
967
968 for _ in 0..times {
969 found = false;
970 to = find_boundary(map, to, FindRange::SingleLine, |_, right| {
971 found = right == target;
972 found
973 });
974 }
975
976 if found {
977 if before && to.column() > 0 {
978 *to.column_mut() -= 1;
979 Some(map.clip_point(to, Bias::Left))
980 } else {
981 Some(to)
982 }
983 } else {
984 None
985 }
986}
987
988fn find_backward(
989 map: &DisplaySnapshot,
990 from: DisplayPoint,
991 after: bool,
992 target: char,
993 times: usize,
994) -> DisplayPoint {
995 let mut to = from;
996
997 for _ in 0..times {
998 to = find_preceding_boundary(map, to, FindRange::SingleLine, |_, right| right == target);
999 }
1000
1001 if map.buffer_snapshot.chars_at(to.to_point(map)).next() == Some(target) {
1002 if after {
1003 *to.column_mut() += 1;
1004 map.clip_point(to, Bias::Right)
1005 } else {
1006 to
1007 }
1008 } else {
1009 from
1010 }
1011}
1012
1013fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1014 let correct_line = start_of_relative_buffer_row(map, point, times as isize);
1015 first_non_whitespace(map, false, correct_line)
1016}
1017
1018fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1019 let correct_line = start_of_relative_buffer_row(map, point, 0);
1020 right(map, correct_line, times.saturating_sub(1))
1021}
1022
1023pub(crate) fn next_line_end(
1024 map: &DisplaySnapshot,
1025 mut point: DisplayPoint,
1026 times: usize,
1027) -> DisplayPoint {
1028 if times > 1 {
1029 point = start_of_relative_buffer_row(map, point, times as isize - 1);
1030 }
1031 end_of_line(map, false, point)
1032}
1033
1034fn window_top(
1035 map: &DisplaySnapshot,
1036 point: DisplayPoint,
1037 text_layout_details: &TextLayoutDetails,
1038 mut times: usize,
1039) -> (DisplayPoint, SelectionGoal) {
1040 let first_visible_line = text_layout_details
1041 .scroll_anchor
1042 .anchor
1043 .to_display_point(map);
1044
1045 if first_visible_line.row() != 0 && text_layout_details.vertical_scroll_margin as usize > times
1046 {
1047 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1048 }
1049
1050 if let Some(visible_rows) = text_layout_details.visible_rows {
1051 let bottom_row = first_visible_line.row() + visible_rows as u32;
1052 let new_row = (first_visible_line.row() + (times as u32)).min(bottom_row);
1053 let new_col = point.column().min(map.line_len(first_visible_line.row()));
1054
1055 let new_point = DisplayPoint::new(new_row, new_col);
1056 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1057 } else {
1058 let new_row = first_visible_line.row() + (times as u32);
1059 let new_col = point.column().min(map.line_len(first_visible_line.row()));
1060
1061 let new_point = DisplayPoint::new(new_row, new_col);
1062 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1063 }
1064}
1065
1066fn window_middle(
1067 map: &DisplaySnapshot,
1068 point: DisplayPoint,
1069 text_layout_details: &TextLayoutDetails,
1070) -> (DisplayPoint, SelectionGoal) {
1071 if let Some(visible_rows) = text_layout_details.visible_rows {
1072 let first_visible_line = text_layout_details
1073 .scroll_anchor
1074 .anchor
1075 .to_display_point(map);
1076 let max_rows = (visible_rows as u32).min(map.max_buffer_row());
1077 let new_row = first_visible_line.row() + (max_rows.div_euclid(2));
1078 let new_col = point.column().min(map.line_len(new_row));
1079 let new_point = DisplayPoint::new(new_row, new_col);
1080 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1081 } else {
1082 (point, SelectionGoal::None)
1083 }
1084}
1085
1086fn window_bottom(
1087 map: &DisplaySnapshot,
1088 point: DisplayPoint,
1089 text_layout_details: &TextLayoutDetails,
1090 mut times: usize,
1091) -> (DisplayPoint, SelectionGoal) {
1092 if let Some(visible_rows) = text_layout_details.visible_rows {
1093 let first_visible_line = text_layout_details
1094 .scroll_anchor
1095 .anchor
1096 .to_display_point(map);
1097 let bottom_row = first_visible_line.row()
1098 + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
1099 if bottom_row < map.max_buffer_row()
1100 && text_layout_details.vertical_scroll_margin as usize > times
1101 {
1102 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1103 }
1104 let bottom_row_capped = bottom_row.min(map.max_buffer_row());
1105 let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row() {
1106 first_visible_line.row()
1107 } else {
1108 bottom_row_capped.saturating_sub(times as u32)
1109 };
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
1118#[cfg(test)]
1119mod test {
1120
1121 use crate::test::NeovimBackedTestContext;
1122 use indoc::indoc;
1123
1124 #[gpui::test]
1125 async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
1126 let mut cx = NeovimBackedTestContext::new(cx).await;
1127
1128 let initial_state = indoc! {r"ˇabc
1129 def
1130
1131 paragraph
1132 the second
1133
1134
1135
1136 third and
1137 final"};
1138
1139 // goes down once
1140 cx.set_shared_state(initial_state).await;
1141 cx.simulate_shared_keystrokes(["}"]).await;
1142 cx.assert_shared_state(indoc! {r"abc
1143 def
1144 ˇ
1145 paragraph
1146 the second
1147
1148
1149
1150 third and
1151 final"})
1152 .await;
1153
1154 // goes up once
1155 cx.simulate_shared_keystrokes(["{"]).await;
1156 cx.assert_shared_state(initial_state).await;
1157
1158 // goes down twice
1159 cx.simulate_shared_keystrokes(["2", "}"]).await;
1160 cx.assert_shared_state(indoc! {r"abc
1161 def
1162
1163 paragraph
1164 the second
1165 ˇ
1166
1167
1168 third and
1169 final"})
1170 .await;
1171
1172 // goes down over multiple blanks
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 finaˇl"})
1184 .await;
1185
1186 // goes up twice
1187 cx.simulate_shared_keystrokes(["2", "{"]).await;
1188 cx.assert_shared_state(indoc! {r"abc
1189 def
1190 ˇ
1191 paragraph
1192 the second
1193
1194
1195
1196 third and
1197 final"})
1198 .await
1199 }
1200
1201 #[gpui::test]
1202 async fn test_matching(cx: &mut gpui::TestAppContext) {
1203 let mut cx = NeovimBackedTestContext::new(cx).await;
1204
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 the last character of the line
1216 cx.set_shared_state(indoc! {r"func (a string) ˇ{
1217 do(something(with<Types>.and_arrays[0, 2]))
1218 }"})
1219 .await;
1220 cx.simulate_shared_keystrokes(["%"]).await;
1221 cx.assert_shared_state(indoc! {r"func (a string) {
1222 do(something(with<Types>.and_arrays[0, 2]))
1223 ˇ}"})
1224 .await;
1225
1226 // test it works on immediate nesting
1227 cx.set_shared_state("ˇ{()}").await;
1228 cx.simulate_shared_keystrokes(["%"]).await;
1229 cx.assert_shared_state("{()ˇ}").await;
1230 cx.simulate_shared_keystrokes(["%"]).await;
1231 cx.assert_shared_state("ˇ{()}").await;
1232
1233 // test it works on immediate nesting inside braces
1234 cx.set_shared_state("{\n ˇ{()}\n}").await;
1235 cx.simulate_shared_keystrokes(["%"]).await;
1236 cx.assert_shared_state("{\n {()ˇ}\n}").await;
1237
1238 // test it jumps to the next paren on a line
1239 cx.set_shared_state("func ˇboop() {\n}").await;
1240 cx.simulate_shared_keystrokes(["%"]).await;
1241 cx.assert_shared_state("func boop(ˇ) {\n}").await;
1242 }
1243
1244 #[gpui::test]
1245 async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
1246 let mut cx = NeovimBackedTestContext::new(cx).await;
1247
1248 // f and F
1249 cx.set_shared_state("ˇone two three four").await;
1250 cx.simulate_shared_keystrokes(["f", "o"]).await;
1251 cx.assert_shared_state("one twˇo three four").await;
1252 cx.simulate_shared_keystrokes([","]).await;
1253 cx.assert_shared_state("ˇone two three four").await;
1254 cx.simulate_shared_keystrokes(["2", ";"]).await;
1255 cx.assert_shared_state("one two three fˇour").await;
1256 cx.simulate_shared_keystrokes(["shift-f", "e"]).await;
1257 cx.assert_shared_state("one two threˇe four").await;
1258 cx.simulate_shared_keystrokes(["2", ";"]).await;
1259 cx.assert_shared_state("onˇe two three four").await;
1260 cx.simulate_shared_keystrokes([","]).await;
1261 cx.assert_shared_state("one two thrˇee four").await;
1262
1263 // t and T
1264 cx.set_shared_state("ˇone two three four").await;
1265 cx.simulate_shared_keystrokes(["t", "o"]).await;
1266 cx.assert_shared_state("one tˇwo three four").await;
1267 cx.simulate_shared_keystrokes([","]).await;
1268 cx.assert_shared_state("oˇne two three four").await;
1269 cx.simulate_shared_keystrokes(["2", ";"]).await;
1270 cx.assert_shared_state("one two three ˇfour").await;
1271 cx.simulate_shared_keystrokes(["shift-t", "e"]).await;
1272 cx.assert_shared_state("one two threeˇ four").await;
1273 cx.simulate_shared_keystrokes(["3", ";"]).await;
1274 cx.assert_shared_state("oneˇ two three four").await;
1275 cx.simulate_shared_keystrokes([","]).await;
1276 cx.assert_shared_state("one two thˇree four").await;
1277 }
1278
1279 #[gpui::test]
1280 async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
1281 let mut cx = NeovimBackedTestContext::new(cx).await;
1282 let initial_state = indoc! {r"something(ˇfoo)"};
1283 cx.set_shared_state(initial_state).await;
1284 cx.simulate_shared_keystrokes(["}"]).await;
1285 cx.assert_shared_state(indoc! {r"something(fooˇ)"}).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}