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