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