1use std::sync::Arc;
2
3use editor::{
4 char_kind,
5 display_map::{DisplaySnapshot, ToDisplayPoint},
6 movement, Bias, CharKind, DisplayPoint, ToOffset,
7};
8use gpui::{actions, impl_actions, AppContext, WindowContext};
9use language::{Point, Selection, SelectionGoal};
10use serde::Deserialize;
11use workspace::Workspace;
12
13use crate::{
14 normal::normal_motion,
15 state::{Mode, Operator},
16 visual::visual_motion,
17 Vim,
18};
19
20#[derive(Clone, Debug, PartialEq, Eq)]
21pub enum Motion {
22 Left,
23 Backspace,
24 Down,
25 Up,
26 Right,
27 NextWordStart { ignore_punctuation: bool },
28 NextWordEnd { ignore_punctuation: bool },
29 PreviousWordStart { ignore_punctuation: bool },
30 FirstNonWhitespace,
31 CurrentLine,
32 StartOfLine,
33 EndOfLine,
34 StartOfParagraph,
35 EndOfParagraph,
36 StartOfDocument,
37 EndOfDocument,
38 Matching,
39 FindForward { before: bool, text: Arc<str> },
40 FindBackward { after: bool, text: Arc<str> },
41 NextLineStart,
42}
43
44#[derive(Clone, Deserialize, PartialEq)]
45#[serde(rename_all = "camelCase")]
46struct NextWordStart {
47 #[serde(default)]
48 ignore_punctuation: bool,
49}
50
51#[derive(Clone, Deserialize, PartialEq)]
52#[serde(rename_all = "camelCase")]
53struct NextWordEnd {
54 #[serde(default)]
55 ignore_punctuation: bool,
56}
57
58#[derive(Clone, Deserialize, PartialEq)]
59#[serde(rename_all = "camelCase")]
60struct PreviousWordStart {
61 #[serde(default)]
62 ignore_punctuation: bool,
63}
64
65#[derive(Clone, Deserialize, PartialEq)]
66struct RepeatFind {
67 #[serde(default)]
68 backwards: bool,
69}
70
71actions!(
72 vim,
73 [
74 Left,
75 Backspace,
76 Down,
77 Up,
78 Right,
79 FirstNonWhitespace,
80 StartOfLine,
81 EndOfLine,
82 CurrentLine,
83 StartOfParagraph,
84 EndOfParagraph,
85 StartOfDocument,
86 EndOfDocument,
87 Matching,
88 NextLineStart,
89 ]
90);
91impl_actions!(
92 vim,
93 [NextWordStart, NextWordEnd, PreviousWordStart, RepeatFind]
94);
95
96pub fn init(cx: &mut AppContext) {
97 cx.add_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx));
98 cx.add_action(|_: &mut Workspace, _: &Backspace, cx: _| motion(Motion::Backspace, cx));
99 cx.add_action(|_: &mut Workspace, _: &Down, cx: _| motion(Motion::Down, cx));
100 cx.add_action(|_: &mut Workspace, _: &Up, cx: _| motion(Motion::Up, cx));
101 cx.add_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx));
102 cx.add_action(|_: &mut Workspace, _: &FirstNonWhitespace, cx: _| {
103 motion(Motion::FirstNonWhitespace, cx)
104 });
105 cx.add_action(|_: &mut Workspace, _: &StartOfLine, cx: _| motion(Motion::StartOfLine, cx));
106 cx.add_action(|_: &mut Workspace, _: &EndOfLine, cx: _| motion(Motion::EndOfLine, cx));
107 cx.add_action(|_: &mut Workspace, _: &CurrentLine, cx: _| motion(Motion::CurrentLine, cx));
108 cx.add_action(|_: &mut Workspace, _: &StartOfParagraph, cx: _| {
109 motion(Motion::StartOfParagraph, cx)
110 });
111 cx.add_action(|_: &mut Workspace, _: &EndOfParagraph, cx: _| {
112 motion(Motion::EndOfParagraph, cx)
113 });
114 cx.add_action(|_: &mut Workspace, _: &StartOfDocument, cx: _| {
115 motion(Motion::StartOfDocument, cx)
116 });
117 cx.add_action(|_: &mut Workspace, _: &EndOfDocument, cx: _| motion(Motion::EndOfDocument, cx));
118 cx.add_action(|_: &mut Workspace, _: &Matching, cx: _| motion(Motion::Matching, cx));
119
120 cx.add_action(
121 |_: &mut Workspace, &NextWordStart { ignore_punctuation }: &NextWordStart, cx: _| {
122 motion(Motion::NextWordStart { ignore_punctuation }, cx)
123 },
124 );
125 cx.add_action(
126 |_: &mut Workspace, &NextWordEnd { ignore_punctuation }: &NextWordEnd, cx: _| {
127 motion(Motion::NextWordEnd { ignore_punctuation }, cx)
128 },
129 );
130 cx.add_action(
131 |_: &mut Workspace,
132 &PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
133 cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
134 );
135 cx.add_action(|_: &mut Workspace, &NextLineStart, cx: _| motion(Motion::NextLineStart, cx));
136 cx.add_action(|_: &mut Workspace, action: &RepeatFind, cx: _| {
137 repeat_motion(action.backwards, cx)
138 })
139}
140
141pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
142 if let Some(Operator::FindForward { .. }) | Some(Operator::FindBackward { .. }) =
143 Vim::read(cx).active_operator()
144 {
145 Vim::update(cx, |vim, cx| vim.pop_operator(cx));
146 }
147
148 let times = Vim::update(cx, |vim, cx| vim.pop_number_operator(cx));
149 let operator = Vim::read(cx).active_operator();
150 match Vim::read(cx).state.mode {
151 Mode::Normal => normal_motion(motion, operator, times, cx),
152 Mode::Visual { .. } => visual_motion(motion, times, cx),
153 Mode::Insert => {
154 // Shouldn't execute a motion in insert mode. Ignoring
155 }
156 }
157 Vim::update(cx, |vim, cx| vim.clear_operator(cx));
158}
159
160fn repeat_motion(backwards: bool, cx: &mut WindowContext) {
161 let find = match Vim::read(cx).state.last_find.clone() {
162 Some(Motion::FindForward { before, text }) => {
163 if backwards {
164 Motion::FindBackward {
165 after: before,
166 text,
167 }
168 } else {
169 Motion::FindForward { before, text }
170 }
171 }
172
173 Some(Motion::FindBackward { after, text }) => {
174 if backwards {
175 Motion::FindForward {
176 before: after,
177 text,
178 }
179 } else {
180 Motion::FindBackward { after, text }
181 }
182 }
183 _ => return,
184 };
185
186 motion(find, cx)
187}
188
189// Motion handling is specified here:
190// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
191impl Motion {
192 pub fn linewise(&self) -> bool {
193 use Motion::*;
194 match self {
195 Down | Up | StartOfDocument | EndOfDocument | CurrentLine | NextLineStart
196 | StartOfParagraph | EndOfParagraph => true,
197 EndOfLine
198 | NextWordEnd { .. }
199 | Matching
200 | FindForward { .. }
201 | Left
202 | Backspace
203 | Right
204 | StartOfLine
205 | NextWordStart { .. }
206 | PreviousWordStart { .. }
207 | FirstNonWhitespace
208 | FindBackward { .. } => false,
209 }
210 }
211
212 pub fn infallible(&self) -> bool {
213 use Motion::*;
214 match self {
215 StartOfDocument | EndOfDocument | CurrentLine => true,
216 Down
217 | Up
218 | EndOfLine
219 | NextWordEnd { .. }
220 | Matching
221 | FindForward { .. }
222 | Left
223 | Backspace
224 | Right
225 | StartOfLine
226 | StartOfParagraph
227 | EndOfParagraph
228 | NextWordStart { .. }
229 | PreviousWordStart { .. }
230 | FirstNonWhitespace
231 | FindBackward { .. }
232 | NextLineStart => false,
233 }
234 }
235
236 pub fn inclusive(&self) -> bool {
237 use Motion::*;
238 match self {
239 Down
240 | Up
241 | StartOfDocument
242 | EndOfDocument
243 | CurrentLine
244 | EndOfLine
245 | NextWordEnd { .. }
246 | Matching
247 | FindForward { .. }
248 | NextLineStart => true,
249 Left
250 | Backspace
251 | Right
252 | StartOfLine
253 | StartOfParagraph
254 | EndOfParagraph
255 | NextWordStart { .. }
256 | PreviousWordStart { .. }
257 | FirstNonWhitespace
258 | FindBackward { .. } => false,
259 }
260 }
261
262 pub fn move_point(
263 &self,
264 map: &DisplaySnapshot,
265 point: DisplayPoint,
266 goal: SelectionGoal,
267 maybe_times: Option<usize>,
268 ) -> Option<(DisplayPoint, SelectionGoal)> {
269 let times = maybe_times.unwrap_or(1);
270 use Motion::*;
271 let infallible = self.infallible();
272 let (new_point, goal) = match self {
273 Left => (left(map, point, times), SelectionGoal::None),
274 Backspace => (backspace(map, point, times), SelectionGoal::None),
275 Down => down(map, point, goal, times),
276 Up => up(map, point, goal, times),
277 Right => (right(map, point, times), SelectionGoal::None),
278 NextWordStart { ignore_punctuation } => (
279 next_word_start(map, point, *ignore_punctuation, times),
280 SelectionGoal::None,
281 ),
282 NextWordEnd { ignore_punctuation } => (
283 next_word_end(map, point, *ignore_punctuation, times),
284 SelectionGoal::None,
285 ),
286 PreviousWordStart { ignore_punctuation } => (
287 previous_word_start(map, point, *ignore_punctuation, times),
288 SelectionGoal::None,
289 ),
290 FirstNonWhitespace => (first_non_whitespace(map, point), SelectionGoal::None),
291 StartOfLine => (start_of_line(map, point), SelectionGoal::None),
292 EndOfLine => (end_of_line(map, point), SelectionGoal::None),
293 StartOfParagraph => (
294 movement::start_of_paragraph(map, point, times),
295 SelectionGoal::None,
296 ),
297 EndOfParagraph => (
298 map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
299 SelectionGoal::None,
300 ),
301 CurrentLine => (end_of_line(map, point), SelectionGoal::None),
302 StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
303 EndOfDocument => (
304 end_of_document(map, point, maybe_times),
305 SelectionGoal::None,
306 ),
307 Matching => (matching(map, point), SelectionGoal::None),
308 FindForward { before, text } => (
309 find_forward(map, point, *before, text.clone(), times),
310 SelectionGoal::None,
311 ),
312 FindBackward { after, text } => (
313 find_backward(map, point, *after, text.clone(), times),
314 SelectionGoal::None,
315 ),
316 NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
317 };
318
319 (new_point != point || infallible).then_some((new_point, goal))
320 }
321
322 // Expands a selection using self motion for an operator
323 pub fn expand_selection(
324 &self,
325 map: &DisplaySnapshot,
326 selection: &mut Selection<DisplayPoint>,
327 times: Option<usize>,
328 expand_to_surrounding_newline: bool,
329 ) -> bool {
330 if let Some((new_head, goal)) =
331 self.move_point(map, selection.head(), selection.goal, times)
332 {
333 selection.set_head(new_head, goal);
334
335 if self.linewise() {
336 selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
337
338 if expand_to_surrounding_newline {
339 if selection.end.row() < map.max_point().row() {
340 *selection.end.row_mut() += 1;
341 *selection.end.column_mut() = 0;
342 selection.end = map.clip_point(selection.end, Bias::Right);
343 // Don't reset the end here
344 return true;
345 } else if selection.start.row() > 0 {
346 *selection.start.row_mut() -= 1;
347 *selection.start.column_mut() = map.line_len(selection.start.row());
348 selection.start = map.clip_point(selection.start, Bias::Left);
349 }
350 }
351
352 (_, selection.end) = map.next_line_boundary(selection.end.to_point(map));
353 } else {
354 // If the motion is exclusive and the end of the motion is in column 1, the
355 // end of the motion is moved to the end of the previous line and the motion
356 // becomes inclusive. Example: "}" moves to the first line after a paragraph,
357 // but "d}" will not include that line.
358 let mut inclusive = self.inclusive();
359 if !inclusive
360 && self != &Motion::Backspace
361 && selection.end.row() > selection.start.row()
362 && selection.end.column() == 0
363 {
364 inclusive = true;
365 *selection.end.row_mut() -= 1;
366 *selection.end.column_mut() = 0;
367 selection.end = map.clip_point(
368 map.next_line_boundary(selection.end.to_point(map)).1,
369 Bias::Left,
370 );
371 }
372
373 if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
374 *selection.end.column_mut() += 1;
375 }
376 }
377 true
378 } else {
379 false
380 }
381 }
382}
383
384fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
385 for _ in 0..times {
386 *point.column_mut() = point.column().saturating_sub(1);
387 point = map.clip_point(point, Bias::Left);
388 if point.column() == 0 {
389 break;
390 }
391 }
392 point
393}
394
395fn backspace(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
396 for _ in 0..times {
397 point = movement::left(map, point);
398 }
399 point
400}
401
402fn down(
403 map: &DisplaySnapshot,
404 mut point: DisplayPoint,
405 mut goal: SelectionGoal,
406 times: usize,
407) -> (DisplayPoint, SelectionGoal) {
408 for _ in 0..times {
409 (point, goal) = movement::down(map, point, goal, true);
410 }
411 (point, goal)
412}
413
414fn up(
415 map: &DisplaySnapshot,
416 mut point: DisplayPoint,
417 mut goal: SelectionGoal,
418 times: usize,
419) -> (DisplayPoint, SelectionGoal) {
420 for _ in 0..times {
421 (point, goal) = movement::up(map, point, goal, true);
422 }
423 (point, goal)
424}
425
426pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
427 for _ in 0..times {
428 let mut new_point = point;
429 *new_point.column_mut() += 1;
430 let new_point = map.clip_point(new_point, Bias::Right);
431 if point == new_point {
432 break;
433 }
434 point = new_point;
435 }
436 point
437}
438
439pub(crate) fn next_word_start(
440 map: &DisplaySnapshot,
441 mut point: DisplayPoint,
442 ignore_punctuation: bool,
443 times: usize,
444) -> DisplayPoint {
445 for _ in 0..times {
446 let mut crossed_newline = false;
447 point = movement::find_boundary(map, point, |left, right| {
448 let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
449 let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
450 let at_newline = right == '\n';
451
452 let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
453 || at_newline && crossed_newline
454 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
455
456 crossed_newline |= at_newline;
457 found
458 })
459 }
460 point
461}
462
463fn next_word_end(
464 map: &DisplaySnapshot,
465 mut point: DisplayPoint,
466 ignore_punctuation: bool,
467 times: usize,
468) -> DisplayPoint {
469 for _ in 0..times {
470 *point.column_mut() += 1;
471 point = movement::find_boundary(map, point, |left, right| {
472 let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
473 let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
474
475 left_kind != right_kind && left_kind != CharKind::Whitespace
476 });
477
478 // find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
479 // we have backtracked already
480 if !map
481 .chars_at(point)
482 .nth(1)
483 .map(|(c, _)| c == '\n')
484 .unwrap_or(true)
485 {
486 *point.column_mut() = point.column().saturating_sub(1);
487 }
488 point = map.clip_point(point, Bias::Left);
489 }
490 point
491}
492
493fn previous_word_start(
494 map: &DisplaySnapshot,
495 mut point: DisplayPoint,
496 ignore_punctuation: bool,
497 times: usize,
498) -> DisplayPoint {
499 for _ in 0..times {
500 // This works even though find_preceding_boundary is called for every character in the line containing
501 // cursor because the newline is checked only once.
502 point = movement::find_preceding_boundary(map, point, |left, right| {
503 let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
504 let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
505
506 (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
507 });
508 }
509 point
510}
511
512fn first_non_whitespace(map: &DisplaySnapshot, from: DisplayPoint) -> DisplayPoint {
513 let mut last_point = DisplayPoint::new(from.row(), 0);
514 for (ch, point) in map.chars_at(last_point) {
515 if ch == '\n' {
516 return from;
517 }
518
519 last_point = point;
520
521 if char_kind(ch) != CharKind::Whitespace {
522 break;
523 }
524 }
525
526 map.clip_point(last_point, Bias::Left)
527}
528
529fn start_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
530 map.prev_line_boundary(point.to_point(map)).1
531}
532
533fn end_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
534 map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
535}
536
537fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
538 let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
539 *new_point.column_mut() = point.column();
540 map.clip_point(new_point, Bias::Left)
541}
542
543fn end_of_document(
544 map: &DisplaySnapshot,
545 point: DisplayPoint,
546 line: Option<usize>,
547) -> DisplayPoint {
548 let new_row = if let Some(line) = line {
549 (line - 1) as u32
550 } else {
551 map.max_buffer_row()
552 };
553
554 let new_point = Point::new(new_row, point.column());
555 map.clip_point(new_point.to_display_point(map), Bias::Left)
556}
557
558fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
559 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
560 let point = display_point.to_point(map);
561 let offset = point.to_offset(&map.buffer_snapshot);
562
563 // Ensure the range is contained by the current line.
564 let mut line_end = map.next_line_boundary(point).0;
565 if line_end == point {
566 line_end = map.max_point().to_point(map);
567 }
568
569 let line_range = map.prev_line_boundary(point).0..line_end;
570 let visible_line_range =
571 line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
572 let ranges = map
573 .buffer_snapshot
574 .bracket_ranges(visible_line_range.clone());
575 if let Some(ranges) = ranges {
576 let line_range = line_range.start.to_offset(&map.buffer_snapshot)
577 ..line_range.end.to_offset(&map.buffer_snapshot);
578 let mut closest_pair_destination = None;
579 let mut closest_distance = usize::MAX;
580
581 for (open_range, close_range) in ranges {
582 if open_range.start >= offset && line_range.contains(&open_range.start) {
583 let distance = open_range.start - offset;
584 if distance < closest_distance {
585 closest_pair_destination = Some(close_range.start);
586 closest_distance = distance;
587 continue;
588 }
589 }
590
591 if close_range.start >= offset && line_range.contains(&close_range.start) {
592 let distance = close_range.start - offset;
593 if distance < closest_distance {
594 closest_pair_destination = Some(open_range.start);
595 closest_distance = distance;
596 continue;
597 }
598 }
599
600 continue;
601 }
602
603 closest_pair_destination
604 .map(|destination| destination.to_display_point(map))
605 .unwrap_or(display_point)
606 } else {
607 display_point
608 }
609}
610
611fn find_forward(
612 map: &DisplaySnapshot,
613 from: DisplayPoint,
614 before: bool,
615 target: Arc<str>,
616 times: usize,
617) -> DisplayPoint {
618 map.find_while(from, target.as_ref(), |ch, _| ch != '\n')
619 .skip_while(|found_at| found_at == &from)
620 .nth(times - 1)
621 .map(|mut found| {
622 if before {
623 *found.column_mut() -= 1;
624 found = map.clip_point(found, Bias::Right);
625 found
626 } else {
627 found
628 }
629 })
630 .unwrap_or(from)
631}
632
633fn find_backward(
634 map: &DisplaySnapshot,
635 from: DisplayPoint,
636 after: bool,
637 target: Arc<str>,
638 times: usize,
639) -> DisplayPoint {
640 map.reverse_find_while(from, target.as_ref(), |ch, _| ch != '\n')
641 .skip_while(|found_at| found_at == &from)
642 .nth(times - 1)
643 .map(|mut found| {
644 if after {
645 *found.column_mut() += 1;
646 found = map.clip_point(found, Bias::Left);
647 found
648 } else {
649 found
650 }
651 })
652 .unwrap_or(from)
653}
654
655fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
656 let new_row = (point.row() + times as u32).min(map.max_buffer_row());
657 map.clip_point(DisplayPoint::new(new_row, 0), Bias::Left)
658}
659
660#[cfg(test)]
661
662mod test {
663
664 use crate::test::NeovimBackedTestContext;
665 use indoc::indoc;
666
667 #[gpui::test]
668 async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
669 let mut cx = NeovimBackedTestContext::new(cx).await;
670
671 let initial_state = indoc! {r"ˇabc
672 def
673
674 paragraph
675 the second
676
677
678
679 third and
680 final"};
681
682 // goes down once
683 cx.set_shared_state(initial_state).await;
684 cx.simulate_shared_keystrokes(["}"]).await;
685 cx.assert_shared_state(indoc! {r"abc
686 def
687 ˇ
688 paragraph
689 the second
690
691
692
693 third and
694 final"})
695 .await;
696
697 // goes up once
698 cx.simulate_shared_keystrokes(["{"]).await;
699 cx.assert_shared_state(initial_state).await;
700
701 // goes down twice
702 cx.simulate_shared_keystrokes(["2", "}"]).await;
703 cx.assert_shared_state(indoc! {r"abc
704 def
705
706 paragraph
707 the second
708 ˇ
709
710
711 third and
712 final"})
713 .await;
714
715 // goes down over multiple blanks
716 cx.simulate_shared_keystrokes(["}"]).await;
717 cx.assert_shared_state(indoc! {r"abc
718 def
719
720 paragraph
721 the second
722
723
724
725 third and
726 finaˇl"})
727 .await;
728
729 // goes up twice
730 cx.simulate_shared_keystrokes(["2", "{"]).await;
731 cx.assert_shared_state(indoc! {r"abc
732 def
733 ˇ
734 paragraph
735 the second
736
737
738
739 third and
740 final"})
741 .await
742 }
743
744 #[gpui::test]
745 async fn test_matching(cx: &mut gpui::TestAppContext) {
746 let mut cx = NeovimBackedTestContext::new(cx).await;
747
748 cx.set_shared_state(indoc! {r"func ˇ(a string) {
749 do(something(with<Types>.and_arrays[0, 2]))
750 }"})
751 .await;
752 cx.simulate_shared_keystrokes(["%"]).await;
753 cx.assert_shared_state(indoc! {r"func (a stringˇ) {
754 do(something(with<Types>.and_arrays[0, 2]))
755 }"})
756 .await;
757
758 // test it works on the last character of the line
759 cx.set_shared_state(indoc! {r"func (a string) ˇ{
760 do(something(with<Types>.and_arrays[0, 2]))
761 }"})
762 .await;
763 cx.simulate_shared_keystrokes(["%"]).await;
764 cx.assert_shared_state(indoc! {r"func (a string) {
765 do(something(with<Types>.and_arrays[0, 2]))
766 ˇ}"})
767 .await;
768
769 // test it works on immediate nesting
770 cx.set_shared_state("ˇ{()}").await;
771 cx.simulate_shared_keystrokes(["%"]).await;
772 cx.assert_shared_state("{()ˇ}").await;
773 cx.simulate_shared_keystrokes(["%"]).await;
774 cx.assert_shared_state("ˇ{()}").await;
775
776 // test it works on immediate nesting inside braces
777 cx.set_shared_state("{\n ˇ{()}\n}").await;
778 cx.simulate_shared_keystrokes(["%"]).await;
779 cx.assert_shared_state("{\n {()ˇ}\n}").await;
780
781 // test it jumps to the next paren on a line
782 cx.set_shared_state("func ˇboop() {\n}").await;
783 cx.simulate_shared_keystrokes(["%"]).await;
784 cx.assert_shared_state("func boop(ˇ) {\n}").await;
785 }
786
787 #[gpui::test]
788 async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
789 let mut cx = NeovimBackedTestContext::new(cx).await;
790
791 cx.set_shared_state("ˇone two three four").await;
792 cx.simulate_shared_keystrokes(["f", "o"]).await;
793 cx.assert_shared_state("one twˇo three four").await;
794 cx.simulate_shared_keystrokes([","]).await;
795 cx.assert_shared_state("ˇone two three four").await;
796 cx.simulate_shared_keystrokes(["2", ";"]).await;
797 cx.assert_shared_state("one two three fˇour").await;
798 cx.simulate_shared_keystrokes(["shift-t", "e"]).await;
799 cx.assert_shared_state("one two threeˇ four").await;
800 cx.simulate_shared_keystrokes(["3", ";"]).await;
801 cx.assert_shared_state("oneˇ two three four").await;
802 cx.simulate_shared_keystrokes([","]).await;
803 cx.assert_shared_state("one two thˇree four").await;
804 }
805}