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