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