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