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