visual.rs

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