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