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