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