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