1use editor::{
2 char_kind,
3 display_map::{DisplaySnapshot, FoldPoint, ToDisplayPoint},
4 movement::{self, find_boundary, find_preceding_boundary, FindRange, TextLayoutDetails},
5 Bias, CharKind, DisplayPoint, ToOffset,
6};
7use gpui::{actions, px, Action, AppContext, WindowContext};
8use language::{Point, Selection, SelectionGoal};
9use serde::Deserialize;
10use workspace::Workspace;
11
12use crate::{
13 normal::normal_motion,
14 state::{Mode, Operator},
15 visual::visual_motion,
16 Vim,
17};
18
19#[derive(Clone, Debug, PartialEq, Eq)]
20pub enum Motion {
21 Left,
22 Backspace,
23 Down { display_lines: bool },
24 Up { display_lines: bool },
25 Right,
26 NextWordStart { ignore_punctuation: bool },
27 NextWordEnd { ignore_punctuation: bool },
28 PreviousWordStart { ignore_punctuation: bool },
29 FirstNonWhitespace { display_lines: bool },
30 CurrentLine,
31 StartOfLine { display_lines: bool },
32 EndOfLine { display_lines: bool },
33 StartOfParagraph,
34 EndOfParagraph,
35 StartOfDocument,
36 EndOfDocument,
37 Matching,
38 FindForward { before: bool, char: char },
39 FindBackward { after: bool, char: char },
40 NextLineStart,
41 StartOfLineDownward,
42 EndOfLineDownward,
43 GoToColumn,
44}
45
46#[derive(Action, Clone, Deserialize, PartialEq)]
47#[serde(rename_all = "camelCase")]
48struct NextWordStart {
49 #[serde(default)]
50 ignore_punctuation: bool,
51}
52
53#[derive(Action, Clone, Deserialize, PartialEq)]
54#[serde(rename_all = "camelCase")]
55struct NextWordEnd {
56 #[serde(default)]
57 ignore_punctuation: bool,
58}
59
60#[derive(Action, Clone, Deserialize, PartialEq)]
61#[serde(rename_all = "camelCase")]
62struct PreviousWordStart {
63 #[serde(default)]
64 ignore_punctuation: bool,
65}
66
67#[derive(Action, Clone, Deserialize, PartialEq)]
68#[serde(rename_all = "camelCase")]
69pub(crate) struct Up {
70 #[serde(default)]
71 pub(crate) display_lines: bool,
72}
73
74#[derive(Action, Clone, Deserialize, PartialEq)]
75#[serde(rename_all = "camelCase")]
76struct Down {
77 #[serde(default)]
78 display_lines: bool,
79}
80
81#[derive(Action, Clone, Deserialize, PartialEq)]
82#[serde(rename_all = "camelCase")]
83struct FirstNonWhitespace {
84 #[serde(default)]
85 display_lines: bool,
86}
87
88#[derive(Action, Clone, Deserialize, PartialEq)]
89#[serde(rename_all = "camelCase")]
90struct EndOfLine {
91 #[serde(default)]
92 display_lines: bool,
93}
94
95#[derive(Action, Clone, Deserialize, PartialEq)]
96#[serde(rename_all = "camelCase")]
97pub struct StartOfLine {
98 #[serde(default)]
99 pub(crate) display_lines: bool,
100}
101
102#[derive(Action, Clone, Deserialize, PartialEq)]
103struct RepeatFind {
104 #[serde(default)]
105 backwards: bool,
106}
107
108actions!(
109 Left,
110 Backspace,
111 Right,
112 CurrentLine,
113 StartOfParagraph,
114 EndOfParagraph,
115 StartOfDocument,
116 EndOfDocument,
117 Matching,
118 NextLineStart,
119 StartOfLineDownward,
120 EndOfLineDownward,
121 GoToColumn,
122);
123
124pub fn init(cx: &mut AppContext) {
125 // todo!()
126 // cx.add_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx));
127 // cx.add_action(|_: &mut Workspace, _: &Backspace, cx: _| motion(Motion::Backspace, cx));
128 // cx.add_action(|_: &mut Workspace, action: &Down, cx: _| {
129 // motion(
130 // Motion::Down {
131 // display_lines: action.display_lines,
132 // },
133 // cx,
134 // )
135 // });
136 // cx.add_action(|_: &mut Workspace, action: &Up, cx: _| {
137 // motion(
138 // Motion::Up {
139 // display_lines: action.display_lines,
140 // },
141 // cx,
142 // )
143 // });
144 // cx.add_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx));
145 // cx.add_action(|_: &mut Workspace, action: &FirstNonWhitespace, cx: _| {
146 // motion(
147 // Motion::FirstNonWhitespace {
148 // display_lines: action.display_lines,
149 // },
150 // cx,
151 // )
152 // });
153 // cx.add_action(|_: &mut Workspace, action: &StartOfLine, cx: _| {
154 // motion(
155 // Motion::StartOfLine {
156 // display_lines: action.display_lines,
157 // },
158 // cx,
159 // )
160 // });
161 // cx.add_action(|_: &mut Workspace, action: &EndOfLine, cx: _| {
162 // motion(
163 // Motion::EndOfLine {
164 // display_lines: action.display_lines,
165 // },
166 // cx,
167 // )
168 // });
169 // cx.add_action(|_: &mut Workspace, _: &CurrentLine, cx: _| motion(Motion::CurrentLine, cx));
170 // cx.add_action(|_: &mut Workspace, _: &StartOfParagraph, cx: _| {
171 // motion(Motion::StartOfParagraph, cx)
172 // });
173 // cx.add_action(|_: &mut Workspace, _: &EndOfParagraph, cx: _| {
174 // motion(Motion::EndOfParagraph, cx)
175 // });
176 // cx.add_action(|_: &mut Workspace, _: &StartOfDocument, cx: _| {
177 // motion(Motion::StartOfDocument, cx)
178 // });
179 // cx.add_action(|_: &mut Workspace, _: &EndOfDocument, cx: _| motion(Motion::EndOfDocument, cx));
180 // cx.add_action(|_: &mut Workspace, _: &Matching, cx: _| motion(Motion::Matching, cx));
181
182 // cx.add_action(
183 // |_: &mut Workspace, &NextWordStart { ignore_punctuation }: &NextWordStart, cx: _| {
184 // motion(Motion::NextWordStart { ignore_punctuation }, cx)
185 // },
186 // );
187 // cx.add_action(
188 // |_: &mut Workspace, &NextWordEnd { ignore_punctuation }: &NextWordEnd, cx: _| {
189 // motion(Motion::NextWordEnd { ignore_punctuation }, cx)
190 // },
191 // );
192 // cx.add_action(
193 // |_: &mut Workspace,
194 // &PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
195 // cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
196 // );
197 // cx.add_action(|_: &mut Workspace, &NextLineStart, cx: _| motion(Motion::NextLineStart, cx));
198 // cx.add_action(|_: &mut Workspace, &StartOfLineDownward, cx: _| {
199 // motion(Motion::StartOfLineDownward, cx)
200 // });
201 // cx.add_action(|_: &mut Workspace, &EndOfLineDownward, cx: _| {
202 // motion(Motion::EndOfLineDownward, cx)
203 // });
204 // cx.add_action(|_: &mut Workspace, &GoToColumn, cx: _| motion(Motion::GoToColumn, cx));
205 // cx.add_action(|_: &mut Workspace, action: &RepeatFind, cx: _| {
206 // repeat_motion(action.backwards, cx)
207 // })
208}
209
210pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
211 if let Some(Operator::FindForward { .. }) | Some(Operator::FindBackward { .. }) =
212 Vim::read(cx).active_operator()
213 {
214 Vim::update(cx, |vim, cx| vim.pop_operator(cx));
215 }
216
217 let count = Vim::update(cx, |vim, cx| vim.take_count(cx));
218 let operator = Vim::read(cx).active_operator();
219 match Vim::read(cx).state().mode {
220 Mode::Normal => normal_motion(motion, operator, count, cx),
221 Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_motion(motion, count, cx),
222 Mode::Insert => {
223 // Shouldn't execute a motion in insert mode. Ignoring
224 }
225 }
226 Vim::update(cx, |vim, cx| vim.clear_operator(cx));
227}
228
229fn repeat_motion(backwards: bool, cx: &mut WindowContext) {
230 let find = match Vim::read(cx).workspace_state.last_find.clone() {
231 Some(Motion::FindForward { before, char }) => {
232 if backwards {
233 Motion::FindBackward {
234 after: before,
235 char,
236 }
237 } else {
238 Motion::FindForward { before, char }
239 }
240 }
241
242 Some(Motion::FindBackward { after, char }) => {
243 if backwards {
244 Motion::FindForward {
245 before: after,
246 char,
247 }
248 } else {
249 Motion::FindBackward { after, char }
250 }
251 }
252 _ => return,
253 };
254
255 motion(find, cx)
256}
257
258// Motion handling is specified here:
259// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
260impl Motion {
261 pub fn linewise(&self) -> bool {
262 use Motion::*;
263 match self {
264 Down { .. }
265 | Up { .. }
266 | StartOfDocument
267 | EndOfDocument
268 | CurrentLine
269 | NextLineStart
270 | StartOfLineDownward
271 | StartOfParagraph
272 | EndOfParagraph => true,
273 EndOfLine { .. }
274 | NextWordEnd { .. }
275 | Matching
276 | FindForward { .. }
277 | Left
278 | Backspace
279 | Right
280 | StartOfLine { .. }
281 | EndOfLineDownward
282 | GoToColumn
283 | NextWordStart { .. }
284 | PreviousWordStart { .. }
285 | FirstNonWhitespace { .. }
286 | FindBackward { .. } => false,
287 }
288 }
289
290 pub fn infallible(&self) -> bool {
291 use Motion::*;
292 match self {
293 StartOfDocument | EndOfDocument | CurrentLine => true,
294 Down { .. }
295 | Up { .. }
296 | EndOfLine { .. }
297 | NextWordEnd { .. }
298 | Matching
299 | FindForward { .. }
300 | Left
301 | Backspace
302 | Right
303 | StartOfLine { .. }
304 | StartOfParagraph
305 | EndOfParagraph
306 | StartOfLineDownward
307 | EndOfLineDownward
308 | GoToColumn
309 | NextWordStart { .. }
310 | PreviousWordStart { .. }
311 | FirstNonWhitespace { .. }
312 | FindBackward { .. }
313 | NextLineStart => false,
314 }
315 }
316
317 pub fn inclusive(&self) -> bool {
318 use Motion::*;
319 match self {
320 Down { .. }
321 | Up { .. }
322 | StartOfDocument
323 | EndOfDocument
324 | CurrentLine
325 | EndOfLine { .. }
326 | EndOfLineDownward
327 | NextWordEnd { .. }
328 | Matching
329 | FindForward { .. }
330 | NextLineStart => true,
331 Left
332 | Backspace
333 | Right
334 | StartOfLine { .. }
335 | StartOfLineDownward
336 | StartOfParagraph
337 | EndOfParagraph
338 | GoToColumn
339 | NextWordStart { .. }
340 | PreviousWordStart { .. }
341 | FirstNonWhitespace { .. }
342 | FindBackward { .. } => false,
343 }
344 }
345
346 pub fn move_point(
347 &self,
348 map: &DisplaySnapshot,
349 point: DisplayPoint,
350 goal: SelectionGoal,
351 maybe_times: Option<usize>,
352 text_layout_details: &TextLayoutDetails,
353 ) -> Option<(DisplayPoint, SelectionGoal)> {
354 let times = maybe_times.unwrap_or(1);
355 use Motion::*;
356 let infallible = self.infallible();
357 let (new_point, goal) = match self {
358 Left => (left(map, point, times), SelectionGoal::None),
359 Backspace => (backspace(map, point, times), SelectionGoal::None),
360 Down {
361 display_lines: false,
362 } => up_down_buffer_rows(map, point, goal, times as isize, &text_layout_details),
363 Down {
364 display_lines: true,
365 } => down_display(map, point, goal, times, &text_layout_details),
366 Up {
367 display_lines: false,
368 } => up_down_buffer_rows(map, point, goal, 0 - times as isize, &text_layout_details),
369 Up {
370 display_lines: true,
371 } => up_display(map, point, goal, times, &text_layout_details),
372 Right => (right(map, point, times), SelectionGoal::None),
373 NextWordStart { ignore_punctuation } => (
374 next_word_start(map, point, *ignore_punctuation, times),
375 SelectionGoal::None,
376 ),
377 NextWordEnd { ignore_punctuation } => (
378 next_word_end(map, point, *ignore_punctuation, times),
379 SelectionGoal::None,
380 ),
381 PreviousWordStart { ignore_punctuation } => (
382 previous_word_start(map, point, *ignore_punctuation, times),
383 SelectionGoal::None,
384 ),
385 FirstNonWhitespace { display_lines } => (
386 first_non_whitespace(map, *display_lines, point),
387 SelectionGoal::None,
388 ),
389 StartOfLine { display_lines } => (
390 start_of_line(map, *display_lines, point),
391 SelectionGoal::None,
392 ),
393 EndOfLine { display_lines } => {
394 (end_of_line(map, *display_lines, point), SelectionGoal::None)
395 }
396 StartOfParagraph => (
397 movement::start_of_paragraph(map, point, times),
398 SelectionGoal::None,
399 ),
400 EndOfParagraph => (
401 map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
402 SelectionGoal::None,
403 ),
404 CurrentLine => (next_line_end(map, point, times), SelectionGoal::None),
405 StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
406 EndOfDocument => (
407 end_of_document(map, point, maybe_times),
408 SelectionGoal::None,
409 ),
410 Matching => (matching(map, point), SelectionGoal::None),
411 FindForward { before, char } => (
412 find_forward(map, point, *before, *char, times),
413 SelectionGoal::None,
414 ),
415 FindBackward { after, char } => (
416 find_backward(map, point, *after, *char, times),
417 SelectionGoal::None,
418 ),
419 NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
420 StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None),
421 EndOfLineDownward => (next_line_end(map, point, times), SelectionGoal::None),
422 GoToColumn => (go_to_column(map, point, times), SelectionGoal::None),
423 };
424
425 (new_point != point || infallible).then_some((new_point, goal))
426 }
427
428 // Expands a selection using self motion for an operator
429 pub fn expand_selection(
430 &self,
431 map: &DisplaySnapshot,
432 selection: &mut Selection<DisplayPoint>,
433 times: Option<usize>,
434 expand_to_surrounding_newline: bool,
435 text_layout_details: &TextLayoutDetails,
436 ) -> bool {
437 if let Some((new_head, goal)) = self.move_point(
438 map,
439 selection.head(),
440 selection.goal,
441 times,
442 &text_layout_details,
443 ) {
444 selection.set_head(new_head, goal);
445
446 if self.linewise() {
447 selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
448
449 if expand_to_surrounding_newline {
450 if selection.end.row() < map.max_point().row() {
451 *selection.end.row_mut() += 1;
452 *selection.end.column_mut() = 0;
453 selection.end = map.clip_point(selection.end, Bias::Right);
454 // Don't reset the end here
455 return true;
456 } else if selection.start.row() > 0 {
457 *selection.start.row_mut() -= 1;
458 *selection.start.column_mut() = map.line_len(selection.start.row());
459 selection.start = map.clip_point(selection.start, Bias::Left);
460 }
461 }
462
463 (_, selection.end) = map.next_line_boundary(selection.end.to_point(map));
464 } else {
465 // Another special case: When using the "w" motion in combination with an
466 // operator and the last word moved over is at the end of a line, the end of
467 // that word becomes the end of the operated text, not the first word in the
468 // next line.
469 if let Motion::NextWordStart {
470 ignore_punctuation: _,
471 } = self
472 {
473 let start_row = selection.start.to_point(&map).row;
474 if selection.end.to_point(&map).row > start_row {
475 selection.end =
476 Point::new(start_row, map.buffer_snapshot.line_len(start_row))
477 .to_display_point(&map)
478 }
479 }
480
481 // If the motion is exclusive and the end of the motion is in column 1, the
482 // end of the motion is moved to the end of the previous line and the motion
483 // becomes inclusive. Example: "}" moves to the first line after a paragraph,
484 // but "d}" will not include that line.
485 let mut inclusive = self.inclusive();
486 if !inclusive
487 && self != &Motion::Backspace
488 && selection.end.row() > selection.start.row()
489 && selection.end.column() == 0
490 {
491 inclusive = true;
492 *selection.end.row_mut() -= 1;
493 *selection.end.column_mut() = 0;
494 selection.end = map.clip_point(
495 map.next_line_boundary(selection.end.to_point(map)).1,
496 Bias::Left,
497 );
498 }
499
500 if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
501 *selection.end.column_mut() += 1;
502 }
503 }
504 true
505 } else {
506 false
507 }
508 }
509}
510
511fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
512 for _ in 0..times {
513 point = movement::saturating_left(map, point);
514 if point.column() == 0 {
515 break;
516 }
517 }
518 point
519}
520
521fn backspace(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
522 for _ in 0..times {
523 point = movement::left(map, point);
524 }
525 point
526}
527
528pub(crate) fn start_of_relative_buffer_row(
529 map: &DisplaySnapshot,
530 point: DisplayPoint,
531 times: isize,
532) -> DisplayPoint {
533 let start = map.display_point_to_fold_point(point, Bias::Left);
534 let target = start.row() as isize + times;
535 let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
536
537 map.clip_point(
538 map.fold_point_to_display_point(
539 map.fold_snapshot
540 .clip_point(FoldPoint::new(new_row, 0), Bias::Right),
541 ),
542 Bias::Right,
543 )
544}
545
546fn up_down_buffer_rows(
547 map: &DisplaySnapshot,
548 point: DisplayPoint,
549 mut goal: SelectionGoal,
550 times: isize,
551 text_layout_details: &TextLayoutDetails,
552) -> (DisplayPoint, SelectionGoal) {
553 let start = map.display_point_to_fold_point(point, Bias::Left);
554 let begin_folded_line = map.fold_point_to_display_point(
555 map.fold_snapshot
556 .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
557 );
558 let select_nth_wrapped_row = point.row() - begin_folded_line.row();
559
560 let (goal_wrap, goal_x) = match goal {
561 SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
562 SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
563 SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
564 _ => {
565 let x = map.x_for_display_point(point, text_layout_details);
566 goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x.0));
567 (select_nth_wrapped_row, x.0)
568 }
569 };
570
571 let target = start.row() as isize + times;
572 let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
573
574 let mut begin_folded_line = map.fold_point_to_display_point(
575 map.fold_snapshot
576 .clip_point(FoldPoint::new(new_row, 0), Bias::Left),
577 );
578
579 let mut i = 0;
580 while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
581 let next_folded_line = DisplayPoint::new(begin_folded_line.row() + 1, 0);
582 if map
583 .display_point_to_fold_point(next_folded_line, Bias::Right)
584 .row()
585 == new_row
586 {
587 i += 1;
588 begin_folded_line = next_folded_line;
589 } else {
590 break;
591 }
592 }
593
594 let new_col = if i == goal_wrap {
595 map.display_column_for_x(begin_folded_line.row(), px(goal_x), text_layout_details)
596 } else {
597 map.line_len(begin_folded_line.row())
598 };
599
600 (
601 map.clip_point(
602 DisplayPoint::new(begin_folded_line.row(), new_col),
603 Bias::Left,
604 ),
605 goal,
606 )
607}
608
609fn down_display(
610 map: &DisplaySnapshot,
611 mut point: DisplayPoint,
612 mut goal: SelectionGoal,
613 times: usize,
614 text_layout_details: &TextLayoutDetails,
615) -> (DisplayPoint, SelectionGoal) {
616 for _ in 0..times {
617 (point, goal) = movement::down(map, point, goal, true, text_layout_details);
618 }
619
620 (point, goal)
621}
622
623fn up_display(
624 map: &DisplaySnapshot,
625 mut point: DisplayPoint,
626 mut goal: SelectionGoal,
627 times: usize,
628 text_layout_details: &TextLayoutDetails,
629) -> (DisplayPoint, SelectionGoal) {
630 for _ in 0..times {
631 (point, goal) = movement::up(map, point, goal, true, &text_layout_details);
632 }
633
634 (point, goal)
635}
636
637pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
638 for _ in 0..times {
639 let new_point = movement::saturating_right(map, point);
640 if point == new_point {
641 break;
642 }
643 point = new_point;
644 }
645 point
646}
647
648pub(crate) fn next_word_start(
649 map: &DisplaySnapshot,
650 mut point: DisplayPoint,
651 ignore_punctuation: bool,
652 times: usize,
653) -> DisplayPoint {
654 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
655 for _ in 0..times {
656 let mut crossed_newline = false;
657 point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
658 let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
659 let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
660 let at_newline = right == '\n';
661
662 let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
663 || at_newline && crossed_newline
664 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
665
666 crossed_newline |= at_newline;
667 found
668 })
669 }
670 point
671}
672
673fn next_word_end(
674 map: &DisplaySnapshot,
675 mut point: DisplayPoint,
676 ignore_punctuation: bool,
677 times: usize,
678) -> DisplayPoint {
679 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
680 for _ in 0..times {
681 if point.column() < map.line_len(point.row()) {
682 *point.column_mut() += 1;
683 } else if point.row() < map.max_buffer_row() {
684 *point.row_mut() += 1;
685 *point.column_mut() = 0;
686 }
687 point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
688 let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
689 let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
690
691 left_kind != right_kind && left_kind != CharKind::Whitespace
692 });
693
694 // find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
695 // we have backtracked already
696 if !map
697 .chars_at(point)
698 .nth(1)
699 .map(|(c, _)| c == '\n')
700 .unwrap_or(true)
701 {
702 *point.column_mut() = point.column().saturating_sub(1);
703 }
704 point = map.clip_point(point, Bias::Left);
705 }
706 point
707}
708
709fn previous_word_start(
710 map: &DisplaySnapshot,
711 mut point: DisplayPoint,
712 ignore_punctuation: bool,
713 times: usize,
714) -> DisplayPoint {
715 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
716 for _ in 0..times {
717 // This works even though find_preceding_boundary is called for every character in the line containing
718 // cursor because the newline is checked only once.
719 point =
720 movement::find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
721 let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
722 let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
723
724 (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
725 });
726 }
727 point
728}
729
730pub(crate) fn first_non_whitespace(
731 map: &DisplaySnapshot,
732 display_lines: bool,
733 from: DisplayPoint,
734) -> DisplayPoint {
735 let mut last_point = start_of_line(map, display_lines, from);
736 let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
737 for (ch, point) in map.chars_at(last_point) {
738 if ch == '\n' {
739 return from;
740 }
741
742 last_point = point;
743
744 if char_kind(&scope, ch) != CharKind::Whitespace {
745 break;
746 }
747 }
748
749 map.clip_point(last_point, Bias::Left)
750}
751
752pub(crate) fn start_of_line(
753 map: &DisplaySnapshot,
754 display_lines: bool,
755 point: DisplayPoint,
756) -> DisplayPoint {
757 if display_lines {
758 map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
759 } else {
760 map.prev_line_boundary(point.to_point(map)).1
761 }
762}
763
764pub(crate) fn end_of_line(
765 map: &DisplaySnapshot,
766 display_lines: bool,
767 point: DisplayPoint,
768) -> DisplayPoint {
769 if display_lines {
770 map.clip_point(
771 DisplayPoint::new(point.row(), map.line_len(point.row())),
772 Bias::Left,
773 )
774 } else {
775 map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
776 }
777}
778
779fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
780 let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
781 *new_point.column_mut() = point.column();
782 map.clip_point(new_point, Bias::Left)
783}
784
785fn end_of_document(
786 map: &DisplaySnapshot,
787 point: DisplayPoint,
788 line: Option<usize>,
789) -> DisplayPoint {
790 let new_row = if let Some(line) = line {
791 (line - 1) as u32
792 } else {
793 map.max_buffer_row()
794 };
795
796 let new_point = Point::new(new_row, point.column());
797 map.clip_point(new_point.to_display_point(map), Bias::Left)
798}
799
800fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
801 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
802 let point = display_point.to_point(map);
803 let offset = point.to_offset(&map.buffer_snapshot);
804
805 // Ensure the range is contained by the current line.
806 let mut line_end = map.next_line_boundary(point).0;
807 if line_end == point {
808 line_end = map.max_point().to_point(map);
809 }
810
811 let line_range = map.prev_line_boundary(point).0..line_end;
812 let visible_line_range =
813 line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
814 let ranges = map
815 .buffer_snapshot
816 .bracket_ranges(visible_line_range.clone());
817 if let Some(ranges) = ranges {
818 let line_range = line_range.start.to_offset(&map.buffer_snapshot)
819 ..line_range.end.to_offset(&map.buffer_snapshot);
820 let mut closest_pair_destination = None;
821 let mut closest_distance = usize::MAX;
822
823 for (open_range, close_range) in ranges {
824 if open_range.start >= offset && line_range.contains(&open_range.start) {
825 let distance = open_range.start - offset;
826 if distance < closest_distance {
827 closest_pair_destination = Some(close_range.start);
828 closest_distance = distance;
829 continue;
830 }
831 }
832
833 if close_range.start >= offset && line_range.contains(&close_range.start) {
834 let distance = close_range.start - offset;
835 if distance < closest_distance {
836 closest_pair_destination = Some(open_range.start);
837 closest_distance = distance;
838 continue;
839 }
840 }
841
842 continue;
843 }
844
845 closest_pair_destination
846 .map(|destination| destination.to_display_point(map))
847 .unwrap_or(display_point)
848 } else {
849 display_point
850 }
851}
852
853fn find_forward(
854 map: &DisplaySnapshot,
855 from: DisplayPoint,
856 before: bool,
857 target: char,
858 times: usize,
859) -> DisplayPoint {
860 let mut to = from;
861 let mut found = false;
862
863 for _ in 0..times {
864 found = false;
865 to = find_boundary(map, to, FindRange::SingleLine, |_, right| {
866 found = right == target;
867 found
868 });
869 }
870
871 if found {
872 if before && to.column() > 0 {
873 *to.column_mut() -= 1;
874 map.clip_point(to, Bias::Left)
875 } else {
876 to
877 }
878 } else {
879 from
880 }
881}
882
883fn find_backward(
884 map: &DisplaySnapshot,
885 from: DisplayPoint,
886 after: bool,
887 target: char,
888 times: usize,
889) -> DisplayPoint {
890 let mut to = from;
891
892 for _ in 0..times {
893 to = find_preceding_boundary(map, to, FindRange::SingleLine, |_, right| right == target);
894 }
895
896 if map.buffer_snapshot.chars_at(to.to_point(map)).next() == Some(target) {
897 if after {
898 *to.column_mut() += 1;
899 map.clip_point(to, Bias::Right)
900 } else {
901 to
902 }
903 } else {
904 from
905 }
906}
907
908fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
909 let correct_line = start_of_relative_buffer_row(map, point, times as isize);
910 first_non_whitespace(map, false, correct_line)
911}
912
913fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
914 let correct_line = start_of_relative_buffer_row(map, point, 0);
915 right(map, correct_line, times.saturating_sub(1))
916}
917
918pub(crate) fn next_line_end(
919 map: &DisplaySnapshot,
920 mut point: DisplayPoint,
921 times: usize,
922) -> DisplayPoint {
923 if times > 1 {
924 point = start_of_relative_buffer_row(map, point, times as isize - 1);
925 }
926 end_of_line(map, false, point)
927}
928
929// #[cfg(test)]
930// mod test {
931
932// use crate::test::NeovimBackedTestContext;
933// use indoc::indoc;
934
935// #[gpui::test]
936// async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
937// let mut cx = NeovimBackedTestContext::new(cx).await;
938
939// let initial_state = indoc! {r"ˇabc
940// def
941
942// paragraph
943// the second
944
945// third and
946// final"};
947
948// // goes down once
949// cx.set_shared_state(initial_state).await;
950// cx.simulate_shared_keystrokes(["}"]).await;
951// cx.assert_shared_state(indoc! {r"abc
952// def
953// ˇ
954// paragraph
955// the second
956
957// third and
958// final"})
959// .await;
960
961// // goes up once
962// cx.simulate_shared_keystrokes(["{"]).await;
963// cx.assert_shared_state(initial_state).await;
964
965// // goes down twice
966// cx.simulate_shared_keystrokes(["2", "}"]).await;
967// cx.assert_shared_state(indoc! {r"abc
968// def
969
970// paragraph
971// the second
972// ˇ
973
974// third and
975// final"})
976// .await;
977
978// // goes down over multiple blanks
979// cx.simulate_shared_keystrokes(["}"]).await;
980// cx.assert_shared_state(indoc! {r"abc
981// def
982
983// paragraph
984// the second
985
986// third and
987// finaˇl"})
988// .await;
989
990// // goes up twice
991// cx.simulate_shared_keystrokes(["2", "{"]).await;
992// cx.assert_shared_state(indoc! {r"abc
993// def
994// ˇ
995// paragraph
996// the second
997
998// third and
999// final"})
1000// .await
1001// }
1002
1003// #[gpui::test]
1004// async fn test_matching(cx: &mut gpui::TestAppContext) {
1005// let mut cx = NeovimBackedTestContext::new(cx).await;
1006
1007// cx.set_shared_state(indoc! {r"func ˇ(a string) {
1008// do(something(with<Types>.and_arrays[0, 2]))
1009// }"})
1010// .await;
1011// cx.simulate_shared_keystrokes(["%"]).await;
1012// cx.assert_shared_state(indoc! {r"func (a stringˇ) {
1013// do(something(with<Types>.and_arrays[0, 2]))
1014// }"})
1015// .await;
1016
1017// // test it works on the last character of the line
1018// cx.set_shared_state(indoc! {r"func (a string) ˇ{
1019// do(something(with<Types>.and_arrays[0, 2]))
1020// }"})
1021// .await;
1022// cx.simulate_shared_keystrokes(["%"]).await;
1023// cx.assert_shared_state(indoc! {r"func (a string) {
1024// do(something(with<Types>.and_arrays[0, 2]))
1025// ˇ}"})
1026// .await;
1027
1028// // test it works on immediate nesting
1029// cx.set_shared_state("ˇ{()}").await;
1030// cx.simulate_shared_keystrokes(["%"]).await;
1031// cx.assert_shared_state("{()ˇ}").await;
1032// cx.simulate_shared_keystrokes(["%"]).await;
1033// cx.assert_shared_state("ˇ{()}").await;
1034
1035// // test it works on immediate nesting inside braces
1036// cx.set_shared_state("{\n ˇ{()}\n}").await;
1037// cx.simulate_shared_keystrokes(["%"]).await;
1038// cx.assert_shared_state("{\n {()ˇ}\n}").await;
1039
1040// // test it jumps to the next paren on a line
1041// cx.set_shared_state("func ˇboop() {\n}").await;
1042// cx.simulate_shared_keystrokes(["%"]).await;
1043// cx.assert_shared_state("func boop(ˇ) {\n}").await;
1044// }
1045
1046// #[gpui::test]
1047// async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
1048// let mut cx = NeovimBackedTestContext::new(cx).await;
1049
1050// cx.set_shared_state("ˇone two three four").await;
1051// cx.simulate_shared_keystrokes(["f", "o"]).await;
1052// cx.assert_shared_state("one twˇo three four").await;
1053// cx.simulate_shared_keystrokes([","]).await;
1054// cx.assert_shared_state("ˇone two three four").await;
1055// cx.simulate_shared_keystrokes(["2", ";"]).await;
1056// cx.assert_shared_state("one two three fˇour").await;
1057// cx.simulate_shared_keystrokes(["shift-t", "e"]).await;
1058// cx.assert_shared_state("one two threeˇ four").await;
1059// cx.simulate_shared_keystrokes(["3", ";"]).await;
1060// cx.assert_shared_state("oneˇ two three four").await;
1061// cx.simulate_shared_keystrokes([","]).await;
1062// cx.assert_shared_state("one two thˇree four").await;
1063// }
1064
1065// #[gpui::test]
1066// async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
1067// let mut cx = NeovimBackedTestContext::new(cx).await;
1068// cx.set_shared_state("ˇone\n two\nthree").await;
1069// cx.simulate_shared_keystrokes(["enter"]).await;
1070// cx.assert_shared_state("one\n ˇtwo\nthree").await;
1071// }
1072// }