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