1use std::sync::Arc;
2
3use collections::HashMap;
4use editor::{
5 display_map::{DisplaySnapshot, ToDisplayPoint},
6 movement,
7 scroll::Autoscroll,
8 Bias, DisplayPoint, Editor, ToOffset,
9};
10use gpui::{actions, ViewContext};
11use language::{Point, Selection, SelectionGoal};
12use multi_buffer::MultiBufferRow;
13use search::BufferSearchBar;
14use util::ResultExt;
15use workspace::searchable::Direction;
16
17use crate::{
18 motion::{first_non_whitespace, next_line_end, start_of_line, Motion},
19 object::Object,
20 state::{Mode, Operator},
21 Vim,
22};
23
24actions!(
25 vim,
26 [
27 ToggleVisual,
28 ToggleVisualLine,
29 ToggleVisualBlock,
30 VisualDelete,
31 VisualDeleteLine,
32 VisualYank,
33 VisualYankLine,
34 OtherEnd,
35 SelectNext,
36 SelectPrevious,
37 SelectNextMatch,
38 SelectPreviousMatch,
39 RestoreVisualSelection,
40 VisualInsertEndOfLine,
41 VisualInsertFirstNonWhiteSpace,
42 ]
43);
44
45pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
46 Vim::action(editor, cx, |vim, _: &ToggleVisual, cx| {
47 vim.toggle_mode(Mode::Visual, cx)
48 });
49 Vim::action(editor, cx, |vim, _: &ToggleVisualLine, cx| {
50 vim.toggle_mode(Mode::VisualLine, cx)
51 });
52 Vim::action(editor, cx, |vim, _: &ToggleVisualBlock, cx| {
53 vim.toggle_mode(Mode::VisualBlock, cx)
54 });
55 Vim::action(editor, cx, Vim::other_end);
56 Vim::action(editor, cx, Vim::visual_insert_end_of_line);
57 Vim::action(editor, cx, Vim::visual_insert_first_non_white_space);
58 Vim::action(editor, cx, |vim, _: &VisualDelete, cx| {
59 vim.record_current_action(cx);
60 vim.visual_delete(false, cx);
61 });
62 Vim::action(editor, cx, |vim, _: &VisualDeleteLine, cx| {
63 vim.record_current_action(cx);
64 vim.visual_delete(true, cx);
65 });
66 Vim::action(editor, cx, |vim, _: &VisualYank, cx| vim.visual_yank(cx));
67
68 Vim::action(editor, cx, Vim::select_next);
69 Vim::action(editor, cx, Vim::select_previous);
70 Vim::action(editor, cx, |vim, _: &SelectNextMatch, cx| {
71 vim.select_match(Direction::Next, cx);
72 });
73 Vim::action(editor, cx, |vim, _: &SelectPreviousMatch, cx| {
74 vim.select_match(Direction::Prev, cx);
75 });
76
77 Vim::action(editor, cx, |vim, _: &RestoreVisualSelection, cx| {
78 let Some((stored_mode, reversed)) = vim.stored_visual_mode.take() else {
79 return;
80 };
81 let Some((start, end)) = vim.marks.get("<").zip(vim.marks.get(">")) else {
82 return;
83 };
84 let ranges = start
85 .iter()
86 .zip(end)
87 .zip(reversed)
88 .map(|((start, end), reversed)| (*start, *end, reversed))
89 .collect::<Vec<_>>();
90
91 if vim.mode.is_visual() {
92 vim.create_visual_marks(vim.mode, cx);
93 }
94
95 vim.update_editor(cx, |_, editor, cx| {
96 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
97 let map = s.display_map();
98 let ranges = ranges
99 .into_iter()
100 .map(|(start, end, reversed)| {
101 let new_end = movement::saturating_right(&map, end.to_display_point(&map));
102 Selection {
103 id: s.new_selection_id(),
104 start: start.to_offset(&map.buffer_snapshot),
105 end: new_end.to_offset(&map, Bias::Left),
106 reversed,
107 goal: SelectionGoal::None,
108 }
109 })
110 .collect();
111 s.select(ranges);
112 })
113 });
114 vim.switch_mode(stored_mode, true, cx)
115 });
116}
117
118impl Vim {
119 pub fn visual_motion(
120 &mut self,
121 motion: Motion,
122 times: Option<usize>,
123 cx: &mut ViewContext<Self>,
124 ) {
125 self.update_editor(cx, |vim, editor, cx| {
126 let text_layout_details = editor.text_layout_details(cx);
127 if vim.mode == Mode::VisualBlock
128 && !matches!(
129 motion,
130 Motion::EndOfLine {
131 display_lines: false
132 }
133 )
134 {
135 let is_up_or_down = matches!(motion, Motion::Up { .. } | Motion::Down { .. });
136 vim.visual_block_motion(is_up_or_down, editor, cx, |map, point, goal| {
137 motion.move_point(map, point, goal, times, &text_layout_details)
138 })
139 } else {
140 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
141 s.move_with(|map, selection| {
142 let was_reversed = selection.reversed;
143 let mut current_head = selection.head();
144
145 // our motions assume the current character is after the cursor,
146 // but in (forward) visual mode the current character is just
147 // before the end of the selection.
148
149 // If the file ends with a newline (which is common) we don't do this.
150 // so that if you go to the end of such a file you can use "up" to go
151 // to the previous line and have it work somewhat as expected.
152 #[allow(clippy::nonminimal_bool)]
153 if !selection.reversed
154 && !selection.is_empty()
155 && !(selection.end.column() == 0 && selection.end == map.max_point())
156 {
157 current_head = movement::left(map, selection.end)
158 }
159
160 let Some((new_head, goal)) = motion.move_point(
161 map,
162 current_head,
163 selection.goal,
164 times,
165 &text_layout_details,
166 ) else {
167 return;
168 };
169
170 selection.set_head(new_head, goal);
171
172 // ensure the current character is included in the selection.
173 if !selection.reversed {
174 let next_point = if vim.mode == Mode::VisualBlock {
175 movement::saturating_right(map, selection.end)
176 } else {
177 movement::right(map, selection.end)
178 };
179
180 if !(next_point.column() == 0 && next_point == map.max_point()) {
181 selection.end = next_point;
182 }
183 }
184
185 // vim always ensures the anchor character stays selected.
186 // if our selection has reversed, we need to move the opposite end
187 // to ensure the anchor is still selected.
188 if was_reversed && !selection.reversed {
189 selection.start = movement::left(map, selection.start);
190 } else if !was_reversed && selection.reversed {
191 selection.end = movement::right(map, selection.end);
192 }
193 })
194 });
195 }
196 });
197 }
198
199 pub fn visual_block_motion(
200 &mut self,
201 preserve_goal: bool,
202 editor: &mut Editor,
203 cx: &mut ViewContext<Editor>,
204 mut move_selection: impl FnMut(
205 &DisplaySnapshot,
206 DisplayPoint,
207 SelectionGoal,
208 ) -> Option<(DisplayPoint, SelectionGoal)>,
209 ) {
210 let text_layout_details = editor.text_layout_details(cx);
211 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
212 let map = &s.display_map();
213 let mut head = s.newest_anchor().head().to_display_point(map);
214 let mut tail = s.oldest_anchor().tail().to_display_point(map);
215
216 let mut head_x = map.x_for_display_point(head, &text_layout_details);
217 let mut tail_x = map.x_for_display_point(tail, &text_layout_details);
218
219 let (start, end) = match s.newest_anchor().goal {
220 SelectionGoal::HorizontalRange { start, end } if preserve_goal => (start, end),
221 SelectionGoal::HorizontalPosition(start) if preserve_goal => (start, start),
222 _ => (tail_x.0, head_x.0),
223 };
224 let mut goal = SelectionGoal::HorizontalRange { start, end };
225
226 let was_reversed = tail_x > head_x;
227 if !was_reversed && !preserve_goal {
228 head = movement::saturating_left(map, head);
229 }
230
231 let Some((new_head, _)) = move_selection(map, head, goal) else {
232 return;
233 };
234 head = new_head;
235 head_x = map.x_for_display_point(head, &text_layout_details);
236
237 let is_reversed = tail_x > head_x;
238 if was_reversed && !is_reversed {
239 tail = movement::saturating_left(map, tail);
240 tail_x = map.x_for_display_point(tail, &text_layout_details);
241 } else if !was_reversed && is_reversed {
242 tail = movement::saturating_right(map, tail);
243 tail_x = map.x_for_display_point(tail, &text_layout_details);
244 }
245 if !is_reversed && !preserve_goal {
246 head = movement::saturating_right(map, head);
247 head_x = map.x_for_display_point(head, &text_layout_details);
248 }
249
250 let positions = if is_reversed {
251 head_x..tail_x
252 } else {
253 tail_x..head_x
254 };
255
256 if !preserve_goal {
257 goal = SelectionGoal::HorizontalRange {
258 start: positions.start.0,
259 end: positions.end.0,
260 };
261 }
262
263 let mut selections = Vec::new();
264 let mut row = tail.row();
265
266 loop {
267 let laid_out_line = map.layout_row(row, &text_layout_details);
268 let start = DisplayPoint::new(
269 row,
270 laid_out_line.closest_index_for_x(positions.start) as u32,
271 );
272 let mut end =
273 DisplayPoint::new(row, laid_out_line.closest_index_for_x(positions.end) as u32);
274 if end <= start {
275 if start.column() == map.line_len(start.row()) {
276 end = start;
277 } else {
278 end = movement::saturating_right(map, start);
279 }
280 }
281
282 if positions.start <= laid_out_line.width {
283 let selection = Selection {
284 id: s.new_selection_id(),
285 start: start.to_point(map),
286 end: end.to_point(map),
287 reversed: is_reversed,
288 goal,
289 };
290
291 selections.push(selection);
292 }
293 if row == head.row() {
294 break;
295 }
296 if tail.row() > head.row() {
297 row.0 -= 1
298 } else {
299 row.0 += 1
300 }
301 }
302
303 s.select(selections);
304 })
305 }
306
307 pub fn visual_object(&mut self, object: Object, cx: &mut ViewContext<Vim>) {
308 if let Some(Operator::Object { around }) = self.active_operator() {
309 self.pop_operator(cx);
310 let current_mode = self.mode;
311 let target_mode = object.target_visual_mode(current_mode);
312 if target_mode != current_mode {
313 self.switch_mode(target_mode, true, cx);
314 }
315
316 self.update_editor(cx, |_, editor, cx| {
317 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
318 s.move_with(|map, selection| {
319 let mut mut_selection = selection.clone();
320
321 // all our motions assume that the current character is
322 // after the cursor; however in the case of a visual selection
323 // the current character is before the cursor.
324 // But this will affect the judgment of the html tag
325 // so the html tag needs to skip this logic.
326 if !selection.reversed && object != Object::Tag {
327 mut_selection.set_head(
328 movement::left(map, mut_selection.head()),
329 mut_selection.goal,
330 );
331 }
332
333 if let Some(range) = object.range(map, mut_selection, around) {
334 if !range.is_empty() {
335 let expand_both_ways = object.always_expands_both_ways()
336 || selection.is_empty()
337 || movement::right(map, selection.start) == selection.end;
338
339 if expand_both_ways {
340 selection.start = range.start;
341 selection.end = range.end;
342 } else if selection.reversed {
343 selection.start = range.start;
344 } else {
345 selection.end = range.end;
346 }
347 }
348
349 // In the visual selection result of a paragraph object, the cursor is
350 // placed at the start of the last line. And in the visual mode, the
351 // selection end is located after the end character. So, adjustment of
352 // selection end is needed.
353 //
354 // We don't do this adjustment for a one-line blank paragraph since the
355 // trailing newline is included in its selection from the beginning.
356 if object == Object::Paragraph && range.start != range.end {
357 let row_of_selection_end_line = selection.end.to_point(map).row;
358 let new_selection_end = if map
359 .buffer_snapshot
360 .line_len(MultiBufferRow(row_of_selection_end_line))
361 == 0
362 {
363 Point::new(row_of_selection_end_line + 1, 0)
364 } else {
365 Point::new(row_of_selection_end_line, 1)
366 };
367 selection.end = new_selection_end.to_display_point(map);
368 }
369 }
370 });
371 });
372 });
373 }
374 }
375
376 fn visual_insert_end_of_line(&mut self, _: &VisualInsertEndOfLine, cx: &mut ViewContext<Self>) {
377 self.update_editor(cx, |_, editor, cx| {
378 editor.split_selection_into_lines(&Default::default(), cx);
379 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
380 s.move_cursors_with(|map, cursor, _| {
381 (next_line_end(map, cursor, 1), SelectionGoal::None)
382 });
383 });
384 });
385
386 self.switch_mode(Mode::Insert, false, cx);
387 }
388
389 fn visual_insert_first_non_white_space(
390 &mut self,
391 _: &VisualInsertFirstNonWhiteSpace,
392 cx: &mut ViewContext<Self>,
393 ) {
394 self.update_editor(cx, |_, editor, cx| {
395 editor.split_selection_into_lines(&Default::default(), cx);
396 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
397 s.move_cursors_with(|map, cursor, _| {
398 (
399 first_non_whitespace(map, false, cursor),
400 SelectionGoal::None,
401 )
402 });
403 });
404 });
405
406 self.switch_mode(Mode::Insert, false, cx);
407 }
408
409 fn toggle_mode(&mut self, mode: Mode, cx: &mut ViewContext<Self>) {
410 if self.mode == mode {
411 self.switch_mode(Mode::Normal, false, cx);
412 } else {
413 self.switch_mode(mode, false, cx);
414 }
415 }
416
417 pub fn other_end(&mut self, _: &OtherEnd, cx: &mut ViewContext<Self>) {
418 self.update_editor(cx, |_, editor, cx| {
419 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
420 s.move_with(|_, selection| {
421 selection.reversed = !selection.reversed;
422 })
423 })
424 });
425 }
426
427 pub fn visual_delete(&mut self, line_mode: bool, cx: &mut ViewContext<Self>) {
428 self.store_visual_marks(cx);
429 self.update_editor(cx, |vim, editor, cx| {
430 let mut original_columns: HashMap<_, _> = Default::default();
431 let line_mode = line_mode || editor.selections.line_mode;
432
433 editor.transact(cx, |editor, cx| {
434 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
435 s.move_with(|map, selection| {
436 if line_mode {
437 let mut position = selection.head();
438 if !selection.reversed {
439 position = movement::left(map, position);
440 }
441 original_columns.insert(selection.id, position.to_point(map).column);
442 if vim.mode == Mode::VisualBlock {
443 *selection.end.column_mut() = map.line_len(selection.end.row())
444 } else if vim.mode != Mode::VisualLine {
445 selection.start = DisplayPoint::new(selection.start.row(), 0);
446 if selection.end.row() == map.max_point().row() {
447 selection.end = map.max_point()
448 } else {
449 *selection.end.row_mut() += 1;
450 *selection.end.column_mut() = 0;
451 }
452 }
453 }
454 selection.goal = SelectionGoal::None;
455 });
456 });
457 vim.copy_selections_content(editor, line_mode, cx);
458 editor.insert("", cx);
459
460 // Fixup cursor position after the deletion
461 editor.set_clip_at_line_ends(true, cx);
462 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
463 s.move_with(|map, selection| {
464 let mut cursor = selection.head().to_point(map);
465
466 if let Some(column) = original_columns.get(&selection.id) {
467 cursor.column = *column
468 }
469 let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left);
470 selection.collapse_to(cursor, selection.goal)
471 });
472 if vim.mode == Mode::VisualBlock {
473 s.select_anchors(vec![s.first_anchor()])
474 }
475 });
476 })
477 });
478 self.switch_mode(Mode::Normal, true, cx);
479 }
480
481 pub fn visual_yank(&mut self, cx: &mut ViewContext<Self>) {
482 self.store_visual_marks(cx);
483 self.update_editor(cx, |vim, editor, cx| {
484 let line_mode = editor.selections.line_mode;
485 vim.yank_selections_content(editor, line_mode, cx);
486 editor.change_selections(None, cx, |s| {
487 s.move_with(|map, selection| {
488 if line_mode {
489 selection.start = start_of_line(map, false, selection.start);
490 };
491 selection.collapse_to(selection.start, SelectionGoal::None)
492 });
493 if vim.mode == Mode::VisualBlock {
494 s.select_anchors(vec![s.first_anchor()])
495 }
496 });
497 });
498 self.switch_mode(Mode::Normal, true, cx);
499 }
500
501 pub(crate) fn visual_replace(&mut self, text: Arc<str>, cx: &mut ViewContext<Self>) {
502 self.stop_recording(cx);
503 self.update_editor(cx, |_, editor, cx| {
504 editor.transact(cx, |editor, cx| {
505 let (display_map, selections) = editor.selections.all_adjusted_display(cx);
506
507 // Selections are biased right at the start. So we need to store
508 // anchors that are biased left so that we can restore the selections
509 // after the change
510 let stable_anchors = editor
511 .selections
512 .disjoint_anchors()
513 .iter()
514 .map(|selection| {
515 let start = selection.start.bias_left(&display_map.buffer_snapshot);
516 start..start
517 })
518 .collect::<Vec<_>>();
519
520 let mut edits = Vec::new();
521 for selection in selections.iter() {
522 let selection = selection.clone();
523 for row_range in
524 movement::split_display_range_by_lines(&display_map, selection.range())
525 {
526 let range = row_range.start.to_offset(&display_map, Bias::Right)
527 ..row_range.end.to_offset(&display_map, Bias::Right);
528 let text = text.repeat(range.len());
529 edits.push((range, text));
530 }
531 }
532
533 editor.buffer().update(cx, |buffer, cx| {
534 buffer.edit(edits, None, cx);
535 });
536 editor.change_selections(None, cx, |s| s.select_ranges(stable_anchors));
537 });
538 });
539 self.switch_mode(Mode::Normal, false, cx);
540 }
541
542 pub fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
543 let count = self
544 .take_count(cx)
545 .unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 });
546 self.update_editor(cx, |_, editor, cx| {
547 editor.set_clip_at_line_ends(false, cx);
548 for _ in 0..count {
549 if editor
550 .select_next(&Default::default(), cx)
551 .log_err()
552 .is_none()
553 {
554 break;
555 }
556 }
557 });
558 }
559
560 pub fn select_previous(&mut self, _: &SelectPrevious, cx: &mut ViewContext<Self>) {
561 let count = self
562 .take_count(cx)
563 .unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 });
564 self.update_editor(cx, |_, editor, cx| {
565 for _ in 0..count {
566 if editor
567 .select_previous(&Default::default(), cx)
568 .log_err()
569 .is_none()
570 {
571 break;
572 }
573 }
574 });
575 }
576
577 pub fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
578 let count = self.take_count(cx).unwrap_or(1);
579 let Some(workspace) = self
580 .editor
581 .upgrade()
582 .and_then(|editor| editor.read(cx).workspace())
583 else {
584 return;
585 };
586 let pane = workspace.read(cx).active_pane().clone();
587 let vim_is_normal = self.mode == Mode::Normal;
588 let mut start_selection = 0usize;
589 let mut end_selection = 0usize;
590
591 self.update_editor(cx, |_, editor, _| {
592 editor.set_collapse_matches(false);
593 });
594 if vim_is_normal {
595 pane.update(cx, |pane, cx| {
596 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()
597 {
598 search_bar.update(cx, |search_bar, cx| {
599 if !search_bar.has_active_match() || !search_bar.show(cx) {
600 return;
601 }
602 // without update_match_index there is a bug when the cursor is before the first match
603 search_bar.update_match_index(cx);
604 search_bar.select_match(direction.opposite(), 1, cx);
605 });
606 }
607 });
608 }
609 self.update_editor(cx, |_, editor, cx| {
610 let latest = editor.selections.newest::<usize>(cx);
611 start_selection = latest.start;
612 end_selection = latest.end;
613 });
614
615 let mut match_exists = false;
616 pane.update(cx, |pane, cx| {
617 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
618 search_bar.update(cx, |search_bar, cx| {
619 search_bar.update_match_index(cx);
620 search_bar.select_match(direction, count, cx);
621 match_exists = search_bar.match_exists(cx);
622 });
623 }
624 });
625 if !match_exists {
626 self.clear_operator(cx);
627 self.stop_replaying(cx);
628 return;
629 }
630 self.update_editor(cx, |_, editor, cx| {
631 let latest = editor.selections.newest::<usize>(cx);
632 if vim_is_normal {
633 start_selection = latest.start;
634 end_selection = latest.end;
635 } else {
636 start_selection = start_selection.min(latest.start);
637 end_selection = end_selection.max(latest.end);
638 }
639 if direction == Direction::Prev {
640 std::mem::swap(&mut start_selection, &mut end_selection);
641 }
642 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
643 s.select_ranges([start_selection..end_selection]);
644 });
645 editor.set_collapse_matches(true);
646 });
647
648 match self.maybe_pop_operator() {
649 Some(Operator::Change) => self.substitute(None, false, cx),
650 Some(Operator::Delete) => {
651 self.stop_recording(cx);
652 self.visual_delete(false, cx)
653 }
654 Some(Operator::Yank) => self.visual_yank(cx),
655 _ => {} // Ignoring other operators
656 }
657 }
658}
659#[cfg(test)]
660mod test {
661 use indoc::indoc;
662 use workspace::item::Item;
663
664 use crate::{
665 state::Mode,
666 test::{NeovimBackedTestContext, VimTestContext},
667 };
668
669 #[gpui::test]
670 async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
671 let mut cx = NeovimBackedTestContext::new(cx).await;
672
673 cx.set_shared_state(indoc! {
674 "The ˇquick brown
675 fox jumps over
676 the lazy dog"
677 })
678 .await;
679 let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
680
681 // entering visual mode should select the character
682 // under cursor
683 cx.simulate_shared_keystrokes("v").await;
684 cx.shared_state()
685 .await
686 .assert_eq(indoc! { "The «qˇ»uick brown
687 fox jumps over
688 the lazy dog"});
689 cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
690
691 // forwards motions should extend the selection
692 cx.simulate_shared_keystrokes("w j").await;
693 cx.shared_state().await.assert_eq(indoc! { "The «quick brown
694 fox jumps oˇ»ver
695 the lazy dog"});
696
697 cx.simulate_shared_keystrokes("escape").await;
698 cx.shared_state().await.assert_eq(indoc! { "The quick brown
699 fox jumps ˇover
700 the lazy dog"});
701
702 // motions work backwards
703 cx.simulate_shared_keystrokes("v k b").await;
704 cx.shared_state()
705 .await
706 .assert_eq(indoc! { "The «ˇquick brown
707 fox jumps o»ver
708 the lazy dog"});
709
710 // works on empty lines
711 cx.set_shared_state(indoc! {"
712 a
713 ˇ
714 b
715 "})
716 .await;
717 let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
718 cx.simulate_shared_keystrokes("v").await;
719 cx.shared_state().await.assert_eq(indoc! {"
720 a
721 «
722 ˇ»b
723 "});
724 cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
725
726 // toggles off again
727 cx.simulate_shared_keystrokes("v").await;
728 cx.shared_state().await.assert_eq(indoc! {"
729 a
730 ˇ
731 b
732 "});
733
734 // works at the end of a document
735 cx.set_shared_state(indoc! {"
736 a
737 b
738 ˇ"})
739 .await;
740
741 cx.simulate_shared_keystrokes("v").await;
742 cx.shared_state().await.assert_eq(indoc! {"
743 a
744 b
745 ˇ"});
746 }
747
748 #[gpui::test]
749 async fn test_visual_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
750 let mut cx = VimTestContext::new(cx, true).await;
751
752 cx.set_state(
753 indoc! {
754 "«The quick brown
755 fox jumps over
756 the lazy dogˇ»"
757 },
758 Mode::Visual,
759 );
760 cx.simulate_keystrokes("g I");
761 cx.assert_state(
762 indoc! {
763 "ˇThe quick brown
764 ˇfox jumps over
765 ˇthe lazy dog"
766 },
767 Mode::Insert,
768 );
769 }
770
771 #[gpui::test]
772 async fn test_visual_insert_end_of_line(cx: &mut gpui::TestAppContext) {
773 let mut cx = VimTestContext::new(cx, true).await;
774
775 cx.set_state(
776 indoc! {
777 "«The quick brown
778 fox jumps over
779 the lazy dogˇ»"
780 },
781 Mode::Visual,
782 );
783 cx.simulate_keystrokes("g A");
784 cx.assert_state(
785 indoc! {
786 "The quick brownˇ
787 fox jumps overˇ
788 the lazy dogˇ"
789 },
790 Mode::Insert,
791 );
792 }
793
794 #[gpui::test]
795 async fn test_enter_visual_line_mode(cx: &mut gpui::TestAppContext) {
796 let mut cx = NeovimBackedTestContext::new(cx).await;
797
798 cx.set_shared_state(indoc! {
799 "The ˇquick brown
800 fox jumps over
801 the lazy dog"
802 })
803 .await;
804 cx.simulate_shared_keystrokes("shift-v").await;
805 cx.shared_state()
806 .await
807 .assert_eq(indoc! { "The «qˇ»uick brown
808 fox jumps over
809 the lazy dog"});
810 cx.simulate_shared_keystrokes("x").await;
811 cx.shared_state().await.assert_eq(indoc! { "fox ˇjumps over
812 the lazy dog"});
813
814 // it should work on empty lines
815 cx.set_shared_state(indoc! {"
816 a
817 ˇ
818 b"})
819 .await;
820 cx.simulate_shared_keystrokes("shift-v").await;
821 cx.shared_state().await.assert_eq(indoc! {"
822 a
823 «
824 ˇ»b"});
825 cx.simulate_shared_keystrokes("x").await;
826 cx.shared_state().await.assert_eq(indoc! {"
827 a
828 ˇb"});
829
830 // it should work at the end of the document
831 cx.set_shared_state(indoc! {"
832 a
833 b
834 ˇ"})
835 .await;
836 let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
837 cx.simulate_shared_keystrokes("shift-v").await;
838 cx.shared_state().await.assert_eq(indoc! {"
839 a
840 b
841 ˇ"});
842 cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
843 cx.simulate_shared_keystrokes("x").await;
844 cx.shared_state().await.assert_eq(indoc! {"
845 a
846 ˇb"});
847 }
848
849 #[gpui::test]
850 async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
851 let mut cx = NeovimBackedTestContext::new(cx).await;
852
853 cx.simulate("v w", "The quick ˇbrown")
854 .await
855 .assert_matches();
856
857 cx.simulate("v w x", "The quick ˇbrown")
858 .await
859 .assert_matches();
860 cx.simulate(
861 "v w j x",
862 indoc! {"
863 The ˇquick brown
864 fox jumps over
865 the lazy dog"},
866 )
867 .await
868 .assert_matches();
869 // Test pasting code copied on delete
870 cx.simulate_shared_keystrokes("j p").await;
871 cx.shared_state().await.assert_matches();
872
873 cx.simulate_at_each_offset(
874 "v w j x",
875 indoc! {"
876 The ˇquick brown
877 fox jumps over
878 the ˇlazy dog"},
879 )
880 .await
881 .assert_matches();
882 cx.simulate_at_each_offset(
883 "v b k x",
884 indoc! {"
885 The ˇquick brown
886 fox jumps ˇover
887 the ˇlazy dog"},
888 )
889 .await
890 .assert_matches();
891 }
892
893 #[gpui::test]
894 async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
895 let mut cx = NeovimBackedTestContext::new(cx).await;
896
897 cx.set_shared_state(indoc! {"
898 The quˇick brown
899 fox jumps over
900 the lazy dog"})
901 .await;
902 cx.simulate_shared_keystrokes("shift-v x").await;
903 cx.shared_state().await.assert_matches();
904
905 // Test pasting code copied on delete
906 cx.simulate_shared_keystrokes("p").await;
907 cx.shared_state().await.assert_matches();
908
909 cx.set_shared_state(indoc! {"
910 The quick brown
911 fox jumps over
912 the laˇzy dog"})
913 .await;
914 cx.simulate_shared_keystrokes("shift-v x").await;
915 cx.shared_state().await.assert_matches();
916 cx.shared_clipboard().await.assert_eq("the lazy dog\n");
917
918 cx.set_shared_state(indoc! {"
919 The quˇick brown
920 fox jumps over
921 the lazy dog"})
922 .await;
923 cx.simulate_shared_keystrokes("shift-v j x").await;
924 cx.shared_state().await.assert_matches();
925 // Test pasting code copied on delete
926 cx.simulate_shared_keystrokes("p").await;
927 cx.shared_state().await.assert_matches();
928
929 cx.set_shared_state(indoc! {"
930 The ˇlong line
931 should not
932 crash
933 "})
934 .await;
935 cx.simulate_shared_keystrokes("shift-v $ x").await;
936 cx.shared_state().await.assert_matches();
937 }
938
939 #[gpui::test]
940 async fn test_visual_yank(cx: &mut gpui::TestAppContext) {
941 let mut cx = NeovimBackedTestContext::new(cx).await;
942
943 cx.set_shared_state("The quick ˇbrown").await;
944 cx.simulate_shared_keystrokes("v w y").await;
945 cx.shared_state().await.assert_eq("The quick ˇbrown");
946 cx.shared_clipboard().await.assert_eq("brown");
947
948 cx.set_shared_state(indoc! {"
949 The ˇquick brown
950 fox jumps over
951 the lazy dog"})
952 .await;
953 cx.simulate_shared_keystrokes("v w j y").await;
954 cx.shared_state().await.assert_eq(indoc! {"
955 The ˇquick brown
956 fox jumps over
957 the lazy dog"});
958 cx.shared_clipboard().await.assert_eq(indoc! {"
959 quick brown
960 fox jumps o"});
961
962 cx.set_shared_state(indoc! {"
963 The quick brown
964 fox jumps over
965 the ˇlazy dog"})
966 .await;
967 cx.simulate_shared_keystrokes("v w j y").await;
968 cx.shared_state().await.assert_eq(indoc! {"
969 The quick brown
970 fox jumps over
971 the ˇlazy dog"});
972 cx.shared_clipboard().await.assert_eq("lazy d");
973 cx.simulate_shared_keystrokes("shift-v y").await;
974 cx.shared_clipboard().await.assert_eq("the lazy dog\n");
975
976 cx.set_shared_state(indoc! {"
977 The ˇquick brown
978 fox jumps over
979 the lazy dog"})
980 .await;
981 cx.simulate_shared_keystrokes("v b k y").await;
982 cx.shared_state().await.assert_eq(indoc! {"
983 ˇThe quick brown
984 fox jumps over
985 the lazy dog"});
986 assert_eq!(
987 cx.read_from_clipboard()
988 .map(|item| item.text().unwrap().to_string())
989 .unwrap(),
990 "The q"
991 );
992
993 cx.set_shared_state(indoc! {"
994 The quick brown
995 fox ˇjumps over
996 the lazy dog"})
997 .await;
998 cx.simulate_shared_keystrokes("shift-v shift-g shift-y")
999 .await;
1000 cx.shared_state().await.assert_eq(indoc! {"
1001 The quick brown
1002 ˇfox jumps over
1003 the lazy dog"});
1004 cx.shared_clipboard()
1005 .await
1006 .assert_eq("fox jumps over\nthe lazy dog\n");
1007 }
1008
1009 #[gpui::test]
1010 async fn test_visual_block_mode(cx: &mut gpui::TestAppContext) {
1011 let mut cx = NeovimBackedTestContext::new(cx).await;
1012
1013 cx.set_shared_state(indoc! {
1014 "The ˇquick brown
1015 fox jumps over
1016 the lazy dog"
1017 })
1018 .await;
1019 cx.simulate_shared_keystrokes("ctrl-v").await;
1020 cx.shared_state().await.assert_eq(indoc! {
1021 "The «qˇ»uick brown
1022 fox jumps over
1023 the lazy dog"
1024 });
1025 cx.simulate_shared_keystrokes("2 down").await;
1026 cx.shared_state().await.assert_eq(indoc! {
1027 "The «qˇ»uick brown
1028 fox «jˇ»umps over
1029 the «lˇ»azy dog"
1030 });
1031 cx.simulate_shared_keystrokes("e").await;
1032 cx.shared_state().await.assert_eq(indoc! {
1033 "The «quicˇ»k brown
1034 fox «jumpˇ»s over
1035 the «lazyˇ» dog"
1036 });
1037 cx.simulate_shared_keystrokes("^").await;
1038 cx.shared_state().await.assert_eq(indoc! {
1039 "«ˇThe q»uick brown
1040 «ˇfox j»umps over
1041 «ˇthe l»azy dog"
1042 });
1043 cx.simulate_shared_keystrokes("$").await;
1044 cx.shared_state().await.assert_eq(indoc! {
1045 "The «quick brownˇ»
1046 fox «jumps overˇ»
1047 the «lazy dogˇ»"
1048 });
1049 cx.simulate_shared_keystrokes("shift-f space").await;
1050 cx.shared_state().await.assert_eq(indoc! {
1051 "The «quickˇ» brown
1052 fox «jumpsˇ» over
1053 the «lazy ˇ»dog"
1054 });
1055
1056 // toggling through visual mode works as expected
1057 cx.simulate_shared_keystrokes("v").await;
1058 cx.shared_state().await.assert_eq(indoc! {
1059 "The «quick brown
1060 fox jumps over
1061 the lazy ˇ»dog"
1062 });
1063 cx.simulate_shared_keystrokes("ctrl-v").await;
1064 cx.shared_state().await.assert_eq(indoc! {
1065 "The «quickˇ» brown
1066 fox «jumpsˇ» over
1067 the «lazy ˇ»dog"
1068 });
1069
1070 cx.set_shared_state(indoc! {
1071 "The ˇquick
1072 brown
1073 fox
1074 jumps over the
1075
1076 lazy dog
1077 "
1078 })
1079 .await;
1080 cx.simulate_shared_keystrokes("ctrl-v down down").await;
1081 cx.shared_state().await.assert_eq(indoc! {
1082 "The«ˇ q»uick
1083 bro«ˇwn»
1084 foxˇ
1085 jumps over the
1086
1087 lazy dog
1088 "
1089 });
1090 cx.simulate_shared_keystrokes("down").await;
1091 cx.shared_state().await.assert_eq(indoc! {
1092 "The «qˇ»uick
1093 brow«nˇ»
1094 fox
1095 jump«sˇ» over the
1096
1097 lazy dog
1098 "
1099 });
1100 cx.simulate_shared_keystrokes("left").await;
1101 cx.shared_state().await.assert_eq(indoc! {
1102 "The«ˇ q»uick
1103 bro«ˇwn»
1104 foxˇ
1105 jum«ˇps» over the
1106
1107 lazy dog
1108 "
1109 });
1110 cx.simulate_shared_keystrokes("s o escape").await;
1111 cx.shared_state().await.assert_eq(indoc! {
1112 "Theˇouick
1113 broo
1114 foxo
1115 jumo over the
1116
1117 lazy dog
1118 "
1119 });
1120
1121 // https://github.com/zed-industries/zed/issues/6274
1122 cx.set_shared_state(indoc! {
1123 "Theˇ quick brown
1124
1125 fox jumps over
1126 the lazy dog
1127 "
1128 })
1129 .await;
1130 cx.simulate_shared_keystrokes("l ctrl-v j j").await;
1131 cx.shared_state().await.assert_eq(indoc! {
1132 "The «qˇ»uick brown
1133
1134 fox «jˇ»umps over
1135 the lazy dog
1136 "
1137 });
1138 }
1139
1140 #[gpui::test]
1141 async fn test_visual_block_issue_2123(cx: &mut gpui::TestAppContext) {
1142 let mut cx = NeovimBackedTestContext::new(cx).await;
1143
1144 cx.set_shared_state(indoc! {
1145 "The ˇquick brown
1146 fox jumps over
1147 the lazy dog
1148 "
1149 })
1150 .await;
1151 cx.simulate_shared_keystrokes("ctrl-v right down").await;
1152 cx.shared_state().await.assert_eq(indoc! {
1153 "The «quˇ»ick brown
1154 fox «juˇ»mps over
1155 the lazy dog
1156 "
1157 });
1158 }
1159
1160 #[gpui::test]
1161 async fn test_visual_block_insert(cx: &mut gpui::TestAppContext) {
1162 let mut cx = NeovimBackedTestContext::new(cx).await;
1163
1164 cx.set_shared_state(indoc! {
1165 "ˇThe quick brown
1166 fox jumps over
1167 the lazy dog
1168 "
1169 })
1170 .await;
1171 cx.simulate_shared_keystrokes("ctrl-v 9 down").await;
1172 cx.shared_state().await.assert_eq(indoc! {
1173 "«Tˇ»he quick brown
1174 «fˇ»ox jumps over
1175 «tˇ»he lazy dog
1176 ˇ"
1177 });
1178
1179 cx.simulate_shared_keystrokes("shift-i k escape").await;
1180 cx.shared_state().await.assert_eq(indoc! {
1181 "ˇkThe quick brown
1182 kfox jumps over
1183 kthe lazy dog
1184 k"
1185 });
1186
1187 cx.set_shared_state(indoc! {
1188 "ˇThe quick brown
1189 fox jumps over
1190 the lazy dog
1191 "
1192 })
1193 .await;
1194 cx.simulate_shared_keystrokes("ctrl-v 9 down").await;
1195 cx.shared_state().await.assert_eq(indoc! {
1196 "«Tˇ»he quick brown
1197 «fˇ»ox jumps over
1198 «tˇ»he lazy dog
1199 ˇ"
1200 });
1201 cx.simulate_shared_keystrokes("c k escape").await;
1202 cx.shared_state().await.assert_eq(indoc! {
1203 "ˇkhe quick brown
1204 kox jumps over
1205 khe lazy dog
1206 k"
1207 });
1208 }
1209
1210 #[gpui::test]
1211 async fn test_visual_object(cx: &mut gpui::TestAppContext) {
1212 let mut cx = NeovimBackedTestContext::new(cx).await;
1213
1214 cx.set_shared_state("hello (in [parˇens] o)").await;
1215 cx.simulate_shared_keystrokes("ctrl-v l").await;
1216 cx.simulate_shared_keystrokes("a ]").await;
1217 cx.shared_state()
1218 .await
1219 .assert_eq("hello (in «[parens]ˇ» o)");
1220 cx.simulate_shared_keystrokes("i (").await;
1221 cx.shared_state()
1222 .await
1223 .assert_eq("hello («in [parens] oˇ»)");
1224
1225 cx.set_shared_state("hello in a wˇord again.").await;
1226 cx.simulate_shared_keystrokes("ctrl-v l i w").await;
1227 cx.shared_state()
1228 .await
1229 .assert_eq("hello in a w«ordˇ» again.");
1230 assert_eq!(cx.mode(), Mode::VisualBlock);
1231 cx.simulate_shared_keystrokes("o a s").await;
1232 cx.shared_state()
1233 .await
1234 .assert_eq("«ˇhello in a word» again.");
1235 }
1236
1237 #[gpui::test]
1238 async fn test_mode_across_command(cx: &mut gpui::TestAppContext) {
1239 let mut cx = VimTestContext::new(cx, true).await;
1240
1241 cx.set_state("aˇbc", Mode::Normal);
1242 cx.simulate_keystrokes("ctrl-v");
1243 assert_eq!(cx.mode(), Mode::VisualBlock);
1244 cx.simulate_keystrokes("cmd-shift-p escape");
1245 assert_eq!(cx.mode(), Mode::VisualBlock);
1246 }
1247
1248 #[gpui::test]
1249 async fn test_gn(cx: &mut gpui::TestAppContext) {
1250 let mut cx = NeovimBackedTestContext::new(cx).await;
1251
1252 cx.set_shared_state("aaˇ aa aa aa aa").await;
1253 cx.simulate_shared_keystrokes("/ a a enter").await;
1254 cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1255 cx.simulate_shared_keystrokes("g n").await;
1256 cx.shared_state().await.assert_eq("aa «aaˇ» aa aa aa");
1257 cx.simulate_shared_keystrokes("g n").await;
1258 cx.shared_state().await.assert_eq("aa «aa aaˇ» aa aa");
1259 cx.simulate_shared_keystrokes("escape d g n").await;
1260 cx.shared_state().await.assert_eq("aa aa ˇ aa aa");
1261
1262 cx.set_shared_state("aaˇ aa aa aa aa").await;
1263 cx.simulate_shared_keystrokes("/ a a enter").await;
1264 cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1265 cx.simulate_shared_keystrokes("3 g n").await;
1266 cx.shared_state().await.assert_eq("aa aa aa «aaˇ» aa");
1267
1268 cx.set_shared_state("aaˇ aa aa aa aa").await;
1269 cx.simulate_shared_keystrokes("/ a a enter").await;
1270 cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1271 cx.simulate_shared_keystrokes("g shift-n").await;
1272 cx.shared_state().await.assert_eq("aa «ˇaa» aa aa aa");
1273 cx.simulate_shared_keystrokes("g shift-n").await;
1274 cx.shared_state().await.assert_eq("«ˇaa aa» aa aa aa");
1275 }
1276
1277 #[gpui::test]
1278 async fn test_gl(cx: &mut gpui::TestAppContext) {
1279 let mut cx = VimTestContext::new(cx, true).await;
1280
1281 cx.set_state("aaˇ aa\naa", Mode::Normal);
1282 cx.simulate_keystrokes("g l");
1283 cx.assert_state("«aaˇ» «aaˇ»\naa", Mode::Visual);
1284 cx.simulate_keystrokes("g >");
1285 cx.assert_state("«aaˇ» aa\n«aaˇ»", Mode::Visual);
1286 }
1287
1288 #[gpui::test]
1289 async fn test_dgn_repeat(cx: &mut gpui::TestAppContext) {
1290 let mut cx = NeovimBackedTestContext::new(cx).await;
1291
1292 cx.set_shared_state("aaˇ aa aa aa aa").await;
1293 cx.simulate_shared_keystrokes("/ a a enter").await;
1294 cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1295 cx.simulate_shared_keystrokes("d g n").await;
1296
1297 cx.shared_state().await.assert_eq("aa ˇ aa aa aa");
1298 cx.simulate_shared_keystrokes(".").await;
1299 cx.shared_state().await.assert_eq("aa ˇ aa aa");
1300 cx.simulate_shared_keystrokes(".").await;
1301 cx.shared_state().await.assert_eq("aa ˇ aa");
1302 }
1303
1304 #[gpui::test]
1305 async fn test_cgn_repeat(cx: &mut gpui::TestAppContext) {
1306 let mut cx = NeovimBackedTestContext::new(cx).await;
1307
1308 cx.set_shared_state("aaˇ aa aa aa aa").await;
1309 cx.simulate_shared_keystrokes("/ a a enter").await;
1310 cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1311 cx.simulate_shared_keystrokes("c g n x escape").await;
1312 cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1313 cx.simulate_shared_keystrokes(".").await;
1314 cx.shared_state().await.assert_eq("aa x ˇx aa aa");
1315 }
1316
1317 #[gpui::test]
1318 async fn test_cgn_nomatch(cx: &mut gpui::TestAppContext) {
1319 let mut cx = NeovimBackedTestContext::new(cx).await;
1320
1321 cx.set_shared_state("aaˇ aa aa aa aa").await;
1322 cx.simulate_shared_keystrokes("/ b b enter").await;
1323 cx.shared_state().await.assert_eq("aaˇ aa aa aa aa");
1324 cx.simulate_shared_keystrokes("c g n x escape").await;
1325 cx.shared_state().await.assert_eq("aaˇaa aa aa aa");
1326 cx.simulate_shared_keystrokes(".").await;
1327 cx.shared_state().await.assert_eq("aaˇa aa aa aa");
1328
1329 cx.set_shared_state("aaˇ bb aa aa aa").await;
1330 cx.simulate_shared_keystrokes("/ b b enter").await;
1331 cx.shared_state().await.assert_eq("aa ˇbb aa aa aa");
1332 cx.simulate_shared_keystrokes("c g n x escape").await;
1333 cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1334 cx.simulate_shared_keystrokes(".").await;
1335 cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1336 }
1337
1338 #[gpui::test]
1339 async fn test_visual_shift_d(cx: &mut gpui::TestAppContext) {
1340 let mut cx = NeovimBackedTestContext::new(cx).await;
1341
1342 cx.set_shared_state(indoc! {
1343 "The ˇquick brown
1344 fox jumps over
1345 the lazy dog
1346 "
1347 })
1348 .await;
1349 cx.simulate_shared_keystrokes("v down shift-d").await;
1350 cx.shared_state().await.assert_eq(indoc! {
1351 "the ˇlazy dog\n"
1352 });
1353
1354 cx.set_shared_state(indoc! {
1355 "The ˇquick brown
1356 fox jumps over
1357 the lazy dog
1358 "
1359 })
1360 .await;
1361 cx.simulate_shared_keystrokes("ctrl-v down shift-d").await;
1362 cx.shared_state().await.assert_eq(indoc! {
1363 "Theˇ•
1364 fox•
1365 the lazy dog
1366 "
1367 });
1368 }
1369
1370 #[gpui::test]
1371 async fn test_gv(cx: &mut gpui::TestAppContext) {
1372 let mut cx = NeovimBackedTestContext::new(cx).await;
1373
1374 cx.set_shared_state(indoc! {
1375 "The ˇquick brown"
1376 })
1377 .await;
1378 cx.simulate_shared_keystrokes("v i w escape g v").await;
1379 cx.shared_state().await.assert_eq(indoc! {
1380 "The «quickˇ» brown"
1381 });
1382
1383 cx.simulate_shared_keystrokes("o escape g v").await;
1384 cx.shared_state().await.assert_eq(indoc! {
1385 "The «ˇquick» brown"
1386 });
1387
1388 cx.simulate_shared_keystrokes("escape ^ ctrl-v l").await;
1389 cx.shared_state().await.assert_eq(indoc! {
1390 "«Thˇ»e quick brown"
1391 });
1392 cx.simulate_shared_keystrokes("g v").await;
1393 cx.shared_state().await.assert_eq(indoc! {
1394 "The «ˇquick» brown"
1395 });
1396 cx.simulate_shared_keystrokes("g v").await;
1397 cx.shared_state().await.assert_eq(indoc! {
1398 "«Thˇ»e quick brown"
1399 });
1400
1401 cx.set_state(
1402 indoc! {"
1403 fiˇsh one
1404 fish two
1405 fish red
1406 fish blue
1407 "},
1408 Mode::Normal,
1409 );
1410 cx.simulate_keystrokes("4 g l escape escape g v");
1411 cx.assert_state(
1412 indoc! {"
1413 «fishˇ» one
1414 «fishˇ» two
1415 «fishˇ» red
1416 «fishˇ» blue
1417 "},
1418 Mode::Visual,
1419 );
1420 cx.simulate_keystrokes("y g v");
1421 cx.assert_state(
1422 indoc! {"
1423 «fishˇ» one
1424 «fishˇ» two
1425 «fishˇ» red
1426 «fishˇ» blue
1427 "},
1428 Mode::Visual,
1429 );
1430 }
1431}