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