Skip to repository content2055 lines · 74.7 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T06:47:26.060Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
visual.rs
1use std::sync::Arc;
2
3use collections::HashMap;
4use editor::{
5 Bias, DisplayPoint, Editor, MultiBufferOffset, SelectionEffects,
6 display_map::{DisplaySnapshot, ToDisplayPoint},
7 movement,
8};
9use gpui::{Context, Window, actions};
10use language::{Point, Selection, SelectionGoal};
11use multi_buffer::MultiBufferRow;
12use search::BufferSearchBar;
13use text::TransactionId;
14use util::ResultExt;
15use workspace::searchable::Direction;
16
17use crate::{
18 Vim,
19 motion::{Motion, MotionKind, first_non_whitespace, next_line_end, start_of_line},
20 object::Object,
21 state::{Mark, Mode, Operator},
22};
23
24actions!(
25 vim,
26 [
27 /// Toggles visual mode.
28 ToggleVisual,
29 /// Toggles visual line mode.
30 ToggleVisualLine,
31 /// Toggles visual block mode.
32 ToggleVisualBlock,
33 /// Deletes the visual selection.
34 VisualDelete,
35 /// Deletes entire lines in visual selection.
36 VisualDeleteLine,
37 /// Yanks (copies) the visual selection.
38 VisualYank,
39 /// Yanks entire lines in visual selection.
40 VisualYankLine,
41 /// Moves cursor to the other end of the selection.
42 OtherEnd,
43 /// Moves cursor to the other end of the selection (row-aware).
44 OtherEndRowAware,
45 /// Selects the next occurrence of the current selection.
46 SelectNext,
47 /// Selects the previous occurrence of the current selection.
48 SelectPrevious,
49 /// Selects the next match of the current selection.
50 SelectNextMatch,
51 /// Selects the previous match of the current selection.
52 SelectPreviousMatch,
53 /// Selects the next smaller syntax node.
54 SelectSmallerSyntaxNode,
55 /// Selects the next larger syntax node.
56 SelectLargerSyntaxNode,
57 /// Selects the next syntax node sibling.
58 SelectNextSyntaxNode,
59 /// Selects the previous syntax node sibling.
60 SelectPreviousSyntaxNode,
61 /// Restores the previous visual selection.
62 RestoreVisualSelection,
63 /// Inserts at the end of each line in visual selection.
64 VisualInsertEndOfLine,
65 /// Inserts at the first non-whitespace character of each line.
66 VisualInsertFirstNonWhiteSpace,
67 ]
68);
69
70pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
71 Vim::action(editor, cx, |vim, _: &ToggleVisual, window, cx| {
72 vim.toggle_mode(Mode::Visual, window, cx)
73 });
74 Vim::action(editor, cx, |vim, _: &ToggleVisualLine, window, cx| {
75 vim.toggle_mode(Mode::VisualLine, window, cx)
76 });
77 Vim::action(editor, cx, |vim, _: &ToggleVisualBlock, window, cx| {
78 vim.toggle_mode(Mode::VisualBlock, window, cx)
79 });
80 Vim::action(editor, cx, Vim::other_end);
81 Vim::action(editor, cx, Vim::other_end_row_aware);
82 Vim::action(editor, cx, Vim::visual_insert_end_of_line);
83 Vim::action(editor, cx, Vim::visual_insert_first_non_white_space);
84 Vim::action(editor, cx, |vim, _: &VisualDelete, window, cx| {
85 vim.record_current_action(cx);
86 vim.visual_delete(false, window, cx);
87 });
88 Vim::action(editor, cx, |vim, _: &VisualDeleteLine, window, cx| {
89 vim.record_current_action(cx);
90 vim.visual_delete(true, window, cx);
91 });
92 Vim::action(editor, cx, |vim, _: &VisualYank, window, cx| {
93 vim.visual_yank(false, window, cx)
94 });
95 Vim::action(editor, cx, |vim, _: &VisualYankLine, window, cx| {
96 vim.visual_yank(true, window, cx)
97 });
98
99 Vim::action(editor, cx, Vim::select_next);
100 Vim::action(editor, cx, Vim::select_previous);
101 Vim::action(editor, cx, |vim, _: &SelectNextMatch, window, cx| {
102 vim.select_match(Direction::Next, window, cx);
103 });
104 Vim::action(editor, cx, |vim, _: &SelectPreviousMatch, window, cx| {
105 vim.select_match(Direction::Prev, window, cx);
106 });
107
108 Vim::action(editor, cx, |vim, _: &SelectLargerSyntaxNode, window, cx| {
109 let count = Vim::take_count(cx).unwrap_or(1);
110 Vim::take_forced_motion(cx);
111 for _ in 0..count {
112 vim.update_editor(cx, |_, editor, cx| {
113 editor.select_larger_syntax_node(&Default::default(), window, cx);
114 });
115 }
116 });
117
118 Vim::action(editor, cx, |vim, _: &SelectNextSyntaxNode, window, cx| {
119 let count = Vim::take_count(cx).unwrap_or(1);
120 Vim::take_forced_motion(cx);
121 for _ in 0..count {
122 vim.update_editor(cx, |_, editor, cx| {
123 editor.select_next_syntax_node(&Default::default(), window, cx);
124 });
125 }
126 });
127
128 Vim::action(
129 editor,
130 cx,
131 |vim, _: &SelectPreviousSyntaxNode, window, cx| {
132 let count = Vim::take_count(cx).unwrap_or(1);
133 Vim::take_forced_motion(cx);
134 for _ in 0..count {
135 vim.update_editor(cx, |_, editor, cx| {
136 editor.select_prev_syntax_node(&Default::default(), window, cx);
137 });
138 }
139 },
140 );
141
142 Vim::action(
143 editor,
144 cx,
145 |vim, _: &SelectSmallerSyntaxNode, window, cx| {
146 let count = Vim::take_count(cx).unwrap_or(1);
147 Vim::take_forced_motion(cx);
148 for _ in 0..count {
149 vim.update_editor(cx, |_, editor, cx| {
150 editor.select_smaller_syntax_node(&Default::default(), window, cx);
151 });
152 }
153 },
154 );
155
156 Vim::action(editor, cx, |vim, _: &RestoreVisualSelection, window, cx| {
157 let Some((stored_mode, reversed)) = vim.stored_visual_mode.take() else {
158 return;
159 };
160 let marks = vim
161 .update_editor(cx, |vim, editor, cx| {
162 vim.get_mark("<", editor, window, cx)
163 .zip(vim.get_mark(">", editor, window, cx))
164 })
165 .flatten();
166 let Some((Mark::Local(start), Mark::Local(end))) = marks else {
167 return;
168 };
169 let ranges = start
170 .iter()
171 .zip(end)
172 .zip(reversed)
173 .map(|((start, end), reversed)| (*start, end, reversed))
174 .collect::<Vec<_>>();
175
176 if vim.mode.is_visual() {
177 vim.create_visual_marks(vim.mode, window, cx);
178 }
179
180 vim.update_editor(cx, |_, editor, cx| {
181 editor.set_clip_at_line_ends(false, cx);
182 editor.change_selections(Default::default(), window, cx, |s| {
183 let map = s.display_snapshot();
184 let ranges = ranges
185 .into_iter()
186 .map(|(start, end, reversed)| {
187 let mut new_end =
188 movement::saturating_right(&map, end.to_display_point(&map));
189 let mut new_start = start.to_display_point(&map);
190 if new_start >= new_end {
191 if new_end.column() == 0 {
192 new_end = movement::right(&map, new_end)
193 } else {
194 new_start = movement::saturating_left(&map, new_end);
195 }
196 }
197 Selection {
198 id: s.new_selection_id(),
199 start: new_start.to_point(&map),
200 end: new_end.to_point(&map),
201 reversed,
202 goal: SelectionGoal::None,
203 }
204 })
205 .collect();
206 s.select(ranges);
207 })
208 });
209 vim.switch_mode(stored_mode, true, window, cx)
210 });
211}
212
213impl Vim {
214 pub fn visual_motion(
215 &mut self,
216 motion: Motion,
217 times: Option<usize>,
218 window: &mut Window,
219 cx: &mut Context<Self>,
220 ) {
221 self.update_editor(cx, |vim, editor, cx| {
222 let text_layout_details = editor.text_layout_details(window, cx);
223 if vim.mode == Mode::VisualBlock
224 && !matches!(
225 motion,
226 Motion::EndOfLine {
227 display_lines: false
228 }
229 )
230 {
231 let is_up_or_down = matches!(motion, Motion::Up { .. } | Motion::Down { .. });
232 vim.visual_block_motion(
233 is_up_or_down,
234 editor,
235 window,
236 cx,
237 &mut |map, point, goal| {
238 motion.move_point(map, point, goal, times, &text_layout_details)
239 },
240 )
241 } else {
242 editor.change_selections(Default::default(), window, cx, |s| {
243 s.move_with(&mut |map, selection| {
244 let was_reversed = selection.reversed;
245 let mut current_head = selection.head();
246
247 // our motions assume the current character is after the cursor,
248 // but in (forward) visual mode the current character is just
249 // before the end of the selection.
250 if !selection.reversed && !selection.is_empty() {
251 current_head = movement::left(map, selection.end)
252 }
253
254 let Some((new_head, goal)) = motion.move_point(
255 map,
256 current_head,
257 selection.goal,
258 times,
259 &text_layout_details,
260 ) else {
261 return;
262 };
263
264 selection.set_head(new_head, goal);
265
266 // ensure the current character is included in the selection.
267 if !selection.reversed {
268 let next_point = if vim.mode == Mode::VisualBlock {
269 movement::saturating_right(map, selection.end)
270 } else {
271 movement::right(map, selection.end)
272 };
273
274 selection.end = next_point;
275 }
276
277 // vim always ensures the anchor character stays selected.
278 // if our selection has reversed, we need to move the opposite end
279 // to ensure the anchor is still selected.
280 if was_reversed && !selection.reversed {
281 selection.start = movement::left(map, selection.start);
282 } else if !was_reversed && selection.reversed {
283 selection.end = movement::right(map, selection.end);
284 }
285 })
286 });
287 }
288 });
289 }
290
291 pub fn visual_block_motion(
292 &mut self,
293 preserve_goal: bool,
294 editor: &mut Editor,
295 window: &mut Window,
296 cx: &mut Context<Editor>,
297 move_selection: &mut dyn FnMut(
298 &DisplaySnapshot,
299 DisplayPoint,
300 SelectionGoal,
301 ) -> Option<(DisplayPoint, SelectionGoal)>,
302 ) {
303 let text_layout_details = editor.text_layout_details(window, cx);
304 editor.change_selections(Default::default(), window, cx, |s| {
305 let map = &s.display_snapshot();
306 let mut head = s.newest_anchor().head().to_display_point(map);
307 let mut tail = s.oldest_anchor().tail().to_display_point(map);
308
309 let mut head_x = map.x_for_display_point(head, &text_layout_details);
310 let mut tail_x = map.x_for_display_point(tail, &text_layout_details);
311
312 let (start, end) = match s.newest_anchor().goal {
313 SelectionGoal::HorizontalRange { start, end } if preserve_goal => (start, end),
314 SelectionGoal::HorizontalPosition(start) if preserve_goal => (start, start),
315 _ => (tail_x.into(), head_x.into()),
316 };
317 let mut goal = SelectionGoal::HorizontalRange { start, end };
318
319 let was_reversed = tail_x > head_x;
320 if !was_reversed && !preserve_goal {
321 head = movement::saturating_left(map, head);
322 }
323
324 let reverse_aware_goal = if was_reversed {
325 SelectionGoal::HorizontalRange {
326 start: end,
327 end: start,
328 }
329 } else {
330 goal
331 };
332
333 let Some((new_head, _)) = move_selection(map, head, reverse_aware_goal) else {
334 return;
335 };
336 head = new_head;
337 head_x = map.x_for_display_point(head, &text_layout_details);
338
339 let is_reversed = tail_x > head_x;
340 if was_reversed && !is_reversed {
341 tail = movement::saturating_left(map, tail);
342 tail_x = map.x_for_display_point(tail, &text_layout_details);
343 } else if !was_reversed && is_reversed {
344 tail = movement::saturating_right(map, tail);
345 tail_x = map.x_for_display_point(tail, &text_layout_details);
346 }
347 if !is_reversed && !preserve_goal {
348 head = movement::saturating_right(map, head);
349 head_x = map.x_for_display_point(head, &text_layout_details);
350 }
351
352 let positions = if is_reversed {
353 head_x..tail_x
354 } else {
355 tail_x..head_x
356 };
357
358 if !preserve_goal {
359 goal = SelectionGoal::HorizontalRange {
360 start: f64::from(positions.start),
361 end: f64::from(positions.end),
362 };
363 }
364
365 let mut selections = Vec::new();
366 let mut row = tail.row();
367 let going_up = tail.row() > head.row();
368 let direction = if going_up { -1 } else { 1 };
369
370 loop {
371 let laid_out_line = map.layout_row(row, &text_layout_details);
372 let start = DisplayPoint::new(
373 row,
374 laid_out_line.closest_index_for_x(positions.start) as u32,
375 );
376 let mut end =
377 DisplayPoint::new(row, laid_out_line.closest_index_for_x(positions.end) as u32);
378 if end <= start {
379 if start.column() == map.line_len(start.row()) {
380 end = start;
381 } else {
382 end = movement::saturating_right(map, start);
383 }
384 }
385
386 if positions.start <= laid_out_line.width {
387 let selection = Selection {
388 id: s.new_selection_id(),
389 start: start.to_point(map),
390 end: end.to_point(map),
391 reversed: is_reversed &&
392 // For neovim parity: cursor is not reversed when column is a single character
393 end.column() - start.column() > 1,
394 goal,
395 };
396
397 selections.push(selection);
398 }
399
400 // When dealing with soft wrapped lines, it's possible that
401 // `row` ends up being set to a value other than `head.row()` as
402 // `head.row()` might be a `DisplayPoint` mapped to a soft
403 // wrapped line, hence the need for `<=` and `>=` instead of
404 // `==`.
405 if going_up && row <= head.row() || !going_up && row >= head.row() {
406 break;
407 }
408
409 // Find the next or previous buffer row where the `row` should
410 // be moved to, so that wrapped lines are skipped.
411 row = map
412 .start_of_relative_buffer_row(DisplayPoint::new(row, 0), direction)
413 .row();
414 }
415
416 s.select(selections);
417 })
418 }
419
420 pub fn visual_object(
421 &mut self,
422 object: Object,
423 count: Option<usize>,
424 window: &mut Window,
425 cx: &mut Context<Vim>,
426 ) {
427 if let Some(Operator::Object { around }) = self.active_operator() {
428 self.pop_operator(window, cx);
429 let current_mode = self.mode;
430 let target_mode = object.target_visual_mode(current_mode, around);
431 if target_mode != current_mode {
432 self.switch_mode(target_mode, true, window, cx);
433 }
434
435 self.update_editor(cx, |_, editor, cx| {
436 editor.change_selections(Default::default(), window, cx, |s| {
437 s.move_with(&mut |map, selection| {
438 let mut mut_selection = selection.clone();
439
440 // all our motions assume that the current character is
441 // after the cursor; however in the case of a visual selection
442 // the current character is before the cursor.
443 // But this will affect the judgment of the html tag
444 // so the html tag needs to skip this logic.
445 if !selection.reversed && object != Object::Tag {
446 mut_selection.set_head(
447 movement::left(map, mut_selection.head()),
448 mut_selection.goal,
449 );
450 }
451
452 let original_point = selection.tail().to_point(map);
453
454 if let Some(range) = object.range(map, mut_selection, around, count) {
455 if !range.is_empty() {
456 let expand_both_ways = object.always_expands_both_ways()
457 || selection.is_empty()
458 || movement::right(map, selection.start) == selection.end;
459
460 if expand_both_ways {
461 if selection.start == range.start
462 && selection.end == range.end
463 && object.always_expands_both_ways()
464 {
465 if let Some(range) =
466 object.range(map, selection.clone(), around, count)
467 {
468 selection.start = range.start;
469 selection.end = range.end;
470 }
471 } else {
472 selection.start = range.start;
473 selection.end = range.end;
474 }
475 } else if selection.reversed {
476 selection.start = range.start;
477 } else {
478 selection.end = range.end;
479 }
480 }
481
482 // In the visual selection result of a paragraph object, the cursor is
483 // placed at the start of the last line. And in the visual mode, the
484 // selection end is located after the end character. So, adjustment of
485 // selection end is needed.
486 //
487 // We don't do this adjustment for a one-line blank paragraph since the
488 // trailing newline is included in its selection from the beginning.
489 if object == Object::Paragraph && range.start != range.end {
490 let row_of_selection_end_line = selection.end.to_point(map).row;
491 let new_selection_end = if map
492 .buffer_snapshot()
493 .line_len(MultiBufferRow(row_of_selection_end_line))
494 == 0
495 {
496 Point::new(row_of_selection_end_line + 1, 0)
497 } else {
498 Point::new(row_of_selection_end_line, 1)
499 };
500 selection.end = new_selection_end.to_display_point(map);
501 }
502
503 // To match vim, if the range starts of the same line as it originally
504 // did, we keep the tail of the selection in the same place instead of
505 // snapping it to the start of the line
506 if target_mode == Mode::VisualLine {
507 let new_start_point = selection.start.to_point(map);
508 if new_start_point.row == original_point.row {
509 if selection.end.to_point(map).row > new_start_point.row {
510 if original_point.column
511 == map
512 .buffer_snapshot()
513 .line_len(MultiBufferRow(original_point.row))
514 {
515 selection.start = movement::saturating_left(
516 map,
517 original_point.to_display_point(map),
518 )
519 } else {
520 selection.start = original_point.to_display_point(map)
521 }
522 } else {
523 let original_display_point =
524 original_point.to_display_point(map);
525 if selection.end <= original_display_point {
526 selection.end = movement::saturating_right(
527 map,
528 original_display_point,
529 );
530 if original_point.column > 0 {
531 selection.reversed = true
532 }
533 }
534 }
535 }
536 }
537 }
538 });
539 });
540 });
541 }
542 }
543
544 fn visual_insert_end_of_line(
545 &mut self,
546 _: &VisualInsertEndOfLine,
547 window: &mut Window,
548 cx: &mut Context<Self>,
549 ) {
550 self.update_editor(cx, |_, editor, cx| {
551 editor.split_selection_into_lines(&Default::default(), window, cx);
552 editor.change_selections(Default::default(), window, cx, |s| {
553 s.move_cursors_with(&mut |map, cursor, _| {
554 (next_line_end(map, cursor, 1), SelectionGoal::None)
555 });
556 });
557 });
558
559 self.switch_mode(Mode::Insert, false, window, cx);
560 }
561
562 fn visual_insert_first_non_white_space(
563 &mut self,
564 _: &VisualInsertFirstNonWhiteSpace,
565 window: &mut Window,
566 cx: &mut Context<Self>,
567 ) {
568 self.update_editor(cx, |_, editor, cx| {
569 editor.split_selection_into_lines(&Default::default(), window, cx);
570 editor.change_selections(Default::default(), window, cx, |s| {
571 s.move_cursors_with(&mut |map, cursor, _| {
572 (
573 first_non_whitespace(map, false, cursor),
574 SelectionGoal::None,
575 )
576 });
577 });
578 });
579
580 self.switch_mode(Mode::Insert, false, window, cx);
581 }
582
583 fn toggle_mode(&mut self, mode: Mode, window: &mut Window, cx: &mut Context<Self>) {
584 if self.mode == mode {
585 self.switch_mode(Mode::Normal, false, window, cx);
586 } else {
587 self.switch_mode(mode, false, window, cx);
588 }
589 }
590
591 pub fn other_end(&mut self, _: &OtherEnd, window: &mut Window, cx: &mut Context<Self>) {
592 self.update_editor(cx, |_, editor, cx| {
593 editor.change_selections(Default::default(), window, cx, |s| {
594 s.move_with(&mut |_, selection| {
595 selection.reversed = !selection.reversed;
596 });
597 })
598 });
599 }
600
601 pub fn other_end_row_aware(
602 &mut self,
603 _: &OtherEndRowAware,
604 window: &mut Window,
605 cx: &mut Context<Self>,
606 ) {
607 let mode = self.mode;
608 self.update_editor(cx, |_, editor, cx| {
609 editor.change_selections(Default::default(), window, cx, |s| {
610 s.move_with(&mut |_, selection| {
611 selection.reversed = !selection.reversed;
612 });
613 if mode == Mode::VisualBlock {
614 s.reverse_selections();
615 }
616 })
617 });
618 }
619
620 pub fn visual_delete(
621 &mut self,
622 line_mode: bool,
623 window: &mut Window,
624 cx: &mut Context<Self>,
625 ) -> Option<TransactionId> {
626 self.store_visual_marks(window, cx);
627 let transaction_id = self.update_editor(cx, |vim, editor, cx| {
628 let mut original_columns: HashMap<_, _> = Default::default();
629 let line_mode = line_mode || editor.selections.line_mode();
630 editor.selections.set_line_mode(false);
631
632 editor.transact(window, cx, |editor, window, cx| {
633 editor.change_selections(Default::default(), window, cx, |s| {
634 s.move_with(&mut |map, selection| {
635 if line_mode {
636 let mut position = selection.head();
637 if !selection.reversed {
638 position = movement::left(map, position);
639 }
640 original_columns.insert(selection.id, position.to_point(map).column);
641 if vim.mode == Mode::VisualBlock {
642 *selection.end.column_mut() = map.line_len(selection.end.row())
643 } else {
644 let start = selection.start.to_point(map);
645 let end = selection.end.to_point(map);
646 selection.start = map.prev_line_boundary(start).1;
647 if end.column == 0 && end > start {
648 let row = end.row.saturating_sub(1);
649 selection.end = Point::new(
650 row,
651 map.buffer_snapshot().line_len(MultiBufferRow(row)),
652 )
653 .to_display_point(map)
654 } else {
655 selection.end = map.next_line_boundary(end).1;
656 }
657 }
658 }
659 selection.goal = SelectionGoal::None;
660 });
661 });
662 let kind = if line_mode {
663 MotionKind::Linewise
664 } else {
665 MotionKind::Exclusive
666 };
667 vim.copy_selections_content(editor, kind, window, cx);
668
669 if line_mode && vim.mode != Mode::VisualBlock {
670 editor.change_selections(Default::default(), window, cx, |s| {
671 s.move_with(&mut |map, selection| {
672 let end = selection.end.to_point(map);
673 let start = selection.start.to_point(map);
674 if end.row < map.buffer_snapshot().max_point().row {
675 selection.end = Point::new(end.row + 1, 0).to_display_point(map)
676 } else if start.row > 0 {
677 selection.start = Point::new(
678 start.row - 1,
679 map.buffer_snapshot()
680 .line_len(MultiBufferRow(start.row - 1)),
681 )
682 .to_display_point(map)
683 }
684 });
685 });
686 }
687 editor.delete_selections_with_linked_edits(window, cx);
688
689 // Fixup cursor position after the deletion. Helix keeps the
690 // cursor on the trailing newline, so only clip in Vim modes.
691 editor.set_clip_at_line_ends(!vim.mode.is_helix(), cx);
692 editor.change_selections(Default::default(), window, cx, |s| {
693 s.move_with(&mut |map, selection| {
694 let mut cursor = selection.head().to_point(map);
695
696 if let Some(column) = original_columns.get(&selection.id) {
697 cursor.column = *column
698 }
699 let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left);
700 selection.collapse_to(cursor, selection.goal)
701 });
702 if vim.mode == Mode::VisualBlock {
703 s.select_anchors(vec![s.first_anchor()])
704 }
705 });
706 })
707 });
708 let transaction_id = transaction_id.flatten();
709 self.switch_mode(Mode::Normal, true, window, cx);
710 transaction_id
711 }
712
713 pub fn visual_yank(&mut self, line_mode: bool, window: &mut Window, cx: &mut Context<Self>) {
714 self.store_visual_marks(window, cx);
715 self.update_editor(cx, |vim, editor, cx| {
716 let line_mode = line_mode || editor.selections.line_mode();
717
718 // For visual line mode, adjust selections to avoid yanking the next line when on \n
719 if line_mode && vim.mode != Mode::VisualBlock {
720 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
721 s.move_with(&mut |map, selection| {
722 let start = selection.start.to_point(map);
723 let end = selection.end.to_point(map);
724 if end.column == 0 && end > start {
725 let row = end.row.saturating_sub(1);
726 selection.end = Point::new(
727 row,
728 map.buffer_snapshot().line_len(MultiBufferRow(row)),
729 )
730 .to_display_point(map);
731 }
732 });
733 });
734 }
735
736 editor.selections.set_line_mode(line_mode);
737 let kind = if line_mode {
738 MotionKind::Linewise
739 } else {
740 MotionKind::Exclusive
741 };
742 vim.yank_selections_content(editor, kind, window, cx);
743 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
744 s.move_with(&mut |map, selection| {
745 if line_mode {
746 selection.start = start_of_line(map, false, selection.start);
747 };
748 selection.collapse_to(selection.start, SelectionGoal::None)
749 });
750 if vim.mode == Mode::VisualBlock {
751 s.select_anchors(vec![s.first_anchor()])
752 }
753 });
754 });
755 self.switch_mode(Mode::Normal, true, window, cx);
756 }
757
758 pub(crate) fn visual_replace(
759 &mut self,
760 text: Arc<str>,
761 window: &mut Window,
762 cx: &mut Context<Self>,
763 ) {
764 self.stop_recording(cx);
765 self.update_editor(cx, |_, editor, cx| {
766 editor.transact(window, cx, |editor, window, cx| {
767 let display_map = editor.display_snapshot(cx);
768 let selections = editor.selections.all_adjusted_display(&display_map);
769
770 // Selections are biased right at the start. So we need to store
771 // anchors that are biased left so that we can restore the selections
772 // after the change
773 let stable_anchors = editor
774 .selections
775 .disjoint_anchors_arc()
776 .iter()
777 .map(|selection| {
778 let start = selection.start.bias_left(&display_map.buffer_snapshot());
779 start..start
780 })
781 .collect::<Vec<_>>();
782
783 let mut edits = Vec::new();
784 for selection in selections.iter() {
785 let selection = selection.clone();
786 for row_range in
787 movement::split_display_range_by_lines(&display_map, selection.range())
788 {
789 let range = row_range.start.to_offset(&display_map, Bias::Right)
790 ..row_range.end.to_offset(&display_map, Bias::Right);
791 let grapheme_count = display_map
792 .buffer_snapshot()
793 .grapheme_count_for_range(&range);
794 let text = text.repeat(grapheme_count);
795 edits.push((range, text));
796 }
797 }
798
799 editor.edit(edits, cx);
800 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
801 s.select_ranges(stable_anchors)
802 });
803 });
804 });
805 self.switch_mode(Mode::Normal, false, window, cx);
806 }
807
808 pub fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
809 Vim::take_forced_motion(cx);
810 let count =
811 Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 });
812 self.update_editor(cx, |_, editor, cx| {
813 editor.set_clip_at_line_ends(false, cx);
814 for _ in 0..count {
815 if editor
816 .select_next(&Default::default(), window, cx)
817 .log_err()
818 .is_none()
819 {
820 break;
821 }
822 }
823 });
824 }
825
826 pub fn select_previous(
827 &mut self,
828 _: &SelectPrevious,
829 window: &mut Window,
830 cx: &mut Context<Self>,
831 ) {
832 Vim::take_forced_motion(cx);
833 let count =
834 Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 });
835 self.update_editor(cx, |_, editor, cx| {
836 for _ in 0..count {
837 if editor
838 .select_previous(&Default::default(), window, cx)
839 .log_err()
840 .is_none()
841 {
842 break;
843 }
844 }
845 });
846 }
847
848 pub fn select_match(
849 &mut self,
850 direction: Direction,
851 window: &mut Window,
852 cx: &mut Context<Self>,
853 ) {
854 Vim::take_forced_motion(cx);
855 let count = Vim::take_count(cx).unwrap_or(1);
856 let Some(pane) = self.pane(window, cx) else {
857 return;
858 };
859 let vim_is_normal = self.mode == Mode::Normal;
860 let mut start_selection = MultiBufferOffset(0);
861 let mut end_selection = MultiBufferOffset(0);
862
863 self.update_editor(cx, |_, editor, _| {
864 editor.set_collapse_matches(false);
865 });
866 if vim_is_normal {
867 pane.update(cx, |pane, cx| {
868 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()
869 {
870 search_bar.update(cx, |search_bar, cx| {
871 if !search_bar.has_active_match() || !search_bar.show(window, cx) {
872 return;
873 }
874 // without update_match_index there is a bug when the cursor is before the first match
875 search_bar.update_match_index(window, cx);
876 search_bar.select_match(direction.opposite(), 1, window, cx);
877 });
878 }
879 });
880 }
881 self.update_editor(cx, |_, editor, cx| {
882 let latest = editor
883 .selections
884 .newest::<MultiBufferOffset>(&editor.display_snapshot(cx));
885 start_selection = latest.start;
886 end_selection = latest.end;
887 });
888
889 let mut match_exists = false;
890 pane.update(cx, |pane, cx| {
891 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
892 search_bar.update(cx, |search_bar, cx| {
893 search_bar.update_match_index(window, cx);
894 search_bar.select_match(direction, count, window, cx);
895 match_exists = search_bar.match_exists(window, cx);
896 });
897 }
898 });
899 if !match_exists {
900 self.update_editor(cx, |_, editor, _| {
901 editor.set_collapse_matches(true);
902 });
903 self.clear_operator(window, cx);
904 self.stop_replaying(cx);
905 return;
906 }
907 self.update_editor(cx, |_, editor, cx| {
908 let latest = editor
909 .selections
910 .newest::<MultiBufferOffset>(&editor.display_snapshot(cx));
911 if vim_is_normal {
912 start_selection = latest.start;
913 end_selection = latest.end;
914 } else {
915 start_selection = start_selection.min(latest.start);
916 end_selection = end_selection.max(latest.end);
917 }
918 if direction == Direction::Prev {
919 std::mem::swap(&mut start_selection, &mut end_selection);
920 }
921 editor.change_selections(Default::default(), window, cx, |s| {
922 s.select_ranges([start_selection..end_selection]);
923 });
924 editor.set_collapse_matches(true);
925 });
926
927 match self.maybe_pop_operator() {
928 Some(Operator::Change) => self.substitute(None, false, window, cx),
929 Some(Operator::Delete) => {
930 self.stop_recording(cx);
931 self.visual_delete(false, window, cx);
932 }
933 Some(Operator::Yank) => self.visual_yank(false, window, cx),
934 _ => {} // Ignoring other operators
935 }
936 }
937}
938#[cfg(test)]
939mod test {
940 use indoc::indoc;
941 use workspace::item::Item;
942
943 use crate::{
944 state::Mode,
945 test::{NeovimBackedTestContext, VimTestContext},
946 };
947
948 #[gpui::test]
949 async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
950 let mut cx = NeovimBackedTestContext::new(cx).await;
951
952 cx.set_shared_state(indoc! {
953 "The ˇquick brown
954 fox jumps over
955 the lazy dog"
956 })
957 .await;
958 let cursor = cx.update_editor(|editor, _, cx| editor.pixel_position_of_cursor(cx));
959
960 // entering visual mode should select the character
961 // under cursor
962 cx.simulate_shared_keystrokes("v").await;
963 cx.shared_state()
964 .await
965 .assert_eq(indoc! { "The «qˇ»uick brown
966 fox jumps over
967 the lazy dog"});
968 cx.update_editor(|editor, _, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
969
970 // forwards motions should extend the selection
971 cx.simulate_shared_keystrokes("w j").await;
972 cx.shared_state().await.assert_eq(indoc! { "The «quick brown
973 fox jumps oˇ»ver
974 the lazy dog"});
975
976 cx.simulate_shared_keystrokes("escape").await;
977 cx.shared_state().await.assert_eq(indoc! { "The quick brown
978 fox jumps ˇover
979 the lazy dog"});
980
981 // motions work backwards
982 cx.simulate_shared_keystrokes("v k b").await;
983 cx.shared_state()
984 .await
985 .assert_eq(indoc! { "The «ˇquick brown
986 fox jumps o»ver
987 the lazy dog"});
988
989 // works on empty lines
990 cx.set_shared_state(indoc! {"
991 a
992 ˇ
993 b
994 "})
995 .await;
996 let cursor = cx.update_editor(|editor, _, cx| editor.pixel_position_of_cursor(cx));
997 cx.simulate_shared_keystrokes("v").await;
998 cx.shared_state().await.assert_eq(indoc! {"
999 a
1000 «
1001 ˇ»b
1002 "});
1003 cx.update_editor(|editor, _, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
1004
1005 // toggles off again
1006 cx.simulate_shared_keystrokes("v").await;
1007 cx.shared_state().await.assert_eq(indoc! {"
1008 a
1009 ˇ
1010 b
1011 "});
1012
1013 // works at the end of a document
1014 cx.set_shared_state(indoc! {"
1015 a
1016 b
1017 ˇ"})
1018 .await;
1019
1020 cx.simulate_shared_keystrokes("v").await;
1021 cx.shared_state().await.assert_eq(indoc! {"
1022 a
1023 b
1024 ˇ"});
1025 }
1026
1027 #[gpui::test]
1028 async fn test_visual_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
1029 let mut cx = VimTestContext::new(cx, true).await;
1030
1031 cx.set_state(
1032 indoc! {
1033 "«The quick brown
1034 fox jumps over
1035 the lazy dogˇ»"
1036 },
1037 Mode::Visual,
1038 );
1039 cx.simulate_keystrokes("g shift-i");
1040 cx.assert_state(
1041 indoc! {
1042 "ˇThe quick brown
1043 ˇfox jumps over
1044 ˇthe lazy dog"
1045 },
1046 Mode::Insert,
1047 );
1048 }
1049
1050 #[gpui::test]
1051 async fn test_visual_insert_end_of_line(cx: &mut gpui::TestAppContext) {
1052 let mut cx = VimTestContext::new(cx, true).await;
1053
1054 cx.set_state(
1055 indoc! {
1056 "«The quick brown
1057 fox jumps over
1058 the lazy dogˇ»"
1059 },
1060 Mode::Visual,
1061 );
1062 cx.simulate_keystrokes("g shift-a");
1063 cx.assert_state(
1064 indoc! {
1065 "The quick brownˇ
1066 fox jumps overˇ
1067 the lazy dogˇ"
1068 },
1069 Mode::Insert,
1070 );
1071 }
1072
1073 #[gpui::test]
1074 async fn test_enter_visual_line_mode(cx: &mut gpui::TestAppContext) {
1075 let mut cx = NeovimBackedTestContext::new(cx).await;
1076
1077 cx.set_shared_state(indoc! {
1078 "The ˇquick brown
1079 fox jumps over
1080 the lazy dog"
1081 })
1082 .await;
1083 cx.simulate_shared_keystrokes("shift-v").await;
1084 cx.shared_state()
1085 .await
1086 .assert_eq(indoc! { "The «qˇ»uick brown
1087 fox jumps over
1088 the lazy dog"});
1089 cx.simulate_shared_keystrokes("x").await;
1090 cx.shared_state().await.assert_eq(indoc! { "fox ˇjumps over
1091 the lazy dog"});
1092
1093 // it should work on empty lines
1094 cx.set_shared_state(indoc! {"
1095 a
1096 ˇ
1097 b"})
1098 .await;
1099 cx.simulate_shared_keystrokes("shift-v").await;
1100 cx.shared_state().await.assert_eq(indoc! {"
1101 a
1102 «
1103 ˇ»b"});
1104 cx.simulate_shared_keystrokes("x").await;
1105 cx.shared_state().await.assert_eq(indoc! {"
1106 a
1107 ˇb"});
1108
1109 // it should work at the end of the document
1110 cx.set_shared_state(indoc! {"
1111 a
1112 b
1113 ˇ"})
1114 .await;
1115 let cursor = cx.update_editor(|editor, _, cx| editor.pixel_position_of_cursor(cx));
1116 cx.simulate_shared_keystrokes("shift-v").await;
1117 cx.shared_state().await.assert_eq(indoc! {"
1118 a
1119 b
1120 ˇ"});
1121 cx.update_editor(|editor, _, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
1122 cx.simulate_shared_keystrokes("x").await;
1123 cx.shared_state().await.assert_eq(indoc! {"
1124 a
1125 ˇb"});
1126 }
1127
1128 #[gpui::test]
1129 async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
1130 let mut cx = NeovimBackedTestContext::new(cx).await;
1131
1132 cx.simulate("v w", "The quick ˇbrown")
1133 .await
1134 .assert_matches();
1135
1136 cx.simulate("v w x", "The quick ˇbrown")
1137 .await
1138 .assert_matches();
1139 cx.simulate(
1140 "v w j x",
1141 indoc! {"
1142 The ˇquick brown
1143 fox jumps over
1144 the lazy dog"},
1145 )
1146 .await
1147 .assert_matches();
1148 // Test pasting code copied on delete
1149 cx.simulate_shared_keystrokes("j p").await;
1150 cx.shared_state().await.assert_matches();
1151
1152 cx.simulate_at_each_offset(
1153 "v w j x",
1154 indoc! {"
1155 The ˇquick brown
1156 fox jumps over
1157 the ˇlazy dog"},
1158 )
1159 .await
1160 .assert_matches();
1161 cx.simulate_at_each_offset(
1162 "v b k x",
1163 indoc! {"
1164 The ˇquick brown
1165 fox jumps ˇover
1166 the ˇlazy dog"},
1167 )
1168 .await
1169 .assert_matches();
1170 }
1171
1172 #[gpui::test]
1173 async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
1174 let mut cx = NeovimBackedTestContext::new(cx).await;
1175
1176 cx.set_shared_state(indoc! {"
1177 The quˇick brown
1178 fox jumps over
1179 the lazy dog"})
1180 .await;
1181 cx.simulate_shared_keystrokes("shift-v x").await;
1182 cx.shared_state().await.assert_matches();
1183
1184 // Test pasting code copied on delete
1185 cx.simulate_shared_keystrokes("p").await;
1186 cx.shared_state().await.assert_matches();
1187
1188 cx.set_shared_state(indoc! {"
1189 The quick brown
1190 fox jumps over
1191 the laˇzy dog"})
1192 .await;
1193 cx.simulate_shared_keystrokes("shift-v x").await;
1194 cx.shared_state().await.assert_matches();
1195 cx.shared_clipboard().await.assert_eq("the lazy dog\n");
1196
1197 cx.set_shared_state(indoc! {"
1198 The quˇick brown
1199 fox jumps over
1200 the lazy dog"})
1201 .await;
1202 cx.simulate_shared_keystrokes("shift-v j x").await;
1203 cx.shared_state().await.assert_matches();
1204 // Test pasting code copied on delete
1205 cx.simulate_shared_keystrokes("p").await;
1206 cx.shared_state().await.assert_matches();
1207
1208 cx.set_shared_state(indoc! {"
1209 The ˇlong line
1210 should not
1211 crash
1212 "})
1213 .await;
1214 cx.simulate_shared_keystrokes("shift-v $ x").await;
1215 cx.shared_state().await.assert_matches();
1216 }
1217
1218 #[gpui::test]
1219 async fn test_visual_yank(cx: &mut gpui::TestAppContext) {
1220 let mut cx = NeovimBackedTestContext::new(cx).await;
1221
1222 cx.set_shared_state("The quick ˇbrown").await;
1223 cx.simulate_shared_keystrokes("v w y").await;
1224 cx.shared_state().await.assert_eq("The quick ˇbrown");
1225 cx.shared_clipboard().await.assert_eq("brown");
1226
1227 cx.set_shared_state(indoc! {"
1228 The ˇquick brown
1229 fox jumps over
1230 the lazy dog"})
1231 .await;
1232 cx.simulate_shared_keystrokes("v w j y").await;
1233 cx.shared_state().await.assert_eq(indoc! {"
1234 The ˇquick brown
1235 fox jumps over
1236 the lazy dog"});
1237 cx.shared_clipboard().await.assert_eq(indoc! {"
1238 quick brown
1239 fox jumps o"});
1240
1241 cx.set_shared_state(indoc! {"
1242 The quick brown
1243 fox jumps over
1244 the ˇlazy dog"})
1245 .await;
1246 cx.simulate_shared_keystrokes("v w j y").await;
1247 cx.shared_state().await.assert_eq(indoc! {"
1248 The quick brown
1249 fox jumps over
1250 the ˇlazy dog"});
1251 cx.shared_clipboard().await.assert_eq("lazy d");
1252 cx.simulate_shared_keystrokes("shift-v y").await;
1253 cx.shared_clipboard().await.assert_eq("the lazy dog\n");
1254
1255 cx.set_shared_state(indoc! {"
1256 The ˇquick brown
1257 fox jumps over
1258 the lazy dog"})
1259 .await;
1260 cx.simulate_shared_keystrokes("v b k y").await;
1261 cx.shared_state().await.assert_eq(indoc! {"
1262 ˇThe quick brown
1263 fox jumps over
1264 the lazy dog"});
1265 assert_eq!(
1266 cx.read_from_clipboard()
1267 .map(|item| item.text().unwrap())
1268 .unwrap(),
1269 "The q"
1270 );
1271
1272 cx.set_shared_state(indoc! {"
1273 The quick brown
1274 fox ˇjumps over
1275 the lazy dog"})
1276 .await;
1277 cx.simulate_shared_keystrokes("shift-v shift-g shift-y")
1278 .await;
1279 cx.shared_state().await.assert_eq(indoc! {"
1280 The quick brown
1281 ˇfox jumps over
1282 the lazy dog"});
1283 cx.shared_clipboard()
1284 .await
1285 .assert_eq("fox jumps over\nthe lazy dog\n");
1286
1287 cx.set_shared_state(indoc! {"
1288 The quick brown
1289 fox ˇjumps over
1290 the lazy dog"})
1291 .await;
1292 cx.simulate_shared_keystrokes("shift-v $ shift-y").await;
1293 cx.shared_state().await.assert_eq(indoc! {"
1294 The quick brown
1295 ˇfox jumps over
1296 the lazy dog"});
1297 cx.shared_clipboard().await.assert_eq("fox jumps over\n");
1298 }
1299
1300 #[gpui::test]
1301 async fn test_visual_block_mode(cx: &mut gpui::TestAppContext) {
1302 let mut cx = NeovimBackedTestContext::new(cx).await;
1303
1304 cx.set_shared_state(indoc! {
1305 "The ˇquick brown
1306 fox jumps over
1307 the lazy dog"
1308 })
1309 .await;
1310 cx.simulate_shared_keystrokes("ctrl-v").await;
1311 cx.shared_state().await.assert_eq(indoc! {
1312 "The «qˇ»uick brown
1313 fox jumps over
1314 the lazy dog"
1315 });
1316 cx.simulate_shared_keystrokes("2 down").await;
1317 cx.shared_state().await.assert_eq(indoc! {
1318 "The «qˇ»uick brown
1319 fox «jˇ»umps over
1320 the «lˇ»azy dog"
1321 });
1322 cx.simulate_shared_keystrokes("e").await;
1323 cx.shared_state().await.assert_eq(indoc! {
1324 "The «quicˇ»k brown
1325 fox «jumpˇ»s over
1326 the «lazyˇ» dog"
1327 });
1328 cx.simulate_shared_keystrokes("^").await;
1329 cx.shared_state().await.assert_eq(indoc! {
1330 "«ˇThe q»uick brown
1331 «ˇfox j»umps over
1332 «ˇthe l»azy dog"
1333 });
1334 cx.simulate_shared_keystrokes("$").await;
1335 cx.shared_state().await.assert_eq(indoc! {
1336 "The «quick brownˇ»
1337 fox «jumps overˇ»
1338 the «lazy dogˇ»"
1339 });
1340 cx.simulate_shared_keystrokes("shift-f space").await;
1341 cx.shared_state().await.assert_eq(indoc! {
1342 "The «quickˇ» brown
1343 fox «jumpsˇ» over
1344 the «lazy ˇ»dog"
1345 });
1346
1347 // toggling through visual mode works as expected
1348 cx.simulate_shared_keystrokes("v").await;
1349 cx.shared_state().await.assert_eq(indoc! {
1350 "The «quick brown
1351 fox jumps over
1352 the lazy ˇ»dog"
1353 });
1354 cx.simulate_shared_keystrokes("ctrl-v").await;
1355 cx.shared_state().await.assert_eq(indoc! {
1356 "The «quickˇ» brown
1357 fox «jumpsˇ» over
1358 the «lazy ˇ»dog"
1359 });
1360
1361 cx.set_shared_state(indoc! {
1362 "The ˇquick
1363 brown
1364 fox
1365 jumps over the
1366
1367 lazy dog
1368 "
1369 })
1370 .await;
1371 cx.simulate_shared_keystrokes("ctrl-v down down").await;
1372 cx.shared_state().await.assert_eq(indoc! {
1373 "The«ˇ q»uick
1374 bro«ˇwn»
1375 foxˇ
1376 jumps over the
1377
1378 lazy dog
1379 "
1380 });
1381 cx.simulate_shared_keystrokes("down").await;
1382 cx.shared_state().await.assert_eq(indoc! {
1383 "The «qˇ»uick
1384 brow«nˇ»
1385 fox
1386 jump«sˇ» over the
1387
1388 lazy dog
1389 "
1390 });
1391 cx.simulate_shared_keystrokes("left").await;
1392 cx.shared_state().await.assert_eq(indoc! {
1393 "The«ˇ q»uick
1394 bro«ˇwn»
1395 foxˇ
1396 jum«ˇps» over the
1397
1398 lazy dog
1399 "
1400 });
1401 cx.simulate_shared_keystrokes("s o escape").await;
1402 cx.shared_state().await.assert_eq(indoc! {
1403 "Theˇouick
1404 broo
1405 foxo
1406 jumo over the
1407
1408 lazy dog
1409 "
1410 });
1411
1412 // https://github.com/zed-industries/zed/issues/6274
1413 cx.set_shared_state(indoc! {
1414 "Theˇ quick brown
1415
1416 fox jumps over
1417 the lazy dog
1418 "
1419 })
1420 .await;
1421 cx.simulate_shared_keystrokes("l ctrl-v j j").await;
1422 cx.shared_state().await.assert_eq(indoc! {
1423 "The «qˇ»uick brown
1424
1425 fox «jˇ»umps over
1426 the lazy dog
1427 "
1428 });
1429 }
1430
1431 #[gpui::test]
1432 async fn test_visual_block_issue_2123(cx: &mut gpui::TestAppContext) {
1433 let mut cx = NeovimBackedTestContext::new(cx).await;
1434
1435 cx.set_shared_state(indoc! {
1436 "The ˇquick brown
1437 fox jumps over
1438 the lazy dog
1439 "
1440 })
1441 .await;
1442 cx.simulate_shared_keystrokes("ctrl-v right down").await;
1443 cx.shared_state().await.assert_eq(indoc! {
1444 "The «quˇ»ick brown
1445 fox «juˇ»mps over
1446 the lazy dog
1447 "
1448 });
1449 }
1450 #[gpui::test]
1451 async fn test_visual_block_mode_down_right(cx: &mut gpui::TestAppContext) {
1452 let mut cx = NeovimBackedTestContext::new(cx).await;
1453 cx.set_shared_state(indoc! {"
1454 The ˇquick brown
1455 fox jumps over
1456 the lazy dog"})
1457 .await;
1458 cx.simulate_shared_keystrokes("ctrl-v l l l l l j").await;
1459 cx.shared_state().await.assert_eq(indoc! {"
1460 The «quick ˇ»brown
1461 fox «jumps ˇ»over
1462 the lazy dog"});
1463 }
1464
1465 #[gpui::test]
1466 async fn test_visual_block_mode_up_left(cx: &mut gpui::TestAppContext) {
1467 let mut cx = NeovimBackedTestContext::new(cx).await;
1468 cx.set_shared_state(indoc! {"
1469 The quick brown
1470 fox jumpsˇ over
1471 the lazy dog"})
1472 .await;
1473 cx.simulate_shared_keystrokes("ctrl-v h h h h h k").await;
1474 cx.shared_state().await.assert_eq(indoc! {"
1475 The «ˇquick »brown
1476 fox «ˇjumps »over
1477 the lazy dog"});
1478 }
1479
1480 #[gpui::test]
1481 async fn test_visual_block_mode_other_end(cx: &mut gpui::TestAppContext) {
1482 let mut cx = NeovimBackedTestContext::new(cx).await;
1483 cx.set_shared_state(indoc! {"
1484 The quick brown
1485 fox jˇumps over
1486 the lazy dog"})
1487 .await;
1488 cx.simulate_shared_keystrokes("ctrl-v l l l l j").await;
1489 cx.shared_state().await.assert_eq(indoc! {"
1490 The quick brown
1491 fox j«umps ˇ»over
1492 the l«azy dˇ»og"});
1493 cx.simulate_shared_keystrokes("o k").await;
1494 cx.shared_state().await.assert_eq(indoc! {"
1495 The q«ˇuick »brown
1496 fox j«ˇumps »over
1497 the l«ˇazy d»og"});
1498 }
1499
1500 #[gpui::test]
1501 async fn test_visual_block_mode_shift_other_end(cx: &mut gpui::TestAppContext) {
1502 let mut cx = NeovimBackedTestContext::new(cx).await;
1503 cx.set_shared_state(indoc! {"
1504 The quick brown
1505 fox jˇumps over
1506 the lazy dog"})
1507 .await;
1508 cx.simulate_shared_keystrokes("ctrl-v l l l l j").await;
1509 cx.shared_state().await.assert_eq(indoc! {"
1510 The quick brown
1511 fox j«umps ˇ»over
1512 the l«azy dˇ»og"});
1513 cx.simulate_shared_keystrokes("shift-o k").await;
1514 cx.shared_state().await.assert_eq(indoc! {"
1515 The quick brown
1516 fox j«ˇumps »over
1517 the lazy dog"});
1518 }
1519
1520 #[gpui::test]
1521 async fn test_visual_block_insert(cx: &mut gpui::TestAppContext) {
1522 let mut cx = NeovimBackedTestContext::new(cx).await;
1523
1524 cx.set_shared_state(indoc! {
1525 "ˇThe quick brown
1526 fox jumps over
1527 the lazy dog
1528 "
1529 })
1530 .await;
1531 cx.simulate_shared_keystrokes("ctrl-v 9 down").await;
1532 cx.shared_state().await.assert_eq(indoc! {
1533 "«Tˇ»he quick brown
1534 «fˇ»ox jumps over
1535 «tˇ»he lazy dog
1536 ˇ"
1537 });
1538
1539 cx.simulate_shared_keystrokes("shift-i k escape").await;
1540 cx.shared_state().await.assert_eq(indoc! {
1541 "ˇkThe quick brown
1542 kfox jumps over
1543 kthe lazy dog
1544 k"
1545 });
1546
1547 cx.set_shared_state(indoc! {
1548 "ˇThe quick brown
1549 fox jumps over
1550 the lazy dog
1551 "
1552 })
1553 .await;
1554 cx.simulate_shared_keystrokes("ctrl-v 9 down").await;
1555 cx.shared_state().await.assert_eq(indoc! {
1556 "«Tˇ»he quick brown
1557 «fˇ»ox jumps over
1558 «tˇ»he lazy dog
1559 ˇ"
1560 });
1561 cx.simulate_shared_keystrokes("c k escape").await;
1562 cx.shared_state().await.assert_eq(indoc! {
1563 "ˇkhe quick brown
1564 kox jumps over
1565 khe lazy dog
1566 k"
1567 });
1568 }
1569
1570 #[gpui::test]
1571 async fn test_visual_block_insert_after_ctrl_d_scroll(cx: &mut gpui::TestAppContext) {
1572 let mut cx = NeovimBackedTestContext::new(cx).await;
1573 let shared_state_lines = (1..=10)
1574 .map(|line_number| format!("{line_number:02}"))
1575 .collect::<Vec<_>>()
1576 .join("\n");
1577 let shared_state = format!("ˇ{shared_state_lines}\n");
1578
1579 cx.set_scroll_height(5).await;
1580 cx.set_shared_state(&shared_state).await;
1581
1582 cx.simulate_shared_keystrokes("ctrl-v ctrl-d").await;
1583 cx.shared_state().await.assert_matches();
1584
1585 cx.simulate_shared_keystrokes("shift-i x escape").await;
1586 cx.shared_state().await.assert_eq(indoc! {
1587 "
1588 ˇx01
1589 x02
1590 x03
1591 x04
1592 x05
1593 06
1594 07
1595 08
1596 09
1597 10
1598 "
1599 });
1600 }
1601
1602 #[gpui::test]
1603 async fn test_visual_block_wrapping_selection(cx: &mut gpui::TestAppContext) {
1604 let mut cx = NeovimBackedTestContext::new(cx).await;
1605
1606 // Ensure that the editor is wrapping lines at 12 columns so that each
1607 // of the lines ends up being wrapped.
1608 cx.set_shared_wrap(12).await;
1609 cx.set_shared_state(indoc! {
1610 "ˇ12345678901234567890
1611 12345678901234567890
1612 12345678901234567890
1613 "
1614 })
1615 .await;
1616 cx.simulate_shared_keystrokes("ctrl-v j").await;
1617 cx.shared_state().await.assert_eq(indoc! {
1618 "«1ˇ»2345678901234567890
1619 «1ˇ»2345678901234567890
1620 12345678901234567890
1621 "
1622 });
1623
1624 // Test with lines taking up different amounts of display rows to ensure
1625 // that, even in that case, only the buffer rows are taken into account.
1626 cx.set_shared_state(indoc! {
1627 "ˇ123456789012345678901234567890123456789012345678901234567890
1628 1234567890123456789012345678901234567890
1629 12345678901234567890
1630 "
1631 })
1632 .await;
1633 cx.simulate_shared_keystrokes("ctrl-v 2 j").await;
1634 cx.shared_state().await.assert_eq(indoc! {
1635 "«1ˇ»23456789012345678901234567890123456789012345678901234567890
1636 «1ˇ»234567890123456789012345678901234567890
1637 «1ˇ»2345678901234567890
1638 "
1639 });
1640
1641 // Same scenario as above, but using the up motion to ensure that the
1642 // result is the same.
1643 cx.set_shared_state(indoc! {
1644 "123456789012345678901234567890123456789012345678901234567890
1645 1234567890123456789012345678901234567890
1646 ˇ12345678901234567890
1647 "
1648 })
1649 .await;
1650 cx.simulate_shared_keystrokes("ctrl-v 2 k").await;
1651 cx.shared_state().await.assert_eq(indoc! {
1652 "«1ˇ»23456789012345678901234567890123456789012345678901234567890
1653 «1ˇ»234567890123456789012345678901234567890
1654 «1ˇ»2345678901234567890
1655 "
1656 });
1657 }
1658
1659 #[gpui::test]
1660 async fn test_visual_object(cx: &mut gpui::TestAppContext) {
1661 let mut cx = NeovimBackedTestContext::new(cx).await;
1662
1663 cx.set_shared_state("hello (in [parˇens] o)").await;
1664 cx.simulate_shared_keystrokes("ctrl-v l").await;
1665 cx.simulate_shared_keystrokes("a ]").await;
1666 cx.shared_state()
1667 .await
1668 .assert_eq("hello (in «[parens]ˇ» o)");
1669 cx.simulate_shared_keystrokes("i (").await;
1670 cx.shared_state()
1671 .await
1672 .assert_eq("hello («in [parens] oˇ»)");
1673
1674 cx.set_shared_state("hello in a wˇord again.").await;
1675 cx.simulate_shared_keystrokes("ctrl-v l i w").await;
1676 cx.shared_state()
1677 .await
1678 .assert_eq("hello in a w«ordˇ» again.");
1679 assert_eq!(cx.mode(), Mode::VisualBlock);
1680 cx.simulate_shared_keystrokes("o a s").await;
1681 cx.shared_state()
1682 .await
1683 .assert_eq("«ˇhello in a word» again.");
1684 }
1685
1686 #[gpui::test]
1687 async fn test_visual_object_expands(cx: &mut gpui::TestAppContext) {
1688 let mut cx = NeovimBackedTestContext::new(cx).await;
1689
1690 cx.set_shared_state(indoc! {
1691 "{
1692 {
1693 ˇ }
1694 }
1695 {
1696 }
1697 "
1698 })
1699 .await;
1700 cx.simulate_shared_keystrokes("v l").await;
1701 cx.shared_state().await.assert_eq(indoc! {
1702 "{
1703 {
1704 « }ˇ»
1705 }
1706 {
1707 }
1708 "
1709 });
1710 cx.simulate_shared_keystrokes("a {").await;
1711 cx.shared_state().await.assert_eq(indoc! {
1712 "{
1713 «{
1714 }ˇ»
1715 }
1716 {
1717 }
1718 "
1719 });
1720 cx.simulate_shared_keystrokes("a {").await;
1721 cx.shared_state().await.assert_eq(indoc! {
1722 "«{
1723 {
1724 }
1725 }ˇ»
1726 {
1727 }
1728 "
1729 });
1730 // cx.simulate_shared_keystrokes("a {").await;
1731 // cx.shared_state().await.assert_eq(indoc! {
1732 // "{
1733 // «{
1734 // }ˇ»
1735 // }
1736 // {
1737 // }
1738 // "
1739 // });
1740 }
1741
1742 #[gpui::test]
1743 async fn test_visual_move_trailing_newline(cx: &mut gpui::TestAppContext) {
1744 let mut cx = VimTestContext::new(cx, true).await;
1745
1746 cx.set_state("This is a newlinˇe\n", Mode::Normal);
1747 cx.simulate_keystrokes("v");
1748 cx.assert_state("This is a newlin«eˇ»\n", Mode::Visual);
1749 cx.simulate_keystrokes("l");
1750 cx.assert_state("This is a newlin«e\nˇ»", Mode::Visual);
1751 }
1752
1753 #[gpui::test]
1754 async fn test_mode_across_command(cx: &mut gpui::TestAppContext) {
1755 let mut cx = VimTestContext::new(cx, true).await;
1756
1757 cx.set_state("aˇbc", Mode::Normal);
1758 cx.simulate_keystrokes("ctrl-v");
1759 assert_eq!(cx.mode(), Mode::VisualBlock);
1760 cx.simulate_keystrokes("cmd-shift-p escape");
1761 assert_eq!(cx.mode(), Mode::VisualBlock);
1762 }
1763
1764 #[gpui::test]
1765 async fn test_gn(cx: &mut gpui::TestAppContext) {
1766 let mut cx = NeovimBackedTestContext::new(cx).await;
1767
1768 cx.set_shared_state("aaˇ aa aa aa aa").await;
1769 cx.simulate_shared_keystrokes("/ a a enter").await;
1770 cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1771 cx.simulate_shared_keystrokes("g n").await;
1772 cx.shared_state().await.assert_eq("aa «aaˇ» aa aa aa");
1773 cx.simulate_shared_keystrokes("g n").await;
1774 cx.shared_state().await.assert_eq("aa «aa aaˇ» aa aa");
1775 cx.simulate_shared_keystrokes("escape d g n").await;
1776 cx.shared_state().await.assert_eq("aa aa ˇ aa aa");
1777
1778 cx.set_shared_state("aaˇ aa aa aa aa").await;
1779 cx.simulate_shared_keystrokes("/ a a enter").await;
1780 cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1781 cx.simulate_shared_keystrokes("3 g n").await;
1782 cx.shared_state().await.assert_eq("aa aa aa «aaˇ» aa");
1783
1784 cx.set_shared_state("aaˇ aa aa aa aa").await;
1785 cx.simulate_shared_keystrokes("/ a a enter").await;
1786 cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1787 cx.simulate_shared_keystrokes("g shift-n").await;
1788 cx.shared_state().await.assert_eq("aa «ˇaa» aa aa aa");
1789 cx.simulate_shared_keystrokes("g shift-n").await;
1790 cx.shared_state().await.assert_eq("«ˇaa aa» aa aa aa");
1791 }
1792
1793 #[gpui::test]
1794 async fn test_gl(cx: &mut gpui::TestAppContext) {
1795 let mut cx = VimTestContext::new(cx, true).await;
1796
1797 cx.set_state("aaˇ aa\naa", Mode::Normal);
1798 cx.simulate_keystrokes("g l");
1799 cx.assert_state("«aaˇ» «aaˇ»\naa", Mode::Visual);
1800 cx.simulate_keystrokes("g >");
1801 cx.assert_state("«aaˇ» aa\n«aaˇ»", Mode::Visual);
1802 }
1803
1804 #[gpui::test]
1805 async fn test_dgn_repeat(cx: &mut gpui::TestAppContext) {
1806 let mut cx = NeovimBackedTestContext::new(cx).await;
1807
1808 cx.set_shared_state("aaˇ aa aa aa aa").await;
1809 cx.simulate_shared_keystrokes("/ a a enter").await;
1810 cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1811 cx.simulate_shared_keystrokes("d g n").await;
1812
1813 cx.shared_state().await.assert_eq("aa ˇ aa aa aa");
1814 cx.simulate_shared_keystrokes(".").await;
1815 cx.shared_state().await.assert_eq("aa ˇ aa aa");
1816 cx.simulate_shared_keystrokes(".").await;
1817 cx.shared_state().await.assert_eq("aa ˇ aa");
1818 }
1819
1820 #[gpui::test]
1821 async fn test_cgn_repeat(cx: &mut gpui::TestAppContext) {
1822 let mut cx = NeovimBackedTestContext::new(cx).await;
1823
1824 cx.set_shared_state("aaˇ aa aa aa aa").await;
1825 cx.simulate_shared_keystrokes("/ a a enter").await;
1826 cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1827 cx.simulate_shared_keystrokes("c g n x escape").await;
1828 cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1829 cx.simulate_shared_keystrokes(".").await;
1830 cx.shared_state().await.assert_eq("aa x ˇx aa aa");
1831 }
1832
1833 #[gpui::test]
1834 async fn test_cgn_nomatch(cx: &mut gpui::TestAppContext) {
1835 let mut cx = NeovimBackedTestContext::new(cx).await;
1836
1837 cx.set_shared_state("aaˇ aa aa aa aa").await;
1838 cx.simulate_shared_keystrokes("/ b b enter").await;
1839 cx.shared_state().await.assert_eq("aaˇ aa aa aa aa");
1840 cx.simulate_shared_keystrokes("c g n x escape").await;
1841 cx.shared_state().await.assert_eq("aaˇaa aa aa aa");
1842 cx.simulate_shared_keystrokes(".").await;
1843 cx.shared_state().await.assert_eq("aaˇa aa aa aa");
1844
1845 cx.set_shared_state("aaˇ bb aa aa aa").await;
1846 cx.simulate_shared_keystrokes("/ b b enter").await;
1847 cx.shared_state().await.assert_eq("aa ˇbb aa aa aa");
1848 cx.simulate_shared_keystrokes("c g n x escape").await;
1849 cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1850 cx.simulate_shared_keystrokes(".").await;
1851 cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1852 }
1853
1854 #[gpui::test]
1855 async fn test_visual_shift_d(cx: &mut gpui::TestAppContext) {
1856 let mut cx = NeovimBackedTestContext::new(cx).await;
1857
1858 cx.set_shared_state(indoc! {
1859 "The ˇquick brown
1860 fox jumps over
1861 the lazy dog
1862 "
1863 })
1864 .await;
1865 cx.simulate_shared_keystrokes("v down shift-d").await;
1866 cx.shared_state().await.assert_eq(indoc! {
1867 "the ˇlazy dog\n"
1868 });
1869
1870 cx.set_shared_state(indoc! {
1871 "The ˇquick brown
1872 fox jumps over
1873 the lazy dog
1874 "
1875 })
1876 .await;
1877 cx.simulate_shared_keystrokes("ctrl-v down shift-d").await;
1878 cx.shared_state().await.assert_eq(indoc! {
1879 "Theˇ•
1880 fox•
1881 the lazy dog
1882 "
1883 });
1884 }
1885
1886 #[gpui::test]
1887 async fn test_shift_y(cx: &mut gpui::TestAppContext) {
1888 let mut cx = NeovimBackedTestContext::new(cx).await;
1889
1890 cx.set_shared_state(indoc! {
1891 "The ˇquick brown\n"
1892 })
1893 .await;
1894 cx.simulate_shared_keystrokes("v i w shift-y").await;
1895 cx.shared_clipboard().await.assert_eq(indoc! {
1896 "The quick brown\n"
1897 });
1898 }
1899
1900 #[gpui::test]
1901 async fn test_gv(cx: &mut gpui::TestAppContext) {
1902 let mut cx = NeovimBackedTestContext::new(cx).await;
1903
1904 cx.set_shared_state(indoc! {
1905 "The ˇquick brown"
1906 })
1907 .await;
1908 cx.simulate_shared_keystrokes("v i w escape g v").await;
1909 cx.shared_state().await.assert_eq(indoc! {
1910 "The «quickˇ» brown"
1911 });
1912
1913 cx.simulate_shared_keystrokes("o escape g v").await;
1914 cx.shared_state().await.assert_eq(indoc! {
1915 "The «ˇquick» brown"
1916 });
1917
1918 cx.simulate_shared_keystrokes("escape ^ ctrl-v l").await;
1919 cx.shared_state().await.assert_eq(indoc! {
1920 "«Thˇ»e quick brown"
1921 });
1922 cx.simulate_shared_keystrokes("g v").await;
1923 cx.shared_state().await.assert_eq(indoc! {
1924 "The «ˇquick» brown"
1925 });
1926 cx.simulate_shared_keystrokes("g v").await;
1927 cx.shared_state().await.assert_eq(indoc! {
1928 "«Thˇ»e quick brown"
1929 });
1930
1931 cx.set_state(
1932 indoc! {"
1933 fiˇsh one
1934 fish two
1935 fish red
1936 fish blue
1937 "},
1938 Mode::Normal,
1939 );
1940 cx.simulate_keystrokes("4 g l escape escape g v");
1941 cx.assert_state(
1942 indoc! {"
1943 «fishˇ» one
1944 «fishˇ» two
1945 «fishˇ» red
1946 «fishˇ» blue
1947 "},
1948 Mode::Visual,
1949 );
1950 cx.simulate_keystrokes("y g v");
1951 cx.assert_state(
1952 indoc! {"
1953 «fishˇ» one
1954 «fishˇ» two
1955 «fishˇ» red
1956 «fishˇ» blue
1957 "},
1958 Mode::Visual,
1959 );
1960 }
1961
1962 #[gpui::test]
1963 async fn test_p_g_v_y(cx: &mut gpui::TestAppContext) {
1964 let mut cx = NeovimBackedTestContext::new(cx).await;
1965
1966 cx.set_shared_state(indoc! {
1967 "The
1968 quicˇk
1969 brown
1970 fox"
1971 })
1972 .await;
1973 cx.simulate_shared_keystrokes("y y j shift-v p g v y").await;
1974 cx.shared_state().await.assert_eq(indoc! {
1975 "The
1976 quick
1977 ˇquick
1978 fox"
1979 });
1980 cx.shared_clipboard().await.assert_eq("quick\n");
1981 }
1982
1983 #[gpui::test]
1984 async fn test_v2ap(cx: &mut gpui::TestAppContext) {
1985 let mut cx = NeovimBackedTestContext::new(cx).await;
1986
1987 cx.set_shared_state(indoc! {
1988 "The
1989 quicˇk
1990
1991 brown
1992 fox"
1993 })
1994 .await;
1995 cx.simulate_shared_keystrokes("v 2 a p").await;
1996 cx.shared_state().await.assert_eq(indoc! {
1997 "«The
1998 quick
1999
2000 brown
2001 fˇ»ox"
2002 });
2003 }
2004
2005 #[gpui::test]
2006 async fn test_visual_syntax_sibling_selection(cx: &mut gpui::TestAppContext) {
2007 let mut cx = VimTestContext::new(cx, true).await;
2008
2009 cx.set_state(
2010 indoc! {"
2011 fn test() {
2012 let ˇa = 1;
2013 let b = 2;
2014 let c = 3;
2015 }
2016 "},
2017 Mode::Normal,
2018 );
2019
2020 // Enter visual mode and select the statement
2021 cx.simulate_keystrokes("v w w w");
2022 cx.assert_state(
2023 indoc! {"
2024 fn test() {
2025 let «a = 1;ˇ»
2026 let b = 2;
2027 let c = 3;
2028 }
2029 "},
2030 Mode::Visual,
2031 );
2032
2033 // The specific behavior of syntax sibling selection in vim mode
2034 // would depend on the key bindings configured, but the actions
2035 // are now available for use
2036 }
2037
2038 #[gpui::test]
2039 async fn test_visual_replace_uses_graphemes(cx: &mut gpui::TestAppContext) {
2040 let mut cx = VimTestContext::new(cx, true).await;
2041
2042 cx.set_state("«Hällöˇ» Wörld", Mode::Visual);
2043 cx.simulate_keystrokes("r 1");
2044 cx.assert_state("ˇ11111 Wörld", Mode::Normal);
2045
2046 cx.set_state("«e\u{301}ˇ»", Mode::Visual);
2047 cx.simulate_keystrokes("r 1");
2048 cx.assert_state("ˇ1", Mode::Normal);
2049
2050 cx.set_state("«🙂ˇ»", Mode::Visual);
2051 cx.simulate_keystrokes("r 1");
2052 cx.assert_state("ˇ1", Mode::Normal);
2053 }
2054}
2055