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