visual.rs

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