visual.rs

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