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