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