visual.rs

   1use anyhow::Result;
   2use std::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 = tail_x > head_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::saturating_left(map, tail);
 173            tail_x = map.x_for_point(tail, &text_layout_details);
 174        } else if !was_reversed && is_reversed {
 175            tail = movement::saturating_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 = object.always_expands_both_ways()
 267                                    || selection.is_empty()
 268                                    || movement::right(map, selection.start) == selection.end;
 269
 270                                if expand_both_ways {
 271                                    selection.start = range.start;
 272                                    selection.end = range.end;
 273                                } else if selection.reversed {
 274                                    selection.start = range.start;
 275                                } else {
 276                                    selection.end = range.end;
 277                                }
 278                            }
 279                        }
 280                    });
 281                });
 282            });
 283        }
 284    });
 285}
 286
 287fn toggle_mode(mode: Mode, cx: &mut ViewContext<Workspace>) {
 288    Vim::update(cx, |vim, cx| {
 289        if vim.state().mode == mode {
 290            vim.switch_mode(Mode::Normal, false, cx);
 291        } else {
 292            vim.switch_mode(mode, false, cx);
 293        }
 294    })
 295}
 296
 297pub fn other_end(_: &mut Workspace, _: &OtherEnd, cx: &mut ViewContext<Workspace>) {
 298    Vim::update(cx, |vim, cx| {
 299        vim.update_active_editor(cx, |editor, cx| {
 300            editor.change_selections(None, cx, |s| {
 301                s.move_with(|_, selection| {
 302                    selection.reversed = !selection.reversed;
 303                })
 304            })
 305        })
 306    });
 307}
 308
 309pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspace>) {
 310    Vim::update(cx, |vim, cx| {
 311        vim.record_current_action(cx);
 312        vim.update_active_editor(cx, |editor, cx| {
 313            let mut original_columns: HashMap<_, _> = Default::default();
 314            let line_mode = editor.selections.line_mode;
 315
 316            editor.transact(cx, |editor, cx| {
 317                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 318                    s.move_with(|map, selection| {
 319                        if line_mode {
 320                            let mut position = selection.head();
 321                            if !selection.reversed {
 322                                position = movement::left(map, position);
 323                            }
 324                            original_columns.insert(selection.id, position.to_point(map).column);
 325                        }
 326                        selection.goal = SelectionGoal::None;
 327                    });
 328                });
 329                copy_selections_content(editor, line_mode, cx);
 330                editor.insert("", cx);
 331
 332                // Fixup cursor position after the deletion
 333                editor.set_clip_at_line_ends(true, cx);
 334                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 335                    s.move_with(|map, selection| {
 336                        let mut cursor = selection.head().to_point(map);
 337
 338                        if let Some(column) = original_columns.get(&selection.id) {
 339                            cursor.column = *column
 340                        }
 341                        let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left);
 342                        selection.collapse_to(cursor, selection.goal)
 343                    });
 344                    if vim.state().mode == Mode::VisualBlock {
 345                        s.select_anchors(vec![s.first_anchor()])
 346                    }
 347                });
 348            })
 349        });
 350        vim.switch_mode(Mode::Normal, true, cx);
 351    });
 352}
 353
 354pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext<Workspace>) {
 355    Vim::update(cx, |vim, cx| {
 356        vim.update_active_editor(cx, |editor, cx| {
 357            let line_mode = editor.selections.line_mode;
 358            copy_selections_content(editor, line_mode, cx);
 359            editor.change_selections(None, cx, |s| {
 360                s.move_with(|map, selection| {
 361                    if line_mode {
 362                        selection.start = start_of_line(map, false, selection.start);
 363                    };
 364                    selection.collapse_to(selection.start, SelectionGoal::None)
 365                });
 366                if vim.state().mode == Mode::VisualBlock {
 367                    s.select_anchors(vec![s.first_anchor()])
 368                }
 369            });
 370        });
 371        vim.switch_mode(Mode::Normal, true, cx);
 372    });
 373}
 374
 375pub(crate) fn visual_replace(text: Arc<str>, cx: &mut WindowContext) {
 376    Vim::update(cx, |vim, cx| {
 377        vim.stop_recording();
 378        vim.update_active_editor(cx, |editor, cx| {
 379            editor.transact(cx, |editor, cx| {
 380                let (display_map, selections) = editor.selections.all_adjusted_display(cx);
 381
 382                // Selections are biased right at the start. So we need to store
 383                // anchors that are biased left so that we can restore the selections
 384                // after the change
 385                let stable_anchors = editor
 386                    .selections
 387                    .disjoint_anchors()
 388                    .into_iter()
 389                    .map(|selection| {
 390                        let start = selection.start.bias_left(&display_map.buffer_snapshot);
 391                        start..start
 392                    })
 393                    .collect::<Vec<_>>();
 394
 395                let mut edits = Vec::new();
 396                for selection in selections.iter() {
 397                    let selection = selection.clone();
 398                    for row_range in
 399                        movement::split_display_range_by_lines(&display_map, selection.range())
 400                    {
 401                        let range = row_range.start.to_offset(&display_map, Bias::Right)
 402                            ..row_range.end.to_offset(&display_map, Bias::Right);
 403                        let text = text.repeat(range.len());
 404                        edits.push((range, text));
 405                    }
 406                }
 407
 408                editor.buffer().update(cx, |buffer, cx| {
 409                    buffer.edit(edits, None, cx);
 410                });
 411                editor.change_selections(None, cx, |s| s.select_ranges(stable_anchors));
 412            });
 413        });
 414        vim.switch_mode(Mode::Normal, false, cx);
 415    });
 416}
 417
 418pub fn select_next(
 419    _: &mut Workspace,
 420    _: &SelectNext,
 421    cx: &mut ViewContext<Workspace>,
 422) -> Result<()> {
 423    Vim::update(cx, |vim, cx| {
 424        let count =
 425            vim.take_count(cx)
 426                .unwrap_or_else(|| if vim.state().mode.is_visual() { 1 } else { 2 });
 427        vim.update_active_editor(cx, |editor, cx| {
 428            for _ in 0..count {
 429                match editor.select_next(&Default::default(), cx) {
 430                    Err(a) => return Err(a),
 431                    _ => {}
 432                }
 433            }
 434            Ok(())
 435        })
 436    })
 437    .unwrap_or(Ok(()))
 438}
 439
 440pub fn select_previous(
 441    _: &mut Workspace,
 442    _: &SelectPrevious,
 443    cx: &mut ViewContext<Workspace>,
 444) -> Result<()> {
 445    Vim::update(cx, |vim, cx| {
 446        let count =
 447            vim.take_count(cx)
 448                .unwrap_or_else(|| if vim.state().mode.is_visual() { 1 } else { 2 });
 449        vim.update_active_editor(cx, |editor, cx| {
 450            for _ in 0..count {
 451                match editor.select_previous(&Default::default(), cx) {
 452                    Err(a) => return Err(a),
 453                    _ => {}
 454                }
 455            }
 456            Ok(())
 457        })
 458    })
 459    .unwrap_or(Ok(()))
 460}
 461
 462#[cfg(test)]
 463mod test {
 464    use indoc::indoc;
 465    use workspace::item::Item;
 466
 467    use crate::{
 468        state::Mode,
 469        test::{NeovimBackedTestContext, VimTestContext},
 470    };
 471
 472    #[gpui::test]
 473    async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
 474        let mut cx = NeovimBackedTestContext::new(cx).await;
 475
 476        cx.set_shared_state(indoc! {
 477            "The ˇquick brown
 478            fox jumps over
 479            the lazy dog"
 480        })
 481        .await;
 482        let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
 483
 484        // entering visual mode should select the character
 485        // under cursor
 486        cx.simulate_shared_keystrokes(["v"]).await;
 487        cx.assert_shared_state(indoc! { "The «qˇ»uick brown
 488            fox jumps over
 489            the lazy dog"})
 490            .await;
 491        cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
 492
 493        // forwards motions should extend the selection
 494        cx.simulate_shared_keystrokes(["w", "j"]).await;
 495        cx.assert_shared_state(indoc! { "The «quick brown
 496            fox jumps oˇ»ver
 497            the lazy dog"})
 498            .await;
 499
 500        cx.simulate_shared_keystrokes(["escape"]).await;
 501        assert_eq!(Mode::Normal, cx.neovim_mode().await);
 502        cx.assert_shared_state(indoc! { "The quick brown
 503            fox jumps ˇover
 504            the lazy dog"})
 505            .await;
 506
 507        // motions work backwards
 508        cx.simulate_shared_keystrokes(["v", "k", "b"]).await;
 509        cx.assert_shared_state(indoc! { "The «ˇquick brown
 510            fox jumps o»ver
 511            the lazy dog"})
 512            .await;
 513
 514        // works on empty lines
 515        cx.set_shared_state(indoc! {"
 516            a
 517            ˇ
 518            b
 519            "})
 520            .await;
 521        let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
 522        cx.simulate_shared_keystrokes(["v"]).await;
 523        cx.assert_shared_state(indoc! {"
 524            a
 525            «
 526            ˇ»b
 527        "})
 528            .await;
 529        cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
 530
 531        // toggles off again
 532        cx.simulate_shared_keystrokes(["v"]).await;
 533        cx.assert_shared_state(indoc! {"
 534            a
 535            ˇ
 536            b
 537            "})
 538            .await;
 539
 540        // works at the end of a document
 541        cx.set_shared_state(indoc! {"
 542            a
 543            b
 544            ˇ"})
 545            .await;
 546
 547        cx.simulate_shared_keystrokes(["v"]).await;
 548        cx.assert_shared_state(indoc! {"
 549            a
 550            b
 551            ˇ"})
 552            .await;
 553        assert_eq!(cx.mode(), cx.neovim_mode().await);
 554    }
 555
 556    #[gpui::test]
 557    async fn test_enter_visual_line_mode(cx: &mut gpui::TestAppContext) {
 558        let mut cx = NeovimBackedTestContext::new(cx).await;
 559
 560        cx.set_shared_state(indoc! {
 561            "The ˇquick brown
 562            fox jumps over
 563            the lazy dog"
 564        })
 565        .await;
 566        cx.simulate_shared_keystrokes(["shift-v"]).await;
 567        cx.assert_shared_state(indoc! { "The «qˇ»uick brown
 568            fox jumps over
 569            the lazy dog"})
 570            .await;
 571        assert_eq!(cx.mode(), cx.neovim_mode().await);
 572        cx.simulate_shared_keystrokes(["x"]).await;
 573        cx.assert_shared_state(indoc! { "fox ˇjumps over
 574        the lazy dog"})
 575            .await;
 576
 577        // it should work on empty lines
 578        cx.set_shared_state(indoc! {"
 579            a
 580            ˇ
 581            b"})
 582            .await;
 583        cx.simulate_shared_keystrokes(["shift-v"]).await;
 584        cx.assert_shared_state(indoc! { "
 585            a
 586            «
 587            ˇ»b"})
 588            .await;
 589        cx.simulate_shared_keystrokes(["x"]).await;
 590        cx.assert_shared_state(indoc! { "
 591            a
 592            ˇb"})
 593            .await;
 594
 595        // it should work at the end of the document
 596        cx.set_shared_state(indoc! {"
 597            a
 598            b
 599            ˇ"})
 600            .await;
 601        let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
 602        cx.simulate_shared_keystrokes(["shift-v"]).await;
 603        cx.assert_shared_state(indoc! {"
 604            a
 605            b
 606            ˇ"})
 607            .await;
 608        assert_eq!(cx.mode(), cx.neovim_mode().await);
 609        cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
 610        cx.simulate_shared_keystrokes(["x"]).await;
 611        cx.assert_shared_state(indoc! {"
 612            a
 613            ˇb"})
 614            .await;
 615    }
 616
 617    #[gpui::test]
 618    async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
 619        let mut cx = NeovimBackedTestContext::new(cx).await;
 620
 621        cx.assert_binding_matches(["v", "w"], "The quick ˇbrown")
 622            .await;
 623
 624        cx.assert_binding_matches(["v", "w", "x"], "The quick ˇbrown")
 625            .await;
 626        cx.assert_binding_matches(
 627            ["v", "w", "j", "x"],
 628            indoc! {"
 629                The ˇquick brown
 630                fox jumps over
 631                the lazy dog"},
 632        )
 633        .await;
 634        // Test pasting code copied on delete
 635        cx.simulate_shared_keystrokes(["j", "p"]).await;
 636        cx.assert_state_matches().await;
 637
 638        let mut cx = cx.binding(["v", "w", "j", "x"]);
 639        cx.assert_all(indoc! {"
 640                The ˇquick brown
 641                fox jumps over
 642                the ˇlazy dog"})
 643            .await;
 644        let mut cx = cx.binding(["v", "b", "k", "x"]);
 645        cx.assert_all(indoc! {"
 646                The ˇquick brown
 647                fox jumps ˇover
 648                the ˇlazy dog"})
 649            .await;
 650    }
 651
 652    #[gpui::test]
 653    async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
 654        let mut cx = NeovimBackedTestContext::new(cx).await;
 655
 656        cx.set_shared_state(indoc! {"
 657                The quˇick brown
 658                fox jumps over
 659                the lazy dog"})
 660            .await;
 661        cx.simulate_shared_keystrokes(["shift-v", "x"]).await;
 662        cx.assert_state_matches().await;
 663
 664        // Test pasting code copied on delete
 665        cx.simulate_shared_keystroke("p").await;
 666        cx.assert_state_matches().await;
 667
 668        cx.set_shared_state(indoc! {"
 669                The quick brown
 670                fox jumps over
 671                the laˇzy dog"})
 672            .await;
 673        cx.simulate_shared_keystrokes(["shift-v", "x"]).await;
 674        cx.assert_state_matches().await;
 675        cx.assert_shared_clipboard("the lazy dog\n").await;
 676
 677        for marked_text in cx.each_marked_position(indoc! {"
 678                        The quˇick brown
 679                        fox jumps over
 680                        the lazy dog"})
 681        {
 682            cx.set_shared_state(&marked_text).await;
 683            cx.simulate_shared_keystrokes(["shift-v", "j", "x"]).await;
 684            cx.assert_state_matches().await;
 685            // Test pasting code copied on delete
 686            cx.simulate_shared_keystroke("p").await;
 687            cx.assert_state_matches().await;
 688        }
 689
 690        cx.set_shared_state(indoc! {"
 691            The ˇlong line
 692            should not
 693            crash
 694            "})
 695            .await;
 696        cx.simulate_shared_keystrokes(["shift-v", "$", "x"]).await;
 697        cx.assert_state_matches().await;
 698    }
 699
 700    #[gpui::test]
 701    async fn test_visual_yank(cx: &mut gpui::TestAppContext) {
 702        let mut cx = NeovimBackedTestContext::new(cx).await;
 703
 704        cx.set_shared_state("The quick ˇbrown").await;
 705        cx.simulate_shared_keystrokes(["v", "w", "y"]).await;
 706        cx.assert_shared_state("The quick ˇbrown").await;
 707        cx.assert_shared_clipboard("brown").await;
 708
 709        cx.set_shared_state(indoc! {"
 710                The ˇquick brown
 711                fox jumps over
 712                the lazy dog"})
 713            .await;
 714        cx.simulate_shared_keystrokes(["v", "w", "j", "y"]).await;
 715        cx.assert_shared_state(indoc! {"
 716                    The ˇquick brown
 717                    fox jumps over
 718                    the lazy dog"})
 719            .await;
 720        cx.assert_shared_clipboard(indoc! {"
 721                quick brown
 722                fox jumps o"})
 723            .await;
 724
 725        cx.set_shared_state(indoc! {"
 726                    The quick brown
 727                    fox jumps over
 728                    the ˇlazy dog"})
 729            .await;
 730        cx.simulate_shared_keystrokes(["v", "w", "j", "y"]).await;
 731        cx.assert_shared_state(indoc! {"
 732                    The quick brown
 733                    fox jumps over
 734                    the ˇlazy dog"})
 735            .await;
 736        cx.assert_shared_clipboard("lazy d").await;
 737        cx.simulate_shared_keystrokes(["shift-v", "y"]).await;
 738        cx.assert_shared_clipboard("the lazy dog\n").await;
 739
 740        let mut cx = cx.binding(["v", "b", "k", "y"]);
 741        cx.set_shared_state(indoc! {"
 742                    The ˇquick brown
 743                    fox jumps over
 744                    the lazy dog"})
 745            .await;
 746        cx.simulate_shared_keystrokes(["v", "b", "k", "y"]).await;
 747        cx.assert_shared_state(indoc! {"
 748                    ˇThe quick brown
 749                    fox jumps over
 750                    the lazy dog"})
 751            .await;
 752        cx.assert_clipboard_content(Some("The q"));
 753
 754        cx.set_shared_state(indoc! {"
 755                    The quick brown
 756                    fox ˇjumps over
 757                    the lazy dog"})
 758            .await;
 759        cx.simulate_shared_keystrokes(["shift-v", "shift-g", "shift-y"])
 760            .await;
 761        cx.assert_shared_state(indoc! {"
 762                    The quick brown
 763                    ˇfox jumps over
 764                    the lazy dog"})
 765            .await;
 766        cx.assert_shared_clipboard("fox jumps over\nthe lazy dog\n")
 767            .await;
 768    }
 769
 770    #[gpui::test]
 771    async fn test_visual_block_mode(cx: &mut gpui::TestAppContext) {
 772        let mut cx = NeovimBackedTestContext::new(cx).await;
 773
 774        cx.set_shared_state(indoc! {
 775            "The ˇquick brown
 776             fox jumps over
 777             the lazy dog"
 778        })
 779        .await;
 780        cx.simulate_shared_keystrokes(["ctrl-v"]).await;
 781        cx.assert_shared_state(indoc! {
 782            "The «qˇ»uick brown
 783            fox jumps over
 784            the lazy dog"
 785        })
 786        .await;
 787        cx.simulate_shared_keystrokes(["2", "down"]).await;
 788        cx.assert_shared_state(indoc! {
 789            "The «qˇ»uick brown
 790            fox «jˇ»umps over
 791            the «lˇ»azy dog"
 792        })
 793        .await;
 794        cx.simulate_shared_keystrokes(["e"]).await;
 795        cx.assert_shared_state(indoc! {
 796            "The «quicˇ»k brown
 797            fox «jumpˇ»s over
 798            the «lazyˇ» dog"
 799        })
 800        .await;
 801        cx.simulate_shared_keystrokes(["^"]).await;
 802        cx.assert_shared_state(indoc! {
 803            "«ˇThe q»uick brown
 804            «ˇfox j»umps over
 805            «ˇthe l»azy dog"
 806        })
 807        .await;
 808        cx.simulate_shared_keystrokes(["$"]).await;
 809        cx.assert_shared_state(indoc! {
 810            "The «quick brownˇ»
 811            fox «jumps overˇ»
 812            the «lazy dogˇ»"
 813        })
 814        .await;
 815        cx.simulate_shared_keystrokes(["shift-f", " "]).await;
 816        cx.assert_shared_state(indoc! {
 817            "The «quickˇ» brown
 818            fox «jumpsˇ» over
 819            the «lazy ˇ»dog"
 820        })
 821        .await;
 822
 823        // toggling through visual mode works as expected
 824        cx.simulate_shared_keystrokes(["v"]).await;
 825        cx.assert_shared_state(indoc! {
 826            "The «quick brown
 827            fox jumps over
 828            the lazy ˇ»dog"
 829        })
 830        .await;
 831        cx.simulate_shared_keystrokes(["ctrl-v"]).await;
 832        cx.assert_shared_state(indoc! {
 833            "The «quickˇ» brown
 834            fox «jumpsˇ» over
 835            the «lazy ˇ»dog"
 836        })
 837        .await;
 838
 839        cx.set_shared_state(indoc! {
 840            "The ˇquick
 841             brown
 842             fox
 843             jumps over the
 844
 845             lazy dog
 846            "
 847        })
 848        .await;
 849        cx.simulate_shared_keystrokes(["ctrl-v", "down", "down"])
 850            .await;
 851        cx.assert_shared_state(indoc! {
 852            "The«ˇ q»uick
 853            bro«ˇwn»
 854            foxˇ
 855            jumps over the
 856
 857            lazy dog
 858            "
 859        })
 860        .await;
 861        cx.simulate_shared_keystrokes(["down"]).await;
 862        cx.assert_shared_state(indoc! {
 863            "The «qˇ»uick
 864            brow«nˇ»
 865            fox
 866            jump«sˇ» over the
 867
 868            lazy dog
 869            "
 870        })
 871        .await;
 872        cx.simulate_shared_keystroke("left").await;
 873        cx.assert_shared_state(indoc! {
 874            "The«ˇ q»uick
 875            bro«ˇwn»
 876            foxˇ
 877            jum«ˇps» over the
 878
 879            lazy dog
 880            "
 881        })
 882        .await;
 883        cx.simulate_shared_keystrokes(["s", "o", "escape"]).await;
 884        cx.assert_shared_state(indoc! {
 885            "Theˇouick
 886            broo
 887            foxo
 888            jumo over the
 889
 890            lazy dog
 891            "
 892        })
 893        .await;
 894
 895        //https://github.com/zed-industries/community/issues/1950
 896        cx.set_shared_state(indoc! {
 897            "Theˇ quick brown
 898
 899            fox jumps over
 900            the lazy dog
 901            "
 902        })
 903        .await;
 904        cx.simulate_shared_keystrokes(["l", "ctrl-v", "j", "j"])
 905            .await;
 906        cx.assert_shared_state(indoc! {
 907            "The «qˇ»uick brown
 908
 909            fox «jˇ»umps over
 910            the lazy dog
 911            "
 912        })
 913        .await;
 914    }
 915
 916    #[gpui::test]
 917    async fn test_visual_block_issue_2123(cx: &mut gpui::TestAppContext) {
 918        let mut cx = NeovimBackedTestContext::new(cx).await;
 919
 920        cx.set_shared_state(indoc! {
 921            "The ˇquick brown
 922            fox jumps over
 923            the lazy dog
 924            "
 925        })
 926        .await;
 927        cx.simulate_shared_keystrokes(["ctrl-v", "right", "down"])
 928            .await;
 929        cx.assert_shared_state(indoc! {
 930            "The «quˇ»ick brown
 931            fox «juˇ»mps over
 932            the lazy dog
 933            "
 934        })
 935        .await;
 936    }
 937
 938    #[gpui::test]
 939    async fn test_visual_block_insert(cx: &mut gpui::TestAppContext) {
 940        let mut cx = NeovimBackedTestContext::new(cx).await;
 941
 942        cx.set_shared_state(indoc! {
 943            "ˇThe quick brown
 944            fox jumps over
 945            the lazy dog
 946            "
 947        })
 948        .await;
 949        cx.simulate_shared_keystrokes(["ctrl-v", "9", "down"]).await;
 950        cx.assert_shared_state(indoc! {
 951            "«Tˇ»he quick brown
 952            «fˇ»ox jumps over
 953            «tˇ»he lazy dog
 954            ˇ"
 955        })
 956        .await;
 957
 958        cx.simulate_shared_keystrokes(["shift-i", "k", "escape"])
 959            .await;
 960        cx.assert_shared_state(indoc! {
 961            "ˇkThe quick brown
 962            kfox jumps over
 963            kthe lazy dog
 964            k"
 965        })
 966        .await;
 967
 968        cx.set_shared_state(indoc! {
 969            "ˇThe quick brown
 970            fox jumps over
 971            the lazy dog
 972            "
 973        })
 974        .await;
 975        cx.simulate_shared_keystrokes(["ctrl-v", "9", "down"]).await;
 976        cx.assert_shared_state(indoc! {
 977            "«Tˇ»he quick brown
 978            «fˇ»ox jumps over
 979            «tˇ»he lazy dog
 980            ˇ"
 981        })
 982        .await;
 983        cx.simulate_shared_keystrokes(["c", "k", "escape"]).await;
 984        cx.assert_shared_state(indoc! {
 985            "ˇkhe quick brown
 986            kox jumps over
 987            khe lazy dog
 988            k"
 989        })
 990        .await;
 991    }
 992
 993    #[gpui::test]
 994    async fn test_visual_object(cx: &mut gpui::TestAppContext) {
 995        let mut cx = NeovimBackedTestContext::new(cx).await;
 996
 997        cx.set_shared_state("hello (in [parˇens] o)").await;
 998        cx.simulate_shared_keystrokes(["ctrl-v", "l"]).await;
 999        cx.simulate_shared_keystrokes(["a", "]"]).await;
1000        cx.assert_shared_state("hello (in «[parens]ˇ» o)").await;
1001        assert_eq!(cx.mode(), Mode::Visual);
1002        cx.simulate_shared_keystrokes(["i", "("]).await;
1003        cx.assert_shared_state("hello («in [parens] oˇ»)").await;
1004
1005        cx.set_shared_state("hello in a wˇord again.").await;
1006        cx.simulate_shared_keystrokes(["ctrl-v", "l", "i", "w"])
1007            .await;
1008        cx.assert_shared_state("hello in a w«ordˇ» again.").await;
1009        assert_eq!(cx.mode(), Mode::VisualBlock);
1010        cx.simulate_shared_keystrokes(["o", "a", "s"]).await;
1011        cx.assert_shared_state("«ˇhello in a word» again.").await;
1012        assert_eq!(cx.mode(), Mode::Visual);
1013    }
1014
1015    #[gpui::test]
1016    async fn test_mode_across_command(cx: &mut gpui::TestAppContext) {
1017        let mut cx = VimTestContext::new(cx, true).await;
1018
1019        cx.set_state("aˇbc", Mode::Normal);
1020        cx.simulate_keystrokes(["ctrl-v"]);
1021        assert_eq!(cx.mode(), Mode::VisualBlock);
1022        cx.simulate_keystrokes(["cmd-shift-p", "escape"]);
1023        assert_eq!(cx.mode(), Mode::VisualBlock);
1024    }
1025}