visual.rs

   1use std::sync::Arc;
   2
   3use collections::HashMap;
   4use editor::{
   5    display_map::{DisplaySnapshot, ToDisplayPoint},
   6    movement,
   7    scroll::Autoscroll,
   8    Bias, DisplayPoint, Editor,
   9};
  10use gpui::{actions, ViewContext, WindowContext};
  11use language::{Point, Selection, SelectionGoal};
  12use multi_buffer::MultiBufferRow;
  13use search::BufferSearchBar;
  14use util::ResultExt;
  15use workspace::{searchable::Direction, Workspace};
  16
  17use crate::{
  18    motion::{start_of_line, Motion},
  19    normal::substitute::substitute,
  20    object::Object,
  21    state::{Mode, Operator},
  22    utils::{copy_selections_content, yank_selections_content},
  23    Vim,
  24};
  25
  26actions!(
  27    vim,
  28    [
  29        ToggleVisual,
  30        ToggleVisualLine,
  31        ToggleVisualBlock,
  32        VisualDelete,
  33        VisualDeleteLine,
  34        VisualYank,
  35        OtherEnd,
  36        SelectNext,
  37        SelectPrevious,
  38        SelectNextMatch,
  39        SelectPreviousMatch,
  40    ]
  41);
  42
  43pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
  44    workspace.register_action(|_, _: &ToggleVisual, cx: &mut ViewContext<Workspace>| {
  45        toggle_mode(Mode::Visual, cx)
  46    });
  47    workspace.register_action(|_, _: &ToggleVisualLine, cx: &mut ViewContext<Workspace>| {
  48        toggle_mode(Mode::VisualLine, cx)
  49    });
  50    workspace.register_action(
  51        |_, _: &ToggleVisualBlock, cx: &mut ViewContext<Workspace>| {
  52            toggle_mode(Mode::VisualBlock, cx)
  53        },
  54    );
  55    workspace.register_action(other_end);
  56    workspace.register_action(|_, _: &VisualDelete, cx| {
  57        Vim::update(cx, |vim, cx| {
  58            vim.record_current_action(cx);
  59            delete(vim, false, cx);
  60        });
  61    });
  62    workspace.register_action(|_, _: &VisualDeleteLine, cx| {
  63        Vim::update(cx, |vim, cx| {
  64            vim.record_current_action(cx);
  65            delete(vim, true, cx);
  66        });
  67    });
  68    workspace.register_action(|_, _: &VisualYank, cx| {
  69        Vim::update(cx, |vim, cx| {
  70            yank(vim, cx);
  71        });
  72    });
  73
  74    workspace.register_action(select_next);
  75    workspace.register_action(select_previous);
  76    workspace.register_action(|workspace, _: &SelectNextMatch, cx| {
  77        Vim::update(cx, |vim, cx| {
  78            select_match(workspace, vim, Direction::Next, cx);
  79        });
  80    });
  81    workspace.register_action(|workspace, _: &SelectPreviousMatch, cx| {
  82        Vim::update(cx, |vim, cx| {
  83            select_match(workspace, vim, Direction::Prev, cx);
  84        });
  85    });
  86}
  87
  88pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
  89    Vim::update(cx, |vim, cx| {
  90        vim.update_active_editor(cx, |vim, editor, cx| {
  91            let text_layout_details = editor.text_layout_details(cx);
  92            if vim.state().mode == Mode::VisualBlock
  93                && !matches!(
  94                    motion,
  95                    Motion::EndOfLine {
  96                        display_lines: false
  97                    }
  98                )
  99            {
 100                let is_up_or_down = matches!(motion, Motion::Up { .. } | Motion::Down { .. });
 101                visual_block_motion(is_up_or_down, editor, cx, |map, point, goal| {
 102                    motion.move_point(map, point, goal, times, &text_layout_details)
 103                })
 104            } else {
 105                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 106                    s.move_with(|map, selection| {
 107                        let was_reversed = selection.reversed;
 108                        let mut current_head = selection.head();
 109
 110                        // our motions assume the current character is after the cursor,
 111                        // but in (forward) visual mode the current character is just
 112                        // before the end of the selection.
 113
 114                        // If the file ends with a newline (which is common) we don't do this.
 115                        // so that if you go to the end of such a file you can use "up" to go
 116                        // to the previous line and have it work somewhat as expected.
 117                        #[allow(clippy::nonminimal_bool)]
 118                        if !selection.reversed
 119                            && !selection.is_empty()
 120                            && !(selection.end.column() == 0 && selection.end == map.max_point())
 121                        {
 122                            current_head = movement::left(map, selection.end)
 123                        }
 124
 125                        let Some((new_head, goal)) = motion.move_point(
 126                            map,
 127                            current_head,
 128                            selection.goal,
 129                            times,
 130                            &text_layout_details,
 131                        ) else {
 132                            return;
 133                        };
 134
 135                        selection.set_head(new_head, goal);
 136
 137                        // ensure the current character is included in the selection.
 138                        if !selection.reversed {
 139                            let next_point = if vim.state().mode == Mode::VisualBlock {
 140                                movement::saturating_right(map, selection.end)
 141                            } else {
 142                                movement::right(map, selection.end)
 143                            };
 144
 145                            if !(next_point.column() == 0 && next_point == map.max_point()) {
 146                                selection.end = next_point;
 147                            }
 148                        }
 149
 150                        // vim always ensures the anchor character stays selected.
 151                        // if our selection has reversed, we need to move the opposite end
 152                        // to ensure the anchor is still selected.
 153                        if was_reversed && !selection.reversed {
 154                            selection.start = movement::left(map, selection.start);
 155                        } else if !was_reversed && selection.reversed {
 156                            selection.end = movement::right(map, selection.end);
 157                        }
 158                    })
 159                });
 160            }
 161        });
 162    });
 163}
 164
 165pub fn visual_block_motion(
 166    preserve_goal: bool,
 167    editor: &mut Editor,
 168    cx: &mut ViewContext<Editor>,
 169    mut move_selection: impl FnMut(
 170        &DisplaySnapshot,
 171        DisplayPoint,
 172        SelectionGoal,
 173    ) -> Option<(DisplayPoint, SelectionGoal)>,
 174) {
 175    let text_layout_details = editor.text_layout_details(cx);
 176    editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 177        let map = &s.display_map();
 178        let mut head = s.newest_anchor().head().to_display_point(map);
 179        let mut tail = s.oldest_anchor().tail().to_display_point(map);
 180
 181        let mut head_x = map.x_for_display_point(head, &text_layout_details);
 182        let mut tail_x = map.x_for_display_point(tail, &text_layout_details);
 183
 184        let (start, end) = match s.newest_anchor().goal {
 185            SelectionGoal::HorizontalRange { start, end } if preserve_goal => (start, end),
 186            SelectionGoal::HorizontalPosition(start) if preserve_goal => (start, start),
 187            _ => (tail_x.0, head_x.0),
 188        };
 189        let mut goal = SelectionGoal::HorizontalRange { start, end };
 190
 191        let was_reversed = tail_x > head_x;
 192        if !was_reversed && !preserve_goal {
 193            head = movement::saturating_left(map, head);
 194        }
 195
 196        let Some((new_head, _)) = move_selection(&map, head, goal) else {
 197            return;
 198        };
 199        head = new_head;
 200        head_x = map.x_for_display_point(head, &text_layout_details);
 201
 202        let is_reversed = tail_x > head_x;
 203        if was_reversed && !is_reversed {
 204            tail = movement::saturating_left(map, tail);
 205            tail_x = map.x_for_display_point(tail, &text_layout_details);
 206        } else if !was_reversed && is_reversed {
 207            tail = movement::saturating_right(map, tail);
 208            tail_x = map.x_for_display_point(tail, &text_layout_details);
 209        }
 210        if !is_reversed && !preserve_goal {
 211            head = movement::saturating_right(map, head);
 212            head_x = map.x_for_display_point(head, &text_layout_details);
 213        }
 214
 215        let positions = if is_reversed {
 216            head_x..tail_x
 217        } else {
 218            tail_x..head_x
 219        };
 220
 221        if !preserve_goal {
 222            goal = SelectionGoal::HorizontalRange {
 223                start: positions.start.0,
 224                end: positions.end.0,
 225            };
 226        }
 227
 228        let mut selections = Vec::new();
 229        let mut row = tail.row();
 230
 231        loop {
 232            let laid_out_line = map.layout_row(row, &text_layout_details);
 233            let start = DisplayPoint::new(
 234                row,
 235                laid_out_line.closest_index_for_x(positions.start) as u32,
 236            );
 237            let mut end =
 238                DisplayPoint::new(row, laid_out_line.closest_index_for_x(positions.end) as u32);
 239            if end <= start {
 240                if start.column() == map.line_len(start.row()) {
 241                    end = start;
 242                } else {
 243                    end = movement::saturating_right(map, start);
 244                }
 245            }
 246
 247            if positions.start <= laid_out_line.width {
 248                let selection = Selection {
 249                    id: s.new_selection_id(),
 250                    start: start.to_point(map),
 251                    end: end.to_point(map),
 252                    reversed: is_reversed,
 253                    goal,
 254                };
 255
 256                selections.push(selection);
 257            }
 258            if row == head.row() {
 259                break;
 260            }
 261            if tail.row() > head.row() {
 262                row.0 -= 1
 263            } else {
 264                row.0 += 1
 265            }
 266        }
 267
 268        s.select(selections);
 269    })
 270}
 271
 272pub fn visual_object(object: Object, cx: &mut WindowContext) {
 273    Vim::update(cx, |vim, cx| {
 274        if let Some(Operator::Object { around }) = vim.active_operator() {
 275            vim.pop_operator(cx);
 276            let current_mode = vim.state().mode;
 277            let target_mode = object.target_visual_mode(current_mode);
 278            if target_mode != current_mode {
 279                vim.switch_mode(target_mode, true, cx);
 280            }
 281
 282            vim.update_active_editor(cx, |_, editor, cx| {
 283                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 284                    s.move_with(|map, selection| {
 285                        let mut mut_selection = selection.clone();
 286
 287                        // all our motions assume that the current character is
 288                        // after the cursor; however in the case of a visual selection
 289                        // the current character is before the cursor.
 290                        // But this will affect the judgment of the html tag
 291                        // so the html tag needs to skip this logic.
 292                        if !selection.reversed && object != Object::Tag {
 293                            mut_selection.set_head(
 294                                movement::left(map, mut_selection.head()),
 295                                mut_selection.goal,
 296                            );
 297                        }
 298
 299                        if let Some(range) = object.range(map, mut_selection, around) {
 300                            if !range.is_empty() {
 301                                let expand_both_ways = object.always_expands_both_ways()
 302                                    || selection.is_empty()
 303                                    || movement::right(map, selection.start) == selection.end;
 304
 305                                if expand_both_ways {
 306                                    selection.start = range.start;
 307                                    selection.end = range.end;
 308                                } else if selection.reversed {
 309                                    selection.start = range.start;
 310                                } else {
 311                                    selection.end = range.end;
 312                                }
 313                            }
 314
 315                            // In the visual selection result of a paragraph object, the cursor is
 316                            // placed at the start of the last line. And in the visual mode, the
 317                            // selection end is located after the end character. So, adjustment of
 318                            // selection end is needed.
 319                            //
 320                            // We don't do this adjustment for a one-line blank paragraph since the
 321                            // trailing newline is included in its selection from the beginning.
 322                            if object == Object::Paragraph && range.start != range.end {
 323                                let row_of_selection_end_line = selection.end.to_point(map).row;
 324                                let new_selection_end = if map
 325                                    .buffer_snapshot
 326                                    .line_len(MultiBufferRow(row_of_selection_end_line))
 327                                    == 0
 328                                {
 329                                    Point::new(row_of_selection_end_line + 1, 0)
 330                                } else {
 331                                    Point::new(row_of_selection_end_line, 1)
 332                                };
 333                                selection.end = new_selection_end.to_display_point(map);
 334                            }
 335                        }
 336                    });
 337                });
 338            });
 339        }
 340    });
 341}
 342
 343fn toggle_mode(mode: Mode, cx: &mut ViewContext<Workspace>) {
 344    Vim::update(cx, |vim, cx| {
 345        if vim.state().mode == mode {
 346            vim.switch_mode(Mode::Normal, false, cx);
 347        } else {
 348            vim.switch_mode(mode, false, cx);
 349        }
 350    })
 351}
 352
 353pub fn other_end(_: &mut Workspace, _: &OtherEnd, cx: &mut ViewContext<Workspace>) {
 354    Vim::update(cx, |vim, cx| {
 355        vim.update_active_editor(cx, |_, editor, cx| {
 356            editor.change_selections(None, cx, |s| {
 357                s.move_with(|_, selection| {
 358                    selection.reversed = !selection.reversed;
 359                })
 360            })
 361        })
 362    });
 363}
 364
 365pub fn delete(vim: &mut Vim, line_mode: bool, cx: &mut WindowContext) {
 366    vim.update_active_editor(cx, |vim, editor, cx| {
 367        let mut original_columns: HashMap<_, _> = Default::default();
 368        let line_mode = line_mode || editor.selections.line_mode;
 369
 370        editor.transact(cx, |editor, cx| {
 371            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 372                s.move_with(|map, selection| {
 373                    if line_mode {
 374                        let mut position = selection.head();
 375                        if !selection.reversed {
 376                            position = movement::left(map, position);
 377                        }
 378                        original_columns.insert(selection.id, position.to_point(map).column);
 379                        if vim.state().mode == Mode::VisualBlock {
 380                            *selection.end.column_mut() = map.line_len(selection.end.row())
 381                        } else if vim.state().mode != Mode::VisualLine {
 382                            selection.start = DisplayPoint::new(selection.start.row(), 0);
 383                            if selection.end.row() == map.max_point().row() {
 384                                selection.end = map.max_point()
 385                            } else {
 386                                *selection.end.row_mut() += 1;
 387                                *selection.end.column_mut() = 0;
 388                            }
 389                        }
 390                    }
 391                    selection.goal = SelectionGoal::None;
 392                });
 393            });
 394            copy_selections_content(vim, editor, line_mode, cx);
 395            editor.insert("", cx);
 396
 397            // Fixup cursor position after the deletion
 398            editor.set_clip_at_line_ends(true, cx);
 399            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 400                s.move_with(|map, selection| {
 401                    let mut cursor = selection.head().to_point(map);
 402
 403                    if let Some(column) = original_columns.get(&selection.id) {
 404                        cursor.column = *column
 405                    }
 406                    let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left);
 407                    selection.collapse_to(cursor, selection.goal)
 408                });
 409                if vim.state().mode == Mode::VisualBlock {
 410                    s.select_anchors(vec![s.first_anchor()])
 411                }
 412            });
 413        })
 414    });
 415    vim.switch_mode(Mode::Normal, true, cx);
 416}
 417
 418pub fn yank(vim: &mut Vim, cx: &mut WindowContext) {
 419    vim.update_active_editor(cx, |vim, editor, cx| {
 420        let line_mode = editor.selections.line_mode;
 421        yank_selections_content(vim, editor, line_mode, cx);
 422        editor.change_selections(None, cx, |s| {
 423            s.move_with(|map, selection| {
 424                if line_mode {
 425                    selection.start = start_of_line(map, false, selection.start);
 426                };
 427                selection.collapse_to(selection.start, SelectionGoal::None)
 428            });
 429            if vim.state().mode == Mode::VisualBlock {
 430                s.select_anchors(vec![s.first_anchor()])
 431            }
 432        });
 433    });
 434    vim.switch_mode(Mode::Normal, true, cx);
 435}
 436
 437pub(crate) fn visual_replace(text: Arc<str>, cx: &mut WindowContext) {
 438    Vim::update(cx, |vim, cx| {
 439        vim.stop_recording();
 440        vim.update_active_editor(cx, |_, editor, cx| {
 441            editor.transact(cx, |editor, cx| {
 442                let (display_map, selections) = editor.selections.all_adjusted_display(cx);
 443
 444                // Selections are biased right at the start. So we need to store
 445                // anchors that are biased left so that we can restore the selections
 446                // after the change
 447                let stable_anchors = editor
 448                    .selections
 449                    .disjoint_anchors()
 450                    .into_iter()
 451                    .map(|selection| {
 452                        let start = selection.start.bias_left(&display_map.buffer_snapshot);
 453                        start..start
 454                    })
 455                    .collect::<Vec<_>>();
 456
 457                let mut edits = Vec::new();
 458                for selection in selections.iter() {
 459                    let selection = selection.clone();
 460                    for row_range in
 461                        movement::split_display_range_by_lines(&display_map, selection.range())
 462                    {
 463                        let range = row_range.start.to_offset(&display_map, Bias::Right)
 464                            ..row_range.end.to_offset(&display_map, Bias::Right);
 465                        let text = text.repeat(range.len());
 466                        edits.push((range, text));
 467                    }
 468                }
 469
 470                editor.buffer().update(cx, |buffer, cx| {
 471                    buffer.edit(edits, None, cx);
 472                });
 473                editor.change_selections(None, cx, |s| s.select_ranges(stable_anchors));
 474            });
 475        });
 476        vim.switch_mode(Mode::Normal, false, cx);
 477    });
 478}
 479
 480pub fn select_next(_: &mut Workspace, _: &SelectNext, cx: &mut ViewContext<Workspace>) {
 481    Vim::update(cx, |vim, cx| {
 482        let count =
 483            vim.take_count(cx)
 484                .unwrap_or_else(|| if vim.state().mode.is_visual() { 1 } else { 2 });
 485        vim.update_active_editor(cx, |_, editor, cx| {
 486            for _ in 0..count {
 487                if editor
 488                    .select_next(&Default::default(), cx)
 489                    .log_err()
 490                    .is_none()
 491                {
 492                    break;
 493                }
 494            }
 495        })
 496    });
 497}
 498
 499pub fn select_previous(_: &mut Workspace, _: &SelectPrevious, cx: &mut ViewContext<Workspace>) {
 500    Vim::update(cx, |vim, cx| {
 501        let count =
 502            vim.take_count(cx)
 503                .unwrap_or_else(|| if vim.state().mode.is_visual() { 1 } else { 2 });
 504        vim.update_active_editor(cx, |_, editor, cx| {
 505            for _ in 0..count {
 506                if editor
 507                    .select_previous(&Default::default(), cx)
 508                    .log_err()
 509                    .is_none()
 510                {
 511                    break;
 512                }
 513            }
 514        })
 515    });
 516}
 517
 518pub fn select_match(
 519    workspace: &mut Workspace,
 520    vim: &mut Vim,
 521    direction: Direction,
 522    cx: &mut WindowContext,
 523) {
 524    let count = vim.take_count(cx).unwrap_or(1);
 525    let pane = workspace.active_pane().clone();
 526    let vim_is_normal = vim.state().mode == Mode::Normal;
 527    let mut start_selection = 0usize;
 528    let mut end_selection = 0usize;
 529
 530    vim.update_active_editor(cx, |_, editor, _| {
 531        editor.set_collapse_matches(false);
 532    });
 533    if vim_is_normal {
 534        pane.update(cx, |pane, cx| {
 535            if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 536                search_bar.update(cx, |search_bar, cx| {
 537                    if !search_bar.has_active_match() || !search_bar.show(cx) {
 538                        return;
 539                    }
 540                    // without update_match_index there is a bug when the cursor is before the first match
 541                    search_bar.update_match_index(cx);
 542                    search_bar.select_match(direction.opposite(), 1, cx);
 543                });
 544            }
 545        });
 546    }
 547    vim.update_active_editor(cx, |_, editor, cx| {
 548        let latest = editor.selections.newest::<usize>(cx);
 549        start_selection = latest.start;
 550        end_selection = latest.end;
 551    });
 552
 553    let mut match_exists = false;
 554    pane.update(cx, |pane, cx| {
 555        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 556            search_bar.update(cx, |search_bar, cx| {
 557                search_bar.update_match_index(cx);
 558                search_bar.select_match(direction, count, cx);
 559                match_exists = search_bar.match_exists(cx);
 560            });
 561        }
 562    });
 563    if !match_exists {
 564        vim.clear_operator(cx);
 565        vim.stop_replaying();
 566        return;
 567    }
 568    vim.update_active_editor(cx, |_, editor, cx| {
 569        let latest = editor.selections.newest::<usize>(cx);
 570        if vim_is_normal {
 571            start_selection = latest.start;
 572            end_selection = latest.end;
 573        } else {
 574            start_selection = start_selection.min(latest.start);
 575            end_selection = end_selection.max(latest.end);
 576        }
 577        if direction == Direction::Prev {
 578            std::mem::swap(&mut start_selection, &mut end_selection);
 579        }
 580        editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 581            s.select_ranges([start_selection..end_selection]);
 582        });
 583        editor.set_collapse_matches(true);
 584    });
 585
 586    match vim.maybe_pop_operator() {
 587        Some(Operator::Change) => substitute(vim, None, false, cx),
 588        Some(Operator::Delete) => {
 589            vim.stop_recording();
 590            delete(vim, false, cx)
 591        }
 592        Some(Operator::Yank) => yank(vim, cx),
 593        _ => {} // Ignoring other operators
 594    }
 595}
 596
 597#[cfg(test)]
 598mod test {
 599    use indoc::indoc;
 600    use workspace::item::Item;
 601
 602    use crate::{
 603        state::Mode,
 604        test::{NeovimBackedTestContext, VimTestContext},
 605    };
 606
 607    #[gpui::test]
 608    async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
 609        let mut cx = NeovimBackedTestContext::new(cx).await;
 610
 611        cx.set_shared_state(indoc! {
 612            "The ˇquick brown
 613            fox jumps over
 614            the lazy dog"
 615        })
 616        .await;
 617        let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
 618
 619        // entering visual mode should select the character
 620        // under cursor
 621        cx.simulate_shared_keystrokes("v").await;
 622        cx.shared_state()
 623            .await
 624            .assert_eq(indoc! { "The «qˇ»uick brown
 625            fox jumps over
 626            the lazy dog"});
 627        cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
 628
 629        // forwards motions should extend the selection
 630        cx.simulate_shared_keystrokes("w j").await;
 631        cx.shared_state().await.assert_eq(indoc! { "The «quick brown
 632            fox jumps oˇ»ver
 633            the lazy dog"});
 634
 635        cx.simulate_shared_keystrokes("escape").await;
 636        cx.shared_state().await.assert_eq(indoc! { "The quick brown
 637            fox jumps ˇover
 638            the lazy dog"});
 639
 640        // motions work backwards
 641        cx.simulate_shared_keystrokes("v k b").await;
 642        cx.shared_state()
 643            .await
 644            .assert_eq(indoc! { "The «ˇquick brown
 645            fox jumps o»ver
 646            the lazy dog"});
 647
 648        // works on empty lines
 649        cx.set_shared_state(indoc! {"
 650            a
 651            ˇ
 652            b
 653            "})
 654            .await;
 655        let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
 656        cx.simulate_shared_keystrokes("v").await;
 657        cx.shared_state().await.assert_eq(indoc! {"
 658            a
 659            «
 660            ˇ»b
 661        "});
 662        cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
 663
 664        // toggles off again
 665        cx.simulate_shared_keystrokes("v").await;
 666        cx.shared_state().await.assert_eq(indoc! {"
 667            a
 668            ˇ
 669            b
 670            "});
 671
 672        // works at the end of a document
 673        cx.set_shared_state(indoc! {"
 674            a
 675            b
 676            ˇ"})
 677            .await;
 678
 679        cx.simulate_shared_keystrokes("v").await;
 680        cx.shared_state().await.assert_eq(indoc! {"
 681            a
 682            b
 683            ˇ"});
 684    }
 685
 686    #[gpui::test]
 687    async fn test_enter_visual_line_mode(cx: &mut gpui::TestAppContext) {
 688        let mut cx = NeovimBackedTestContext::new(cx).await;
 689
 690        cx.set_shared_state(indoc! {
 691            "The ˇquick brown
 692            fox jumps over
 693            the lazy dog"
 694        })
 695        .await;
 696        cx.simulate_shared_keystrokes("shift-v").await;
 697        cx.shared_state()
 698            .await
 699            .assert_eq(indoc! { "The «qˇ»uick brown
 700            fox jumps over
 701            the lazy dog"});
 702        cx.simulate_shared_keystrokes("x").await;
 703        cx.shared_state().await.assert_eq(indoc! { "fox ˇjumps over
 704        the lazy dog"});
 705
 706        // it should work on empty lines
 707        cx.set_shared_state(indoc! {"
 708            a
 709            ˇ
 710            b"})
 711            .await;
 712        cx.simulate_shared_keystrokes("shift-v").await;
 713        cx.shared_state().await.assert_eq(indoc! {"
 714            a
 715            «
 716            ˇ»b"});
 717        cx.simulate_shared_keystrokes("x").await;
 718        cx.shared_state().await.assert_eq(indoc! {"
 719            a
 720            ˇb"});
 721
 722        // it should work at the end of the document
 723        cx.set_shared_state(indoc! {"
 724            a
 725            b
 726            ˇ"})
 727            .await;
 728        let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
 729        cx.simulate_shared_keystrokes("shift-v").await;
 730        cx.shared_state().await.assert_eq(indoc! {"
 731            a
 732            b
 733            ˇ"});
 734        cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
 735        cx.simulate_shared_keystrokes("x").await;
 736        cx.shared_state().await.assert_eq(indoc! {"
 737            a
 738            ˇb"});
 739    }
 740
 741    #[gpui::test]
 742    async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
 743        let mut cx = NeovimBackedTestContext::new(cx).await;
 744
 745        cx.simulate("v w", "The quick ˇbrown")
 746            .await
 747            .assert_matches();
 748
 749        cx.simulate("v w x", "The quick ˇbrown")
 750            .await
 751            .assert_matches();
 752        cx.simulate(
 753            "v w j x",
 754            indoc! {"
 755                The ˇquick brown
 756                fox jumps over
 757                the lazy dog"},
 758        )
 759        .await
 760        .assert_matches();
 761        // Test pasting code copied on delete
 762        cx.simulate_shared_keystrokes("j p").await;
 763        cx.shared_state().await.assert_matches();
 764
 765        cx.simulate_at_each_offset(
 766            "v w j x",
 767            indoc! {"
 768                The ˇquick brown
 769                fox jumps over
 770                the ˇlazy dog"},
 771        )
 772        .await
 773        .assert_matches();
 774        cx.simulate_at_each_offset(
 775            "v b k x",
 776            indoc! {"
 777                The ˇquick brown
 778                fox jumps ˇover
 779                the ˇlazy dog"},
 780        )
 781        .await
 782        .assert_matches();
 783    }
 784
 785    #[gpui::test]
 786    async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
 787        let mut cx = NeovimBackedTestContext::new(cx).await;
 788
 789        cx.set_shared_state(indoc! {"
 790                The quˇick brown
 791                fox jumps over
 792                the lazy dog"})
 793            .await;
 794        cx.simulate_shared_keystrokes("shift-v x").await;
 795        cx.shared_state().await.assert_matches();
 796
 797        // Test pasting code copied on delete
 798        cx.simulate_shared_keystrokes("p").await;
 799        cx.shared_state().await.assert_matches();
 800
 801        cx.set_shared_state(indoc! {"
 802                The quick brown
 803                fox jumps over
 804                the laˇzy dog"})
 805            .await;
 806        cx.simulate_shared_keystrokes("shift-v x").await;
 807        cx.shared_state().await.assert_matches();
 808        cx.shared_clipboard().await.assert_eq("the lazy dog\n");
 809
 810        cx.set_shared_state(indoc! {"
 811                                The quˇick brown
 812                                fox jumps over
 813                                the lazy dog"})
 814            .await;
 815        cx.simulate_shared_keystrokes("shift-v j x").await;
 816        cx.shared_state().await.assert_matches();
 817        // Test pasting code copied on delete
 818        cx.simulate_shared_keystrokes("p").await;
 819        cx.shared_state().await.assert_matches();
 820
 821        cx.set_shared_state(indoc! {"
 822            The ˇlong line
 823            should not
 824            crash
 825            "})
 826            .await;
 827        cx.simulate_shared_keystrokes("shift-v $ x").await;
 828        cx.shared_state().await.assert_matches();
 829    }
 830
 831    #[gpui::test]
 832    async fn test_visual_yank(cx: &mut gpui::TestAppContext) {
 833        let mut cx = NeovimBackedTestContext::new(cx).await;
 834
 835        cx.set_shared_state("The quick ˇbrown").await;
 836        cx.simulate_shared_keystrokes("v w y").await;
 837        cx.shared_state().await.assert_eq("The quick ˇbrown");
 838        cx.shared_clipboard().await.assert_eq("brown");
 839
 840        cx.set_shared_state(indoc! {"
 841                The ˇquick brown
 842                fox jumps over
 843                the lazy dog"})
 844            .await;
 845        cx.simulate_shared_keystrokes("v w j y").await;
 846        cx.shared_state().await.assert_eq(indoc! {"
 847                    The ˇquick brown
 848                    fox jumps over
 849                    the lazy dog"});
 850        cx.shared_clipboard().await.assert_eq(indoc! {"
 851                quick brown
 852                fox jumps o"});
 853
 854        cx.set_shared_state(indoc! {"
 855                    The quick brown
 856                    fox jumps over
 857                    the ˇlazy dog"})
 858            .await;
 859        cx.simulate_shared_keystrokes("v w j y").await;
 860        cx.shared_state().await.assert_eq(indoc! {"
 861                    The quick brown
 862                    fox jumps over
 863                    the ˇlazy dog"});
 864        cx.shared_clipboard().await.assert_eq("lazy d");
 865        cx.simulate_shared_keystrokes("shift-v y").await;
 866        cx.shared_clipboard().await.assert_eq("the lazy dog\n");
 867
 868        cx.set_shared_state(indoc! {"
 869                    The ˇquick brown
 870                    fox jumps over
 871                    the lazy dog"})
 872            .await;
 873        cx.simulate_shared_keystrokes("v b k y").await;
 874        cx.shared_state().await.assert_eq(indoc! {"
 875                    ˇThe quick brown
 876                    fox jumps over
 877                    the lazy dog"});
 878        assert_eq!(
 879            cx.read_from_clipboard()
 880                .map(|item| item.text().clone())
 881                .unwrap(),
 882            "The q"
 883        );
 884
 885        cx.set_shared_state(indoc! {"
 886                    The quick brown
 887                    fox ˇjumps over
 888                    the lazy dog"})
 889            .await;
 890        cx.simulate_shared_keystrokes("shift-v shift-g shift-y")
 891            .await;
 892        cx.shared_state().await.assert_eq(indoc! {"
 893                    The quick brown
 894                    ˇfox jumps over
 895                    the lazy dog"});
 896        cx.shared_clipboard()
 897            .await
 898            .assert_eq("fox jumps over\nthe lazy dog\n");
 899    }
 900
 901    #[gpui::test]
 902    async fn test_visual_block_mode(cx: &mut gpui::TestAppContext) {
 903        let mut cx = NeovimBackedTestContext::new(cx).await;
 904
 905        cx.set_shared_state(indoc! {
 906            "The ˇquick brown
 907             fox jumps over
 908             the lazy dog"
 909        })
 910        .await;
 911        cx.simulate_shared_keystrokes("ctrl-v").await;
 912        cx.shared_state().await.assert_eq(indoc! {
 913            "The «qˇ»uick brown
 914            fox jumps over
 915            the lazy dog"
 916        });
 917        cx.simulate_shared_keystrokes("2 down").await;
 918        cx.shared_state().await.assert_eq(indoc! {
 919            "The «qˇ»uick brown
 920            fox «jˇ»umps over
 921            the «lˇ»azy dog"
 922        });
 923        cx.simulate_shared_keystrokes("e").await;
 924        cx.shared_state().await.assert_eq(indoc! {
 925            "The «quicˇ»k brown
 926            fox «jumpˇ»s over
 927            the «lazyˇ» dog"
 928        });
 929        cx.simulate_shared_keystrokes("^").await;
 930        cx.shared_state().await.assert_eq(indoc! {
 931            "«ˇThe q»uick brown
 932            «ˇfox j»umps over
 933            «ˇthe l»azy dog"
 934        });
 935        cx.simulate_shared_keystrokes("$").await;
 936        cx.shared_state().await.assert_eq(indoc! {
 937            "The «quick brownˇ»
 938            fox «jumps overˇ»
 939            the «lazy dogˇ»"
 940        });
 941        cx.simulate_shared_keystrokes("shift-f space").await;
 942        cx.shared_state().await.assert_eq(indoc! {
 943            "The «quickˇ» brown
 944            fox «jumpsˇ» over
 945            the «lazy ˇ»dog"
 946        });
 947
 948        // toggling through visual mode works as expected
 949        cx.simulate_shared_keystrokes("v").await;
 950        cx.shared_state().await.assert_eq(indoc! {
 951            "The «quick brown
 952            fox jumps over
 953            the lazy ˇ»dog"
 954        });
 955        cx.simulate_shared_keystrokes("ctrl-v").await;
 956        cx.shared_state().await.assert_eq(indoc! {
 957            "The «quickˇ» brown
 958            fox «jumpsˇ» over
 959            the «lazy ˇ»dog"
 960        });
 961
 962        cx.set_shared_state(indoc! {
 963            "The ˇquick
 964             brown
 965             fox
 966             jumps over the
 967
 968             lazy dog
 969            "
 970        })
 971        .await;
 972        cx.simulate_shared_keystrokes("ctrl-v down down").await;
 973        cx.shared_state().await.assert_eq(indoc! {
 974            "The«ˇ q»uick
 975            bro«ˇwn»
 976            foxˇ
 977            jumps over the
 978
 979            lazy dog
 980            "
 981        });
 982        cx.simulate_shared_keystrokes("down").await;
 983        cx.shared_state().await.assert_eq(indoc! {
 984            "The «qˇ»uick
 985            brow«nˇ»
 986            fox
 987            jump«sˇ» over the
 988
 989            lazy dog
 990            "
 991        });
 992        cx.simulate_shared_keystrokes("left").await;
 993        cx.shared_state().await.assert_eq(indoc! {
 994            "The«ˇ q»uick
 995            bro«ˇwn»
 996            foxˇ
 997            jum«ˇps» over the
 998
 999            lazy dog
1000            "
1001        });
1002        cx.simulate_shared_keystrokes("s o escape").await;
1003        cx.shared_state().await.assert_eq(indoc! {
1004            "Theˇouick
1005            broo
1006            foxo
1007            jumo over the
1008
1009            lazy dog
1010            "
1011        });
1012
1013        // https://github.com/zed-industries/zed/issues/6274
1014        cx.set_shared_state(indoc! {
1015            "Theˇ quick brown
1016
1017            fox jumps over
1018            the lazy dog
1019            "
1020        })
1021        .await;
1022        cx.simulate_shared_keystrokes("l ctrl-v j j").await;
1023        cx.shared_state().await.assert_eq(indoc! {
1024            "The «qˇ»uick brown
1025
1026            fox «jˇ»umps over
1027            the lazy dog
1028            "
1029        });
1030    }
1031
1032    #[gpui::test]
1033    async fn test_visual_block_issue_2123(cx: &mut gpui::TestAppContext) {
1034        let mut cx = NeovimBackedTestContext::new(cx).await;
1035
1036        cx.set_shared_state(indoc! {
1037            "The ˇquick brown
1038            fox jumps over
1039            the lazy dog
1040            "
1041        })
1042        .await;
1043        cx.simulate_shared_keystrokes("ctrl-v right down").await;
1044        cx.shared_state().await.assert_eq(indoc! {
1045            "The «quˇ»ick brown
1046            fox «juˇ»mps over
1047            the lazy dog
1048            "
1049        });
1050    }
1051
1052    #[gpui::test]
1053    async fn test_visual_block_insert(cx: &mut gpui::TestAppContext) {
1054        let mut cx = NeovimBackedTestContext::new(cx).await;
1055
1056        cx.set_shared_state(indoc! {
1057            "ˇThe quick brown
1058            fox jumps over
1059            the lazy dog
1060            "
1061        })
1062        .await;
1063        cx.simulate_shared_keystrokes("ctrl-v 9 down").await;
1064        cx.shared_state().await.assert_eq(indoc! {
1065            "«Tˇ»he quick brown
1066            «fˇ»ox jumps over
1067            «tˇ»he lazy dog
1068            ˇ"
1069        });
1070
1071        cx.simulate_shared_keystrokes("shift-i k escape").await;
1072        cx.shared_state().await.assert_eq(indoc! {
1073            "ˇkThe quick brown
1074            kfox jumps over
1075            kthe lazy dog
1076            k"
1077        });
1078
1079        cx.set_shared_state(indoc! {
1080            "ˇThe quick brown
1081            fox jumps over
1082            the lazy dog
1083            "
1084        })
1085        .await;
1086        cx.simulate_shared_keystrokes("ctrl-v 9 down").await;
1087        cx.shared_state().await.assert_eq(indoc! {
1088            "«Tˇ»he quick brown
1089            «fˇ»ox jumps over
1090            «tˇ»he lazy dog
1091            ˇ"
1092        });
1093        cx.simulate_shared_keystrokes("c k escape").await;
1094        cx.shared_state().await.assert_eq(indoc! {
1095            "ˇkhe quick brown
1096            kox jumps over
1097            khe lazy dog
1098            k"
1099        });
1100    }
1101
1102    #[gpui::test]
1103    async fn test_visual_object(cx: &mut gpui::TestAppContext) {
1104        let mut cx = NeovimBackedTestContext::new(cx).await;
1105
1106        cx.set_shared_state("hello (in [parˇens] o)").await;
1107        cx.simulate_shared_keystrokes("ctrl-v l").await;
1108        cx.simulate_shared_keystrokes("a ]").await;
1109        cx.shared_state()
1110            .await
1111            .assert_eq("hello (in «[parens]ˇ» o)");
1112        cx.simulate_shared_keystrokes("i (").await;
1113        cx.shared_state()
1114            .await
1115            .assert_eq("hello («in [parens] oˇ»)");
1116
1117        cx.set_shared_state("hello in a wˇord again.").await;
1118        cx.simulate_shared_keystrokes("ctrl-v l i w").await;
1119        cx.shared_state()
1120            .await
1121            .assert_eq("hello in a w«ordˇ» again.");
1122        assert_eq!(cx.mode(), Mode::VisualBlock);
1123        cx.simulate_shared_keystrokes("o a s").await;
1124        cx.shared_state()
1125            .await
1126            .assert_eq("«ˇhello in a word» again.");
1127    }
1128
1129    #[gpui::test]
1130    async fn test_mode_across_command(cx: &mut gpui::TestAppContext) {
1131        let mut cx = VimTestContext::new(cx, true).await;
1132
1133        cx.set_state("aˇbc", Mode::Normal);
1134        cx.simulate_keystrokes("ctrl-v");
1135        assert_eq!(cx.mode(), Mode::VisualBlock);
1136        cx.simulate_keystrokes("cmd-shift-p escape");
1137        assert_eq!(cx.mode(), Mode::VisualBlock);
1138    }
1139
1140    #[gpui::test]
1141    async fn test_gn(cx: &mut gpui::TestAppContext) {
1142        let mut cx = NeovimBackedTestContext::new(cx).await;
1143
1144        cx.set_shared_state("aaˇ aa aa aa aa").await;
1145        cx.simulate_shared_keystrokes("/ a a enter").await;
1146        cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1147        cx.simulate_shared_keystrokes("g n").await;
1148        cx.shared_state().await.assert_eq("aa «aaˇ» aa aa aa");
1149        cx.simulate_shared_keystrokes("g n").await;
1150        cx.shared_state().await.assert_eq("aa «aa aaˇ» aa aa");
1151        cx.simulate_shared_keystrokes("escape d g n").await;
1152        cx.shared_state().await.assert_eq("aa aa ˇ aa aa");
1153
1154        cx.set_shared_state("aaˇ aa aa aa aa").await;
1155        cx.simulate_shared_keystrokes("/ a a enter").await;
1156        cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1157        cx.simulate_shared_keystrokes("3 g n").await;
1158        cx.shared_state().await.assert_eq("aa aa aa «aaˇ» aa");
1159
1160        cx.set_shared_state("aaˇ aa aa aa aa").await;
1161        cx.simulate_shared_keystrokes("/ a a enter").await;
1162        cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1163        cx.simulate_shared_keystrokes("g shift-n").await;
1164        cx.shared_state().await.assert_eq("aa «ˇaa» aa aa aa");
1165        cx.simulate_shared_keystrokes("g shift-n").await;
1166        cx.shared_state().await.assert_eq("«ˇaa aa» aa aa aa");
1167    }
1168
1169    #[gpui::test]
1170    async fn test_dgn_repeat(cx: &mut gpui::TestAppContext) {
1171        let mut cx = NeovimBackedTestContext::new(cx).await;
1172
1173        cx.set_shared_state("aaˇ aa aa aa aa").await;
1174        cx.simulate_shared_keystrokes("/ a a enter").await;
1175        cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1176        cx.simulate_shared_keystrokes("d g n").await;
1177
1178        cx.shared_state().await.assert_eq("aa ˇ aa aa aa");
1179        cx.simulate_shared_keystrokes(".").await;
1180        cx.shared_state().await.assert_eq("aa  ˇ aa aa");
1181        cx.simulate_shared_keystrokes(".").await;
1182        cx.shared_state().await.assert_eq("aa   ˇ aa");
1183    }
1184
1185    #[gpui::test]
1186    async fn test_cgn_repeat(cx: &mut gpui::TestAppContext) {
1187        let mut cx = NeovimBackedTestContext::new(cx).await;
1188
1189        cx.set_shared_state("aaˇ aa aa aa aa").await;
1190        cx.simulate_shared_keystrokes("/ a a enter").await;
1191        cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1192        cx.simulate_shared_keystrokes("c g n x escape").await;
1193        cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1194        cx.simulate_shared_keystrokes(".").await;
1195        cx.shared_state().await.assert_eq("aa x ˇx aa aa");
1196    }
1197
1198    #[gpui::test]
1199    async fn test_cgn_nomatch(cx: &mut gpui::TestAppContext) {
1200        let mut cx = NeovimBackedTestContext::new(cx).await;
1201
1202        cx.set_shared_state("aaˇ aa aa aa aa").await;
1203        cx.simulate_shared_keystrokes("/ b b enter").await;
1204        cx.shared_state().await.assert_eq("aaˇ aa aa aa aa");
1205        cx.simulate_shared_keystrokes("c g n x escape").await;
1206        cx.shared_state().await.assert_eq("aaˇaa aa aa aa");
1207        cx.simulate_shared_keystrokes(".").await;
1208        cx.shared_state().await.assert_eq("aaˇa aa aa aa");
1209
1210        cx.set_shared_state("aaˇ bb aa aa aa").await;
1211        cx.simulate_shared_keystrokes("/ b b enter").await;
1212        cx.shared_state().await.assert_eq("aa ˇbb aa aa aa");
1213        cx.simulate_shared_keystrokes("c g n x escape").await;
1214        cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1215        cx.simulate_shared_keystrokes(".").await;
1216        cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1217    }
1218
1219    #[gpui::test]
1220    async fn test_visual_shift_d(cx: &mut gpui::TestAppContext) {
1221        let mut cx = NeovimBackedTestContext::new(cx).await;
1222
1223        cx.set_shared_state(indoc! {
1224            "The ˇquick brown
1225            fox jumps over
1226            the lazy dog
1227            "
1228        })
1229        .await;
1230        cx.simulate_shared_keystrokes("v down shift-d").await;
1231        cx.shared_state().await.assert_eq(indoc! {
1232            "the ˇlazy dog\n"
1233        });
1234
1235        cx.set_shared_state(indoc! {
1236            "The ˇquick brown
1237            fox jumps over
1238            the lazy dog
1239            "
1240        })
1241        .await;
1242        cx.simulate_shared_keystrokes("ctrl-v down shift-d").await;
1243        cx.shared_state().await.assert_eq(indoc! {
1244            "Theˇ•
1245            fox•
1246            the lazy dog
1247            "
1248        });
1249    }
1250}