1use editor::{
2 display_map::{DisplaySnapshot, FoldPoint, ToDisplayPoint},
3 movement::{self, find_boundary, find_preceding_boundary, FindRange, TextLayoutDetails},
4 Bias, DisplayPoint, ToOffset,
5};
6use gpui::{actions, impl_actions, px, ViewContext, WindowContext};
7use language::{char_kind, CharKind, Point, Selection, SelectionGoal};
8use serde::Deserialize;
9use workspace::Workspace;
10
11use crate::{
12 normal::normal_motion,
13 state::{Mode, Operator},
14 utils::coerce_punctuation,
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 RepeatFind { last_find: Box<Motion> },
41 RepeatFindReversed { last_find: Box<Motion> },
42 NextLineStart,
43 StartOfLineDownward,
44 EndOfLineDownward,
45 GoToColumn,
46 WindowTop,
47 WindowMiddle,
48 WindowBottom,
49}
50
51#[derive(Clone, Deserialize, PartialEq)]
52#[serde(rename_all = "camelCase")]
53struct NextWordStart {
54 #[serde(default)]
55 ignore_punctuation: bool,
56}
57
58#[derive(Clone, Deserialize, PartialEq)]
59#[serde(rename_all = "camelCase")]
60struct NextWordEnd {
61 #[serde(default)]
62 ignore_punctuation: bool,
63}
64
65#[derive(Clone, Deserialize, PartialEq)]
66#[serde(rename_all = "camelCase")]
67struct PreviousWordStart {
68 #[serde(default)]
69 ignore_punctuation: bool,
70}
71
72#[derive(Clone, Deserialize, PartialEq)]
73#[serde(rename_all = "camelCase")]
74pub(crate) struct Up {
75 #[serde(default)]
76 pub(crate) display_lines: bool,
77}
78
79#[derive(Clone, Deserialize, PartialEq)]
80#[serde(rename_all = "camelCase")]
81pub(crate) struct Down {
82 #[serde(default)]
83 pub(crate) display_lines: bool,
84}
85
86#[derive(Clone, Deserialize, PartialEq)]
87#[serde(rename_all = "camelCase")]
88struct FirstNonWhitespace {
89 #[serde(default)]
90 display_lines: bool,
91}
92
93#[derive(Clone, Deserialize, PartialEq)]
94#[serde(rename_all = "camelCase")]
95struct EndOfLine {
96 #[serde(default)]
97 display_lines: bool,
98}
99
100#[derive(Clone, Deserialize, PartialEq)]
101#[serde(rename_all = "camelCase")]
102pub struct StartOfLine {
103 #[serde(default)]
104 pub(crate) display_lines: bool,
105}
106
107impl_actions!(
108 vim,
109 [
110 StartOfLine,
111 EndOfLine,
112 FirstNonWhitespace,
113 Down,
114 Up,
115 PreviousWordStart,
116 NextWordEnd,
117 NextWordStart
118 ]
119);
120
121actions!(
122 vim,
123 [
124 Left,
125 Backspace,
126 Right,
127 CurrentLine,
128 StartOfParagraph,
129 EndOfParagraph,
130 StartOfDocument,
131 EndOfDocument,
132 Matching,
133 NextLineStart,
134 StartOfLineDownward,
135 EndOfLineDownward,
136 GoToColumn,
137 RepeatFind,
138 RepeatFindReversed,
139 WindowTop,
140 WindowMiddle,
141 WindowBottom,
142 ]
143);
144
145pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
146 workspace.register_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx));
147 workspace
148 .register_action(|_: &mut Workspace, _: &Backspace, cx: _| motion(Motion::Backspace, cx));
149 workspace.register_action(|_: &mut Workspace, action: &Down, cx: _| {
150 motion(
151 Motion::Down {
152 display_lines: action.display_lines,
153 },
154 cx,
155 )
156 });
157 workspace.register_action(|_: &mut Workspace, action: &Up, cx: _| {
158 motion(
159 Motion::Up {
160 display_lines: action.display_lines,
161 },
162 cx,
163 )
164 });
165 workspace.register_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx));
166 workspace.register_action(|_: &mut Workspace, action: &FirstNonWhitespace, cx: _| {
167 motion(
168 Motion::FirstNonWhitespace {
169 display_lines: action.display_lines,
170 },
171 cx,
172 )
173 });
174 workspace.register_action(|_: &mut Workspace, action: &StartOfLine, cx: _| {
175 motion(
176 Motion::StartOfLine {
177 display_lines: action.display_lines,
178 },
179 cx,
180 )
181 });
182 workspace.register_action(|_: &mut Workspace, action: &EndOfLine, cx: _| {
183 motion(
184 Motion::EndOfLine {
185 display_lines: action.display_lines,
186 },
187 cx,
188 )
189 });
190 workspace.register_action(|_: &mut Workspace, _: &CurrentLine, cx: _| {
191 motion(Motion::CurrentLine, cx)
192 });
193 workspace.register_action(|_: &mut Workspace, _: &StartOfParagraph, cx: _| {
194 motion(Motion::StartOfParagraph, cx)
195 });
196 workspace.register_action(|_: &mut Workspace, _: &EndOfParagraph, cx: _| {
197 motion(Motion::EndOfParagraph, cx)
198 });
199 workspace.register_action(|_: &mut Workspace, _: &StartOfDocument, cx: _| {
200 motion(Motion::StartOfDocument, cx)
201 });
202 workspace.register_action(|_: &mut Workspace, _: &EndOfDocument, cx: _| {
203 motion(Motion::EndOfDocument, cx)
204 });
205 workspace
206 .register_action(|_: &mut Workspace, _: &Matching, cx: _| motion(Motion::Matching, cx));
207
208 workspace.register_action(
209 |_: &mut Workspace, &NextWordStart { ignore_punctuation }: &NextWordStart, cx: _| {
210 motion(Motion::NextWordStart { ignore_punctuation }, cx)
211 },
212 );
213 workspace.register_action(
214 |_: &mut Workspace, &NextWordEnd { ignore_punctuation }: &NextWordEnd, cx: _| {
215 motion(Motion::NextWordEnd { ignore_punctuation }, cx)
216 },
217 );
218 workspace.register_action(
219 |_: &mut Workspace,
220 &PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
221 cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
222 );
223 workspace.register_action(|_: &mut Workspace, &NextLineStart, cx: _| {
224 motion(Motion::NextLineStart, cx)
225 });
226 workspace.register_action(|_: &mut Workspace, &StartOfLineDownward, cx: _| {
227 motion(Motion::StartOfLineDownward, cx)
228 });
229 workspace.register_action(|_: &mut Workspace, &EndOfLineDownward, cx: _| {
230 motion(Motion::EndOfLineDownward, cx)
231 });
232 workspace
233 .register_action(|_: &mut Workspace, &GoToColumn, cx: _| motion(Motion::GoToColumn, cx));
234
235 workspace.register_action(|_: &mut Workspace, _: &RepeatFind, cx: _| {
236 if let Some(last_find) = Vim::read(cx)
237 .workspace_state
238 .last_find
239 .clone()
240 .map(Box::new)
241 {
242 motion(Motion::RepeatFind { last_find }, cx);
243 }
244 });
245
246 workspace.register_action(|_: &mut Workspace, _: &RepeatFindReversed, cx: _| {
247 if let Some(last_find) = Vim::read(cx)
248 .workspace_state
249 .last_find
250 .clone()
251 .map(Box::new)
252 {
253 motion(Motion::RepeatFindReversed { last_find }, cx);
254 }
255 });
256 workspace.register_action(|_: &mut Workspace, &WindowTop, cx: _| motion(Motion::WindowTop, cx));
257 workspace.register_action(|_: &mut Workspace, &WindowMiddle, cx: _| {
258 motion(Motion::WindowMiddle, cx)
259 });
260 workspace.register_action(|_: &mut Workspace, &WindowBottom, cx: _| {
261 motion(Motion::WindowBottom, cx)
262 });
263}
264
265pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
266 if let Some(Operator::FindForward { .. }) | Some(Operator::FindBackward { .. }) =
267 Vim::read(cx).active_operator()
268 {
269 Vim::update(cx, |vim, cx| vim.pop_operator(cx));
270 }
271
272 let count = Vim::update(cx, |vim, cx| vim.take_count(cx));
273 let operator = Vim::read(cx).active_operator();
274 match Vim::read(cx).state().mode {
275 Mode::Normal => normal_motion(motion, operator, count, cx),
276 Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_motion(motion, count, cx),
277 Mode::Insert => {
278 // Shouldn't execute a motion in insert mode. Ignoring
279 }
280 }
281 Vim::update(cx, |vim, cx| vim.clear_operator(cx));
282}
283
284// Motion handling is specified here:
285// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
286impl Motion {
287 pub fn linewise(&self) -> bool {
288 use Motion::*;
289 match self {
290 Down { .. }
291 | Up { .. }
292 | StartOfDocument
293 | EndOfDocument
294 | CurrentLine
295 | NextLineStart
296 | StartOfLineDownward
297 | StartOfParagraph
298 | WindowTop
299 | WindowMiddle
300 | WindowBottom
301 | EndOfParagraph => true,
302 EndOfLine { .. }
303 | NextWordEnd { .. }
304 | Matching
305 | FindForward { .. }
306 | Left
307 | Backspace
308 | Right
309 | StartOfLine { .. }
310 | EndOfLineDownward
311 | GoToColumn
312 | NextWordStart { .. }
313 | PreviousWordStart { .. }
314 | FirstNonWhitespace { .. }
315 | FindBackward { .. }
316 | RepeatFind { .. }
317 | RepeatFindReversed { .. } => false,
318 }
319 }
320
321 pub fn infallible(&self) -> bool {
322 use Motion::*;
323 match self {
324 StartOfDocument | EndOfDocument | CurrentLine => true,
325 Down { .. }
326 | Up { .. }
327 | EndOfLine { .. }
328 | NextWordEnd { .. }
329 | Matching
330 | FindForward { .. }
331 | RepeatFind { .. }
332 | Left
333 | Backspace
334 | Right
335 | StartOfLine { .. }
336 | StartOfParagraph
337 | EndOfParagraph
338 | StartOfLineDownward
339 | EndOfLineDownward
340 | GoToColumn
341 | NextWordStart { .. }
342 | PreviousWordStart { .. }
343 | FirstNonWhitespace { .. }
344 | FindBackward { .. }
345 | RepeatFindReversed { .. }
346 | WindowTop
347 | WindowMiddle
348 | WindowBottom
349 | NextLineStart => false,
350 }
351 }
352
353 pub fn inclusive(&self) -> bool {
354 use Motion::*;
355 match self {
356 Down { .. }
357 | Up { .. }
358 | StartOfDocument
359 | EndOfDocument
360 | CurrentLine
361 | EndOfLine { .. }
362 | EndOfLineDownward
363 | NextWordEnd { .. }
364 | Matching
365 | FindForward { .. }
366 | WindowTop
367 | WindowMiddle
368 | WindowBottom
369 | NextLineStart => true,
370 Left
371 | Backspace
372 | Right
373 | StartOfLine { .. }
374 | StartOfLineDownward
375 | StartOfParagraph
376 | EndOfParagraph
377 | GoToColumn
378 | NextWordStart { .. }
379 | PreviousWordStart { .. }
380 | FirstNonWhitespace { .. }
381 | FindBackward { .. } => false,
382 RepeatFind { last_find: motion } | RepeatFindReversed { last_find: motion } => {
383 motion.inclusive()
384 }
385 }
386 }
387
388 pub fn move_point(
389 &self,
390 map: &DisplaySnapshot,
391 point: DisplayPoint,
392 goal: SelectionGoal,
393 maybe_times: Option<usize>,
394 text_layout_details: &TextLayoutDetails,
395 ) -> Option<(DisplayPoint, SelectionGoal)> {
396 let times = maybe_times.unwrap_or(1);
397 use Motion::*;
398 let infallible = self.infallible();
399 let (new_point, goal) = match self {
400 Left => (left(map, point, times), SelectionGoal::None),
401 Backspace => (backspace(map, point, times), SelectionGoal::None),
402 Down {
403 display_lines: false,
404 } => up_down_buffer_rows(map, point, goal, times as isize, &text_layout_details),
405 Down {
406 display_lines: true,
407 } => down_display(map, point, goal, times, &text_layout_details),
408 Up {
409 display_lines: false,
410 } => up_down_buffer_rows(map, point, goal, 0 - times as isize, &text_layout_details),
411 Up {
412 display_lines: true,
413 } => up_display(map, point, goal, times, &text_layout_details),
414 Right => (right(map, point, times), SelectionGoal::None),
415 NextWordStart { ignore_punctuation } => (
416 next_word_start(map, point, *ignore_punctuation, times),
417 SelectionGoal::None,
418 ),
419 NextWordEnd { ignore_punctuation } => (
420 next_word_end(map, point, *ignore_punctuation, times),
421 SelectionGoal::None,
422 ),
423 PreviousWordStart { ignore_punctuation } => (
424 previous_word_start(map, point, *ignore_punctuation, times),
425 SelectionGoal::None,
426 ),
427 FirstNonWhitespace { display_lines } => (
428 first_non_whitespace(map, *display_lines, point),
429 SelectionGoal::None,
430 ),
431 StartOfLine { display_lines } => (
432 start_of_line(map, *display_lines, point),
433 SelectionGoal::None,
434 ),
435 EndOfLine { display_lines } => {
436 (end_of_line(map, *display_lines, point), SelectionGoal::None)
437 }
438 StartOfParagraph => (
439 movement::start_of_paragraph(map, point, times),
440 SelectionGoal::None,
441 ),
442 EndOfParagraph => (
443 map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
444 SelectionGoal::None,
445 ),
446 CurrentLine => (next_line_end(map, point, times), SelectionGoal::None),
447 StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
448 EndOfDocument => (
449 end_of_document(map, point, maybe_times),
450 SelectionGoal::None,
451 ),
452 Matching => (matching(map, point), SelectionGoal::None),
453 // t f
454 FindForward { before, char } => {
455 return find_forward(map, point, *before, *char, times)
456 .map(|new_point| (new_point, SelectionGoal::None))
457 }
458 // T F
459 FindBackward { after, char } => (
460 find_backward(map, point, *after, *char, times),
461 SelectionGoal::None,
462 ),
463 // ; -- repeat the last find done with t, f, T, F
464 RepeatFind { last_find } => match **last_find {
465 Motion::FindForward { before, char } => {
466 let mut new_point = find_forward(map, point, before, char, times);
467 if new_point == Some(point) {
468 new_point = find_forward(map, point, before, char, times + 1);
469 }
470
471 return new_point.map(|new_point| (new_point, SelectionGoal::None));
472 }
473
474 Motion::FindBackward { after, char } => {
475 let mut new_point = find_backward(map, point, after, char, times);
476 if new_point == point {
477 new_point = find_backward(map, point, after, char, times + 1);
478 }
479
480 (new_point, SelectionGoal::None)
481 }
482 _ => return None,
483 },
484 // , -- repeat the last find done with t, f, T, F, in opposite direction
485 RepeatFindReversed { last_find } => match **last_find {
486 Motion::FindForward { before, char } => {
487 let mut new_point = find_backward(map, point, before, char, times);
488 if new_point == point {
489 new_point = find_backward(map, point, before, char, times + 1);
490 }
491
492 (new_point, SelectionGoal::None)
493 }
494
495 Motion::FindBackward { after, char } => {
496 let mut new_point = find_forward(map, point, after, char, times);
497 if new_point == Some(point) {
498 new_point = find_forward(map, point, after, char, times + 1);
499 }
500
501 return new_point.map(|new_point| (new_point, SelectionGoal::None));
502 }
503 _ => return None,
504 },
505 NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
506 StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None),
507 EndOfLineDownward => (next_line_end(map, point, times), SelectionGoal::None),
508 GoToColumn => (go_to_column(map, point, times), SelectionGoal::None),
509 WindowTop => window_top(map, point, &text_layout_details),
510 WindowMiddle => window_middle(map, point, &text_layout_details),
511 WindowBottom => window_bottom(map, point, &text_layout_details),
512 };
513
514 (new_point != point || infallible).then_some((new_point, goal))
515 }
516
517 // Expands a selection using self motion for an operator
518 pub fn expand_selection(
519 &self,
520 map: &DisplaySnapshot,
521 selection: &mut Selection<DisplayPoint>,
522 times: Option<usize>,
523 expand_to_surrounding_newline: bool,
524 text_layout_details: &TextLayoutDetails,
525 ) -> bool {
526 if let Some((new_head, goal)) = self.move_point(
527 map,
528 selection.head(),
529 selection.goal,
530 times,
531 &text_layout_details,
532 ) {
533 selection.set_head(new_head, goal);
534
535 if self.linewise() {
536 selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
537
538 if expand_to_surrounding_newline {
539 if selection.end.row() < map.max_point().row() {
540 *selection.end.row_mut() += 1;
541 *selection.end.column_mut() = 0;
542 selection.end = map.clip_point(selection.end, Bias::Right);
543 // Don't reset the end here
544 return true;
545 } else if selection.start.row() > 0 {
546 *selection.start.row_mut() -= 1;
547 *selection.start.column_mut() = map.line_len(selection.start.row());
548 selection.start = map.clip_point(selection.start, Bias::Left);
549 }
550 }
551
552 (_, selection.end) = map.next_line_boundary(selection.end.to_point(map));
553 } else {
554 // Another special case: When using the "w" motion in combination with an
555 // operator and the last word moved over is at the end of a line, the end of
556 // that word becomes the end of the operated text, not the first word in the
557 // next line.
558 if let Motion::NextWordStart {
559 ignore_punctuation: _,
560 } = self
561 {
562 let start_row = selection.start.to_point(&map).row;
563 if selection.end.to_point(&map).row > start_row {
564 selection.end =
565 Point::new(start_row, map.buffer_snapshot.line_len(start_row))
566 .to_display_point(&map)
567 }
568 }
569
570 // If the motion is exclusive and the end of the motion is in column 1, the
571 // end of the motion is moved to the end of the previous line and the motion
572 // becomes inclusive. Example: "}" moves to the first line after a paragraph,
573 // but "d}" will not include that line.
574 let mut inclusive = self.inclusive();
575 if !inclusive
576 && self != &Motion::Backspace
577 && selection.end.row() > selection.start.row()
578 && selection.end.column() == 0
579 {
580 inclusive = true;
581 *selection.end.row_mut() -= 1;
582 *selection.end.column_mut() = 0;
583 selection.end = map.clip_point(
584 map.next_line_boundary(selection.end.to_point(map)).1,
585 Bias::Left,
586 );
587 }
588
589 if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
590 *selection.end.column_mut() += 1;
591 }
592 }
593 true
594 } else {
595 false
596 }
597 }
598}
599
600fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
601 for _ in 0..times {
602 point = movement::saturating_left(map, point);
603 if point.column() == 0 {
604 break;
605 }
606 }
607 point
608}
609
610fn backspace(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
611 for _ in 0..times {
612 point = movement::left(map, point);
613 }
614 point
615}
616
617pub(crate) fn start_of_relative_buffer_row(
618 map: &DisplaySnapshot,
619 point: DisplayPoint,
620 times: isize,
621) -> DisplayPoint {
622 let start = map.display_point_to_fold_point(point, Bias::Left);
623 let target = start.row() as isize + times;
624 let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
625
626 map.clip_point(
627 map.fold_point_to_display_point(
628 map.fold_snapshot
629 .clip_point(FoldPoint::new(new_row, 0), Bias::Right),
630 ),
631 Bias::Right,
632 )
633}
634
635fn up_down_buffer_rows(
636 map: &DisplaySnapshot,
637 point: DisplayPoint,
638 mut goal: SelectionGoal,
639 times: isize,
640 text_layout_details: &TextLayoutDetails,
641) -> (DisplayPoint, SelectionGoal) {
642 let start = map.display_point_to_fold_point(point, Bias::Left);
643 let begin_folded_line = map.fold_point_to_display_point(
644 map.fold_snapshot
645 .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
646 );
647 let select_nth_wrapped_row = point.row() - begin_folded_line.row();
648
649 let (goal_wrap, goal_x) = match goal {
650 SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
651 SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
652 SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
653 _ => {
654 let x = map.x_for_display_point(point, text_layout_details);
655 goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x.0));
656 (select_nth_wrapped_row, x.0)
657 }
658 };
659
660 let target = start.row() as isize + times;
661 let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
662
663 let mut begin_folded_line = map.fold_point_to_display_point(
664 map.fold_snapshot
665 .clip_point(FoldPoint::new(new_row, 0), Bias::Left),
666 );
667
668 let mut i = 0;
669 while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
670 let next_folded_line = DisplayPoint::new(begin_folded_line.row() + 1, 0);
671 if map
672 .display_point_to_fold_point(next_folded_line, Bias::Right)
673 .row()
674 == new_row
675 {
676 i += 1;
677 begin_folded_line = next_folded_line;
678 } else {
679 break;
680 }
681 }
682
683 let new_col = if i == goal_wrap {
684 map.display_column_for_x(begin_folded_line.row(), px(goal_x), text_layout_details)
685 } else {
686 map.line_len(begin_folded_line.row())
687 };
688
689 (
690 map.clip_point(
691 DisplayPoint::new(begin_folded_line.row(), new_col),
692 Bias::Left,
693 ),
694 goal,
695 )
696}
697
698fn down_display(
699 map: &DisplaySnapshot,
700 mut point: DisplayPoint,
701 mut goal: SelectionGoal,
702 times: usize,
703 text_layout_details: &TextLayoutDetails,
704) -> (DisplayPoint, SelectionGoal) {
705 for _ in 0..times {
706 (point, goal) = movement::down(map, point, goal, true, text_layout_details);
707 }
708
709 (point, goal)
710}
711
712fn up_display(
713 map: &DisplaySnapshot,
714 mut point: DisplayPoint,
715 mut goal: SelectionGoal,
716 times: usize,
717 text_layout_details: &TextLayoutDetails,
718) -> (DisplayPoint, SelectionGoal) {
719 for _ in 0..times {
720 (point, goal) = movement::up(map, point, goal, true, &text_layout_details);
721 }
722
723 (point, goal)
724}
725
726pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
727 for _ in 0..times {
728 let new_point = movement::saturating_right(map, point);
729 if point == new_point {
730 break;
731 }
732 point = new_point;
733 }
734 point
735}
736
737pub(crate) fn next_word_start(
738 map: &DisplaySnapshot,
739 mut point: DisplayPoint,
740 ignore_punctuation: bool,
741 times: usize,
742) -> DisplayPoint {
743 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
744 for _ in 0..times {
745 let mut crossed_newline = false;
746 point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
747 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
748 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
749 let at_newline = right == '\n';
750
751 let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
752 || at_newline && crossed_newline
753 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
754
755 crossed_newline |= at_newline;
756 found
757 })
758 }
759 point
760}
761
762fn next_word_end(
763 map: &DisplaySnapshot,
764 mut point: DisplayPoint,
765 ignore_punctuation: bool,
766 times: usize,
767) -> DisplayPoint {
768 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
769 for _ in 0..times {
770 if point.column() < map.line_len(point.row()) {
771 *point.column_mut() += 1;
772 } else if point.row() < map.max_buffer_row() {
773 *point.row_mut() += 1;
774 *point.column_mut() = 0;
775 }
776 point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
777 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
778 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
779
780 left_kind != right_kind && left_kind != CharKind::Whitespace
781 });
782
783 // find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
784 // we have backtracked already
785 if !map
786 .chars_at(point)
787 .nth(1)
788 .map(|(c, _)| c == '\n')
789 .unwrap_or(true)
790 {
791 *point.column_mut() = point.column().saturating_sub(1);
792 }
793 point = map.clip_point(point, Bias::Left);
794 }
795 point
796}
797
798fn previous_word_start(
799 map: &DisplaySnapshot,
800 mut point: DisplayPoint,
801 ignore_punctuation: bool,
802 times: usize,
803) -> DisplayPoint {
804 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
805 for _ in 0..times {
806 // This works even though find_preceding_boundary is called for every character in the line containing
807 // cursor because the newline is checked only once.
808 point =
809 movement::find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
810 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
811 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
812
813 (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
814 });
815 }
816 point
817}
818
819pub(crate) fn first_non_whitespace(
820 map: &DisplaySnapshot,
821 display_lines: bool,
822 from: DisplayPoint,
823) -> DisplayPoint {
824 let mut last_point = start_of_line(map, display_lines, from);
825 let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
826 for (ch, point) in map.chars_at(last_point) {
827 if ch == '\n' {
828 return from;
829 }
830
831 last_point = point;
832
833 if char_kind(&scope, ch) != CharKind::Whitespace {
834 break;
835 }
836 }
837
838 map.clip_point(last_point, Bias::Left)
839}
840
841pub(crate) fn start_of_line(
842 map: &DisplaySnapshot,
843 display_lines: bool,
844 point: DisplayPoint,
845) -> DisplayPoint {
846 if display_lines {
847 map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
848 } else {
849 map.prev_line_boundary(point.to_point(map)).1
850 }
851}
852
853pub(crate) fn end_of_line(
854 map: &DisplaySnapshot,
855 display_lines: bool,
856 point: DisplayPoint,
857) -> DisplayPoint {
858 if display_lines {
859 map.clip_point(
860 DisplayPoint::new(point.row(), map.line_len(point.row())),
861 Bias::Left,
862 )
863 } else {
864 map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
865 }
866}
867
868fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
869 let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
870 *new_point.column_mut() = point.column();
871 map.clip_point(new_point, Bias::Left)
872}
873
874fn end_of_document(
875 map: &DisplaySnapshot,
876 point: DisplayPoint,
877 line: Option<usize>,
878) -> DisplayPoint {
879 let new_row = if let Some(line) = line {
880 (line - 1) as u32
881 } else {
882 map.max_buffer_row()
883 };
884
885 let new_point = Point::new(new_row, point.column());
886 map.clip_point(new_point.to_display_point(map), Bias::Left)
887}
888
889fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
890 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
891 let point = display_point.to_point(map);
892 let offset = point.to_offset(&map.buffer_snapshot);
893
894 // Ensure the range is contained by the current line.
895 let mut line_end = map.next_line_boundary(point).0;
896 if line_end == point {
897 line_end = map.max_point().to_point(map);
898 }
899
900 let line_range = map.prev_line_boundary(point).0..line_end;
901 let visible_line_range =
902 line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
903 let ranges = map
904 .buffer_snapshot
905 .bracket_ranges(visible_line_range.clone());
906 if let Some(ranges) = ranges {
907 let line_range = line_range.start.to_offset(&map.buffer_snapshot)
908 ..line_range.end.to_offset(&map.buffer_snapshot);
909 let mut closest_pair_destination = None;
910 let mut closest_distance = usize::MAX;
911
912 for (open_range, close_range) in ranges {
913 if open_range.start >= offset && line_range.contains(&open_range.start) {
914 let distance = open_range.start - offset;
915 if distance < closest_distance {
916 closest_pair_destination = Some(close_range.start);
917 closest_distance = distance;
918 continue;
919 }
920 }
921
922 if close_range.start >= offset && line_range.contains(&close_range.start) {
923 let distance = close_range.start - offset;
924 if distance < closest_distance {
925 closest_pair_destination = Some(open_range.start);
926 closest_distance = distance;
927 continue;
928 }
929 }
930
931 continue;
932 }
933
934 closest_pair_destination
935 .map(|destination| destination.to_display_point(map))
936 .unwrap_or(display_point)
937 } else {
938 display_point
939 }
940}
941
942fn find_forward(
943 map: &DisplaySnapshot,
944 from: DisplayPoint,
945 before: bool,
946 target: char,
947 times: usize,
948) -> Option<DisplayPoint> {
949 let mut to = from;
950 let mut found = false;
951
952 for _ in 0..times {
953 found = false;
954 to = find_boundary(map, to, FindRange::SingleLine, |_, right| {
955 found = right == target;
956 found
957 });
958 }
959
960 if found {
961 if before && to.column() > 0 {
962 *to.column_mut() -= 1;
963 Some(map.clip_point(to, Bias::Left))
964 } else {
965 Some(to)
966 }
967 } else {
968 None
969 }
970}
971
972fn find_backward(
973 map: &DisplaySnapshot,
974 from: DisplayPoint,
975 after: bool,
976 target: char,
977 times: usize,
978) -> DisplayPoint {
979 let mut to = from;
980
981 for _ in 0..times {
982 to = find_preceding_boundary(map, to, FindRange::SingleLine, |_, right| right == target);
983 }
984
985 if map.buffer_snapshot.chars_at(to.to_point(map)).next() == Some(target) {
986 if after {
987 *to.column_mut() += 1;
988 map.clip_point(to, Bias::Right)
989 } else {
990 to
991 }
992 } else {
993 from
994 }
995}
996
997fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
998 let correct_line = start_of_relative_buffer_row(map, point, times as isize);
999 first_non_whitespace(map, false, correct_line)
1000}
1001
1002fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1003 let correct_line = start_of_relative_buffer_row(map, point, 0);
1004 right(map, correct_line, times.saturating_sub(1))
1005}
1006
1007pub(crate) fn next_line_end(
1008 map: &DisplaySnapshot,
1009 mut point: DisplayPoint,
1010 times: usize,
1011) -> DisplayPoint {
1012 if times > 1 {
1013 point = start_of_relative_buffer_row(map, point, times as isize - 1);
1014 }
1015 end_of_line(map, false, point)
1016}
1017
1018fn window_top(
1019 map: &DisplaySnapshot,
1020 point: DisplayPoint,
1021 text_layout_details: &TextLayoutDetails,
1022) -> (DisplayPoint, SelectionGoal) {
1023 let first_visible_line = text_layout_details.anchor.to_display_point(map);
1024 let new_col = point.column().min(map.line_len(first_visible_line.row()));
1025 let new_point = DisplayPoint::new(first_visible_line.row(), new_col);
1026 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1027}
1028
1029fn window_middle(
1030 map: &DisplaySnapshot,
1031 point: DisplayPoint,
1032 text_layout_details: &TextLayoutDetails,
1033) -> (DisplayPoint, SelectionGoal) {
1034 if let Some(visible_rows) = text_layout_details.visible_rows {
1035 let first_visible_line = text_layout_details.anchor.to_display_point(map);
1036 let max_rows = (visible_rows as u32).min(map.max_buffer_row());
1037 let new_row = first_visible_line.row() + (max_rows.div_euclid(2));
1038 let new_col = point.column().min(map.line_len(new_row));
1039 let new_point = DisplayPoint::new(new_row, new_col);
1040 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1041 } else {
1042 (point, SelectionGoal::None)
1043 }
1044}
1045
1046fn window_bottom(
1047 map: &DisplaySnapshot,
1048 point: DisplayPoint,
1049 text_layout_details: &TextLayoutDetails,
1050) -> (DisplayPoint, SelectionGoal) {
1051 if let Some(visible_rows) = text_layout_details.visible_rows {
1052 let first_visible_line = text_layout_details.anchor.to_display_point(map);
1053 let bottom_row = first_visible_line.row() + (visible_rows) as u32;
1054 let bottom_row_capped = bottom_row.min(map.max_buffer_row());
1055 let new_col = point.column().min(map.line_len(bottom_row_capped));
1056 let new_point = DisplayPoint::new(bottom_row_capped, new_col);
1057 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1058 } else {
1059 (point, SelectionGoal::None)
1060 }
1061}
1062
1063#[cfg(test)]
1064mod test {
1065
1066 use crate::test::NeovimBackedTestContext;
1067 use indoc::indoc;
1068
1069 #[gpui::test]
1070 async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
1071 let mut cx = NeovimBackedTestContext::new(cx).await;
1072
1073 let initial_state = indoc! {r"ˇabc
1074 def
1075
1076 paragraph
1077 the second
1078
1079
1080
1081 third and
1082 final"};
1083
1084 // goes down once
1085 cx.set_shared_state(initial_state).await;
1086 cx.simulate_shared_keystrokes(["}"]).await;
1087 cx.assert_shared_state(indoc! {r"abc
1088 def
1089 ˇ
1090 paragraph
1091 the second
1092
1093
1094
1095 third and
1096 final"})
1097 .await;
1098
1099 // goes up once
1100 cx.simulate_shared_keystrokes(["{"]).await;
1101 cx.assert_shared_state(initial_state).await;
1102
1103 // goes down twice
1104 cx.simulate_shared_keystrokes(["2", "}"]).await;
1105 cx.assert_shared_state(indoc! {r"abc
1106 def
1107
1108 paragraph
1109 the second
1110 ˇ
1111
1112
1113 third and
1114 final"})
1115 .await;
1116
1117 // goes down over multiple blanks
1118 cx.simulate_shared_keystrokes(["}"]).await;
1119 cx.assert_shared_state(indoc! {r"abc
1120 def
1121
1122 paragraph
1123 the second
1124
1125
1126
1127 third and
1128 finaˇl"})
1129 .await;
1130
1131 // goes up twice
1132 cx.simulate_shared_keystrokes(["2", "{"]).await;
1133 cx.assert_shared_state(indoc! {r"abc
1134 def
1135 ˇ
1136 paragraph
1137 the second
1138
1139
1140
1141 third and
1142 final"})
1143 .await
1144 }
1145
1146 #[gpui::test]
1147 async fn test_matching(cx: &mut gpui::TestAppContext) {
1148 let mut cx = NeovimBackedTestContext::new(cx).await;
1149
1150 cx.set_shared_state(indoc! {r"func ˇ(a string) {
1151 do(something(with<Types>.and_arrays[0, 2]))
1152 }"})
1153 .await;
1154 cx.simulate_shared_keystrokes(["%"]).await;
1155 cx.assert_shared_state(indoc! {r"func (a stringˇ) {
1156 do(something(with<Types>.and_arrays[0, 2]))
1157 }"})
1158 .await;
1159
1160 // test it works on the last character of the line
1161 cx.set_shared_state(indoc! {r"func (a string) ˇ{
1162 do(something(with<Types>.and_arrays[0, 2]))
1163 }"})
1164 .await;
1165 cx.simulate_shared_keystrokes(["%"]).await;
1166 cx.assert_shared_state(indoc! {r"func (a string) {
1167 do(something(with<Types>.and_arrays[0, 2]))
1168 ˇ}"})
1169 .await;
1170
1171 // test it works on immediate nesting
1172 cx.set_shared_state("ˇ{()}").await;
1173 cx.simulate_shared_keystrokes(["%"]).await;
1174 cx.assert_shared_state("{()ˇ}").await;
1175 cx.simulate_shared_keystrokes(["%"]).await;
1176 cx.assert_shared_state("ˇ{()}").await;
1177
1178 // test it works on immediate nesting inside braces
1179 cx.set_shared_state("{\n ˇ{()}\n}").await;
1180 cx.simulate_shared_keystrokes(["%"]).await;
1181 cx.assert_shared_state("{\n {()ˇ}\n}").await;
1182
1183 // test it jumps to the next paren on a line
1184 cx.set_shared_state("func ˇboop() {\n}").await;
1185 cx.simulate_shared_keystrokes(["%"]).await;
1186 cx.assert_shared_state("func boop(ˇ) {\n}").await;
1187 }
1188
1189 #[gpui::test]
1190 async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
1191 let mut cx = NeovimBackedTestContext::new(cx).await;
1192
1193 // f and F
1194 cx.set_shared_state("ˇone two three four").await;
1195 cx.simulate_shared_keystrokes(["f", "o"]).await;
1196 cx.assert_shared_state("one twˇo three four").await;
1197 cx.simulate_shared_keystrokes([","]).await;
1198 cx.assert_shared_state("ˇone two three four").await;
1199 cx.simulate_shared_keystrokes(["2", ";"]).await;
1200 cx.assert_shared_state("one two three fˇour").await;
1201 cx.simulate_shared_keystrokes(["shift-f", "e"]).await;
1202 cx.assert_shared_state("one two threˇe four").await;
1203 cx.simulate_shared_keystrokes(["2", ";"]).await;
1204 cx.assert_shared_state("onˇe two three four").await;
1205 cx.simulate_shared_keystrokes([","]).await;
1206 cx.assert_shared_state("one two thrˇee four").await;
1207
1208 // t and T
1209 cx.set_shared_state("ˇone two three four").await;
1210 cx.simulate_shared_keystrokes(["t", "o"]).await;
1211 cx.assert_shared_state("one tˇwo three four").await;
1212 cx.simulate_shared_keystrokes([","]).await;
1213 cx.assert_shared_state("oˇne two three four").await;
1214 cx.simulate_shared_keystrokes(["2", ";"]).await;
1215 cx.assert_shared_state("one two three ˇfour").await;
1216 cx.simulate_shared_keystrokes(["shift-t", "e"]).await;
1217 cx.assert_shared_state("one two threeˇ four").await;
1218 cx.simulate_shared_keystrokes(["3", ";"]).await;
1219 cx.assert_shared_state("oneˇ two three four").await;
1220 cx.simulate_shared_keystrokes([","]).await;
1221 cx.assert_shared_state("one two thˇree four").await;
1222 }
1223
1224 #[gpui::test]
1225 async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
1226 let mut cx = NeovimBackedTestContext::new(cx).await;
1227 cx.set_shared_state("ˇone\n two\nthree").await;
1228 cx.simulate_shared_keystrokes(["enter"]).await;
1229 cx.assert_shared_state("one\n ˇtwo\nthree").await;
1230 }
1231
1232 #[gpui::test]
1233 async fn test_window_top(cx: &mut gpui::TestAppContext) {
1234 let mut cx = NeovimBackedTestContext::new(cx).await;
1235 let initial_state = indoc! {r"abc
1236 def
1237 paragraph
1238 the second
1239 third ˇand
1240 final"};
1241
1242 cx.set_shared_state(initial_state).await;
1243 cx.simulate_shared_keystrokes(["shift-h"]).await;
1244 cx.assert_shared_state(indoc! {r"abˇc
1245 def
1246 paragraph
1247 the second
1248 third and
1249 final"})
1250 .await;
1251
1252 // clip point
1253 cx.set_shared_state(indoc! {r"
1254 1 2 3
1255 4 5 6
1256 7 8 ˇ9
1257 "})
1258 .await;
1259 cx.simulate_shared_keystrokes(["shift-h"]).await;
1260 cx.assert_shared_state(indoc! {r"
1261 1 2 ˇ3
1262 4 5 6
1263 7 8 9
1264 "})
1265 .await;
1266
1267 cx.set_shared_state(indoc! {r"
1268 1 2 3
1269 4 5 6
1270 ˇ7 8 9
1271 "})
1272 .await;
1273 cx.simulate_shared_keystrokes(["shift-h"]).await;
1274 cx.assert_shared_state(indoc! {r"
1275 ˇ1 2 3
1276 4 5 6
1277 7 8 9
1278 "})
1279 .await;
1280 }
1281
1282 #[gpui::test]
1283 async fn test_window_middle(cx: &mut gpui::TestAppContext) {
1284 let mut cx = NeovimBackedTestContext::new(cx).await;
1285 let initial_state = indoc! {r"abˇc
1286 def
1287 paragraph
1288 the second
1289 third and
1290 final"};
1291
1292 cx.set_shared_state(initial_state).await;
1293 cx.simulate_shared_keystrokes(["shift-m"]).await;
1294 cx.assert_shared_state(indoc! {r"abc
1295 def
1296 paˇragraph
1297 the second
1298 third and
1299 final"})
1300 .await;
1301
1302 cx.set_shared_state(indoc! {r"
1303 1 2 3
1304 4 5 6
1305 7 8 ˇ9
1306 "})
1307 .await;
1308 cx.simulate_shared_keystrokes(["shift-m"]).await;
1309 cx.assert_shared_state(indoc! {r"
1310 1 2 3
1311 4 5 ˇ6
1312 7 8 9
1313 "})
1314 .await;
1315 cx.set_shared_state(indoc! {r"
1316 1 2 3
1317 4 5 6
1318 ˇ7 8 9
1319 "})
1320 .await;
1321 cx.simulate_shared_keystrokes(["shift-m"]).await;
1322 cx.assert_shared_state(indoc! {r"
1323 1 2 3
1324 ˇ4 5 6
1325 7 8 9
1326 "})
1327 .await;
1328 cx.set_shared_state(indoc! {r"
1329 ˇ1 2 3
1330 4 5 6
1331 7 8 9
1332 "})
1333 .await;
1334 cx.simulate_shared_keystrokes(["shift-m"]).await;
1335 cx.assert_shared_state(indoc! {r"
1336 1 2 3
1337 ˇ4 5 6
1338 7 8 9
1339 "})
1340 .await;
1341 cx.set_shared_state(indoc! {r"
1342 1 2 3
1343 ˇ4 5 6
1344 7 8 9
1345 "})
1346 .await;
1347 cx.simulate_shared_keystrokes(["shift-m"]).await;
1348 cx.assert_shared_state(indoc! {r"
1349 1 2 3
1350 ˇ4 5 6
1351 7 8 9
1352 "})
1353 .await;
1354 cx.set_shared_state(indoc! {r"
1355 1 2 3
1356 4 5 ˇ6
1357 7 8 9
1358 "})
1359 .await;
1360 cx.simulate_shared_keystrokes(["shift-m"]).await;
1361 cx.assert_shared_state(indoc! {r"
1362 1 2 3
1363 4 5 ˇ6
1364 7 8 9
1365 "})
1366 .await;
1367 }
1368
1369 #[gpui::test]
1370 async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
1371 let mut cx = NeovimBackedTestContext::new(cx).await;
1372 let initial_state = indoc! {r"abc
1373 deˇf
1374 paragraph
1375 the second
1376 third and
1377 final"};
1378
1379 cx.set_shared_state(initial_state).await;
1380 cx.simulate_shared_keystrokes(["shift-l"]).await;
1381 cx.assert_shared_state(indoc! {r"abc
1382 def
1383 paragraph
1384 the second
1385 third and
1386 fiˇnal"})
1387 .await;
1388
1389 cx.set_shared_state(indoc! {r"
1390 1 2 3
1391 4 5 ˇ6
1392 7 8 9
1393 "})
1394 .await;
1395 cx.simulate_shared_keystrokes(["shift-l"]).await;
1396 cx.assert_shared_state(indoc! {r"
1397 1 2 3
1398 4 5 6
1399 7 8 9
1400 ˇ"})
1401 .await;
1402
1403 cx.set_shared_state(indoc! {r"
1404 1 2 3
1405 ˇ4 5 6
1406 7 8 9
1407 "})
1408 .await;
1409 cx.simulate_shared_keystrokes(["shift-l"]).await;
1410 cx.assert_shared_state(indoc! {r"
1411 1 2 3
1412 4 5 6
1413 7 8 9
1414 ˇ"})
1415 .await;
1416
1417 cx.set_shared_state(indoc! {r"
1418 1 2 ˇ3
1419 4 5 6
1420 7 8 9
1421 "})
1422 .await;
1423 cx.simulate_shared_keystrokes(["shift-l"]).await;
1424 cx.assert_shared_state(indoc! {r"
1425 1 2 3
1426 4 5 6
1427 7 8 9
1428 ˇ"})
1429 .await;
1430
1431 cx.set_shared_state(indoc! {r"
1432 ˇ1 2 3
1433 4 5 6
1434 7 8 9
1435 "})
1436 .await;
1437 cx.simulate_shared_keystrokes(["shift-l"]).await;
1438 cx.assert_shared_state(indoc! {r"
1439 1 2 3
1440 4 5 6
1441 7 8 9
1442 ˇ"})
1443 .await;
1444 }
1445}