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