visual.rs

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