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.update_active_editor(cx, |vim, editor, cx| {
414 let mut original_columns: HashMap<_, _> = Default::default();
415 let line_mode = line_mode || editor.selections.line_mode;
416
417 editor.transact(cx, |editor, cx| {
418 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
419 s.move_with(|map, selection| {
420 if line_mode {
421 let mut position = selection.head();
422 if !selection.reversed {
423 position = movement::left(map, position);
424 }
425 original_columns.insert(selection.id, position.to_point(map).column);
426 if vim.state().mode == Mode::VisualBlock {
427 *selection.end.column_mut() = map.line_len(selection.end.row())
428 } else if vim.state().mode != Mode::VisualLine {
429 selection.start = DisplayPoint::new(selection.start.row(), 0);
430 if selection.end.row() == map.max_point().row() {
431 selection.end = map.max_point()
432 } else {
433 *selection.end.row_mut() += 1;
434 *selection.end.column_mut() = 0;
435 }
436 }
437 }
438 selection.goal = SelectionGoal::None;
439 });
440 });
441 copy_selections_content(vim, editor, line_mode, cx);
442 editor.insert("", cx);
443
444 // Fixup cursor position after the deletion
445 editor.set_clip_at_line_ends(true, cx);
446 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
447 s.move_with(|map, selection| {
448 let mut cursor = selection.head().to_point(map);
449
450 if let Some(column) = original_columns.get(&selection.id) {
451 cursor.column = *column
452 }
453 let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left);
454 selection.collapse_to(cursor, selection.goal)
455 });
456 if vim.state().mode == Mode::VisualBlock {
457 s.select_anchors(vec![s.first_anchor()])
458 }
459 });
460 })
461 });
462 vim.switch_mode(Mode::Normal, true, cx);
463}
464
465pub fn yank(vim: &mut Vim, cx: &mut WindowContext) {
466 vim.update_active_editor(cx, |vim, editor, cx| {
467 let line_mode = editor.selections.line_mode;
468 yank_selections_content(vim, editor, line_mode, cx);
469 editor.change_selections(None, cx, |s| {
470 s.move_with(|map, selection| {
471 if line_mode {
472 selection.start = start_of_line(map, false, selection.start);
473 };
474 selection.collapse_to(selection.start, SelectionGoal::None)
475 });
476 if vim.state().mode == Mode::VisualBlock {
477 s.select_anchors(vec![s.first_anchor()])
478 }
479 });
480 });
481 vim.switch_mode(Mode::Normal, true, cx);
482}
483
484pub(crate) fn visual_replace(text: Arc<str>, cx: &mut WindowContext) {
485 Vim::update(cx, |vim, cx| {
486 vim.stop_recording();
487 vim.update_active_editor(cx, |_, editor, cx| {
488 editor.transact(cx, |editor, cx| {
489 let (display_map, selections) = editor.selections.all_adjusted_display(cx);
490
491 // Selections are biased right at the start. So we need to store
492 // anchors that are biased left so that we can restore the selections
493 // after the change
494 let stable_anchors = editor
495 .selections
496 .disjoint_anchors()
497 .into_iter()
498 .map(|selection| {
499 let start = selection.start.bias_left(&display_map.buffer_snapshot);
500 start..start
501 })
502 .collect::<Vec<_>>();
503
504 let mut edits = Vec::new();
505 for selection in selections.iter() {
506 let selection = selection.clone();
507 for row_range in
508 movement::split_display_range_by_lines(&display_map, selection.range())
509 {
510 let range = row_range.start.to_offset(&display_map, Bias::Right)
511 ..row_range.end.to_offset(&display_map, Bias::Right);
512 let text = text.repeat(range.len());
513 edits.push((range, text));
514 }
515 }
516
517 editor.buffer().update(cx, |buffer, cx| {
518 buffer.edit(edits, None, cx);
519 });
520 editor.change_selections(None, cx, |s| s.select_ranges(stable_anchors));
521 });
522 });
523 vim.switch_mode(Mode::Normal, false, cx);
524 });
525}
526
527pub fn select_next(_: &mut Workspace, _: &SelectNext, cx: &mut ViewContext<Workspace>) {
528 Vim::update(cx, |vim, cx| {
529 let count =
530 vim.take_count(cx)
531 .unwrap_or_else(|| if vim.state().mode.is_visual() { 1 } else { 2 });
532 vim.update_active_editor(cx, |_, editor, cx| {
533 editor.set_clip_at_line_ends(false, cx);
534 for _ in 0..count {
535 if editor
536 .select_next(&Default::default(), cx)
537 .log_err()
538 .is_none()
539 {
540 break;
541 }
542 }
543 })
544 });
545}
546
547pub fn select_previous(_: &mut Workspace, _: &SelectPrevious, cx: &mut ViewContext<Workspace>) {
548 Vim::update(cx, |vim, cx| {
549 let count =
550 vim.take_count(cx)
551 .unwrap_or_else(|| if vim.state().mode.is_visual() { 1 } else { 2 });
552 vim.update_active_editor(cx, |_, editor, cx| {
553 for _ in 0..count {
554 if editor
555 .select_previous(&Default::default(), cx)
556 .log_err()
557 .is_none()
558 {
559 break;
560 }
561 }
562 })
563 });
564}
565
566pub fn select_match(
567 workspace: &mut Workspace,
568 vim: &mut Vim,
569 direction: Direction,
570 cx: &mut WindowContext,
571) {
572 let count = vim.take_count(cx).unwrap_or(1);
573 let pane = workspace.active_pane().clone();
574 let vim_is_normal = vim.state().mode == Mode::Normal;
575 let mut start_selection = 0usize;
576 let mut end_selection = 0usize;
577
578 vim.update_active_editor(cx, |_, editor, _| {
579 editor.set_collapse_matches(false);
580 });
581 if vim_is_normal {
582 pane.update(cx, |pane, cx| {
583 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
584 search_bar.update(cx, |search_bar, cx| {
585 if !search_bar.has_active_match() || !search_bar.show(cx) {
586 return;
587 }
588 // without update_match_index there is a bug when the cursor is before the first match
589 search_bar.update_match_index(cx);
590 search_bar.select_match(direction.opposite(), 1, cx);
591 });
592 }
593 });
594 }
595 vim.update_active_editor(cx, |_, editor, cx| {
596 let latest = editor.selections.newest::<usize>(cx);
597 start_selection = latest.start;
598 end_selection = latest.end;
599 });
600
601 let mut match_exists = false;
602 pane.update(cx, |pane, cx| {
603 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
604 search_bar.update(cx, |search_bar, cx| {
605 search_bar.update_match_index(cx);
606 search_bar.select_match(direction, count, cx);
607 match_exists = search_bar.match_exists(cx);
608 });
609 }
610 });
611 if !match_exists {
612 vim.clear_operator(cx);
613 vim.stop_replaying();
614 return;
615 }
616 vim.update_active_editor(cx, |_, editor, cx| {
617 let latest = editor.selections.newest::<usize>(cx);
618 if vim_is_normal {
619 start_selection = latest.start;
620 end_selection = latest.end;
621 } else {
622 start_selection = start_selection.min(latest.start);
623 end_selection = end_selection.max(latest.end);
624 }
625 if direction == Direction::Prev {
626 std::mem::swap(&mut start_selection, &mut end_selection);
627 }
628 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
629 s.select_ranges([start_selection..end_selection]);
630 });
631 editor.set_collapse_matches(true);
632 });
633
634 match vim.maybe_pop_operator() {
635 Some(Operator::Change) => substitute(vim, None, false, cx),
636 Some(Operator::Delete) => {
637 vim.stop_recording();
638 delete(vim, false, cx)
639 }
640 Some(Operator::Yank) => yank(vim, cx),
641 _ => {} // Ignoring other operators
642 }
643}
644
645#[cfg(test)]
646mod test {
647 use indoc::indoc;
648 use workspace::item::Item;
649
650 use crate::{
651 state::Mode,
652 test::{NeovimBackedTestContext, VimTestContext},
653 };
654
655 #[gpui::test]
656 async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
657 let mut cx = NeovimBackedTestContext::new(cx).await;
658
659 cx.set_shared_state(indoc! {
660 "The ˇquick brown
661 fox jumps over
662 the lazy dog"
663 })
664 .await;
665 let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
666
667 // entering visual mode should select the character
668 // under cursor
669 cx.simulate_shared_keystrokes("v").await;
670 cx.shared_state()
671 .await
672 .assert_eq(indoc! { "The «qˇ»uick brown
673 fox jumps over
674 the lazy dog"});
675 cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
676
677 // forwards motions should extend the selection
678 cx.simulate_shared_keystrokes("w j").await;
679 cx.shared_state().await.assert_eq(indoc! { "The «quick brown
680 fox jumps oˇ»ver
681 the lazy dog"});
682
683 cx.simulate_shared_keystrokes("escape").await;
684 cx.shared_state().await.assert_eq(indoc! { "The quick brown
685 fox jumps ˇover
686 the lazy dog"});
687
688 // motions work backwards
689 cx.simulate_shared_keystrokes("v k b").await;
690 cx.shared_state()
691 .await
692 .assert_eq(indoc! { "The «ˇquick brown
693 fox jumps o»ver
694 the lazy dog"});
695
696 // works on empty lines
697 cx.set_shared_state(indoc! {"
698 a
699 ˇ
700 b
701 "})
702 .await;
703 let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
704 cx.simulate_shared_keystrokes("v").await;
705 cx.shared_state().await.assert_eq(indoc! {"
706 a
707 «
708 ˇ»b
709 "});
710 cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
711
712 // toggles off again
713 cx.simulate_shared_keystrokes("v").await;
714 cx.shared_state().await.assert_eq(indoc! {"
715 a
716 ˇ
717 b
718 "});
719
720 // works at the end of a document
721 cx.set_shared_state(indoc! {"
722 a
723 b
724 ˇ"})
725 .await;
726
727 cx.simulate_shared_keystrokes("v").await;
728 cx.shared_state().await.assert_eq(indoc! {"
729 a
730 b
731 ˇ"});
732 }
733
734 #[gpui::test]
735 async fn test_enter_visual_line_mode(cx: &mut gpui::TestAppContext) {
736 let mut cx = NeovimBackedTestContext::new(cx).await;
737
738 cx.set_shared_state(indoc! {
739 "The ˇquick brown
740 fox jumps over
741 the lazy dog"
742 })
743 .await;
744 cx.simulate_shared_keystrokes("shift-v").await;
745 cx.shared_state()
746 .await
747 .assert_eq(indoc! { "The «qˇ»uick brown
748 fox jumps over
749 the lazy dog"});
750 cx.simulate_shared_keystrokes("x").await;
751 cx.shared_state().await.assert_eq(indoc! { "fox ˇjumps over
752 the lazy dog"});
753
754 // it should work on empty lines
755 cx.set_shared_state(indoc! {"
756 a
757 ˇ
758 b"})
759 .await;
760 cx.simulate_shared_keystrokes("shift-v").await;
761 cx.shared_state().await.assert_eq(indoc! {"
762 a
763 «
764 ˇ»b"});
765 cx.simulate_shared_keystrokes("x").await;
766 cx.shared_state().await.assert_eq(indoc! {"
767 a
768 ˇb"});
769
770 // it should work at the end of the document
771 cx.set_shared_state(indoc! {"
772 a
773 b
774 ˇ"})
775 .await;
776 let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
777 cx.simulate_shared_keystrokes("shift-v").await;
778 cx.shared_state().await.assert_eq(indoc! {"
779 a
780 b
781 ˇ"});
782 cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
783 cx.simulate_shared_keystrokes("x").await;
784 cx.shared_state().await.assert_eq(indoc! {"
785 a
786 ˇb"});
787 }
788
789 #[gpui::test]
790 async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
791 let mut cx = NeovimBackedTestContext::new(cx).await;
792
793 cx.simulate("v w", "The quick ˇbrown")
794 .await
795 .assert_matches();
796
797 cx.simulate("v w x", "The quick ˇbrown")
798 .await
799 .assert_matches();
800 cx.simulate(
801 "v w j x",
802 indoc! {"
803 The ˇquick brown
804 fox jumps over
805 the lazy dog"},
806 )
807 .await
808 .assert_matches();
809 // Test pasting code copied on delete
810 cx.simulate_shared_keystrokes("j p").await;
811 cx.shared_state().await.assert_matches();
812
813 cx.simulate_at_each_offset(
814 "v w j x",
815 indoc! {"
816 The ˇquick brown
817 fox jumps over
818 the ˇlazy dog"},
819 )
820 .await
821 .assert_matches();
822 cx.simulate_at_each_offset(
823 "v b k x",
824 indoc! {"
825 The ˇquick brown
826 fox jumps ˇover
827 the ˇlazy dog"},
828 )
829 .await
830 .assert_matches();
831 }
832
833 #[gpui::test]
834 async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
835 let mut cx = NeovimBackedTestContext::new(cx).await;
836
837 cx.set_shared_state(indoc! {"
838 The quˇick brown
839 fox jumps over
840 the lazy dog"})
841 .await;
842 cx.simulate_shared_keystrokes("shift-v x").await;
843 cx.shared_state().await.assert_matches();
844
845 // Test pasting code copied on delete
846 cx.simulate_shared_keystrokes("p").await;
847 cx.shared_state().await.assert_matches();
848
849 cx.set_shared_state(indoc! {"
850 The quick brown
851 fox jumps over
852 the laˇzy dog"})
853 .await;
854 cx.simulate_shared_keystrokes("shift-v x").await;
855 cx.shared_state().await.assert_matches();
856 cx.shared_clipboard().await.assert_eq("the lazy dog\n");
857
858 cx.set_shared_state(indoc! {"
859 The quˇick brown
860 fox jumps over
861 the lazy dog"})
862 .await;
863 cx.simulate_shared_keystrokes("shift-v j x").await;
864 cx.shared_state().await.assert_matches();
865 // Test pasting code copied on delete
866 cx.simulate_shared_keystrokes("p").await;
867 cx.shared_state().await.assert_matches();
868
869 cx.set_shared_state(indoc! {"
870 The ˇlong line
871 should not
872 crash
873 "})
874 .await;
875 cx.simulate_shared_keystrokes("shift-v $ x").await;
876 cx.shared_state().await.assert_matches();
877 }
878
879 #[gpui::test]
880 async fn test_visual_yank(cx: &mut gpui::TestAppContext) {
881 let mut cx = NeovimBackedTestContext::new(cx).await;
882
883 cx.set_shared_state("The quick ˇbrown").await;
884 cx.simulate_shared_keystrokes("v w y").await;
885 cx.shared_state().await.assert_eq("The quick ˇbrown");
886 cx.shared_clipboard().await.assert_eq("brown");
887
888 cx.set_shared_state(indoc! {"
889 The ˇquick brown
890 fox jumps over
891 the lazy dog"})
892 .await;
893 cx.simulate_shared_keystrokes("v w j y").await;
894 cx.shared_state().await.assert_eq(indoc! {"
895 The ˇquick brown
896 fox jumps over
897 the lazy dog"});
898 cx.shared_clipboard().await.assert_eq(indoc! {"
899 quick brown
900 fox jumps o"});
901
902 cx.set_shared_state(indoc! {"
903 The quick brown
904 fox jumps over
905 the ˇlazy dog"})
906 .await;
907 cx.simulate_shared_keystrokes("v w j y").await;
908 cx.shared_state().await.assert_eq(indoc! {"
909 The quick brown
910 fox jumps over
911 the ˇlazy dog"});
912 cx.shared_clipboard().await.assert_eq("lazy d");
913 cx.simulate_shared_keystrokes("shift-v y").await;
914 cx.shared_clipboard().await.assert_eq("the lazy dog\n");
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("v b k y").await;
922 cx.shared_state().await.assert_eq(indoc! {"
923 ˇThe quick brown
924 fox jumps over
925 the lazy dog"});
926 assert_eq!(
927 cx.read_from_clipboard()
928 .map(|item| item.text().clone())
929 .unwrap(),
930 "The q"
931 );
932
933 cx.set_shared_state(indoc! {"
934 The quick brown
935 fox ˇjumps over
936 the lazy dog"})
937 .await;
938 cx.simulate_shared_keystrokes("shift-v shift-g shift-y")
939 .await;
940 cx.shared_state().await.assert_eq(indoc! {"
941 The quick brown
942 ˇfox jumps over
943 the lazy dog"});
944 cx.shared_clipboard()
945 .await
946 .assert_eq("fox jumps over\nthe lazy dog\n");
947 }
948
949 #[gpui::test]
950 async fn test_visual_block_mode(cx: &mut gpui::TestAppContext) {
951 let mut cx = NeovimBackedTestContext::new(cx).await;
952
953 cx.set_shared_state(indoc! {
954 "The ˇquick brown
955 fox jumps over
956 the lazy dog"
957 })
958 .await;
959 cx.simulate_shared_keystrokes("ctrl-v").await;
960 cx.shared_state().await.assert_eq(indoc! {
961 "The «qˇ»uick brown
962 fox jumps over
963 the lazy dog"
964 });
965 cx.simulate_shared_keystrokes("2 down").await;
966 cx.shared_state().await.assert_eq(indoc! {
967 "The «qˇ»uick brown
968 fox «jˇ»umps over
969 the «lˇ»azy dog"
970 });
971 cx.simulate_shared_keystrokes("e").await;
972 cx.shared_state().await.assert_eq(indoc! {
973 "The «quicˇ»k brown
974 fox «jumpˇ»s over
975 the «lazyˇ» dog"
976 });
977 cx.simulate_shared_keystrokes("^").await;
978 cx.shared_state().await.assert_eq(indoc! {
979 "«ˇThe q»uick brown
980 «ˇfox j»umps over
981 «ˇthe l»azy dog"
982 });
983 cx.simulate_shared_keystrokes("$").await;
984 cx.shared_state().await.assert_eq(indoc! {
985 "The «quick brownˇ»
986 fox «jumps overˇ»
987 the «lazy dogˇ»"
988 });
989 cx.simulate_shared_keystrokes("shift-f space").await;
990 cx.shared_state().await.assert_eq(indoc! {
991 "The «quickˇ» brown
992 fox «jumpsˇ» over
993 the «lazy ˇ»dog"
994 });
995
996 // toggling through visual mode works as expected
997 cx.simulate_shared_keystrokes("v").await;
998 cx.shared_state().await.assert_eq(indoc! {
999 "The «quick brown
1000 fox jumps over
1001 the lazy ˇ»dog"
1002 });
1003 cx.simulate_shared_keystrokes("ctrl-v").await;
1004 cx.shared_state().await.assert_eq(indoc! {
1005 "The «quickˇ» brown
1006 fox «jumpsˇ» over
1007 the «lazy ˇ»dog"
1008 });
1009
1010 cx.set_shared_state(indoc! {
1011 "The ˇquick
1012 brown
1013 fox
1014 jumps over the
1015
1016 lazy dog
1017 "
1018 })
1019 .await;
1020 cx.simulate_shared_keystrokes("ctrl-v down down").await;
1021 cx.shared_state().await.assert_eq(indoc! {
1022 "The«ˇ q»uick
1023 bro«ˇwn»
1024 foxˇ
1025 jumps over the
1026
1027 lazy dog
1028 "
1029 });
1030 cx.simulate_shared_keystrokes("down").await;
1031 cx.shared_state().await.assert_eq(indoc! {
1032 "The «qˇ»uick
1033 brow«nˇ»
1034 fox
1035 jump«sˇ» over the
1036
1037 lazy dog
1038 "
1039 });
1040 cx.simulate_shared_keystrokes("left").await;
1041 cx.shared_state().await.assert_eq(indoc! {
1042 "The«ˇ q»uick
1043 bro«ˇwn»
1044 foxˇ
1045 jum«ˇps» over the
1046
1047 lazy dog
1048 "
1049 });
1050 cx.simulate_shared_keystrokes("s o escape").await;
1051 cx.shared_state().await.assert_eq(indoc! {
1052 "Theˇouick
1053 broo
1054 foxo
1055 jumo over the
1056
1057 lazy dog
1058 "
1059 });
1060
1061 // https://github.com/zed-industries/zed/issues/6274
1062 cx.set_shared_state(indoc! {
1063 "Theˇ quick brown
1064
1065 fox jumps over
1066 the lazy dog
1067 "
1068 })
1069 .await;
1070 cx.simulate_shared_keystrokes("l ctrl-v j j").await;
1071 cx.shared_state().await.assert_eq(indoc! {
1072 "The «qˇ»uick brown
1073
1074 fox «jˇ»umps over
1075 the lazy dog
1076 "
1077 });
1078 }
1079
1080 #[gpui::test]
1081 async fn test_visual_block_issue_2123(cx: &mut gpui::TestAppContext) {
1082 let mut cx = NeovimBackedTestContext::new(cx).await;
1083
1084 cx.set_shared_state(indoc! {
1085 "The ˇquick brown
1086 fox jumps over
1087 the lazy dog
1088 "
1089 })
1090 .await;
1091 cx.simulate_shared_keystrokes("ctrl-v right down").await;
1092 cx.shared_state().await.assert_eq(indoc! {
1093 "The «quˇ»ick brown
1094 fox «juˇ»mps over
1095 the lazy dog
1096 "
1097 });
1098 }
1099
1100 #[gpui::test]
1101 async fn test_visual_block_insert(cx: &mut gpui::TestAppContext) {
1102 let mut cx = NeovimBackedTestContext::new(cx).await;
1103
1104 cx.set_shared_state(indoc! {
1105 "ˇThe quick brown
1106 fox jumps over
1107 the lazy dog
1108 "
1109 })
1110 .await;
1111 cx.simulate_shared_keystrokes("ctrl-v 9 down").await;
1112 cx.shared_state().await.assert_eq(indoc! {
1113 "«Tˇ»he quick brown
1114 «fˇ»ox jumps over
1115 «tˇ»he lazy dog
1116 ˇ"
1117 });
1118
1119 cx.simulate_shared_keystrokes("shift-i k escape").await;
1120 cx.shared_state().await.assert_eq(indoc! {
1121 "ˇkThe quick brown
1122 kfox jumps over
1123 kthe lazy dog
1124 k"
1125 });
1126
1127 cx.set_shared_state(indoc! {
1128 "ˇThe quick brown
1129 fox jumps over
1130 the lazy dog
1131 "
1132 })
1133 .await;
1134 cx.simulate_shared_keystrokes("ctrl-v 9 down").await;
1135 cx.shared_state().await.assert_eq(indoc! {
1136 "«Tˇ»he quick brown
1137 «fˇ»ox jumps over
1138 «tˇ»he lazy dog
1139 ˇ"
1140 });
1141 cx.simulate_shared_keystrokes("c k escape").await;
1142 cx.shared_state().await.assert_eq(indoc! {
1143 "ˇkhe quick brown
1144 kox jumps over
1145 khe lazy dog
1146 k"
1147 });
1148 }
1149
1150 #[gpui::test]
1151 async fn test_visual_object(cx: &mut gpui::TestAppContext) {
1152 let mut cx = NeovimBackedTestContext::new(cx).await;
1153
1154 cx.set_shared_state("hello (in [parˇens] o)").await;
1155 cx.simulate_shared_keystrokes("ctrl-v l").await;
1156 cx.simulate_shared_keystrokes("a ]").await;
1157 cx.shared_state()
1158 .await
1159 .assert_eq("hello (in «[parens]ˇ» o)");
1160 cx.simulate_shared_keystrokes("i (").await;
1161 cx.shared_state()
1162 .await
1163 .assert_eq("hello («in [parens] oˇ»)");
1164
1165 cx.set_shared_state("hello in a wˇord again.").await;
1166 cx.simulate_shared_keystrokes("ctrl-v l i w").await;
1167 cx.shared_state()
1168 .await
1169 .assert_eq("hello in a w«ordˇ» again.");
1170 assert_eq!(cx.mode(), Mode::VisualBlock);
1171 cx.simulate_shared_keystrokes("o a s").await;
1172 cx.shared_state()
1173 .await
1174 .assert_eq("«ˇhello in a word» again.");
1175 }
1176
1177 #[gpui::test]
1178 async fn test_mode_across_command(cx: &mut gpui::TestAppContext) {
1179 let mut cx = VimTestContext::new(cx, true).await;
1180
1181 cx.set_state("aˇbc", Mode::Normal);
1182 cx.simulate_keystrokes("ctrl-v");
1183 assert_eq!(cx.mode(), Mode::VisualBlock);
1184 cx.simulate_keystrokes("cmd-shift-p escape");
1185 assert_eq!(cx.mode(), Mode::VisualBlock);
1186 }
1187
1188 #[gpui::test]
1189 async fn test_gn(cx: &mut gpui::TestAppContext) {
1190 let mut cx = NeovimBackedTestContext::new(cx).await;
1191
1192 cx.set_shared_state("aaˇ aa aa aa aa").await;
1193 cx.simulate_shared_keystrokes("/ a a enter").await;
1194 cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1195 cx.simulate_shared_keystrokes("g n").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("escape d g n").await;
1200 cx.shared_state().await.assert_eq("aa aa ˇ aa aa");
1201
1202 cx.set_shared_state("aaˇ aa aa aa aa").await;
1203 cx.simulate_shared_keystrokes("/ a a enter").await;
1204 cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1205 cx.simulate_shared_keystrokes("3 g n").await;
1206 cx.shared_state().await.assert_eq("aa aa aa «aaˇ» aa");
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("g shift-n").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 }
1216
1217 #[gpui::test]
1218 async fn test_gl(cx: &mut gpui::TestAppContext) {
1219 let mut cx = VimTestContext::new(cx, true).await;
1220
1221 cx.set_state("aaˇ aa\naa", Mode::Normal);
1222 cx.simulate_keystrokes("g l");
1223 cx.assert_state("«aaˇ» «aaˇ»\naa", Mode::Visual);
1224 cx.simulate_keystrokes("g >");
1225 cx.assert_state("«aaˇ» aa\n«aaˇ»", Mode::Visual);
1226 }
1227
1228 #[gpui::test]
1229 async fn test_dgn_repeat(cx: &mut gpui::TestAppContext) {
1230 let mut cx = NeovimBackedTestContext::new(cx).await;
1231
1232 cx.set_shared_state("aaˇ aa aa aa aa").await;
1233 cx.simulate_shared_keystrokes("/ a a enter").await;
1234 cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1235 cx.simulate_shared_keystrokes("d g n").await;
1236
1237 cx.shared_state().await.assert_eq("aa ˇ aa aa aa");
1238 cx.simulate_shared_keystrokes(".").await;
1239 cx.shared_state().await.assert_eq("aa ˇ aa aa");
1240 cx.simulate_shared_keystrokes(".").await;
1241 cx.shared_state().await.assert_eq("aa ˇ aa");
1242 }
1243
1244 #[gpui::test]
1245 async fn test_cgn_repeat(cx: &mut gpui::TestAppContext) {
1246 let mut cx = NeovimBackedTestContext::new(cx).await;
1247
1248 cx.set_shared_state("aaˇ aa aa aa aa").await;
1249 cx.simulate_shared_keystrokes("/ a a enter").await;
1250 cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1251 cx.simulate_shared_keystrokes("c g n x escape").await;
1252 cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1253 cx.simulate_shared_keystrokes(".").await;
1254 cx.shared_state().await.assert_eq("aa x ˇx aa aa");
1255 }
1256
1257 #[gpui::test]
1258 async fn test_cgn_nomatch(cx: &mut gpui::TestAppContext) {
1259 let mut cx = NeovimBackedTestContext::new(cx).await;
1260
1261 cx.set_shared_state("aaˇ aa aa aa aa").await;
1262 cx.simulate_shared_keystrokes("/ b b enter").await;
1263 cx.shared_state().await.assert_eq("aaˇ aa aa aa aa");
1264 cx.simulate_shared_keystrokes("c g n x escape").await;
1265 cx.shared_state().await.assert_eq("aaˇaa aa aa aa");
1266 cx.simulate_shared_keystrokes(".").await;
1267 cx.shared_state().await.assert_eq("aaˇa aa aa aa");
1268
1269 cx.set_shared_state("aaˇ bb aa aa aa").await;
1270 cx.simulate_shared_keystrokes("/ b b enter").await;
1271 cx.shared_state().await.assert_eq("aa ˇbb aa aa aa");
1272 cx.simulate_shared_keystrokes("c g n x escape").await;
1273 cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1274 cx.simulate_shared_keystrokes(".").await;
1275 cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1276 }
1277
1278 #[gpui::test]
1279 async fn test_visual_shift_d(cx: &mut gpui::TestAppContext) {
1280 let mut cx = NeovimBackedTestContext::new(cx).await;
1281
1282 cx.set_shared_state(indoc! {
1283 "The ˇquick brown
1284 fox jumps over
1285 the lazy dog
1286 "
1287 })
1288 .await;
1289 cx.simulate_shared_keystrokes("v down shift-d").await;
1290 cx.shared_state().await.assert_eq(indoc! {
1291 "the ˇlazy dog\n"
1292 });
1293
1294 cx.set_shared_state(indoc! {
1295 "The ˇquick brown
1296 fox jumps over
1297 the lazy dog
1298 "
1299 })
1300 .await;
1301 cx.simulate_shared_keystrokes("ctrl-v down shift-d").await;
1302 cx.shared_state().await.assert_eq(indoc! {
1303 "Theˇ•
1304 fox•
1305 the lazy dog
1306 "
1307 });
1308 }
1309
1310 #[gpui::test]
1311 async fn test_gv(cx: &mut gpui::TestAppContext) {
1312 let mut cx = NeovimBackedTestContext::new(cx).await;
1313
1314 cx.set_shared_state(indoc! {
1315 "The ˇquick brown"
1316 })
1317 .await;
1318 cx.simulate_shared_keystrokes("v i w escape g v").await;
1319 cx.shared_state().await.assert_eq(indoc! {
1320 "The «quickˇ» brown"
1321 });
1322
1323 cx.simulate_shared_keystrokes("o escape g v").await;
1324 cx.shared_state().await.assert_eq(indoc! {
1325 "The «ˇquick» brown"
1326 });
1327
1328 cx.simulate_shared_keystrokes("escape ^ ctrl-v l").await;
1329 cx.shared_state().await.assert_eq(indoc! {
1330 "«Thˇ»e quick brown"
1331 });
1332 cx.simulate_shared_keystrokes("g v").await;
1333 cx.shared_state().await.assert_eq(indoc! {
1334 "The «ˇquick» brown"
1335 });
1336 cx.simulate_shared_keystrokes("g v").await;
1337 cx.shared_state().await.assert_eq(indoc! {
1338 "«Thˇ»e quick brown"
1339 });
1340
1341 cx.set_state(
1342 indoc! {"
1343 fiˇsh one
1344 fish two
1345 fish red
1346 fish blue
1347 "},
1348 Mode::Normal,
1349 );
1350 cx.simulate_keystrokes("4 g l escape escape g v");
1351 cx.assert_state(
1352 indoc! {"
1353 «fishˇ» one
1354 «fishˇ» two
1355 «fishˇ» red
1356 «fishˇ» blue
1357 "},
1358 Mode::Visual,
1359 );
1360 }
1361}