visual.rs

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