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, around);
 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.edit(edits, cx);
 534                editor.change_selections(None, cx, |s| s.select_ranges(stable_anchors));
 535            });
 536        });
 537        self.switch_mode(Mode::Normal, false, cx);
 538    }
 539
 540    pub fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
 541        let count =
 542            Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 });
 543        self.update_editor(cx, |_, editor, cx| {
 544            editor.set_clip_at_line_ends(false, cx);
 545            for _ in 0..count {
 546                if editor
 547                    .select_next(&Default::default(), cx)
 548                    .log_err()
 549                    .is_none()
 550                {
 551                    break;
 552                }
 553            }
 554        });
 555    }
 556
 557    pub fn select_previous(&mut self, _: &SelectPrevious, cx: &mut ViewContext<Self>) {
 558        let count =
 559            Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 });
 560        self.update_editor(cx, |_, editor, cx| {
 561            for _ in 0..count {
 562                if editor
 563                    .select_previous(&Default::default(), cx)
 564                    .log_err()
 565                    .is_none()
 566                {
 567                    break;
 568                }
 569            }
 570        });
 571    }
 572
 573    pub fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
 574        let count = Vim::take_count(cx).unwrap_or(1);
 575        let Some(pane) = self.pane(cx) else {
 576            return;
 577        };
 578        let vim_is_normal = self.mode == Mode::Normal;
 579        let mut start_selection = 0usize;
 580        let mut end_selection = 0usize;
 581
 582        self.update_editor(cx, |_, editor, _| {
 583            editor.set_collapse_matches(false);
 584        });
 585        if vim_is_normal {
 586            pane.update(cx, |pane, cx| {
 587                if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()
 588                {
 589                    search_bar.update(cx, |search_bar, cx| {
 590                        if !search_bar.has_active_match() || !search_bar.show(cx) {
 591                            return;
 592                        }
 593                        // without update_match_index there is a bug when the cursor is before the first match
 594                        search_bar.update_match_index(cx);
 595                        search_bar.select_match(direction.opposite(), 1, cx);
 596                    });
 597                }
 598            });
 599        }
 600        self.update_editor(cx, |_, editor, cx| {
 601            let latest = editor.selections.newest::<usize>(cx);
 602            start_selection = latest.start;
 603            end_selection = latest.end;
 604        });
 605
 606        let mut match_exists = false;
 607        pane.update(cx, |pane, cx| {
 608            if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 609                search_bar.update(cx, |search_bar, cx| {
 610                    search_bar.update_match_index(cx);
 611                    search_bar.select_match(direction, count, cx);
 612                    match_exists = search_bar.match_exists(cx);
 613                });
 614            }
 615        });
 616        if !match_exists {
 617            self.clear_operator(cx);
 618            self.stop_replaying(cx);
 619            return;
 620        }
 621        self.update_editor(cx, |_, editor, cx| {
 622            let latest = editor.selections.newest::<usize>(cx);
 623            if vim_is_normal {
 624                start_selection = latest.start;
 625                end_selection = latest.end;
 626            } else {
 627                start_selection = start_selection.min(latest.start);
 628                end_selection = end_selection.max(latest.end);
 629            }
 630            if direction == Direction::Prev {
 631                std::mem::swap(&mut start_selection, &mut end_selection);
 632            }
 633            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 634                s.select_ranges([start_selection..end_selection]);
 635            });
 636            editor.set_collapse_matches(true);
 637        });
 638
 639        match self.maybe_pop_operator() {
 640            Some(Operator::Change) => self.substitute(None, false, cx),
 641            Some(Operator::Delete) => {
 642                self.stop_recording(cx);
 643                self.visual_delete(false, cx)
 644            }
 645            Some(Operator::Yank) => self.visual_yank(cx),
 646            _ => {} // Ignoring other operators
 647        }
 648    }
 649}
 650#[cfg(test)]
 651mod test {
 652    use indoc::indoc;
 653    use workspace::item::Item;
 654
 655    use crate::{
 656        state::Mode,
 657        test::{NeovimBackedTestContext, VimTestContext},
 658    };
 659
 660    #[gpui::test]
 661    async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
 662        let mut cx = NeovimBackedTestContext::new(cx).await;
 663
 664        cx.set_shared_state(indoc! {
 665            "The ˇquick brown
 666            fox jumps over
 667            the lazy dog"
 668        })
 669        .await;
 670        let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
 671
 672        // entering visual mode should select the character
 673        // under cursor
 674        cx.simulate_shared_keystrokes("v").await;
 675        cx.shared_state()
 676            .await
 677            .assert_eq(indoc! { "The «qˇ»uick brown
 678            fox jumps over
 679            the lazy dog"});
 680        cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
 681
 682        // forwards motions should extend the selection
 683        cx.simulate_shared_keystrokes("w j").await;
 684        cx.shared_state().await.assert_eq(indoc! { "The «quick brown
 685            fox jumps oˇ»ver
 686            the lazy dog"});
 687
 688        cx.simulate_shared_keystrokes("escape").await;
 689        cx.shared_state().await.assert_eq(indoc! { "The quick brown
 690            fox jumps ˇover
 691            the lazy dog"});
 692
 693        // motions work backwards
 694        cx.simulate_shared_keystrokes("v k b").await;
 695        cx.shared_state()
 696            .await
 697            .assert_eq(indoc! { "The «ˇquick brown
 698            fox jumps o»ver
 699            the lazy dog"});
 700
 701        // works on empty lines
 702        cx.set_shared_state(indoc! {"
 703            a
 704            ˇ
 705            b
 706            "})
 707            .await;
 708        let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
 709        cx.simulate_shared_keystrokes("v").await;
 710        cx.shared_state().await.assert_eq(indoc! {"
 711            a
 712            «
 713            ˇ»b
 714        "});
 715        cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
 716
 717        // toggles off again
 718        cx.simulate_shared_keystrokes("v").await;
 719        cx.shared_state().await.assert_eq(indoc! {"
 720            a
 721            ˇ
 722            b
 723            "});
 724
 725        // works at the end of a document
 726        cx.set_shared_state(indoc! {"
 727            a
 728            b
 729            ˇ"})
 730            .await;
 731
 732        cx.simulate_shared_keystrokes("v").await;
 733        cx.shared_state().await.assert_eq(indoc! {"
 734            a
 735            b
 736            ˇ"});
 737    }
 738
 739    #[gpui::test]
 740    async fn test_visual_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
 741        let mut cx = VimTestContext::new(cx, true).await;
 742
 743        cx.set_state(
 744            indoc! {
 745                "«The quick brown
 746                fox jumps over
 747                the lazy dogˇ»"
 748            },
 749            Mode::Visual,
 750        );
 751        cx.simulate_keystrokes("g shift-i");
 752        cx.assert_state(
 753            indoc! {
 754                "ˇThe quick brown
 755                ˇfox jumps over
 756                ˇthe lazy dog"
 757            },
 758            Mode::Insert,
 759        );
 760    }
 761
 762    #[gpui::test]
 763    async fn test_visual_insert_end_of_line(cx: &mut gpui::TestAppContext) {
 764        let mut cx = VimTestContext::new(cx, true).await;
 765
 766        cx.set_state(
 767            indoc! {
 768                "«The quick brown
 769                fox jumps over
 770                the lazy dogˇ»"
 771            },
 772            Mode::Visual,
 773        );
 774        cx.simulate_keystrokes("g shift-a");
 775        cx.assert_state(
 776            indoc! {
 777                "The quick brownˇ
 778                fox jumps overˇ
 779                the lazy dogˇ"
 780            },
 781            Mode::Insert,
 782        );
 783    }
 784
 785    #[gpui::test]
 786    async fn test_enter_visual_line_mode(cx: &mut gpui::TestAppContext) {
 787        let mut cx = NeovimBackedTestContext::new(cx).await;
 788
 789        cx.set_shared_state(indoc! {
 790            "The ˇquick brown
 791            fox jumps over
 792            the lazy dog"
 793        })
 794        .await;
 795        cx.simulate_shared_keystrokes("shift-v").await;
 796        cx.shared_state()
 797            .await
 798            .assert_eq(indoc! { "The «qˇ»uick brown
 799            fox jumps over
 800            the lazy dog"});
 801        cx.simulate_shared_keystrokes("x").await;
 802        cx.shared_state().await.assert_eq(indoc! { "fox ˇjumps over
 803        the lazy dog"});
 804
 805        // it should work on empty lines
 806        cx.set_shared_state(indoc! {"
 807            a
 808            ˇ
 809            b"})
 810            .await;
 811        cx.simulate_shared_keystrokes("shift-v").await;
 812        cx.shared_state().await.assert_eq(indoc! {"
 813            a
 814            «
 815            ˇ»b"});
 816        cx.simulate_shared_keystrokes("x").await;
 817        cx.shared_state().await.assert_eq(indoc! {"
 818            a
 819            ˇb"});
 820
 821        // it should work at the end of the document
 822        cx.set_shared_state(indoc! {"
 823            a
 824            b
 825            ˇ"})
 826            .await;
 827        let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
 828        cx.simulate_shared_keystrokes("shift-v").await;
 829        cx.shared_state().await.assert_eq(indoc! {"
 830            a
 831            b
 832            ˇ"});
 833        cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
 834        cx.simulate_shared_keystrokes("x").await;
 835        cx.shared_state().await.assert_eq(indoc! {"
 836            a
 837            ˇb"});
 838    }
 839
 840    #[gpui::test]
 841    async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
 842        let mut cx = NeovimBackedTestContext::new(cx).await;
 843
 844        cx.simulate("v w", "The quick ˇbrown")
 845            .await
 846            .assert_matches();
 847
 848        cx.simulate("v w x", "The quick ˇbrown")
 849            .await
 850            .assert_matches();
 851        cx.simulate(
 852            "v w j x",
 853            indoc! {"
 854                The ˇquick brown
 855                fox jumps over
 856                the lazy dog"},
 857        )
 858        .await
 859        .assert_matches();
 860        // Test pasting code copied on delete
 861        cx.simulate_shared_keystrokes("j p").await;
 862        cx.shared_state().await.assert_matches();
 863
 864        cx.simulate_at_each_offset(
 865            "v w j x",
 866            indoc! {"
 867                The ˇquick brown
 868                fox jumps over
 869                the ˇlazy dog"},
 870        )
 871        .await
 872        .assert_matches();
 873        cx.simulate_at_each_offset(
 874            "v b k x",
 875            indoc! {"
 876                The ˇquick brown
 877                fox jumps ˇover
 878                the ˇlazy dog"},
 879        )
 880        .await
 881        .assert_matches();
 882    }
 883
 884    #[gpui::test]
 885    async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
 886        let mut cx = NeovimBackedTestContext::new(cx).await;
 887
 888        cx.set_shared_state(indoc! {"
 889                The quˇick brown
 890                fox jumps over
 891                the lazy dog"})
 892            .await;
 893        cx.simulate_shared_keystrokes("shift-v x").await;
 894        cx.shared_state().await.assert_matches();
 895
 896        // Test pasting code copied on delete
 897        cx.simulate_shared_keystrokes("p").await;
 898        cx.shared_state().await.assert_matches();
 899
 900        cx.set_shared_state(indoc! {"
 901                The quick brown
 902                fox jumps over
 903                the laˇzy dog"})
 904            .await;
 905        cx.simulate_shared_keystrokes("shift-v x").await;
 906        cx.shared_state().await.assert_matches();
 907        cx.shared_clipboard().await.assert_eq("the lazy dog\n");
 908
 909        cx.set_shared_state(indoc! {"
 910                                The quˇick brown
 911                                fox jumps over
 912                                the lazy dog"})
 913            .await;
 914        cx.simulate_shared_keystrokes("shift-v j x").await;
 915        cx.shared_state().await.assert_matches();
 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 ˇlong line
 922            should not
 923            crash
 924            "})
 925            .await;
 926        cx.simulate_shared_keystrokes("shift-v $ x").await;
 927        cx.shared_state().await.assert_matches();
 928    }
 929
 930    #[gpui::test]
 931    async fn test_visual_yank(cx: &mut gpui::TestAppContext) {
 932        let mut cx = NeovimBackedTestContext::new(cx).await;
 933
 934        cx.set_shared_state("The quick ˇbrown").await;
 935        cx.simulate_shared_keystrokes("v w y").await;
 936        cx.shared_state().await.assert_eq("The quick ˇbrown");
 937        cx.shared_clipboard().await.assert_eq("brown");
 938
 939        cx.set_shared_state(indoc! {"
 940                The ˇquick brown
 941                fox jumps over
 942                the lazy dog"})
 943            .await;
 944        cx.simulate_shared_keystrokes("v w j y").await;
 945        cx.shared_state().await.assert_eq(indoc! {"
 946                    The ˇquick brown
 947                    fox jumps over
 948                    the lazy dog"});
 949        cx.shared_clipboard().await.assert_eq(indoc! {"
 950                quick brown
 951                fox jumps o"});
 952
 953        cx.set_shared_state(indoc! {"
 954                    The quick brown
 955                    fox jumps over
 956                    the ˇlazy dog"})
 957            .await;
 958        cx.simulate_shared_keystrokes("v w j y").await;
 959        cx.shared_state().await.assert_eq(indoc! {"
 960                    The quick brown
 961                    fox jumps over
 962                    the ˇlazy dog"});
 963        cx.shared_clipboard().await.assert_eq("lazy d");
 964        cx.simulate_shared_keystrokes("shift-v y").await;
 965        cx.shared_clipboard().await.assert_eq("the lazy dog\n");
 966
 967        cx.set_shared_state(indoc! {"
 968                    The ˇquick brown
 969                    fox jumps over
 970                    the lazy dog"})
 971            .await;
 972        cx.simulate_shared_keystrokes("v b k y").await;
 973        cx.shared_state().await.assert_eq(indoc! {"
 974                    ˇThe quick brown
 975                    fox jumps over
 976                    the lazy dog"});
 977        assert_eq!(
 978            cx.read_from_clipboard()
 979                .map(|item| item.text().unwrap().to_string())
 980                .unwrap(),
 981            "The q"
 982        );
 983
 984        cx.set_shared_state(indoc! {"
 985                    The quick brown
 986                    fox ˇjumps over
 987                    the lazy dog"})
 988            .await;
 989        cx.simulate_shared_keystrokes("shift-v shift-g shift-y")
 990            .await;
 991        cx.shared_state().await.assert_eq(indoc! {"
 992                    The quick brown
 993                    ˇfox jumps over
 994                    the lazy dog"});
 995        cx.shared_clipboard()
 996            .await
 997            .assert_eq("fox jumps over\nthe lazy dog\n");
 998    }
 999
1000    #[gpui::test]
1001    async fn test_visual_block_mode(cx: &mut gpui::TestAppContext) {
1002        let mut cx = NeovimBackedTestContext::new(cx).await;
1003
1004        cx.set_shared_state(indoc! {
1005            "The ˇquick brown
1006             fox jumps over
1007             the lazy dog"
1008        })
1009        .await;
1010        cx.simulate_shared_keystrokes("ctrl-v").await;
1011        cx.shared_state().await.assert_eq(indoc! {
1012            "The «qˇ»uick brown
1013            fox jumps over
1014            the lazy dog"
1015        });
1016        cx.simulate_shared_keystrokes("2 down").await;
1017        cx.shared_state().await.assert_eq(indoc! {
1018            "The «qˇ»uick brown
1019            fox «jˇ»umps over
1020            the «lˇ»azy dog"
1021        });
1022        cx.simulate_shared_keystrokes("e").await;
1023        cx.shared_state().await.assert_eq(indoc! {
1024            "The «quicˇ»k brown
1025            fox «jumpˇ»s over
1026            the «lazyˇ» dog"
1027        });
1028        cx.simulate_shared_keystrokes("^").await;
1029        cx.shared_state().await.assert_eq(indoc! {
1030            "«ˇThe q»uick brown
1031            «ˇfox j»umps over
1032            «ˇthe l»azy dog"
1033        });
1034        cx.simulate_shared_keystrokes("$").await;
1035        cx.shared_state().await.assert_eq(indoc! {
1036            "The «quick brownˇ»
1037            fox «jumps overˇ»
1038            the «lazy dogˇ»"
1039        });
1040        cx.simulate_shared_keystrokes("shift-f space").await;
1041        cx.shared_state().await.assert_eq(indoc! {
1042            "The «quickˇ» brown
1043            fox «jumpsˇ» over
1044            the «lazy ˇ»dog"
1045        });
1046
1047        // toggling through visual mode works as expected
1048        cx.simulate_shared_keystrokes("v").await;
1049        cx.shared_state().await.assert_eq(indoc! {
1050            "The «quick brown
1051            fox jumps over
1052            the lazy ˇ»dog"
1053        });
1054        cx.simulate_shared_keystrokes("ctrl-v").await;
1055        cx.shared_state().await.assert_eq(indoc! {
1056            "The «quickˇ» brown
1057            fox «jumpsˇ» over
1058            the «lazy ˇ»dog"
1059        });
1060
1061        cx.set_shared_state(indoc! {
1062            "The ˇquick
1063             brown
1064             fox
1065             jumps over the
1066
1067             lazy dog
1068            "
1069        })
1070        .await;
1071        cx.simulate_shared_keystrokes("ctrl-v down down").await;
1072        cx.shared_state().await.assert_eq(indoc! {
1073            "The«ˇ q»uick
1074            bro«ˇwn»
1075            foxˇ
1076            jumps over the
1077
1078            lazy dog
1079            "
1080        });
1081        cx.simulate_shared_keystrokes("down").await;
1082        cx.shared_state().await.assert_eq(indoc! {
1083            "The «qˇ»uick
1084            brow«nˇ»
1085            fox
1086            jump«sˇ» over the
1087
1088            lazy dog
1089            "
1090        });
1091        cx.simulate_shared_keystrokes("left").await;
1092        cx.shared_state().await.assert_eq(indoc! {
1093            "The«ˇ q»uick
1094            bro«ˇwn»
1095            foxˇ
1096            jum«ˇps» over the
1097
1098            lazy dog
1099            "
1100        });
1101        cx.simulate_shared_keystrokes("s o escape").await;
1102        cx.shared_state().await.assert_eq(indoc! {
1103            "Theˇouick
1104            broo
1105            foxo
1106            jumo over the
1107
1108            lazy dog
1109            "
1110        });
1111
1112        // https://github.com/zed-industries/zed/issues/6274
1113        cx.set_shared_state(indoc! {
1114            "Theˇ quick brown
1115
1116            fox jumps over
1117            the lazy dog
1118            "
1119        })
1120        .await;
1121        cx.simulate_shared_keystrokes("l ctrl-v j j").await;
1122        cx.shared_state().await.assert_eq(indoc! {
1123            "The «qˇ»uick brown
1124
1125            fox «jˇ»umps over
1126            the lazy dog
1127            "
1128        });
1129    }
1130
1131    #[gpui::test]
1132    async fn test_visual_block_issue_2123(cx: &mut gpui::TestAppContext) {
1133        let mut cx = NeovimBackedTestContext::new(cx).await;
1134
1135        cx.set_shared_state(indoc! {
1136            "The ˇquick brown
1137            fox jumps over
1138            the lazy dog
1139            "
1140        })
1141        .await;
1142        cx.simulate_shared_keystrokes("ctrl-v right down").await;
1143        cx.shared_state().await.assert_eq(indoc! {
1144            "The «quˇ»ick brown
1145            fox «juˇ»mps over
1146            the lazy dog
1147            "
1148        });
1149    }
1150
1151    #[gpui::test]
1152    async fn test_visual_block_insert(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 9 down").await;
1163        cx.shared_state().await.assert_eq(indoc! {
1164            "«Tˇ»he quick brown
1165            «fˇ»ox jumps over
1166            «tˇ»he lazy dog
1167            ˇ"
1168        });
1169
1170        cx.simulate_shared_keystrokes("shift-i k escape").await;
1171        cx.shared_state().await.assert_eq(indoc! {
1172            "ˇkThe quick brown
1173            kfox jumps over
1174            kthe lazy dog
1175            k"
1176        });
1177
1178        cx.set_shared_state(indoc! {
1179            "ˇThe quick brown
1180            fox jumps over
1181            the lazy dog
1182            "
1183        })
1184        .await;
1185        cx.simulate_shared_keystrokes("ctrl-v 9 down").await;
1186        cx.shared_state().await.assert_eq(indoc! {
1187            "«Tˇ»he quick brown
1188            «fˇ»ox jumps over
1189            «tˇ»he lazy dog
1190            ˇ"
1191        });
1192        cx.simulate_shared_keystrokes("c k escape").await;
1193        cx.shared_state().await.assert_eq(indoc! {
1194            "ˇkhe quick brown
1195            kox jumps over
1196            khe lazy dog
1197            k"
1198        });
1199    }
1200
1201    #[gpui::test]
1202    async fn test_visual_object(cx: &mut gpui::TestAppContext) {
1203        let mut cx = NeovimBackedTestContext::new(cx).await;
1204
1205        cx.set_shared_state("hello (in [parˇens] o)").await;
1206        cx.simulate_shared_keystrokes("ctrl-v l").await;
1207        cx.simulate_shared_keystrokes("a ]").await;
1208        cx.shared_state()
1209            .await
1210            .assert_eq("hello (in «[parens]ˇ» o)");
1211        cx.simulate_shared_keystrokes("i (").await;
1212        cx.shared_state()
1213            .await
1214            .assert_eq("hello («in [parens] oˇ»)");
1215
1216        cx.set_shared_state("hello in a wˇord again.").await;
1217        cx.simulate_shared_keystrokes("ctrl-v l i w").await;
1218        cx.shared_state()
1219            .await
1220            .assert_eq("hello in a w«ordˇ» again.");
1221        assert_eq!(cx.mode(), Mode::VisualBlock);
1222        cx.simulate_shared_keystrokes("o a s").await;
1223        cx.shared_state()
1224            .await
1225            .assert_eq("«ˇhello in a word» again.");
1226    }
1227
1228    #[gpui::test]
1229    async fn test_mode_across_command(cx: &mut gpui::TestAppContext) {
1230        let mut cx = VimTestContext::new(cx, true).await;
1231
1232        cx.set_state("aˇbc", Mode::Normal);
1233        cx.simulate_keystrokes("ctrl-v");
1234        assert_eq!(cx.mode(), Mode::VisualBlock);
1235        cx.simulate_keystrokes("cmd-shift-p escape");
1236        assert_eq!(cx.mode(), Mode::VisualBlock);
1237    }
1238
1239    #[gpui::test]
1240    async fn test_gn(cx: &mut gpui::TestAppContext) {
1241        let mut cx = NeovimBackedTestContext::new(cx).await;
1242
1243        cx.set_shared_state("aaˇ aa aa aa aa").await;
1244        cx.simulate_shared_keystrokes("/ a a enter").await;
1245        cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1246        cx.simulate_shared_keystrokes("g n").await;
1247        cx.shared_state().await.assert_eq("aa «aaˇ» aa aa aa");
1248        cx.simulate_shared_keystrokes("g n").await;
1249        cx.shared_state().await.assert_eq("aa «aa aaˇ» aa aa");
1250        cx.simulate_shared_keystrokes("escape d g n").await;
1251        cx.shared_state().await.assert_eq("aa aa ˇ aa aa");
1252
1253        cx.set_shared_state("aaˇ aa aa aa aa").await;
1254        cx.simulate_shared_keystrokes("/ a a enter").await;
1255        cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1256        cx.simulate_shared_keystrokes("3 g n").await;
1257        cx.shared_state().await.assert_eq("aa aa aa «aaˇ» aa");
1258
1259        cx.set_shared_state("aaˇ aa aa aa aa").await;
1260        cx.simulate_shared_keystrokes("/ a a enter").await;
1261        cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1262        cx.simulate_shared_keystrokes("g shift-n").await;
1263        cx.shared_state().await.assert_eq("aa «ˇaa» aa aa aa");
1264        cx.simulate_shared_keystrokes("g shift-n").await;
1265        cx.shared_state().await.assert_eq("«ˇaa aa» aa aa aa");
1266    }
1267
1268    #[gpui::test]
1269    async fn test_gl(cx: &mut gpui::TestAppContext) {
1270        let mut cx = VimTestContext::new(cx, true).await;
1271
1272        cx.set_state("aaˇ aa\naa", Mode::Normal);
1273        cx.simulate_keystrokes("g l");
1274        cx.assert_state("«aaˇ» «aaˇ»\naa", Mode::Visual);
1275        cx.simulate_keystrokes("g >");
1276        cx.assert_state("«aaˇ» aa\n«aaˇ»", Mode::Visual);
1277    }
1278
1279    #[gpui::test]
1280    async fn test_dgn_repeat(cx: &mut gpui::TestAppContext) {
1281        let mut cx = NeovimBackedTestContext::new(cx).await;
1282
1283        cx.set_shared_state("aaˇ aa aa aa aa").await;
1284        cx.simulate_shared_keystrokes("/ a a enter").await;
1285        cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1286        cx.simulate_shared_keystrokes("d g n").await;
1287
1288        cx.shared_state().await.assert_eq("aa ˇ aa aa aa");
1289        cx.simulate_shared_keystrokes(".").await;
1290        cx.shared_state().await.assert_eq("aa  ˇ aa aa");
1291        cx.simulate_shared_keystrokes(".").await;
1292        cx.shared_state().await.assert_eq("aa   ˇ aa");
1293    }
1294
1295    #[gpui::test]
1296    async fn test_cgn_repeat(cx: &mut gpui::TestAppContext) {
1297        let mut cx = NeovimBackedTestContext::new(cx).await;
1298
1299        cx.set_shared_state("aaˇ aa aa aa aa").await;
1300        cx.simulate_shared_keystrokes("/ a a enter").await;
1301        cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1302        cx.simulate_shared_keystrokes("c g n x escape").await;
1303        cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1304        cx.simulate_shared_keystrokes(".").await;
1305        cx.shared_state().await.assert_eq("aa x ˇx aa aa");
1306    }
1307
1308    #[gpui::test]
1309    async fn test_cgn_nomatch(cx: &mut gpui::TestAppContext) {
1310        let mut cx = NeovimBackedTestContext::new(cx).await;
1311
1312        cx.set_shared_state("aaˇ aa aa aa aa").await;
1313        cx.simulate_shared_keystrokes("/ b b enter").await;
1314        cx.shared_state().await.assert_eq("aaˇ aa aa aa aa");
1315        cx.simulate_shared_keystrokes("c g n x escape").await;
1316        cx.shared_state().await.assert_eq("aaˇaa aa aa aa");
1317        cx.simulate_shared_keystrokes(".").await;
1318        cx.shared_state().await.assert_eq("aaˇa aa aa aa");
1319
1320        cx.set_shared_state("aaˇ bb aa aa aa").await;
1321        cx.simulate_shared_keystrokes("/ b b enter").await;
1322        cx.shared_state().await.assert_eq("aa ˇbb aa aa aa");
1323        cx.simulate_shared_keystrokes("c g n x escape").await;
1324        cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1325        cx.simulate_shared_keystrokes(".").await;
1326        cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1327    }
1328
1329    #[gpui::test]
1330    async fn test_visual_shift_d(cx: &mut gpui::TestAppContext) {
1331        let mut cx = NeovimBackedTestContext::new(cx).await;
1332
1333        cx.set_shared_state(indoc! {
1334            "The ˇquick brown
1335            fox jumps over
1336            the lazy dog
1337            "
1338        })
1339        .await;
1340        cx.simulate_shared_keystrokes("v down shift-d").await;
1341        cx.shared_state().await.assert_eq(indoc! {
1342            "the ˇlazy dog\n"
1343        });
1344
1345        cx.set_shared_state(indoc! {
1346            "The ˇquick brown
1347            fox jumps over
1348            the lazy dog
1349            "
1350        })
1351        .await;
1352        cx.simulate_shared_keystrokes("ctrl-v down shift-d").await;
1353        cx.shared_state().await.assert_eq(indoc! {
1354            "Theˇ•
1355            fox•
1356            the lazy dog
1357            "
1358        });
1359    }
1360
1361    #[gpui::test]
1362    async fn test_gv(cx: &mut gpui::TestAppContext) {
1363        let mut cx = NeovimBackedTestContext::new(cx).await;
1364
1365        cx.set_shared_state(indoc! {
1366            "The ˇquick brown"
1367        })
1368        .await;
1369        cx.simulate_shared_keystrokes("v i w escape g v").await;
1370        cx.shared_state().await.assert_eq(indoc! {
1371            "The «quickˇ» brown"
1372        });
1373
1374        cx.simulate_shared_keystrokes("o escape g v").await;
1375        cx.shared_state().await.assert_eq(indoc! {
1376            "The «ˇquick» brown"
1377        });
1378
1379        cx.simulate_shared_keystrokes("escape ^ ctrl-v l").await;
1380        cx.shared_state().await.assert_eq(indoc! {
1381            "«Thˇ»e quick brown"
1382        });
1383        cx.simulate_shared_keystrokes("g v").await;
1384        cx.shared_state().await.assert_eq(indoc! {
1385            "The «ˇquick» brown"
1386        });
1387        cx.simulate_shared_keystrokes("g v").await;
1388        cx.shared_state().await.assert_eq(indoc! {
1389            "«Thˇ»e quick brown"
1390        });
1391
1392        cx.set_state(
1393            indoc! {"
1394            fiˇsh one
1395            fish two
1396            fish red
1397            fish blue
1398        "},
1399            Mode::Normal,
1400        );
1401        cx.simulate_keystrokes("4 g l escape escape g v");
1402        cx.assert_state(
1403            indoc! {"
1404                «fishˇ» one
1405                «fishˇ» two
1406                «fishˇ» red
1407                «fishˇ» blue
1408            "},
1409            Mode::Visual,
1410        );
1411        cx.simulate_keystrokes("y g v");
1412        cx.assert_state(
1413            indoc! {"
1414                «fishˇ» one
1415                «fishˇ» two
1416                «fishˇ» red
1417                «fishˇ» blue
1418            "},
1419            Mode::Visual,
1420        );
1421    }
1422}