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