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