visual.rs

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