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