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