visual.rs

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