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 | Mode::VisualLine | Mode::VisualBlock => 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).workspace_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 = movement::saturating_left(map, point);
387 if point.column() == 0 {
388 break;
389 }
390 }
391 point
392}
393
394fn backspace(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
395 for _ in 0..times {
396 point = movement::left(map, point);
397 }
398 point
399}
400
401fn down(
402 map: &DisplaySnapshot,
403 mut point: DisplayPoint,
404 mut goal: SelectionGoal,
405 times: usize,
406) -> (DisplayPoint, SelectionGoal) {
407 for _ in 0..times {
408 (point, goal) = movement::down(map, point, goal, true);
409 }
410 (point, goal)
411}
412
413fn up(
414 map: &DisplaySnapshot,
415 mut point: DisplayPoint,
416 mut goal: SelectionGoal,
417 times: usize,
418) -> (DisplayPoint, SelectionGoal) {
419 for _ in 0..times {
420 (point, goal) = movement::up(map, point, goal, true);
421 }
422 (point, goal)
423}
424
425pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
426 for _ in 0..times {
427 let new_point = movement::saturating_right(map, point);
428 if point == new_point {
429 break;
430 }
431 point = new_point;
432 }
433 point
434}
435
436pub(crate) fn next_word_start(
437 map: &DisplaySnapshot,
438 mut point: DisplayPoint,
439 ignore_punctuation: bool,
440 times: usize,
441) -> DisplayPoint {
442 let language = map.buffer_snapshot.language_at(point.to_point(map));
443 for _ in 0..times {
444 let mut crossed_newline = false;
445 point = movement::find_boundary(map, point, |left, right| {
446 let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation);
447 let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation);
448 let at_newline = right == '\n';
449
450 let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
451 || at_newline && crossed_newline
452 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
453
454 crossed_newline |= at_newline;
455 found
456 })
457 }
458 point
459}
460
461fn next_word_end(
462 map: &DisplaySnapshot,
463 mut point: DisplayPoint,
464 ignore_punctuation: bool,
465 times: usize,
466) -> DisplayPoint {
467 let language = map.buffer_snapshot.language_at(point.to_point(map));
468 for _ in 0..times {
469 *point.column_mut() += 1;
470 point = movement::find_boundary(map, point, |left, right| {
471 let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation);
472 let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation);
473
474 left_kind != right_kind && left_kind != CharKind::Whitespace
475 });
476
477 // find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
478 // we have backtracked already
479 if !map
480 .chars_at(point)
481 .nth(1)
482 .map(|(c, _)| c == '\n')
483 .unwrap_or(true)
484 {
485 *point.column_mut() = point.column().saturating_sub(1);
486 }
487 point = map.clip_point(point, Bias::Left);
488 }
489 point
490}
491
492fn previous_word_start(
493 map: &DisplaySnapshot,
494 mut point: DisplayPoint,
495 ignore_punctuation: bool,
496 times: usize,
497) -> DisplayPoint {
498 let language = map.buffer_snapshot.language_at(point.to_point(map));
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(language, left).coerce_punctuation(ignore_punctuation);
504 let right_kind = char_kind(language, 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 let language = map.buffer_snapshot.language_at(from.to_point(map));
515 for (ch, point) in map.chars_at(last_point) {
516 if ch == '\n' {
517 return from;
518 }
519
520 last_point = point;
521
522 if char_kind(language, ch) != CharKind::Whitespace {
523 break;
524 }
525 }
526
527 map.clip_point(last_point, Bias::Left)
528}
529
530fn start_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
531 map.prev_line_boundary(point.to_point(map)).1
532}
533
534fn end_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
535 map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
536}
537
538fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
539 let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
540 *new_point.column_mut() = point.column();
541 map.clip_point(new_point, Bias::Left)
542}
543
544fn end_of_document(
545 map: &DisplaySnapshot,
546 point: DisplayPoint,
547 line: Option<usize>,
548) -> DisplayPoint {
549 let new_row = if let Some(line) = line {
550 (line - 1) as u32
551 } else {
552 map.max_buffer_row()
553 };
554
555 let new_point = Point::new(new_row, point.column());
556 map.clip_point(new_point.to_display_point(map), Bias::Left)
557}
558
559fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
560 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
561 let point = display_point.to_point(map);
562 let offset = point.to_offset(&map.buffer_snapshot);
563
564 // Ensure the range is contained by the current line.
565 let mut line_end = map.next_line_boundary(point).0;
566 if line_end == point {
567 line_end = map.max_point().to_point(map);
568 }
569
570 let line_range = map.prev_line_boundary(point).0..line_end;
571 let visible_line_range =
572 line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
573 let ranges = map
574 .buffer_snapshot
575 .bracket_ranges(visible_line_range.clone());
576 if let Some(ranges) = ranges {
577 let line_range = line_range.start.to_offset(&map.buffer_snapshot)
578 ..line_range.end.to_offset(&map.buffer_snapshot);
579 let mut closest_pair_destination = None;
580 let mut closest_distance = usize::MAX;
581
582 for (open_range, close_range) in ranges {
583 if open_range.start >= offset && line_range.contains(&open_range.start) {
584 let distance = open_range.start - offset;
585 if distance < closest_distance {
586 closest_pair_destination = Some(close_range.start);
587 closest_distance = distance;
588 continue;
589 }
590 }
591
592 if close_range.start >= offset && line_range.contains(&close_range.start) {
593 let distance = close_range.start - offset;
594 if distance < closest_distance {
595 closest_pair_destination = Some(open_range.start);
596 closest_distance = distance;
597 continue;
598 }
599 }
600
601 continue;
602 }
603
604 closest_pair_destination
605 .map(|destination| destination.to_display_point(map))
606 .unwrap_or(display_point)
607 } else {
608 display_point
609 }
610}
611
612fn find_forward(
613 map: &DisplaySnapshot,
614 from: DisplayPoint,
615 before: bool,
616 target: Arc<str>,
617 times: usize,
618) -> DisplayPoint {
619 map.find_while(from, target.as_ref(), |ch, _| ch != '\n')
620 .skip_while(|found_at| found_at == &from)
621 .nth(times - 1)
622 .map(|mut found| {
623 if before {
624 *found.column_mut() -= 1;
625 found = map.clip_point(found, Bias::Right);
626 found
627 } else {
628 found
629 }
630 })
631 .unwrap_or(from)
632}
633
634fn find_backward(
635 map: &DisplaySnapshot,
636 from: DisplayPoint,
637 after: bool,
638 target: Arc<str>,
639 times: usize,
640) -> DisplayPoint {
641 map.reverse_find_while(from, target.as_ref(), |ch, _| ch != '\n')
642 .skip_while(|found_at| found_at == &from)
643 .nth(times - 1)
644 .map(|mut found| {
645 if after {
646 *found.column_mut() += 1;
647 found = map.clip_point(found, Bias::Left);
648 found
649 } else {
650 found
651 }
652 })
653 .unwrap_or(from)
654}
655
656fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
657 let new_row = (point.row() + times as u32).min(map.max_buffer_row());
658 first_non_whitespace(
659 map,
660 map.clip_point(DisplayPoint::new(new_row, 0), Bias::Left),
661 )
662}
663
664#[cfg(test)]
665
666mod test {
667
668 use crate::test::NeovimBackedTestContext;
669 use indoc::indoc;
670
671 #[gpui::test]
672 async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
673 let mut cx = NeovimBackedTestContext::new(cx).await;
674
675 let initial_state = indoc! {r"ˇabc
676 def
677
678 paragraph
679 the second
680
681
682
683 third and
684 final"};
685
686 // goes down once
687 cx.set_shared_state(initial_state).await;
688 cx.simulate_shared_keystrokes(["}"]).await;
689 cx.assert_shared_state(indoc! {r"abc
690 def
691 ˇ
692 paragraph
693 the second
694
695
696
697 third and
698 final"})
699 .await;
700
701 // goes up once
702 cx.simulate_shared_keystrokes(["{"]).await;
703 cx.assert_shared_state(initial_state).await;
704
705 // goes down twice
706 cx.simulate_shared_keystrokes(["2", "}"]).await;
707 cx.assert_shared_state(indoc! {r"abc
708 def
709
710 paragraph
711 the second
712 ˇ
713
714
715 third and
716 final"})
717 .await;
718
719 // goes down over multiple blanks
720 cx.simulate_shared_keystrokes(["}"]).await;
721 cx.assert_shared_state(indoc! {r"abc
722 def
723
724 paragraph
725 the second
726
727
728
729 third and
730 finaˇl"})
731 .await;
732
733 // goes up twice
734 cx.simulate_shared_keystrokes(["2", "{"]).await;
735 cx.assert_shared_state(indoc! {r"abc
736 def
737 ˇ
738 paragraph
739 the second
740
741
742
743 third and
744 final"})
745 .await
746 }
747
748 #[gpui::test]
749 async fn test_matching(cx: &mut gpui::TestAppContext) {
750 let mut cx = NeovimBackedTestContext::new(cx).await;
751
752 cx.set_shared_state(indoc! {r"func ˇ(a string) {
753 do(something(with<Types>.and_arrays[0, 2]))
754 }"})
755 .await;
756 cx.simulate_shared_keystrokes(["%"]).await;
757 cx.assert_shared_state(indoc! {r"func (a stringˇ) {
758 do(something(with<Types>.and_arrays[0, 2]))
759 }"})
760 .await;
761
762 // test it works on the last character of the line
763 cx.set_shared_state(indoc! {r"func (a string) ˇ{
764 do(something(with<Types>.and_arrays[0, 2]))
765 }"})
766 .await;
767 cx.simulate_shared_keystrokes(["%"]).await;
768 cx.assert_shared_state(indoc! {r"func (a string) {
769 do(something(with<Types>.and_arrays[0, 2]))
770 ˇ}"})
771 .await;
772
773 // test it works on immediate nesting
774 cx.set_shared_state("ˇ{()}").await;
775 cx.simulate_shared_keystrokes(["%"]).await;
776 cx.assert_shared_state("{()ˇ}").await;
777 cx.simulate_shared_keystrokes(["%"]).await;
778 cx.assert_shared_state("ˇ{()}").await;
779
780 // test it works on immediate nesting inside braces
781 cx.set_shared_state("{\n ˇ{()}\n}").await;
782 cx.simulate_shared_keystrokes(["%"]).await;
783 cx.assert_shared_state("{\n {()ˇ}\n}").await;
784
785 // test it jumps to the next paren on a line
786 cx.set_shared_state("func ˇboop() {\n}").await;
787 cx.simulate_shared_keystrokes(["%"]).await;
788 cx.assert_shared_state("func boop(ˇ) {\n}").await;
789 }
790
791 #[gpui::test]
792 async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
793 let mut cx = NeovimBackedTestContext::new(cx).await;
794
795 cx.set_shared_state("ˇone two three four").await;
796 cx.simulate_shared_keystrokes(["f", "o"]).await;
797 cx.assert_shared_state("one twˇo three four").await;
798 cx.simulate_shared_keystrokes([","]).await;
799 cx.assert_shared_state("ˇone two three four").await;
800 cx.simulate_shared_keystrokes(["2", ";"]).await;
801 cx.assert_shared_state("one two three fˇour").await;
802 cx.simulate_shared_keystrokes(["shift-t", "e"]).await;
803 cx.assert_shared_state("one two threeˇ four").await;
804 cx.simulate_shared_keystrokes(["3", ";"]).await;
805 cx.assert_shared_state("oneˇ two three four").await;
806 cx.simulate_shared_keystrokes([","]).await;
807 cx.assert_shared_state("one two thˇree four").await;
808 }
809
810 #[gpui::test]
811 async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
812 let mut cx = NeovimBackedTestContext::new(cx).await;
813 cx.set_shared_state("ˇone\n two\nthree").await;
814 cx.simulate_shared_keystrokes(["enter"]).await;
815 cx.assert_shared_state("one\n ˇtwo\nthree").await;
816 }
817}