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