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