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 last_point = start_of_line(map, display_lines, from);
1287 let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
1288 for (ch, point) in map.chars_at(last_point) {
1289 if ch == '\n' {
1290 return from;
1291 }
1292
1293 last_point = point;
1294
1295 if char_kind(&scope, ch) != CharKind::Whitespace {
1296 break;
1297 }
1298 }
1299
1300 map.clip_point(last_point, Bias::Left)
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 point = display_point.to_point(map);
1358 let offset = point.to_offset(&map.buffer_snapshot);
1359
1360 // Ensure the range is contained by the current line.
1361 let mut line_end = map.next_line_boundary(point).0;
1362 if line_end == point {
1363 line_end = map.max_point().to_point(map);
1364 }
1365
1366 let line_range = map.prev_line_boundary(point).0..line_end;
1367 let visible_line_range =
1368 line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
1369 let ranges = map
1370 .buffer_snapshot
1371 .bracket_ranges(visible_line_range.clone());
1372 if let Some(ranges) = ranges {
1373 let line_range = line_range.start.to_offset(&map.buffer_snapshot)
1374 ..line_range.end.to_offset(&map.buffer_snapshot);
1375 let mut closest_pair_destination = None;
1376 let mut closest_distance = usize::MAX;
1377
1378 for (open_range, close_range) in ranges {
1379 if open_range.start >= offset && line_range.contains(&open_range.start) {
1380 let distance = open_range.start - offset;
1381 if distance < closest_distance {
1382 closest_pair_destination = Some(close_range.start);
1383 closest_distance = distance;
1384 continue;
1385 }
1386 }
1387
1388 if close_range.start >= offset && line_range.contains(&close_range.start) {
1389 let distance = close_range.start - offset;
1390 if distance < closest_distance {
1391 closest_pair_destination = Some(open_range.start);
1392 closest_distance = distance;
1393 continue;
1394 }
1395 }
1396
1397 continue;
1398 }
1399
1400 closest_pair_destination
1401 .map(|destination| destination.to_display_point(map))
1402 .unwrap_or(display_point)
1403 } else {
1404 display_point
1405 }
1406}
1407
1408fn find_forward(
1409 map: &DisplaySnapshot,
1410 from: DisplayPoint,
1411 before: bool,
1412 target: char,
1413 times: usize,
1414 mode: FindRange,
1415 smartcase: bool,
1416) -> Option<DisplayPoint> {
1417 let mut to = from;
1418 let mut found = false;
1419
1420 for _ in 0..times {
1421 found = false;
1422 let new_to = find_boundary(map, to, mode, |_, right| {
1423 found = is_character_match(target, right, smartcase);
1424 found
1425 });
1426 if to == new_to {
1427 break;
1428 }
1429 to = new_to;
1430 }
1431
1432 if found {
1433 if before && to.column() > 0 {
1434 *to.column_mut() -= 1;
1435 Some(map.clip_point(to, Bias::Left))
1436 } else {
1437 Some(to)
1438 }
1439 } else {
1440 None
1441 }
1442}
1443
1444fn find_backward(
1445 map: &DisplaySnapshot,
1446 from: DisplayPoint,
1447 after: bool,
1448 target: char,
1449 times: usize,
1450 mode: FindRange,
1451 smartcase: bool,
1452) -> DisplayPoint {
1453 let mut to = from;
1454
1455 for _ in 0..times {
1456 let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
1457 is_character_match(target, right, smartcase)
1458 });
1459 if to == new_to {
1460 break;
1461 }
1462 to = new_to;
1463 }
1464
1465 let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
1466 if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
1467 if after {
1468 *to.column_mut() += 1;
1469 map.clip_point(to, Bias::Right)
1470 } else {
1471 to
1472 }
1473 } else {
1474 from
1475 }
1476}
1477
1478fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
1479 if smartcase {
1480 if target.is_uppercase() {
1481 target == other
1482 } else {
1483 target == other.to_ascii_lowercase()
1484 }
1485 } else {
1486 target == other
1487 }
1488}
1489
1490fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1491 let correct_line = start_of_relative_buffer_row(map, point, times as isize);
1492 first_non_whitespace(map, false, correct_line)
1493}
1494
1495fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1496 let correct_line = start_of_relative_buffer_row(map, point, 0);
1497 right(map, correct_line, times.saturating_sub(1))
1498}
1499
1500pub(crate) fn next_line_end(
1501 map: &DisplaySnapshot,
1502 mut point: DisplayPoint,
1503 times: usize,
1504) -> DisplayPoint {
1505 if times > 1 {
1506 point = start_of_relative_buffer_row(map, point, times as isize - 1);
1507 }
1508 end_of_line(map, false, point, 1)
1509}
1510
1511fn window_top(
1512 map: &DisplaySnapshot,
1513 point: DisplayPoint,
1514 text_layout_details: &TextLayoutDetails,
1515 mut times: usize,
1516) -> (DisplayPoint, SelectionGoal) {
1517 let first_visible_line = text_layout_details
1518 .scroll_anchor
1519 .anchor
1520 .to_display_point(map);
1521
1522 if first_visible_line.row() != 0 && text_layout_details.vertical_scroll_margin as usize > times
1523 {
1524 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1525 }
1526
1527 if let Some(visible_rows) = text_layout_details.visible_rows {
1528 let bottom_row = first_visible_line.row() + visible_rows as u32;
1529 let new_row = (first_visible_line.row() + (times as u32))
1530 .min(bottom_row)
1531 .min(map.max_point().row());
1532 let new_col = point.column().min(map.line_len(first_visible_line.row()));
1533
1534 let new_point = DisplayPoint::new(new_row, new_col);
1535 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1536 } else {
1537 let new_row = (first_visible_line.row() + (times as u32)).min(map.max_point().row());
1538 let new_col = point.column().min(map.line_len(first_visible_line.row()));
1539
1540 let new_point = DisplayPoint::new(new_row, new_col);
1541 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1542 }
1543}
1544
1545fn window_middle(
1546 map: &DisplaySnapshot,
1547 point: DisplayPoint,
1548 text_layout_details: &TextLayoutDetails,
1549) -> (DisplayPoint, SelectionGoal) {
1550 if let Some(visible_rows) = text_layout_details.visible_rows {
1551 let first_visible_line = text_layout_details
1552 .scroll_anchor
1553 .anchor
1554 .to_display_point(map);
1555
1556 let max_visible_rows =
1557 (visible_rows as u32).min(map.max_point().row() - first_visible_line.row());
1558
1559 let new_row =
1560 (first_visible_line.row() + (max_visible_rows / 2)).min(map.max_point().row());
1561 let new_col = point.column().min(map.line_len(new_row));
1562 let new_point = DisplayPoint::new(new_row, new_col);
1563 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1564 } else {
1565 (point, SelectionGoal::None)
1566 }
1567}
1568
1569fn window_bottom(
1570 map: &DisplaySnapshot,
1571 point: DisplayPoint,
1572 text_layout_details: &TextLayoutDetails,
1573 mut times: usize,
1574) -> (DisplayPoint, SelectionGoal) {
1575 if let Some(visible_rows) = text_layout_details.visible_rows {
1576 let first_visible_line = text_layout_details
1577 .scroll_anchor
1578 .anchor
1579 .to_display_point(map);
1580 let bottom_row = first_visible_line.row()
1581 + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
1582 if bottom_row < map.max_point().row()
1583 && text_layout_details.vertical_scroll_margin as usize > times
1584 {
1585 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1586 }
1587 let bottom_row_capped = bottom_row.min(map.max_point().row());
1588 let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row() {
1589 first_visible_line.row()
1590 } else {
1591 bottom_row_capped.saturating_sub(times as u32)
1592 };
1593 let new_col = point.column().min(map.line_len(new_row));
1594 let new_point = DisplayPoint::new(new_row, new_col);
1595 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1596 } else {
1597 (point, SelectionGoal::None)
1598 }
1599}
1600
1601#[cfg(test)]
1602mod test {
1603
1604 use crate::test::NeovimBackedTestContext;
1605 use indoc::indoc;
1606
1607 #[gpui::test]
1608 async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
1609 let mut cx = NeovimBackedTestContext::new(cx).await;
1610
1611 let initial_state = indoc! {r"ˇabc
1612 def
1613
1614 paragraph
1615 the second
1616
1617
1618
1619 third and
1620 final"};
1621
1622 // goes down once
1623 cx.set_shared_state(initial_state).await;
1624 cx.simulate_shared_keystrokes(["}"]).await;
1625 cx.assert_shared_state(indoc! {r"abc
1626 def
1627 ˇ
1628 paragraph
1629 the second
1630
1631
1632
1633 third and
1634 final"})
1635 .await;
1636
1637 // goes up once
1638 cx.simulate_shared_keystrokes(["{"]).await;
1639 cx.assert_shared_state(initial_state).await;
1640
1641 // goes down twice
1642 cx.simulate_shared_keystrokes(["2", "}"]).await;
1643 cx.assert_shared_state(indoc! {r"abc
1644 def
1645
1646 paragraph
1647 the second
1648 ˇ
1649
1650
1651 third and
1652 final"})
1653 .await;
1654
1655 // goes down over multiple blanks
1656 cx.simulate_shared_keystrokes(["}"]).await;
1657 cx.assert_shared_state(indoc! {r"abc
1658 def
1659
1660 paragraph
1661 the second
1662
1663
1664
1665 third and
1666 finaˇl"})
1667 .await;
1668
1669 // goes up twice
1670 cx.simulate_shared_keystrokes(["2", "{"]).await;
1671 cx.assert_shared_state(indoc! {r"abc
1672 def
1673 ˇ
1674 paragraph
1675 the second
1676
1677
1678
1679 third and
1680 final"})
1681 .await
1682 }
1683
1684 #[gpui::test]
1685 async fn test_matching(cx: &mut gpui::TestAppContext) {
1686 let mut cx = NeovimBackedTestContext::new(cx).await;
1687
1688 cx.set_shared_state(indoc! {r"func ˇ(a string) {
1689 do(something(with<Types>.and_arrays[0, 2]))
1690 }"})
1691 .await;
1692 cx.simulate_shared_keystrokes(["%"]).await;
1693 cx.assert_shared_state(indoc! {r"func (a stringˇ) {
1694 do(something(with<Types>.and_arrays[0, 2]))
1695 }"})
1696 .await;
1697
1698 // test it works on the last character of the line
1699 cx.set_shared_state(indoc! {r"func (a string) ˇ{
1700 do(something(with<Types>.and_arrays[0, 2]))
1701 }"})
1702 .await;
1703 cx.simulate_shared_keystrokes(["%"]).await;
1704 cx.assert_shared_state(indoc! {r"func (a string) {
1705 do(something(with<Types>.and_arrays[0, 2]))
1706 ˇ}"})
1707 .await;
1708
1709 // test it works on immediate nesting
1710 cx.set_shared_state("ˇ{()}").await;
1711 cx.simulate_shared_keystrokes(["%"]).await;
1712 cx.assert_shared_state("{()ˇ}").await;
1713 cx.simulate_shared_keystrokes(["%"]).await;
1714 cx.assert_shared_state("ˇ{()}").await;
1715
1716 // test it works on immediate nesting inside braces
1717 cx.set_shared_state("{\n ˇ{()}\n}").await;
1718 cx.simulate_shared_keystrokes(["%"]).await;
1719 cx.assert_shared_state("{\n {()ˇ}\n}").await;
1720
1721 // test it jumps to the next paren on a line
1722 cx.set_shared_state("func ˇboop() {\n}").await;
1723 cx.simulate_shared_keystrokes(["%"]).await;
1724 cx.assert_shared_state("func boop(ˇ) {\n}").await;
1725 }
1726
1727 #[gpui::test]
1728 async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
1729 let mut cx = NeovimBackedTestContext::new(cx).await;
1730
1731 // f and F
1732 cx.set_shared_state("ˇone two three four").await;
1733 cx.simulate_shared_keystrokes(["f", "o"]).await;
1734 cx.assert_shared_state("one twˇo three four").await;
1735 cx.simulate_shared_keystrokes([","]).await;
1736 cx.assert_shared_state("ˇone two three four").await;
1737 cx.simulate_shared_keystrokes(["2", ";"]).await;
1738 cx.assert_shared_state("one two three fˇour").await;
1739 cx.simulate_shared_keystrokes(["shift-f", "e"]).await;
1740 cx.assert_shared_state("one two threˇe four").await;
1741 cx.simulate_shared_keystrokes(["2", ";"]).await;
1742 cx.assert_shared_state("onˇe two three four").await;
1743 cx.simulate_shared_keystrokes([","]).await;
1744 cx.assert_shared_state("one two thrˇee four").await;
1745
1746 // t and T
1747 cx.set_shared_state("ˇone two three four").await;
1748 cx.simulate_shared_keystrokes(["t", "o"]).await;
1749 cx.assert_shared_state("one tˇwo three four").await;
1750 cx.simulate_shared_keystrokes([","]).await;
1751 cx.assert_shared_state("oˇne two three four").await;
1752 cx.simulate_shared_keystrokes(["2", ";"]).await;
1753 cx.assert_shared_state("one two three ˇfour").await;
1754 cx.simulate_shared_keystrokes(["shift-t", "e"]).await;
1755 cx.assert_shared_state("one two threeˇ four").await;
1756 cx.simulate_shared_keystrokes(["3", ";"]).await;
1757 cx.assert_shared_state("oneˇ two three four").await;
1758 cx.simulate_shared_keystrokes([","]).await;
1759 cx.assert_shared_state("one two thˇree four").await;
1760 }
1761
1762 #[gpui::test]
1763 async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
1764 let mut cx = NeovimBackedTestContext::new(cx).await;
1765 let initial_state = indoc! {r"something(ˇfoo)"};
1766 cx.set_shared_state(initial_state).await;
1767 cx.simulate_shared_keystrokes(["}"]).await;
1768 cx.assert_shared_state(indoc! {r"something(fooˇ)"}).await;
1769 }
1770
1771 #[gpui::test]
1772 async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
1773 let mut cx = NeovimBackedTestContext::new(cx).await;
1774 cx.set_shared_state("ˇone\n two\nthree").await;
1775 cx.simulate_shared_keystrokes(["enter"]).await;
1776 cx.assert_shared_state("one\n ˇtwo\nthree").await;
1777 }
1778
1779 #[gpui::test]
1780 async fn test_window_top(cx: &mut gpui::TestAppContext) {
1781 let mut cx = NeovimBackedTestContext::new(cx).await;
1782 let initial_state = indoc! {r"abc
1783 def
1784 paragraph
1785 the second
1786 third ˇand
1787 final"};
1788
1789 cx.set_shared_state(initial_state).await;
1790 cx.simulate_shared_keystrokes(["shift-h"]).await;
1791 cx.assert_shared_state(indoc! {r"abˇc
1792 def
1793 paragraph
1794 the second
1795 third and
1796 final"})
1797 .await;
1798
1799 // clip point
1800 cx.set_shared_state(indoc! {r"
1801 1 2 3
1802 4 5 6
1803 7 8 ˇ9
1804 "})
1805 .await;
1806 cx.simulate_shared_keystrokes(["shift-h"]).await;
1807 cx.assert_shared_state(indoc! {r"
1808 1 2 ˇ3
1809 4 5 6
1810 7 8 9
1811 "})
1812 .await;
1813
1814 cx.set_shared_state(indoc! {r"
1815 1 2 3
1816 4 5 6
1817 ˇ7 8 9
1818 "})
1819 .await;
1820 cx.simulate_shared_keystrokes(["shift-h"]).await;
1821 cx.assert_shared_state(indoc! {r"
1822 ˇ1 2 3
1823 4 5 6
1824 7 8 9
1825 "})
1826 .await;
1827
1828 cx.set_shared_state(indoc! {r"
1829 1 2 3
1830 4 5 ˇ6
1831 7 8 9"})
1832 .await;
1833 cx.simulate_shared_keystrokes(["9", "shift-h"]).await;
1834 cx.assert_shared_state(indoc! {r"
1835 1 2 3
1836 4 5 6
1837 7 8 ˇ9"})
1838 .await;
1839 }
1840
1841 #[gpui::test]
1842 async fn test_window_middle(cx: &mut gpui::TestAppContext) {
1843 let mut cx = NeovimBackedTestContext::new(cx).await;
1844 let initial_state = indoc! {r"abˇc
1845 def
1846 paragraph
1847 the second
1848 third and
1849 final"};
1850
1851 cx.set_shared_state(initial_state).await;
1852 cx.simulate_shared_keystrokes(["shift-m"]).await;
1853 cx.assert_shared_state(indoc! {r"abc
1854 def
1855 paˇragraph
1856 the second
1857 third and
1858 final"})
1859 .await;
1860
1861 cx.set_shared_state(indoc! {r"
1862 1 2 3
1863 4 5 6
1864 7 8 ˇ9
1865 "})
1866 .await;
1867 cx.simulate_shared_keystrokes(["shift-m"]).await;
1868 cx.assert_shared_state(indoc! {r"
1869 1 2 3
1870 4 5 ˇ6
1871 7 8 9
1872 "})
1873 .await;
1874 cx.set_shared_state(indoc! {r"
1875 1 2 3
1876 4 5 6
1877 ˇ7 8 9
1878 "})
1879 .await;
1880 cx.simulate_shared_keystrokes(["shift-m"]).await;
1881 cx.assert_shared_state(indoc! {r"
1882 1 2 3
1883 ˇ4 5 6
1884 7 8 9
1885 "})
1886 .await;
1887 cx.set_shared_state(indoc! {r"
1888 ˇ1 2 3
1889 4 5 6
1890 7 8 9
1891 "})
1892 .await;
1893 cx.simulate_shared_keystrokes(["shift-m"]).await;
1894 cx.assert_shared_state(indoc! {r"
1895 1 2 3
1896 ˇ4 5 6
1897 7 8 9
1898 "})
1899 .await;
1900 cx.set_shared_state(indoc! {r"
1901 1 2 3
1902 ˇ4 5 6
1903 7 8 9
1904 "})
1905 .await;
1906 cx.simulate_shared_keystrokes(["shift-m"]).await;
1907 cx.assert_shared_state(indoc! {r"
1908 1 2 3
1909 ˇ4 5 6
1910 7 8 9
1911 "})
1912 .await;
1913 cx.set_shared_state(indoc! {r"
1914 1 2 3
1915 4 5 ˇ6
1916 7 8 9
1917 "})
1918 .await;
1919 cx.simulate_shared_keystrokes(["shift-m"]).await;
1920 cx.assert_shared_state(indoc! {r"
1921 1 2 3
1922 4 5 ˇ6
1923 7 8 9
1924 "})
1925 .await;
1926 }
1927
1928 #[gpui::test]
1929 async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
1930 let mut cx = NeovimBackedTestContext::new(cx).await;
1931 let initial_state = indoc! {r"abc
1932 deˇf
1933 paragraph
1934 the second
1935 third and
1936 final"};
1937
1938 cx.set_shared_state(initial_state).await;
1939 cx.simulate_shared_keystrokes(["shift-l"]).await;
1940 cx.assert_shared_state(indoc! {r"abc
1941 def
1942 paragraph
1943 the second
1944 third and
1945 fiˇnal"})
1946 .await;
1947
1948 cx.set_shared_state(indoc! {r"
1949 1 2 3
1950 4 5 ˇ6
1951 7 8 9
1952 "})
1953 .await;
1954 cx.simulate_shared_keystrokes(["shift-l"]).await;
1955 cx.assert_shared_state(indoc! {r"
1956 1 2 3
1957 4 5 6
1958 7 8 9
1959 ˇ"})
1960 .await;
1961
1962 cx.set_shared_state(indoc! {r"
1963 1 2 3
1964 ˇ4 5 6
1965 7 8 9
1966 "})
1967 .await;
1968 cx.simulate_shared_keystrokes(["shift-l"]).await;
1969 cx.assert_shared_state(indoc! {r"
1970 1 2 3
1971 4 5 6
1972 7 8 9
1973 ˇ"})
1974 .await;
1975
1976 cx.set_shared_state(indoc! {r"
1977 1 2 ˇ3
1978 4 5 6
1979 7 8 9
1980 "})
1981 .await;
1982 cx.simulate_shared_keystrokes(["shift-l"]).await;
1983 cx.assert_shared_state(indoc! {r"
1984 1 2 3
1985 4 5 6
1986 7 8 9
1987 ˇ"})
1988 .await;
1989
1990 cx.set_shared_state(indoc! {r"
1991 ˇ1 2 3
1992 4 5 6
1993 7 8 9
1994 "})
1995 .await;
1996 cx.simulate_shared_keystrokes(["shift-l"]).await;
1997 cx.assert_shared_state(indoc! {r"
1998 1 2 3
1999 4 5 6
2000 7 8 9
2001 ˇ"})
2002 .await;
2003
2004 cx.set_shared_state(indoc! {r"
2005 1 2 3
2006 4 5 ˇ6
2007 7 8 9
2008 "})
2009 .await;
2010 cx.simulate_shared_keystrokes(["9", "shift-l"]).await;
2011 cx.assert_shared_state(indoc! {r"
2012 1 2 ˇ3
2013 4 5 6
2014 7 8 9
2015 "})
2016 .await;
2017 }
2018
2019 #[gpui::test]
2020 async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
2021 let mut cx = NeovimBackedTestContext::new(cx).await;
2022 cx.set_shared_state(indoc! {r"
2023 456 5ˇ67 678
2024 "})
2025 .await;
2026 cx.simulate_shared_keystrokes(["g", "e"]).await;
2027 cx.assert_shared_state(indoc! {r"
2028 45ˇ6 567 678
2029 "})
2030 .await;
2031
2032 // Test times
2033 cx.set_shared_state(indoc! {r"
2034 123 234 345
2035 456 5ˇ67 678
2036 "})
2037 .await;
2038 cx.simulate_shared_keystrokes(["4", "g", "e"]).await;
2039 cx.assert_shared_state(indoc! {r"
2040 12ˇ3 234 345
2041 456 567 678
2042 "})
2043 .await;
2044
2045 // With punctuation
2046 cx.set_shared_state(indoc! {r"
2047 123 234 345
2048 4;5.6 5ˇ67 678
2049 789 890 901
2050 "})
2051 .await;
2052 cx.simulate_shared_keystrokes(["g", "e"]).await;
2053 cx.assert_shared_state(indoc! {r"
2054 123 234 345
2055 4;5.ˇ6 567 678
2056 789 890 901
2057 "})
2058 .await;
2059
2060 // With punctuation and count
2061 cx.set_shared_state(indoc! {r"
2062 123 234 345
2063 4;5.6 5ˇ67 678
2064 789 890 901
2065 "})
2066 .await;
2067 cx.simulate_shared_keystrokes(["5", "g", "e"]).await;
2068 cx.assert_shared_state(indoc! {r"
2069 123 234 345
2070 ˇ4;5.6 567 678
2071 789 890 901
2072 "})
2073 .await;
2074
2075 // newlines
2076 cx.set_shared_state(indoc! {r"
2077 123 234 345
2078
2079 78ˇ9 890 901
2080 "})
2081 .await;
2082 cx.simulate_shared_keystrokes(["g", "e"]).await;
2083 cx.assert_shared_state(indoc! {r"
2084 123 234 345
2085 ˇ
2086 789 890 901
2087 "})
2088 .await;
2089 cx.simulate_shared_keystrokes(["g", "e"]).await;
2090 cx.assert_shared_state(indoc! {r"
2091 123 234 34ˇ5
2092
2093 789 890 901
2094 "})
2095 .await;
2096
2097 // With punctuation
2098 cx.set_shared_state(indoc! {r"
2099 123 234 345
2100 4;5.ˇ6 567 678
2101 789 890 901
2102 "})
2103 .await;
2104 cx.simulate_shared_keystrokes(["g", "shift-e"]).await;
2105 cx.assert_shared_state(indoc! {r"
2106 123 234 34ˇ5
2107 4;5.6 567 678
2108 789 890 901
2109 "})
2110 .await;
2111 }
2112}