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