visual.rs

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