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