visual.rs

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