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