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