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