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