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