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(FoldPoint::new(new_row, new_col));
540
541 (map.clip_point(point, Bias::Left), goal)
542}
543
544fn down_display(
545 map: &DisplaySnapshot,
546 mut point: DisplayPoint,
547 mut goal: SelectionGoal,
548 times: usize,
549) -> (DisplayPoint, SelectionGoal) {
550 for _ in 0..times {
551 (point, goal) = movement::down(map, point, goal, true);
552 }
553
554 (point, goal)
555}
556
557pub(crate) fn up(
558 map: &DisplaySnapshot,
559 point: DisplayPoint,
560 mut goal: SelectionGoal,
561 times: usize,
562) -> (DisplayPoint, SelectionGoal) {
563 let start = map.display_point_to_fold_point(point, Bias::Left);
564
565 let goal_column = match goal {
566 SelectionGoal::Column(column) => column,
567 SelectionGoal::ColumnRange { end, .. } => end,
568 _ => {
569 goal = SelectionGoal::Column(start.column());
570 start.column()
571 }
572 };
573
574 let new_row = start.row().saturating_sub(times as u32);
575 let new_col = cmp::min(goal_column, map.fold_snapshot.line_len(new_row));
576 let point = map.fold_point_to_display_point(FoldPoint::new(new_row, new_col));
577
578 (map.clip_point(point, Bias::Left), goal)
579}
580
581fn up_display(
582 map: &DisplaySnapshot,
583 mut point: DisplayPoint,
584 mut goal: SelectionGoal,
585 times: usize,
586) -> (DisplayPoint, SelectionGoal) {
587 for _ in 0..times {
588 (point, goal) = movement::up(map, point, goal, true);
589 }
590
591 (point, goal)
592}
593
594pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
595 for _ in 0..times {
596 let new_point = movement::saturating_right(map, point);
597 if point == new_point {
598 break;
599 }
600 point = new_point;
601 }
602 point
603}
604
605pub(crate) fn next_word_start(
606 map: &DisplaySnapshot,
607 mut point: DisplayPoint,
608 ignore_punctuation: bool,
609 times: usize,
610) -> DisplayPoint {
611 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
612 for _ in 0..times {
613 let mut crossed_newline = false;
614 point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
615 let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
616 let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
617 let at_newline = right == '\n';
618
619 let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
620 || at_newline && crossed_newline
621 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
622
623 crossed_newline |= at_newline;
624 found
625 })
626 }
627 point
628}
629
630fn next_word_end(
631 map: &DisplaySnapshot,
632 mut point: DisplayPoint,
633 ignore_punctuation: bool,
634 times: usize,
635) -> DisplayPoint {
636 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
637 for _ in 0..times {
638 if point.column() < map.line_len(point.row()) {
639 *point.column_mut() += 1;
640 } else if point.row() < map.max_buffer_row() {
641 *point.row_mut() += 1;
642 *point.column_mut() = 0;
643 }
644 point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
645 let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
646 let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
647
648 left_kind != right_kind && left_kind != CharKind::Whitespace
649 });
650
651 // find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
652 // we have backtracked already
653 if !map
654 .chars_at(point)
655 .nth(1)
656 .map(|(c, _)| c == '\n')
657 .unwrap_or(true)
658 {
659 *point.column_mut() = point.column().saturating_sub(1);
660 }
661 point = map.clip_point(point, Bias::Left);
662 }
663 point
664}
665
666fn previous_word_start(
667 map: &DisplaySnapshot,
668 mut point: DisplayPoint,
669 ignore_punctuation: bool,
670 times: usize,
671) -> DisplayPoint {
672 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
673 for _ in 0..times {
674 // This works even though find_preceding_boundary is called for every character in the line containing
675 // cursor because the newline is checked only once.
676 point =
677 movement::find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
678 let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
679 let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
680
681 (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
682 });
683 }
684 point
685}
686
687fn first_non_whitespace(
688 map: &DisplaySnapshot,
689 display_lines: bool,
690 from: DisplayPoint,
691) -> DisplayPoint {
692 let mut last_point = start_of_line(map, display_lines, from);
693 let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
694 for (ch, point) in map.chars_at(last_point) {
695 if ch == '\n' {
696 return from;
697 }
698
699 last_point = point;
700
701 if char_kind(&scope, ch) != CharKind::Whitespace {
702 break;
703 }
704 }
705
706 map.clip_point(last_point, Bias::Left)
707}
708
709pub(crate) fn start_of_line(
710 map: &DisplaySnapshot,
711 display_lines: bool,
712 point: DisplayPoint,
713) -> DisplayPoint {
714 if display_lines {
715 map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
716 } else {
717 map.prev_line_boundary(point.to_point(map)).1
718 }
719}
720
721pub(crate) fn end_of_line(
722 map: &DisplaySnapshot,
723 display_lines: bool,
724 point: DisplayPoint,
725) -> DisplayPoint {
726 if display_lines {
727 map.clip_point(
728 DisplayPoint::new(point.row(), map.line_len(point.row())),
729 Bias::Left,
730 )
731 } else {
732 map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
733 }
734}
735
736fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
737 let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
738 *new_point.column_mut() = point.column();
739 map.clip_point(new_point, Bias::Left)
740}
741
742fn end_of_document(
743 map: &DisplaySnapshot,
744 point: DisplayPoint,
745 line: Option<usize>,
746) -> DisplayPoint {
747 let new_row = if let Some(line) = line {
748 (line - 1) as u32
749 } else {
750 map.max_buffer_row()
751 };
752
753 let new_point = Point::new(new_row, point.column());
754 map.clip_point(new_point.to_display_point(map), Bias::Left)
755}
756
757fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
758 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
759 let point = display_point.to_point(map);
760 let offset = point.to_offset(&map.buffer_snapshot);
761
762 // Ensure the range is contained by the current line.
763 let mut line_end = map.next_line_boundary(point).0;
764 if line_end == point {
765 line_end = map.max_point().to_point(map);
766 }
767
768 let line_range = map.prev_line_boundary(point).0..line_end;
769 let visible_line_range =
770 line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
771 let ranges = map
772 .buffer_snapshot
773 .bracket_ranges(visible_line_range.clone());
774 if let Some(ranges) = ranges {
775 let line_range = line_range.start.to_offset(&map.buffer_snapshot)
776 ..line_range.end.to_offset(&map.buffer_snapshot);
777 let mut closest_pair_destination = None;
778 let mut closest_distance = usize::MAX;
779
780 for (open_range, close_range) in ranges {
781 if open_range.start >= offset && line_range.contains(&open_range.start) {
782 let distance = open_range.start - offset;
783 if distance < closest_distance {
784 closest_pair_destination = Some(close_range.start);
785 closest_distance = distance;
786 continue;
787 }
788 }
789
790 if close_range.start >= offset && line_range.contains(&close_range.start) {
791 let distance = close_range.start - offset;
792 if distance < closest_distance {
793 closest_pair_destination = Some(open_range.start);
794 closest_distance = distance;
795 continue;
796 }
797 }
798
799 continue;
800 }
801
802 closest_pair_destination
803 .map(|destination| destination.to_display_point(map))
804 .unwrap_or(display_point)
805 } else {
806 display_point
807 }
808}
809
810fn find_forward(
811 map: &DisplaySnapshot,
812 from: DisplayPoint,
813 before: bool,
814 target: char,
815 times: usize,
816) -> DisplayPoint {
817 let mut to = from;
818 let mut found = false;
819
820 for _ in 0..times {
821 found = false;
822 to = find_boundary(map, to, FindRange::SingleLine, |_, right| {
823 found = right == target;
824 found
825 });
826 }
827
828 if found {
829 if before && to.column() > 0 {
830 *to.column_mut() -= 1;
831 map.clip_point(to, Bias::Left)
832 } else {
833 to
834 }
835 } else {
836 from
837 }
838}
839
840fn find_backward(
841 map: &DisplaySnapshot,
842 from: DisplayPoint,
843 after: bool,
844 target: char,
845 times: usize,
846) -> DisplayPoint {
847 let mut to = from;
848
849 for _ in 0..times {
850 to = find_preceding_boundary(map, to, FindRange::SingleLine, |_, right| right == target);
851 }
852
853 if map.buffer_snapshot.chars_at(to.to_point(map)).next() == Some(target) {
854 if after {
855 *to.column_mut() += 1;
856 map.clip_point(to, Bias::Right)
857 } else {
858 to
859 }
860 } else {
861 from
862 }
863}
864
865fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
866 let correct_line = down(map, point, SelectionGoal::None, times).0;
867 first_non_whitespace(map, false, correct_line)
868}
869
870fn next_line_end(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
871 if times > 1 {
872 point = down(map, point, SelectionGoal::None, times - 1).0;
873 }
874 end_of_line(map, false, point)
875}
876
877#[cfg(test)]
878
879mod test {
880
881 use crate::test::NeovimBackedTestContext;
882 use indoc::indoc;
883
884 #[gpui::test]
885 async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
886 let mut cx = NeovimBackedTestContext::new(cx).await;
887
888 let initial_state = indoc! {r"ˇabc
889 def
890
891 paragraph
892 the second
893
894
895
896 third and
897 final"};
898
899 // goes down once
900 cx.set_shared_state(initial_state).await;
901 cx.simulate_shared_keystrokes(["}"]).await;
902 cx.assert_shared_state(indoc! {r"abc
903 def
904 ˇ
905 paragraph
906 the second
907
908
909
910 third and
911 final"})
912 .await;
913
914 // goes up once
915 cx.simulate_shared_keystrokes(["{"]).await;
916 cx.assert_shared_state(initial_state).await;
917
918 // goes down twice
919 cx.simulate_shared_keystrokes(["2", "}"]).await;
920 cx.assert_shared_state(indoc! {r"abc
921 def
922
923 paragraph
924 the second
925 ˇ
926
927
928 third and
929 final"})
930 .await;
931
932 // goes down over multiple blanks
933 cx.simulate_shared_keystrokes(["}"]).await;
934 cx.assert_shared_state(indoc! {r"abc
935 def
936
937 paragraph
938 the second
939
940
941
942 third and
943 finaˇl"})
944 .await;
945
946 // goes up twice
947 cx.simulate_shared_keystrokes(["2", "{"]).await;
948 cx.assert_shared_state(indoc! {r"abc
949 def
950 ˇ
951 paragraph
952 the second
953
954
955
956 third and
957 final"})
958 .await
959 }
960
961 #[gpui::test]
962 async fn test_matching(cx: &mut gpui::TestAppContext) {
963 let mut cx = NeovimBackedTestContext::new(cx).await;
964
965 cx.set_shared_state(indoc! {r"func ˇ(a string) {
966 do(something(with<Types>.and_arrays[0, 2]))
967 }"})
968 .await;
969 cx.simulate_shared_keystrokes(["%"]).await;
970 cx.assert_shared_state(indoc! {r"func (a stringˇ) {
971 do(something(with<Types>.and_arrays[0, 2]))
972 }"})
973 .await;
974
975 // test it works on the last character of the line
976 cx.set_shared_state(indoc! {r"func (a string) ˇ{
977 do(something(with<Types>.and_arrays[0, 2]))
978 }"})
979 .await;
980 cx.simulate_shared_keystrokes(["%"]).await;
981 cx.assert_shared_state(indoc! {r"func (a string) {
982 do(something(with<Types>.and_arrays[0, 2]))
983 ˇ}"})
984 .await;
985
986 // test it works on immediate nesting
987 cx.set_shared_state("ˇ{()}").await;
988 cx.simulate_shared_keystrokes(["%"]).await;
989 cx.assert_shared_state("{()ˇ}").await;
990 cx.simulate_shared_keystrokes(["%"]).await;
991 cx.assert_shared_state("ˇ{()}").await;
992
993 // test it works on immediate nesting inside braces
994 cx.set_shared_state("{\n ˇ{()}\n}").await;
995 cx.simulate_shared_keystrokes(["%"]).await;
996 cx.assert_shared_state("{\n {()ˇ}\n}").await;
997
998 // test it jumps to the next paren on a line
999 cx.set_shared_state("func ˇboop() {\n}").await;
1000 cx.simulate_shared_keystrokes(["%"]).await;
1001 cx.assert_shared_state("func boop(ˇ) {\n}").await;
1002 }
1003
1004 #[gpui::test]
1005 async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
1006 let mut cx = NeovimBackedTestContext::new(cx).await;
1007
1008 cx.set_shared_state("ˇone two three four").await;
1009 cx.simulate_shared_keystrokes(["f", "o"]).await;
1010 cx.assert_shared_state("one twˇo three four").await;
1011 cx.simulate_shared_keystrokes([","]).await;
1012 cx.assert_shared_state("ˇone two three four").await;
1013 cx.simulate_shared_keystrokes(["2", ";"]).await;
1014 cx.assert_shared_state("one two three fˇour").await;
1015 cx.simulate_shared_keystrokes(["shift-t", "e"]).await;
1016 cx.assert_shared_state("one two threeˇ four").await;
1017 cx.simulate_shared_keystrokes(["3", ";"]).await;
1018 cx.assert_shared_state("oneˇ two three four").await;
1019 cx.simulate_shared_keystrokes([","]).await;
1020 cx.assert_shared_state("one two thˇree four").await;
1021 }
1022
1023 #[gpui::test]
1024 async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
1025 let mut cx = NeovimBackedTestContext::new(cx).await;
1026 cx.set_shared_state("ˇone\n two\nthree").await;
1027 cx.simulate_shared_keystrokes(["enter"]).await;
1028 cx.assert_shared_state("one\n ˇtwo\nthree").await;
1029 }
1030}