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