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.line_len(MultiBufferRow(start.row - 1)),
 667                                )
 668                                .to_display_point(map)
 669                            }
 670                        });
 671                    });
 672                }
 673                editor.insert("", window, cx);
 674
 675                // Fixup cursor position after the deletion
 676                editor.set_clip_at_line_ends(true, cx);
 677                editor.change_selections(Default::default(), window, cx, |s| {
 678                    s.move_with(|map, selection| {
 679                        let mut cursor = selection.head().to_point(map);
 680
 681                        if let Some(column) = original_columns.get(&selection.id) {
 682                            cursor.column = *column
 683                        }
 684                        let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left);
 685                        selection.collapse_to(cursor, selection.goal)
 686                    });
 687                    if vim.mode == Mode::VisualBlock {
 688                        s.select_anchors(vec![s.first_anchor()])
 689                    }
 690                });
 691            })
 692        });
 693        self.switch_mode(Mode::Normal, true, window, cx);
 694    }
 695
 696    pub fn visual_yank(&mut self, line_mode: bool, window: &mut Window, cx: &mut Context<Self>) {
 697        self.store_visual_marks(window, cx);
 698        self.update_editor(cx, |vim, editor, cx| {
 699            let line_mode = line_mode || editor.selections.line_mode();
 700
 701            // For visual line mode, adjust selections to avoid yanking the next line when on \n
 702            if line_mode && vim.mode != Mode::VisualBlock {
 703                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
 704                    s.move_with(|map, selection| {
 705                        let start = selection.start.to_point(map);
 706                        let end = selection.end.to_point(map);
 707                        if end.column == 0 && end > start {
 708                            let row = end.row.saturating_sub(1);
 709                            selection.end =
 710                                Point::new(row, map.buffer_snapshot.line_len(MultiBufferRow(row)))
 711                                    .to_display_point(map);
 712                        }
 713                    });
 714                });
 715            }
 716
 717            editor.selections.set_line_mode(line_mode);
 718            let kind = if line_mode {
 719                MotionKind::Linewise
 720            } else {
 721                MotionKind::Exclusive
 722            };
 723            vim.yank_selections_content(editor, kind, window, cx);
 724            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
 725                s.move_with(|map, selection| {
 726                    if line_mode {
 727                        selection.start = start_of_line(map, false, selection.start);
 728                    };
 729                    selection.collapse_to(selection.start, SelectionGoal::None)
 730                });
 731                if vim.mode == Mode::VisualBlock {
 732                    s.select_anchors(vec![s.first_anchor()])
 733                }
 734            });
 735        });
 736        self.switch_mode(Mode::Normal, true, window, cx);
 737    }
 738
 739    pub(crate) fn visual_replace(
 740        &mut self,
 741        text: Arc<str>,
 742        window: &mut Window,
 743        cx: &mut Context<Self>,
 744    ) {
 745        self.stop_recording(cx);
 746        self.update_editor(cx, |_, editor, cx| {
 747            editor.transact(window, cx, |editor, window, cx| {
 748                let (display_map, selections) = editor.selections.all_adjusted_display(cx);
 749
 750                // Selections are biased right at the start. So we need to store
 751                // anchors that are biased left so that we can restore the selections
 752                // after the change
 753                let stable_anchors = editor
 754                    .selections
 755                    .disjoint_anchors_arc()
 756                    .iter()
 757                    .map(|selection| {
 758                        let start = selection.start.bias_left(&display_map.buffer_snapshot);
 759                        start..start
 760                    })
 761                    .collect::<Vec<_>>();
 762
 763                let mut edits = Vec::new();
 764                for selection in selections.iter() {
 765                    let selection = selection.clone();
 766                    for row_range in
 767                        movement::split_display_range_by_lines(&display_map, selection.range())
 768                    {
 769                        let range = row_range.start.to_offset(&display_map, Bias::Right)
 770                            ..row_range.end.to_offset(&display_map, Bias::Right);
 771                        let text = text.repeat(range.len());
 772                        edits.push((range, text));
 773                    }
 774                }
 775
 776                editor.edit(edits, cx);
 777                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
 778                    s.select_ranges(stable_anchors)
 779                });
 780            });
 781        });
 782        self.switch_mode(Mode::Normal, false, window, cx);
 783    }
 784
 785    pub fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
 786        Vim::take_forced_motion(cx);
 787        let count =
 788            Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 });
 789        self.update_editor(cx, |_, editor, cx| {
 790            editor.set_clip_at_line_ends(false, cx);
 791            for _ in 0..count {
 792                if editor
 793                    .select_next(&Default::default(), window, cx)
 794                    .log_err()
 795                    .is_none()
 796                {
 797                    break;
 798                }
 799            }
 800        });
 801    }
 802
 803    pub fn select_previous(
 804        &mut self,
 805        _: &SelectPrevious,
 806        window: &mut Window,
 807        cx: &mut Context<Self>,
 808    ) {
 809        Vim::take_forced_motion(cx);
 810        let count =
 811            Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 });
 812        self.update_editor(cx, |_, editor, cx| {
 813            for _ in 0..count {
 814                if editor
 815                    .select_previous(&Default::default(), window, cx)
 816                    .log_err()
 817                    .is_none()
 818                {
 819                    break;
 820                }
 821            }
 822        });
 823    }
 824
 825    pub fn select_match(
 826        &mut self,
 827        direction: Direction,
 828        window: &mut Window,
 829        cx: &mut Context<Self>,
 830    ) {
 831        Vim::take_forced_motion(cx);
 832        let count = Vim::take_count(cx).unwrap_or(1);
 833        let Some(pane) = self.pane(window, cx) else {
 834            return;
 835        };
 836        let vim_is_normal = self.mode == Mode::Normal;
 837        let mut start_selection = 0usize;
 838        let mut end_selection = 0usize;
 839
 840        self.update_editor(cx, |_, editor, _| {
 841            editor.set_collapse_matches(false);
 842        });
 843        if vim_is_normal {
 844            pane.update(cx, |pane, cx| {
 845                if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()
 846                {
 847                    search_bar.update(cx, |search_bar, cx| {
 848                        if !search_bar.has_active_match() || !search_bar.show(window, cx) {
 849                            return;
 850                        }
 851                        // without update_match_index there is a bug when the cursor is before the first match
 852                        search_bar.update_match_index(window, cx);
 853                        search_bar.select_match(direction.opposite(), 1, window, cx);
 854                    });
 855                }
 856            });
 857        }
 858        self.update_editor(cx, |_, editor, cx| {
 859            let latest = editor.selections.newest::<usize>(cx);
 860            start_selection = latest.start;
 861            end_selection = latest.end;
 862        });
 863
 864        let mut match_exists = false;
 865        pane.update(cx, |pane, cx| {
 866            if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 867                search_bar.update(cx, |search_bar, cx| {
 868                    search_bar.update_match_index(window, cx);
 869                    search_bar.select_match(direction, count, window, cx);
 870                    match_exists = search_bar.match_exists(window, cx);
 871                });
 872            }
 873        });
 874        if !match_exists {
 875            self.clear_operator(window, cx);
 876            self.stop_replaying(cx);
 877            return;
 878        }
 879        self.update_editor(cx, |_, editor, cx| {
 880            let latest = editor.selections.newest::<usize>(cx);
 881            if vim_is_normal {
 882                start_selection = latest.start;
 883                end_selection = latest.end;
 884            } else {
 885                start_selection = start_selection.min(latest.start);
 886                end_selection = end_selection.max(latest.end);
 887            }
 888            if direction == Direction::Prev {
 889                std::mem::swap(&mut start_selection, &mut end_selection);
 890            }
 891            editor.change_selections(Default::default(), window, cx, |s| {
 892                s.select_ranges([start_selection..end_selection]);
 893            });
 894            editor.set_collapse_matches(true);
 895        });
 896
 897        match self.maybe_pop_operator() {
 898            Some(Operator::Change) => self.substitute(None, false, window, cx),
 899            Some(Operator::Delete) => {
 900                self.stop_recording(cx);
 901                self.visual_delete(false, window, cx)
 902            }
 903            Some(Operator::Yank) => self.visual_yank(false, window, cx),
 904            _ => {} // Ignoring other operators
 905        }
 906    }
 907}
 908#[cfg(test)]
 909mod test {
 910    use indoc::indoc;
 911    use workspace::item::Item;
 912
 913    use crate::{
 914        state::Mode,
 915        test::{NeovimBackedTestContext, VimTestContext},
 916    };
 917
 918    #[gpui::test]
 919    async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
 920        let mut cx = NeovimBackedTestContext::new(cx).await;
 921
 922        cx.set_shared_state(indoc! {
 923            "The ˇquick brown
 924            fox jumps over
 925            the lazy dog"
 926        })
 927        .await;
 928        let cursor = cx.update_editor(|editor, _, cx| editor.pixel_position_of_cursor(cx));
 929
 930        // entering visual mode should select the character
 931        // under cursor
 932        cx.simulate_shared_keystrokes("v").await;
 933        cx.shared_state()
 934            .await
 935            .assert_eq(indoc! { "The «qˇ»uick brown
 936            fox jumps over
 937            the lazy dog"});
 938        cx.update_editor(|editor, _, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
 939
 940        // forwards motions should extend the selection
 941        cx.simulate_shared_keystrokes("w j").await;
 942        cx.shared_state().await.assert_eq(indoc! { "The «quick brown
 943            fox jumps oˇ»ver
 944            the lazy dog"});
 945
 946        cx.simulate_shared_keystrokes("escape").await;
 947        cx.shared_state().await.assert_eq(indoc! { "The quick brown
 948            fox jumps ˇover
 949            the lazy dog"});
 950
 951        // motions work backwards
 952        cx.simulate_shared_keystrokes("v k b").await;
 953        cx.shared_state()
 954            .await
 955            .assert_eq(indoc! { "The «ˇquick brown
 956            fox jumps o»ver
 957            the lazy dog"});
 958
 959        // works on empty lines
 960        cx.set_shared_state(indoc! {"
 961            a
 962            ˇ
 963            b
 964            "})
 965            .await;
 966        let cursor = cx.update_editor(|editor, _, cx| editor.pixel_position_of_cursor(cx));
 967        cx.simulate_shared_keystrokes("v").await;
 968        cx.shared_state().await.assert_eq(indoc! {"
 969            a
 970            «
 971            ˇ»b
 972        "});
 973        cx.update_editor(|editor, _, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
 974
 975        // toggles off again
 976        cx.simulate_shared_keystrokes("v").await;
 977        cx.shared_state().await.assert_eq(indoc! {"
 978            a
 979            ˇ
 980            b
 981            "});
 982
 983        // works at the end of a document
 984        cx.set_shared_state(indoc! {"
 985            a
 986            b
 987            ˇ"})
 988            .await;
 989
 990        cx.simulate_shared_keystrokes("v").await;
 991        cx.shared_state().await.assert_eq(indoc! {"
 992            a
 993            b
 994            ˇ"});
 995    }
 996
 997    #[gpui::test]
 998    async fn test_visual_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
 999        let mut cx = VimTestContext::new(cx, true).await;
1000
1001        cx.set_state(
1002            indoc! {
1003                "«The quick brown
1004                fox jumps over
1005                the lazy dogˇ»"
1006            },
1007            Mode::Visual,
1008        );
1009        cx.simulate_keystrokes("g shift-i");
1010        cx.assert_state(
1011            indoc! {
1012                "ˇThe quick brown
1013                ˇfox jumps over
1014                ˇthe lazy dog"
1015            },
1016            Mode::Insert,
1017        );
1018    }
1019
1020    #[gpui::test]
1021    async fn test_visual_insert_end_of_line(cx: &mut gpui::TestAppContext) {
1022        let mut cx = VimTestContext::new(cx, true).await;
1023
1024        cx.set_state(
1025            indoc! {
1026                "«The quick brown
1027                fox jumps over
1028                the lazy dogˇ»"
1029            },
1030            Mode::Visual,
1031        );
1032        cx.simulate_keystrokes("g shift-a");
1033        cx.assert_state(
1034            indoc! {
1035                "The quick brownˇ
1036                fox jumps overˇ
1037                the lazy dogˇ"
1038            },
1039            Mode::Insert,
1040        );
1041    }
1042
1043    #[gpui::test]
1044    async fn test_enter_visual_line_mode(cx: &mut gpui::TestAppContext) {
1045        let mut cx = NeovimBackedTestContext::new(cx).await;
1046
1047        cx.set_shared_state(indoc! {
1048            "The ˇquick brown
1049            fox jumps over
1050            the lazy dog"
1051        })
1052        .await;
1053        cx.simulate_shared_keystrokes("shift-v").await;
1054        cx.shared_state()
1055            .await
1056            .assert_eq(indoc! { "The «qˇ»uick brown
1057            fox jumps over
1058            the lazy dog"});
1059        cx.simulate_shared_keystrokes("x").await;
1060        cx.shared_state().await.assert_eq(indoc! { "fox ˇjumps over
1061        the lazy dog"});
1062
1063        // it should work on empty lines
1064        cx.set_shared_state(indoc! {"
1065            a
1066            ˇ
1067            b"})
1068            .await;
1069        cx.simulate_shared_keystrokes("shift-v").await;
1070        cx.shared_state().await.assert_eq(indoc! {"
1071            a
1072            «
1073            ˇ»b"});
1074        cx.simulate_shared_keystrokes("x").await;
1075        cx.shared_state().await.assert_eq(indoc! {"
1076            a
1077            ˇb"});
1078
1079        // it should work at the end of the document
1080        cx.set_shared_state(indoc! {"
1081            a
1082            b
1083            ˇ"})
1084            .await;
1085        let cursor = cx.update_editor(|editor, _, cx| editor.pixel_position_of_cursor(cx));
1086        cx.simulate_shared_keystrokes("shift-v").await;
1087        cx.shared_state().await.assert_eq(indoc! {"
1088            a
1089            b
1090            ˇ"});
1091        cx.update_editor(|editor, _, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
1092        cx.simulate_shared_keystrokes("x").await;
1093        cx.shared_state().await.assert_eq(indoc! {"
1094            a
1095            ˇb"});
1096    }
1097
1098    #[gpui::test]
1099    async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
1100        let mut cx = NeovimBackedTestContext::new(cx).await;
1101
1102        cx.simulate("v w", "The quick ˇbrown")
1103            .await
1104            .assert_matches();
1105
1106        cx.simulate("v w x", "The quick ˇbrown")
1107            .await
1108            .assert_matches();
1109        cx.simulate(
1110            "v w j x",
1111            indoc! {"
1112                The ˇquick brown
1113                fox jumps over
1114                the lazy dog"},
1115        )
1116        .await
1117        .assert_matches();
1118        // Test pasting code copied on delete
1119        cx.simulate_shared_keystrokes("j p").await;
1120        cx.shared_state().await.assert_matches();
1121
1122        cx.simulate_at_each_offset(
1123            "v w j x",
1124            indoc! {"
1125                The ˇquick brown
1126                fox jumps over
1127                the ˇlazy dog"},
1128        )
1129        .await
1130        .assert_matches();
1131        cx.simulate_at_each_offset(
1132            "v b k x",
1133            indoc! {"
1134                The ˇquick brown
1135                fox jumps ˇover
1136                the ˇlazy dog"},
1137        )
1138        .await
1139        .assert_matches();
1140    }
1141
1142    #[gpui::test]
1143    async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
1144        let mut cx = NeovimBackedTestContext::new(cx).await;
1145
1146        cx.set_shared_state(indoc! {"
1147                The quˇick brown
1148                fox jumps over
1149                the lazy dog"})
1150            .await;
1151        cx.simulate_shared_keystrokes("shift-v x").await;
1152        cx.shared_state().await.assert_matches();
1153
1154        // Test pasting code copied on delete
1155        cx.simulate_shared_keystrokes("p").await;
1156        cx.shared_state().await.assert_matches();
1157
1158        cx.set_shared_state(indoc! {"
1159                The quick brown
1160                fox jumps over
1161                the laˇzy dog"})
1162            .await;
1163        cx.simulate_shared_keystrokes("shift-v x").await;
1164        cx.shared_state().await.assert_matches();
1165        cx.shared_clipboard().await.assert_eq("the lazy dog\n");
1166
1167        cx.set_shared_state(indoc! {"
1168                                The quˇick brown
1169                                fox jumps over
1170                                the lazy dog"})
1171            .await;
1172        cx.simulate_shared_keystrokes("shift-v j x").await;
1173        cx.shared_state().await.assert_matches();
1174        // Test pasting code copied on delete
1175        cx.simulate_shared_keystrokes("p").await;
1176        cx.shared_state().await.assert_matches();
1177
1178        cx.set_shared_state(indoc! {"
1179            The ˇlong line
1180            should not
1181            crash
1182            "})
1183            .await;
1184        cx.simulate_shared_keystrokes("shift-v $ x").await;
1185        cx.shared_state().await.assert_matches();
1186    }
1187
1188    #[gpui::test]
1189    async fn test_visual_yank(cx: &mut gpui::TestAppContext) {
1190        let mut cx = NeovimBackedTestContext::new(cx).await;
1191
1192        cx.set_shared_state("The quick ˇbrown").await;
1193        cx.simulate_shared_keystrokes("v w y").await;
1194        cx.shared_state().await.assert_eq("The quick ˇbrown");
1195        cx.shared_clipboard().await.assert_eq("brown");
1196
1197        cx.set_shared_state(indoc! {"
1198                The ˇquick brown
1199                fox jumps over
1200                the lazy dog"})
1201            .await;
1202        cx.simulate_shared_keystrokes("v w j y").await;
1203        cx.shared_state().await.assert_eq(indoc! {"
1204                    The ˇquick brown
1205                    fox jumps over
1206                    the lazy dog"});
1207        cx.shared_clipboard().await.assert_eq(indoc! {"
1208                quick brown
1209                fox jumps o"});
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("v w j y").await;
1217        cx.shared_state().await.assert_eq(indoc! {"
1218                    The quick brown
1219                    fox jumps over
1220                    the ˇlazy dog"});
1221        cx.shared_clipboard().await.assert_eq("lazy d");
1222        cx.simulate_shared_keystrokes("shift-v y").await;
1223        cx.shared_clipboard().await.assert_eq("the lazy dog\n");
1224
1225        cx.set_shared_state(indoc! {"
1226                    The ˇquick brown
1227                    fox jumps over
1228                    the lazy dog"})
1229            .await;
1230        cx.simulate_shared_keystrokes("v b k y").await;
1231        cx.shared_state().await.assert_eq(indoc! {"
1232                    ˇThe quick brown
1233                    fox jumps over
1234                    the lazy dog"});
1235        assert_eq!(
1236            cx.read_from_clipboard()
1237                .map(|item| item.text().unwrap())
1238                .unwrap(),
1239            "The q"
1240        );
1241
1242        cx.set_shared_state(indoc! {"
1243                    The quick brown
1244                    fox ˇjumps over
1245                    the lazy dog"})
1246            .await;
1247        cx.simulate_shared_keystrokes("shift-v shift-g shift-y")
1248            .await;
1249        cx.shared_state().await.assert_eq(indoc! {"
1250                    The quick brown
1251                    ˇfox jumps over
1252                    the lazy dog"});
1253        cx.shared_clipboard()
1254            .await
1255            .assert_eq("fox jumps over\nthe lazy dog\n");
1256
1257        cx.set_shared_state(indoc! {"
1258                    The quick brown
1259                    fox ˇjumps over
1260                    the lazy dog"})
1261            .await;
1262        cx.simulate_shared_keystrokes("shift-v $ shift-y").await;
1263        cx.shared_state().await.assert_eq(indoc! {"
1264                    The quick brown
1265                    ˇfox jumps over
1266                    the lazy dog"});
1267        cx.shared_clipboard().await.assert_eq("fox jumps over\n");
1268    }
1269
1270    #[gpui::test]
1271    async fn test_visual_block_mode(cx: &mut gpui::TestAppContext) {
1272        let mut cx = NeovimBackedTestContext::new(cx).await;
1273
1274        cx.set_shared_state(indoc! {
1275            "The ˇquick brown
1276             fox jumps over
1277             the lazy dog"
1278        })
1279        .await;
1280        cx.simulate_shared_keystrokes("ctrl-v").await;
1281        cx.shared_state().await.assert_eq(indoc! {
1282            "The «qˇ»uick brown
1283            fox jumps over
1284            the lazy dog"
1285        });
1286        cx.simulate_shared_keystrokes("2 down").await;
1287        cx.shared_state().await.assert_eq(indoc! {
1288            "The «qˇ»uick brown
1289            fox «jˇ»umps over
1290            the «lˇ»azy dog"
1291        });
1292        cx.simulate_shared_keystrokes("e").await;
1293        cx.shared_state().await.assert_eq(indoc! {
1294            "The «quicˇ»k brown
1295            fox «jumpˇ»s over
1296            the «lazyˇ» dog"
1297        });
1298        cx.simulate_shared_keystrokes("^").await;
1299        cx.shared_state().await.assert_eq(indoc! {
1300            "«ˇThe q»uick brown
1301            «ˇfox j»umps over
1302            «ˇthe l»azy dog"
1303        });
1304        cx.simulate_shared_keystrokes("$").await;
1305        cx.shared_state().await.assert_eq(indoc! {
1306            "The «quick brownˇ»
1307            fox «jumps overˇ»
1308            the «lazy dogˇ»"
1309        });
1310        cx.simulate_shared_keystrokes("shift-f space").await;
1311        cx.shared_state().await.assert_eq(indoc! {
1312            "The «quickˇ» brown
1313            fox «jumpsˇ» over
1314            the «lazy ˇ»dog"
1315        });
1316
1317        // toggling through visual mode works as expected
1318        cx.simulate_shared_keystrokes("v").await;
1319        cx.shared_state().await.assert_eq(indoc! {
1320            "The «quick brown
1321            fox jumps over
1322            the lazy ˇ»dog"
1323        });
1324        cx.simulate_shared_keystrokes("ctrl-v").await;
1325        cx.shared_state().await.assert_eq(indoc! {
1326            "The «quickˇ» brown
1327            fox «jumpsˇ» over
1328            the «lazy ˇ»dog"
1329        });
1330
1331        cx.set_shared_state(indoc! {
1332            "The ˇquick
1333             brown
1334             fox
1335             jumps over the
1336
1337             lazy dog
1338            "
1339        })
1340        .await;
1341        cx.simulate_shared_keystrokes("ctrl-v down down").await;
1342        cx.shared_state().await.assert_eq(indoc! {
1343            "The«ˇ q»uick
1344            bro«ˇwn»
1345            foxˇ
1346            jumps over the
1347
1348            lazy dog
1349            "
1350        });
1351        cx.simulate_shared_keystrokes("down").await;
1352        cx.shared_state().await.assert_eq(indoc! {
1353            "The «qˇ»uick
1354            brow«nˇ»
1355            fox
1356            jump«sˇ» over the
1357
1358            lazy dog
1359            "
1360        });
1361        cx.simulate_shared_keystrokes("left").await;
1362        cx.shared_state().await.assert_eq(indoc! {
1363            "The«ˇ q»uick
1364            bro«ˇwn»
1365            foxˇ
1366            jum«ˇps» over the
1367
1368            lazy dog
1369            "
1370        });
1371        cx.simulate_shared_keystrokes("s o escape").await;
1372        cx.shared_state().await.assert_eq(indoc! {
1373            "Theˇouick
1374            broo
1375            foxo
1376            jumo over the
1377
1378            lazy dog
1379            "
1380        });
1381
1382        // https://github.com/zed-industries/zed/issues/6274
1383        cx.set_shared_state(indoc! {
1384            "Theˇ quick brown
1385
1386            fox jumps over
1387            the lazy dog
1388            "
1389        })
1390        .await;
1391        cx.simulate_shared_keystrokes("l ctrl-v j j").await;
1392        cx.shared_state().await.assert_eq(indoc! {
1393            "The «qˇ»uick brown
1394
1395            fox «jˇ»umps over
1396            the lazy dog
1397            "
1398        });
1399    }
1400
1401    #[gpui::test]
1402    async fn test_visual_block_issue_2123(cx: &mut gpui::TestAppContext) {
1403        let mut cx = NeovimBackedTestContext::new(cx).await;
1404
1405        cx.set_shared_state(indoc! {
1406            "The ˇquick brown
1407            fox jumps over
1408            the lazy dog
1409            "
1410        })
1411        .await;
1412        cx.simulate_shared_keystrokes("ctrl-v right down").await;
1413        cx.shared_state().await.assert_eq(indoc! {
1414            "The «quˇ»ick brown
1415            fox «juˇ»mps over
1416            the lazy dog
1417            "
1418        });
1419    }
1420    #[gpui::test]
1421    async fn test_visual_block_mode_down_right(cx: &mut gpui::TestAppContext) {
1422        let mut cx = NeovimBackedTestContext::new(cx).await;
1423        cx.set_shared_state(indoc! {"
1424            The ˇquick brown
1425            fox jumps over
1426            the lazy dog"})
1427            .await;
1428        cx.simulate_shared_keystrokes("ctrl-v l l l l l j").await;
1429        cx.shared_state().await.assert_eq(indoc! {"
1430            The «quick ˇ»brown
1431            fox «jumps ˇ»over
1432            the lazy dog"});
1433    }
1434
1435    #[gpui::test]
1436    async fn test_visual_block_mode_up_left(cx: &mut gpui::TestAppContext) {
1437        let mut cx = NeovimBackedTestContext::new(cx).await;
1438        cx.set_shared_state(indoc! {"
1439            The quick brown
1440            fox jumpsˇ over
1441            the lazy dog"})
1442            .await;
1443        cx.simulate_shared_keystrokes("ctrl-v h h h h h k").await;
1444        cx.shared_state().await.assert_eq(indoc! {"
1445            The «ˇquick »brown
1446            fox «ˇjumps »over
1447            the lazy dog"});
1448    }
1449
1450    #[gpui::test]
1451    async fn test_visual_block_mode_other_end(cx: &mut gpui::TestAppContext) {
1452        let mut cx = NeovimBackedTestContext::new(cx).await;
1453        cx.set_shared_state(indoc! {"
1454            The quick brown
1455            fox jˇumps over
1456            the lazy dog"})
1457            .await;
1458        cx.simulate_shared_keystrokes("ctrl-v l l l l j").await;
1459        cx.shared_state().await.assert_eq(indoc! {"
1460            The quick brown
1461            fox j«umps ˇ»over
1462            the l«azy dˇ»og"});
1463        cx.simulate_shared_keystrokes("o k").await;
1464        cx.shared_state().await.assert_eq(indoc! {"
1465            The q«ˇuick »brown
1466            fox j«ˇumps »over
1467            the l«ˇazy d»og"});
1468    }
1469
1470    #[gpui::test]
1471    async fn test_visual_block_mode_shift_other_end(cx: &mut gpui::TestAppContext) {
1472        let mut cx = NeovimBackedTestContext::new(cx).await;
1473        cx.set_shared_state(indoc! {"
1474            The quick brown
1475            fox jˇumps over
1476            the lazy dog"})
1477            .await;
1478        cx.simulate_shared_keystrokes("ctrl-v l l l l j").await;
1479        cx.shared_state().await.assert_eq(indoc! {"
1480            The quick brown
1481            fox j«umps ˇ»over
1482            the l«azy dˇ»og"});
1483        cx.simulate_shared_keystrokes("shift-o k").await;
1484        cx.shared_state().await.assert_eq(indoc! {"
1485            The quick brown
1486            fox j«ˇumps »over
1487            the lazy dog"});
1488    }
1489
1490    #[gpui::test]
1491    async fn test_visual_block_insert(cx: &mut gpui::TestAppContext) {
1492        let mut cx = NeovimBackedTestContext::new(cx).await;
1493
1494        cx.set_shared_state(indoc! {
1495            "ˇThe quick brown
1496            fox jumps over
1497            the lazy dog
1498            "
1499        })
1500        .await;
1501        cx.simulate_shared_keystrokes("ctrl-v 9 down").await;
1502        cx.shared_state().await.assert_eq(indoc! {
1503            "«Tˇ»he quick brown
1504            «fˇ»ox jumps over
1505            «tˇ»he lazy dog
1506            ˇ"
1507        });
1508
1509        cx.simulate_shared_keystrokes("shift-i k escape").await;
1510        cx.shared_state().await.assert_eq(indoc! {
1511            "ˇkThe quick brown
1512            kfox jumps over
1513            kthe lazy dog
1514            k"
1515        });
1516
1517        cx.set_shared_state(indoc! {
1518            "ˇThe quick brown
1519            fox jumps over
1520            the lazy dog
1521            "
1522        })
1523        .await;
1524        cx.simulate_shared_keystrokes("ctrl-v 9 down").await;
1525        cx.shared_state().await.assert_eq(indoc! {
1526            "«Tˇ»he quick brown
1527            «fˇ»ox jumps over
1528            «tˇ»he lazy dog
1529            ˇ"
1530        });
1531        cx.simulate_shared_keystrokes("c k escape").await;
1532        cx.shared_state().await.assert_eq(indoc! {
1533            "ˇkhe quick brown
1534            kox jumps over
1535            khe lazy dog
1536            k"
1537        });
1538    }
1539
1540    #[gpui::test]
1541    async fn test_visual_block_wrapping_selection(cx: &mut gpui::TestAppContext) {
1542        let mut cx = NeovimBackedTestContext::new(cx).await;
1543
1544        // Ensure that the editor is wrapping lines at 12 columns so that each
1545        // of the lines ends up being wrapped.
1546        cx.set_shared_wrap(12).await;
1547        cx.set_shared_state(indoc! {
1548            "ˇ12345678901234567890
1549            12345678901234567890
1550            12345678901234567890
1551            "
1552        })
1553        .await;
1554        cx.simulate_shared_keystrokes("ctrl-v j").await;
1555        cx.shared_state().await.assert_eq(indoc! {
1556            "«1ˇ»2345678901234567890
1557            «1ˇ»2345678901234567890
1558            12345678901234567890
1559            "
1560        });
1561
1562        // Test with lines taking up different amounts of display rows to ensure
1563        // that, even in that case, only the buffer rows are taken into account.
1564        cx.set_shared_state(indoc! {
1565            "ˇ123456789012345678901234567890123456789012345678901234567890
1566            1234567890123456789012345678901234567890
1567            12345678901234567890
1568            "
1569        })
1570        .await;
1571        cx.simulate_shared_keystrokes("ctrl-v 2 j").await;
1572        cx.shared_state().await.assert_eq(indoc! {
1573            "«1ˇ»23456789012345678901234567890123456789012345678901234567890
1574            «1ˇ»234567890123456789012345678901234567890
1575            «1ˇ»2345678901234567890
1576            "
1577        });
1578
1579        // Same scenario as above, but using the up motion to ensure that the
1580        // result is the same.
1581        cx.set_shared_state(indoc! {
1582            "123456789012345678901234567890123456789012345678901234567890
1583            1234567890123456789012345678901234567890
1584            ˇ12345678901234567890
1585            "
1586        })
1587        .await;
1588        cx.simulate_shared_keystrokes("ctrl-v 2 k").await;
1589        cx.shared_state().await.assert_eq(indoc! {
1590            "«1ˇ»23456789012345678901234567890123456789012345678901234567890
1591            «1ˇ»234567890123456789012345678901234567890
1592            «1ˇ»2345678901234567890
1593            "
1594        });
1595    }
1596
1597    #[gpui::test]
1598    async fn test_visual_object(cx: &mut gpui::TestAppContext) {
1599        let mut cx = NeovimBackedTestContext::new(cx).await;
1600
1601        cx.set_shared_state("hello (in [parˇens] o)").await;
1602        cx.simulate_shared_keystrokes("ctrl-v l").await;
1603        cx.simulate_shared_keystrokes("a ]").await;
1604        cx.shared_state()
1605            .await
1606            .assert_eq("hello (in «[parens]ˇ» o)");
1607        cx.simulate_shared_keystrokes("i (").await;
1608        cx.shared_state()
1609            .await
1610            .assert_eq("hello («in [parens] oˇ»)");
1611
1612        cx.set_shared_state("hello in a wˇord again.").await;
1613        cx.simulate_shared_keystrokes("ctrl-v l i w").await;
1614        cx.shared_state()
1615            .await
1616            .assert_eq("hello in a w«ordˇ» again.");
1617        assert_eq!(cx.mode(), Mode::VisualBlock);
1618        cx.simulate_shared_keystrokes("o a s").await;
1619        cx.shared_state()
1620            .await
1621            .assert_eq("«ˇhello in a word» again.");
1622    }
1623
1624    #[gpui::test]
1625    async fn test_visual_object_expands(cx: &mut gpui::TestAppContext) {
1626        let mut cx = NeovimBackedTestContext::new(cx).await;
1627
1628        cx.set_shared_state(indoc! {
1629            "{
1630                {
1631               ˇ }
1632            }
1633            {
1634            }
1635            "
1636        })
1637        .await;
1638        cx.simulate_shared_keystrokes("v l").await;
1639        cx.shared_state().await.assert_eq(indoc! {
1640            "{
1641                {
1642               « }ˇ»
1643            }
1644            {
1645            }
1646            "
1647        });
1648        cx.simulate_shared_keystrokes("a {").await;
1649        cx.shared_state().await.assert_eq(indoc! {
1650            "{
1651                «{
1652                }ˇ»
1653            }
1654            {
1655            }
1656            "
1657        });
1658        cx.simulate_shared_keystrokes("a {").await;
1659        cx.shared_state().await.assert_eq(indoc! {
1660            "«{
1661                {
1662                }
1663            }ˇ»
1664            {
1665            }
1666            "
1667        });
1668        // cx.simulate_shared_keystrokes("a {").await;
1669        // cx.shared_state().await.assert_eq(indoc! {
1670        //     "{
1671        //         «{
1672        //         }ˇ»
1673        //     }
1674        //     {
1675        //     }
1676        //     "
1677        // });
1678    }
1679
1680    #[gpui::test]
1681    async fn test_mode_across_command(cx: &mut gpui::TestAppContext) {
1682        let mut cx = VimTestContext::new(cx, true).await;
1683
1684        cx.set_state("aˇbc", Mode::Normal);
1685        cx.simulate_keystrokes("ctrl-v");
1686        assert_eq!(cx.mode(), Mode::VisualBlock);
1687        cx.simulate_keystrokes("cmd-shift-p escape");
1688        assert_eq!(cx.mode(), Mode::VisualBlock);
1689    }
1690
1691    #[gpui::test]
1692    async fn test_gn(cx: &mut gpui::TestAppContext) {
1693        let mut cx = NeovimBackedTestContext::new(cx).await;
1694
1695        cx.set_shared_state("aaˇ aa aa aa aa").await;
1696        cx.simulate_shared_keystrokes("/ a a enter").await;
1697        cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1698        cx.simulate_shared_keystrokes("g n").await;
1699        cx.shared_state().await.assert_eq("aa «aaˇ» aa aa aa");
1700        cx.simulate_shared_keystrokes("g n").await;
1701        cx.shared_state().await.assert_eq("aa «aa aaˇ» aa aa");
1702        cx.simulate_shared_keystrokes("escape d g n").await;
1703        cx.shared_state().await.assert_eq("aa aa ˇ aa aa");
1704
1705        cx.set_shared_state("aaˇ aa aa aa aa").await;
1706        cx.simulate_shared_keystrokes("/ a a enter").await;
1707        cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1708        cx.simulate_shared_keystrokes("3 g n").await;
1709        cx.shared_state().await.assert_eq("aa aa aa «aaˇ» aa");
1710
1711        cx.set_shared_state("aaˇ aa aa aa aa").await;
1712        cx.simulate_shared_keystrokes("/ a a enter").await;
1713        cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1714        cx.simulate_shared_keystrokes("g shift-n").await;
1715        cx.shared_state().await.assert_eq("aa «ˇaa» aa aa aa");
1716        cx.simulate_shared_keystrokes("g shift-n").await;
1717        cx.shared_state().await.assert_eq("«ˇaa aa» aa aa aa");
1718    }
1719
1720    #[gpui::test]
1721    async fn test_gl(cx: &mut gpui::TestAppContext) {
1722        let mut cx = VimTestContext::new(cx, true).await;
1723
1724        cx.set_state("aaˇ aa\naa", Mode::Normal);
1725        cx.simulate_keystrokes("g l");
1726        cx.assert_state("«aaˇ» «aaˇ»\naa", Mode::Visual);
1727        cx.simulate_keystrokes("g >");
1728        cx.assert_state("«aaˇ» aa\n«aaˇ»", Mode::Visual);
1729    }
1730
1731    #[gpui::test]
1732    async fn test_dgn_repeat(cx: &mut gpui::TestAppContext) {
1733        let mut cx = NeovimBackedTestContext::new(cx).await;
1734
1735        cx.set_shared_state("aaˇ aa aa aa aa").await;
1736        cx.simulate_shared_keystrokes("/ a a enter").await;
1737        cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1738        cx.simulate_shared_keystrokes("d g n").await;
1739
1740        cx.shared_state().await.assert_eq("aa ˇ aa aa aa");
1741        cx.simulate_shared_keystrokes(".").await;
1742        cx.shared_state().await.assert_eq("aa  ˇ aa aa");
1743        cx.simulate_shared_keystrokes(".").await;
1744        cx.shared_state().await.assert_eq("aa   ˇ aa");
1745    }
1746
1747    #[gpui::test]
1748    async fn test_cgn_repeat(cx: &mut gpui::TestAppContext) {
1749        let mut cx = NeovimBackedTestContext::new(cx).await;
1750
1751        cx.set_shared_state("aaˇ aa aa aa aa").await;
1752        cx.simulate_shared_keystrokes("/ a a enter").await;
1753        cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1754        cx.simulate_shared_keystrokes("c g n x escape").await;
1755        cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1756        cx.simulate_shared_keystrokes(".").await;
1757        cx.shared_state().await.assert_eq("aa x ˇx aa aa");
1758    }
1759
1760    #[gpui::test]
1761    async fn test_cgn_nomatch(cx: &mut gpui::TestAppContext) {
1762        let mut cx = NeovimBackedTestContext::new(cx).await;
1763
1764        cx.set_shared_state("aaˇ aa aa aa aa").await;
1765        cx.simulate_shared_keystrokes("/ b b enter").await;
1766        cx.shared_state().await.assert_eq("aaˇ aa aa aa aa");
1767        cx.simulate_shared_keystrokes("c g n x escape").await;
1768        cx.shared_state().await.assert_eq("aaˇaa aa aa aa");
1769        cx.simulate_shared_keystrokes(".").await;
1770        cx.shared_state().await.assert_eq("aaˇa aa aa aa");
1771
1772        cx.set_shared_state("aaˇ bb aa aa aa").await;
1773        cx.simulate_shared_keystrokes("/ b b enter").await;
1774        cx.shared_state().await.assert_eq("aa ˇbb aa aa aa");
1775        cx.simulate_shared_keystrokes("c g n x escape").await;
1776        cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1777        cx.simulate_shared_keystrokes(".").await;
1778        cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1779    }
1780
1781    #[gpui::test]
1782    async fn test_visual_shift_d(cx: &mut gpui::TestAppContext) {
1783        let mut cx = NeovimBackedTestContext::new(cx).await;
1784
1785        cx.set_shared_state(indoc! {
1786            "The ˇquick brown
1787            fox jumps over
1788            the lazy dog
1789            "
1790        })
1791        .await;
1792        cx.simulate_shared_keystrokes("v down shift-d").await;
1793        cx.shared_state().await.assert_eq(indoc! {
1794            "the ˇlazy dog\n"
1795        });
1796
1797        cx.set_shared_state(indoc! {
1798            "The ˇquick brown
1799            fox jumps over
1800            the lazy dog
1801            "
1802        })
1803        .await;
1804        cx.simulate_shared_keystrokes("ctrl-v down shift-d").await;
1805        cx.shared_state().await.assert_eq(indoc! {
1806            "Theˇ•
1807            fox•
1808            the lazy dog
1809            "
1810        });
1811    }
1812
1813    #[gpui::test]
1814    async fn test_shift_y(cx: &mut gpui::TestAppContext) {
1815        let mut cx = NeovimBackedTestContext::new(cx).await;
1816
1817        cx.set_shared_state(indoc! {
1818            "The ˇquick brown\n"
1819        })
1820        .await;
1821        cx.simulate_shared_keystrokes("v i w shift-y").await;
1822        cx.shared_clipboard().await.assert_eq(indoc! {
1823            "The quick brown\n"
1824        });
1825    }
1826
1827    #[gpui::test]
1828    async fn test_gv(cx: &mut gpui::TestAppContext) {
1829        let mut cx = NeovimBackedTestContext::new(cx).await;
1830
1831        cx.set_shared_state(indoc! {
1832            "The ˇquick brown"
1833        })
1834        .await;
1835        cx.simulate_shared_keystrokes("v i w escape g v").await;
1836        cx.shared_state().await.assert_eq(indoc! {
1837            "The «quickˇ» brown"
1838        });
1839
1840        cx.simulate_shared_keystrokes("o escape g v").await;
1841        cx.shared_state().await.assert_eq(indoc! {
1842            "The «ˇquick» brown"
1843        });
1844
1845        cx.simulate_shared_keystrokes("escape ^ ctrl-v l").await;
1846        cx.shared_state().await.assert_eq(indoc! {
1847            "«Thˇ»e quick brown"
1848        });
1849        cx.simulate_shared_keystrokes("g v").await;
1850        cx.shared_state().await.assert_eq(indoc! {
1851            "The «ˇquick» brown"
1852        });
1853        cx.simulate_shared_keystrokes("g v").await;
1854        cx.shared_state().await.assert_eq(indoc! {
1855            "«Thˇ»e quick brown"
1856        });
1857
1858        cx.set_state(
1859            indoc! {"
1860            fiˇsh one
1861            fish two
1862            fish red
1863            fish blue
1864        "},
1865            Mode::Normal,
1866        );
1867        cx.simulate_keystrokes("4 g l escape escape g v");
1868        cx.assert_state(
1869            indoc! {"
1870                «fishˇ» one
1871                «fishˇ» two
1872                «fishˇ» red
1873                «fishˇ» blue
1874            "},
1875            Mode::Visual,
1876        );
1877        cx.simulate_keystrokes("y g v");
1878        cx.assert_state(
1879            indoc! {"
1880                «fishˇ» one
1881                «fishˇ» two
1882                «fishˇ» red
1883                «fishˇ» blue
1884            "},
1885            Mode::Visual,
1886        );
1887    }
1888
1889    #[gpui::test]
1890    async fn test_p_g_v_y(cx: &mut gpui::TestAppContext) {
1891        let mut cx = NeovimBackedTestContext::new(cx).await;
1892
1893        cx.set_shared_state(indoc! {
1894            "The
1895            quicˇk
1896            brown
1897            fox"
1898        })
1899        .await;
1900        cx.simulate_shared_keystrokes("y y j shift-v p g v y").await;
1901        cx.shared_state().await.assert_eq(indoc! {
1902            "The
1903            quick
1904            ˇquick
1905            fox"
1906        });
1907        cx.shared_clipboard().await.assert_eq("quick\n");
1908    }
1909
1910    #[gpui::test]
1911    async fn test_v2ap(cx: &mut gpui::TestAppContext) {
1912        let mut cx = NeovimBackedTestContext::new(cx).await;
1913
1914        cx.set_shared_state(indoc! {
1915            "The
1916            quicˇk
1917
1918            brown
1919            fox"
1920        })
1921        .await;
1922        cx.simulate_shared_keystrokes("v 2 a p").await;
1923        cx.shared_state().await.assert_eq(indoc! {
1924            "«The
1925            quick
1926
1927            brown
1928            fˇ»ox"
1929        });
1930    }
1931
1932    #[gpui::test]
1933    async fn test_visual_syntax_sibling_selection(cx: &mut gpui::TestAppContext) {
1934        let mut cx = VimTestContext::new(cx, true).await;
1935
1936        cx.set_state(
1937            indoc! {"
1938                fn test() {
1939                    let ˇa = 1;
1940                    let b = 2;
1941                    let c = 3;
1942                }
1943            "},
1944            Mode::Normal,
1945        );
1946
1947        // Enter visual mode and select the statement
1948        cx.simulate_keystrokes("v w w w");
1949        cx.assert_state(
1950            indoc! {"
1951                fn test() {
1952                    let «a = 1;ˇ»
1953                    let b = 2;
1954                    let c = 3;
1955                }
1956            "},
1957            Mode::Visual,
1958        );
1959
1960        // The specific behavior of syntax sibling selection in vim mode
1961        // would depend on the key bindings configured, but the actions
1962        // are now available for use
1963    }
1964}