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