visual.rs

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