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 | Mode::Replace => 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
803pub(crate) fn backspace(
804 map: &DisplaySnapshot,
805 mut point: DisplayPoint,
806 times: usize,
807) -> DisplayPoint {
808 for _ in 0..times {
809 point = movement::left(map, point);
810 if point.is_zero() {
811 break;
812 }
813 }
814 point
815}
816
817fn space(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
818 for _ in 0..times {
819 point = wrapping_right(map, point);
820 if point == map.max_point() {
821 break;
822 }
823 }
824 point
825}
826
827fn wrapping_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
828 let max_column = map.line_len(point.row()).saturating_sub(1);
829 if point.column() < max_column {
830 *point.column_mut() += 1;
831 } else if point.row() < map.max_point().row() {
832 *point.row_mut() += 1;
833 *point.column_mut() = 0;
834 }
835 point
836}
837
838pub(crate) fn start_of_relative_buffer_row(
839 map: &DisplaySnapshot,
840 point: DisplayPoint,
841 times: isize,
842) -> DisplayPoint {
843 let start = map.display_point_to_fold_point(point, Bias::Left);
844 let target = start.row() as isize + times;
845 let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
846
847 map.clip_point(
848 map.fold_point_to_display_point(
849 map.fold_snapshot
850 .clip_point(FoldPoint::new(new_row, 0), Bias::Right),
851 ),
852 Bias::Right,
853 )
854}
855
856fn up_down_buffer_rows(
857 map: &DisplaySnapshot,
858 point: DisplayPoint,
859 mut goal: SelectionGoal,
860 times: isize,
861 text_layout_details: &TextLayoutDetails,
862) -> (DisplayPoint, SelectionGoal) {
863 let start = map.display_point_to_fold_point(point, Bias::Left);
864 let begin_folded_line = map.fold_point_to_display_point(
865 map.fold_snapshot
866 .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
867 );
868 let select_nth_wrapped_row = point.row() - begin_folded_line.row();
869
870 let (goal_wrap, goal_x) = match goal {
871 SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
872 SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
873 SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
874 _ => {
875 let x = map.x_for_display_point(point, text_layout_details);
876 goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x.0));
877 (select_nth_wrapped_row, x.0)
878 }
879 };
880
881 let target = start.row() as isize + times;
882 let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
883
884 let mut begin_folded_line = map.fold_point_to_display_point(
885 map.fold_snapshot
886 .clip_point(FoldPoint::new(new_row, 0), Bias::Left),
887 );
888
889 let mut i = 0;
890 while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
891 let next_folded_line = DisplayPoint::new(begin_folded_line.row() + 1, 0);
892 if map
893 .display_point_to_fold_point(next_folded_line, Bias::Right)
894 .row()
895 == new_row
896 {
897 i += 1;
898 begin_folded_line = next_folded_line;
899 } else {
900 break;
901 }
902 }
903
904 let new_col = if i == goal_wrap {
905 map.display_column_for_x(begin_folded_line.row(), px(goal_x), text_layout_details)
906 } else {
907 map.line_len(begin_folded_line.row())
908 };
909
910 (
911 map.clip_point(
912 DisplayPoint::new(begin_folded_line.row(), new_col),
913 Bias::Left,
914 ),
915 goal,
916 )
917}
918
919fn down_display(
920 map: &DisplaySnapshot,
921 mut point: DisplayPoint,
922 mut goal: SelectionGoal,
923 times: usize,
924 text_layout_details: &TextLayoutDetails,
925) -> (DisplayPoint, SelectionGoal) {
926 for _ in 0..times {
927 (point, goal) = movement::down(map, point, goal, true, text_layout_details);
928 }
929
930 (point, goal)
931}
932
933fn up_display(
934 map: &DisplaySnapshot,
935 mut point: DisplayPoint,
936 mut goal: SelectionGoal,
937 times: usize,
938 text_layout_details: &TextLayoutDetails,
939) -> (DisplayPoint, SelectionGoal) {
940 for _ in 0..times {
941 (point, goal) = movement::up(map, point, goal, true, &text_layout_details);
942 }
943
944 (point, goal)
945}
946
947pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
948 for _ in 0..times {
949 let new_point = movement::saturating_right(map, point);
950 if point == new_point {
951 break;
952 }
953 point = new_point;
954 }
955 point
956}
957
958pub(crate) fn next_char(
959 map: &DisplaySnapshot,
960 point: DisplayPoint,
961 allow_cross_newline: bool,
962) -> DisplayPoint {
963 let mut new_point = point;
964 let mut max_column = map.line_len(new_point.row());
965 if !allow_cross_newline {
966 max_column -= 1;
967 }
968 if new_point.column() < max_column {
969 *new_point.column_mut() += 1;
970 } else if new_point < map.max_point() && allow_cross_newline {
971 *new_point.row_mut() += 1;
972 *new_point.column_mut() = 0;
973 }
974 new_point
975}
976
977pub(crate) fn next_word_start(
978 map: &DisplaySnapshot,
979 mut point: DisplayPoint,
980 ignore_punctuation: bool,
981 times: usize,
982) -> DisplayPoint {
983 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
984 for _ in 0..times {
985 let mut crossed_newline = false;
986 let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
987 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
988 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
989 let at_newline = right == '\n';
990
991 let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
992 || at_newline && crossed_newline
993 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
994
995 crossed_newline |= at_newline;
996 found
997 });
998 if point == new_point {
999 break;
1000 }
1001 point = new_point;
1002 }
1003 point
1004}
1005
1006pub(crate) fn next_word_end(
1007 map: &DisplaySnapshot,
1008 mut point: DisplayPoint,
1009 ignore_punctuation: bool,
1010 times: usize,
1011 allow_cross_newline: bool,
1012) -> DisplayPoint {
1013 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1014 for _ in 0..times {
1015 let new_point = next_char(map, point, allow_cross_newline);
1016 let mut need_next_char = false;
1017 let new_point = movement::find_boundary_exclusive(
1018 map,
1019 new_point,
1020 FindRange::MultiLine,
1021 |left, right| {
1022 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1023 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1024 let at_newline = right == '\n';
1025
1026 if !allow_cross_newline && at_newline {
1027 need_next_char = true;
1028 return true;
1029 }
1030
1031 left_kind != right_kind && left_kind != CharKind::Whitespace
1032 },
1033 );
1034 let new_point = if need_next_char {
1035 next_char(map, new_point, true)
1036 } else {
1037 new_point
1038 };
1039 let new_point = map.clip_point(new_point, Bias::Left);
1040 if point == new_point {
1041 break;
1042 }
1043 point = new_point;
1044 }
1045 point
1046}
1047
1048fn previous_word_start(
1049 map: &DisplaySnapshot,
1050 mut point: DisplayPoint,
1051 ignore_punctuation: bool,
1052 times: usize,
1053) -> DisplayPoint {
1054 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1055 for _ in 0..times {
1056 // This works even though find_preceding_boundary is called for every character in the line containing
1057 // cursor because the newline is checked only once.
1058 let new_point = movement::find_preceding_boundary_display_point(
1059 map,
1060 point,
1061 FindRange::MultiLine,
1062 |left, right| {
1063 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1064 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1065
1066 (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
1067 },
1068 );
1069 if point == new_point {
1070 break;
1071 }
1072 point = new_point;
1073 }
1074 point
1075}
1076
1077fn previous_word_end(
1078 map: &DisplaySnapshot,
1079 point: DisplayPoint,
1080 ignore_punctuation: bool,
1081 times: usize,
1082) -> DisplayPoint {
1083 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1084 let mut point = point.to_point(map);
1085
1086 if point.column < map.buffer_snapshot.line_len(point.row) {
1087 point.column += 1;
1088 }
1089 for _ in 0..times {
1090 let new_point = movement::find_preceding_boundary_point(
1091 &map.buffer_snapshot,
1092 point,
1093 FindRange::MultiLine,
1094 |left, right| {
1095 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1096 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1097 match (left_kind, right_kind) {
1098 (CharKind::Punctuation, CharKind::Whitespace)
1099 | (CharKind::Punctuation, CharKind::Word)
1100 | (CharKind::Word, CharKind::Whitespace)
1101 | (CharKind::Word, CharKind::Punctuation) => true,
1102 (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1103 _ => false,
1104 }
1105 },
1106 );
1107 if new_point == point {
1108 break;
1109 }
1110 point = new_point;
1111 }
1112 movement::saturating_left(map, point.to_display_point(map))
1113}
1114
1115fn next_subword_start(
1116 map: &DisplaySnapshot,
1117 mut point: DisplayPoint,
1118 ignore_punctuation: bool,
1119 times: usize,
1120) -> DisplayPoint {
1121 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1122 for _ in 0..times {
1123 let mut crossed_newline = false;
1124 let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1125 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1126 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1127 let at_newline = right == '\n';
1128
1129 let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1130 let is_subword_start =
1131 left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1132
1133 let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1134 || at_newline && crossed_newline
1135 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1136
1137 crossed_newline |= at_newline;
1138 found
1139 });
1140 if point == new_point {
1141 break;
1142 }
1143 point = new_point;
1144 }
1145 point
1146}
1147
1148pub(crate) fn next_subword_end(
1149 map: &DisplaySnapshot,
1150 mut point: DisplayPoint,
1151 ignore_punctuation: bool,
1152 times: usize,
1153 allow_cross_newline: bool,
1154) -> DisplayPoint {
1155 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1156 for _ in 0..times {
1157 let new_point = next_char(map, point, allow_cross_newline);
1158
1159 let mut crossed_newline = false;
1160 let mut need_backtrack = false;
1161 let new_point =
1162 movement::find_boundary(map, new_point, FindRange::MultiLine, |left, right| {
1163 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1164 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1165 let at_newline = right == '\n';
1166
1167 if !allow_cross_newline && at_newline {
1168 return true;
1169 }
1170
1171 let is_word_end = (left_kind != right_kind) && !right.is_alphanumeric();
1172 let is_subword_end =
1173 left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1174
1175 let found = !left.is_whitespace() && !at_newline && (is_word_end || is_subword_end);
1176
1177 if found && (is_word_end || is_subword_end) {
1178 need_backtrack = true;
1179 }
1180
1181 crossed_newline |= at_newline;
1182 found
1183 });
1184 let mut new_point = map.clip_point(new_point, Bias::Left);
1185 if need_backtrack {
1186 *new_point.column_mut() -= 1;
1187 }
1188 if point == new_point {
1189 break;
1190 }
1191 point = new_point;
1192 }
1193 point
1194}
1195
1196fn previous_subword_start(
1197 map: &DisplaySnapshot,
1198 mut point: DisplayPoint,
1199 ignore_punctuation: bool,
1200 times: usize,
1201) -> DisplayPoint {
1202 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1203 for _ in 0..times {
1204 let mut crossed_newline = false;
1205 // This works even though find_preceding_boundary is called for every character in the line containing
1206 // cursor because the newline is checked only once.
1207 let new_point = movement::find_preceding_boundary_display_point(
1208 map,
1209 point,
1210 FindRange::MultiLine,
1211 |left, right| {
1212 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1213 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1214 let at_newline = right == '\n';
1215
1216 let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1217 let is_subword_start =
1218 left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1219
1220 let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1221 || at_newline && crossed_newline
1222 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1223
1224 crossed_newline |= at_newline;
1225
1226 found
1227 },
1228 );
1229 if point == new_point {
1230 break;
1231 }
1232 point = new_point;
1233 }
1234 point
1235}
1236
1237fn previous_subword_end(
1238 map: &DisplaySnapshot,
1239 point: DisplayPoint,
1240 ignore_punctuation: bool,
1241 times: usize,
1242) -> DisplayPoint {
1243 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1244 let mut point = point.to_point(map);
1245
1246 if point.column < map.buffer_snapshot.line_len(point.row) {
1247 point.column += 1;
1248 }
1249 for _ in 0..times {
1250 let new_point = movement::find_preceding_boundary_point(
1251 &map.buffer_snapshot,
1252 point,
1253 FindRange::MultiLine,
1254 |left, right| {
1255 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1256 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1257
1258 let is_subword_end =
1259 left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1260
1261 if is_subword_end {
1262 return true;
1263 }
1264
1265 match (left_kind, right_kind) {
1266 (CharKind::Word, CharKind::Whitespace)
1267 | (CharKind::Word, CharKind::Punctuation) => true,
1268 (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1269 _ => false,
1270 }
1271 },
1272 );
1273 if new_point == point {
1274 break;
1275 }
1276 point = new_point;
1277 }
1278 movement::saturating_left(map, point.to_display_point(map))
1279}
1280
1281pub(crate) fn first_non_whitespace(
1282 map: &DisplaySnapshot,
1283 display_lines: bool,
1284 from: DisplayPoint,
1285) -> DisplayPoint {
1286 let mut start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left);
1287 let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
1288 for (ch, offset) in map.buffer_chars_at(start_offset) {
1289 if ch == '\n' {
1290 return from;
1291 }
1292
1293 start_offset = offset;
1294
1295 if char_kind(&scope, ch) != CharKind::Whitespace {
1296 break;
1297 }
1298 }
1299
1300 start_offset.to_display_point(map)
1301}
1302
1303pub(crate) fn start_of_line(
1304 map: &DisplaySnapshot,
1305 display_lines: bool,
1306 point: DisplayPoint,
1307) -> DisplayPoint {
1308 if display_lines {
1309 map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
1310 } else {
1311 map.prev_line_boundary(point.to_point(map)).1
1312 }
1313}
1314
1315pub(crate) fn end_of_line(
1316 map: &DisplaySnapshot,
1317 display_lines: bool,
1318 mut point: DisplayPoint,
1319 times: usize,
1320) -> DisplayPoint {
1321 if times > 1 {
1322 point = start_of_relative_buffer_row(map, point, times as isize - 1);
1323 }
1324 if display_lines {
1325 map.clip_point(
1326 DisplayPoint::new(point.row(), map.line_len(point.row())),
1327 Bias::Left,
1328 )
1329 } else {
1330 map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
1331 }
1332}
1333
1334fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
1335 let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
1336 *new_point.column_mut() = point.column();
1337 map.clip_point(new_point, Bias::Left)
1338}
1339
1340fn end_of_document(
1341 map: &DisplaySnapshot,
1342 point: DisplayPoint,
1343 line: Option<usize>,
1344) -> DisplayPoint {
1345 let new_row = if let Some(line) = line {
1346 (line - 1) as u32
1347 } else {
1348 map.max_buffer_row()
1349 };
1350
1351 let new_point = Point::new(new_row, point.column());
1352 map.clip_point(new_point.to_display_point(map), Bias::Left)
1353}
1354
1355fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
1356 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
1357 let display_point = map.clip_at_line_end(display_point);
1358 let point = display_point.to_point(map);
1359 let offset = point.to_offset(&map.buffer_snapshot);
1360
1361 // Ensure the range is contained by the current line.
1362 let mut line_end = map.next_line_boundary(point).0;
1363 if line_end == point {
1364 line_end = map.max_point().to_point(map);
1365 }
1366
1367 let line_range = map.prev_line_boundary(point).0..line_end;
1368 let visible_line_range =
1369 line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
1370 let ranges = map
1371 .buffer_snapshot
1372 .bracket_ranges(visible_line_range.clone());
1373 if let Some(ranges) = ranges {
1374 let line_range = line_range.start.to_offset(&map.buffer_snapshot)
1375 ..line_range.end.to_offset(&map.buffer_snapshot);
1376 let mut closest_pair_destination = None;
1377 let mut closest_distance = usize::MAX;
1378
1379 for (open_range, close_range) in ranges {
1380 if open_range.start >= offset && line_range.contains(&open_range.start) {
1381 let distance = open_range.start - offset;
1382 if distance < closest_distance {
1383 closest_pair_destination = Some(close_range.start);
1384 closest_distance = distance;
1385 continue;
1386 }
1387 }
1388
1389 if close_range.start >= offset && line_range.contains(&close_range.start) {
1390 let distance = close_range.start - offset;
1391 if distance < closest_distance {
1392 closest_pair_destination = Some(open_range.start);
1393 closest_distance = distance;
1394 continue;
1395 }
1396 }
1397
1398 continue;
1399 }
1400
1401 closest_pair_destination
1402 .map(|destination| destination.to_display_point(map))
1403 .unwrap_or(display_point)
1404 } else {
1405 display_point
1406 }
1407}
1408
1409fn find_forward(
1410 map: &DisplaySnapshot,
1411 from: DisplayPoint,
1412 before: bool,
1413 target: char,
1414 times: usize,
1415 mode: FindRange,
1416 smartcase: bool,
1417) -> Option<DisplayPoint> {
1418 let mut to = from;
1419 let mut found = false;
1420
1421 for _ in 0..times {
1422 found = false;
1423 let new_to = find_boundary(map, to, mode, |_, right| {
1424 found = is_character_match(target, right, smartcase);
1425 found
1426 });
1427 if to == new_to {
1428 break;
1429 }
1430 to = new_to;
1431 }
1432
1433 if found {
1434 if before && to.column() > 0 {
1435 *to.column_mut() -= 1;
1436 Some(map.clip_point(to, Bias::Left))
1437 } else {
1438 Some(to)
1439 }
1440 } else {
1441 None
1442 }
1443}
1444
1445fn find_backward(
1446 map: &DisplaySnapshot,
1447 from: DisplayPoint,
1448 after: bool,
1449 target: char,
1450 times: usize,
1451 mode: FindRange,
1452 smartcase: bool,
1453) -> DisplayPoint {
1454 let mut to = from;
1455
1456 for _ in 0..times {
1457 let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
1458 is_character_match(target, right, smartcase)
1459 });
1460 if to == new_to {
1461 break;
1462 }
1463 to = new_to;
1464 }
1465
1466 let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
1467 if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
1468 if after {
1469 *to.column_mut() += 1;
1470 map.clip_point(to, Bias::Right)
1471 } else {
1472 to
1473 }
1474 } else {
1475 from
1476 }
1477}
1478
1479fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
1480 if smartcase {
1481 if target.is_uppercase() {
1482 target == other
1483 } else {
1484 target == other.to_ascii_lowercase()
1485 }
1486 } else {
1487 target == other
1488 }
1489}
1490
1491fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1492 let correct_line = start_of_relative_buffer_row(map, point, times as isize);
1493 first_non_whitespace(map, false, correct_line)
1494}
1495
1496fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1497 let correct_line = start_of_relative_buffer_row(map, point, 0);
1498 right(map, correct_line, times.saturating_sub(1))
1499}
1500
1501pub(crate) fn next_line_end(
1502 map: &DisplaySnapshot,
1503 mut point: DisplayPoint,
1504 times: usize,
1505) -> DisplayPoint {
1506 if times > 1 {
1507 point = start_of_relative_buffer_row(map, point, times as isize - 1);
1508 }
1509 end_of_line(map, false, point, 1)
1510}
1511
1512fn window_top(
1513 map: &DisplaySnapshot,
1514 point: DisplayPoint,
1515 text_layout_details: &TextLayoutDetails,
1516 mut times: usize,
1517) -> (DisplayPoint, SelectionGoal) {
1518 let first_visible_line = text_layout_details
1519 .scroll_anchor
1520 .anchor
1521 .to_display_point(map);
1522
1523 if first_visible_line.row() != 0 && text_layout_details.vertical_scroll_margin as usize > times
1524 {
1525 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1526 }
1527
1528 if let Some(visible_rows) = text_layout_details.visible_rows {
1529 let bottom_row = first_visible_line.row() + visible_rows as u32;
1530 let new_row = (first_visible_line.row() + (times as u32))
1531 .min(bottom_row)
1532 .min(map.max_point().row());
1533 let new_col = point.column().min(map.line_len(first_visible_line.row()));
1534
1535 let new_point = DisplayPoint::new(new_row, new_col);
1536 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1537 } else {
1538 let new_row = (first_visible_line.row() + (times as u32)).min(map.max_point().row());
1539 let new_col = point.column().min(map.line_len(first_visible_line.row()));
1540
1541 let new_point = DisplayPoint::new(new_row, new_col);
1542 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1543 }
1544}
1545
1546fn window_middle(
1547 map: &DisplaySnapshot,
1548 point: DisplayPoint,
1549 text_layout_details: &TextLayoutDetails,
1550) -> (DisplayPoint, SelectionGoal) {
1551 if let Some(visible_rows) = text_layout_details.visible_rows {
1552 let first_visible_line = text_layout_details
1553 .scroll_anchor
1554 .anchor
1555 .to_display_point(map);
1556
1557 let max_visible_rows =
1558 (visible_rows as u32).min(map.max_point().row() - first_visible_line.row());
1559
1560 let new_row =
1561 (first_visible_line.row() + (max_visible_rows / 2)).min(map.max_point().row());
1562 let new_col = point.column().min(map.line_len(new_row));
1563 let new_point = DisplayPoint::new(new_row, new_col);
1564 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1565 } else {
1566 (point, SelectionGoal::None)
1567 }
1568}
1569
1570fn window_bottom(
1571 map: &DisplaySnapshot,
1572 point: DisplayPoint,
1573 text_layout_details: &TextLayoutDetails,
1574 mut times: usize,
1575) -> (DisplayPoint, SelectionGoal) {
1576 if let Some(visible_rows) = text_layout_details.visible_rows {
1577 let first_visible_line = text_layout_details
1578 .scroll_anchor
1579 .anchor
1580 .to_display_point(map);
1581 let bottom_row = first_visible_line.row()
1582 + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
1583 if bottom_row < map.max_point().row()
1584 && text_layout_details.vertical_scroll_margin as usize > times
1585 {
1586 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1587 }
1588 let bottom_row_capped = bottom_row.min(map.max_point().row());
1589 let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row() {
1590 first_visible_line.row()
1591 } else {
1592 bottom_row_capped.saturating_sub(times as u32)
1593 };
1594 let new_col = point.column().min(map.line_len(new_row));
1595 let new_point = DisplayPoint::new(new_row, new_col);
1596 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1597 } else {
1598 (point, SelectionGoal::None)
1599 }
1600}
1601
1602#[cfg(test)]
1603mod test {
1604
1605 use crate::test::NeovimBackedTestContext;
1606 use indoc::indoc;
1607
1608 #[gpui::test]
1609 async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
1610 let mut cx = NeovimBackedTestContext::new(cx).await;
1611
1612 let initial_state = indoc! {r"ˇabc
1613 def
1614
1615 paragraph
1616 the second
1617
1618
1619
1620 third and
1621 final"};
1622
1623 // goes down once
1624 cx.set_shared_state(initial_state).await;
1625 cx.simulate_shared_keystrokes(["}"]).await;
1626 cx.assert_shared_state(indoc! {r"abc
1627 def
1628 ˇ
1629 paragraph
1630 the second
1631
1632
1633
1634 third and
1635 final"})
1636 .await;
1637
1638 // goes up once
1639 cx.simulate_shared_keystrokes(["{"]).await;
1640 cx.assert_shared_state(initial_state).await;
1641
1642 // goes down twice
1643 cx.simulate_shared_keystrokes(["2", "}"]).await;
1644 cx.assert_shared_state(indoc! {r"abc
1645 def
1646
1647 paragraph
1648 the second
1649 ˇ
1650
1651
1652 third and
1653 final"})
1654 .await;
1655
1656 // goes down over multiple blanks
1657 cx.simulate_shared_keystrokes(["}"]).await;
1658 cx.assert_shared_state(indoc! {r"abc
1659 def
1660
1661 paragraph
1662 the second
1663
1664
1665
1666 third and
1667 finaˇl"})
1668 .await;
1669
1670 // goes up twice
1671 cx.simulate_shared_keystrokes(["2", "{"]).await;
1672 cx.assert_shared_state(indoc! {r"abc
1673 def
1674 ˇ
1675 paragraph
1676 the second
1677
1678
1679
1680 third and
1681 final"})
1682 .await
1683 }
1684
1685 #[gpui::test]
1686 async fn test_matching(cx: &mut gpui::TestAppContext) {
1687 let mut cx = NeovimBackedTestContext::new(cx).await;
1688
1689 cx.set_shared_state(indoc! {r"func ˇ(a string) {
1690 do(something(with<Types>.and_arrays[0, 2]))
1691 }"})
1692 .await;
1693 cx.simulate_shared_keystrokes(["%"]).await;
1694 cx.assert_shared_state(indoc! {r"func (a stringˇ) {
1695 do(something(with<Types>.and_arrays[0, 2]))
1696 }"})
1697 .await;
1698
1699 // test it works on the last character of the line
1700 cx.set_shared_state(indoc! {r"func (a string) ˇ{
1701 do(something(with<Types>.and_arrays[0, 2]))
1702 }"})
1703 .await;
1704 cx.simulate_shared_keystrokes(["%"]).await;
1705 cx.assert_shared_state(indoc! {r"func (a string) {
1706 do(something(with<Types>.and_arrays[0, 2]))
1707 ˇ}"})
1708 .await;
1709
1710 // test it works on immediate nesting
1711 cx.set_shared_state("ˇ{()}").await;
1712 cx.simulate_shared_keystrokes(["%"]).await;
1713 cx.assert_shared_state("{()ˇ}").await;
1714 cx.simulate_shared_keystrokes(["%"]).await;
1715 cx.assert_shared_state("ˇ{()}").await;
1716
1717 // test it works on immediate nesting inside braces
1718 cx.set_shared_state("{\n ˇ{()}\n}").await;
1719 cx.simulate_shared_keystrokes(["%"]).await;
1720 cx.assert_shared_state("{\n {()ˇ}\n}").await;
1721
1722 // test it jumps to the next paren on a line
1723 cx.set_shared_state("func ˇboop() {\n}").await;
1724 cx.simulate_shared_keystrokes(["%"]).await;
1725 cx.assert_shared_state("func boop(ˇ) {\n}").await;
1726 }
1727
1728 #[gpui::test]
1729 async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
1730 let mut cx = NeovimBackedTestContext::new(cx).await;
1731
1732 // f and F
1733 cx.set_shared_state("ˇone two three four").await;
1734 cx.simulate_shared_keystrokes(["f", "o"]).await;
1735 cx.assert_shared_state("one twˇo three four").await;
1736 cx.simulate_shared_keystrokes([","]).await;
1737 cx.assert_shared_state("ˇone two three four").await;
1738 cx.simulate_shared_keystrokes(["2", ";"]).await;
1739 cx.assert_shared_state("one two three fˇour").await;
1740 cx.simulate_shared_keystrokes(["shift-f", "e"]).await;
1741 cx.assert_shared_state("one two threˇe four").await;
1742 cx.simulate_shared_keystrokes(["2", ";"]).await;
1743 cx.assert_shared_state("onˇe two three four").await;
1744 cx.simulate_shared_keystrokes([","]).await;
1745 cx.assert_shared_state("one two thrˇee four").await;
1746
1747 // t and T
1748 cx.set_shared_state("ˇone two three four").await;
1749 cx.simulate_shared_keystrokes(["t", "o"]).await;
1750 cx.assert_shared_state("one tˇwo three four").await;
1751 cx.simulate_shared_keystrokes([","]).await;
1752 cx.assert_shared_state("oˇne two three four").await;
1753 cx.simulate_shared_keystrokes(["2", ";"]).await;
1754 cx.assert_shared_state("one two three ˇfour").await;
1755 cx.simulate_shared_keystrokes(["shift-t", "e"]).await;
1756 cx.assert_shared_state("one two threeˇ four").await;
1757 cx.simulate_shared_keystrokes(["3", ";"]).await;
1758 cx.assert_shared_state("oneˇ two three four").await;
1759 cx.simulate_shared_keystrokes([","]).await;
1760 cx.assert_shared_state("one two thˇree four").await;
1761 }
1762
1763 #[gpui::test]
1764 async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
1765 let mut cx = NeovimBackedTestContext::new(cx).await;
1766 let initial_state = indoc! {r"something(ˇfoo)"};
1767 cx.set_shared_state(initial_state).await;
1768 cx.simulate_shared_keystrokes(["}"]).await;
1769 cx.assert_shared_state(indoc! {r"something(fooˇ)"}).await;
1770 }
1771
1772 #[gpui::test]
1773 async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
1774 let mut cx = NeovimBackedTestContext::new(cx).await;
1775 cx.set_shared_state("ˇone\n two\nthree").await;
1776 cx.simulate_shared_keystrokes(["enter"]).await;
1777 cx.assert_shared_state("one\n ˇtwo\nthree").await;
1778 }
1779
1780 #[gpui::test]
1781 async fn test_window_top(cx: &mut gpui::TestAppContext) {
1782 let mut cx = NeovimBackedTestContext::new(cx).await;
1783 let initial_state = indoc! {r"abc
1784 def
1785 paragraph
1786 the second
1787 third ˇand
1788 final"};
1789
1790 cx.set_shared_state(initial_state).await;
1791 cx.simulate_shared_keystrokes(["shift-h"]).await;
1792 cx.assert_shared_state(indoc! {r"abˇc
1793 def
1794 paragraph
1795 the second
1796 third and
1797 final"})
1798 .await;
1799
1800 // clip point
1801 cx.set_shared_state(indoc! {r"
1802 1 2 3
1803 4 5 6
1804 7 8 ˇ9
1805 "})
1806 .await;
1807 cx.simulate_shared_keystrokes(["shift-h"]).await;
1808 cx.assert_shared_state(indoc! {r"
1809 1 2 ˇ3
1810 4 5 6
1811 7 8 9
1812 "})
1813 .await;
1814
1815 cx.set_shared_state(indoc! {r"
1816 1 2 3
1817 4 5 6
1818 ˇ7 8 9
1819 "})
1820 .await;
1821 cx.simulate_shared_keystrokes(["shift-h"]).await;
1822 cx.assert_shared_state(indoc! {r"
1823 ˇ1 2 3
1824 4 5 6
1825 7 8 9
1826 "})
1827 .await;
1828
1829 cx.set_shared_state(indoc! {r"
1830 1 2 3
1831 4 5 ˇ6
1832 7 8 9"})
1833 .await;
1834 cx.simulate_shared_keystrokes(["9", "shift-h"]).await;
1835 cx.assert_shared_state(indoc! {r"
1836 1 2 3
1837 4 5 6
1838 7 8 ˇ9"})
1839 .await;
1840 }
1841
1842 #[gpui::test]
1843 async fn test_window_middle(cx: &mut gpui::TestAppContext) {
1844 let mut cx = NeovimBackedTestContext::new(cx).await;
1845 let initial_state = indoc! {r"abˇc
1846 def
1847 paragraph
1848 the second
1849 third and
1850 final"};
1851
1852 cx.set_shared_state(initial_state).await;
1853 cx.simulate_shared_keystrokes(["shift-m"]).await;
1854 cx.assert_shared_state(indoc! {r"abc
1855 def
1856 paˇragraph
1857 the second
1858 third and
1859 final"})
1860 .await;
1861
1862 cx.set_shared_state(indoc! {r"
1863 1 2 3
1864 4 5 6
1865 7 8 ˇ9
1866 "})
1867 .await;
1868 cx.simulate_shared_keystrokes(["shift-m"]).await;
1869 cx.assert_shared_state(indoc! {r"
1870 1 2 3
1871 4 5 ˇ6
1872 7 8 9
1873 "})
1874 .await;
1875 cx.set_shared_state(indoc! {r"
1876 1 2 3
1877 4 5 6
1878 ˇ7 8 9
1879 "})
1880 .await;
1881 cx.simulate_shared_keystrokes(["shift-m"]).await;
1882 cx.assert_shared_state(indoc! {r"
1883 1 2 3
1884 ˇ4 5 6
1885 7 8 9
1886 "})
1887 .await;
1888 cx.set_shared_state(indoc! {r"
1889 ˇ1 2 3
1890 4 5 6
1891 7 8 9
1892 "})
1893 .await;
1894 cx.simulate_shared_keystrokes(["shift-m"]).await;
1895 cx.assert_shared_state(indoc! {r"
1896 1 2 3
1897 ˇ4 5 6
1898 7 8 9
1899 "})
1900 .await;
1901 cx.set_shared_state(indoc! {r"
1902 1 2 3
1903 ˇ4 5 6
1904 7 8 9
1905 "})
1906 .await;
1907 cx.simulate_shared_keystrokes(["shift-m"]).await;
1908 cx.assert_shared_state(indoc! {r"
1909 1 2 3
1910 ˇ4 5 6
1911 7 8 9
1912 "})
1913 .await;
1914 cx.set_shared_state(indoc! {r"
1915 1 2 3
1916 4 5 ˇ6
1917 7 8 9
1918 "})
1919 .await;
1920 cx.simulate_shared_keystrokes(["shift-m"]).await;
1921 cx.assert_shared_state(indoc! {r"
1922 1 2 3
1923 4 5 ˇ6
1924 7 8 9
1925 "})
1926 .await;
1927 }
1928
1929 #[gpui::test]
1930 async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
1931 let mut cx = NeovimBackedTestContext::new(cx).await;
1932 let initial_state = indoc! {r"abc
1933 deˇf
1934 paragraph
1935 the second
1936 third and
1937 final"};
1938
1939 cx.set_shared_state(initial_state).await;
1940 cx.simulate_shared_keystrokes(["shift-l"]).await;
1941 cx.assert_shared_state(indoc! {r"abc
1942 def
1943 paragraph
1944 the second
1945 third and
1946 fiˇnal"})
1947 .await;
1948
1949 cx.set_shared_state(indoc! {r"
1950 1 2 3
1951 4 5 ˇ6
1952 7 8 9
1953 "})
1954 .await;
1955 cx.simulate_shared_keystrokes(["shift-l"]).await;
1956 cx.assert_shared_state(indoc! {r"
1957 1 2 3
1958 4 5 6
1959 7 8 9
1960 ˇ"})
1961 .await;
1962
1963 cx.set_shared_state(indoc! {r"
1964 1 2 3
1965 ˇ4 5 6
1966 7 8 9
1967 "})
1968 .await;
1969 cx.simulate_shared_keystrokes(["shift-l"]).await;
1970 cx.assert_shared_state(indoc! {r"
1971 1 2 3
1972 4 5 6
1973 7 8 9
1974 ˇ"})
1975 .await;
1976
1977 cx.set_shared_state(indoc! {r"
1978 1 2 ˇ3
1979 4 5 6
1980 7 8 9
1981 "})
1982 .await;
1983 cx.simulate_shared_keystrokes(["shift-l"]).await;
1984 cx.assert_shared_state(indoc! {r"
1985 1 2 3
1986 4 5 6
1987 7 8 9
1988 ˇ"})
1989 .await;
1990
1991 cx.set_shared_state(indoc! {r"
1992 ˇ1 2 3
1993 4 5 6
1994 7 8 9
1995 "})
1996 .await;
1997 cx.simulate_shared_keystrokes(["shift-l"]).await;
1998 cx.assert_shared_state(indoc! {r"
1999 1 2 3
2000 4 5 6
2001 7 8 9
2002 ˇ"})
2003 .await;
2004
2005 cx.set_shared_state(indoc! {r"
2006 1 2 3
2007 4 5 ˇ6
2008 7 8 9
2009 "})
2010 .await;
2011 cx.simulate_shared_keystrokes(["9", "shift-l"]).await;
2012 cx.assert_shared_state(indoc! {r"
2013 1 2 ˇ3
2014 4 5 6
2015 7 8 9
2016 "})
2017 .await;
2018 }
2019
2020 #[gpui::test]
2021 async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
2022 let mut cx = NeovimBackedTestContext::new(cx).await;
2023 cx.set_shared_state(indoc! {r"
2024 456 5ˇ67 678
2025 "})
2026 .await;
2027 cx.simulate_shared_keystrokes(["g", "e"]).await;
2028 cx.assert_shared_state(indoc! {r"
2029 45ˇ6 567 678
2030 "})
2031 .await;
2032
2033 // Test times
2034 cx.set_shared_state(indoc! {r"
2035 123 234 345
2036 456 5ˇ67 678
2037 "})
2038 .await;
2039 cx.simulate_shared_keystrokes(["4", "g", "e"]).await;
2040 cx.assert_shared_state(indoc! {r"
2041 12ˇ3 234 345
2042 456 567 678
2043 "})
2044 .await;
2045
2046 // With punctuation
2047 cx.set_shared_state(indoc! {r"
2048 123 234 345
2049 4;5.6 5ˇ67 678
2050 789 890 901
2051 "})
2052 .await;
2053 cx.simulate_shared_keystrokes(["g", "e"]).await;
2054 cx.assert_shared_state(indoc! {r"
2055 123 234 345
2056 4;5.ˇ6 567 678
2057 789 890 901
2058 "})
2059 .await;
2060
2061 // With punctuation and count
2062 cx.set_shared_state(indoc! {r"
2063 123 234 345
2064 4;5.6 5ˇ67 678
2065 789 890 901
2066 "})
2067 .await;
2068 cx.simulate_shared_keystrokes(["5", "g", "e"]).await;
2069 cx.assert_shared_state(indoc! {r"
2070 123 234 345
2071 ˇ4;5.6 567 678
2072 789 890 901
2073 "})
2074 .await;
2075
2076 // newlines
2077 cx.set_shared_state(indoc! {r"
2078 123 234 345
2079
2080 78ˇ9 890 901
2081 "})
2082 .await;
2083 cx.simulate_shared_keystrokes(["g", "e"]).await;
2084 cx.assert_shared_state(indoc! {r"
2085 123 234 345
2086 ˇ
2087 789 890 901
2088 "})
2089 .await;
2090 cx.simulate_shared_keystrokes(["g", "e"]).await;
2091 cx.assert_shared_state(indoc! {r"
2092 123 234 34ˇ5
2093
2094 789 890 901
2095 "})
2096 .await;
2097
2098 // With punctuation
2099 cx.set_shared_state(indoc! {r"
2100 123 234 345
2101 4;5.ˇ6 567 678
2102 789 890 901
2103 "})
2104 .await;
2105 cx.simulate_shared_keystrokes(["g", "shift-e"]).await;
2106 cx.assert_shared_state(indoc! {r"
2107 123 234 34ˇ5
2108 4;5.6 567 678
2109 789 890 901
2110 "})
2111 .await;
2112 }
2113
2114 #[gpui::test]
2115 async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
2116 let mut cx = NeovimBackedTestContext::new(cx).await;
2117
2118 cx.set_shared_state(indoc! {"
2119 fn aˇ() {
2120 return
2121 }
2122 "})
2123 .await;
2124 cx.simulate_shared_keystrokes(["v", "$", "%"]).await;
2125 cx.assert_shared_state(indoc! {"
2126 fn a«() {
2127 return
2128 }ˇ»
2129 "})
2130 .await;
2131 }
2132}