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