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