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