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