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