visual.rs

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