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