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