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