visual.rs

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