visual.rs

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