visual.rs

   1use std::sync::Arc;
   2
   3use collections::HashMap;
   4use editor::{
   5    Bias, DisplayPoint, Editor, SelectionEffects,
   6    display_map::{DisplaySnapshot, ToDisplayPoint},
   7    movement,
   8};
   9use gpui::{Context, Window, actions};
  10use language::{Point, Selection, SelectionGoal};
  11use multi_buffer::MultiBufferRow;
  12use search::BufferSearchBar;
  13use util::ResultExt;
  14use workspace::searchable::Direction;
  15
  16use crate::{
  17    Vim,
  18    motion::{Motion, MotionKind, first_non_whitespace, next_line_end, start_of_line},
  19    object::Object,
  20    state::{Mark, Mode, Operator},
  21};
  22
  23actions!(
  24    vim,
  25    [
  26        ToggleVisual,
  27        ToggleVisualLine,
  28        ToggleVisualBlock,
  29        VisualDelete,
  30        VisualDeleteLine,
  31        VisualYank,
  32        VisualYankLine,
  33        OtherEnd,
  34        OtherEndRowAware,
  35        SelectNext,
  36        SelectPrevious,
  37        SelectNextMatch,
  38        SelectPreviousMatch,
  39        SelectSmallerSyntaxNode,
  40        SelectLargerSyntaxNode,
  41        RestoreVisualSelection,
  42        VisualInsertEndOfLine,
  43        VisualInsertFirstNonWhiteSpace,
  44    ]
  45);
  46
  47pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
  48    Vim::action(editor, cx, |vim, _: &ToggleVisual, window, cx| {
  49        vim.toggle_mode(Mode::Visual, window, cx)
  50    });
  51    Vim::action(editor, cx, |vim, _: &ToggleVisualLine, window, cx| {
  52        vim.toggle_mode(Mode::VisualLine, window, cx)
  53    });
  54    Vim::action(editor, cx, |vim, _: &ToggleVisualBlock, window, cx| {
  55        vim.toggle_mode(Mode::VisualBlock, window, cx)
  56    });
  57    Vim::action(editor, cx, Vim::other_end);
  58    Vim::action(editor, cx, Vim::other_end_row_aware);
  59    Vim::action(editor, cx, Vim::visual_insert_end_of_line);
  60    Vim::action(editor, cx, Vim::visual_insert_first_non_white_space);
  61    Vim::action(editor, cx, |vim, _: &VisualDelete, window, cx| {
  62        vim.record_current_action(cx);
  63        vim.visual_delete(false, window, cx);
  64    });
  65    Vim::action(editor, cx, |vim, _: &VisualDeleteLine, window, cx| {
  66        vim.record_current_action(cx);
  67        vim.visual_delete(true, window, cx);
  68    });
  69    Vim::action(editor, cx, |vim, _: &VisualYank, window, cx| {
  70        vim.visual_yank(false, window, cx)
  71    });
  72    Vim::action(editor, cx, |vim, _: &VisualYankLine, window, cx| {
  73        vim.visual_yank(true, window, cx)
  74    });
  75
  76    Vim::action(editor, cx, Vim::select_next);
  77    Vim::action(editor, cx, Vim::select_previous);
  78    Vim::action(editor, cx, |vim, _: &SelectNextMatch, window, cx| {
  79        vim.select_match(Direction::Next, window, cx);
  80    });
  81    Vim::action(editor, cx, |vim, _: &SelectPreviousMatch, window, cx| {
  82        vim.select_match(Direction::Prev, window, cx);
  83    });
  84
  85    Vim::action(editor, cx, |vim, _: &SelectLargerSyntaxNode, window, cx| {
  86        let count = Vim::take_count(cx).unwrap_or(1);
  87        Vim::take_forced_motion(cx);
  88        for _ in 0..count {
  89            vim.update_editor(window, cx, |_, editor, window, cx| {
  90                editor.select_larger_syntax_node(&Default::default(), window, cx);
  91            });
  92        }
  93    });
  94
  95    Vim::action(
  96        editor,
  97        cx,
  98        |vim, _: &SelectSmallerSyntaxNode, window, cx| {
  99            let count = Vim::take_count(cx).unwrap_or(1);
 100            Vim::take_forced_motion(cx);
 101            for _ in 0..count {
 102                vim.update_editor(window, cx, |_, editor, window, cx| {
 103                    editor.select_smaller_syntax_node(&Default::default(), window, cx);
 104                });
 105            }
 106        },
 107    );
 108
 109    Vim::action(editor, cx, |vim, _: &RestoreVisualSelection, window, cx| {
 110        let Some((stored_mode, reversed)) = vim.stored_visual_mode.take() else {
 111            return;
 112        };
 113        let marks = vim
 114            .update_editor(window, cx, |vim, editor, window, cx| {
 115                vim.get_mark("<", editor, window, cx)
 116                    .zip(vim.get_mark(">", editor, window, cx))
 117            })
 118            .flatten();
 119        let Some((Mark::Local(start), Mark::Local(end))) = marks else {
 120            return;
 121        };
 122        let ranges = start
 123            .iter()
 124            .zip(end)
 125            .zip(reversed)
 126            .map(|((start, end), reversed)| (*start, end, reversed))
 127            .collect::<Vec<_>>();
 128
 129        if vim.mode.is_visual() {
 130            vim.create_visual_marks(vim.mode, window, cx);
 131        }
 132
 133        vim.update_editor(window, cx, |_, editor, window, cx| {
 134            editor.set_clip_at_line_ends(false, cx);
 135            editor.change_selections(Default::default(), window, cx, |s| {
 136                let map = s.display_map();
 137                let ranges = ranges
 138                    .into_iter()
 139                    .map(|(start, end, reversed)| {
 140                        let mut new_end =
 141                            movement::saturating_right(&map, end.to_display_point(&map));
 142                        let mut new_start = start.to_display_point(&map);
 143                        if new_start >= new_end {
 144                            if new_end.column() == 0 {
 145                                new_end = movement::right(&map, new_end)
 146                            } else {
 147                                new_start = movement::saturating_left(&map, new_end);
 148                            }
 149                        }
 150                        Selection {
 151                            id: s.new_selection_id(),
 152                            start: new_start.to_point(&map),
 153                            end: new_end.to_point(&map),
 154                            reversed,
 155                            goal: SelectionGoal::None,
 156                        }
 157                    })
 158                    .collect();
 159                s.select(ranges);
 160            })
 161        });
 162        vim.switch_mode(stored_mode, true, window, cx)
 163    });
 164}
 165
 166impl Vim {
 167    pub fn visual_motion(
 168        &mut self,
 169        motion: Motion,
 170        times: Option<usize>,
 171        window: &mut Window,
 172        cx: &mut Context<Self>,
 173    ) {
 174        self.update_editor(window, cx, |vim, editor, window, cx| {
 175            let text_layout_details = editor.text_layout_details(window);
 176            if vim.mode == Mode::VisualBlock
 177                && !matches!(
 178                    motion,
 179                    Motion::EndOfLine {
 180                        display_lines: false
 181                    }
 182                )
 183            {
 184                let is_up_or_down = matches!(motion, Motion::Up { .. } | Motion::Down { .. });
 185                vim.visual_block_motion(is_up_or_down, editor, window, cx, |map, point, goal| {
 186                    motion.move_point(map, point, goal, times, &text_layout_details)
 187                })
 188            } else {
 189                editor.change_selections(Default::default(), window, cx, |s| {
 190                    s.move_with(|map, selection| {
 191                        let was_reversed = selection.reversed;
 192                        let mut current_head = selection.head();
 193
 194                        // our motions assume the current character is after the cursor,
 195                        // but in (forward) visual mode the current character is just
 196                        // before the end of the selection.
 197
 198                        // If the file ends with a newline (which is common) we don't do this.
 199                        // so that if you go to the end of such a file you can use "up" to go
 200                        // to the previous line and have it work somewhat as expected.
 201                        #[allow(clippy::nonminimal_bool)]
 202                        if !selection.reversed
 203                            && !selection.is_empty()
 204                            && !(selection.end.column() == 0 && selection.end == map.max_point())
 205                        {
 206                            current_head = movement::left(map, selection.end)
 207                        }
 208
 209                        let Some((new_head, goal)) = motion.move_point(
 210                            map,
 211                            current_head,
 212                            selection.goal,
 213                            times,
 214                            &text_layout_details,
 215                        ) else {
 216                            return;
 217                        };
 218
 219                        selection.set_head(new_head, goal);
 220
 221                        // ensure the current character is included in the selection.
 222                        if !selection.reversed {
 223                            let next_point = if vim.mode == Mode::VisualBlock {
 224                                movement::saturating_right(map, selection.end)
 225                            } else {
 226                                movement::right(map, selection.end)
 227                            };
 228
 229                            if !(next_point.column() == 0 && next_point == map.max_point()) {
 230                                selection.end = next_point;
 231                            }
 232                        }
 233
 234                        // vim always ensures the anchor character stays selected.
 235                        // if our selection has reversed, we need to move the opposite end
 236                        // to ensure the anchor is still selected.
 237                        if was_reversed && !selection.reversed {
 238                            selection.start = movement::left(map, selection.start);
 239                        } else if !was_reversed && selection.reversed {
 240                            selection.end = movement::right(map, selection.end);
 241                        }
 242                    })
 243                });
 244            }
 245        });
 246    }
 247
 248    pub fn visual_block_motion(
 249        &mut self,
 250        preserve_goal: bool,
 251        editor: &mut Editor,
 252        window: &mut Window,
 253        cx: &mut Context<Editor>,
 254        mut move_selection: impl FnMut(
 255            &DisplaySnapshot,
 256            DisplayPoint,
 257            SelectionGoal,
 258        ) -> Option<(DisplayPoint, SelectionGoal)>,
 259    ) {
 260        let text_layout_details = editor.text_layout_details(window);
 261        editor.change_selections(Default::default(), window, cx, |s| {
 262            let map = &s.display_map();
 263            let mut head = s.newest_anchor().head().to_display_point(map);
 264            let mut tail = s.oldest_anchor().tail().to_display_point(map);
 265
 266            let mut head_x = map.x_for_display_point(head, &text_layout_details);
 267            let mut tail_x = map.x_for_display_point(tail, &text_layout_details);
 268
 269            let (start, end) = match s.newest_anchor().goal {
 270                SelectionGoal::HorizontalRange { start, end } if preserve_goal => (start, end),
 271                SelectionGoal::HorizontalPosition(start) if preserve_goal => (start, start),
 272                _ => (tail_x.0, head_x.0),
 273            };
 274            let mut goal = SelectionGoal::HorizontalRange { start, end };
 275
 276            let was_reversed = tail_x > head_x;
 277            if !was_reversed && !preserve_goal {
 278                head = movement::saturating_left(map, head);
 279            }
 280
 281            let reverse_aware_goal = if was_reversed {
 282                SelectionGoal::HorizontalRange {
 283                    start: end,
 284                    end: start,
 285                }
 286            } else {
 287                goal
 288            };
 289
 290            let Some((new_head, _)) = move_selection(map, head, reverse_aware_goal) else {
 291                return;
 292            };
 293            head = new_head;
 294            head_x = map.x_for_display_point(head, &text_layout_details);
 295
 296            let is_reversed = tail_x > head_x;
 297            if was_reversed && !is_reversed {
 298                tail = movement::saturating_left(map, tail);
 299                tail_x = map.x_for_display_point(tail, &text_layout_details);
 300            } else if !was_reversed && is_reversed {
 301                tail = movement::saturating_right(map, tail);
 302                tail_x = map.x_for_display_point(tail, &text_layout_details);
 303            }
 304            if !is_reversed && !preserve_goal {
 305                head = movement::saturating_right(map, head);
 306                head_x = map.x_for_display_point(head, &text_layout_details);
 307            }
 308
 309            let positions = if is_reversed {
 310                head_x..tail_x
 311            } else {
 312                tail_x..head_x
 313            };
 314
 315            if !preserve_goal {
 316                goal = SelectionGoal::HorizontalRange {
 317                    start: positions.start.0,
 318                    end: positions.end.0,
 319                };
 320            }
 321
 322            let mut selections = Vec::new();
 323            let mut row = tail.row();
 324
 325            loop {
 326                let laid_out_line = map.layout_row(row, &text_layout_details);
 327                let start = DisplayPoint::new(
 328                    row,
 329                    laid_out_line.closest_index_for_x(positions.start) as u32,
 330                );
 331                let mut end =
 332                    DisplayPoint::new(row, laid_out_line.closest_index_for_x(positions.end) as u32);
 333                if end <= start {
 334                    if start.column() == map.line_len(start.row()) {
 335                        end = start;
 336                    } else {
 337                        end = movement::saturating_right(map, start);
 338                    }
 339                }
 340
 341                if positions.start <= laid_out_line.width {
 342                    let selection = Selection {
 343                        id: s.new_selection_id(),
 344                        start: start.to_point(map),
 345                        end: end.to_point(map),
 346                        reversed: is_reversed &&
 347                                    // For neovim parity: cursor is not reversed when column is a single character
 348                                    end.column() - start.column() > 1,
 349                        goal,
 350                    };
 351
 352                    selections.push(selection);
 353                }
 354                if row == head.row() {
 355                    break;
 356                }
 357                if tail.row() > head.row() {
 358                    row.0 -= 1
 359                } else {
 360                    row.0 += 1
 361                }
 362            }
 363            s.select(selections);
 364        })
 365    }
 366
 367    pub fn visual_object(
 368        &mut self,
 369        object: Object,
 370        count: Option<usize>,
 371        window: &mut Window,
 372        cx: &mut Context<Vim>,
 373    ) {
 374        if let Some(Operator::Object { around }) = self.active_operator() {
 375            self.pop_operator(window, cx);
 376            let current_mode = self.mode;
 377            let target_mode = object.target_visual_mode(current_mode, around);
 378            if target_mode != current_mode {
 379                self.switch_mode(target_mode, true, window, cx);
 380            }
 381
 382            self.update_editor(window, cx, |_, editor, window, cx| {
 383                editor.change_selections(Default::default(), window, cx, |s| {
 384                    s.move_with(|map, selection| {
 385                        let mut mut_selection = selection.clone();
 386
 387                        // all our motions assume that the current character is
 388                        // after the cursor; however in the case of a visual selection
 389                        // the current character is before the cursor.
 390                        // But this will affect the judgment of the html tag
 391                        // so the html tag needs to skip this logic.
 392                        if !selection.reversed && object != Object::Tag {
 393                            mut_selection.set_head(
 394                                movement::left(map, mut_selection.head()),
 395                                mut_selection.goal,
 396                            );
 397                        }
 398
 399                        if let Some(range) = object.range(map, mut_selection, around, count) {
 400                            if !range.is_empty() {
 401                                let expand_both_ways = object.always_expands_both_ways()
 402                                    || selection.is_empty()
 403                                    || movement::right(map, selection.start) == selection.end;
 404
 405                                if expand_both_ways {
 406                                    if selection.start == range.start
 407                                        && selection.end == range.end
 408                                        && object.always_expands_both_ways()
 409                                    {
 410                                        if let Some(range) =
 411                                            object.range(map, selection.clone(), around, count)
 412                                        {
 413                                            selection.start = range.start;
 414                                            selection.end = range.end;
 415                                        }
 416                                    } else {
 417                                        selection.start = range.start;
 418                                        selection.end = range.end;
 419                                    }
 420                                } else if selection.reversed {
 421                                    selection.start = range.start;
 422                                } else {
 423                                    selection.end = range.end;
 424                                }
 425                            }
 426
 427                            // In the visual selection result of a paragraph object, the cursor is
 428                            // placed at the start of the last line. And in the visual mode, the
 429                            // selection end is located after the end character. So, adjustment of
 430                            // selection end is needed.
 431                            //
 432                            // We don't do this adjustment for a one-line blank paragraph since the
 433                            // trailing newline is included in its selection from the beginning.
 434                            if object == Object::Paragraph && range.start != range.end {
 435                                let row_of_selection_end_line = selection.end.to_point(map).row;
 436                                let new_selection_end = if map
 437                                    .buffer_snapshot
 438                                    .line_len(MultiBufferRow(row_of_selection_end_line))
 439                                    == 0
 440                                {
 441                                    Point::new(row_of_selection_end_line + 1, 0)
 442                                } else {
 443                                    Point::new(row_of_selection_end_line, 1)
 444                                };
 445                                selection.end = new_selection_end.to_display_point(map);
 446                            }
 447                        }
 448                    });
 449                });
 450            });
 451        }
 452    }
 453
 454    fn visual_insert_end_of_line(
 455        &mut self,
 456        _: &VisualInsertEndOfLine,
 457        window: &mut Window,
 458        cx: &mut Context<Self>,
 459    ) {
 460        self.update_editor(window, cx, |_, editor, window, cx| {
 461            editor.split_selection_into_lines(&Default::default(), window, cx);
 462            editor.change_selections(Default::default(), window, cx, |s| {
 463                s.move_cursors_with(|map, cursor, _| {
 464                    (next_line_end(map, cursor, 1), SelectionGoal::None)
 465                });
 466            });
 467        });
 468
 469        self.switch_mode(Mode::Insert, false, window, cx);
 470    }
 471
 472    fn visual_insert_first_non_white_space(
 473        &mut self,
 474        _: &VisualInsertFirstNonWhiteSpace,
 475        window: &mut Window,
 476        cx: &mut Context<Self>,
 477    ) {
 478        self.update_editor(window, cx, |_, editor, window, cx| {
 479            editor.split_selection_into_lines(&Default::default(), window, cx);
 480            editor.change_selections(Default::default(), window, cx, |s| {
 481                s.move_cursors_with(|map, cursor, _| {
 482                    (
 483                        first_non_whitespace(map, false, cursor),
 484                        SelectionGoal::None,
 485                    )
 486                });
 487            });
 488        });
 489
 490        self.switch_mode(Mode::Insert, false, window, cx);
 491    }
 492
 493    fn toggle_mode(&mut self, mode: Mode, window: &mut Window, cx: &mut Context<Self>) {
 494        if self.mode == mode {
 495            self.switch_mode(Mode::Normal, false, window, cx);
 496        } else {
 497            self.switch_mode(mode, false, window, cx);
 498        }
 499    }
 500
 501    pub fn other_end(&mut self, _: &OtherEnd, window: &mut Window, cx: &mut Context<Self>) {
 502        self.update_editor(window, cx, |_, editor, window, cx| {
 503            editor.change_selections(Default::default(), window, cx, |s| {
 504                s.move_with(|_, selection| {
 505                    selection.reversed = !selection.reversed;
 506                });
 507            })
 508        });
 509    }
 510
 511    pub fn other_end_row_aware(
 512        &mut self,
 513        _: &OtherEndRowAware,
 514        window: &mut Window,
 515        cx: &mut Context<Self>,
 516    ) {
 517        let mode = self.mode;
 518        self.update_editor(window, cx, |_, editor, window, cx| {
 519            editor.change_selections(Default::default(), window, cx, |s| {
 520                s.move_with(|_, selection| {
 521                    selection.reversed = !selection.reversed;
 522                });
 523                if mode == Mode::VisualBlock {
 524                    s.reverse_selections();
 525                }
 526            })
 527        });
 528    }
 529
 530    pub fn visual_delete(&mut self, line_mode: bool, window: &mut Window, cx: &mut Context<Self>) {
 531        self.store_visual_marks(window, cx);
 532        self.update_editor(window, cx, |vim, editor, window, cx| {
 533            let mut original_columns: HashMap<_, _> = Default::default();
 534            let line_mode = line_mode || editor.selections.line_mode;
 535            editor.selections.line_mode = false;
 536
 537            editor.transact(window, cx, |editor, window, cx| {
 538                editor.change_selections(Default::default(), window, cx, |s| {
 539                    s.move_with(|map, selection| {
 540                        if line_mode {
 541                            let mut position = selection.head();
 542                            if !selection.reversed {
 543                                position = movement::left(map, position);
 544                            }
 545                            original_columns.insert(selection.id, position.to_point(map).column);
 546                            if vim.mode == Mode::VisualBlock {
 547                                *selection.end.column_mut() = map.line_len(selection.end.row())
 548                            } else {
 549                                let start = selection.start.to_point(map);
 550                                let end = selection.end.to_point(map);
 551                                selection.start = map.prev_line_boundary(start).1;
 552                                if end.column == 0 && end > start {
 553                                    let row = end.row.saturating_sub(1);
 554                                    selection.end = Point::new(
 555                                        row,
 556                                        map.buffer_snapshot.line_len(MultiBufferRow(row)),
 557                                    )
 558                                    .to_display_point(map)
 559                                } else {
 560                                    selection.end = map.next_line_boundary(end).1;
 561                                }
 562                            }
 563                        }
 564                        selection.goal = SelectionGoal::None;
 565                    });
 566                });
 567                let kind = if line_mode {
 568                    MotionKind::Linewise
 569                } else {
 570                    MotionKind::Exclusive
 571                };
 572                vim.copy_selections_content(editor, kind, window, cx);
 573
 574                if line_mode && vim.mode != Mode::VisualBlock {
 575                    editor.change_selections(Default::default(), window, cx, |s| {
 576                        s.move_with(|map, selection| {
 577                            let end = selection.end.to_point(map);
 578                            let start = selection.start.to_point(map);
 579                            if end.row < map.buffer_snapshot.max_point().row {
 580                                selection.end = Point::new(end.row + 1, 0).to_display_point(map)
 581                            } else if start.row > 0 {
 582                                selection.start = Point::new(
 583                                    start.row - 1,
 584                                    map.buffer_snapshot.line_len(MultiBufferRow(start.row - 1)),
 585                                )
 586                                .to_display_point(map)
 587                            }
 588                        });
 589                    });
 590                }
 591                editor.insert("", window, cx);
 592
 593                // Fixup cursor position after the deletion
 594                editor.set_clip_at_line_ends(true, cx);
 595                editor.change_selections(Default::default(), window, cx, |s| {
 596                    s.move_with(|map, selection| {
 597                        let mut cursor = selection.head().to_point(map);
 598
 599                        if let Some(column) = original_columns.get(&selection.id) {
 600                            cursor.column = *column
 601                        }
 602                        let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left);
 603                        selection.collapse_to(cursor, selection.goal)
 604                    });
 605                    if vim.mode == Mode::VisualBlock {
 606                        s.select_anchors(vec![s.first_anchor()])
 607                    }
 608                });
 609            })
 610        });
 611        self.switch_mode(Mode::Normal, true, window, cx);
 612    }
 613
 614    pub fn visual_yank(&mut self, line_mode: bool, window: &mut Window, cx: &mut Context<Self>) {
 615        self.store_visual_marks(window, cx);
 616        self.update_editor(window, cx, |vim, editor, window, cx| {
 617            let line_mode = line_mode || editor.selections.line_mode;
 618
 619            // For visual line mode, adjust selections to avoid yanking the next line when on \n
 620            if line_mode && vim.mode != Mode::VisualBlock {
 621                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
 622                    s.move_with(|map, selection| {
 623                        let start = selection.start.to_point(map);
 624                        let end = selection.end.to_point(map);
 625                        if end.column == 0 && end > start {
 626                            let row = end.row.saturating_sub(1);
 627                            selection.end =
 628                                Point::new(row, map.buffer_snapshot.line_len(MultiBufferRow(row)))
 629                                    .to_display_point(map);
 630                        }
 631                    });
 632                });
 633            }
 634
 635            editor.selections.line_mode = line_mode;
 636            let kind = if line_mode {
 637                MotionKind::Linewise
 638            } else {
 639                MotionKind::Exclusive
 640            };
 641            vim.yank_selections_content(editor, kind, window, cx);
 642            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
 643                s.move_with(|map, selection| {
 644                    if line_mode {
 645                        selection.start = start_of_line(map, false, selection.start);
 646                    };
 647                    selection.collapse_to(selection.start, SelectionGoal::None)
 648                });
 649                if vim.mode == Mode::VisualBlock {
 650                    s.select_anchors(vec![s.first_anchor()])
 651                }
 652            });
 653        });
 654        self.switch_mode(Mode::Normal, true, window, cx);
 655    }
 656
 657    pub(crate) fn visual_replace(
 658        &mut self,
 659        text: Arc<str>,
 660        window: &mut Window,
 661        cx: &mut Context<Self>,
 662    ) {
 663        self.stop_recording(cx);
 664        self.update_editor(window, cx, |_, editor, window, cx| {
 665            editor.transact(window, cx, |editor, window, cx| {
 666                let (display_map, selections) = editor.selections.all_adjusted_display(cx);
 667
 668                // Selections are biased right at the start. So we need to store
 669                // anchors that are biased left so that we can restore the selections
 670                // after the change
 671                let stable_anchors = editor
 672                    .selections
 673                    .disjoint_anchors()
 674                    .iter()
 675                    .map(|selection| {
 676                        let start = selection.start.bias_left(&display_map.buffer_snapshot);
 677                        start..start
 678                    })
 679                    .collect::<Vec<_>>();
 680
 681                let mut edits = Vec::new();
 682                for selection in selections.iter() {
 683                    let selection = selection.clone();
 684                    for row_range in
 685                        movement::split_display_range_by_lines(&display_map, selection.range())
 686                    {
 687                        let range = row_range.start.to_offset(&display_map, Bias::Right)
 688                            ..row_range.end.to_offset(&display_map, Bias::Right);
 689                        let text = text.repeat(range.len());
 690                        edits.push((range, text));
 691                    }
 692                }
 693
 694                editor.edit(edits, cx);
 695                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
 696                    s.select_ranges(stable_anchors)
 697                });
 698            });
 699        });
 700        self.switch_mode(Mode::Normal, false, window, cx);
 701    }
 702
 703    pub fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
 704        Vim::take_forced_motion(cx);
 705        let count =
 706            Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 });
 707        self.update_editor(window, cx, |_, editor, window, cx| {
 708            editor.set_clip_at_line_ends(false, cx);
 709            for _ in 0..count {
 710                if editor
 711                    .select_next(&Default::default(), window, cx)
 712                    .log_err()
 713                    .is_none()
 714                {
 715                    break;
 716                }
 717            }
 718        });
 719    }
 720
 721    pub fn select_previous(
 722        &mut self,
 723        _: &SelectPrevious,
 724        window: &mut Window,
 725        cx: &mut Context<Self>,
 726    ) {
 727        Vim::take_forced_motion(cx);
 728        let count =
 729            Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 });
 730        self.update_editor(window, cx, |_, editor, window, cx| {
 731            for _ in 0..count {
 732                if editor
 733                    .select_previous(&Default::default(), window, cx)
 734                    .log_err()
 735                    .is_none()
 736                {
 737                    break;
 738                }
 739            }
 740        });
 741    }
 742
 743    pub fn select_match(
 744        &mut self,
 745        direction: Direction,
 746        window: &mut Window,
 747        cx: &mut Context<Self>,
 748    ) {
 749        Vim::take_forced_motion(cx);
 750        let count = Vim::take_count(cx).unwrap_or(1);
 751        let Some(pane) = self.pane(window, cx) else {
 752            return;
 753        };
 754        let vim_is_normal = self.mode == Mode::Normal;
 755        let mut start_selection = 0usize;
 756        let mut end_selection = 0usize;
 757
 758        self.update_editor(window, cx, |_, editor, _, _| {
 759            editor.set_collapse_matches(false);
 760        });
 761        if vim_is_normal {
 762            pane.update(cx, |pane, cx| {
 763                if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()
 764                {
 765                    search_bar.update(cx, |search_bar, cx| {
 766                        if !search_bar.has_active_match() || !search_bar.show(window, cx) {
 767                            return;
 768                        }
 769                        // without update_match_index there is a bug when the cursor is before the first match
 770                        search_bar.update_match_index(window, cx);
 771                        search_bar.select_match(direction.opposite(), 1, window, cx);
 772                    });
 773                }
 774            });
 775        }
 776        self.update_editor(window, cx, |_, editor, _, cx| {
 777            let latest = editor.selections.newest::<usize>(cx);
 778            start_selection = latest.start;
 779            end_selection = latest.end;
 780        });
 781
 782        let mut match_exists = false;
 783        pane.update(cx, |pane, cx| {
 784            if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 785                search_bar.update(cx, |search_bar, cx| {
 786                    search_bar.update_match_index(window, cx);
 787                    search_bar.select_match(direction, count, window, cx);
 788                    match_exists = search_bar.match_exists(window, cx);
 789                });
 790            }
 791        });
 792        if !match_exists {
 793            self.clear_operator(window, cx);
 794            self.stop_replaying(cx);
 795            return;
 796        }
 797        self.update_editor(window, cx, |_, editor, window, cx| {
 798            let latest = editor.selections.newest::<usize>(cx);
 799            if vim_is_normal {
 800                start_selection = latest.start;
 801                end_selection = latest.end;
 802            } else {
 803                start_selection = start_selection.min(latest.start);
 804                end_selection = end_selection.max(latest.end);
 805            }
 806            if direction == Direction::Prev {
 807                std::mem::swap(&mut start_selection, &mut end_selection);
 808            }
 809            editor.change_selections(Default::default(), window, cx, |s| {
 810                s.select_ranges([start_selection..end_selection]);
 811            });
 812            editor.set_collapse_matches(true);
 813        });
 814
 815        match self.maybe_pop_operator() {
 816            Some(Operator::Change) => self.substitute(None, false, window, cx),
 817            Some(Operator::Delete) => {
 818                self.stop_recording(cx);
 819                self.visual_delete(false, window, cx)
 820            }
 821            Some(Operator::Yank) => self.visual_yank(false, window, cx),
 822            _ => {} // Ignoring other operators
 823        }
 824    }
 825}
 826#[cfg(test)]
 827mod test {
 828    use indoc::indoc;
 829    use workspace::item::Item;
 830
 831    use crate::{
 832        state::Mode,
 833        test::{NeovimBackedTestContext, VimTestContext},
 834    };
 835
 836    #[gpui::test]
 837    async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
 838        let mut cx = NeovimBackedTestContext::new(cx).await;
 839
 840        cx.set_shared_state(indoc! {
 841            "The ˇquick brown
 842            fox jumps over
 843            the lazy dog"
 844        })
 845        .await;
 846        let cursor = cx.update_editor(|editor, _, cx| editor.pixel_position_of_cursor(cx));
 847
 848        // entering visual mode should select the character
 849        // under cursor
 850        cx.simulate_shared_keystrokes("v").await;
 851        cx.shared_state()
 852            .await
 853            .assert_eq(indoc! { "The «qˇ»uick brown
 854            fox jumps over
 855            the lazy dog"});
 856        cx.update_editor(|editor, _, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
 857
 858        // forwards motions should extend the selection
 859        cx.simulate_shared_keystrokes("w j").await;
 860        cx.shared_state().await.assert_eq(indoc! { "The «quick brown
 861            fox jumps oˇ»ver
 862            the lazy dog"});
 863
 864        cx.simulate_shared_keystrokes("escape").await;
 865        cx.shared_state().await.assert_eq(indoc! { "The quick brown
 866            fox jumps ˇover
 867            the lazy dog"});
 868
 869        // motions work backwards
 870        cx.simulate_shared_keystrokes("v k b").await;
 871        cx.shared_state()
 872            .await
 873            .assert_eq(indoc! { "The «ˇquick brown
 874            fox jumps o»ver
 875            the lazy dog"});
 876
 877        // works on empty lines
 878        cx.set_shared_state(indoc! {"
 879            a
 880            ˇ
 881            b
 882            "})
 883            .await;
 884        let cursor = cx.update_editor(|editor, _, cx| editor.pixel_position_of_cursor(cx));
 885        cx.simulate_shared_keystrokes("v").await;
 886        cx.shared_state().await.assert_eq(indoc! {"
 887            a
 888            «
 889            ˇ»b
 890        "});
 891        cx.update_editor(|editor, _, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
 892
 893        // toggles off again
 894        cx.simulate_shared_keystrokes("v").await;
 895        cx.shared_state().await.assert_eq(indoc! {"
 896            a
 897            ˇ
 898            b
 899            "});
 900
 901        // works at the end of a document
 902        cx.set_shared_state(indoc! {"
 903            a
 904            b
 905            ˇ"})
 906            .await;
 907
 908        cx.simulate_shared_keystrokes("v").await;
 909        cx.shared_state().await.assert_eq(indoc! {"
 910            a
 911            b
 912            ˇ"});
 913    }
 914
 915    #[gpui::test]
 916    async fn test_visual_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
 917        let mut cx = VimTestContext::new(cx, true).await;
 918
 919        cx.set_state(
 920            indoc! {
 921                "«The quick brown
 922                fox jumps over
 923                the lazy dogˇ»"
 924            },
 925            Mode::Visual,
 926        );
 927        cx.simulate_keystrokes("g shift-i");
 928        cx.assert_state(
 929            indoc! {
 930                "ˇThe quick brown
 931                ˇfox jumps over
 932                ˇthe lazy dog"
 933            },
 934            Mode::Insert,
 935        );
 936    }
 937
 938    #[gpui::test]
 939    async fn test_visual_insert_end_of_line(cx: &mut gpui::TestAppContext) {
 940        let mut cx = VimTestContext::new(cx, true).await;
 941
 942        cx.set_state(
 943            indoc! {
 944                "«The quick brown
 945                fox jumps over
 946                the lazy dogˇ»"
 947            },
 948            Mode::Visual,
 949        );
 950        cx.simulate_keystrokes("g shift-a");
 951        cx.assert_state(
 952            indoc! {
 953                "The quick brownˇ
 954                fox jumps overˇ
 955                the lazy dogˇ"
 956            },
 957            Mode::Insert,
 958        );
 959    }
 960
 961    #[gpui::test]
 962    async fn test_enter_visual_line_mode(cx: &mut gpui::TestAppContext) {
 963        let mut cx = NeovimBackedTestContext::new(cx).await;
 964
 965        cx.set_shared_state(indoc! {
 966            "The ˇquick brown
 967            fox jumps over
 968            the lazy dog"
 969        })
 970        .await;
 971        cx.simulate_shared_keystrokes("shift-v").await;
 972        cx.shared_state()
 973            .await
 974            .assert_eq(indoc! { "The «qˇ»uick brown
 975            fox jumps over
 976            the lazy dog"});
 977        cx.simulate_shared_keystrokes("x").await;
 978        cx.shared_state().await.assert_eq(indoc! { "fox ˇjumps over
 979        the lazy dog"});
 980
 981        // it should work on empty lines
 982        cx.set_shared_state(indoc! {"
 983            a
 984            ˇ
 985            b"})
 986            .await;
 987        cx.simulate_shared_keystrokes("shift-v").await;
 988        cx.shared_state().await.assert_eq(indoc! {"
 989            a
 990            «
 991            ˇ»b"});
 992        cx.simulate_shared_keystrokes("x").await;
 993        cx.shared_state().await.assert_eq(indoc! {"
 994            a
 995            ˇb"});
 996
 997        // it should work at the end of the document
 998        cx.set_shared_state(indoc! {"
 999            a
1000            b
1001            ˇ"})
1002            .await;
1003        let cursor = cx.update_editor(|editor, _, cx| editor.pixel_position_of_cursor(cx));
1004        cx.simulate_shared_keystrokes("shift-v").await;
1005        cx.shared_state().await.assert_eq(indoc! {"
1006            a
1007            b
1008            ˇ"});
1009        cx.update_editor(|editor, _, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
1010        cx.simulate_shared_keystrokes("x").await;
1011        cx.shared_state().await.assert_eq(indoc! {"
1012            a
1013            ˇb"});
1014    }
1015
1016    #[gpui::test]
1017    async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
1018        let mut cx = NeovimBackedTestContext::new(cx).await;
1019
1020        cx.simulate("v w", "The quick ˇbrown")
1021            .await
1022            .assert_matches();
1023
1024        cx.simulate("v w x", "The quick ˇbrown")
1025            .await
1026            .assert_matches();
1027        cx.simulate(
1028            "v w j x",
1029            indoc! {"
1030                The ˇquick brown
1031                fox jumps over
1032                the lazy dog"},
1033        )
1034        .await
1035        .assert_matches();
1036        // Test pasting code copied on delete
1037        cx.simulate_shared_keystrokes("j p").await;
1038        cx.shared_state().await.assert_matches();
1039
1040        cx.simulate_at_each_offset(
1041            "v w j x",
1042            indoc! {"
1043                The ˇquick brown
1044                fox jumps over
1045                the ˇlazy dog"},
1046        )
1047        .await
1048        .assert_matches();
1049        cx.simulate_at_each_offset(
1050            "v b k x",
1051            indoc! {"
1052                The ˇquick brown
1053                fox jumps ˇover
1054                the ˇlazy dog"},
1055        )
1056        .await
1057        .assert_matches();
1058    }
1059
1060    #[gpui::test]
1061    async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
1062        let mut cx = NeovimBackedTestContext::new(cx).await;
1063
1064        cx.set_shared_state(indoc! {"
1065                The quˇick brown
1066                fox jumps over
1067                the lazy dog"})
1068            .await;
1069        cx.simulate_shared_keystrokes("shift-v x").await;
1070        cx.shared_state().await.assert_matches();
1071
1072        // Test pasting code copied on delete
1073        cx.simulate_shared_keystrokes("p").await;
1074        cx.shared_state().await.assert_matches();
1075
1076        cx.set_shared_state(indoc! {"
1077                The quick brown
1078                fox jumps over
1079                the laˇzy dog"})
1080            .await;
1081        cx.simulate_shared_keystrokes("shift-v x").await;
1082        cx.shared_state().await.assert_matches();
1083        cx.shared_clipboard().await.assert_eq("the lazy dog\n");
1084
1085        cx.set_shared_state(indoc! {"
1086                                The quˇick brown
1087                                fox jumps over
1088                                the lazy dog"})
1089            .await;
1090        cx.simulate_shared_keystrokes("shift-v j x").await;
1091        cx.shared_state().await.assert_matches();
1092        // Test pasting code copied on delete
1093        cx.simulate_shared_keystrokes("p").await;
1094        cx.shared_state().await.assert_matches();
1095
1096        cx.set_shared_state(indoc! {"
1097            The ˇlong line
1098            should not
1099            crash
1100            "})
1101            .await;
1102        cx.simulate_shared_keystrokes("shift-v $ x").await;
1103        cx.shared_state().await.assert_matches();
1104    }
1105
1106    #[gpui::test]
1107    async fn test_visual_yank(cx: &mut gpui::TestAppContext) {
1108        let mut cx = NeovimBackedTestContext::new(cx).await;
1109
1110        cx.set_shared_state("The quick ˇbrown").await;
1111        cx.simulate_shared_keystrokes("v w y").await;
1112        cx.shared_state().await.assert_eq("The quick ˇbrown");
1113        cx.shared_clipboard().await.assert_eq("brown");
1114
1115        cx.set_shared_state(indoc! {"
1116                The ˇquick brown
1117                fox jumps over
1118                the lazy dog"})
1119            .await;
1120        cx.simulate_shared_keystrokes("v w j y").await;
1121        cx.shared_state().await.assert_eq(indoc! {"
1122                    The ˇquick brown
1123                    fox jumps over
1124                    the lazy dog"});
1125        cx.shared_clipboard().await.assert_eq(indoc! {"
1126                quick brown
1127                fox jumps o"});
1128
1129        cx.set_shared_state(indoc! {"
1130                    The quick brown
1131                    fox jumps over
1132                    the ˇlazy dog"})
1133            .await;
1134        cx.simulate_shared_keystrokes("v w j y").await;
1135        cx.shared_state().await.assert_eq(indoc! {"
1136                    The quick brown
1137                    fox jumps over
1138                    the ˇlazy dog"});
1139        cx.shared_clipboard().await.assert_eq("lazy d");
1140        cx.simulate_shared_keystrokes("shift-v y").await;
1141        cx.shared_clipboard().await.assert_eq("the lazy dog\n");
1142
1143        cx.set_shared_state(indoc! {"
1144                    The ˇquick brown
1145                    fox jumps over
1146                    the lazy dog"})
1147            .await;
1148        cx.simulate_shared_keystrokes("v b k y").await;
1149        cx.shared_state().await.assert_eq(indoc! {"
1150                    ˇThe quick brown
1151                    fox jumps over
1152                    the lazy dog"});
1153        assert_eq!(
1154            cx.read_from_clipboard()
1155                .map(|item| item.text().unwrap().to_string())
1156                .unwrap(),
1157            "The q"
1158        );
1159
1160        cx.set_shared_state(indoc! {"
1161                    The quick brown
1162                    fox ˇjumps over
1163                    the lazy dog"})
1164            .await;
1165        cx.simulate_shared_keystrokes("shift-v shift-g shift-y")
1166            .await;
1167        cx.shared_state().await.assert_eq(indoc! {"
1168                    The quick brown
1169                    ˇfox jumps over
1170                    the lazy dog"});
1171        cx.shared_clipboard()
1172            .await
1173            .assert_eq("fox jumps over\nthe lazy dog\n");
1174
1175        cx.set_shared_state(indoc! {"
1176                    The quick brown
1177                    fox ˇjumps over
1178                    the lazy dog"})
1179            .await;
1180        cx.simulate_shared_keystrokes("shift-v $ shift-y").await;
1181        cx.shared_state().await.assert_eq(indoc! {"
1182                    The quick brown
1183                    ˇfox jumps over
1184                    the lazy dog"});
1185        cx.shared_clipboard().await.assert_eq("fox jumps over\n");
1186    }
1187
1188    #[gpui::test]
1189    async fn test_visual_block_mode(cx: &mut gpui::TestAppContext) {
1190        let mut cx = NeovimBackedTestContext::new(cx).await;
1191
1192        cx.set_shared_state(indoc! {
1193            "The ˇquick brown
1194             fox jumps over
1195             the lazy dog"
1196        })
1197        .await;
1198        cx.simulate_shared_keystrokes("ctrl-v").await;
1199        cx.shared_state().await.assert_eq(indoc! {
1200            "The «qˇ»uick brown
1201            fox jumps over
1202            the lazy dog"
1203        });
1204        cx.simulate_shared_keystrokes("2 down").await;
1205        cx.shared_state().await.assert_eq(indoc! {
1206            "The «qˇ»uick brown
1207            fox «jˇ»umps over
1208            the «lˇ»azy dog"
1209        });
1210        cx.simulate_shared_keystrokes("e").await;
1211        cx.shared_state().await.assert_eq(indoc! {
1212            "The «quicˇ»k brown
1213            fox «jumpˇ»s over
1214            the «lazyˇ» dog"
1215        });
1216        cx.simulate_shared_keystrokes("^").await;
1217        cx.shared_state().await.assert_eq(indoc! {
1218            "«ˇThe q»uick brown
1219            «ˇfox j»umps over
1220            «ˇthe l»azy dog"
1221        });
1222        cx.simulate_shared_keystrokes("$").await;
1223        cx.shared_state().await.assert_eq(indoc! {
1224            "The «quick brownˇ»
1225            fox «jumps overˇ»
1226            the «lazy dogˇ»"
1227        });
1228        cx.simulate_shared_keystrokes("shift-f space").await;
1229        cx.shared_state().await.assert_eq(indoc! {
1230            "The «quickˇ» brown
1231            fox «jumpsˇ» over
1232            the «lazy ˇ»dog"
1233        });
1234
1235        // toggling through visual mode works as expected
1236        cx.simulate_shared_keystrokes("v").await;
1237        cx.shared_state().await.assert_eq(indoc! {
1238            "The «quick brown
1239            fox jumps over
1240            the lazy ˇ»dog"
1241        });
1242        cx.simulate_shared_keystrokes("ctrl-v").await;
1243        cx.shared_state().await.assert_eq(indoc! {
1244            "The «quickˇ» brown
1245            fox «jumpsˇ» over
1246            the «lazy ˇ»dog"
1247        });
1248
1249        cx.set_shared_state(indoc! {
1250            "The ˇquick
1251             brown
1252             fox
1253             jumps over the
1254
1255             lazy dog
1256            "
1257        })
1258        .await;
1259        cx.simulate_shared_keystrokes("ctrl-v down down").await;
1260        cx.shared_state().await.assert_eq(indoc! {
1261            "The«ˇ q»uick
1262            bro«ˇwn»
1263            foxˇ
1264            jumps over the
1265
1266            lazy dog
1267            "
1268        });
1269        cx.simulate_shared_keystrokes("down").await;
1270        cx.shared_state().await.assert_eq(indoc! {
1271            "The «qˇ»uick
1272            brow«nˇ»
1273            fox
1274            jump«sˇ» over the
1275
1276            lazy dog
1277            "
1278        });
1279        cx.simulate_shared_keystrokes("left").await;
1280        cx.shared_state().await.assert_eq(indoc! {
1281            "The«ˇ q»uick
1282            bro«ˇwn»
1283            foxˇ
1284            jum«ˇps» over the
1285
1286            lazy dog
1287            "
1288        });
1289        cx.simulate_shared_keystrokes("s o escape").await;
1290        cx.shared_state().await.assert_eq(indoc! {
1291            "Theˇouick
1292            broo
1293            foxo
1294            jumo over the
1295
1296            lazy dog
1297            "
1298        });
1299
1300        // https://github.com/zed-industries/zed/issues/6274
1301        cx.set_shared_state(indoc! {
1302            "Theˇ quick brown
1303
1304            fox jumps over
1305            the lazy dog
1306            "
1307        })
1308        .await;
1309        cx.simulate_shared_keystrokes("l ctrl-v j j").await;
1310        cx.shared_state().await.assert_eq(indoc! {
1311            "The «qˇ»uick brown
1312
1313            fox «jˇ»umps over
1314            the lazy dog
1315            "
1316        });
1317    }
1318
1319    #[gpui::test]
1320    async fn test_visual_block_issue_2123(cx: &mut gpui::TestAppContext) {
1321        let mut cx = NeovimBackedTestContext::new(cx).await;
1322
1323        cx.set_shared_state(indoc! {
1324            "The ˇquick brown
1325            fox jumps over
1326            the lazy dog
1327            "
1328        })
1329        .await;
1330        cx.simulate_shared_keystrokes("ctrl-v right down").await;
1331        cx.shared_state().await.assert_eq(indoc! {
1332            "The «quˇ»ick brown
1333            fox «juˇ»mps over
1334            the lazy dog
1335            "
1336        });
1337    }
1338    #[gpui::test]
1339    async fn test_visual_block_mode_down_right(cx: &mut gpui::TestAppContext) {
1340        let mut cx = NeovimBackedTestContext::new(cx).await;
1341        cx.set_shared_state(indoc! {"
1342            The ˇquick brown
1343            fox jumps over
1344            the lazy dog"})
1345            .await;
1346        cx.simulate_shared_keystrokes("ctrl-v l l l l l j").await;
1347        cx.shared_state().await.assert_eq(indoc! {"
1348            The «quick ˇ»brown
1349            fox «jumps ˇ»over
1350            the lazy dog"});
1351    }
1352
1353    #[gpui::test]
1354    async fn test_visual_block_mode_up_left(cx: &mut gpui::TestAppContext) {
1355        let mut cx = NeovimBackedTestContext::new(cx).await;
1356        cx.set_shared_state(indoc! {"
1357            The quick brown
1358            fox jumpsˇ over
1359            the lazy dog"})
1360            .await;
1361        cx.simulate_shared_keystrokes("ctrl-v h h h h h k").await;
1362        cx.shared_state().await.assert_eq(indoc! {"
1363            The «ˇquick »brown
1364            fox «ˇjumps »over
1365            the lazy dog"});
1366    }
1367
1368    #[gpui::test]
1369    async fn test_visual_block_mode_other_end(cx: &mut gpui::TestAppContext) {
1370        let mut cx = NeovimBackedTestContext::new(cx).await;
1371        cx.set_shared_state(indoc! {"
1372            The quick brown
1373            fox jˇumps over
1374            the lazy dog"})
1375            .await;
1376        cx.simulate_shared_keystrokes("ctrl-v l l l l j").await;
1377        cx.shared_state().await.assert_eq(indoc! {"
1378            The quick brown
1379            fox j«umps ˇ»over
1380            the l«azy dˇ»og"});
1381        cx.simulate_shared_keystrokes("o k").await;
1382        cx.shared_state().await.assert_eq(indoc! {"
1383            The q«ˇuick »brown
1384            fox j«ˇumps »over
1385            the l«ˇazy d»og"});
1386    }
1387
1388    #[gpui::test]
1389    async fn test_visual_block_mode_shift_other_end(cx: &mut gpui::TestAppContext) {
1390        let mut cx = NeovimBackedTestContext::new(cx).await;
1391        cx.set_shared_state(indoc! {"
1392            The quick brown
1393            fox jˇumps over
1394            the lazy dog"})
1395            .await;
1396        cx.simulate_shared_keystrokes("ctrl-v l l l l j").await;
1397        cx.shared_state().await.assert_eq(indoc! {"
1398            The quick brown
1399            fox j«umps ˇ»over
1400            the l«azy dˇ»og"});
1401        cx.simulate_shared_keystrokes("shift-o k").await;
1402        cx.shared_state().await.assert_eq(indoc! {"
1403            The quick brown
1404            fox j«ˇumps »over
1405            the lazy dog"});
1406    }
1407
1408    #[gpui::test]
1409    async fn test_visual_block_insert(cx: &mut gpui::TestAppContext) {
1410        let mut cx = NeovimBackedTestContext::new(cx).await;
1411
1412        cx.set_shared_state(indoc! {
1413            "ˇThe quick brown
1414            fox jumps over
1415            the lazy dog
1416            "
1417        })
1418        .await;
1419        cx.simulate_shared_keystrokes("ctrl-v 9 down").await;
1420        cx.shared_state().await.assert_eq(indoc! {
1421            "«Tˇ»he quick brown
1422            «fˇ»ox jumps over
1423            «tˇ»he lazy dog
1424            ˇ"
1425        });
1426
1427        cx.simulate_shared_keystrokes("shift-i k escape").await;
1428        cx.shared_state().await.assert_eq(indoc! {
1429            "ˇkThe quick brown
1430            kfox jumps over
1431            kthe lazy dog
1432            k"
1433        });
1434
1435        cx.set_shared_state(indoc! {
1436            "ˇThe quick brown
1437            fox jumps over
1438            the lazy dog
1439            "
1440        })
1441        .await;
1442        cx.simulate_shared_keystrokes("ctrl-v 9 down").await;
1443        cx.shared_state().await.assert_eq(indoc! {
1444            "«Tˇ»he quick brown
1445            «fˇ»ox jumps over
1446            «tˇ»he lazy dog
1447            ˇ"
1448        });
1449        cx.simulate_shared_keystrokes("c k escape").await;
1450        cx.shared_state().await.assert_eq(indoc! {
1451            "ˇkhe quick brown
1452            kox jumps over
1453            khe lazy dog
1454            k"
1455        });
1456    }
1457
1458    #[gpui::test]
1459    async fn test_visual_object(cx: &mut gpui::TestAppContext) {
1460        let mut cx = NeovimBackedTestContext::new(cx).await;
1461
1462        cx.set_shared_state("hello (in [parˇens] o)").await;
1463        cx.simulate_shared_keystrokes("ctrl-v l").await;
1464        cx.simulate_shared_keystrokes("a ]").await;
1465        cx.shared_state()
1466            .await
1467            .assert_eq("hello (in «[parens]ˇ» o)");
1468        cx.simulate_shared_keystrokes("i (").await;
1469        cx.shared_state()
1470            .await
1471            .assert_eq("hello («in [parens] oˇ»)");
1472
1473        cx.set_shared_state("hello in a wˇord again.").await;
1474        cx.simulate_shared_keystrokes("ctrl-v l i w").await;
1475        cx.shared_state()
1476            .await
1477            .assert_eq("hello in a w«ordˇ» again.");
1478        assert_eq!(cx.mode(), Mode::VisualBlock);
1479        cx.simulate_shared_keystrokes("o a s").await;
1480        cx.shared_state()
1481            .await
1482            .assert_eq("«ˇhello in a word» again.");
1483    }
1484
1485    #[gpui::test]
1486    async fn test_visual_object_expands(cx: &mut gpui::TestAppContext) {
1487        let mut cx = NeovimBackedTestContext::new(cx).await;
1488
1489        cx.set_shared_state(indoc! {
1490            "{
1491                {
1492               ˇ }
1493            }
1494            {
1495            }
1496            "
1497        })
1498        .await;
1499        cx.simulate_shared_keystrokes("v l").await;
1500        cx.shared_state().await.assert_eq(indoc! {
1501            "{
1502                {
1503               « }ˇ»
1504            }
1505            {
1506            }
1507            "
1508        });
1509        cx.simulate_shared_keystrokes("a {").await;
1510        cx.shared_state().await.assert_eq(indoc! {
1511            "{
1512                «{
1513                }ˇ»
1514            }
1515            {
1516            }
1517            "
1518        });
1519        cx.simulate_shared_keystrokes("a {").await;
1520        cx.shared_state().await.assert_eq(indoc! {
1521            "«{
1522                {
1523                }
1524            }ˇ»
1525            {
1526            }
1527            "
1528        });
1529        // cx.simulate_shared_keystrokes("a {").await;
1530        // cx.shared_state().await.assert_eq(indoc! {
1531        //     "{
1532        //         «{
1533        //         }ˇ»
1534        //     }
1535        //     {
1536        //     }
1537        //     "
1538        // });
1539    }
1540
1541    #[gpui::test]
1542    async fn test_mode_across_command(cx: &mut gpui::TestAppContext) {
1543        let mut cx = VimTestContext::new(cx, true).await;
1544
1545        cx.set_state("aˇbc", Mode::Normal);
1546        cx.simulate_keystrokes("ctrl-v");
1547        assert_eq!(cx.mode(), Mode::VisualBlock);
1548        cx.simulate_keystrokes("cmd-shift-p escape");
1549        assert_eq!(cx.mode(), Mode::VisualBlock);
1550    }
1551
1552    #[gpui::test]
1553    async fn test_gn(cx: &mut gpui::TestAppContext) {
1554        let mut cx = NeovimBackedTestContext::new(cx).await;
1555
1556        cx.set_shared_state("aaˇ aa aa aa aa").await;
1557        cx.simulate_shared_keystrokes("/ a a enter").await;
1558        cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1559        cx.simulate_shared_keystrokes("g n").await;
1560        cx.shared_state().await.assert_eq("aa «aaˇ» aa aa aa");
1561        cx.simulate_shared_keystrokes("g n").await;
1562        cx.shared_state().await.assert_eq("aa «aa aaˇ» aa aa");
1563        cx.simulate_shared_keystrokes("escape d g n").await;
1564        cx.shared_state().await.assert_eq("aa aa ˇ aa aa");
1565
1566        cx.set_shared_state("aaˇ aa aa aa aa").await;
1567        cx.simulate_shared_keystrokes("/ a a enter").await;
1568        cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1569        cx.simulate_shared_keystrokes("3 g n").await;
1570        cx.shared_state().await.assert_eq("aa aa aa «aaˇ» aa");
1571
1572        cx.set_shared_state("aaˇ aa aa aa aa").await;
1573        cx.simulate_shared_keystrokes("/ a a enter").await;
1574        cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1575        cx.simulate_shared_keystrokes("g shift-n").await;
1576        cx.shared_state().await.assert_eq("aa «ˇaa» aa aa aa");
1577        cx.simulate_shared_keystrokes("g shift-n").await;
1578        cx.shared_state().await.assert_eq("«ˇaa aa» aa aa aa");
1579    }
1580
1581    #[gpui::test]
1582    async fn test_gl(cx: &mut gpui::TestAppContext) {
1583        let mut cx = VimTestContext::new(cx, true).await;
1584
1585        cx.set_state("aaˇ aa\naa", Mode::Normal);
1586        cx.simulate_keystrokes("g l");
1587        cx.assert_state("«aaˇ» «aaˇ»\naa", Mode::Visual);
1588        cx.simulate_keystrokes("g >");
1589        cx.assert_state("«aaˇ» aa\n«aaˇ»", Mode::Visual);
1590    }
1591
1592    #[gpui::test]
1593    async fn test_dgn_repeat(cx: &mut gpui::TestAppContext) {
1594        let mut cx = NeovimBackedTestContext::new(cx).await;
1595
1596        cx.set_shared_state("aaˇ aa aa aa aa").await;
1597        cx.simulate_shared_keystrokes("/ a a enter").await;
1598        cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1599        cx.simulate_shared_keystrokes("d g n").await;
1600
1601        cx.shared_state().await.assert_eq("aa ˇ aa aa aa");
1602        cx.simulate_shared_keystrokes(".").await;
1603        cx.shared_state().await.assert_eq("aa  ˇ aa aa");
1604        cx.simulate_shared_keystrokes(".").await;
1605        cx.shared_state().await.assert_eq("aa   ˇ aa");
1606    }
1607
1608    #[gpui::test]
1609    async fn test_cgn_repeat(cx: &mut gpui::TestAppContext) {
1610        let mut cx = NeovimBackedTestContext::new(cx).await;
1611
1612        cx.set_shared_state("aaˇ aa aa aa aa").await;
1613        cx.simulate_shared_keystrokes("/ a a enter").await;
1614        cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1615        cx.simulate_shared_keystrokes("c g n x escape").await;
1616        cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1617        cx.simulate_shared_keystrokes(".").await;
1618        cx.shared_state().await.assert_eq("aa x ˇx aa aa");
1619    }
1620
1621    #[gpui::test]
1622    async fn test_cgn_nomatch(cx: &mut gpui::TestAppContext) {
1623        let mut cx = NeovimBackedTestContext::new(cx).await;
1624
1625        cx.set_shared_state("aaˇ aa aa aa aa").await;
1626        cx.simulate_shared_keystrokes("/ b b enter").await;
1627        cx.shared_state().await.assert_eq("aaˇ aa aa aa aa");
1628        cx.simulate_shared_keystrokes("c g n x escape").await;
1629        cx.shared_state().await.assert_eq("aaˇaa aa aa aa");
1630        cx.simulate_shared_keystrokes(".").await;
1631        cx.shared_state().await.assert_eq("aaˇa aa aa aa");
1632
1633        cx.set_shared_state("aaˇ bb aa aa aa").await;
1634        cx.simulate_shared_keystrokes("/ b b enter").await;
1635        cx.shared_state().await.assert_eq("aa ˇbb aa aa aa");
1636        cx.simulate_shared_keystrokes("c g n x escape").await;
1637        cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1638        cx.simulate_shared_keystrokes(".").await;
1639        cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1640    }
1641
1642    #[gpui::test]
1643    async fn test_visual_shift_d(cx: &mut gpui::TestAppContext) {
1644        let mut cx = NeovimBackedTestContext::new(cx).await;
1645
1646        cx.set_shared_state(indoc! {
1647            "The ˇquick brown
1648            fox jumps over
1649            the lazy dog
1650            "
1651        })
1652        .await;
1653        cx.simulate_shared_keystrokes("v down shift-d").await;
1654        cx.shared_state().await.assert_eq(indoc! {
1655            "the ˇlazy dog\n"
1656        });
1657
1658        cx.set_shared_state(indoc! {
1659            "The ˇquick brown
1660            fox jumps over
1661            the lazy dog
1662            "
1663        })
1664        .await;
1665        cx.simulate_shared_keystrokes("ctrl-v down shift-d").await;
1666        cx.shared_state().await.assert_eq(indoc! {
1667            "Theˇ•
1668            fox•
1669            the lazy dog
1670            "
1671        });
1672    }
1673
1674    #[gpui::test]
1675    async fn test_shift_y(cx: &mut gpui::TestAppContext) {
1676        let mut cx = NeovimBackedTestContext::new(cx).await;
1677
1678        cx.set_shared_state(indoc! {
1679            "The ˇquick brown\n"
1680        })
1681        .await;
1682        cx.simulate_shared_keystrokes("v i w shift-y").await;
1683        cx.shared_clipboard().await.assert_eq(indoc! {
1684            "The quick brown\n"
1685        });
1686    }
1687
1688    #[gpui::test]
1689    async fn test_gv(cx: &mut gpui::TestAppContext) {
1690        let mut cx = NeovimBackedTestContext::new(cx).await;
1691
1692        cx.set_shared_state(indoc! {
1693            "The ˇquick brown"
1694        })
1695        .await;
1696        cx.simulate_shared_keystrokes("v i w escape g v").await;
1697        cx.shared_state().await.assert_eq(indoc! {
1698            "The «quickˇ» brown"
1699        });
1700
1701        cx.simulate_shared_keystrokes("o escape g v").await;
1702        cx.shared_state().await.assert_eq(indoc! {
1703            "The «ˇquick» brown"
1704        });
1705
1706        cx.simulate_shared_keystrokes("escape ^ ctrl-v l").await;
1707        cx.shared_state().await.assert_eq(indoc! {
1708            "«Thˇ»e quick brown"
1709        });
1710        cx.simulate_shared_keystrokes("g v").await;
1711        cx.shared_state().await.assert_eq(indoc! {
1712            "The «ˇquick» brown"
1713        });
1714        cx.simulate_shared_keystrokes("g v").await;
1715        cx.shared_state().await.assert_eq(indoc! {
1716            "«Thˇ»e quick brown"
1717        });
1718
1719        cx.set_state(
1720            indoc! {"
1721            fiˇsh one
1722            fish two
1723            fish red
1724            fish blue
1725        "},
1726            Mode::Normal,
1727        );
1728        cx.simulate_keystrokes("4 g l escape escape g v");
1729        cx.assert_state(
1730            indoc! {"
1731                «fishˇ» one
1732                «fishˇ» two
1733                «fishˇ» red
1734                «fishˇ» blue
1735            "},
1736            Mode::Visual,
1737        );
1738        cx.simulate_keystrokes("y g v");
1739        cx.assert_state(
1740            indoc! {"
1741                «fishˇ» one
1742                «fishˇ» two
1743                «fishˇ» red
1744                «fishˇ» blue
1745            "},
1746            Mode::Visual,
1747        );
1748    }
1749
1750    #[gpui::test]
1751    async fn test_p_g_v_y(cx: &mut gpui::TestAppContext) {
1752        let mut cx = NeovimBackedTestContext::new(cx).await;
1753
1754        cx.set_shared_state(indoc! {
1755            "The
1756            quicˇk
1757            brown
1758            fox"
1759        })
1760        .await;
1761        cx.simulate_shared_keystrokes("y y j shift-v p g v y").await;
1762        cx.shared_state().await.assert_eq(indoc! {
1763            "The
1764            quick
1765            ˇquick
1766            fox"
1767        });
1768        cx.shared_clipboard().await.assert_eq("quick\n");
1769    }
1770
1771    #[gpui::test]
1772    async fn test_v2ap(cx: &mut gpui::TestAppContext) {
1773        let mut cx = NeovimBackedTestContext::new(cx).await;
1774
1775        cx.set_shared_state(indoc! {
1776            "The
1777            quicˇk
1778
1779            brown
1780            fox"
1781        })
1782        .await;
1783        cx.simulate_shared_keystrokes("v 2 a p").await;
1784        cx.shared_state().await.assert_eq(indoc! {
1785            "«The
1786            quick
1787
1788            brown
1789            fˇ»ox"
1790        });
1791    }
1792}