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