visual.rs

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