1use editor::{
2 display_map::{DisplaySnapshot, FoldPoint, ToDisplayPoint},
3 movement::{self, find_boundary, find_preceding_boundary, FindRange, TextLayoutDetails},
4 Bias, DisplayPoint, ToOffset,
5};
6use gpui::{actions, impl_actions, px, ViewContext, WindowContext};
7use language::{char_kind, CharKind, Point, Selection, SelectionGoal};
8use serde::Deserialize;
9use workspace::Workspace;
10
11use crate::{
12 normal::normal_motion,
13 state::{Mode, Operator},
14 utils::coerce_punctuation,
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 Space,
27 NextWordStart { ignore_punctuation: bool },
28 NextWordEnd { ignore_punctuation: bool },
29 PreviousWordStart { ignore_punctuation: bool },
30 FirstNonWhitespace { display_lines: bool },
31 CurrentLine,
32 StartOfLine { display_lines: bool },
33 EndOfLine { display_lines: bool },
34 StartOfParagraph,
35 EndOfParagraph,
36 StartOfDocument,
37 EndOfDocument,
38 Matching,
39 FindForward { before: bool, char: char },
40 FindBackward { after: bool, char: char },
41 RepeatFind { last_find: Box<Motion> },
42 RepeatFindReversed { last_find: Box<Motion> },
43 NextLineStart,
44 StartOfLineDownward,
45 EndOfLineDownward,
46 GoToColumn,
47 WindowTop,
48 WindowMiddle,
49 WindowBottom,
50}
51
52#[derive(Clone, Deserialize, PartialEq)]
53#[serde(rename_all = "camelCase")]
54struct NextWordStart {
55 #[serde(default)]
56 ignore_punctuation: bool,
57}
58
59#[derive(Clone, Deserialize, PartialEq)]
60#[serde(rename_all = "camelCase")]
61struct NextWordEnd {
62 #[serde(default)]
63 ignore_punctuation: bool,
64}
65
66#[derive(Clone, Deserialize, PartialEq)]
67#[serde(rename_all = "camelCase")]
68struct PreviousWordStart {
69 #[serde(default)]
70 ignore_punctuation: bool,
71}
72
73#[derive(Clone, Deserialize, PartialEq)]
74#[serde(rename_all = "camelCase")]
75pub(crate) struct Up {
76 #[serde(default)]
77 pub(crate) display_lines: bool,
78}
79
80#[derive(Clone, Deserialize, PartialEq)]
81#[serde(rename_all = "camelCase")]
82pub(crate) struct Down {
83 #[serde(default)]
84 pub(crate) display_lines: bool,
85}
86
87#[derive(Clone, Deserialize, PartialEq)]
88#[serde(rename_all = "camelCase")]
89struct FirstNonWhitespace {
90 #[serde(default)]
91 display_lines: bool,
92}
93
94#[derive(Clone, Deserialize, PartialEq)]
95#[serde(rename_all = "camelCase")]
96struct EndOfLine {
97 #[serde(default)]
98 display_lines: bool,
99}
100
101#[derive(Clone, Deserialize, PartialEq)]
102#[serde(rename_all = "camelCase")]
103pub struct StartOfLine {
104 #[serde(default)]
105 pub(crate) display_lines: bool,
106}
107
108impl_actions!(
109 vim,
110 [
111 StartOfLine,
112 EndOfLine,
113 FirstNonWhitespace,
114 Down,
115 Up,
116 PreviousWordStart,
117 NextWordEnd,
118 NextWordStart
119 ]
120);
121
122actions!(
123 vim,
124 [
125 Left,
126 Backspace,
127 Right,
128 Space,
129 CurrentLine,
130 StartOfParagraph,
131 EndOfParagraph,
132 StartOfDocument,
133 EndOfDocument,
134 Matching,
135 NextLineStart,
136 StartOfLineDownward,
137 EndOfLineDownward,
138 GoToColumn,
139 RepeatFind,
140 RepeatFindReversed,
141 WindowTop,
142 WindowMiddle,
143 WindowBottom,
144 ]
145);
146
147pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
148 workspace.register_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx));
149 workspace
150 .register_action(|_: &mut Workspace, _: &Backspace, cx: _| motion(Motion::Backspace, cx));
151 workspace.register_action(|_: &mut Workspace, action: &Down, cx: _| {
152 motion(
153 Motion::Down {
154 display_lines: action.display_lines,
155 },
156 cx,
157 )
158 });
159 workspace.register_action(|_: &mut Workspace, action: &Up, cx: _| {
160 motion(
161 Motion::Up {
162 display_lines: action.display_lines,
163 },
164 cx,
165 )
166 });
167 workspace.register_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx));
168 workspace.register_action(|_: &mut Workspace, _: &Space, cx: _| motion(Motion::Space, cx));
169 workspace.register_action(|_: &mut Workspace, action: &FirstNonWhitespace, cx: _| {
170 motion(
171 Motion::FirstNonWhitespace {
172 display_lines: action.display_lines,
173 },
174 cx,
175 )
176 });
177 workspace.register_action(|_: &mut Workspace, action: &StartOfLine, cx: _| {
178 motion(
179 Motion::StartOfLine {
180 display_lines: action.display_lines,
181 },
182 cx,
183 )
184 });
185 workspace.register_action(|_: &mut Workspace, action: &EndOfLine, cx: _| {
186 motion(
187 Motion::EndOfLine {
188 display_lines: action.display_lines,
189 },
190 cx,
191 )
192 });
193 workspace.register_action(|_: &mut Workspace, _: &CurrentLine, cx: _| {
194 motion(Motion::CurrentLine, cx)
195 });
196 workspace.register_action(|_: &mut Workspace, _: &StartOfParagraph, cx: _| {
197 motion(Motion::StartOfParagraph, cx)
198 });
199 workspace.register_action(|_: &mut Workspace, _: &EndOfParagraph, cx: _| {
200 motion(Motion::EndOfParagraph, cx)
201 });
202 workspace.register_action(|_: &mut Workspace, _: &StartOfDocument, cx: _| {
203 motion(Motion::StartOfDocument, cx)
204 });
205 workspace.register_action(|_: &mut Workspace, _: &EndOfDocument, cx: _| {
206 motion(Motion::EndOfDocument, cx)
207 });
208 workspace
209 .register_action(|_: &mut Workspace, _: &Matching, cx: _| motion(Motion::Matching, cx));
210
211 workspace.register_action(
212 |_: &mut Workspace, &NextWordStart { ignore_punctuation }: &NextWordStart, cx: _| {
213 motion(Motion::NextWordStart { ignore_punctuation }, cx)
214 },
215 );
216 workspace.register_action(
217 |_: &mut Workspace, &NextWordEnd { ignore_punctuation }: &NextWordEnd, cx: _| {
218 motion(Motion::NextWordEnd { ignore_punctuation }, cx)
219 },
220 );
221 workspace.register_action(
222 |_: &mut Workspace,
223 &PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
224 cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
225 );
226 workspace.register_action(|_: &mut Workspace, &NextLineStart, cx: _| {
227 motion(Motion::NextLineStart, cx)
228 });
229 workspace.register_action(|_: &mut Workspace, &StartOfLineDownward, cx: _| {
230 motion(Motion::StartOfLineDownward, cx)
231 });
232 workspace.register_action(|_: &mut Workspace, &EndOfLineDownward, cx: _| {
233 motion(Motion::EndOfLineDownward, cx)
234 });
235 workspace
236 .register_action(|_: &mut Workspace, &GoToColumn, cx: _| motion(Motion::GoToColumn, cx));
237
238 workspace.register_action(|_: &mut Workspace, _: &RepeatFind, cx: _| {
239 if let Some(last_find) = Vim::read(cx)
240 .workspace_state
241 .last_find
242 .clone()
243 .map(Box::new)
244 {
245 motion(Motion::RepeatFind { last_find }, cx);
246 }
247 });
248
249 workspace.register_action(|_: &mut Workspace, _: &RepeatFindReversed, cx: _| {
250 if let Some(last_find) = Vim::read(cx)
251 .workspace_state
252 .last_find
253 .clone()
254 .map(Box::new)
255 {
256 motion(Motion::RepeatFindReversed { last_find }, cx);
257 }
258 });
259 workspace.register_action(|_: &mut Workspace, &WindowTop, cx: _| motion(Motion::WindowTop, cx));
260 workspace.register_action(|_: &mut Workspace, &WindowMiddle, cx: _| {
261 motion(Motion::WindowMiddle, cx)
262 });
263 workspace.register_action(|_: &mut Workspace, &WindowBottom, cx: _| {
264 motion(Motion::WindowBottom, cx)
265 });
266}
267
268pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
269 if let Some(Operator::FindForward { .. }) | Some(Operator::FindBackward { .. }) =
270 Vim::read(cx).active_operator()
271 {
272 Vim::update(cx, |vim, cx| vim.pop_operator(cx));
273 }
274
275 let count = Vim::update(cx, |vim, cx| vim.take_count(cx));
276 let operator = Vim::read(cx).active_operator();
277 match Vim::read(cx).state().mode {
278 Mode::Normal => normal_motion(motion, operator, count, cx),
279 Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_motion(motion, count, cx),
280 Mode::Insert => {
281 // Shouldn't execute a motion in insert mode. Ignoring
282 }
283 }
284 Vim::update(cx, |vim, cx| vim.clear_operator(cx));
285}
286
287// Motion handling is specified here:
288// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
289impl Motion {
290 pub fn linewise(&self) -> bool {
291 use Motion::*;
292 match self {
293 Down { .. }
294 | Up { .. }
295 | StartOfDocument
296 | EndOfDocument
297 | CurrentLine
298 | NextLineStart
299 | StartOfLineDownward
300 | StartOfParagraph
301 | WindowTop
302 | WindowMiddle
303 | WindowBottom
304 | EndOfParagraph => true,
305 EndOfLine { .. }
306 | NextWordEnd { .. }
307 | Matching
308 | FindForward { .. }
309 | Left
310 | Backspace
311 | Right
312 | Space
313 | StartOfLine { .. }
314 | EndOfLineDownward
315 | GoToColumn
316 | NextWordStart { .. }
317 | PreviousWordStart { .. }
318 | FirstNonWhitespace { .. }
319 | FindBackward { .. }
320 | RepeatFind { .. }
321 | RepeatFindReversed { .. } => false,
322 }
323 }
324
325 pub fn infallible(&self) -> bool {
326 use Motion::*;
327 match self {
328 StartOfDocument | EndOfDocument | CurrentLine => true,
329 Down { .. }
330 | Up { .. }
331 | EndOfLine { .. }
332 | NextWordEnd { .. }
333 | Matching
334 | FindForward { .. }
335 | RepeatFind { .. }
336 | Left
337 | Backspace
338 | Right
339 | Space
340 | StartOfLine { .. }
341 | StartOfParagraph
342 | EndOfParagraph
343 | StartOfLineDownward
344 | EndOfLineDownward
345 | GoToColumn
346 | NextWordStart { .. }
347 | PreviousWordStart { .. }
348 | FirstNonWhitespace { .. }
349 | FindBackward { .. }
350 | RepeatFindReversed { .. }
351 | WindowTop
352 | WindowMiddle
353 | WindowBottom
354 | NextLineStart => false,
355 }
356 }
357
358 pub fn inclusive(&self) -> bool {
359 use Motion::*;
360 match self {
361 Down { .. }
362 | Up { .. }
363 | StartOfDocument
364 | EndOfDocument
365 | CurrentLine
366 | EndOfLine { .. }
367 | EndOfLineDownward
368 | NextWordEnd { .. }
369 | Matching
370 | FindForward { .. }
371 | WindowTop
372 | WindowMiddle
373 | WindowBottom
374 | NextLineStart => true,
375 Left
376 | Backspace
377 | Right
378 | Space
379 | StartOfLine { .. }
380 | StartOfLineDownward
381 | StartOfParagraph
382 | EndOfParagraph
383 | GoToColumn
384 | NextWordStart { .. }
385 | PreviousWordStart { .. }
386 | FirstNonWhitespace { .. }
387 | FindBackward { .. } => false,
388 RepeatFind { last_find: motion } | RepeatFindReversed { last_find: motion } => {
389 motion.inclusive()
390 }
391 }
392 }
393
394 pub fn move_point(
395 &self,
396 map: &DisplaySnapshot,
397 point: DisplayPoint,
398 goal: SelectionGoal,
399 maybe_times: Option<usize>,
400 text_layout_details: &TextLayoutDetails,
401 ) -> Option<(DisplayPoint, SelectionGoal)> {
402 let times = maybe_times.unwrap_or(1);
403 use Motion::*;
404 let infallible = self.infallible();
405 let (new_point, goal) = match self {
406 Left => (left(map, point, times), SelectionGoal::None),
407 Backspace => (backspace(map, point, times), SelectionGoal::None),
408 Down {
409 display_lines: false,
410 } => up_down_buffer_rows(map, point, goal, times as isize, &text_layout_details),
411 Down {
412 display_lines: true,
413 } => down_display(map, point, goal, times, &text_layout_details),
414 Up {
415 display_lines: false,
416 } => up_down_buffer_rows(map, point, goal, 0 - times as isize, &text_layout_details),
417 Up {
418 display_lines: true,
419 } => up_display(map, point, goal, times, &text_layout_details),
420 Right => (right(map, point, times), SelectionGoal::None),
421 Space => (space(map, point, times), SelectionGoal::None),
422 NextWordStart { ignore_punctuation } => (
423 next_word_start(map, point, *ignore_punctuation, times),
424 SelectionGoal::None,
425 ),
426 NextWordEnd { ignore_punctuation } => (
427 next_word_end(map, point, *ignore_punctuation, times),
428 SelectionGoal::None,
429 ),
430 PreviousWordStart { ignore_punctuation } => (
431 previous_word_start(map, point, *ignore_punctuation, times),
432 SelectionGoal::None,
433 ),
434 FirstNonWhitespace { display_lines } => (
435 first_non_whitespace(map, *display_lines, point),
436 SelectionGoal::None,
437 ),
438 StartOfLine { display_lines } => (
439 start_of_line(map, *display_lines, point),
440 SelectionGoal::None,
441 ),
442 EndOfLine { display_lines } => {
443 (end_of_line(map, *display_lines, point), SelectionGoal::None)
444 }
445 StartOfParagraph => (
446 movement::start_of_paragraph(map, point, times),
447 SelectionGoal::None,
448 ),
449 EndOfParagraph => (
450 map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
451 SelectionGoal::None,
452 ),
453 CurrentLine => (next_line_end(map, point, times), SelectionGoal::None),
454 StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
455 EndOfDocument => (
456 end_of_document(map, point, maybe_times),
457 SelectionGoal::None,
458 ),
459 Matching => (matching(map, point), SelectionGoal::None),
460 // t f
461 FindForward { before, char } => {
462 return find_forward(map, point, *before, *char, times)
463 .map(|new_point| (new_point, SelectionGoal::None))
464 }
465 // T F
466 FindBackward { after, char } => (
467 find_backward(map, point, *after, *char, times),
468 SelectionGoal::None,
469 ),
470 // ; -- repeat the last find done with t, f, T, F
471 RepeatFind { last_find } => match **last_find {
472 Motion::FindForward { before, char } => {
473 let mut new_point = find_forward(map, point, before, char, times);
474 if new_point == Some(point) {
475 new_point = find_forward(map, point, before, char, times + 1);
476 }
477
478 return new_point.map(|new_point| (new_point, SelectionGoal::None));
479 }
480
481 Motion::FindBackward { after, char } => {
482 let mut new_point = find_backward(map, point, after, char, times);
483 if new_point == point {
484 new_point = find_backward(map, point, after, char, times + 1);
485 }
486
487 (new_point, SelectionGoal::None)
488 }
489 _ => return None,
490 },
491 // , -- repeat the last find done with t, f, T, F, in opposite direction
492 RepeatFindReversed { last_find } => match **last_find {
493 Motion::FindForward { before, char } => {
494 let mut new_point = find_backward(map, point, before, char, times);
495 if new_point == point {
496 new_point = find_backward(map, point, before, char, times + 1);
497 }
498
499 (new_point, SelectionGoal::None)
500 }
501
502 Motion::FindBackward { after, char } => {
503 let mut new_point = find_forward(map, point, after, char, times);
504 if new_point == Some(point) {
505 new_point = find_forward(map, point, after, char, times + 1);
506 }
507
508 return new_point.map(|new_point| (new_point, SelectionGoal::None));
509 }
510 _ => return None,
511 },
512 NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
513 StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None),
514 EndOfLineDownward => (next_line_end(map, point, times), SelectionGoal::None),
515 GoToColumn => (go_to_column(map, point, times), SelectionGoal::None),
516 WindowTop => window_top(map, point, &text_layout_details),
517 WindowMiddle => window_middle(map, point, &text_layout_details),
518 WindowBottom => window_bottom(map, point, &text_layout_details),
519 };
520
521 (new_point != point || infallible).then_some((new_point, goal))
522 }
523
524 // Expands a selection using self motion for an operator
525 pub fn expand_selection(
526 &self,
527 map: &DisplaySnapshot,
528 selection: &mut Selection<DisplayPoint>,
529 times: Option<usize>,
530 expand_to_surrounding_newline: bool,
531 text_layout_details: &TextLayoutDetails,
532 ) -> bool {
533 if let Some((new_head, goal)) = self.move_point(
534 map,
535 selection.head(),
536 selection.goal,
537 times,
538 &text_layout_details,
539 ) {
540 selection.set_head(new_head, goal);
541
542 if self.linewise() {
543 selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
544
545 if expand_to_surrounding_newline {
546 if selection.end.row() < map.max_point().row() {
547 *selection.end.row_mut() += 1;
548 *selection.end.column_mut() = 0;
549 selection.end = map.clip_point(selection.end, Bias::Right);
550 // Don't reset the end here
551 return true;
552 } else if selection.start.row() > 0 {
553 *selection.start.row_mut() -= 1;
554 *selection.start.column_mut() = map.line_len(selection.start.row());
555 selection.start = map.clip_point(selection.start, Bias::Left);
556 }
557 }
558
559 (_, selection.end) = map.next_line_boundary(selection.end.to_point(map));
560 } else {
561 // Another special case: When using the "w" motion in combination with an
562 // operator and the last word moved over is at the end of a line, the end of
563 // that word becomes the end of the operated text, not the first word in the
564 // next line.
565 if let Motion::NextWordStart {
566 ignore_punctuation: _,
567 } = self
568 {
569 let start_row = selection.start.to_point(&map).row;
570 if selection.end.to_point(&map).row > start_row {
571 selection.end =
572 Point::new(start_row, map.buffer_snapshot.line_len(start_row))
573 .to_display_point(&map)
574 }
575 }
576
577 // If the motion is exclusive and the end of the motion is in column 1, the
578 // end of the motion is moved to the end of the previous line and the motion
579 // becomes inclusive. Example: "}" moves to the first line after a paragraph,
580 // but "d}" will not include that line.
581 let mut inclusive = self.inclusive();
582 if !inclusive
583 && self != &Motion::Backspace
584 && selection.end.row() > selection.start.row()
585 && selection.end.column() == 0
586 {
587 inclusive = true;
588 *selection.end.row_mut() -= 1;
589 *selection.end.column_mut() = 0;
590 selection.end = map.clip_point(
591 map.next_line_boundary(selection.end.to_point(map)).1,
592 Bias::Left,
593 );
594 }
595
596 if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
597 *selection.end.column_mut() += 1;
598 }
599 }
600 true
601 } else {
602 false
603 }
604 }
605}
606
607fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
608 for _ in 0..times {
609 point = movement::saturating_left(map, point);
610 if point.column() == 0 {
611 break;
612 }
613 }
614 point
615}
616
617fn backspace(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
618 for _ in 0..times {
619 point = movement::left(map, point);
620 }
621 point
622}
623
624fn space(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
625 for _ in 0..times {
626 point = wrapping_right(map, point);
627 }
628 point
629}
630
631fn wrapping_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
632 let max_column = map.line_len(point.row()).saturating_sub(1);
633 if point.column() < max_column {
634 *point.column_mut() += 1;
635 } else if point.row() < map.max_point().row() {
636 *point.row_mut() += 1;
637 *point.column_mut() = 0;
638 }
639 point
640}
641
642pub(crate) fn start_of_relative_buffer_row(
643 map: &DisplaySnapshot,
644 point: DisplayPoint,
645 times: isize,
646) -> DisplayPoint {
647 let start = map.display_point_to_fold_point(point, Bias::Left);
648 let target = start.row() as isize + times;
649 let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
650
651 map.clip_point(
652 map.fold_point_to_display_point(
653 map.fold_snapshot
654 .clip_point(FoldPoint::new(new_row, 0), Bias::Right),
655 ),
656 Bias::Right,
657 )
658}
659
660fn up_down_buffer_rows(
661 map: &DisplaySnapshot,
662 point: DisplayPoint,
663 mut goal: SelectionGoal,
664 times: isize,
665 text_layout_details: &TextLayoutDetails,
666) -> (DisplayPoint, SelectionGoal) {
667 let start = map.display_point_to_fold_point(point, Bias::Left);
668 let begin_folded_line = map.fold_point_to_display_point(
669 map.fold_snapshot
670 .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
671 );
672 let select_nth_wrapped_row = point.row() - begin_folded_line.row();
673
674 let (goal_wrap, goal_x) = match goal {
675 SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
676 SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
677 SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
678 _ => {
679 let x = map.x_for_display_point(point, text_layout_details);
680 goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x.0));
681 (select_nth_wrapped_row, x.0)
682 }
683 };
684
685 let target = start.row() as isize + times;
686 let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
687
688 let mut begin_folded_line = map.fold_point_to_display_point(
689 map.fold_snapshot
690 .clip_point(FoldPoint::new(new_row, 0), Bias::Left),
691 );
692
693 let mut i = 0;
694 while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
695 let next_folded_line = DisplayPoint::new(begin_folded_line.row() + 1, 0);
696 if map
697 .display_point_to_fold_point(next_folded_line, Bias::Right)
698 .row()
699 == new_row
700 {
701 i += 1;
702 begin_folded_line = next_folded_line;
703 } else {
704 break;
705 }
706 }
707
708 let new_col = if i == goal_wrap {
709 map.display_column_for_x(begin_folded_line.row(), px(goal_x), text_layout_details)
710 } else {
711 map.line_len(begin_folded_line.row())
712 };
713
714 (
715 map.clip_point(
716 DisplayPoint::new(begin_folded_line.row(), new_col),
717 Bias::Left,
718 ),
719 goal,
720 )
721}
722
723fn down_display(
724 map: &DisplaySnapshot,
725 mut point: DisplayPoint,
726 mut goal: SelectionGoal,
727 times: usize,
728 text_layout_details: &TextLayoutDetails,
729) -> (DisplayPoint, SelectionGoal) {
730 for _ in 0..times {
731 (point, goal) = movement::down(map, point, goal, true, text_layout_details);
732 }
733
734 (point, goal)
735}
736
737fn up_display(
738 map: &DisplaySnapshot,
739 mut point: DisplayPoint,
740 mut goal: SelectionGoal,
741 times: usize,
742 text_layout_details: &TextLayoutDetails,
743) -> (DisplayPoint, SelectionGoal) {
744 for _ in 0..times {
745 (point, goal) = movement::up(map, point, goal, true, &text_layout_details);
746 }
747
748 (point, goal)
749}
750
751pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
752 for _ in 0..times {
753 let new_point = movement::saturating_right(map, point);
754 if point == new_point {
755 break;
756 }
757 point = new_point;
758 }
759 point
760}
761
762pub(crate) fn next_word_start(
763 map: &DisplaySnapshot,
764 mut point: DisplayPoint,
765 ignore_punctuation: bool,
766 times: usize,
767) -> DisplayPoint {
768 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
769 for _ in 0..times {
770 let mut crossed_newline = false;
771 point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
772 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
773 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
774 let at_newline = right == '\n';
775
776 let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
777 || at_newline && crossed_newline
778 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
779
780 crossed_newline |= at_newline;
781 found
782 })
783 }
784 point
785}
786
787fn next_word_end(
788 map: &DisplaySnapshot,
789 mut point: DisplayPoint,
790 ignore_punctuation: bool,
791 times: usize,
792) -> DisplayPoint {
793 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
794 for _ in 0..times {
795 if point.column() < map.line_len(point.row()) {
796 *point.column_mut() += 1;
797 } else if point.row() < map.max_buffer_row() {
798 *point.row_mut() += 1;
799 *point.column_mut() = 0;
800 }
801 point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
802 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
803 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
804
805 left_kind != right_kind && left_kind != CharKind::Whitespace
806 });
807
808 // find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
809 // we have backtracked already
810 if !map
811 .chars_at(point)
812 .nth(1)
813 .map(|(c, _)| c == '\n')
814 .unwrap_or(true)
815 {
816 *point.column_mut() = point.column().saturating_sub(1);
817 }
818 point = map.clip_point(point, Bias::Left);
819 }
820 point
821}
822
823fn previous_word_start(
824 map: &DisplaySnapshot,
825 mut point: DisplayPoint,
826 ignore_punctuation: bool,
827 times: usize,
828) -> DisplayPoint {
829 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
830 for _ in 0..times {
831 // This works even though find_preceding_boundary is called for every character in the line containing
832 // cursor because the newline is checked only once.
833 point =
834 movement::find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
835 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
836 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
837
838 (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
839 });
840 }
841 point
842}
843
844pub(crate) fn first_non_whitespace(
845 map: &DisplaySnapshot,
846 display_lines: bool,
847 from: DisplayPoint,
848) -> DisplayPoint {
849 let mut last_point = start_of_line(map, display_lines, from);
850 let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
851 for (ch, point) in map.chars_at(last_point) {
852 if ch == '\n' {
853 return from;
854 }
855
856 last_point = point;
857
858 if char_kind(&scope, ch) != CharKind::Whitespace {
859 break;
860 }
861 }
862
863 map.clip_point(last_point, Bias::Left)
864}
865
866pub(crate) fn start_of_line(
867 map: &DisplaySnapshot,
868 display_lines: bool,
869 point: DisplayPoint,
870) -> DisplayPoint {
871 if display_lines {
872 map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
873 } else {
874 map.prev_line_boundary(point.to_point(map)).1
875 }
876}
877
878pub(crate) fn end_of_line(
879 map: &DisplaySnapshot,
880 display_lines: bool,
881 point: DisplayPoint,
882) -> DisplayPoint {
883 if display_lines {
884 map.clip_point(
885 DisplayPoint::new(point.row(), map.line_len(point.row())),
886 Bias::Left,
887 )
888 } else {
889 map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
890 }
891}
892
893fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
894 let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
895 *new_point.column_mut() = point.column();
896 map.clip_point(new_point, Bias::Left)
897}
898
899fn end_of_document(
900 map: &DisplaySnapshot,
901 point: DisplayPoint,
902 line: Option<usize>,
903) -> DisplayPoint {
904 let new_row = if let Some(line) = line {
905 (line - 1) as u32
906 } else {
907 map.max_buffer_row()
908 };
909
910 let new_point = Point::new(new_row, point.column());
911 map.clip_point(new_point.to_display_point(map), Bias::Left)
912}
913
914fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
915 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
916 let point = display_point.to_point(map);
917 let offset = point.to_offset(&map.buffer_snapshot);
918
919 // Ensure the range is contained by the current line.
920 let mut line_end = map.next_line_boundary(point).0;
921 if line_end == point {
922 line_end = map.max_point().to_point(map);
923 }
924
925 let line_range = map.prev_line_boundary(point).0..line_end;
926 let visible_line_range =
927 line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
928 let ranges = map
929 .buffer_snapshot
930 .bracket_ranges(visible_line_range.clone());
931 if let Some(ranges) = ranges {
932 let line_range = line_range.start.to_offset(&map.buffer_snapshot)
933 ..line_range.end.to_offset(&map.buffer_snapshot);
934 let mut closest_pair_destination = None;
935 let mut closest_distance = usize::MAX;
936
937 for (open_range, close_range) in ranges {
938 if open_range.start >= offset && line_range.contains(&open_range.start) {
939 let distance = open_range.start - offset;
940 if distance < closest_distance {
941 closest_pair_destination = Some(close_range.start);
942 closest_distance = distance;
943 continue;
944 }
945 }
946
947 if close_range.start >= offset && line_range.contains(&close_range.start) {
948 let distance = close_range.start - offset;
949 if distance < closest_distance {
950 closest_pair_destination = Some(open_range.start);
951 closest_distance = distance;
952 continue;
953 }
954 }
955
956 continue;
957 }
958
959 closest_pair_destination
960 .map(|destination| destination.to_display_point(map))
961 .unwrap_or(display_point)
962 } else {
963 display_point
964 }
965}
966
967fn find_forward(
968 map: &DisplaySnapshot,
969 from: DisplayPoint,
970 before: bool,
971 target: char,
972 times: usize,
973) -> Option<DisplayPoint> {
974 let mut to = from;
975 let mut found = false;
976
977 for _ in 0..times {
978 found = false;
979 to = find_boundary(map, to, FindRange::SingleLine, |_, right| {
980 found = right == target;
981 found
982 });
983 }
984
985 if found {
986 if before && to.column() > 0 {
987 *to.column_mut() -= 1;
988 Some(map.clip_point(to, Bias::Left))
989 } else {
990 Some(to)
991 }
992 } else {
993 None
994 }
995}
996
997fn find_backward(
998 map: &DisplaySnapshot,
999 from: DisplayPoint,
1000 after: bool,
1001 target: char,
1002 times: usize,
1003) -> DisplayPoint {
1004 let mut to = from;
1005
1006 for _ in 0..times {
1007 to = find_preceding_boundary(map, to, FindRange::SingleLine, |_, right| right == target);
1008 }
1009
1010 if map.buffer_snapshot.chars_at(to.to_point(map)).next() == Some(target) {
1011 if after {
1012 *to.column_mut() += 1;
1013 map.clip_point(to, Bias::Right)
1014 } else {
1015 to
1016 }
1017 } else {
1018 from
1019 }
1020}
1021
1022fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1023 let correct_line = start_of_relative_buffer_row(map, point, times as isize);
1024 first_non_whitespace(map, false, correct_line)
1025}
1026
1027fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1028 let correct_line = start_of_relative_buffer_row(map, point, 0);
1029 right(map, correct_line, times.saturating_sub(1))
1030}
1031
1032pub(crate) fn next_line_end(
1033 map: &DisplaySnapshot,
1034 mut point: DisplayPoint,
1035 times: usize,
1036) -> DisplayPoint {
1037 if times > 1 {
1038 point = start_of_relative_buffer_row(map, point, times as isize - 1);
1039 }
1040 end_of_line(map, false, point)
1041}
1042
1043fn window_top(
1044 map: &DisplaySnapshot,
1045 point: DisplayPoint,
1046 text_layout_details: &TextLayoutDetails,
1047) -> (DisplayPoint, SelectionGoal) {
1048 let first_visible_line = text_layout_details.anchor.to_display_point(map);
1049 let new_col = point.column().min(map.line_len(first_visible_line.row()));
1050 let new_point = DisplayPoint::new(first_visible_line.row(), new_col);
1051 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1052}
1053
1054fn window_middle(
1055 map: &DisplaySnapshot,
1056 point: DisplayPoint,
1057 text_layout_details: &TextLayoutDetails,
1058) -> (DisplayPoint, SelectionGoal) {
1059 if let Some(visible_rows) = text_layout_details.visible_rows {
1060 let first_visible_line = text_layout_details.anchor.to_display_point(map);
1061 let max_rows = (visible_rows as u32).min(map.max_buffer_row());
1062 let new_row = first_visible_line.row() + (max_rows.div_euclid(2));
1063 let new_col = point.column().min(map.line_len(new_row));
1064 let new_point = DisplayPoint::new(new_row, new_col);
1065 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1066 } else {
1067 (point, SelectionGoal::None)
1068 }
1069}
1070
1071fn window_bottom(
1072 map: &DisplaySnapshot,
1073 point: DisplayPoint,
1074 text_layout_details: &TextLayoutDetails,
1075) -> (DisplayPoint, SelectionGoal) {
1076 if let Some(visible_rows) = text_layout_details.visible_rows {
1077 let first_visible_line = text_layout_details.anchor.to_display_point(map);
1078 let bottom_row = first_visible_line.row() + (visible_rows) as u32;
1079 let bottom_row_capped = bottom_row.min(map.max_buffer_row());
1080 let new_col = point.column().min(map.line_len(bottom_row_capped));
1081 let new_point = DisplayPoint::new(bottom_row_capped, new_col);
1082 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1083 } else {
1084 (point, SelectionGoal::None)
1085 }
1086}
1087
1088#[cfg(test)]
1089mod test {
1090
1091 use crate::test::NeovimBackedTestContext;
1092 use indoc::indoc;
1093
1094 #[gpui::test]
1095 async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
1096 let mut cx = NeovimBackedTestContext::new(cx).await;
1097
1098 let initial_state = indoc! {r"ˇabc
1099 def
1100
1101 paragraph
1102 the second
1103
1104
1105
1106 third and
1107 final"};
1108
1109 // goes down once
1110 cx.set_shared_state(initial_state).await;
1111 cx.simulate_shared_keystrokes(["}"]).await;
1112 cx.assert_shared_state(indoc! {r"abc
1113 def
1114 ˇ
1115 paragraph
1116 the second
1117
1118
1119
1120 third and
1121 final"})
1122 .await;
1123
1124 // goes up once
1125 cx.simulate_shared_keystrokes(["{"]).await;
1126 cx.assert_shared_state(initial_state).await;
1127
1128 // goes down twice
1129 cx.simulate_shared_keystrokes(["2", "}"]).await;
1130 cx.assert_shared_state(indoc! {r"abc
1131 def
1132
1133 paragraph
1134 the second
1135 ˇ
1136
1137
1138 third and
1139 final"})
1140 .await;
1141
1142 // goes down over multiple blanks
1143 cx.simulate_shared_keystrokes(["}"]).await;
1144 cx.assert_shared_state(indoc! {r"abc
1145 def
1146
1147 paragraph
1148 the second
1149
1150
1151
1152 third and
1153 finaˇl"})
1154 .await;
1155
1156 // goes up twice
1157 cx.simulate_shared_keystrokes(["2", "{"]).await;
1158 cx.assert_shared_state(indoc! {r"abc
1159 def
1160 ˇ
1161 paragraph
1162 the second
1163
1164
1165
1166 third and
1167 final"})
1168 .await
1169 }
1170
1171 #[gpui::test]
1172 async fn test_matching(cx: &mut gpui::TestAppContext) {
1173 let mut cx = NeovimBackedTestContext::new(cx).await;
1174
1175 cx.set_shared_state(indoc! {r"func ˇ(a string) {
1176 do(something(with<Types>.and_arrays[0, 2]))
1177 }"})
1178 .await;
1179 cx.simulate_shared_keystrokes(["%"]).await;
1180 cx.assert_shared_state(indoc! {r"func (a stringˇ) {
1181 do(something(with<Types>.and_arrays[0, 2]))
1182 }"})
1183 .await;
1184
1185 // test it works on the last character of the line
1186 cx.set_shared_state(indoc! {r"func (a string) ˇ{
1187 do(something(with<Types>.and_arrays[0, 2]))
1188 }"})
1189 .await;
1190 cx.simulate_shared_keystrokes(["%"]).await;
1191 cx.assert_shared_state(indoc! {r"func (a string) {
1192 do(something(with<Types>.and_arrays[0, 2]))
1193 ˇ}"})
1194 .await;
1195
1196 // test it works on immediate nesting
1197 cx.set_shared_state("ˇ{()}").await;
1198 cx.simulate_shared_keystrokes(["%"]).await;
1199 cx.assert_shared_state("{()ˇ}").await;
1200 cx.simulate_shared_keystrokes(["%"]).await;
1201 cx.assert_shared_state("ˇ{()}").await;
1202
1203 // test it works on immediate nesting inside braces
1204 cx.set_shared_state("{\n ˇ{()}\n}").await;
1205 cx.simulate_shared_keystrokes(["%"]).await;
1206 cx.assert_shared_state("{\n {()ˇ}\n}").await;
1207
1208 // test it jumps to the next paren on a line
1209 cx.set_shared_state("func ˇboop() {\n}").await;
1210 cx.simulate_shared_keystrokes(["%"]).await;
1211 cx.assert_shared_state("func boop(ˇ) {\n}").await;
1212 }
1213
1214 #[gpui::test]
1215 async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
1216 let mut cx = NeovimBackedTestContext::new(cx).await;
1217
1218 // f and F
1219 cx.set_shared_state("ˇone two three four").await;
1220 cx.simulate_shared_keystrokes(["f", "o"]).await;
1221 cx.assert_shared_state("one twˇo three four").await;
1222 cx.simulate_shared_keystrokes([","]).await;
1223 cx.assert_shared_state("ˇone two three four").await;
1224 cx.simulate_shared_keystrokes(["2", ";"]).await;
1225 cx.assert_shared_state("one two three fˇour").await;
1226 cx.simulate_shared_keystrokes(["shift-f", "e"]).await;
1227 cx.assert_shared_state("one two threˇe four").await;
1228 cx.simulate_shared_keystrokes(["2", ";"]).await;
1229 cx.assert_shared_state("onˇe two three four").await;
1230 cx.simulate_shared_keystrokes([","]).await;
1231 cx.assert_shared_state("one two thrˇee four").await;
1232
1233 // t and T
1234 cx.set_shared_state("ˇone two three four").await;
1235 cx.simulate_shared_keystrokes(["t", "o"]).await;
1236 cx.assert_shared_state("one tˇwo three four").await;
1237 cx.simulate_shared_keystrokes([","]).await;
1238 cx.assert_shared_state("oˇne two three four").await;
1239 cx.simulate_shared_keystrokes(["2", ";"]).await;
1240 cx.assert_shared_state("one two three ˇfour").await;
1241 cx.simulate_shared_keystrokes(["shift-t", "e"]).await;
1242 cx.assert_shared_state("one two threeˇ four").await;
1243 cx.simulate_shared_keystrokes(["3", ";"]).await;
1244 cx.assert_shared_state("oneˇ two three four").await;
1245 cx.simulate_shared_keystrokes([","]).await;
1246 cx.assert_shared_state("one two thˇree four").await;
1247 }
1248
1249 #[gpui::test]
1250 async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
1251 let mut cx = NeovimBackedTestContext::new(cx).await;
1252 cx.set_shared_state("ˇone\n two\nthree").await;
1253 cx.simulate_shared_keystrokes(["enter"]).await;
1254 cx.assert_shared_state("one\n ˇtwo\nthree").await;
1255 }
1256
1257 #[gpui::test]
1258 async fn test_window_top(cx: &mut gpui::TestAppContext) {
1259 let mut cx = NeovimBackedTestContext::new(cx).await;
1260 let initial_state = indoc! {r"abc
1261 def
1262 paragraph
1263 the second
1264 third ˇand
1265 final"};
1266
1267 cx.set_shared_state(initial_state).await;
1268 cx.simulate_shared_keystrokes(["shift-h"]).await;
1269 cx.assert_shared_state(indoc! {r"abˇc
1270 def
1271 paragraph
1272 the second
1273 third and
1274 final"})
1275 .await;
1276
1277 // clip point
1278 cx.set_shared_state(indoc! {r"
1279 1 2 3
1280 4 5 6
1281 7 8 ˇ9
1282 "})
1283 .await;
1284 cx.simulate_shared_keystrokes(["shift-h"]).await;
1285 cx.assert_shared_state(indoc! {r"
1286 1 2 ˇ3
1287 4 5 6
1288 7 8 9
1289 "})
1290 .await;
1291
1292 cx.set_shared_state(indoc! {r"
1293 1 2 3
1294 4 5 6
1295 ˇ7 8 9
1296 "})
1297 .await;
1298 cx.simulate_shared_keystrokes(["shift-h"]).await;
1299 cx.assert_shared_state(indoc! {r"
1300 ˇ1 2 3
1301 4 5 6
1302 7 8 9
1303 "})
1304 .await;
1305 }
1306
1307 #[gpui::test]
1308 async fn test_window_middle(cx: &mut gpui::TestAppContext) {
1309 let mut cx = NeovimBackedTestContext::new(cx).await;
1310 let initial_state = indoc! {r"abˇc
1311 def
1312 paragraph
1313 the second
1314 third and
1315 final"};
1316
1317 cx.set_shared_state(initial_state).await;
1318 cx.simulate_shared_keystrokes(["shift-m"]).await;
1319 cx.assert_shared_state(indoc! {r"abc
1320 def
1321 paˇragraph
1322 the second
1323 third and
1324 final"})
1325 .await;
1326
1327 cx.set_shared_state(indoc! {r"
1328 1 2 3
1329 4 5 6
1330 7 8 ˇ9
1331 "})
1332 .await;
1333 cx.simulate_shared_keystrokes(["shift-m"]).await;
1334 cx.assert_shared_state(indoc! {r"
1335 1 2 3
1336 4 5 ˇ6
1337 7 8 9
1338 "})
1339 .await;
1340 cx.set_shared_state(indoc! {r"
1341 1 2 3
1342 4 5 6
1343 ˇ7 8 9
1344 "})
1345 .await;
1346 cx.simulate_shared_keystrokes(["shift-m"]).await;
1347 cx.assert_shared_state(indoc! {r"
1348 1 2 3
1349 ˇ4 5 6
1350 7 8 9
1351 "})
1352 .await;
1353 cx.set_shared_state(indoc! {r"
1354 ˇ1 2 3
1355 4 5 6
1356 7 8 9
1357 "})
1358 .await;
1359 cx.simulate_shared_keystrokes(["shift-m"]).await;
1360 cx.assert_shared_state(indoc! {r"
1361 1 2 3
1362 ˇ4 5 6
1363 7 8 9
1364 "})
1365 .await;
1366 cx.set_shared_state(indoc! {r"
1367 1 2 3
1368 ˇ4 5 6
1369 7 8 9
1370 "})
1371 .await;
1372 cx.simulate_shared_keystrokes(["shift-m"]).await;
1373 cx.assert_shared_state(indoc! {r"
1374 1 2 3
1375 ˇ4 5 6
1376 7 8 9
1377 "})
1378 .await;
1379 cx.set_shared_state(indoc! {r"
1380 1 2 3
1381 4 5 ˇ6
1382 7 8 9
1383 "})
1384 .await;
1385 cx.simulate_shared_keystrokes(["shift-m"]).await;
1386 cx.assert_shared_state(indoc! {r"
1387 1 2 3
1388 4 5 ˇ6
1389 7 8 9
1390 "})
1391 .await;
1392 }
1393
1394 #[gpui::test]
1395 async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
1396 let mut cx = NeovimBackedTestContext::new(cx).await;
1397 let initial_state = indoc! {r"abc
1398 deˇf
1399 paragraph
1400 the second
1401 third and
1402 final"};
1403
1404 cx.set_shared_state(initial_state).await;
1405 cx.simulate_shared_keystrokes(["shift-l"]).await;
1406 cx.assert_shared_state(indoc! {r"abc
1407 def
1408 paragraph
1409 the second
1410 third and
1411 fiˇnal"})
1412 .await;
1413
1414 cx.set_shared_state(indoc! {r"
1415 1 2 3
1416 4 5 ˇ6
1417 7 8 9
1418 "})
1419 .await;
1420 cx.simulate_shared_keystrokes(["shift-l"]).await;
1421 cx.assert_shared_state(indoc! {r"
1422 1 2 3
1423 4 5 6
1424 7 8 9
1425 ˇ"})
1426 .await;
1427
1428 cx.set_shared_state(indoc! {r"
1429 1 2 3
1430 ˇ4 5 6
1431 7 8 9
1432 "})
1433 .await;
1434 cx.simulate_shared_keystrokes(["shift-l"]).await;
1435 cx.assert_shared_state(indoc! {r"
1436 1 2 3
1437 4 5 6
1438 7 8 9
1439 ˇ"})
1440 .await;
1441
1442 cx.set_shared_state(indoc! {r"
1443 1 2 ˇ3
1444 4 5 6
1445 7 8 9
1446 "})
1447 .await;
1448 cx.simulate_shared_keystrokes(["shift-l"]).await;
1449 cx.assert_shared_state(indoc! {r"
1450 1 2 3
1451 4 5 6
1452 7 8 9
1453 ˇ"})
1454 .await;
1455
1456 cx.set_shared_state(indoc! {r"
1457 ˇ1 2 3
1458 4 5 6
1459 7 8 9
1460 "})
1461 .await;
1462 cx.simulate_shared_keystrokes(["shift-l"]).await;
1463 cx.assert_shared_state(indoc! {r"
1464 1 2 3
1465 4 5 6
1466 7 8 9
1467 ˇ"})
1468 .await;
1469 }
1470}