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