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
513 if vim_is_normal {
514 pane.update(cx, |pane, cx| {
515 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
516 search_bar.update(cx, |search_bar, cx| {
517 // without update_match_index there is a bug when the cursor is before the first match
518 search_bar.update_match_index(cx);
519 search_bar.select_match(direction.opposite(), 1, cx);
520 });
521 }
522 });
523 }
524
525 vim.update_active_editor(cx, |_, editor, cx| {
526 let latest = editor.selections.newest::<usize>(cx);
527 start_selection = latest.start;
528 end_selection = latest.end;
529 });
530
531 pane.update(cx, |pane, cx| {
532 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
533 search_bar.update(cx, |search_bar, cx| {
534 search_bar.update_match_index(cx);
535 search_bar.select_match(direction, count, cx);
536 });
537 }
538 });
539 vim.update_active_editor(cx, |_, editor, cx| {
540 let latest = editor.selections.newest::<usize>(cx);
541 if vim_is_normal {
542 start_selection = latest.start;
543 end_selection = latest.end;
544 } else {
545 start_selection = start_selection.min(latest.start);
546 end_selection = end_selection.max(latest.end);
547 }
548 if direction == Direction::Prev {
549 std::mem::swap(&mut start_selection, &mut end_selection);
550 }
551 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
552 s.select_ranges([start_selection..end_selection]);
553 });
554 editor.set_collapse_matches(true);
555 });
556 match vim.maybe_pop_operator() {
557 Some(Operator::Change) => substitute(vim, None, false, cx),
558 Some(Operator::Delete) => {
559 vim.stop_recording();
560 delete(vim, cx)
561 }
562 Some(Operator::Yank) => yank(vim, cx),
563 _ => {} // Ignoring other operators
564 };
565}
566
567#[cfg(test)]
568mod test {
569 use indoc::indoc;
570 use workspace::item::Item;
571
572 use crate::{
573 state::Mode,
574 test::{NeovimBackedTestContext, VimTestContext},
575 };
576
577 #[gpui::test]
578 async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
579 let mut cx = NeovimBackedTestContext::new(cx).await;
580
581 cx.set_shared_state(indoc! {
582 "The ˇquick brown
583 fox jumps over
584 the lazy dog"
585 })
586 .await;
587 let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
588
589 // entering visual mode should select the character
590 // under cursor
591 cx.simulate_shared_keystrokes(["v"]).await;
592 cx.assert_shared_state(indoc! { "The «qˇ»uick brown
593 fox jumps over
594 the lazy dog"})
595 .await;
596 cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
597
598 // forwards motions should extend the selection
599 cx.simulate_shared_keystrokes(["w", "j"]).await;
600 cx.assert_shared_state(indoc! { "The «quick brown
601 fox jumps oˇ»ver
602 the lazy dog"})
603 .await;
604
605 cx.simulate_shared_keystrokes(["escape"]).await;
606 assert_eq!(Mode::Normal, cx.neovim_mode().await);
607 cx.assert_shared_state(indoc! { "The quick brown
608 fox jumps ˇover
609 the lazy dog"})
610 .await;
611
612 // motions work backwards
613 cx.simulate_shared_keystrokes(["v", "k", "b"]).await;
614 cx.assert_shared_state(indoc! { "The «ˇquick brown
615 fox jumps o»ver
616 the lazy dog"})
617 .await;
618
619 // works on empty lines
620 cx.set_shared_state(indoc! {"
621 a
622 ˇ
623 b
624 "})
625 .await;
626 let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
627 cx.simulate_shared_keystrokes(["v"]).await;
628 cx.assert_shared_state(indoc! {"
629 a
630 «
631 ˇ»b
632 "})
633 .await;
634 cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
635
636 // toggles off again
637 cx.simulate_shared_keystrokes(["v"]).await;
638 cx.assert_shared_state(indoc! {"
639 a
640 ˇ
641 b
642 "})
643 .await;
644
645 // works at the end of a document
646 cx.set_shared_state(indoc! {"
647 a
648 b
649 ˇ"})
650 .await;
651
652 cx.simulate_shared_keystrokes(["v"]).await;
653 cx.assert_shared_state(indoc! {"
654 a
655 b
656 ˇ"})
657 .await;
658 assert_eq!(cx.mode(), cx.neovim_mode().await);
659 }
660
661 #[gpui::test]
662 async fn test_enter_visual_line_mode(cx: &mut gpui::TestAppContext) {
663 let mut cx = NeovimBackedTestContext::new(cx).await;
664
665 cx.set_shared_state(indoc! {
666 "The ˇquick brown
667 fox jumps over
668 the lazy dog"
669 })
670 .await;
671 cx.simulate_shared_keystrokes(["shift-v"]).await;
672 cx.assert_shared_state(indoc! { "The «qˇ»uick brown
673 fox jumps over
674 the lazy dog"})
675 .await;
676 assert_eq!(cx.mode(), cx.neovim_mode().await);
677 cx.simulate_shared_keystrokes(["x"]).await;
678 cx.assert_shared_state(indoc! { "fox ˇjumps over
679 the lazy dog"})
680 .await;
681
682 // it should work on empty lines
683 cx.set_shared_state(indoc! {"
684 a
685 ˇ
686 b"})
687 .await;
688 cx.simulate_shared_keystrokes(["shift-v"]).await;
689 cx.assert_shared_state(indoc! { "
690 a
691 «
692 ˇ»b"})
693 .await;
694 cx.simulate_shared_keystrokes(["x"]).await;
695 cx.assert_shared_state(indoc! { "
696 a
697 ˇb"})
698 .await;
699
700 // it should work at the end of the document
701 cx.set_shared_state(indoc! {"
702 a
703 b
704 ˇ"})
705 .await;
706 let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
707 cx.simulate_shared_keystrokes(["shift-v"]).await;
708 cx.assert_shared_state(indoc! {"
709 a
710 b
711 ˇ"})
712 .await;
713 assert_eq!(cx.mode(), cx.neovim_mode().await);
714 cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
715 cx.simulate_shared_keystrokes(["x"]).await;
716 cx.assert_shared_state(indoc! {"
717 a
718 ˇb"})
719 .await;
720 }
721
722 #[gpui::test]
723 async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
724 let mut cx = NeovimBackedTestContext::new(cx).await;
725
726 cx.assert_binding_matches(["v", "w"], "The quick ˇbrown")
727 .await;
728
729 cx.assert_binding_matches(["v", "w", "x"], "The quick ˇbrown")
730 .await;
731 cx.assert_binding_matches(
732 ["v", "w", "j", "x"],
733 indoc! {"
734 The ˇquick brown
735 fox jumps over
736 the lazy dog"},
737 )
738 .await;
739 // Test pasting code copied on delete
740 cx.simulate_shared_keystrokes(["j", "p"]).await;
741 cx.assert_state_matches().await;
742
743 let mut cx = cx.binding(["v", "w", "j", "x"]);
744 cx.assert_all(indoc! {"
745 The ˇquick brown
746 fox jumps over
747 the ˇlazy dog"})
748 .await;
749 let mut cx = cx.binding(["v", "b", "k", "x"]);
750 cx.assert_all(indoc! {"
751 The ˇquick brown
752 fox jumps ˇover
753 the ˇlazy dog"})
754 .await;
755 }
756
757 #[gpui::test]
758 async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
759 let mut cx = NeovimBackedTestContext::new(cx).await;
760
761 cx.set_shared_state(indoc! {"
762 The quˇick brown
763 fox jumps over
764 the lazy dog"})
765 .await;
766 cx.simulate_shared_keystrokes(["shift-v", "x"]).await;
767 cx.assert_state_matches().await;
768
769 // Test pasting code copied on delete
770 cx.simulate_shared_keystroke("p").await;
771 cx.assert_state_matches().await;
772
773 cx.set_shared_state(indoc! {"
774 The quick brown
775 fox jumps over
776 the laˇzy dog"})
777 .await;
778 cx.simulate_shared_keystrokes(["shift-v", "x"]).await;
779 cx.assert_state_matches().await;
780 cx.assert_shared_clipboard("the lazy dog\n").await;
781
782 for marked_text in cx.each_marked_position(indoc! {"
783 The quˇick brown
784 fox jumps over
785 the lazy dog"})
786 {
787 cx.set_shared_state(&marked_text).await;
788 cx.simulate_shared_keystrokes(["shift-v", "j", "x"]).await;
789 cx.assert_state_matches().await;
790 // Test pasting code copied on delete
791 cx.simulate_shared_keystroke("p").await;
792 cx.assert_state_matches().await;
793 }
794
795 cx.set_shared_state(indoc! {"
796 The ˇlong line
797 should not
798 crash
799 "})
800 .await;
801 cx.simulate_shared_keystrokes(["shift-v", "$", "x"]).await;
802 cx.assert_state_matches().await;
803 }
804
805 #[gpui::test]
806 async fn test_visual_yank(cx: &mut gpui::TestAppContext) {
807 let mut cx = NeovimBackedTestContext::new(cx).await;
808
809 cx.set_shared_state("The quick ˇbrown").await;
810 cx.simulate_shared_keystrokes(["v", "w", "y"]).await;
811 cx.assert_shared_state("The quick ˇbrown").await;
812 cx.assert_shared_clipboard("brown").await;
813
814 cx.set_shared_state(indoc! {"
815 The ˇquick brown
816 fox jumps over
817 the lazy dog"})
818 .await;
819 cx.simulate_shared_keystrokes(["v", "w", "j", "y"]).await;
820 cx.assert_shared_state(indoc! {"
821 The ˇquick brown
822 fox jumps over
823 the lazy dog"})
824 .await;
825 cx.assert_shared_clipboard(indoc! {"
826 quick brown
827 fox jumps o"})
828 .await;
829
830 cx.set_shared_state(indoc! {"
831 The quick brown
832 fox jumps over
833 the ˇlazy dog"})
834 .await;
835 cx.simulate_shared_keystrokes(["v", "w", "j", "y"]).await;
836 cx.assert_shared_state(indoc! {"
837 The quick brown
838 fox jumps over
839 the ˇlazy dog"})
840 .await;
841 cx.assert_shared_clipboard("lazy d").await;
842 cx.simulate_shared_keystrokes(["shift-v", "y"]).await;
843 cx.assert_shared_clipboard("the lazy dog\n").await;
844
845 let mut cx = cx.binding(["v", "b", "k", "y"]);
846 cx.set_shared_state(indoc! {"
847 The ˇquick brown
848 fox jumps over
849 the lazy dog"})
850 .await;
851 cx.simulate_shared_keystrokes(["v", "b", "k", "y"]).await;
852 cx.assert_shared_state(indoc! {"
853 ˇThe quick brown
854 fox jumps over
855 the lazy dog"})
856 .await;
857 assert_eq!(
858 cx.read_from_clipboard()
859 .map(|item| item.text().clone())
860 .unwrap(),
861 "The q"
862 );
863
864 cx.set_shared_state(indoc! {"
865 The quick brown
866 fox ˇjumps over
867 the lazy dog"})
868 .await;
869 cx.simulate_shared_keystrokes(["shift-v", "shift-g", "shift-y"])
870 .await;
871 cx.assert_shared_state(indoc! {"
872 The quick brown
873 ˇfox jumps over
874 the lazy dog"})
875 .await;
876 cx.assert_shared_clipboard("fox jumps over\nthe lazy dog\n")
877 .await;
878 }
879
880 #[gpui::test]
881 async fn test_visual_block_mode(cx: &mut gpui::TestAppContext) {
882 let mut cx = NeovimBackedTestContext::new(cx).await;
883
884 cx.set_shared_state(indoc! {
885 "The ˇquick brown
886 fox jumps over
887 the lazy dog"
888 })
889 .await;
890 cx.simulate_shared_keystrokes(["ctrl-v"]).await;
891 cx.assert_shared_state(indoc! {
892 "The «qˇ»uick brown
893 fox jumps over
894 the lazy dog"
895 })
896 .await;
897 cx.simulate_shared_keystrokes(["2", "down"]).await;
898 cx.assert_shared_state(indoc! {
899 "The «qˇ»uick brown
900 fox «jˇ»umps over
901 the «lˇ»azy dog"
902 })
903 .await;
904 cx.simulate_shared_keystrokes(["e"]).await;
905 cx.assert_shared_state(indoc! {
906 "The «quicˇ»k brown
907 fox «jumpˇ»s over
908 the «lazyˇ» dog"
909 })
910 .await;
911 cx.simulate_shared_keystrokes(["^"]).await;
912 cx.assert_shared_state(indoc! {
913 "«ˇThe q»uick brown
914 «ˇfox j»umps over
915 «ˇthe l»azy dog"
916 })
917 .await;
918 cx.simulate_shared_keystrokes(["$"]).await;
919 cx.assert_shared_state(indoc! {
920 "The «quick brownˇ»
921 fox «jumps overˇ»
922 the «lazy dogˇ»"
923 })
924 .await;
925 cx.simulate_shared_keystrokes(["shift-f", " "]).await;
926 cx.assert_shared_state(indoc! {
927 "The «quickˇ» brown
928 fox «jumpsˇ» over
929 the «lazy ˇ»dog"
930 })
931 .await;
932
933 // toggling through visual mode works as expected
934 cx.simulate_shared_keystrokes(["v"]).await;
935 cx.assert_shared_state(indoc! {
936 "The «quick brown
937 fox jumps over
938 the lazy ˇ»dog"
939 })
940 .await;
941 cx.simulate_shared_keystrokes(["ctrl-v"]).await;
942 cx.assert_shared_state(indoc! {
943 "The «quickˇ» brown
944 fox «jumpsˇ» over
945 the «lazy ˇ»dog"
946 })
947 .await;
948
949 cx.set_shared_state(indoc! {
950 "The ˇquick
951 brown
952 fox
953 jumps over the
954
955 lazy dog
956 "
957 })
958 .await;
959 cx.simulate_shared_keystrokes(["ctrl-v", "down", "down"])
960 .await;
961 cx.assert_shared_state(indoc! {
962 "The«ˇ q»uick
963 bro«ˇwn»
964 foxˇ
965 jumps over the
966
967 lazy dog
968 "
969 })
970 .await;
971 cx.simulate_shared_keystrokes(["down"]).await;
972 cx.assert_shared_state(indoc! {
973 "The «qˇ»uick
974 brow«nˇ»
975 fox
976 jump«sˇ» over the
977
978 lazy dog
979 "
980 })
981 .await;
982 cx.simulate_shared_keystroke("left").await;
983 cx.assert_shared_state(indoc! {
984 "The«ˇ q»uick
985 bro«ˇwn»
986 foxˇ
987 jum«ˇps» over the
988
989 lazy dog
990 "
991 })
992 .await;
993 cx.simulate_shared_keystrokes(["s", "o", "escape"]).await;
994 cx.assert_shared_state(indoc! {
995 "Theˇouick
996 broo
997 foxo
998 jumo over the
999
1000 lazy dog
1001 "
1002 })
1003 .await;
1004
1005 // https://github.com/zed-industries/zed/issues/6274
1006 cx.set_shared_state(indoc! {
1007 "Theˇ quick brown
1008
1009 fox jumps over
1010 the lazy dog
1011 "
1012 })
1013 .await;
1014 cx.simulate_shared_keystrokes(["l", "ctrl-v", "j", "j"])
1015 .await;
1016 cx.assert_shared_state(indoc! {
1017 "The «qˇ»uick brown
1018
1019 fox «jˇ»umps over
1020 the lazy dog
1021 "
1022 })
1023 .await;
1024 }
1025
1026 #[gpui::test]
1027 async fn test_visual_block_issue_2123(cx: &mut gpui::TestAppContext) {
1028 let mut cx = NeovimBackedTestContext::new(cx).await;
1029
1030 cx.set_shared_state(indoc! {
1031 "The ˇquick brown
1032 fox jumps over
1033 the lazy dog
1034 "
1035 })
1036 .await;
1037 cx.simulate_shared_keystrokes(["ctrl-v", "right", "down"])
1038 .await;
1039 cx.assert_shared_state(indoc! {
1040 "The «quˇ»ick brown
1041 fox «juˇ»mps over
1042 the lazy dog
1043 "
1044 })
1045 .await;
1046 }
1047
1048 #[gpui::test]
1049 async fn test_visual_block_insert(cx: &mut gpui::TestAppContext) {
1050 let mut cx = NeovimBackedTestContext::new(cx).await;
1051
1052 cx.set_shared_state(indoc! {
1053 "ˇThe quick brown
1054 fox jumps over
1055 the lazy dog
1056 "
1057 })
1058 .await;
1059 cx.simulate_shared_keystrokes(["ctrl-v", "9", "down"]).await;
1060 cx.assert_shared_state(indoc! {
1061 "«Tˇ»he quick brown
1062 «fˇ»ox jumps over
1063 «tˇ»he lazy dog
1064 ˇ"
1065 })
1066 .await;
1067
1068 cx.simulate_shared_keystrokes(["shift-i", "k", "escape"])
1069 .await;
1070 cx.assert_shared_state(indoc! {
1071 "ˇkThe quick brown
1072 kfox jumps over
1073 kthe lazy dog
1074 k"
1075 })
1076 .await;
1077
1078 cx.set_shared_state(indoc! {
1079 "ˇThe quick brown
1080 fox jumps over
1081 the lazy dog
1082 "
1083 })
1084 .await;
1085 cx.simulate_shared_keystrokes(["ctrl-v", "9", "down"]).await;
1086 cx.assert_shared_state(indoc! {
1087 "«Tˇ»he quick brown
1088 «fˇ»ox jumps over
1089 «tˇ»he lazy dog
1090 ˇ"
1091 })
1092 .await;
1093 cx.simulate_shared_keystrokes(["c", "k", "escape"]).await;
1094 cx.assert_shared_state(indoc! {
1095 "ˇkhe quick brown
1096 kox jumps over
1097 khe lazy dog
1098 k"
1099 })
1100 .await;
1101 }
1102
1103 #[gpui::test]
1104 async fn test_visual_object(cx: &mut gpui::TestAppContext) {
1105 let mut cx = NeovimBackedTestContext::new(cx).await;
1106
1107 cx.set_shared_state("hello (in [parˇens] o)").await;
1108 cx.simulate_shared_keystrokes(["ctrl-v", "l"]).await;
1109 cx.simulate_shared_keystrokes(["a", "]"]).await;
1110 cx.assert_shared_state("hello (in «[parens]ˇ» o)").await;
1111 cx.simulate_shared_keystrokes(["i", "("]).await;
1112 cx.assert_shared_state("hello («in [parens] oˇ»)").await;
1113
1114 cx.set_shared_state("hello in a wˇord again.").await;
1115 cx.simulate_shared_keystrokes(["ctrl-v", "l", "i", "w"])
1116 .await;
1117 cx.assert_shared_state("hello in a w«ordˇ» again.").await;
1118 assert_eq!(cx.mode(), Mode::VisualBlock);
1119 cx.simulate_shared_keystrokes(["o", "a", "s"]).await;
1120 cx.assert_shared_state("«ˇhello in a word» again.").await;
1121 }
1122
1123 #[gpui::test]
1124 async fn test_mode_across_command(cx: &mut gpui::TestAppContext) {
1125 let mut cx = VimTestContext::new(cx, true).await;
1126
1127 cx.set_state("aˇbc", Mode::Normal);
1128 cx.simulate_keystrokes(["ctrl-v"]);
1129 assert_eq!(cx.mode(), Mode::VisualBlock);
1130 cx.simulate_keystrokes(["cmd-shift-p", "escape"]);
1131 assert_eq!(cx.mode(), Mode::VisualBlock);
1132 }
1133
1134 #[gpui::test]
1135 async fn test_gn(cx: &mut gpui::TestAppContext) {
1136 let mut cx = NeovimBackedTestContext::new(cx).await;
1137
1138 cx.set_shared_state("aaˇ aa aa aa aa").await;
1139 cx.simulate_shared_keystrokes(["/", "a", "a", "enter"])
1140 .await;
1141 cx.assert_shared_state("aa ˇaa aa aa aa").await;
1142 cx.simulate_shared_keystrokes(["g", "n"]).await;
1143 cx.assert_shared_state("aa «aaˇ» aa aa aa").await;
1144 cx.simulate_shared_keystrokes(["g", "n"]).await;
1145 cx.assert_shared_state("aa «aa aaˇ» aa aa").await;
1146 cx.simulate_shared_keystrokes(["escape", "d", "g", "n"])
1147 .await;
1148 cx.assert_shared_state("aa aa ˇ aa aa").await;
1149
1150 cx.set_shared_state("aaˇ aa aa aa aa").await;
1151 cx.simulate_shared_keystrokes(["/", "a", "a", "enter"])
1152 .await;
1153 cx.assert_shared_state("aa ˇaa aa aa aa").await;
1154 cx.simulate_shared_keystrokes(["3", "g", "n"]).await;
1155 cx.assert_shared_state("aa aa aa «aaˇ» aa").await;
1156
1157 cx.set_shared_state("aaˇ aa aa aa aa").await;
1158 cx.simulate_shared_keystrokes(["/", "a", "a", "enter"])
1159 .await;
1160 cx.assert_shared_state("aa ˇaa aa aa aa").await;
1161 cx.simulate_shared_keystrokes(["g", "shift-n"]).await;
1162 cx.assert_shared_state("aa «ˇaa» aa aa aa").await;
1163 cx.simulate_shared_keystrokes(["g", "shift-n"]).await;
1164 cx.assert_shared_state("«ˇaa aa» aa aa aa").await;
1165 }
1166
1167 #[gpui::test]
1168 async fn test_dgn_repeat(cx: &mut gpui::TestAppContext) {
1169 let mut cx = NeovimBackedTestContext::new(cx).await;
1170
1171 cx.set_shared_state("aaˇ aa aa aa aa").await;
1172 cx.simulate_shared_keystrokes(["/", "a", "a", "enter"])
1173 .await;
1174 cx.assert_shared_state("aa ˇaa aa aa aa").await;
1175 cx.simulate_shared_keystrokes(["d", "g", "n"]).await;
1176
1177 cx.assert_shared_state("aa ˇ aa aa aa").await;
1178 cx.simulate_shared_keystrokes(["."]).await;
1179 cx.assert_shared_state("aa ˇ aa aa").await;
1180 cx.simulate_shared_keystrokes(["."]).await;
1181 cx.assert_shared_state("aa ˇ aa").await;
1182 }
1183
1184 #[gpui::test]
1185 async fn test_cgn_repeat(cx: &mut gpui::TestAppContext) {
1186 let mut cx = NeovimBackedTestContext::new(cx).await;
1187
1188 cx.set_shared_state("aaˇ aa aa aa aa").await;
1189 cx.simulate_shared_keystrokes(["/", "a", "a", "enter"])
1190 .await;
1191 cx.assert_shared_state("aa ˇaa aa aa aa").await;
1192 cx.simulate_shared_keystrokes(["c", "g", "n", "x", "escape"])
1193 .await;
1194 cx.assert_shared_state("aa ˇx aa aa aa").await;
1195 cx.simulate_shared_keystrokes(["."]).await;
1196 cx.assert_shared_state("aa x ˇx aa aa").await;
1197 }
1198}