1use anyhow::Result;
2use std::{cmp, sync::Arc};
3
4use collections::HashMap;
5use editor::{
6 display_map::{DisplaySnapshot, ToDisplayPoint},
7 movement::{self, TextLayoutDetails},
8 scroll::autoscroll::Autoscroll,
9 Bias, DisplayPoint, Editor,
10};
11use gpui::{actions, AppContext, ViewContext, WindowContext};
12use language::{Selection, SelectionGoal};
13use workspace::Workspace;
14
15use crate::{
16 motion::{start_of_line, Motion},
17 object::Object,
18 state::{Mode, Operator},
19 utils::copy_selections_content,
20 Vim,
21};
22
23actions!(
24 vim,
25 [
26 ToggleVisual,
27 ToggleVisualLine,
28 ToggleVisualBlock,
29 VisualDelete,
30 VisualYank,
31 OtherEnd,
32 SelectNext,
33 SelectPrevious,
34 ]
35);
36
37pub fn init(cx: &mut AppContext) {
38 cx.add_action(|_, _: &ToggleVisual, cx: &mut ViewContext<Workspace>| {
39 toggle_mode(Mode::Visual, cx)
40 });
41 cx.add_action(|_, _: &ToggleVisualLine, cx: &mut ViewContext<Workspace>| {
42 toggle_mode(Mode::VisualLine, cx)
43 });
44 cx.add_action(
45 |_, _: &ToggleVisualBlock, cx: &mut ViewContext<Workspace>| {
46 toggle_mode(Mode::VisualBlock, cx)
47 },
48 );
49 cx.add_action(other_end);
50 cx.add_action(delete);
51 cx.add_action(yank);
52
53 cx.add_action(select_next);
54 cx.add_action(select_previous);
55}
56
57pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
58 Vim::update(cx, |vim, cx| {
59 vim.update_active_editor(cx, |editor, cx| {
60 let text_layout_details = TextLayoutDetails::new(editor, cx);
61 if vim.state().mode == Mode::VisualBlock
62 && !matches!(
63 motion,
64 Motion::EndOfLine {
65 display_lines: false
66 }
67 )
68 {
69 let is_up_or_down = matches!(motion, Motion::Up { .. } | Motion::Down { .. });
70 visual_block_motion(is_up_or_down, editor, cx, |map, point, goal| {
71 motion.move_point(map, point, goal, times, &text_layout_details)
72 })
73 } else {
74 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
75 s.move_with(|map, selection| {
76 let was_reversed = selection.reversed;
77 let mut current_head = selection.head();
78
79 // our motions assume the current character is after the cursor,
80 // but in (forward) visual mode the current character is just
81 // before the end of the selection.
82
83 // If the file ends with a newline (which is common) we don't do this.
84 // so that if you go to the end of such a file you can use "up" to go
85 // to the previous line and have it work somewhat as expected.
86 if !selection.reversed
87 && !selection.is_empty()
88 && !(selection.end.column() == 0 && selection.end == map.max_point())
89 {
90 current_head = movement::left(map, selection.end)
91 }
92
93 let Some((new_head, goal)) = motion.move_point(
94 map,
95 current_head,
96 selection.goal,
97 times,
98 &text_layout_details,
99 ) else {
100 return;
101 };
102
103 selection.set_head(new_head, goal);
104
105 // ensure the current character is included in the selection.
106 if !selection.reversed {
107 let next_point = if vim.state().mode == Mode::VisualBlock {
108 movement::saturating_right(map, selection.end)
109 } else {
110 movement::right(map, selection.end)
111 };
112
113 if !(next_point.column() == 0 && next_point == map.max_point()) {
114 selection.end = next_point;
115 }
116 }
117
118 // vim always ensures the anchor character stays selected.
119 // if our selection has reversed, we need to move the opposite end
120 // to ensure the anchor is still selected.
121 if was_reversed && !selection.reversed {
122 selection.start = movement::left(map, selection.start);
123 } else if !was_reversed && selection.reversed {
124 selection.end = movement::right(map, selection.end);
125 }
126 })
127 });
128 }
129 });
130 });
131}
132
133pub fn visual_block_motion(
134 preserve_goal: bool,
135 editor: &mut Editor,
136 cx: &mut ViewContext<Editor>,
137 mut move_selection: impl FnMut(
138 &DisplaySnapshot,
139 DisplayPoint,
140 SelectionGoal,
141 ) -> Option<(DisplayPoint, SelectionGoal)>,
142) {
143 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
144 let map = &s.display_map();
145 let mut head = s.newest_anchor().head().to_display_point(map);
146 let mut tail = s.oldest_anchor().tail().to_display_point(map);
147
148 let (start, end) = match s.newest_anchor().goal {
149 SelectionGoal::ColumnRange { start, end } if preserve_goal => (start, end),
150 SelectionGoal::Column(start) if preserve_goal => (start, start + 1),
151 _ => (tail.column(), head.column()),
152 };
153 let goal = SelectionGoal::ColumnRange { start, end };
154
155 let was_reversed = tail.column() > head.column();
156 if !was_reversed && !preserve_goal {
157 head = movement::saturating_left(map, head);
158 }
159
160 let Some((new_head, _)) = move_selection(&map, head, goal) else {
161 return;
162 };
163 head = new_head;
164
165 let is_reversed = tail.column() > head.column();
166 if was_reversed && !is_reversed {
167 tail = movement::left(map, tail)
168 } else if !was_reversed && is_reversed {
169 tail = movement::right(map, tail)
170 }
171 if !is_reversed && !preserve_goal {
172 head = movement::saturating_right(map, head)
173 }
174
175 let columns = if is_reversed {
176 head.column()..tail.column()
177 } else if head.column() == tail.column() {
178 head.column()..(head.column() + 1)
179 } else {
180 tail.column()..head.column()
181 };
182
183 let mut selections = Vec::new();
184 let mut row = tail.row();
185
186 loop {
187 let start = map.clip_point(DisplayPoint::new(row, columns.start), Bias::Left);
188 let end = map.clip_point(DisplayPoint::new(row, columns.end), Bias::Left);
189 if columns.start <= map.line_len(row) {
190 let selection = Selection {
191 id: s.new_selection_id(),
192 start: start.to_point(map),
193 end: end.to_point(map),
194 reversed: is_reversed,
195 goal: goal.clone(),
196 };
197
198 selections.push(selection);
199 }
200 if row == head.row() {
201 break;
202 }
203 if tail.row() > head.row() {
204 row -= 1
205 } else {
206 row += 1
207 }
208 }
209
210 s.select(selections);
211 })
212}
213
214pub fn visual_object(object: Object, cx: &mut WindowContext) {
215 Vim::update(cx, |vim, cx| {
216 if let Some(Operator::Object { around }) = vim.active_operator() {
217 vim.pop_operator(cx);
218 let current_mode = vim.state().mode;
219 let target_mode = object.target_visual_mode(current_mode);
220 if target_mode != current_mode {
221 vim.switch_mode(target_mode, true, cx);
222 }
223
224 vim.update_active_editor(cx, |editor, cx| {
225 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
226 s.move_with(|map, selection| {
227 let mut head = selection.head();
228
229 // all our motions assume that the current character is
230 // after the cursor; however in the case of a visual selection
231 // the current character is before the cursor.
232 if !selection.reversed {
233 head = movement::left(map, head);
234 }
235
236 if let Some(range) = object.range(map, head, around) {
237 if !range.is_empty() {
238 let expand_both_ways =
239 if object.always_expands_both_ways() || selection.is_empty() {
240 true
241 // contains only one character
242 } else if let Some((_, start)) =
243 map.reverse_chars_at(selection.end).next()
244 {
245 selection.start == start
246 } else {
247 false
248 };
249
250 if expand_both_ways {
251 selection.start = cmp::min(selection.start, range.start);
252 selection.end = cmp::max(selection.end, range.end);
253 } else if selection.reversed {
254 selection.start = range.start;
255 } else {
256 selection.end = range.end;
257 }
258 }
259 }
260 });
261 });
262 });
263 }
264 });
265}
266
267fn toggle_mode(mode: Mode, cx: &mut ViewContext<Workspace>) {
268 Vim::update(cx, |vim, cx| {
269 if vim.state().mode == mode {
270 vim.switch_mode(Mode::Normal, false, cx);
271 } else {
272 vim.switch_mode(mode, false, cx);
273 }
274 })
275}
276
277pub fn other_end(_: &mut Workspace, _: &OtherEnd, cx: &mut ViewContext<Workspace>) {
278 Vim::update(cx, |vim, cx| {
279 vim.update_active_editor(cx, |editor, cx| {
280 editor.change_selections(None, cx, |s| {
281 s.move_with(|_, selection| {
282 selection.reversed = !selection.reversed;
283 })
284 })
285 })
286 });
287}
288
289pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspace>) {
290 Vim::update(cx, |vim, cx| {
291 vim.record_current_action(cx);
292 vim.update_active_editor(cx, |editor, cx| {
293 let mut original_columns: HashMap<_, _> = Default::default();
294 let line_mode = editor.selections.line_mode;
295
296 editor.transact(cx, |editor, cx| {
297 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
298 s.move_with(|map, selection| {
299 if line_mode {
300 let mut position = selection.head();
301 if !selection.reversed {
302 position = movement::left(map, position);
303 }
304 original_columns.insert(selection.id, position.to_point(map).column);
305 }
306 selection.goal = SelectionGoal::None;
307 });
308 });
309 copy_selections_content(editor, line_mode, cx);
310 editor.insert("", cx);
311
312 // Fixup cursor position after the deletion
313 editor.set_clip_at_line_ends(true, cx);
314 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
315 s.move_with(|map, selection| {
316 let mut cursor = selection.head().to_point(map);
317
318 if let Some(column) = original_columns.get(&selection.id) {
319 cursor.column = *column
320 }
321 let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left);
322 selection.collapse_to(cursor, selection.goal)
323 });
324 if vim.state().mode == Mode::VisualBlock {
325 s.select_anchors(vec![s.first_anchor()])
326 }
327 });
328 })
329 });
330 vim.switch_mode(Mode::Normal, true, cx);
331 });
332}
333
334pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext<Workspace>) {
335 Vim::update(cx, |vim, cx| {
336 vim.update_active_editor(cx, |editor, cx| {
337 let line_mode = editor.selections.line_mode;
338 copy_selections_content(editor, line_mode, cx);
339 editor.change_selections(None, cx, |s| {
340 s.move_with(|map, selection| {
341 if line_mode {
342 selection.start = start_of_line(map, false, selection.start);
343 };
344 selection.collapse_to(selection.start, SelectionGoal::None)
345 });
346 if vim.state().mode == Mode::VisualBlock {
347 s.select_anchors(vec![s.first_anchor()])
348 }
349 });
350 });
351 vim.switch_mode(Mode::Normal, true, cx);
352 });
353}
354
355pub(crate) fn visual_replace(text: Arc<str>, cx: &mut WindowContext) {
356 Vim::update(cx, |vim, cx| {
357 vim.stop_recording();
358 vim.update_active_editor(cx, |editor, cx| {
359 editor.transact(cx, |editor, cx| {
360 let (display_map, selections) = editor.selections.all_adjusted_display(cx);
361
362 // Selections are biased right at the start. So we need to store
363 // anchors that are biased left so that we can restore the selections
364 // after the change
365 let stable_anchors = editor
366 .selections
367 .disjoint_anchors()
368 .into_iter()
369 .map(|selection| {
370 let start = selection.start.bias_left(&display_map.buffer_snapshot);
371 start..start
372 })
373 .collect::<Vec<_>>();
374
375 let mut edits = Vec::new();
376 for selection in selections.iter() {
377 let selection = selection.clone();
378 for row_range in
379 movement::split_display_range_by_lines(&display_map, selection.range())
380 {
381 let range = row_range.start.to_offset(&display_map, Bias::Right)
382 ..row_range.end.to_offset(&display_map, Bias::Right);
383 let text = text.repeat(range.len());
384 edits.push((range, text));
385 }
386 }
387
388 editor.buffer().update(cx, |buffer, cx| {
389 buffer.edit(edits, None, cx);
390 });
391 editor.change_selections(None, cx, |s| s.select_ranges(stable_anchors));
392 });
393 });
394 vim.switch_mode(Mode::Normal, false, cx);
395 });
396}
397
398pub fn select_next(
399 _: &mut Workspace,
400 _: &SelectNext,
401 cx: &mut ViewContext<Workspace>,
402) -> Result<()> {
403 Vim::update(cx, |vim, cx| {
404 let count =
405 vim.take_count(cx)
406 .unwrap_or_else(|| if vim.state().mode.is_visual() { 1 } else { 2 });
407 vim.update_active_editor(cx, |editor, cx| {
408 for _ in 0..count {
409 match editor.select_next(&Default::default(), cx) {
410 Err(a) => return Err(a),
411 _ => {}
412 }
413 }
414 Ok(())
415 })
416 })
417 .unwrap_or(Ok(()))
418}
419
420pub fn select_previous(
421 _: &mut Workspace,
422 _: &SelectPrevious,
423 cx: &mut ViewContext<Workspace>,
424) -> Result<()> {
425 Vim::update(cx, |vim, cx| {
426 let count =
427 vim.take_count(cx)
428 .unwrap_or_else(|| if vim.state().mode.is_visual() { 1 } else { 2 });
429 vim.update_active_editor(cx, |editor, cx| {
430 for _ in 0..count {
431 match editor.select_previous(&Default::default(), cx) {
432 Err(a) => return Err(a),
433 _ => {}
434 }
435 }
436 Ok(())
437 })
438 })
439 .unwrap_or(Ok(()))
440}
441
442#[cfg(test)]
443mod test {
444 use indoc::indoc;
445 use workspace::item::Item;
446
447 use crate::{
448 state::Mode,
449 test::{NeovimBackedTestContext, VimTestContext},
450 };
451
452 #[gpui::test]
453 async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
454 let mut cx = NeovimBackedTestContext::new(cx).await;
455
456 cx.set_shared_state(indoc! {
457 "The ˇquick brown
458 fox jumps over
459 the lazy dog"
460 })
461 .await;
462 let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
463
464 // entering visual mode should select the character
465 // under cursor
466 cx.simulate_shared_keystrokes(["v"]).await;
467 cx.assert_shared_state(indoc! { "The «qˇ»uick brown
468 fox jumps over
469 the lazy dog"})
470 .await;
471 cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
472
473 // forwards motions should extend the selection
474 cx.simulate_shared_keystrokes(["w", "j"]).await;
475 cx.assert_shared_state(indoc! { "The «quick brown
476 fox jumps oˇ»ver
477 the lazy dog"})
478 .await;
479
480 cx.simulate_shared_keystrokes(["escape"]).await;
481 assert_eq!(Mode::Normal, cx.neovim_mode().await);
482 cx.assert_shared_state(indoc! { "The quick brown
483 fox jumps ˇover
484 the lazy dog"})
485 .await;
486
487 // motions work backwards
488 cx.simulate_shared_keystrokes(["v", "k", "b"]).await;
489 cx.assert_shared_state(indoc! { "The «ˇquick brown
490 fox jumps o»ver
491 the lazy dog"})
492 .await;
493
494 // works on empty lines
495 cx.set_shared_state(indoc! {"
496 a
497 ˇ
498 b
499 "})
500 .await;
501 let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
502 cx.simulate_shared_keystrokes(["v"]).await;
503 cx.assert_shared_state(indoc! {"
504 a
505 «
506 ˇ»b
507 "})
508 .await;
509 cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
510
511 // toggles off again
512 cx.simulate_shared_keystrokes(["v"]).await;
513 cx.assert_shared_state(indoc! {"
514 a
515 ˇ
516 b
517 "})
518 .await;
519
520 // works at the end of a document
521 cx.set_shared_state(indoc! {"
522 a
523 b
524 ˇ"})
525 .await;
526
527 cx.simulate_shared_keystrokes(["v"]).await;
528 cx.assert_shared_state(indoc! {"
529 a
530 b
531 ˇ"})
532 .await;
533 assert_eq!(cx.mode(), cx.neovim_mode().await);
534 }
535
536 #[gpui::test]
537 async fn test_enter_visual_line_mode(cx: &mut gpui::TestAppContext) {
538 let mut cx = NeovimBackedTestContext::new(cx).await;
539
540 cx.set_shared_state(indoc! {
541 "The ˇquick brown
542 fox jumps over
543 the lazy dog"
544 })
545 .await;
546 cx.simulate_shared_keystrokes(["shift-v"]).await;
547 cx.assert_shared_state(indoc! { "The «qˇ»uick brown
548 fox jumps over
549 the lazy dog"})
550 .await;
551 assert_eq!(cx.mode(), cx.neovim_mode().await);
552 cx.simulate_shared_keystrokes(["x"]).await;
553 cx.assert_shared_state(indoc! { "fox ˇjumps over
554 the lazy dog"})
555 .await;
556
557 // it should work on empty lines
558 cx.set_shared_state(indoc! {"
559 a
560 ˇ
561 b"})
562 .await;
563 cx.simulate_shared_keystrokes(["shift-v"]).await;
564 cx.assert_shared_state(indoc! { "
565 a
566 «
567 ˇ»b"})
568 .await;
569 cx.simulate_shared_keystrokes(["x"]).await;
570 cx.assert_shared_state(indoc! { "
571 a
572 ˇb"})
573 .await;
574
575 // it should work at the end of the document
576 cx.set_shared_state(indoc! {"
577 a
578 b
579 ˇ"})
580 .await;
581 let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
582 cx.simulate_shared_keystrokes(["shift-v"]).await;
583 cx.assert_shared_state(indoc! {"
584 a
585 b
586 ˇ"})
587 .await;
588 assert_eq!(cx.mode(), cx.neovim_mode().await);
589 cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
590 cx.simulate_shared_keystrokes(["x"]).await;
591 cx.assert_shared_state(indoc! {"
592 a
593 ˇb"})
594 .await;
595 }
596
597 #[gpui::test]
598 async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
599 let mut cx = NeovimBackedTestContext::new(cx).await;
600
601 cx.assert_binding_matches(["v", "w"], "The quick ˇbrown")
602 .await;
603
604 cx.assert_binding_matches(["v", "w", "x"], "The quick ˇbrown")
605 .await;
606 cx.assert_binding_matches(
607 ["v", "w", "j", "x"],
608 indoc! {"
609 The ˇquick brown
610 fox jumps over
611 the lazy dog"},
612 )
613 .await;
614 // Test pasting code copied on delete
615 cx.simulate_shared_keystrokes(["j", "p"]).await;
616 cx.assert_state_matches().await;
617
618 let mut cx = cx.binding(["v", "w", "j", "x"]);
619 cx.assert_all(indoc! {"
620 The ˇquick brown
621 fox jumps over
622 the ˇlazy dog"})
623 .await;
624 let mut cx = cx.binding(["v", "b", "k", "x"]);
625 cx.assert_all(indoc! {"
626 The ˇquick brown
627 fox jumps ˇover
628 the ˇlazy dog"})
629 .await;
630 }
631
632 #[gpui::test]
633 async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
634 let mut cx = NeovimBackedTestContext::new(cx).await;
635
636 cx.set_shared_state(indoc! {"
637 The quˇick brown
638 fox jumps over
639 the lazy dog"})
640 .await;
641 cx.simulate_shared_keystrokes(["shift-v", "x"]).await;
642 cx.assert_state_matches().await;
643
644 // Test pasting code copied on delete
645 cx.simulate_shared_keystroke("p").await;
646 cx.assert_state_matches().await;
647
648 cx.set_shared_state(indoc! {"
649 The quick brown
650 fox jumps over
651 the laˇzy dog"})
652 .await;
653 cx.simulate_shared_keystrokes(["shift-v", "x"]).await;
654 cx.assert_state_matches().await;
655 cx.assert_shared_clipboard("the lazy dog\n").await;
656
657 for marked_text in cx.each_marked_position(indoc! {"
658 The quˇick brown
659 fox jumps over
660 the lazy dog"})
661 {
662 cx.set_shared_state(&marked_text).await;
663 cx.simulate_shared_keystrokes(["shift-v", "j", "x"]).await;
664 cx.assert_state_matches().await;
665 // Test pasting code copied on delete
666 cx.simulate_shared_keystroke("p").await;
667 cx.assert_state_matches().await;
668 }
669
670 cx.set_shared_state(indoc! {"
671 The ˇlong line
672 should not
673 crash
674 "})
675 .await;
676 cx.simulate_shared_keystrokes(["shift-v", "$", "x"]).await;
677 cx.assert_state_matches().await;
678 }
679
680 #[gpui::test]
681 async fn test_visual_yank(cx: &mut gpui::TestAppContext) {
682 let mut cx = NeovimBackedTestContext::new(cx).await;
683
684 cx.set_shared_state("The quick ˇbrown").await;
685 cx.simulate_shared_keystrokes(["v", "w", "y"]).await;
686 cx.assert_shared_state("The quick ˇbrown").await;
687 cx.assert_shared_clipboard("brown").await;
688
689 cx.set_shared_state(indoc! {"
690 The ˇquick brown
691 fox jumps over
692 the lazy dog"})
693 .await;
694 cx.simulate_shared_keystrokes(["v", "w", "j", "y"]).await;
695 cx.assert_shared_state(indoc! {"
696 The ˇquick brown
697 fox jumps over
698 the lazy dog"})
699 .await;
700 cx.assert_shared_clipboard(indoc! {"
701 quick brown
702 fox jumps o"})
703 .await;
704
705 cx.set_shared_state(indoc! {"
706 The quick brown
707 fox jumps over
708 the ˇlazy dog"})
709 .await;
710 cx.simulate_shared_keystrokes(["v", "w", "j", "y"]).await;
711 cx.assert_shared_state(indoc! {"
712 The quick brown
713 fox jumps over
714 the ˇlazy dog"})
715 .await;
716 cx.assert_shared_clipboard("lazy d").await;
717 cx.simulate_shared_keystrokes(["shift-v", "y"]).await;
718 cx.assert_shared_clipboard("the lazy dog\n").await;
719
720 let mut cx = cx.binding(["v", "b", "k", "y"]);
721 cx.set_shared_state(indoc! {"
722 The ˇquick brown
723 fox jumps over
724 the lazy dog"})
725 .await;
726 cx.simulate_shared_keystrokes(["v", "b", "k", "y"]).await;
727 cx.assert_shared_state(indoc! {"
728 ˇThe quick brown
729 fox jumps over
730 the lazy dog"})
731 .await;
732 cx.assert_clipboard_content(Some("The q"));
733
734 cx.set_shared_state(indoc! {"
735 The quick brown
736 fox ˇjumps over
737 the lazy dog"})
738 .await;
739 cx.simulate_shared_keystrokes(["shift-v", "shift-g", "shift-y"])
740 .await;
741 cx.assert_shared_state(indoc! {"
742 The quick brown
743 ˇfox jumps over
744 the lazy dog"})
745 .await;
746 cx.assert_shared_clipboard("fox jumps over\nthe lazy dog\n")
747 .await;
748 }
749
750 #[gpui::test]
751 async fn test_visual_block_mode(cx: &mut gpui::TestAppContext) {
752 let mut cx = NeovimBackedTestContext::new(cx).await;
753
754 cx.set_shared_state(indoc! {
755 "The ˇquick brown
756 fox jumps over
757 the lazy dog"
758 })
759 .await;
760 cx.simulate_shared_keystrokes(["ctrl-v"]).await;
761 cx.assert_shared_state(indoc! {
762 "The «qˇ»uick brown
763 fox jumps over
764 the lazy dog"
765 })
766 .await;
767 cx.simulate_shared_keystrokes(["2", "down"]).await;
768 cx.assert_shared_state(indoc! {
769 "The «qˇ»uick brown
770 fox «jˇ»umps over
771 the «lˇ»azy dog"
772 })
773 .await;
774 cx.simulate_shared_keystrokes(["e"]).await;
775 cx.assert_shared_state(indoc! {
776 "The «quicˇ»k brown
777 fox «jumpˇ»s over
778 the «lazyˇ» dog"
779 })
780 .await;
781 cx.simulate_shared_keystrokes(["^"]).await;
782 cx.assert_shared_state(indoc! {
783 "«ˇThe q»uick brown
784 «ˇfox j»umps over
785 «ˇthe l»azy dog"
786 })
787 .await;
788 cx.simulate_shared_keystrokes(["$"]).await;
789 cx.assert_shared_state(indoc! {
790 "The «quick brownˇ»
791 fox «jumps overˇ»
792 the «lazy dogˇ»"
793 })
794 .await;
795 cx.simulate_shared_keystrokes(["shift-f", " "]).await;
796 cx.assert_shared_state(indoc! {
797 "The «quickˇ» brown
798 fox «jumpsˇ» over
799 the «lazy ˇ»dog"
800 })
801 .await;
802
803 // toggling through visual mode works as expected
804 cx.simulate_shared_keystrokes(["v"]).await;
805 cx.assert_shared_state(indoc! {
806 "The «quick brown
807 fox jumps over
808 the lazy ˇ»dog"
809 })
810 .await;
811 cx.simulate_shared_keystrokes(["ctrl-v"]).await;
812 cx.assert_shared_state(indoc! {
813 "The «quickˇ» brown
814 fox «jumpsˇ» over
815 the «lazy ˇ»dog"
816 })
817 .await;
818
819 cx.set_shared_state(indoc! {
820 "The ˇquick
821 brown
822 fox
823 jumps over the
824
825 lazy dog
826 "
827 })
828 .await;
829 cx.simulate_shared_keystrokes(["ctrl-v", "down", "down"])
830 .await;
831 cx.assert_shared_state(indoc! {
832 "The«ˇ q»uick
833 bro«ˇwn»
834 foxˇ
835 jumps over the
836
837 lazy dog
838 "
839 })
840 .await;
841 cx.simulate_shared_keystrokes(["down"]).await;
842 cx.assert_shared_state(indoc! {
843 "The «qˇ»uick
844 brow«nˇ»
845 fox
846 jump«sˇ» over the
847
848 lazy dog
849 "
850 })
851 .await;
852 cx.simulate_shared_keystroke("left").await;
853 cx.assert_shared_state(indoc! {
854 "The«ˇ q»uick
855 bro«ˇwn»
856 foxˇ
857 jum«ˇps» over the
858
859 lazy dog
860 "
861 })
862 .await;
863 cx.simulate_shared_keystrokes(["s", "o", "escape"]).await;
864 cx.assert_shared_state(indoc! {
865 "Theˇouick
866 broo
867 foxo
868 jumo over the
869
870 lazy dog
871 "
872 })
873 .await;
874
875 //https://github.com/zed-industries/community/issues/1950
876 cx.set_shared_state(indoc! {
877 "Theˇ quick brown
878
879 fox jumps over
880 the lazy dog
881 "
882 })
883 .await;
884 cx.simulate_shared_keystrokes(["l", "ctrl-v", "j", "j"])
885 .await;
886 cx.assert_shared_state(indoc! {
887 "The «qˇ»uick brown
888
889 fox «jˇ»umps over
890 the lazy dog
891 "
892 })
893 .await;
894 }
895
896 #[gpui::test]
897 async fn test_visual_block_insert(cx: &mut gpui::TestAppContext) {
898 let mut cx = NeovimBackedTestContext::new(cx).await;
899
900 cx.set_shared_state(indoc! {
901 "ˇThe quick brown
902 fox jumps over
903 the lazy dog
904 "
905 })
906 .await;
907 cx.simulate_shared_keystrokes(["ctrl-v", "9", "down"]).await;
908 cx.assert_shared_state(indoc! {
909 "«Tˇ»he quick brown
910 «fˇ»ox jumps over
911 «tˇ»he lazy dog
912 ˇ"
913 })
914 .await;
915
916 cx.simulate_shared_keystrokes(["shift-i", "k", "escape"])
917 .await;
918 cx.assert_shared_state(indoc! {
919 "ˇkThe quick brown
920 kfox jumps over
921 kthe lazy dog
922 k"
923 })
924 .await;
925
926 cx.set_shared_state(indoc! {
927 "ˇThe quick brown
928 fox jumps over
929 the lazy dog
930 "
931 })
932 .await;
933 cx.simulate_shared_keystrokes(["ctrl-v", "9", "down"]).await;
934 cx.assert_shared_state(indoc! {
935 "«Tˇ»he quick brown
936 «fˇ»ox jumps over
937 «tˇ»he lazy dog
938 ˇ"
939 })
940 .await;
941 cx.simulate_shared_keystrokes(["c", "k", "escape"]).await;
942 cx.assert_shared_state(indoc! {
943 "ˇkhe quick brown
944 kox jumps over
945 khe lazy dog
946 k"
947 })
948 .await;
949 }
950
951 #[gpui::test]
952 async fn test_visual_object(cx: &mut gpui::TestAppContext) {
953 let mut cx = NeovimBackedTestContext::new(cx).await;
954
955 cx.set_shared_state("hello (in [parˇens] o)").await;
956 cx.simulate_shared_keystrokes(["ctrl-v", "l"]).await;
957 cx.simulate_shared_keystrokes(["a", "]"]).await;
958 cx.assert_shared_state("hello (in «[parens]ˇ» o)").await;
959 assert_eq!(cx.mode(), Mode::Visual);
960 cx.simulate_shared_keystrokes(["i", "("]).await;
961 cx.assert_shared_state("hello («in [parens] oˇ»)").await;
962
963 cx.set_shared_state("hello in a wˇord again.").await;
964 cx.simulate_shared_keystrokes(["ctrl-v", "l", "i", "w"])
965 .await;
966 cx.assert_shared_state("hello in a w«ordˇ» again.").await;
967 assert_eq!(cx.mode(), Mode::VisualBlock);
968 cx.simulate_shared_keystrokes(["o", "a", "s"]).await;
969 cx.assert_shared_state("«ˇhello in a word» again.").await;
970 assert_eq!(cx.mode(), Mode::Visual);
971 }
972
973 #[gpui::test]
974 async fn test_mode_across_command(cx: &mut gpui::TestAppContext) {
975 let mut cx = VimTestContext::new(cx, true).await;
976
977 cx.set_state("aˇbc", Mode::Normal);
978 cx.simulate_keystrokes(["ctrl-v"]);
979 assert_eq!(cx.mode(), Mode::VisualBlock);
980 cx.simulate_keystrokes(["cmd-shift-p", "escape"]);
981 assert_eq!(cx.mode(), Mode::VisualBlock);
982 }
983}