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