visual.rs

   1use std::sync::Arc;
   2
   3use collections::HashMap;
   4use editor::{
   5    Bias, DisplayPoint, Editor, ToOffset,
   6    display_map::{DisplaySnapshot, ToDisplayPoint},
   7    movement,
   8    scroll::Autoscroll,
   9};
  10use gpui::{Context, Window, actions};
  11use language::{Point, Selection, SelectionGoal};
  12use multi_buffer::MultiBufferRow;
  13use search::BufferSearchBar;
  14use util::ResultExt;
  15use workspace::searchable::Direction;
  16
  17use crate::{
  18    Vim,
  19    motion::{Motion, MotionKind, first_non_whitespace, next_line_end, start_of_line},
  20    object::Object,
  21    state::{Mark, Mode, Operator},
  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            editor.selections.line_mode = false;
 507
 508            editor.transact(window, cx, |editor, window, cx| {
 509                editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 510                    s.move_with(|map, selection| {
 511                        if line_mode {
 512                            let mut position = selection.head();
 513                            if !selection.reversed {
 514                                position = movement::left(map, position);
 515                            }
 516                            original_columns.insert(selection.id, position.to_point(map).column);
 517                            if vim.mode == Mode::VisualBlock {
 518                                *selection.end.column_mut() = map.line_len(selection.end.row())
 519                            } else {
 520                                let start = selection.start.to_point(map);
 521                                let end = selection.end.to_point(map);
 522                                selection.start = map.prev_line_boundary(start).1;
 523                                if end.column == 0 && end > start {
 524                                    let row = end.row.saturating_sub(1);
 525                                    selection.end = Point::new(
 526                                        row,
 527                                        map.buffer_snapshot.line_len(MultiBufferRow(row)),
 528                                    )
 529                                    .to_display_point(map)
 530                                } else {
 531                                    selection.end = map.next_line_boundary(end).1;
 532                                }
 533                            }
 534                        }
 535                        selection.goal = SelectionGoal::None;
 536                    });
 537                });
 538                let kind = if line_mode {
 539                    MotionKind::Linewise
 540                } else {
 541                    MotionKind::Exclusive
 542                };
 543                vim.copy_selections_content(editor, kind, window, cx);
 544
 545                if line_mode && vim.mode != Mode::VisualBlock {
 546                    editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 547                        s.move_with(|map, selection| {
 548                            let end = selection.end.to_point(map);
 549                            let start = selection.start.to_point(map);
 550                            if end.row < map.buffer_snapshot.max_point().row {
 551                                selection.end = Point::new(end.row + 1, 0).to_display_point(map)
 552                            } else if start.row > 0 {
 553                                selection.start = Point::new(
 554                                    start.row - 1,
 555                                    map.buffer_snapshot.line_len(MultiBufferRow(start.row - 1)),
 556                                )
 557                                .to_display_point(map)
 558                            }
 559                        });
 560                    });
 561                }
 562                editor.insert("", window, cx);
 563
 564                // Fixup cursor position after the deletion
 565                editor.set_clip_at_line_ends(true, cx);
 566                editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 567                    s.move_with(|map, selection| {
 568                        let mut cursor = selection.head().to_point(map);
 569
 570                        if let Some(column) = original_columns.get(&selection.id) {
 571                            cursor.column = *column
 572                        }
 573                        let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left);
 574                        selection.collapse_to(cursor, selection.goal)
 575                    });
 576                    if vim.mode == Mode::VisualBlock {
 577                        s.select_anchors(vec![s.first_anchor()])
 578                    }
 579                });
 580            })
 581        });
 582        self.switch_mode(Mode::Normal, true, window, cx);
 583    }
 584
 585    pub fn visual_yank(&mut self, line_mode: bool, window: &mut Window, cx: &mut Context<Self>) {
 586        self.store_visual_marks(window, cx);
 587        self.update_editor(window, cx, |vim, editor, window, cx| {
 588            let line_mode = line_mode || editor.selections.line_mode;
 589            editor.selections.line_mode = line_mode;
 590            let kind = if line_mode {
 591                MotionKind::Linewise
 592            } else {
 593                MotionKind::Exclusive
 594            };
 595            vim.yank_selections_content(editor, kind, window, cx);
 596            editor.change_selections(None, window, cx, |s| {
 597                s.move_with(|map, selection| {
 598                    if line_mode {
 599                        selection.start = start_of_line(map, false, selection.start);
 600                    };
 601                    selection.collapse_to(selection.start, SelectionGoal::None)
 602                });
 603                if vim.mode == Mode::VisualBlock {
 604                    s.select_anchors(vec![s.first_anchor()])
 605                }
 606            });
 607        });
 608        self.switch_mode(Mode::Normal, true, window, cx);
 609    }
 610
 611    pub(crate) fn visual_replace(
 612        &mut self,
 613        text: Arc<str>,
 614        window: &mut Window,
 615        cx: &mut Context<Self>,
 616    ) {
 617        self.stop_recording(cx);
 618        self.update_editor(window, cx, |_, editor, window, cx| {
 619            editor.transact(window, cx, |editor, window, cx| {
 620                let (display_map, selections) = editor.selections.all_adjusted_display(cx);
 621
 622                // Selections are biased right at the start. So we need to store
 623                // anchors that are biased left so that we can restore the selections
 624                // after the change
 625                let stable_anchors = editor
 626                    .selections
 627                    .disjoint_anchors()
 628                    .iter()
 629                    .map(|selection| {
 630                        let start = selection.start.bias_left(&display_map.buffer_snapshot);
 631                        start..start
 632                    })
 633                    .collect::<Vec<_>>();
 634
 635                let mut edits = Vec::new();
 636                for selection in selections.iter() {
 637                    let selection = selection.clone();
 638                    for row_range in
 639                        movement::split_display_range_by_lines(&display_map, selection.range())
 640                    {
 641                        let range = row_range.start.to_offset(&display_map, Bias::Right)
 642                            ..row_range.end.to_offset(&display_map, Bias::Right);
 643                        let text = text.repeat(range.len());
 644                        edits.push((range, text));
 645                    }
 646                }
 647
 648                editor.edit(edits, cx);
 649                editor.change_selections(None, window, cx, |s| s.select_ranges(stable_anchors));
 650            });
 651        });
 652        self.switch_mode(Mode::Normal, false, window, cx);
 653    }
 654
 655    pub fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
 656        let count =
 657            Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 });
 658        self.update_editor(window, cx, |_, editor, window, cx| {
 659            editor.set_clip_at_line_ends(false, cx);
 660            for _ in 0..count {
 661                if editor
 662                    .select_next(&Default::default(), window, cx)
 663                    .log_err()
 664                    .is_none()
 665                {
 666                    break;
 667                }
 668            }
 669        });
 670    }
 671
 672    pub fn select_previous(
 673        &mut self,
 674        _: &SelectPrevious,
 675        window: &mut Window,
 676        cx: &mut Context<Self>,
 677    ) {
 678        let count =
 679            Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 });
 680        self.update_editor(window, cx, |_, editor, window, cx| {
 681            for _ in 0..count {
 682                if editor
 683                    .select_previous(&Default::default(), window, cx)
 684                    .log_err()
 685                    .is_none()
 686                {
 687                    break;
 688                }
 689            }
 690        });
 691    }
 692
 693    pub fn select_match(
 694        &mut self,
 695        direction: Direction,
 696        window: &mut Window,
 697        cx: &mut Context<Self>,
 698    ) {
 699        let count = Vim::take_count(cx).unwrap_or(1);
 700        let Some(pane) = self.pane(window, cx) else {
 701            return;
 702        };
 703        let vim_is_normal = self.mode == Mode::Normal;
 704        let mut start_selection = 0usize;
 705        let mut end_selection = 0usize;
 706
 707        self.update_editor(window, cx, |_, editor, _, _| {
 708            editor.set_collapse_matches(false);
 709        });
 710        if vim_is_normal {
 711            pane.update(cx, |pane, cx| {
 712                if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()
 713                {
 714                    search_bar.update(cx, |search_bar, cx| {
 715                        if !search_bar.has_active_match() || !search_bar.show(window, cx) {
 716                            return;
 717                        }
 718                        // without update_match_index there is a bug when the cursor is before the first match
 719                        search_bar.update_match_index(window, cx);
 720                        search_bar.select_match(direction.opposite(), 1, window, cx);
 721                    });
 722                }
 723            });
 724        }
 725        self.update_editor(window, cx, |_, editor, _, cx| {
 726            let latest = editor.selections.newest::<usize>(cx);
 727            start_selection = latest.start;
 728            end_selection = latest.end;
 729        });
 730
 731        let mut match_exists = false;
 732        pane.update(cx, |pane, cx| {
 733            if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 734                search_bar.update(cx, |search_bar, cx| {
 735                    search_bar.update_match_index(window, cx);
 736                    search_bar.select_match(direction, count, window, cx);
 737                    match_exists = search_bar.match_exists(window, cx);
 738                });
 739            }
 740        });
 741        if !match_exists {
 742            self.clear_operator(window, cx);
 743            self.stop_replaying(cx);
 744            return;
 745        }
 746        self.update_editor(window, cx, |_, editor, window, cx| {
 747            let latest = editor.selections.newest::<usize>(cx);
 748            if vim_is_normal {
 749                start_selection = latest.start;
 750                end_selection = latest.end;
 751            } else {
 752                start_selection = start_selection.min(latest.start);
 753                end_selection = end_selection.max(latest.end);
 754            }
 755            if direction == Direction::Prev {
 756                std::mem::swap(&mut start_selection, &mut end_selection);
 757            }
 758            editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 759                s.select_ranges([start_selection..end_selection]);
 760            });
 761            editor.set_collapse_matches(true);
 762        });
 763
 764        match self.maybe_pop_operator() {
 765            Some(Operator::Change) => self.substitute(None, false, window, cx),
 766            Some(Operator::Delete) => {
 767                self.stop_recording(cx);
 768                self.visual_delete(false, window, cx)
 769            }
 770            Some(Operator::Yank) => self.visual_yank(false, window, cx),
 771            _ => {} // Ignoring other operators
 772        }
 773    }
 774}
 775#[cfg(test)]
 776mod test {
 777    use indoc::indoc;
 778    use workspace::item::Item;
 779
 780    use crate::{
 781        state::Mode,
 782        test::{NeovimBackedTestContext, VimTestContext},
 783    };
 784
 785    #[gpui::test]
 786    async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
 787        let mut cx = NeovimBackedTestContext::new(cx).await;
 788
 789        cx.set_shared_state(indoc! {
 790            "The ˇquick brown
 791            fox jumps over
 792            the lazy dog"
 793        })
 794        .await;
 795        let cursor = cx.update_editor(|editor, _, cx| editor.pixel_position_of_cursor(cx));
 796
 797        // entering visual mode should select the character
 798        // under cursor
 799        cx.simulate_shared_keystrokes("v").await;
 800        cx.shared_state()
 801            .await
 802            .assert_eq(indoc! { "The «qˇ»uick brown
 803            fox jumps over
 804            the lazy dog"});
 805        cx.update_editor(|editor, _, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
 806
 807        // forwards motions should extend the selection
 808        cx.simulate_shared_keystrokes("w j").await;
 809        cx.shared_state().await.assert_eq(indoc! { "The «quick brown
 810            fox jumps oˇ»ver
 811            the lazy dog"});
 812
 813        cx.simulate_shared_keystrokes("escape").await;
 814        cx.shared_state().await.assert_eq(indoc! { "The quick brown
 815            fox jumps ˇover
 816            the lazy dog"});
 817
 818        // motions work backwards
 819        cx.simulate_shared_keystrokes("v k b").await;
 820        cx.shared_state()
 821            .await
 822            .assert_eq(indoc! { "The «ˇquick brown
 823            fox jumps o»ver
 824            the lazy dog"});
 825
 826        // works on empty lines
 827        cx.set_shared_state(indoc! {"
 828            a
 829            ˇ
 830            b
 831            "})
 832            .await;
 833        let cursor = cx.update_editor(|editor, _, cx| editor.pixel_position_of_cursor(cx));
 834        cx.simulate_shared_keystrokes("v").await;
 835        cx.shared_state().await.assert_eq(indoc! {"
 836            a
 837            «
 838            ˇ»b
 839        "});
 840        cx.update_editor(|editor, _, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
 841
 842        // toggles off again
 843        cx.simulate_shared_keystrokes("v").await;
 844        cx.shared_state().await.assert_eq(indoc! {"
 845            a
 846            ˇ
 847            b
 848            "});
 849
 850        // works at the end of a document
 851        cx.set_shared_state(indoc! {"
 852            a
 853            b
 854            ˇ"})
 855            .await;
 856
 857        cx.simulate_shared_keystrokes("v").await;
 858        cx.shared_state().await.assert_eq(indoc! {"
 859            a
 860            b
 861            ˇ"});
 862    }
 863
 864    #[gpui::test]
 865    async fn test_visual_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
 866        let mut cx = VimTestContext::new(cx, true).await;
 867
 868        cx.set_state(
 869            indoc! {
 870                "«The quick brown
 871                fox jumps over
 872                the lazy dogˇ»"
 873            },
 874            Mode::Visual,
 875        );
 876        cx.simulate_keystrokes("g shift-i");
 877        cx.assert_state(
 878            indoc! {
 879                "ˇThe quick brown
 880                ˇfox jumps over
 881                ˇthe lazy dog"
 882            },
 883            Mode::Insert,
 884        );
 885    }
 886
 887    #[gpui::test]
 888    async fn test_visual_insert_end_of_line(cx: &mut gpui::TestAppContext) {
 889        let mut cx = VimTestContext::new(cx, true).await;
 890
 891        cx.set_state(
 892            indoc! {
 893                "«The quick brown
 894                fox jumps over
 895                the lazy dogˇ»"
 896            },
 897            Mode::Visual,
 898        );
 899        cx.simulate_keystrokes("g shift-a");
 900        cx.assert_state(
 901            indoc! {
 902                "The quick brownˇ
 903                fox jumps overˇ
 904                the lazy dogˇ"
 905            },
 906            Mode::Insert,
 907        );
 908    }
 909
 910    #[gpui::test]
 911    async fn test_enter_visual_line_mode(cx: &mut gpui::TestAppContext) {
 912        let mut cx = NeovimBackedTestContext::new(cx).await;
 913
 914        cx.set_shared_state(indoc! {
 915            "The ˇquick brown
 916            fox jumps over
 917            the lazy dog"
 918        })
 919        .await;
 920        cx.simulate_shared_keystrokes("shift-v").await;
 921        cx.shared_state()
 922            .await
 923            .assert_eq(indoc! { "The «qˇ»uick brown
 924            fox jumps over
 925            the lazy dog"});
 926        cx.simulate_shared_keystrokes("x").await;
 927        cx.shared_state().await.assert_eq(indoc! { "fox ˇjumps over
 928        the lazy dog"});
 929
 930        // it should work on empty lines
 931        cx.set_shared_state(indoc! {"
 932            a
 933            ˇ
 934            b"})
 935            .await;
 936        cx.simulate_shared_keystrokes("shift-v").await;
 937        cx.shared_state().await.assert_eq(indoc! {"
 938            a
 939            «
 940            ˇ»b"});
 941        cx.simulate_shared_keystrokes("x").await;
 942        cx.shared_state().await.assert_eq(indoc! {"
 943            a
 944            ˇb"});
 945
 946        // it should work at the end of the document
 947        cx.set_shared_state(indoc! {"
 948            a
 949            b
 950            ˇ"})
 951            .await;
 952        let cursor = cx.update_editor(|editor, _, cx| editor.pixel_position_of_cursor(cx));
 953        cx.simulate_shared_keystrokes("shift-v").await;
 954        cx.shared_state().await.assert_eq(indoc! {"
 955            a
 956            b
 957            ˇ"});
 958        cx.update_editor(|editor, _, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
 959        cx.simulate_shared_keystrokes("x").await;
 960        cx.shared_state().await.assert_eq(indoc! {"
 961            a
 962            ˇb"});
 963    }
 964
 965    #[gpui::test]
 966    async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
 967        let mut cx = NeovimBackedTestContext::new(cx).await;
 968
 969        cx.simulate("v w", "The quick ˇbrown")
 970            .await
 971            .assert_matches();
 972
 973        cx.simulate("v w x", "The quick ˇbrown")
 974            .await
 975            .assert_matches();
 976        cx.simulate(
 977            "v w j x",
 978            indoc! {"
 979                The ˇquick brown
 980                fox jumps over
 981                the lazy dog"},
 982        )
 983        .await
 984        .assert_matches();
 985        // Test pasting code copied on delete
 986        cx.simulate_shared_keystrokes("j p").await;
 987        cx.shared_state().await.assert_matches();
 988
 989        cx.simulate_at_each_offset(
 990            "v w j x",
 991            indoc! {"
 992                The ˇquick brown
 993                fox jumps over
 994                the ˇlazy dog"},
 995        )
 996        .await
 997        .assert_matches();
 998        cx.simulate_at_each_offset(
 999            "v b k x",
1000            indoc! {"
1001                The ˇquick brown
1002                fox jumps ˇover
1003                the ˇlazy dog"},
1004        )
1005        .await
1006        .assert_matches();
1007    }
1008
1009    #[gpui::test]
1010    async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
1011        let mut cx = NeovimBackedTestContext::new(cx).await;
1012
1013        cx.set_shared_state(indoc! {"
1014                The quˇick brown
1015                fox jumps over
1016                the lazy dog"})
1017            .await;
1018        cx.simulate_shared_keystrokes("shift-v x").await;
1019        cx.shared_state().await.assert_matches();
1020
1021        // Test pasting code copied on delete
1022        cx.simulate_shared_keystrokes("p").await;
1023        cx.shared_state().await.assert_matches();
1024
1025        cx.set_shared_state(indoc! {"
1026                The quick brown
1027                fox jumps over
1028                the laˇzy dog"})
1029            .await;
1030        cx.simulate_shared_keystrokes("shift-v x").await;
1031        cx.shared_state().await.assert_matches();
1032        cx.shared_clipboard().await.assert_eq("the lazy dog\n");
1033
1034        cx.set_shared_state(indoc! {"
1035                                The quˇick brown
1036                                fox jumps over
1037                                the lazy dog"})
1038            .await;
1039        cx.simulate_shared_keystrokes("shift-v j x").await;
1040        cx.shared_state().await.assert_matches();
1041        // Test pasting code copied on delete
1042        cx.simulate_shared_keystrokes("p").await;
1043        cx.shared_state().await.assert_matches();
1044
1045        cx.set_shared_state(indoc! {"
1046            The ˇlong line
1047            should not
1048            crash
1049            "})
1050            .await;
1051        cx.simulate_shared_keystrokes("shift-v $ x").await;
1052        cx.shared_state().await.assert_matches();
1053    }
1054
1055    #[gpui::test]
1056    async fn test_visual_yank(cx: &mut gpui::TestAppContext) {
1057        let mut cx = NeovimBackedTestContext::new(cx).await;
1058
1059        cx.set_shared_state("The quick ˇbrown").await;
1060        cx.simulate_shared_keystrokes("v w y").await;
1061        cx.shared_state().await.assert_eq("The quick ˇbrown");
1062        cx.shared_clipboard().await.assert_eq("brown");
1063
1064        cx.set_shared_state(indoc! {"
1065                The ˇquick brown
1066                fox jumps over
1067                the lazy dog"})
1068            .await;
1069        cx.simulate_shared_keystrokes("v w j y").await;
1070        cx.shared_state().await.assert_eq(indoc! {"
1071                    The ˇquick brown
1072                    fox jumps over
1073                    the lazy dog"});
1074        cx.shared_clipboard().await.assert_eq(indoc! {"
1075                quick brown
1076                fox jumps o"});
1077
1078        cx.set_shared_state(indoc! {"
1079                    The quick brown
1080                    fox jumps over
1081                    the ˇlazy dog"})
1082            .await;
1083        cx.simulate_shared_keystrokes("v w j y").await;
1084        cx.shared_state().await.assert_eq(indoc! {"
1085                    The quick brown
1086                    fox jumps over
1087                    the ˇlazy dog"});
1088        cx.shared_clipboard().await.assert_eq("lazy d");
1089        cx.simulate_shared_keystrokes("shift-v y").await;
1090        cx.shared_clipboard().await.assert_eq("the lazy dog\n");
1091
1092        cx.set_shared_state(indoc! {"
1093                    The ˇquick brown
1094                    fox jumps over
1095                    the lazy dog"})
1096            .await;
1097        cx.simulate_shared_keystrokes("v b k y").await;
1098        cx.shared_state().await.assert_eq(indoc! {"
1099                    ˇThe quick brown
1100                    fox jumps over
1101                    the lazy dog"});
1102        assert_eq!(
1103            cx.read_from_clipboard()
1104                .map(|item| item.text().unwrap().to_string())
1105                .unwrap(),
1106            "The q"
1107        );
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("shift-v shift-g shift-y")
1115            .await;
1116        cx.shared_state().await.assert_eq(indoc! {"
1117                    The quick brown
1118                    ˇfox jumps over
1119                    the lazy dog"});
1120        cx.shared_clipboard()
1121            .await
1122            .assert_eq("fox jumps over\nthe lazy dog\n");
1123    }
1124
1125    #[gpui::test]
1126    async fn test_visual_block_mode(cx: &mut gpui::TestAppContext) {
1127        let mut cx = NeovimBackedTestContext::new(cx).await;
1128
1129        cx.set_shared_state(indoc! {
1130            "The ˇquick brown
1131             fox jumps over
1132             the lazy dog"
1133        })
1134        .await;
1135        cx.simulate_shared_keystrokes("ctrl-v").await;
1136        cx.shared_state().await.assert_eq(indoc! {
1137            "The «qˇ»uick brown
1138            fox jumps over
1139            the lazy dog"
1140        });
1141        cx.simulate_shared_keystrokes("2 down").await;
1142        cx.shared_state().await.assert_eq(indoc! {
1143            "The «qˇ»uick brown
1144            fox «jˇ»umps over
1145            the «lˇ»azy dog"
1146        });
1147        cx.simulate_shared_keystrokes("e").await;
1148        cx.shared_state().await.assert_eq(indoc! {
1149            "The «quicˇ»k brown
1150            fox «jumpˇ»s over
1151            the «lazyˇ» dog"
1152        });
1153        cx.simulate_shared_keystrokes("^").await;
1154        cx.shared_state().await.assert_eq(indoc! {
1155            "«ˇThe q»uick brown
1156            «ˇfox j»umps over
1157            «ˇthe l»azy dog"
1158        });
1159        cx.simulate_shared_keystrokes("$").await;
1160        cx.shared_state().await.assert_eq(indoc! {
1161            "The «quick brownˇ»
1162            fox «jumps overˇ»
1163            the «lazy dogˇ»"
1164        });
1165        cx.simulate_shared_keystrokes("shift-f space").await;
1166        cx.shared_state().await.assert_eq(indoc! {
1167            "The «quickˇ» brown
1168            fox «jumpsˇ» over
1169            the «lazy ˇ»dog"
1170        });
1171
1172        // toggling through visual mode works as expected
1173        cx.simulate_shared_keystrokes("v").await;
1174        cx.shared_state().await.assert_eq(indoc! {
1175            "The «quick brown
1176            fox jumps over
1177            the lazy ˇ»dog"
1178        });
1179        cx.simulate_shared_keystrokes("ctrl-v").await;
1180        cx.shared_state().await.assert_eq(indoc! {
1181            "The «quickˇ» brown
1182            fox «jumpsˇ» over
1183            the «lazy ˇ»dog"
1184        });
1185
1186        cx.set_shared_state(indoc! {
1187            "The ˇquick
1188             brown
1189             fox
1190             jumps over the
1191
1192             lazy dog
1193            "
1194        })
1195        .await;
1196        cx.simulate_shared_keystrokes("ctrl-v down down").await;
1197        cx.shared_state().await.assert_eq(indoc! {
1198            "The«ˇ q»uick
1199            bro«ˇwn»
1200            foxˇ
1201            jumps over the
1202
1203            lazy dog
1204            "
1205        });
1206        cx.simulate_shared_keystrokes("down").await;
1207        cx.shared_state().await.assert_eq(indoc! {
1208            "The «qˇ»uick
1209            brow«nˇ»
1210            fox
1211            jump«sˇ» over the
1212
1213            lazy dog
1214            "
1215        });
1216        cx.simulate_shared_keystrokes("left").await;
1217        cx.shared_state().await.assert_eq(indoc! {
1218            "The«ˇ q»uick
1219            bro«ˇwn»
1220            foxˇ
1221            jum«ˇps» over the
1222
1223            lazy dog
1224            "
1225        });
1226        cx.simulate_shared_keystrokes("s o escape").await;
1227        cx.shared_state().await.assert_eq(indoc! {
1228            "Theˇouick
1229            broo
1230            foxo
1231            jumo over the
1232
1233            lazy dog
1234            "
1235        });
1236
1237        // https://github.com/zed-industries/zed/issues/6274
1238        cx.set_shared_state(indoc! {
1239            "Theˇ quick brown
1240
1241            fox jumps over
1242            the lazy dog
1243            "
1244        })
1245        .await;
1246        cx.simulate_shared_keystrokes("l ctrl-v j j").await;
1247        cx.shared_state().await.assert_eq(indoc! {
1248            "The «qˇ»uick brown
1249
1250            fox «jˇ»umps over
1251            the lazy dog
1252            "
1253        });
1254    }
1255
1256    #[gpui::test]
1257    async fn test_visual_block_issue_2123(cx: &mut gpui::TestAppContext) {
1258        let mut cx = NeovimBackedTestContext::new(cx).await;
1259
1260        cx.set_shared_state(indoc! {
1261            "The ˇquick brown
1262            fox jumps over
1263            the lazy dog
1264            "
1265        })
1266        .await;
1267        cx.simulate_shared_keystrokes("ctrl-v right down").await;
1268        cx.shared_state().await.assert_eq(indoc! {
1269            "The «quˇ»ick brown
1270            fox «juˇ»mps over
1271            the lazy dog
1272            "
1273        });
1274    }
1275    #[gpui::test]
1276    async fn test_visual_block_mode_down_right(cx: &mut gpui::TestAppContext) {
1277        let mut cx = NeovimBackedTestContext::new(cx).await;
1278        cx.set_shared_state(indoc! {"
1279            The ˇquick brown
1280            fox jumps over
1281            the lazy dog"})
1282            .await;
1283        cx.simulate_shared_keystrokes("ctrl-v l l l l l j").await;
1284        cx.shared_state().await.assert_eq(indoc! {"
1285            The «quick ˇ»brown
1286            fox «jumps ˇ»over
1287            the lazy dog"});
1288    }
1289
1290    #[gpui::test]
1291    async fn test_visual_block_mode_up_left(cx: &mut gpui::TestAppContext) {
1292        let mut cx = NeovimBackedTestContext::new(cx).await;
1293        cx.set_shared_state(indoc! {"
1294            The quick brown
1295            fox jumpsˇ over
1296            the lazy dog"})
1297            .await;
1298        cx.simulate_shared_keystrokes("ctrl-v h h h h h k").await;
1299        cx.shared_state().await.assert_eq(indoc! {"
1300            The «ˇquick »brown
1301            fox «ˇjumps »over
1302            the lazy dog"});
1303    }
1304
1305    #[gpui::test]
1306    async fn test_visual_block_mode_other_end(cx: &mut gpui::TestAppContext) {
1307        let mut cx = NeovimBackedTestContext::new(cx).await;
1308        cx.set_shared_state(indoc! {"
1309            The quick brown
1310            fox jˇumps over
1311            the lazy dog"})
1312            .await;
1313        cx.simulate_shared_keystrokes("ctrl-v l l l l j").await;
1314        cx.shared_state().await.assert_eq(indoc! {"
1315            The quick brown
1316            fox j«umps ˇ»over
1317            the l«azy dˇ»og"});
1318        cx.simulate_shared_keystrokes("o k").await;
1319        cx.shared_state().await.assert_eq(indoc! {"
1320            The q«ˇuick »brown
1321            fox j«ˇumps »over
1322            the l«ˇazy d»og"});
1323    }
1324
1325    #[gpui::test]
1326    async fn test_visual_block_mode_shift_other_end(cx: &mut gpui::TestAppContext) {
1327        let mut cx = NeovimBackedTestContext::new(cx).await;
1328        cx.set_shared_state(indoc! {"
1329            The quick brown
1330            fox jˇumps over
1331            the lazy dog"})
1332            .await;
1333        cx.simulate_shared_keystrokes("ctrl-v l l l l j").await;
1334        cx.shared_state().await.assert_eq(indoc! {"
1335            The quick brown
1336            fox j«umps ˇ»over
1337            the l«azy dˇ»og"});
1338        cx.simulate_shared_keystrokes("shift-o k").await;
1339        cx.shared_state().await.assert_eq(indoc! {"
1340            The quick brown
1341            fox j«ˇumps »over
1342            the lazy dog"});
1343    }
1344
1345    #[gpui::test]
1346    async fn test_visual_block_insert(cx: &mut gpui::TestAppContext) {
1347        let mut cx = NeovimBackedTestContext::new(cx).await;
1348
1349        cx.set_shared_state(indoc! {
1350            "ˇThe quick brown
1351            fox jumps over
1352            the lazy dog
1353            "
1354        })
1355        .await;
1356        cx.simulate_shared_keystrokes("ctrl-v 9 down").await;
1357        cx.shared_state().await.assert_eq(indoc! {
1358            "«Tˇ»he quick brown
1359            «fˇ»ox jumps over
1360            «tˇ»he lazy dog
1361            ˇ"
1362        });
1363
1364        cx.simulate_shared_keystrokes("shift-i k escape").await;
1365        cx.shared_state().await.assert_eq(indoc! {
1366            "ˇkThe quick brown
1367            kfox jumps over
1368            kthe lazy dog
1369            k"
1370        });
1371
1372        cx.set_shared_state(indoc! {
1373            "ˇThe quick brown
1374            fox jumps over
1375            the lazy dog
1376            "
1377        })
1378        .await;
1379        cx.simulate_shared_keystrokes("ctrl-v 9 down").await;
1380        cx.shared_state().await.assert_eq(indoc! {
1381            "«Tˇ»he quick brown
1382            «fˇ»ox jumps over
1383            «tˇ»he lazy dog
1384            ˇ"
1385        });
1386        cx.simulate_shared_keystrokes("c k escape").await;
1387        cx.shared_state().await.assert_eq(indoc! {
1388            "ˇkhe quick brown
1389            kox jumps over
1390            khe lazy dog
1391            k"
1392        });
1393    }
1394
1395    #[gpui::test]
1396    async fn test_visual_object(cx: &mut gpui::TestAppContext) {
1397        let mut cx = NeovimBackedTestContext::new(cx).await;
1398
1399        cx.set_shared_state("hello (in [parˇens] o)").await;
1400        cx.simulate_shared_keystrokes("ctrl-v l").await;
1401        cx.simulate_shared_keystrokes("a ]").await;
1402        cx.shared_state()
1403            .await
1404            .assert_eq("hello (in «[parens]ˇ» o)");
1405        cx.simulate_shared_keystrokes("i (").await;
1406        cx.shared_state()
1407            .await
1408            .assert_eq("hello («in [parens] oˇ»)");
1409
1410        cx.set_shared_state("hello in a wˇord again.").await;
1411        cx.simulate_shared_keystrokes("ctrl-v l i w").await;
1412        cx.shared_state()
1413            .await
1414            .assert_eq("hello in a w«ordˇ» again.");
1415        assert_eq!(cx.mode(), Mode::VisualBlock);
1416        cx.simulate_shared_keystrokes("o a s").await;
1417        cx.shared_state()
1418            .await
1419            .assert_eq("«ˇhello in a word» again.");
1420    }
1421
1422    #[gpui::test]
1423    async fn test_mode_across_command(cx: &mut gpui::TestAppContext) {
1424        let mut cx = VimTestContext::new(cx, true).await;
1425
1426        cx.set_state("aˇbc", Mode::Normal);
1427        cx.simulate_keystrokes("ctrl-v");
1428        assert_eq!(cx.mode(), Mode::VisualBlock);
1429        cx.simulate_keystrokes("cmd-shift-p escape");
1430        assert_eq!(cx.mode(), Mode::VisualBlock);
1431    }
1432
1433    #[gpui::test]
1434    async fn test_gn(cx: &mut gpui::TestAppContext) {
1435        let mut cx = NeovimBackedTestContext::new(cx).await;
1436
1437        cx.set_shared_state("aaˇ aa aa aa aa").await;
1438        cx.simulate_shared_keystrokes("/ a a enter").await;
1439        cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1440        cx.simulate_shared_keystrokes("g n").await;
1441        cx.shared_state().await.assert_eq("aa «aaˇ» aa aa aa");
1442        cx.simulate_shared_keystrokes("g n").await;
1443        cx.shared_state().await.assert_eq("aa «aa aaˇ» aa aa");
1444        cx.simulate_shared_keystrokes("escape d g n").await;
1445        cx.shared_state().await.assert_eq("aa aa ˇ aa aa");
1446
1447        cx.set_shared_state("aaˇ aa aa aa aa").await;
1448        cx.simulate_shared_keystrokes("/ a a enter").await;
1449        cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1450        cx.simulate_shared_keystrokes("3 g n").await;
1451        cx.shared_state().await.assert_eq("aa aa aa «aaˇ» aa");
1452
1453        cx.set_shared_state("aaˇ aa aa aa aa").await;
1454        cx.simulate_shared_keystrokes("/ a a enter").await;
1455        cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1456        cx.simulate_shared_keystrokes("g shift-n").await;
1457        cx.shared_state().await.assert_eq("aa «ˇaa» aa aa aa");
1458        cx.simulate_shared_keystrokes("g shift-n").await;
1459        cx.shared_state().await.assert_eq("«ˇaa aa» aa aa aa");
1460    }
1461
1462    #[gpui::test]
1463    async fn test_gl(cx: &mut gpui::TestAppContext) {
1464        let mut cx = VimTestContext::new(cx, true).await;
1465
1466        cx.set_state("aaˇ aa\naa", Mode::Normal);
1467        cx.simulate_keystrokes("g l");
1468        cx.assert_state("«aaˇ» «aaˇ»\naa", Mode::Visual);
1469        cx.simulate_keystrokes("g >");
1470        cx.assert_state("«aaˇ» aa\n«aaˇ»", Mode::Visual);
1471    }
1472
1473    #[gpui::test]
1474    async fn test_dgn_repeat(cx: &mut gpui::TestAppContext) {
1475        let mut cx = NeovimBackedTestContext::new(cx).await;
1476
1477        cx.set_shared_state("aaˇ aa aa aa aa").await;
1478        cx.simulate_shared_keystrokes("/ a a enter").await;
1479        cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1480        cx.simulate_shared_keystrokes("d g n").await;
1481
1482        cx.shared_state().await.assert_eq("aa ˇ aa aa aa");
1483        cx.simulate_shared_keystrokes(".").await;
1484        cx.shared_state().await.assert_eq("aa  ˇ aa aa");
1485        cx.simulate_shared_keystrokes(".").await;
1486        cx.shared_state().await.assert_eq("aa   ˇ aa");
1487    }
1488
1489    #[gpui::test]
1490    async fn test_cgn_repeat(cx: &mut gpui::TestAppContext) {
1491        let mut cx = NeovimBackedTestContext::new(cx).await;
1492
1493        cx.set_shared_state("aaˇ aa aa aa aa").await;
1494        cx.simulate_shared_keystrokes("/ a a enter").await;
1495        cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1496        cx.simulate_shared_keystrokes("c g n x escape").await;
1497        cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1498        cx.simulate_shared_keystrokes(".").await;
1499        cx.shared_state().await.assert_eq("aa x ˇx aa aa");
1500    }
1501
1502    #[gpui::test]
1503    async fn test_cgn_nomatch(cx: &mut gpui::TestAppContext) {
1504        let mut cx = NeovimBackedTestContext::new(cx).await;
1505
1506        cx.set_shared_state("aaˇ aa aa aa aa").await;
1507        cx.simulate_shared_keystrokes("/ b b enter").await;
1508        cx.shared_state().await.assert_eq("aaˇ aa aa aa aa");
1509        cx.simulate_shared_keystrokes("c g n x escape").await;
1510        cx.shared_state().await.assert_eq("aaˇaa aa aa aa");
1511        cx.simulate_shared_keystrokes(".").await;
1512        cx.shared_state().await.assert_eq("aaˇa aa aa aa");
1513
1514        cx.set_shared_state("aaˇ bb aa aa aa").await;
1515        cx.simulate_shared_keystrokes("/ b b enter").await;
1516        cx.shared_state().await.assert_eq("aa ˇbb aa aa aa");
1517        cx.simulate_shared_keystrokes("c g n x escape").await;
1518        cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1519        cx.simulate_shared_keystrokes(".").await;
1520        cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1521    }
1522
1523    #[gpui::test]
1524    async fn test_visual_shift_d(cx: &mut gpui::TestAppContext) {
1525        let mut cx = NeovimBackedTestContext::new(cx).await;
1526
1527        cx.set_shared_state(indoc! {
1528            "The ˇquick brown
1529            fox jumps over
1530            the lazy dog
1531            "
1532        })
1533        .await;
1534        cx.simulate_shared_keystrokes("v down shift-d").await;
1535        cx.shared_state().await.assert_eq(indoc! {
1536            "the ˇlazy dog\n"
1537        });
1538
1539        cx.set_shared_state(indoc! {
1540            "The ˇquick brown
1541            fox jumps over
1542            the lazy dog
1543            "
1544        })
1545        .await;
1546        cx.simulate_shared_keystrokes("ctrl-v down shift-d").await;
1547        cx.shared_state().await.assert_eq(indoc! {
1548            "Theˇ•
1549            fox•
1550            the lazy dog
1551            "
1552        });
1553    }
1554
1555    #[gpui::test]
1556    async fn test_shift_y(cx: &mut gpui::TestAppContext) {
1557        let mut cx = NeovimBackedTestContext::new(cx).await;
1558
1559        cx.set_shared_state(indoc! {
1560            "The ˇquick brown\n"
1561        })
1562        .await;
1563        cx.simulate_shared_keystrokes("v i w shift-y").await;
1564        cx.shared_clipboard().await.assert_eq(indoc! {
1565            "The quick brown\n"
1566        });
1567    }
1568
1569    #[gpui::test]
1570    async fn test_gv(cx: &mut gpui::TestAppContext) {
1571        let mut cx = NeovimBackedTestContext::new(cx).await;
1572
1573        cx.set_shared_state(indoc! {
1574            "The ˇquick brown"
1575        })
1576        .await;
1577        cx.simulate_shared_keystrokes("v i w escape g v").await;
1578        cx.shared_state().await.assert_eq(indoc! {
1579            "The «quickˇ» brown"
1580        });
1581
1582        cx.simulate_shared_keystrokes("o escape g v").await;
1583        cx.shared_state().await.assert_eq(indoc! {
1584            "The «ˇquick» brown"
1585        });
1586
1587        cx.simulate_shared_keystrokes("escape ^ ctrl-v l").await;
1588        cx.shared_state().await.assert_eq(indoc! {
1589            "«Thˇ»e quick brown"
1590        });
1591        cx.simulate_shared_keystrokes("g v").await;
1592        cx.shared_state().await.assert_eq(indoc! {
1593            "The «ˇquick» brown"
1594        });
1595        cx.simulate_shared_keystrokes("g v").await;
1596        cx.shared_state().await.assert_eq(indoc! {
1597            "«Thˇ»e quick brown"
1598        });
1599
1600        cx.set_state(
1601            indoc! {"
1602            fiˇsh one
1603            fish two
1604            fish red
1605            fish blue
1606        "},
1607            Mode::Normal,
1608        );
1609        cx.simulate_keystrokes("4 g l escape escape g v");
1610        cx.assert_state(
1611            indoc! {"
1612                «fishˇ» one
1613                «fishˇ» two
1614                «fishˇ» red
1615                «fishˇ» blue
1616            "},
1617            Mode::Visual,
1618        );
1619        cx.simulate_keystrokes("y g v");
1620        cx.assert_state(
1621            indoc! {"
1622                «fishˇ» one
1623                «fishˇ» two
1624                «fishˇ» red
1625                «fishˇ» blue
1626            "},
1627            Mode::Visual,
1628        );
1629    }
1630}