1use editor::{
2 display_map::{DisplaySnapshot, FoldPoint, ToDisplayPoint},
3 movement::{
4 self, find_boundary, find_preceding_boundary_display_point, FindRange, TextLayoutDetails,
5 },
6 Bias, DisplayPoint, ToOffset,
7};
8use gpui::{actions, impl_actions, px, ViewContext, WindowContext};
9use language::{char_kind, CharKind, Point, Selection, SelectionGoal};
10use serde::Deserialize;
11use workspace::Workspace;
12
13use crate::{
14 normal::normal_motion,
15 state::{Mode, Operator},
16 utils::coerce_punctuation,
17 visual::visual_motion,
18 Vim,
19};
20
21#[derive(Clone, Debug, PartialEq, Eq)]
22pub enum Motion {
23 Left,
24 Backspace,
25 Down {
26 display_lines: bool,
27 },
28 Up {
29 display_lines: bool,
30 },
31 Right,
32 Space,
33 NextWordStart {
34 ignore_punctuation: bool,
35 },
36 NextWordEnd {
37 ignore_punctuation: bool,
38 },
39 PreviousWordStart {
40 ignore_punctuation: bool,
41 },
42 PreviousWordEnd {
43 ignore_punctuation: bool,
44 },
45 NextSubwordStart {
46 ignore_punctuation: bool,
47 },
48 NextSubwordEnd {
49 ignore_punctuation: bool,
50 },
51 PreviousSubwordStart {
52 ignore_punctuation: bool,
53 },
54 PreviousSubwordEnd {
55 ignore_punctuation: bool,
56 },
57 FirstNonWhitespace {
58 display_lines: bool,
59 },
60 CurrentLine,
61 StartOfLine {
62 display_lines: bool,
63 },
64 EndOfLine {
65 display_lines: bool,
66 },
67 StartOfParagraph,
68 EndOfParagraph,
69 StartOfDocument,
70 EndOfDocument,
71 Matching,
72 FindForward {
73 before: bool,
74 char: char,
75 mode: FindRange,
76 smartcase: bool,
77 },
78 FindBackward {
79 after: bool,
80 char: char,
81 mode: FindRange,
82 smartcase: bool,
83 },
84 RepeatFind {
85 last_find: Box<Motion>,
86 },
87 RepeatFindReversed {
88 last_find: Box<Motion>,
89 },
90 NextLineStart,
91 StartOfLineDownward,
92 EndOfLineDownward,
93 GoToColumn,
94 WindowTop,
95 WindowMiddle,
96 WindowBottom,
97}
98
99#[derive(Clone, Deserialize, PartialEq)]
100#[serde(rename_all = "camelCase")]
101struct NextWordStart {
102 #[serde(default)]
103 ignore_punctuation: bool,
104}
105
106#[derive(Clone, Deserialize, PartialEq)]
107#[serde(rename_all = "camelCase")]
108struct NextWordEnd {
109 #[serde(default)]
110 ignore_punctuation: bool,
111}
112
113#[derive(Clone, Deserialize, PartialEq)]
114#[serde(rename_all = "camelCase")]
115struct PreviousWordStart {
116 #[serde(default)]
117 ignore_punctuation: bool,
118}
119
120#[derive(Clone, Deserialize, PartialEq)]
121#[serde(rename_all = "camelCase")]
122struct PreviousWordEnd {
123 #[serde(default)]
124 ignore_punctuation: bool,
125}
126
127#[derive(Clone, Deserialize, PartialEq)]
128#[serde(rename_all = "camelCase")]
129pub(crate) struct NextSubwordStart {
130 #[serde(default)]
131 pub(crate) ignore_punctuation: bool,
132}
133
134#[derive(Clone, Deserialize, PartialEq)]
135#[serde(rename_all = "camelCase")]
136pub(crate) struct NextSubwordEnd {
137 #[serde(default)]
138 pub(crate) ignore_punctuation: bool,
139}
140
141#[derive(Clone, Deserialize, PartialEq)]
142#[serde(rename_all = "camelCase")]
143pub(crate) struct PreviousSubwordStart {
144 #[serde(default)]
145 pub(crate) ignore_punctuation: bool,
146}
147
148#[derive(Clone, Deserialize, PartialEq)]
149#[serde(rename_all = "camelCase")]
150pub(crate) struct PreviousSubwordEnd {
151 #[serde(default)]
152 pub(crate) ignore_punctuation: bool,
153}
154
155#[derive(Clone, Deserialize, PartialEq)]
156#[serde(rename_all = "camelCase")]
157pub(crate) struct Up {
158 #[serde(default)]
159 pub(crate) display_lines: bool,
160}
161
162#[derive(Clone, Deserialize, PartialEq)]
163#[serde(rename_all = "camelCase")]
164pub(crate) struct Down {
165 #[serde(default)]
166 pub(crate) display_lines: bool,
167}
168
169#[derive(Clone, Deserialize, PartialEq)]
170#[serde(rename_all = "camelCase")]
171struct FirstNonWhitespace {
172 #[serde(default)]
173 display_lines: bool,
174}
175
176#[derive(Clone, Deserialize, PartialEq)]
177#[serde(rename_all = "camelCase")]
178struct EndOfLine {
179 #[serde(default)]
180 display_lines: bool,
181}
182
183#[derive(Clone, Deserialize, PartialEq)]
184#[serde(rename_all = "camelCase")]
185pub struct StartOfLine {
186 #[serde(default)]
187 pub(crate) display_lines: bool,
188}
189
190impl_actions!(
191 vim,
192 [
193 StartOfLine,
194 EndOfLine,
195 FirstNonWhitespace,
196 Down,
197 Up,
198 NextWordStart,
199 NextWordEnd,
200 PreviousWordStart,
201 PreviousWordEnd,
202 NextSubwordStart,
203 NextSubwordEnd,
204 PreviousSubwordStart,
205 PreviousSubwordEnd,
206 ]
207);
208
209actions!(
210 vim,
211 [
212 Left,
213 Backspace,
214 Right,
215 Space,
216 CurrentLine,
217 StartOfParagraph,
218 EndOfParagraph,
219 StartOfDocument,
220 EndOfDocument,
221 Matching,
222 NextLineStart,
223 StartOfLineDownward,
224 EndOfLineDownward,
225 GoToColumn,
226 RepeatFind,
227 RepeatFindReversed,
228 WindowTop,
229 WindowMiddle,
230 WindowBottom,
231 ]
232);
233
234pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
235 workspace.register_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx));
236 workspace
237 .register_action(|_: &mut Workspace, _: &Backspace, cx: _| motion(Motion::Backspace, cx));
238 workspace.register_action(|_: &mut Workspace, action: &Down, cx: _| {
239 motion(
240 Motion::Down {
241 display_lines: action.display_lines,
242 },
243 cx,
244 )
245 });
246 workspace.register_action(|_: &mut Workspace, action: &Up, cx: _| {
247 motion(
248 Motion::Up {
249 display_lines: action.display_lines,
250 },
251 cx,
252 )
253 });
254 workspace.register_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx));
255 workspace.register_action(|_: &mut Workspace, _: &Space, cx: _| motion(Motion::Space, cx));
256 workspace.register_action(|_: &mut Workspace, action: &FirstNonWhitespace, cx: _| {
257 motion(
258 Motion::FirstNonWhitespace {
259 display_lines: action.display_lines,
260 },
261 cx,
262 )
263 });
264 workspace.register_action(|_: &mut Workspace, action: &StartOfLine, cx: _| {
265 motion(
266 Motion::StartOfLine {
267 display_lines: action.display_lines,
268 },
269 cx,
270 )
271 });
272 workspace.register_action(|_: &mut Workspace, action: &EndOfLine, cx: _| {
273 motion(
274 Motion::EndOfLine {
275 display_lines: action.display_lines,
276 },
277 cx,
278 )
279 });
280 workspace.register_action(|_: &mut Workspace, _: &CurrentLine, cx: _| {
281 motion(Motion::CurrentLine, cx)
282 });
283 workspace.register_action(|_: &mut Workspace, _: &StartOfParagraph, cx: _| {
284 motion(Motion::StartOfParagraph, cx)
285 });
286 workspace.register_action(|_: &mut Workspace, _: &EndOfParagraph, cx: _| {
287 motion(Motion::EndOfParagraph, cx)
288 });
289 workspace.register_action(|_: &mut Workspace, _: &StartOfDocument, cx: _| {
290 motion(Motion::StartOfDocument, cx)
291 });
292 workspace.register_action(|_: &mut Workspace, _: &EndOfDocument, cx: _| {
293 motion(Motion::EndOfDocument, cx)
294 });
295 workspace
296 .register_action(|_: &mut Workspace, _: &Matching, cx: _| motion(Motion::Matching, cx));
297
298 workspace.register_action(
299 |_: &mut Workspace, &NextWordStart { ignore_punctuation }: &NextWordStart, cx: _| {
300 motion(Motion::NextWordStart { ignore_punctuation }, cx)
301 },
302 );
303 workspace.register_action(
304 |_: &mut Workspace, &NextWordEnd { ignore_punctuation }: &NextWordEnd, cx: _| {
305 motion(Motion::NextWordEnd { ignore_punctuation }, cx)
306 },
307 );
308 workspace.register_action(
309 |_: &mut Workspace,
310 &PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
311 cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
312 );
313 workspace.register_action(
314 |_: &mut Workspace, &PreviousWordEnd { ignore_punctuation }, cx: _| {
315 motion(Motion::PreviousWordEnd { ignore_punctuation }, cx)
316 },
317 );
318 workspace.register_action(
319 |_: &mut Workspace, &NextSubwordStart { ignore_punctuation }: &NextSubwordStart, cx: _| {
320 motion(Motion::NextSubwordStart { ignore_punctuation }, cx)
321 },
322 );
323 workspace.register_action(
324 |_: &mut Workspace, &NextSubwordEnd { ignore_punctuation }: &NextSubwordEnd, cx: _| {
325 motion(Motion::NextSubwordEnd { ignore_punctuation }, cx)
326 },
327 );
328 workspace.register_action(
329 |_: &mut Workspace,
330 &PreviousSubwordStart { ignore_punctuation }: &PreviousSubwordStart,
331 cx: _| { motion(Motion::PreviousSubwordStart { ignore_punctuation }, cx) },
332 );
333 workspace.register_action(
334 |_: &mut Workspace, &PreviousSubwordEnd { ignore_punctuation }, cx: _| {
335 motion(Motion::PreviousSubwordEnd { ignore_punctuation }, cx)
336 },
337 );
338 workspace.register_action(|_: &mut Workspace, &NextLineStart, cx: _| {
339 motion(Motion::NextLineStart, cx)
340 });
341 workspace.register_action(|_: &mut Workspace, &StartOfLineDownward, cx: _| {
342 motion(Motion::StartOfLineDownward, cx)
343 });
344 workspace.register_action(|_: &mut Workspace, &EndOfLineDownward, cx: _| {
345 motion(Motion::EndOfLineDownward, cx)
346 });
347 workspace
348 .register_action(|_: &mut Workspace, &GoToColumn, cx: _| motion(Motion::GoToColumn, cx));
349
350 workspace.register_action(|_: &mut Workspace, _: &RepeatFind, cx: _| {
351 if let Some(last_find) = Vim::read(cx)
352 .workspace_state
353 .last_find
354 .clone()
355 .map(Box::new)
356 {
357 motion(Motion::RepeatFind { last_find }, cx);
358 }
359 });
360
361 workspace.register_action(|_: &mut Workspace, _: &RepeatFindReversed, cx: _| {
362 if let Some(last_find) = Vim::read(cx)
363 .workspace_state
364 .last_find
365 .clone()
366 .map(Box::new)
367 {
368 motion(Motion::RepeatFindReversed { last_find }, cx);
369 }
370 });
371 workspace.register_action(|_: &mut Workspace, &WindowTop, cx: _| motion(Motion::WindowTop, cx));
372 workspace.register_action(|_: &mut Workspace, &WindowMiddle, cx: _| {
373 motion(Motion::WindowMiddle, cx)
374 });
375 workspace.register_action(|_: &mut Workspace, &WindowBottom, cx: _| {
376 motion(Motion::WindowBottom, cx)
377 });
378}
379
380pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
381 if let Some(Operator::FindForward { .. }) | Some(Operator::FindBackward { .. }) =
382 Vim::read(cx).active_operator()
383 {
384 Vim::update(cx, |vim, cx| vim.pop_operator(cx));
385 }
386
387 let count = Vim::update(cx, |vim, cx| vim.take_count(cx));
388 let operator = Vim::read(cx).active_operator();
389 match Vim::read(cx).state().mode {
390 Mode::Normal => normal_motion(motion, operator, count, cx),
391 Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_motion(motion, count, cx),
392 Mode::Insert => {
393 // Shouldn't execute a motion in insert mode. Ignoring
394 }
395 }
396 Vim::update(cx, |vim, cx| vim.clear_operator(cx));
397}
398
399// Motion handling is specified here:
400// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
401impl Motion {
402 pub fn linewise(&self) -> bool {
403 use Motion::*;
404 match self {
405 Down { .. }
406 | Up { .. }
407 | StartOfDocument
408 | EndOfDocument
409 | CurrentLine
410 | NextLineStart
411 | StartOfLineDownward
412 | StartOfParagraph
413 | WindowTop
414 | WindowMiddle
415 | WindowBottom
416 | EndOfParagraph => true,
417 EndOfLine { .. }
418 | Matching
419 | FindForward { .. }
420 | Left
421 | Backspace
422 | Right
423 | Space
424 | StartOfLine { .. }
425 | EndOfLineDownward
426 | GoToColumn
427 | NextWordStart { .. }
428 | NextWordEnd { .. }
429 | PreviousWordStart { .. }
430 | PreviousWordEnd { .. }
431 | NextSubwordStart { .. }
432 | NextSubwordEnd { .. }
433 | PreviousSubwordStart { .. }
434 | PreviousSubwordEnd { .. }
435 | FirstNonWhitespace { .. }
436 | FindBackward { .. }
437 | RepeatFind { .. }
438 | RepeatFindReversed { .. } => false,
439 }
440 }
441
442 pub fn infallible(&self) -> bool {
443 use Motion::*;
444 match self {
445 StartOfDocument | EndOfDocument | CurrentLine => true,
446 Down { .. }
447 | Up { .. }
448 | EndOfLine { .. }
449 | Matching
450 | FindForward { .. }
451 | RepeatFind { .. }
452 | Left
453 | Backspace
454 | Right
455 | Space
456 | StartOfLine { .. }
457 | StartOfParagraph
458 | EndOfParagraph
459 | StartOfLineDownward
460 | EndOfLineDownward
461 | GoToColumn
462 | NextWordStart { .. }
463 | NextWordEnd { .. }
464 | PreviousWordStart { .. }
465 | PreviousWordEnd { .. }
466 | NextSubwordStart { .. }
467 | NextSubwordEnd { .. }
468 | PreviousSubwordStart { .. }
469 | PreviousSubwordEnd { .. }
470 | FirstNonWhitespace { .. }
471 | FindBackward { .. }
472 | RepeatFindReversed { .. }
473 | WindowTop
474 | WindowMiddle
475 | WindowBottom
476 | NextLineStart => false,
477 }
478 }
479
480 pub fn inclusive(&self) -> bool {
481 use Motion::*;
482 match self {
483 Down { .. }
484 | Up { .. }
485 | StartOfDocument
486 | EndOfDocument
487 | CurrentLine
488 | EndOfLine { .. }
489 | EndOfLineDownward
490 | Matching
491 | FindForward { .. }
492 | WindowTop
493 | WindowMiddle
494 | WindowBottom
495 | NextWordEnd { .. }
496 | PreviousWordEnd { .. }
497 | NextSubwordEnd { .. }
498 | PreviousSubwordEnd { .. }
499 | NextLineStart => true,
500 Left
501 | Backspace
502 | Right
503 | Space
504 | StartOfLine { .. }
505 | StartOfLineDownward
506 | StartOfParagraph
507 | EndOfParagraph
508 | GoToColumn
509 | NextWordStart { .. }
510 | PreviousWordStart { .. }
511 | NextSubwordStart { .. }
512 | PreviousSubwordStart { .. }
513 | FirstNonWhitespace { .. }
514 | FindBackward { .. } => false,
515 RepeatFind { last_find: motion } | RepeatFindReversed { last_find: motion } => {
516 motion.inclusive()
517 }
518 }
519 }
520
521 pub fn move_point(
522 &self,
523 map: &DisplaySnapshot,
524 point: DisplayPoint,
525 goal: SelectionGoal,
526 maybe_times: Option<usize>,
527 text_layout_details: &TextLayoutDetails,
528 ) -> Option<(DisplayPoint, SelectionGoal)> {
529 let times = maybe_times.unwrap_or(1);
530 use Motion::*;
531 let infallible = self.infallible();
532 let (new_point, goal) = match self {
533 Left => (left(map, point, times), SelectionGoal::None),
534 Backspace => (backspace(map, point, times), SelectionGoal::None),
535 Down {
536 display_lines: false,
537 } => up_down_buffer_rows(map, point, goal, times as isize, &text_layout_details),
538 Down {
539 display_lines: true,
540 } => down_display(map, point, goal, times, &text_layout_details),
541 Up {
542 display_lines: false,
543 } => up_down_buffer_rows(map, point, goal, 0 - times as isize, &text_layout_details),
544 Up {
545 display_lines: true,
546 } => up_display(map, point, goal, times, &text_layout_details),
547 Right => (right(map, point, times), SelectionGoal::None),
548 Space => (space(map, point, times), SelectionGoal::None),
549 NextWordStart { ignore_punctuation } => (
550 next_word_start(map, point, *ignore_punctuation, times),
551 SelectionGoal::None,
552 ),
553 NextWordEnd { ignore_punctuation } => (
554 next_word_end(map, point, *ignore_punctuation, times, true),
555 SelectionGoal::None,
556 ),
557 PreviousWordStart { ignore_punctuation } => (
558 previous_word_start(map, point, *ignore_punctuation, times),
559 SelectionGoal::None,
560 ),
561 PreviousWordEnd { ignore_punctuation } => (
562 previous_word_end(map, point, *ignore_punctuation, times),
563 SelectionGoal::None,
564 ),
565 NextSubwordStart { ignore_punctuation } => (
566 next_subword_start(map, point, *ignore_punctuation, times),
567 SelectionGoal::None,
568 ),
569 NextSubwordEnd { ignore_punctuation } => (
570 next_subword_end(map, point, *ignore_punctuation, times, true),
571 SelectionGoal::None,
572 ),
573 PreviousSubwordStart { ignore_punctuation } => (
574 previous_subword_start(map, point, *ignore_punctuation, times),
575 SelectionGoal::None,
576 ),
577 PreviousSubwordEnd { ignore_punctuation } => (
578 previous_subword_end(map, point, *ignore_punctuation, times),
579 SelectionGoal::None,
580 ),
581 FirstNonWhitespace { display_lines } => (
582 first_non_whitespace(map, *display_lines, point),
583 SelectionGoal::None,
584 ),
585 StartOfLine { display_lines } => (
586 start_of_line(map, *display_lines, point),
587 SelectionGoal::None,
588 ),
589 EndOfLine { display_lines } => (
590 end_of_line(map, *display_lines, point, times),
591 SelectionGoal::None,
592 ),
593 StartOfParagraph => (
594 movement::start_of_paragraph(map, point, times),
595 SelectionGoal::None,
596 ),
597 EndOfParagraph => (
598 map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
599 SelectionGoal::None,
600 ),
601 CurrentLine => (next_line_end(map, point, times), SelectionGoal::None),
602 StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
603 EndOfDocument => (
604 end_of_document(map, point, maybe_times),
605 SelectionGoal::None,
606 ),
607 Matching => (matching(map, point), SelectionGoal::None),
608 // t f
609 FindForward {
610 before,
611 char,
612 mode,
613 smartcase,
614 } => {
615 return find_forward(map, point, *before, *char, times, *mode, *smartcase)
616 .map(|new_point| (new_point, SelectionGoal::None))
617 }
618 // T F
619 FindBackward {
620 after,
621 char,
622 mode,
623 smartcase,
624 } => (
625 find_backward(map, point, *after, *char, times, *mode, *smartcase),
626 SelectionGoal::None,
627 ),
628 // ; -- repeat the last find done with t, f, T, F
629 RepeatFind { last_find } => match **last_find {
630 Motion::FindForward {
631 before,
632 char,
633 mode,
634 smartcase,
635 } => {
636 let mut new_point =
637 find_forward(map, point, before, char, times, mode, smartcase);
638 if new_point == Some(point) {
639 new_point =
640 find_forward(map, point, before, char, times + 1, mode, smartcase);
641 }
642
643 return new_point.map(|new_point| (new_point, SelectionGoal::None));
644 }
645
646 Motion::FindBackward {
647 after,
648 char,
649 mode,
650 smartcase,
651 } => {
652 let mut new_point =
653 find_backward(map, point, after, char, times, mode, smartcase);
654 if new_point == point {
655 new_point =
656 find_backward(map, point, after, char, times + 1, mode, smartcase);
657 }
658
659 (new_point, SelectionGoal::None)
660 }
661 _ => return None,
662 },
663 // , -- repeat the last find done with t, f, T, F, in opposite direction
664 RepeatFindReversed { last_find } => match **last_find {
665 Motion::FindForward {
666 before,
667 char,
668 mode,
669 smartcase,
670 } => {
671 let mut new_point =
672 find_backward(map, point, before, char, times, mode, smartcase);
673 if new_point == point {
674 new_point =
675 find_backward(map, point, before, char, times + 1, mode, smartcase);
676 }
677
678 (new_point, SelectionGoal::None)
679 }
680
681 Motion::FindBackward {
682 after,
683 char,
684 mode,
685 smartcase,
686 } => {
687 let mut new_point =
688 find_forward(map, point, after, char, times, mode, smartcase);
689 if new_point == Some(point) {
690 new_point =
691 find_forward(map, point, after, char, times + 1, mode, smartcase);
692 }
693
694 return new_point.map(|new_point| (new_point, SelectionGoal::None));
695 }
696 _ => return None,
697 },
698 NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
699 StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None),
700 EndOfLineDownward => (next_line_end(map, point, times), SelectionGoal::None),
701 GoToColumn => (go_to_column(map, point, times), SelectionGoal::None),
702 WindowTop => window_top(map, point, &text_layout_details, times - 1),
703 WindowMiddle => window_middle(map, point, &text_layout_details),
704 WindowBottom => window_bottom(map, point, &text_layout_details, times - 1),
705 };
706
707 (new_point != point || infallible).then_some((new_point, goal))
708 }
709
710 // Expands a selection using self motion for an operator
711 pub fn expand_selection(
712 &self,
713 map: &DisplaySnapshot,
714 selection: &mut Selection<DisplayPoint>,
715 times: Option<usize>,
716 expand_to_surrounding_newline: bool,
717 text_layout_details: &TextLayoutDetails,
718 ) -> bool {
719 if let Some((new_head, goal)) = self.move_point(
720 map,
721 selection.head(),
722 selection.goal,
723 times,
724 &text_layout_details,
725 ) {
726 selection.set_head(new_head, goal);
727
728 if self.linewise() {
729 selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
730
731 if expand_to_surrounding_newline {
732 if selection.end.row() < map.max_point().row() {
733 *selection.end.row_mut() += 1;
734 *selection.end.column_mut() = 0;
735 selection.end = map.clip_point(selection.end, Bias::Right);
736 // Don't reset the end here
737 return true;
738 } else if selection.start.row() > 0 {
739 *selection.start.row_mut() -= 1;
740 *selection.start.column_mut() = map.line_len(selection.start.row());
741 selection.start = map.clip_point(selection.start, Bias::Left);
742 }
743 }
744
745 (_, selection.end) = map.next_line_boundary(selection.end.to_point(map));
746 } else {
747 // Another special case: When using the "w" motion in combination with an
748 // operator and the last word moved over is at the end of a line, the end of
749 // that word becomes the end of the operated text, not the first word in the
750 // next line.
751 if let Motion::NextWordStart {
752 ignore_punctuation: _,
753 } = self
754 {
755 let start_row = selection.start.to_point(&map).row;
756 if selection.end.to_point(&map).row > start_row {
757 selection.end =
758 Point::new(start_row, map.buffer_snapshot.line_len(start_row))
759 .to_display_point(&map)
760 }
761 }
762
763 // If the motion is exclusive and the end of the motion is in column 1, the
764 // end of the motion is moved to the end of the previous line and the motion
765 // becomes inclusive. Example: "}" moves to the first line after a paragraph,
766 // but "d}" will not include that line.
767 let mut inclusive = self.inclusive();
768 if !inclusive
769 && self != &Motion::Backspace
770 && selection.end.row() > selection.start.row()
771 && selection.end.column() == 0
772 {
773 inclusive = true;
774 *selection.end.row_mut() -= 1;
775 *selection.end.column_mut() = 0;
776 selection.end = map.clip_point(
777 map.next_line_boundary(selection.end.to_point(map)).1,
778 Bias::Left,
779 );
780 }
781
782 if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
783 *selection.end.column_mut() += 1;
784 }
785 }
786 true
787 } else {
788 false
789 }
790 }
791}
792
793fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
794 for _ in 0..times {
795 point = movement::saturating_left(map, point);
796 if point.column() == 0 {
797 break;
798 }
799 }
800 point
801}
802
803fn backspace(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
804 for _ in 0..times {
805 point = movement::left(map, point);
806 if point.is_zero() {
807 break;
808 }
809 }
810 point
811}
812
813fn space(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
814 for _ in 0..times {
815 point = wrapping_right(map, point);
816 if point == map.max_point() {
817 break;
818 }
819 }
820 point
821}
822
823fn wrapping_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
824 let max_column = map.line_len(point.row()).saturating_sub(1);
825 if point.column() < max_column {
826 *point.column_mut() += 1;
827 } else if point.row() < map.max_point().row() {
828 *point.row_mut() += 1;
829 *point.column_mut() = 0;
830 }
831 point
832}
833
834pub(crate) fn start_of_relative_buffer_row(
835 map: &DisplaySnapshot,
836 point: DisplayPoint,
837 times: isize,
838) -> DisplayPoint {
839 let start = map.display_point_to_fold_point(point, Bias::Left);
840 let target = start.row() as isize + times;
841 let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
842
843 map.clip_point(
844 map.fold_point_to_display_point(
845 map.fold_snapshot
846 .clip_point(FoldPoint::new(new_row, 0), Bias::Right),
847 ),
848 Bias::Right,
849 )
850}
851
852fn up_down_buffer_rows(
853 map: &DisplaySnapshot,
854 point: DisplayPoint,
855 mut goal: SelectionGoal,
856 times: isize,
857 text_layout_details: &TextLayoutDetails,
858) -> (DisplayPoint, SelectionGoal) {
859 let start = map.display_point_to_fold_point(point, Bias::Left);
860 let begin_folded_line = map.fold_point_to_display_point(
861 map.fold_snapshot
862 .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
863 );
864 let select_nth_wrapped_row = point.row() - begin_folded_line.row();
865
866 let (goal_wrap, goal_x) = match goal {
867 SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
868 SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
869 SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
870 _ => {
871 let x = map.x_for_display_point(point, text_layout_details);
872 goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x.0));
873 (select_nth_wrapped_row, x.0)
874 }
875 };
876
877 let target = start.row() as isize + times;
878 let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
879
880 let mut begin_folded_line = map.fold_point_to_display_point(
881 map.fold_snapshot
882 .clip_point(FoldPoint::new(new_row, 0), Bias::Left),
883 );
884
885 let mut i = 0;
886 while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
887 let next_folded_line = DisplayPoint::new(begin_folded_line.row() + 1, 0);
888 if map
889 .display_point_to_fold_point(next_folded_line, Bias::Right)
890 .row()
891 == new_row
892 {
893 i += 1;
894 begin_folded_line = next_folded_line;
895 } else {
896 break;
897 }
898 }
899
900 let new_col = if i == goal_wrap {
901 map.display_column_for_x(begin_folded_line.row(), px(goal_x), text_layout_details)
902 } else {
903 map.line_len(begin_folded_line.row())
904 };
905
906 (
907 map.clip_point(
908 DisplayPoint::new(begin_folded_line.row(), new_col),
909 Bias::Left,
910 ),
911 goal,
912 )
913}
914
915fn down_display(
916 map: &DisplaySnapshot,
917 mut point: DisplayPoint,
918 mut goal: SelectionGoal,
919 times: usize,
920 text_layout_details: &TextLayoutDetails,
921) -> (DisplayPoint, SelectionGoal) {
922 for _ in 0..times {
923 (point, goal) = movement::down(map, point, goal, true, text_layout_details);
924 }
925
926 (point, goal)
927}
928
929fn up_display(
930 map: &DisplaySnapshot,
931 mut point: DisplayPoint,
932 mut goal: SelectionGoal,
933 times: usize,
934 text_layout_details: &TextLayoutDetails,
935) -> (DisplayPoint, SelectionGoal) {
936 for _ in 0..times {
937 (point, goal) = movement::up(map, point, goal, true, &text_layout_details);
938 }
939
940 (point, goal)
941}
942
943pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
944 for _ in 0..times {
945 let new_point = movement::saturating_right(map, point);
946 if point == new_point {
947 break;
948 }
949 point = new_point;
950 }
951 point
952}
953
954pub(crate) fn next_char(
955 map: &DisplaySnapshot,
956 point: DisplayPoint,
957 allow_cross_newline: bool,
958) -> DisplayPoint {
959 let mut new_point = point;
960 let mut max_column = map.line_len(new_point.row());
961 if !allow_cross_newline {
962 max_column -= 1;
963 }
964 if new_point.column() < max_column {
965 *new_point.column_mut() += 1;
966 } else if new_point < map.max_point() && allow_cross_newline {
967 *new_point.row_mut() += 1;
968 *new_point.column_mut() = 0;
969 }
970 new_point
971}
972
973pub(crate) fn next_word_start(
974 map: &DisplaySnapshot,
975 mut point: DisplayPoint,
976 ignore_punctuation: bool,
977 times: usize,
978) -> DisplayPoint {
979 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
980 for _ in 0..times {
981 let mut crossed_newline = false;
982 let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
983 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
984 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
985 let at_newline = right == '\n';
986
987 let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
988 || at_newline && crossed_newline
989 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
990
991 crossed_newline |= at_newline;
992 found
993 });
994 if point == new_point {
995 break;
996 }
997 point = new_point;
998 }
999 point
1000}
1001
1002pub(crate) fn next_word_end(
1003 map: &DisplaySnapshot,
1004 mut point: DisplayPoint,
1005 ignore_punctuation: bool,
1006 times: usize,
1007 allow_cross_newline: bool,
1008) -> DisplayPoint {
1009 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1010 for _ in 0..times {
1011 let new_point = next_char(map, point, allow_cross_newline);
1012 let mut need_next_char = false;
1013 let new_point = movement::find_boundary_exclusive(
1014 map,
1015 new_point,
1016 FindRange::MultiLine,
1017 |left, right| {
1018 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1019 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1020 let at_newline = right == '\n';
1021
1022 if !allow_cross_newline && at_newline {
1023 need_next_char = true;
1024 return true;
1025 }
1026
1027 left_kind != right_kind && left_kind != CharKind::Whitespace
1028 },
1029 );
1030 let new_point = if need_next_char {
1031 next_char(map, new_point, true)
1032 } else {
1033 new_point
1034 };
1035 let new_point = map.clip_point(new_point, Bias::Left);
1036 if point == new_point {
1037 break;
1038 }
1039 point = new_point;
1040 }
1041 point
1042}
1043
1044fn previous_word_start(
1045 map: &DisplaySnapshot,
1046 mut point: DisplayPoint,
1047 ignore_punctuation: bool,
1048 times: usize,
1049) -> DisplayPoint {
1050 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1051 for _ in 0..times {
1052 // This works even though find_preceding_boundary is called for every character in the line containing
1053 // cursor because the newline is checked only once.
1054 let new_point = movement::find_preceding_boundary_display_point(
1055 map,
1056 point,
1057 FindRange::MultiLine,
1058 |left, right| {
1059 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1060 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1061
1062 (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
1063 },
1064 );
1065 if point == new_point {
1066 break;
1067 }
1068 point = new_point;
1069 }
1070 point
1071}
1072
1073fn previous_word_end(
1074 map: &DisplaySnapshot,
1075 point: DisplayPoint,
1076 ignore_punctuation: bool,
1077 times: usize,
1078) -> DisplayPoint {
1079 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1080 let mut point = point.to_point(map);
1081
1082 if point.column < map.buffer_snapshot.line_len(point.row) {
1083 point.column += 1;
1084 }
1085 for _ in 0..times {
1086 let new_point = movement::find_preceding_boundary_point(
1087 &map.buffer_snapshot,
1088 point,
1089 FindRange::MultiLine,
1090 |left, right| {
1091 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1092 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1093 match (left_kind, right_kind) {
1094 (CharKind::Punctuation, CharKind::Whitespace)
1095 | (CharKind::Punctuation, CharKind::Word)
1096 | (CharKind::Word, CharKind::Whitespace)
1097 | (CharKind::Word, CharKind::Punctuation) => true,
1098 (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1099 _ => false,
1100 }
1101 },
1102 );
1103 if new_point == point {
1104 break;
1105 }
1106 point = new_point;
1107 }
1108 movement::saturating_left(map, point.to_display_point(map))
1109}
1110
1111fn next_subword_start(
1112 map: &DisplaySnapshot,
1113 mut point: DisplayPoint,
1114 ignore_punctuation: bool,
1115 times: usize,
1116) -> DisplayPoint {
1117 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1118 for _ in 0..times {
1119 let mut crossed_newline = false;
1120 let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1121 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1122 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1123 let at_newline = right == '\n';
1124
1125 let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1126 let is_subword_start =
1127 left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1128
1129 let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1130 || at_newline && crossed_newline
1131 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1132
1133 crossed_newline |= at_newline;
1134 found
1135 });
1136 if point == new_point {
1137 break;
1138 }
1139 point = new_point;
1140 }
1141 point
1142}
1143
1144pub(crate) fn next_subword_end(
1145 map: &DisplaySnapshot,
1146 mut point: DisplayPoint,
1147 ignore_punctuation: bool,
1148 times: usize,
1149 allow_cross_newline: bool,
1150) -> DisplayPoint {
1151 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1152 for _ in 0..times {
1153 let new_point = next_char(map, point, allow_cross_newline);
1154
1155 let mut crossed_newline = false;
1156 let mut need_backtrack = false;
1157 let new_point =
1158 movement::find_boundary(map, new_point, FindRange::MultiLine, |left, right| {
1159 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1160 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1161 let at_newline = right == '\n';
1162
1163 if !allow_cross_newline && at_newline {
1164 return true;
1165 }
1166
1167 let is_word_end = (left_kind != right_kind) && !right.is_alphanumeric();
1168 let is_subword_end =
1169 left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1170
1171 let found = !left.is_whitespace() && !at_newline && (is_word_end || is_subword_end);
1172
1173 if found && (is_word_end || is_subword_end) {
1174 need_backtrack = true;
1175 }
1176
1177 crossed_newline |= at_newline;
1178 found
1179 });
1180 let mut new_point = map.clip_point(new_point, Bias::Left);
1181 if need_backtrack {
1182 *new_point.column_mut() -= 1;
1183 }
1184 if point == new_point {
1185 break;
1186 }
1187 point = new_point;
1188 }
1189 point
1190}
1191
1192fn previous_subword_start(
1193 map: &DisplaySnapshot,
1194 mut point: DisplayPoint,
1195 ignore_punctuation: bool,
1196 times: usize,
1197) -> DisplayPoint {
1198 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1199 for _ in 0..times {
1200 let mut crossed_newline = false;
1201 // This works even though find_preceding_boundary is called for every character in the line containing
1202 // cursor because the newline is checked only once.
1203 let new_point = movement::find_preceding_boundary_display_point(
1204 map,
1205 point,
1206 FindRange::MultiLine,
1207 |left, right| {
1208 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1209 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1210 let at_newline = right == '\n';
1211
1212 let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1213 let is_subword_start =
1214 left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1215
1216 let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1217 || at_newline && crossed_newline
1218 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1219
1220 crossed_newline |= at_newline;
1221
1222 found
1223 },
1224 );
1225 if point == new_point {
1226 break;
1227 }
1228 point = new_point;
1229 }
1230 point
1231}
1232
1233fn previous_subword_end(
1234 map: &DisplaySnapshot,
1235 point: DisplayPoint,
1236 ignore_punctuation: bool,
1237 times: usize,
1238) -> DisplayPoint {
1239 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1240 let mut point = point.to_point(map);
1241
1242 if point.column < map.buffer_snapshot.line_len(point.row) {
1243 point.column += 1;
1244 }
1245 for _ in 0..times {
1246 let new_point = movement::find_preceding_boundary_point(
1247 &map.buffer_snapshot,
1248 point,
1249 FindRange::MultiLine,
1250 |left, right| {
1251 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1252 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1253
1254 let is_subword_end =
1255 left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1256
1257 if is_subword_end {
1258 return true;
1259 }
1260
1261 match (left_kind, right_kind) {
1262 (CharKind::Word, CharKind::Whitespace)
1263 | (CharKind::Word, CharKind::Punctuation) => true,
1264 (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1265 _ => false,
1266 }
1267 },
1268 );
1269 if new_point == point {
1270 break;
1271 }
1272 point = new_point;
1273 }
1274 movement::saturating_left(map, point.to_display_point(map))
1275}
1276
1277pub(crate) fn first_non_whitespace(
1278 map: &DisplaySnapshot,
1279 display_lines: bool,
1280 from: DisplayPoint,
1281) -> DisplayPoint {
1282 let mut last_point = start_of_line(map, display_lines, from);
1283 let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
1284 for (ch, point) in map.chars_at(last_point) {
1285 if ch == '\n' {
1286 return from;
1287 }
1288
1289 last_point = point;
1290
1291 if char_kind(&scope, ch) != CharKind::Whitespace {
1292 break;
1293 }
1294 }
1295
1296 map.clip_point(last_point, Bias::Left)
1297}
1298
1299pub(crate) fn start_of_line(
1300 map: &DisplaySnapshot,
1301 display_lines: bool,
1302 point: DisplayPoint,
1303) -> DisplayPoint {
1304 if display_lines {
1305 map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
1306 } else {
1307 map.prev_line_boundary(point.to_point(map)).1
1308 }
1309}
1310
1311pub(crate) fn end_of_line(
1312 map: &DisplaySnapshot,
1313 display_lines: bool,
1314 mut point: DisplayPoint,
1315 times: usize,
1316) -> DisplayPoint {
1317 if times > 1 {
1318 point = start_of_relative_buffer_row(map, point, times as isize - 1);
1319 }
1320 if display_lines {
1321 map.clip_point(
1322 DisplayPoint::new(point.row(), map.line_len(point.row())),
1323 Bias::Left,
1324 )
1325 } else {
1326 map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
1327 }
1328}
1329
1330fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
1331 let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
1332 *new_point.column_mut() = point.column();
1333 map.clip_point(new_point, Bias::Left)
1334}
1335
1336fn end_of_document(
1337 map: &DisplaySnapshot,
1338 point: DisplayPoint,
1339 line: Option<usize>,
1340) -> DisplayPoint {
1341 let new_row = if let Some(line) = line {
1342 (line - 1) as u32
1343 } else {
1344 map.max_buffer_row()
1345 };
1346
1347 let new_point = Point::new(new_row, point.column());
1348 map.clip_point(new_point.to_display_point(map), Bias::Left)
1349}
1350
1351fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
1352 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
1353 let point = display_point.to_point(map);
1354 let offset = point.to_offset(&map.buffer_snapshot);
1355
1356 // Ensure the range is contained by the current line.
1357 let mut line_end = map.next_line_boundary(point).0;
1358 if line_end == point {
1359 line_end = map.max_point().to_point(map);
1360 }
1361
1362 let line_range = map.prev_line_boundary(point).0..line_end;
1363 let visible_line_range =
1364 line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
1365 let ranges = map
1366 .buffer_snapshot
1367 .bracket_ranges(visible_line_range.clone());
1368 if let Some(ranges) = ranges {
1369 let line_range = line_range.start.to_offset(&map.buffer_snapshot)
1370 ..line_range.end.to_offset(&map.buffer_snapshot);
1371 let mut closest_pair_destination = None;
1372 let mut closest_distance = usize::MAX;
1373
1374 for (open_range, close_range) in ranges {
1375 if open_range.start >= offset && line_range.contains(&open_range.start) {
1376 let distance = open_range.start - offset;
1377 if distance < closest_distance {
1378 closest_pair_destination = Some(close_range.start);
1379 closest_distance = distance;
1380 continue;
1381 }
1382 }
1383
1384 if close_range.start >= offset && line_range.contains(&close_range.start) {
1385 let distance = close_range.start - offset;
1386 if distance < closest_distance {
1387 closest_pair_destination = Some(open_range.start);
1388 closest_distance = distance;
1389 continue;
1390 }
1391 }
1392
1393 continue;
1394 }
1395
1396 closest_pair_destination
1397 .map(|destination| destination.to_display_point(map))
1398 .unwrap_or(display_point)
1399 } else {
1400 display_point
1401 }
1402}
1403
1404fn find_forward(
1405 map: &DisplaySnapshot,
1406 from: DisplayPoint,
1407 before: bool,
1408 target: char,
1409 times: usize,
1410 mode: FindRange,
1411 smartcase: bool,
1412) -> Option<DisplayPoint> {
1413 let mut to = from;
1414 let mut found = false;
1415
1416 for _ in 0..times {
1417 found = false;
1418 let new_to = find_boundary(map, to, mode, |_, right| {
1419 found = is_character_match(target, right, smartcase);
1420 found
1421 });
1422 if to == new_to {
1423 break;
1424 }
1425 to = new_to;
1426 }
1427
1428 if found {
1429 if before && to.column() > 0 {
1430 *to.column_mut() -= 1;
1431 Some(map.clip_point(to, Bias::Left))
1432 } else {
1433 Some(to)
1434 }
1435 } else {
1436 None
1437 }
1438}
1439
1440fn find_backward(
1441 map: &DisplaySnapshot,
1442 from: DisplayPoint,
1443 after: bool,
1444 target: char,
1445 times: usize,
1446 mode: FindRange,
1447 smartcase: bool,
1448) -> DisplayPoint {
1449 let mut to = from;
1450
1451 for _ in 0..times {
1452 let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
1453 is_character_match(target, right, smartcase)
1454 });
1455 if to == new_to {
1456 break;
1457 }
1458 to = new_to;
1459 }
1460
1461 let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
1462 if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
1463 if after {
1464 *to.column_mut() += 1;
1465 map.clip_point(to, Bias::Right)
1466 } else {
1467 to
1468 }
1469 } else {
1470 from
1471 }
1472}
1473
1474fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
1475 if smartcase {
1476 if target.is_uppercase() {
1477 target == other
1478 } else {
1479 target == other.to_ascii_lowercase()
1480 }
1481 } else {
1482 target == other
1483 }
1484}
1485
1486fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1487 let correct_line = start_of_relative_buffer_row(map, point, times as isize);
1488 first_non_whitespace(map, false, correct_line)
1489}
1490
1491fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1492 let correct_line = start_of_relative_buffer_row(map, point, 0);
1493 right(map, correct_line, times.saturating_sub(1))
1494}
1495
1496pub(crate) fn next_line_end(
1497 map: &DisplaySnapshot,
1498 mut point: DisplayPoint,
1499 times: usize,
1500) -> DisplayPoint {
1501 if times > 1 {
1502 point = start_of_relative_buffer_row(map, point, times as isize - 1);
1503 }
1504 end_of_line(map, false, point, 1)
1505}
1506
1507fn window_top(
1508 map: &DisplaySnapshot,
1509 point: DisplayPoint,
1510 text_layout_details: &TextLayoutDetails,
1511 mut times: usize,
1512) -> (DisplayPoint, SelectionGoal) {
1513 let first_visible_line = text_layout_details
1514 .scroll_anchor
1515 .anchor
1516 .to_display_point(map);
1517
1518 if first_visible_line.row() != 0 && text_layout_details.vertical_scroll_margin as usize > times
1519 {
1520 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1521 }
1522
1523 if let Some(visible_rows) = text_layout_details.visible_rows {
1524 let bottom_row = first_visible_line.row() + visible_rows as u32;
1525 let new_row = (first_visible_line.row() + (times as u32))
1526 .min(bottom_row)
1527 .min(map.max_point().row());
1528 let new_col = point.column().min(map.line_len(first_visible_line.row()));
1529
1530 let new_point = DisplayPoint::new(new_row, new_col);
1531 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1532 } else {
1533 let new_row = (first_visible_line.row() + (times as u32)).min(map.max_point().row());
1534 let new_col = point.column().min(map.line_len(first_visible_line.row()));
1535
1536 let new_point = DisplayPoint::new(new_row, new_col);
1537 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1538 }
1539}
1540
1541fn window_middle(
1542 map: &DisplaySnapshot,
1543 point: DisplayPoint,
1544 text_layout_details: &TextLayoutDetails,
1545) -> (DisplayPoint, SelectionGoal) {
1546 if let Some(visible_rows) = text_layout_details.visible_rows {
1547 let first_visible_line = text_layout_details
1548 .scroll_anchor
1549 .anchor
1550 .to_display_point(map);
1551
1552 let max_visible_rows =
1553 (visible_rows as u32).min(map.max_point().row() - first_visible_line.row());
1554
1555 let new_row =
1556 (first_visible_line.row() + (max_visible_rows / 2)).min(map.max_point().row());
1557 let new_col = point.column().min(map.line_len(new_row));
1558 let new_point = DisplayPoint::new(new_row, new_col);
1559 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1560 } else {
1561 (point, SelectionGoal::None)
1562 }
1563}
1564
1565fn window_bottom(
1566 map: &DisplaySnapshot,
1567 point: DisplayPoint,
1568 text_layout_details: &TextLayoutDetails,
1569 mut times: usize,
1570) -> (DisplayPoint, SelectionGoal) {
1571 if let Some(visible_rows) = text_layout_details.visible_rows {
1572 let first_visible_line = text_layout_details
1573 .scroll_anchor
1574 .anchor
1575 .to_display_point(map);
1576 let bottom_row = first_visible_line.row()
1577 + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
1578 if bottom_row < map.max_point().row()
1579 && text_layout_details.vertical_scroll_margin as usize > times
1580 {
1581 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1582 }
1583 let bottom_row_capped = bottom_row.min(map.max_point().row());
1584 let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row() {
1585 first_visible_line.row()
1586 } else {
1587 bottom_row_capped.saturating_sub(times as u32)
1588 };
1589 let new_col = point.column().min(map.line_len(new_row));
1590 let new_point = DisplayPoint::new(new_row, new_col);
1591 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1592 } else {
1593 (point, SelectionGoal::None)
1594 }
1595}
1596
1597#[cfg(test)]
1598mod test {
1599
1600 use crate::test::NeovimBackedTestContext;
1601 use indoc::indoc;
1602
1603 #[gpui::test]
1604 async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
1605 let mut cx = NeovimBackedTestContext::new(cx).await;
1606
1607 let initial_state = indoc! {r"ˇabc
1608 def
1609
1610 paragraph
1611 the second
1612
1613
1614
1615 third and
1616 final"};
1617
1618 // goes down once
1619 cx.set_shared_state(initial_state).await;
1620 cx.simulate_shared_keystrokes(["}"]).await;
1621 cx.assert_shared_state(indoc! {r"abc
1622 def
1623 ˇ
1624 paragraph
1625 the second
1626
1627
1628
1629 third and
1630 final"})
1631 .await;
1632
1633 // goes up once
1634 cx.simulate_shared_keystrokes(["{"]).await;
1635 cx.assert_shared_state(initial_state).await;
1636
1637 // goes down twice
1638 cx.simulate_shared_keystrokes(["2", "}"]).await;
1639 cx.assert_shared_state(indoc! {r"abc
1640 def
1641
1642 paragraph
1643 the second
1644 ˇ
1645
1646
1647 third and
1648 final"})
1649 .await;
1650
1651 // goes down over multiple blanks
1652 cx.simulate_shared_keystrokes(["}"]).await;
1653 cx.assert_shared_state(indoc! {r"abc
1654 def
1655
1656 paragraph
1657 the second
1658
1659
1660
1661 third and
1662 finaˇl"})
1663 .await;
1664
1665 // goes up twice
1666 cx.simulate_shared_keystrokes(["2", "{"]).await;
1667 cx.assert_shared_state(indoc! {r"abc
1668 def
1669 ˇ
1670 paragraph
1671 the second
1672
1673
1674
1675 third and
1676 final"})
1677 .await
1678 }
1679
1680 #[gpui::test]
1681 async fn test_matching(cx: &mut gpui::TestAppContext) {
1682 let mut cx = NeovimBackedTestContext::new(cx).await;
1683
1684 cx.set_shared_state(indoc! {r"func ˇ(a string) {
1685 do(something(with<Types>.and_arrays[0, 2]))
1686 }"})
1687 .await;
1688 cx.simulate_shared_keystrokes(["%"]).await;
1689 cx.assert_shared_state(indoc! {r"func (a stringˇ) {
1690 do(something(with<Types>.and_arrays[0, 2]))
1691 }"})
1692 .await;
1693
1694 // test it works on the last character of the line
1695 cx.set_shared_state(indoc! {r"func (a string) ˇ{
1696 do(something(with<Types>.and_arrays[0, 2]))
1697 }"})
1698 .await;
1699 cx.simulate_shared_keystrokes(["%"]).await;
1700 cx.assert_shared_state(indoc! {r"func (a string) {
1701 do(something(with<Types>.and_arrays[0, 2]))
1702 ˇ}"})
1703 .await;
1704
1705 // test it works on immediate nesting
1706 cx.set_shared_state("ˇ{()}").await;
1707 cx.simulate_shared_keystrokes(["%"]).await;
1708 cx.assert_shared_state("{()ˇ}").await;
1709 cx.simulate_shared_keystrokes(["%"]).await;
1710 cx.assert_shared_state("ˇ{()}").await;
1711
1712 // test it works on immediate nesting inside braces
1713 cx.set_shared_state("{\n ˇ{()}\n}").await;
1714 cx.simulate_shared_keystrokes(["%"]).await;
1715 cx.assert_shared_state("{\n {()ˇ}\n}").await;
1716
1717 // test it jumps to the next paren on a line
1718 cx.set_shared_state("func ˇboop() {\n}").await;
1719 cx.simulate_shared_keystrokes(["%"]).await;
1720 cx.assert_shared_state("func boop(ˇ) {\n}").await;
1721 }
1722
1723 #[gpui::test]
1724 async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
1725 let mut cx = NeovimBackedTestContext::new(cx).await;
1726
1727 // f and F
1728 cx.set_shared_state("ˇone two three four").await;
1729 cx.simulate_shared_keystrokes(["f", "o"]).await;
1730 cx.assert_shared_state("one twˇo three four").await;
1731 cx.simulate_shared_keystrokes([","]).await;
1732 cx.assert_shared_state("ˇone two three four").await;
1733 cx.simulate_shared_keystrokes(["2", ";"]).await;
1734 cx.assert_shared_state("one two three fˇour").await;
1735 cx.simulate_shared_keystrokes(["shift-f", "e"]).await;
1736 cx.assert_shared_state("one two threˇe four").await;
1737 cx.simulate_shared_keystrokes(["2", ";"]).await;
1738 cx.assert_shared_state("onˇe two three four").await;
1739 cx.simulate_shared_keystrokes([","]).await;
1740 cx.assert_shared_state("one two thrˇee four").await;
1741
1742 // t and T
1743 cx.set_shared_state("ˇone two three four").await;
1744 cx.simulate_shared_keystrokes(["t", "o"]).await;
1745 cx.assert_shared_state("one tˇwo three four").await;
1746 cx.simulate_shared_keystrokes([","]).await;
1747 cx.assert_shared_state("oˇne two three four").await;
1748 cx.simulate_shared_keystrokes(["2", ";"]).await;
1749 cx.assert_shared_state("one two three ˇfour").await;
1750 cx.simulate_shared_keystrokes(["shift-t", "e"]).await;
1751 cx.assert_shared_state("one two threeˇ four").await;
1752 cx.simulate_shared_keystrokes(["3", ";"]).await;
1753 cx.assert_shared_state("oneˇ two three four").await;
1754 cx.simulate_shared_keystrokes([","]).await;
1755 cx.assert_shared_state("one two thˇree four").await;
1756 }
1757
1758 #[gpui::test]
1759 async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
1760 let mut cx = NeovimBackedTestContext::new(cx).await;
1761 let initial_state = indoc! {r"something(ˇfoo)"};
1762 cx.set_shared_state(initial_state).await;
1763 cx.simulate_shared_keystrokes(["}"]).await;
1764 cx.assert_shared_state(indoc! {r"something(fooˇ)"}).await;
1765 }
1766
1767 #[gpui::test]
1768 async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
1769 let mut cx = NeovimBackedTestContext::new(cx).await;
1770 cx.set_shared_state("ˇone\n two\nthree").await;
1771 cx.simulate_shared_keystrokes(["enter"]).await;
1772 cx.assert_shared_state("one\n ˇtwo\nthree").await;
1773 }
1774
1775 #[gpui::test]
1776 async fn test_window_top(cx: &mut gpui::TestAppContext) {
1777 let mut cx = NeovimBackedTestContext::new(cx).await;
1778 let initial_state = indoc! {r"abc
1779 def
1780 paragraph
1781 the second
1782 third ˇand
1783 final"};
1784
1785 cx.set_shared_state(initial_state).await;
1786 cx.simulate_shared_keystrokes(["shift-h"]).await;
1787 cx.assert_shared_state(indoc! {r"abˇc
1788 def
1789 paragraph
1790 the second
1791 third and
1792 final"})
1793 .await;
1794
1795 // clip point
1796 cx.set_shared_state(indoc! {r"
1797 1 2 3
1798 4 5 6
1799 7 8 ˇ9
1800 "})
1801 .await;
1802 cx.simulate_shared_keystrokes(["shift-h"]).await;
1803 cx.assert_shared_state(indoc! {r"
1804 1 2 ˇ3
1805 4 5 6
1806 7 8 9
1807 "})
1808 .await;
1809
1810 cx.set_shared_state(indoc! {r"
1811 1 2 3
1812 4 5 6
1813 ˇ7 8 9
1814 "})
1815 .await;
1816 cx.simulate_shared_keystrokes(["shift-h"]).await;
1817 cx.assert_shared_state(indoc! {r"
1818 ˇ1 2 3
1819 4 5 6
1820 7 8 9
1821 "})
1822 .await;
1823
1824 cx.set_shared_state(indoc! {r"
1825 1 2 3
1826 4 5 ˇ6
1827 7 8 9"})
1828 .await;
1829 cx.simulate_shared_keystrokes(["9", "shift-h"]).await;
1830 cx.assert_shared_state(indoc! {r"
1831 1 2 3
1832 4 5 6
1833 7 8 ˇ9"})
1834 .await;
1835 }
1836
1837 #[gpui::test]
1838 async fn test_window_middle(cx: &mut gpui::TestAppContext) {
1839 let mut cx = NeovimBackedTestContext::new(cx).await;
1840 let initial_state = indoc! {r"abˇc
1841 def
1842 paragraph
1843 the second
1844 third and
1845 final"};
1846
1847 cx.set_shared_state(initial_state).await;
1848 cx.simulate_shared_keystrokes(["shift-m"]).await;
1849 cx.assert_shared_state(indoc! {r"abc
1850 def
1851 paˇragraph
1852 the second
1853 third and
1854 final"})
1855 .await;
1856
1857 cx.set_shared_state(indoc! {r"
1858 1 2 3
1859 4 5 6
1860 7 8 ˇ9
1861 "})
1862 .await;
1863 cx.simulate_shared_keystrokes(["shift-m"]).await;
1864 cx.assert_shared_state(indoc! {r"
1865 1 2 3
1866 4 5 ˇ6
1867 7 8 9
1868 "})
1869 .await;
1870 cx.set_shared_state(indoc! {r"
1871 1 2 3
1872 4 5 6
1873 ˇ7 8 9
1874 "})
1875 .await;
1876 cx.simulate_shared_keystrokes(["shift-m"]).await;
1877 cx.assert_shared_state(indoc! {r"
1878 1 2 3
1879 ˇ4 5 6
1880 7 8 9
1881 "})
1882 .await;
1883 cx.set_shared_state(indoc! {r"
1884 ˇ1 2 3
1885 4 5 6
1886 7 8 9
1887 "})
1888 .await;
1889 cx.simulate_shared_keystrokes(["shift-m"]).await;
1890 cx.assert_shared_state(indoc! {r"
1891 1 2 3
1892 ˇ4 5 6
1893 7 8 9
1894 "})
1895 .await;
1896 cx.set_shared_state(indoc! {r"
1897 1 2 3
1898 ˇ4 5 6
1899 7 8 9
1900 "})
1901 .await;
1902 cx.simulate_shared_keystrokes(["shift-m"]).await;
1903 cx.assert_shared_state(indoc! {r"
1904 1 2 3
1905 ˇ4 5 6
1906 7 8 9
1907 "})
1908 .await;
1909 cx.set_shared_state(indoc! {r"
1910 1 2 3
1911 4 5 ˇ6
1912 7 8 9
1913 "})
1914 .await;
1915 cx.simulate_shared_keystrokes(["shift-m"]).await;
1916 cx.assert_shared_state(indoc! {r"
1917 1 2 3
1918 4 5 ˇ6
1919 7 8 9
1920 "})
1921 .await;
1922 }
1923
1924 #[gpui::test]
1925 async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
1926 let mut cx = NeovimBackedTestContext::new(cx).await;
1927 let initial_state = indoc! {r"abc
1928 deˇf
1929 paragraph
1930 the second
1931 third and
1932 final"};
1933
1934 cx.set_shared_state(initial_state).await;
1935 cx.simulate_shared_keystrokes(["shift-l"]).await;
1936 cx.assert_shared_state(indoc! {r"abc
1937 def
1938 paragraph
1939 the second
1940 third and
1941 fiˇnal"})
1942 .await;
1943
1944 cx.set_shared_state(indoc! {r"
1945 1 2 3
1946 4 5 ˇ6
1947 7 8 9
1948 "})
1949 .await;
1950 cx.simulate_shared_keystrokes(["shift-l"]).await;
1951 cx.assert_shared_state(indoc! {r"
1952 1 2 3
1953 4 5 6
1954 7 8 9
1955 ˇ"})
1956 .await;
1957
1958 cx.set_shared_state(indoc! {r"
1959 1 2 3
1960 ˇ4 5 6
1961 7 8 9
1962 "})
1963 .await;
1964 cx.simulate_shared_keystrokes(["shift-l"]).await;
1965 cx.assert_shared_state(indoc! {r"
1966 1 2 3
1967 4 5 6
1968 7 8 9
1969 ˇ"})
1970 .await;
1971
1972 cx.set_shared_state(indoc! {r"
1973 1 2 ˇ3
1974 4 5 6
1975 7 8 9
1976 "})
1977 .await;
1978 cx.simulate_shared_keystrokes(["shift-l"]).await;
1979 cx.assert_shared_state(indoc! {r"
1980 1 2 3
1981 4 5 6
1982 7 8 9
1983 ˇ"})
1984 .await;
1985
1986 cx.set_shared_state(indoc! {r"
1987 ˇ1 2 3
1988 4 5 6
1989 7 8 9
1990 "})
1991 .await;
1992 cx.simulate_shared_keystrokes(["shift-l"]).await;
1993 cx.assert_shared_state(indoc! {r"
1994 1 2 3
1995 4 5 6
1996 7 8 9
1997 ˇ"})
1998 .await;
1999
2000 cx.set_shared_state(indoc! {r"
2001 1 2 3
2002 4 5 ˇ6
2003 7 8 9
2004 "})
2005 .await;
2006 cx.simulate_shared_keystrokes(["9", "shift-l"]).await;
2007 cx.assert_shared_state(indoc! {r"
2008 1 2 ˇ3
2009 4 5 6
2010 7 8 9
2011 "})
2012 .await;
2013 }
2014
2015 #[gpui::test]
2016 async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
2017 let mut cx = NeovimBackedTestContext::new(cx).await;
2018 cx.set_shared_state(indoc! {r"
2019 456 5ˇ67 678
2020 "})
2021 .await;
2022 cx.simulate_shared_keystrokes(["g", "e"]).await;
2023 cx.assert_shared_state(indoc! {r"
2024 45ˇ6 567 678
2025 "})
2026 .await;
2027
2028 // Test times
2029 cx.set_shared_state(indoc! {r"
2030 123 234 345
2031 456 5ˇ67 678
2032 "})
2033 .await;
2034 cx.simulate_shared_keystrokes(["4", "g", "e"]).await;
2035 cx.assert_shared_state(indoc! {r"
2036 12ˇ3 234 345
2037 456 567 678
2038 "})
2039 .await;
2040
2041 // With punctuation
2042 cx.set_shared_state(indoc! {r"
2043 123 234 345
2044 4;5.6 5ˇ67 678
2045 789 890 901
2046 "})
2047 .await;
2048 cx.simulate_shared_keystrokes(["g", "e"]).await;
2049 cx.assert_shared_state(indoc! {r"
2050 123 234 345
2051 4;5.ˇ6 567 678
2052 789 890 901
2053 "})
2054 .await;
2055
2056 // With punctuation and count
2057 cx.set_shared_state(indoc! {r"
2058 123 234 345
2059 4;5.6 5ˇ67 678
2060 789 890 901
2061 "})
2062 .await;
2063 cx.simulate_shared_keystrokes(["5", "g", "e"]).await;
2064 cx.assert_shared_state(indoc! {r"
2065 123 234 345
2066 ˇ4;5.6 567 678
2067 789 890 901
2068 "})
2069 .await;
2070
2071 // newlines
2072 cx.set_shared_state(indoc! {r"
2073 123 234 345
2074
2075 78ˇ9 890 901
2076 "})
2077 .await;
2078 cx.simulate_shared_keystrokes(["g", "e"]).await;
2079 cx.assert_shared_state(indoc! {r"
2080 123 234 345
2081 ˇ
2082 789 890 901
2083 "})
2084 .await;
2085 cx.simulate_shared_keystrokes(["g", "e"]).await;
2086 cx.assert_shared_state(indoc! {r"
2087 123 234 34ˇ5
2088
2089 789 890 901
2090 "})
2091 .await;
2092
2093 // With punctuation
2094 cx.set_shared_state(indoc! {r"
2095 123 234 345
2096 4;5.ˇ6 567 678
2097 789 890 901
2098 "})
2099 .await;
2100 cx.simulate_shared_keystrokes(["g", "shift-e"]).await;
2101 cx.assert_shared_state(indoc! {r"
2102 123 234 34ˇ5
2103 4;5.6 567 678
2104 789 890 901
2105 "})
2106 .await;
2107 }
2108}