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