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            editor.selections.line_mode = line_mode;
 602            let kind = if line_mode {
 603                MotionKind::Linewise
 604            } else {
 605                MotionKind::Exclusive
 606            };
 607            vim.yank_selections_content(editor, kind, window, cx);
 608            editor.change_selections(None, window, cx, |s| {
 609                s.move_with(|map, selection| {
 610                    if line_mode {
 611                        selection.start = start_of_line(map, false, selection.start);
 612                    };
 613                    selection.collapse_to(selection.start, SelectionGoal::None)
 614                });
 615                if vim.mode == Mode::VisualBlock {
 616                    s.select_anchors(vec![s.first_anchor()])
 617                }
 618            });
 619        });
 620        self.switch_mode(Mode::Normal, true, window, cx);
 621    }
 622
 623    pub(crate) fn visual_replace(
 624        &mut self,
 625        text: Arc<str>,
 626        window: &mut Window,
 627        cx: &mut Context<Self>,
 628    ) {
 629        self.stop_recording(cx);
 630        self.update_editor(window, cx, |_, editor, window, cx| {
 631            editor.transact(window, cx, |editor, window, cx| {
 632                let (display_map, selections) = editor.selections.all_adjusted_display(cx);
 633
 634                // Selections are biased right at the start. So we need to store
 635                // anchors that are biased left so that we can restore the selections
 636                // after the change
 637                let stable_anchors = editor
 638                    .selections
 639                    .disjoint_anchors()
 640                    .iter()
 641                    .map(|selection| {
 642                        let start = selection.start.bias_left(&display_map.buffer_snapshot);
 643                        start..start
 644                    })
 645                    .collect::<Vec<_>>();
 646
 647                let mut edits = Vec::new();
 648                for selection in selections.iter() {
 649                    let selection = selection.clone();
 650                    for row_range in
 651                        movement::split_display_range_by_lines(&display_map, selection.range())
 652                    {
 653                        let range = row_range.start.to_offset(&display_map, Bias::Right)
 654                            ..row_range.end.to_offset(&display_map, Bias::Right);
 655                        let text = text.repeat(range.len());
 656                        edits.push((range, text));
 657                    }
 658                }
 659
 660                editor.edit(edits, cx);
 661                editor.change_selections(None, window, cx, |s| s.select_ranges(stable_anchors));
 662            });
 663        });
 664        self.switch_mode(Mode::Normal, false, window, cx);
 665    }
 666
 667    pub fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
 668        let count =
 669            Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 });
 670        self.update_editor(window, cx, |_, editor, window, cx| {
 671            editor.set_clip_at_line_ends(false, cx);
 672            for _ in 0..count {
 673                if editor
 674                    .select_next(&Default::default(), window, cx)
 675                    .log_err()
 676                    .is_none()
 677                {
 678                    break;
 679                }
 680            }
 681        });
 682    }
 683
 684    pub fn select_previous(
 685        &mut self,
 686        _: &SelectPrevious,
 687        window: &mut Window,
 688        cx: &mut Context<Self>,
 689    ) {
 690        let count =
 691            Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 });
 692        self.update_editor(window, cx, |_, editor, window, cx| {
 693            for _ in 0..count {
 694                if editor
 695                    .select_previous(&Default::default(), window, cx)
 696                    .log_err()
 697                    .is_none()
 698                {
 699                    break;
 700                }
 701            }
 702        });
 703    }
 704
 705    pub fn select_match(
 706        &mut self,
 707        direction: Direction,
 708        window: &mut Window,
 709        cx: &mut Context<Self>,
 710    ) {
 711        let count = Vim::take_count(cx).unwrap_or(1);
 712        let Some(pane) = self.pane(window, cx) else {
 713            return;
 714        };
 715        let vim_is_normal = self.mode == Mode::Normal;
 716        let mut start_selection = 0usize;
 717        let mut end_selection = 0usize;
 718
 719        self.update_editor(window, cx, |_, editor, _, _| {
 720            editor.set_collapse_matches(false);
 721        });
 722        if vim_is_normal {
 723            pane.update(cx, |pane, cx| {
 724                if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()
 725                {
 726                    search_bar.update(cx, |search_bar, cx| {
 727                        if !search_bar.has_active_match() || !search_bar.show(window, cx) {
 728                            return;
 729                        }
 730                        // without update_match_index there is a bug when the cursor is before the first match
 731                        search_bar.update_match_index(window, cx);
 732                        search_bar.select_match(direction.opposite(), 1, window, cx);
 733                    });
 734                }
 735            });
 736        }
 737        self.update_editor(window, cx, |_, editor, _, cx| {
 738            let latest = editor.selections.newest::<usize>(cx);
 739            start_selection = latest.start;
 740            end_selection = latest.end;
 741        });
 742
 743        let mut match_exists = false;
 744        pane.update(cx, |pane, cx| {
 745            if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 746                search_bar.update(cx, |search_bar, cx| {
 747                    search_bar.update_match_index(window, cx);
 748                    search_bar.select_match(direction, count, window, cx);
 749                    match_exists = search_bar.match_exists(window, cx);
 750                });
 751            }
 752        });
 753        if !match_exists {
 754            self.clear_operator(window, cx);
 755            self.stop_replaying(cx);
 756            return;
 757        }
 758        self.update_editor(window, cx, |_, editor, window, cx| {
 759            let latest = editor.selections.newest::<usize>(cx);
 760            if vim_is_normal {
 761                start_selection = latest.start;
 762                end_selection = latest.end;
 763            } else {
 764                start_selection = start_selection.min(latest.start);
 765                end_selection = end_selection.max(latest.end);
 766            }
 767            if direction == Direction::Prev {
 768                std::mem::swap(&mut start_selection, &mut end_selection);
 769            }
 770            editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 771                s.select_ranges([start_selection..end_selection]);
 772            });
 773            editor.set_collapse_matches(true);
 774        });
 775
 776        match self.maybe_pop_operator() {
 777            Some(Operator::Change) => self.substitute(None, false, window, cx),
 778            Some(Operator::Delete) => {
 779                self.stop_recording(cx);
 780                self.visual_delete(false, window, cx)
 781            }
 782            Some(Operator::Yank) => self.visual_yank(false, window, cx),
 783            _ => {} // Ignoring other operators
 784        }
 785    }
 786}
 787#[cfg(test)]
 788mod test {
 789    use indoc::indoc;
 790    use workspace::item::Item;
 791
 792    use crate::{
 793        state::Mode,
 794        test::{NeovimBackedTestContext, VimTestContext},
 795    };
 796
 797    #[gpui::test]
 798    async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
 799        let mut cx = NeovimBackedTestContext::new(cx).await;
 800
 801        cx.set_shared_state(indoc! {
 802            "The ˇquick brown
 803            fox jumps over
 804            the lazy dog"
 805        })
 806        .await;
 807        let cursor = cx.update_editor(|editor, _, cx| editor.pixel_position_of_cursor(cx));
 808
 809        // entering visual mode should select the character
 810        // under cursor
 811        cx.simulate_shared_keystrokes("v").await;
 812        cx.shared_state()
 813            .await
 814            .assert_eq(indoc! { "The «qˇ»uick brown
 815            fox jumps over
 816            the lazy dog"});
 817        cx.update_editor(|editor, _, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
 818
 819        // forwards motions should extend the selection
 820        cx.simulate_shared_keystrokes("w j").await;
 821        cx.shared_state().await.assert_eq(indoc! { "The «quick brown
 822            fox jumps oˇ»ver
 823            the lazy dog"});
 824
 825        cx.simulate_shared_keystrokes("escape").await;
 826        cx.shared_state().await.assert_eq(indoc! { "The quick brown
 827            fox jumps ˇover
 828            the lazy dog"});
 829
 830        // motions work backwards
 831        cx.simulate_shared_keystrokes("v k b").await;
 832        cx.shared_state()
 833            .await
 834            .assert_eq(indoc! { "The «ˇquick brown
 835            fox jumps o»ver
 836            the lazy dog"});
 837
 838        // works on empty lines
 839        cx.set_shared_state(indoc! {"
 840            a
 841            ˇ
 842            b
 843            "})
 844            .await;
 845        let cursor = cx.update_editor(|editor, _, cx| editor.pixel_position_of_cursor(cx));
 846        cx.simulate_shared_keystrokes("v").await;
 847        cx.shared_state().await.assert_eq(indoc! {"
 848            a
 849            «
 850            ˇ»b
 851        "});
 852        cx.update_editor(|editor, _, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
 853
 854        // toggles off again
 855        cx.simulate_shared_keystrokes("v").await;
 856        cx.shared_state().await.assert_eq(indoc! {"
 857            a
 858            ˇ
 859            b
 860            "});
 861
 862        // works at the end of a document
 863        cx.set_shared_state(indoc! {"
 864            a
 865            b
 866            ˇ"})
 867            .await;
 868
 869        cx.simulate_shared_keystrokes("v").await;
 870        cx.shared_state().await.assert_eq(indoc! {"
 871            a
 872            b
 873            ˇ"});
 874    }
 875
 876    #[gpui::test]
 877    async fn test_visual_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
 878        let mut cx = VimTestContext::new(cx, true).await;
 879
 880        cx.set_state(
 881            indoc! {
 882                "«The quick brown
 883                fox jumps over
 884                the lazy dogˇ»"
 885            },
 886            Mode::Visual,
 887        );
 888        cx.simulate_keystrokes("g shift-i");
 889        cx.assert_state(
 890            indoc! {
 891                "ˇThe quick brown
 892                ˇfox jumps over
 893                ˇthe lazy dog"
 894            },
 895            Mode::Insert,
 896        );
 897    }
 898
 899    #[gpui::test]
 900    async fn test_visual_insert_end_of_line(cx: &mut gpui::TestAppContext) {
 901        let mut cx = VimTestContext::new(cx, true).await;
 902
 903        cx.set_state(
 904            indoc! {
 905                "«The quick brown
 906                fox jumps over
 907                the lazy dogˇ»"
 908            },
 909            Mode::Visual,
 910        );
 911        cx.simulate_keystrokes("g shift-a");
 912        cx.assert_state(
 913            indoc! {
 914                "The quick brownˇ
 915                fox jumps overˇ
 916                the lazy dogˇ"
 917            },
 918            Mode::Insert,
 919        );
 920    }
 921
 922    #[gpui::test]
 923    async fn test_enter_visual_line_mode(cx: &mut gpui::TestAppContext) {
 924        let mut cx = NeovimBackedTestContext::new(cx).await;
 925
 926        cx.set_shared_state(indoc! {
 927            "The ˇquick brown
 928            fox jumps over
 929            the lazy dog"
 930        })
 931        .await;
 932        cx.simulate_shared_keystrokes("shift-v").await;
 933        cx.shared_state()
 934            .await
 935            .assert_eq(indoc! { "The «qˇ»uick brown
 936            fox jumps over
 937            the lazy dog"});
 938        cx.simulate_shared_keystrokes("x").await;
 939        cx.shared_state().await.assert_eq(indoc! { "fox ˇjumps over
 940        the lazy dog"});
 941
 942        // it should work on empty lines
 943        cx.set_shared_state(indoc! {"
 944            a
 945            ˇ
 946            b"})
 947            .await;
 948        cx.simulate_shared_keystrokes("shift-v").await;
 949        cx.shared_state().await.assert_eq(indoc! {"
 950            a
 951            «
 952            ˇ»b"});
 953        cx.simulate_shared_keystrokes("x").await;
 954        cx.shared_state().await.assert_eq(indoc! {"
 955            a
 956            ˇb"});
 957
 958        // it should work at the end of the document
 959        cx.set_shared_state(indoc! {"
 960            a
 961            b
 962            ˇ"})
 963            .await;
 964        let cursor = cx.update_editor(|editor, _, cx| editor.pixel_position_of_cursor(cx));
 965        cx.simulate_shared_keystrokes("shift-v").await;
 966        cx.shared_state().await.assert_eq(indoc! {"
 967            a
 968            b
 969            ˇ"});
 970        cx.update_editor(|editor, _, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
 971        cx.simulate_shared_keystrokes("x").await;
 972        cx.shared_state().await.assert_eq(indoc! {"
 973            a
 974            ˇb"});
 975    }
 976
 977    #[gpui::test]
 978    async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
 979        let mut cx = NeovimBackedTestContext::new(cx).await;
 980
 981        cx.simulate("v w", "The quick ˇbrown")
 982            .await
 983            .assert_matches();
 984
 985        cx.simulate("v w x", "The quick ˇbrown")
 986            .await
 987            .assert_matches();
 988        cx.simulate(
 989            "v w j x",
 990            indoc! {"
 991                The ˇquick brown
 992                fox jumps over
 993                the lazy dog"},
 994        )
 995        .await
 996        .assert_matches();
 997        // Test pasting code copied on delete
 998        cx.simulate_shared_keystrokes("j p").await;
 999        cx.shared_state().await.assert_matches();
1000
1001        cx.simulate_at_each_offset(
1002            "v w j x",
1003            indoc! {"
1004                The ˇquick brown
1005                fox jumps over
1006                the ˇlazy dog"},
1007        )
1008        .await
1009        .assert_matches();
1010        cx.simulate_at_each_offset(
1011            "v b k x",
1012            indoc! {"
1013                The ˇquick brown
1014                fox jumps ˇover
1015                the ˇlazy dog"},
1016        )
1017        .await
1018        .assert_matches();
1019    }
1020
1021    #[gpui::test]
1022    async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
1023        let mut cx = NeovimBackedTestContext::new(cx).await;
1024
1025        cx.set_shared_state(indoc! {"
1026                The quˇick brown
1027                fox jumps over
1028                the lazy dog"})
1029            .await;
1030        cx.simulate_shared_keystrokes("shift-v x").await;
1031        cx.shared_state().await.assert_matches();
1032
1033        // Test pasting code copied on delete
1034        cx.simulate_shared_keystrokes("p").await;
1035        cx.shared_state().await.assert_matches();
1036
1037        cx.set_shared_state(indoc! {"
1038                The quick brown
1039                fox jumps over
1040                the laˇzy dog"})
1041            .await;
1042        cx.simulate_shared_keystrokes("shift-v x").await;
1043        cx.shared_state().await.assert_matches();
1044        cx.shared_clipboard().await.assert_eq("the lazy dog\n");
1045
1046        cx.set_shared_state(indoc! {"
1047                                The quˇick brown
1048                                fox jumps over
1049                                the lazy dog"})
1050            .await;
1051        cx.simulate_shared_keystrokes("shift-v j x").await;
1052        cx.shared_state().await.assert_matches();
1053        // Test pasting code copied on delete
1054        cx.simulate_shared_keystrokes("p").await;
1055        cx.shared_state().await.assert_matches();
1056
1057        cx.set_shared_state(indoc! {"
1058            The ˇlong line
1059            should not
1060            crash
1061            "})
1062            .await;
1063        cx.simulate_shared_keystrokes("shift-v $ x").await;
1064        cx.shared_state().await.assert_matches();
1065    }
1066
1067    #[gpui::test]
1068    async fn test_visual_yank(cx: &mut gpui::TestAppContext) {
1069        let mut cx = NeovimBackedTestContext::new(cx).await;
1070
1071        cx.set_shared_state("The quick ˇbrown").await;
1072        cx.simulate_shared_keystrokes("v w y").await;
1073        cx.shared_state().await.assert_eq("The quick ˇbrown");
1074        cx.shared_clipboard().await.assert_eq("brown");
1075
1076        cx.set_shared_state(indoc! {"
1077                The ˇquick brown
1078                fox jumps over
1079                the lazy dog"})
1080            .await;
1081        cx.simulate_shared_keystrokes("v w j y").await;
1082        cx.shared_state().await.assert_eq(indoc! {"
1083                    The ˇquick brown
1084                    fox jumps over
1085                    the lazy dog"});
1086        cx.shared_clipboard().await.assert_eq(indoc! {"
1087                quick brown
1088                fox jumps o"});
1089
1090        cx.set_shared_state(indoc! {"
1091                    The quick brown
1092                    fox jumps over
1093                    the ˇlazy dog"})
1094            .await;
1095        cx.simulate_shared_keystrokes("v w j y").await;
1096        cx.shared_state().await.assert_eq(indoc! {"
1097                    The quick brown
1098                    fox jumps over
1099                    the ˇlazy dog"});
1100        cx.shared_clipboard().await.assert_eq("lazy d");
1101        cx.simulate_shared_keystrokes("shift-v y").await;
1102        cx.shared_clipboard().await.assert_eq("the lazy dog\n");
1103
1104        cx.set_shared_state(indoc! {"
1105                    The ˇquick brown
1106                    fox jumps over
1107                    the lazy dog"})
1108            .await;
1109        cx.simulate_shared_keystrokes("v b k y").await;
1110        cx.shared_state().await.assert_eq(indoc! {"
1111                    ˇThe quick brown
1112                    fox jumps over
1113                    the lazy dog"});
1114        assert_eq!(
1115            cx.read_from_clipboard()
1116                .map(|item| item.text().unwrap().to_string())
1117                .unwrap(),
1118            "The q"
1119        );
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("shift-v shift-g shift-y")
1127            .await;
1128        cx.shared_state().await.assert_eq(indoc! {"
1129                    The quick brown
1130                    ˇfox jumps over
1131                    the lazy dog"});
1132        cx.shared_clipboard()
1133            .await
1134            .assert_eq("fox jumps over\nthe lazy dog\n");
1135    }
1136
1137    #[gpui::test]
1138    async fn test_visual_block_mode(cx: &mut gpui::TestAppContext) {
1139        let mut cx = NeovimBackedTestContext::new(cx).await;
1140
1141        cx.set_shared_state(indoc! {
1142            "The ˇquick brown
1143             fox jumps over
1144             the lazy dog"
1145        })
1146        .await;
1147        cx.simulate_shared_keystrokes("ctrl-v").await;
1148        cx.shared_state().await.assert_eq(indoc! {
1149            "The «qˇ»uick brown
1150            fox jumps over
1151            the lazy dog"
1152        });
1153        cx.simulate_shared_keystrokes("2 down").await;
1154        cx.shared_state().await.assert_eq(indoc! {
1155            "The «qˇ»uick brown
1156            fox «jˇ»umps over
1157            the «lˇ»azy dog"
1158        });
1159        cx.simulate_shared_keystrokes("e").await;
1160        cx.shared_state().await.assert_eq(indoc! {
1161            "The «quicˇ»k brown
1162            fox «jumpˇ»s over
1163            the «lazyˇ» dog"
1164        });
1165        cx.simulate_shared_keystrokes("^").await;
1166        cx.shared_state().await.assert_eq(indoc! {
1167            "«ˇThe q»uick brown
1168            «ˇfox j»umps over
1169            «ˇthe l»azy dog"
1170        });
1171        cx.simulate_shared_keystrokes("$").await;
1172        cx.shared_state().await.assert_eq(indoc! {
1173            "The «quick brownˇ»
1174            fox «jumps overˇ»
1175            the «lazy dogˇ»"
1176        });
1177        cx.simulate_shared_keystrokes("shift-f space").await;
1178        cx.shared_state().await.assert_eq(indoc! {
1179            "The «quickˇ» brown
1180            fox «jumpsˇ» over
1181            the «lazy ˇ»dog"
1182        });
1183
1184        // toggling through visual mode works as expected
1185        cx.simulate_shared_keystrokes("v").await;
1186        cx.shared_state().await.assert_eq(indoc! {
1187            "The «quick brown
1188            fox jumps over
1189            the lazy ˇ»dog"
1190        });
1191        cx.simulate_shared_keystrokes("ctrl-v").await;
1192        cx.shared_state().await.assert_eq(indoc! {
1193            "The «quickˇ» brown
1194            fox «jumpsˇ» over
1195            the «lazy ˇ»dog"
1196        });
1197
1198        cx.set_shared_state(indoc! {
1199            "The ˇquick
1200             brown
1201             fox
1202             jumps over the
1203
1204             lazy dog
1205            "
1206        })
1207        .await;
1208        cx.simulate_shared_keystrokes("ctrl-v down down").await;
1209        cx.shared_state().await.assert_eq(indoc! {
1210            "The«ˇ q»uick
1211            bro«ˇwn»
1212            foxˇ
1213            jumps over the
1214
1215            lazy dog
1216            "
1217        });
1218        cx.simulate_shared_keystrokes("down").await;
1219        cx.shared_state().await.assert_eq(indoc! {
1220            "The «qˇ»uick
1221            brow«nˇ»
1222            fox
1223            jump«sˇ» over the
1224
1225            lazy dog
1226            "
1227        });
1228        cx.simulate_shared_keystrokes("left").await;
1229        cx.shared_state().await.assert_eq(indoc! {
1230            "The«ˇ q»uick
1231            bro«ˇwn»
1232            foxˇ
1233            jum«ˇps» over the
1234
1235            lazy dog
1236            "
1237        });
1238        cx.simulate_shared_keystrokes("s o escape").await;
1239        cx.shared_state().await.assert_eq(indoc! {
1240            "Theˇouick
1241            broo
1242            foxo
1243            jumo over the
1244
1245            lazy dog
1246            "
1247        });
1248
1249        // https://github.com/zed-industries/zed/issues/6274
1250        cx.set_shared_state(indoc! {
1251            "Theˇ quick brown
1252
1253            fox jumps over
1254            the lazy dog
1255            "
1256        })
1257        .await;
1258        cx.simulate_shared_keystrokes("l ctrl-v j j").await;
1259        cx.shared_state().await.assert_eq(indoc! {
1260            "The «qˇ»uick brown
1261
1262            fox «jˇ»umps over
1263            the lazy dog
1264            "
1265        });
1266    }
1267
1268    #[gpui::test]
1269    async fn test_visual_block_issue_2123(cx: &mut gpui::TestAppContext) {
1270        let mut cx = NeovimBackedTestContext::new(cx).await;
1271
1272        cx.set_shared_state(indoc! {
1273            "The ˇquick brown
1274            fox jumps over
1275            the lazy dog
1276            "
1277        })
1278        .await;
1279        cx.simulate_shared_keystrokes("ctrl-v right down").await;
1280        cx.shared_state().await.assert_eq(indoc! {
1281            "The «quˇ»ick brown
1282            fox «juˇ»mps over
1283            the lazy dog
1284            "
1285        });
1286    }
1287    #[gpui::test]
1288    async fn test_visual_block_mode_down_right(cx: &mut gpui::TestAppContext) {
1289        let mut cx = NeovimBackedTestContext::new(cx).await;
1290        cx.set_shared_state(indoc! {"
1291            The ˇquick brown
1292            fox jumps over
1293            the lazy dog"})
1294            .await;
1295        cx.simulate_shared_keystrokes("ctrl-v l l l l l j").await;
1296        cx.shared_state().await.assert_eq(indoc! {"
1297            The «quick ˇ»brown
1298            fox «jumps ˇ»over
1299            the lazy dog"});
1300    }
1301
1302    #[gpui::test]
1303    async fn test_visual_block_mode_up_left(cx: &mut gpui::TestAppContext) {
1304        let mut cx = NeovimBackedTestContext::new(cx).await;
1305        cx.set_shared_state(indoc! {"
1306            The quick brown
1307            fox jumpsˇ over
1308            the lazy dog"})
1309            .await;
1310        cx.simulate_shared_keystrokes("ctrl-v h h h h h k").await;
1311        cx.shared_state().await.assert_eq(indoc! {"
1312            The «ˇquick »brown
1313            fox «ˇjumps »over
1314            the lazy dog"});
1315    }
1316
1317    #[gpui::test]
1318    async fn test_visual_block_mode_other_end(cx: &mut gpui::TestAppContext) {
1319        let mut cx = NeovimBackedTestContext::new(cx).await;
1320        cx.set_shared_state(indoc! {"
1321            The quick brown
1322            fox jˇumps over
1323            the lazy dog"})
1324            .await;
1325        cx.simulate_shared_keystrokes("ctrl-v l l l l j").await;
1326        cx.shared_state().await.assert_eq(indoc! {"
1327            The quick brown
1328            fox j«umps ˇ»over
1329            the l«azy dˇ»og"});
1330        cx.simulate_shared_keystrokes("o k").await;
1331        cx.shared_state().await.assert_eq(indoc! {"
1332            The q«ˇuick »brown
1333            fox j«ˇumps »over
1334            the l«ˇazy d»og"});
1335    }
1336
1337    #[gpui::test]
1338    async fn test_visual_block_mode_shift_other_end(cx: &mut gpui::TestAppContext) {
1339        let mut cx = NeovimBackedTestContext::new(cx).await;
1340        cx.set_shared_state(indoc! {"
1341            The quick brown
1342            fox jˇumps over
1343            the lazy dog"})
1344            .await;
1345        cx.simulate_shared_keystrokes("ctrl-v l l l l j").await;
1346        cx.shared_state().await.assert_eq(indoc! {"
1347            The quick brown
1348            fox j«umps ˇ»over
1349            the l«azy dˇ»og"});
1350        cx.simulate_shared_keystrokes("shift-o k").await;
1351        cx.shared_state().await.assert_eq(indoc! {"
1352            The quick brown
1353            fox j«ˇumps »over
1354            the lazy dog"});
1355    }
1356
1357    #[gpui::test]
1358    async fn test_visual_block_insert(cx: &mut gpui::TestAppContext) {
1359        let mut cx = NeovimBackedTestContext::new(cx).await;
1360
1361        cx.set_shared_state(indoc! {
1362            "ˇThe quick brown
1363            fox jumps over
1364            the lazy dog
1365            "
1366        })
1367        .await;
1368        cx.simulate_shared_keystrokes("ctrl-v 9 down").await;
1369        cx.shared_state().await.assert_eq(indoc! {
1370            "«Tˇ»he quick brown
1371            «fˇ»ox jumps over
1372            «tˇ»he lazy dog
1373            ˇ"
1374        });
1375
1376        cx.simulate_shared_keystrokes("shift-i k escape").await;
1377        cx.shared_state().await.assert_eq(indoc! {
1378            "ˇkThe quick brown
1379            kfox jumps over
1380            kthe lazy dog
1381            k"
1382        });
1383
1384        cx.set_shared_state(indoc! {
1385            "ˇThe quick brown
1386            fox jumps over
1387            the lazy dog
1388            "
1389        })
1390        .await;
1391        cx.simulate_shared_keystrokes("ctrl-v 9 down").await;
1392        cx.shared_state().await.assert_eq(indoc! {
1393            "«Tˇ»he quick brown
1394            «fˇ»ox jumps over
1395            «tˇ»he lazy dog
1396            ˇ"
1397        });
1398        cx.simulate_shared_keystrokes("c k escape").await;
1399        cx.shared_state().await.assert_eq(indoc! {
1400            "ˇkhe quick brown
1401            kox jumps over
1402            khe lazy dog
1403            k"
1404        });
1405    }
1406
1407    #[gpui::test]
1408    async fn test_visual_object(cx: &mut gpui::TestAppContext) {
1409        let mut cx = NeovimBackedTestContext::new(cx).await;
1410
1411        cx.set_shared_state("hello (in [parˇens] o)").await;
1412        cx.simulate_shared_keystrokes("ctrl-v l").await;
1413        cx.simulate_shared_keystrokes("a ]").await;
1414        cx.shared_state()
1415            .await
1416            .assert_eq("hello (in «[parens]ˇ» o)");
1417        cx.simulate_shared_keystrokes("i (").await;
1418        cx.shared_state()
1419            .await
1420            .assert_eq("hello («in [parens] oˇ»)");
1421
1422        cx.set_shared_state("hello in a wˇord again.").await;
1423        cx.simulate_shared_keystrokes("ctrl-v l i w").await;
1424        cx.shared_state()
1425            .await
1426            .assert_eq("hello in a w«ordˇ» again.");
1427        assert_eq!(cx.mode(), Mode::VisualBlock);
1428        cx.simulate_shared_keystrokes("o a s").await;
1429        cx.shared_state()
1430            .await
1431            .assert_eq("«ˇhello in a word» again.");
1432    }
1433
1434    #[gpui::test]
1435    async fn test_visual_object_expands(cx: &mut gpui::TestAppContext) {
1436        let mut cx = NeovimBackedTestContext::new(cx).await;
1437
1438        cx.set_shared_state(indoc! {
1439            "{
1440                {
1441               ˇ }
1442            }
1443            {
1444            }
1445            "
1446        })
1447        .await;
1448        cx.simulate_shared_keystrokes("v l").await;
1449        cx.shared_state().await.assert_eq(indoc! {
1450            "{
1451                {
1452               « }ˇ»
1453            }
1454            {
1455            }
1456            "
1457        });
1458        cx.simulate_shared_keystrokes("a {").await;
1459        cx.shared_state().await.assert_eq(indoc! {
1460            "{
1461                «{
1462                }ˇ»
1463            }
1464            {
1465            }
1466            "
1467        });
1468        cx.simulate_shared_keystrokes("a {").await;
1469        cx.shared_state().await.assert_eq(indoc! {
1470            "«{
1471                {
1472                }
1473            }ˇ»
1474            {
1475            }
1476            "
1477        });
1478        // cx.simulate_shared_keystrokes("a {").await;
1479        // cx.shared_state().await.assert_eq(indoc! {
1480        //     "{
1481        //         «{
1482        //         }ˇ»
1483        //     }
1484        //     {
1485        //     }
1486        //     "
1487        // });
1488    }
1489
1490    #[gpui::test]
1491    async fn test_mode_across_command(cx: &mut gpui::TestAppContext) {
1492        let mut cx = VimTestContext::new(cx, true).await;
1493
1494        cx.set_state("aˇbc", Mode::Normal);
1495        cx.simulate_keystrokes("ctrl-v");
1496        assert_eq!(cx.mode(), Mode::VisualBlock);
1497        cx.simulate_keystrokes("cmd-shift-p escape");
1498        assert_eq!(cx.mode(), Mode::VisualBlock);
1499    }
1500
1501    #[gpui::test]
1502    async fn test_gn(cx: &mut gpui::TestAppContext) {
1503        let mut cx = NeovimBackedTestContext::new(cx).await;
1504
1505        cx.set_shared_state("aaˇ aa aa aa aa").await;
1506        cx.simulate_shared_keystrokes("/ a a enter").await;
1507        cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1508        cx.simulate_shared_keystrokes("g n").await;
1509        cx.shared_state().await.assert_eq("aa «aaˇ» aa aa aa");
1510        cx.simulate_shared_keystrokes("g n").await;
1511        cx.shared_state().await.assert_eq("aa «aa aaˇ» aa aa");
1512        cx.simulate_shared_keystrokes("escape d g n").await;
1513        cx.shared_state().await.assert_eq("aa aa ˇ aa aa");
1514
1515        cx.set_shared_state("aaˇ aa aa aa aa").await;
1516        cx.simulate_shared_keystrokes("/ a a enter").await;
1517        cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1518        cx.simulate_shared_keystrokes("3 g n").await;
1519        cx.shared_state().await.assert_eq("aa aa aa «aaˇ» aa");
1520
1521        cx.set_shared_state("aaˇ aa aa aa aa").await;
1522        cx.simulate_shared_keystrokes("/ a a enter").await;
1523        cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1524        cx.simulate_shared_keystrokes("g shift-n").await;
1525        cx.shared_state().await.assert_eq("aa «ˇaa» aa aa aa");
1526        cx.simulate_shared_keystrokes("g shift-n").await;
1527        cx.shared_state().await.assert_eq("«ˇaa aa» aa aa aa");
1528    }
1529
1530    #[gpui::test]
1531    async fn test_gl(cx: &mut gpui::TestAppContext) {
1532        let mut cx = VimTestContext::new(cx, true).await;
1533
1534        cx.set_state("aaˇ aa\naa", Mode::Normal);
1535        cx.simulate_keystrokes("g l");
1536        cx.assert_state("«aaˇ» «aaˇ»\naa", Mode::Visual);
1537        cx.simulate_keystrokes("g >");
1538        cx.assert_state("«aaˇ» aa\n«aaˇ»", Mode::Visual);
1539    }
1540
1541    #[gpui::test]
1542    async fn test_dgn_repeat(cx: &mut gpui::TestAppContext) {
1543        let mut cx = NeovimBackedTestContext::new(cx).await;
1544
1545        cx.set_shared_state("aaˇ aa aa aa aa").await;
1546        cx.simulate_shared_keystrokes("/ a a enter").await;
1547        cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1548        cx.simulate_shared_keystrokes("d g n").await;
1549
1550        cx.shared_state().await.assert_eq("aa ˇ aa aa aa");
1551        cx.simulate_shared_keystrokes(".").await;
1552        cx.shared_state().await.assert_eq("aa  ˇ aa aa");
1553        cx.simulate_shared_keystrokes(".").await;
1554        cx.shared_state().await.assert_eq("aa   ˇ aa");
1555    }
1556
1557    #[gpui::test]
1558    async fn test_cgn_repeat(cx: &mut gpui::TestAppContext) {
1559        let mut cx = NeovimBackedTestContext::new(cx).await;
1560
1561        cx.set_shared_state("aaˇ aa aa aa aa").await;
1562        cx.simulate_shared_keystrokes("/ a a enter").await;
1563        cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1564        cx.simulate_shared_keystrokes("c g n x escape").await;
1565        cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1566        cx.simulate_shared_keystrokes(".").await;
1567        cx.shared_state().await.assert_eq("aa x ˇx aa aa");
1568    }
1569
1570    #[gpui::test]
1571    async fn test_cgn_nomatch(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("/ b b enter").await;
1576        cx.shared_state().await.assert_eq("aaˇ aa aa aa aa");
1577        cx.simulate_shared_keystrokes("c g n x escape").await;
1578        cx.shared_state().await.assert_eq("aaˇaa aa aa aa");
1579        cx.simulate_shared_keystrokes(".").await;
1580        cx.shared_state().await.assert_eq("aaˇa aa aa aa");
1581
1582        cx.set_shared_state("aaˇ bb aa aa aa").await;
1583        cx.simulate_shared_keystrokes("/ b b enter").await;
1584        cx.shared_state().await.assert_eq("aa ˇbb aa aa aa");
1585        cx.simulate_shared_keystrokes("c g n x escape").await;
1586        cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1587        cx.simulate_shared_keystrokes(".").await;
1588        cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1589    }
1590
1591    #[gpui::test]
1592    async fn test_visual_shift_d(cx: &mut gpui::TestAppContext) {
1593        let mut cx = NeovimBackedTestContext::new(cx).await;
1594
1595        cx.set_shared_state(indoc! {
1596            "The ˇquick brown
1597            fox jumps over
1598            the lazy dog
1599            "
1600        })
1601        .await;
1602        cx.simulate_shared_keystrokes("v down shift-d").await;
1603        cx.shared_state().await.assert_eq(indoc! {
1604            "the ˇlazy dog\n"
1605        });
1606
1607        cx.set_shared_state(indoc! {
1608            "The ˇquick brown
1609            fox jumps over
1610            the lazy dog
1611            "
1612        })
1613        .await;
1614        cx.simulate_shared_keystrokes("ctrl-v down shift-d").await;
1615        cx.shared_state().await.assert_eq(indoc! {
1616            "Theˇ•
1617            fox•
1618            the lazy dog
1619            "
1620        });
1621    }
1622
1623    #[gpui::test]
1624    async fn test_shift_y(cx: &mut gpui::TestAppContext) {
1625        let mut cx = NeovimBackedTestContext::new(cx).await;
1626
1627        cx.set_shared_state(indoc! {
1628            "The ˇquick brown\n"
1629        })
1630        .await;
1631        cx.simulate_shared_keystrokes("v i w shift-y").await;
1632        cx.shared_clipboard().await.assert_eq(indoc! {
1633            "The quick brown\n"
1634        });
1635    }
1636
1637    #[gpui::test]
1638    async fn test_gv(cx: &mut gpui::TestAppContext) {
1639        let mut cx = NeovimBackedTestContext::new(cx).await;
1640
1641        cx.set_shared_state(indoc! {
1642            "The ˇquick brown"
1643        })
1644        .await;
1645        cx.simulate_shared_keystrokes("v i w escape g v").await;
1646        cx.shared_state().await.assert_eq(indoc! {
1647            "The «quickˇ» brown"
1648        });
1649
1650        cx.simulate_shared_keystrokes("o escape g v").await;
1651        cx.shared_state().await.assert_eq(indoc! {
1652            "The «ˇquick» brown"
1653        });
1654
1655        cx.simulate_shared_keystrokes("escape ^ ctrl-v l").await;
1656        cx.shared_state().await.assert_eq(indoc! {
1657            "«Thˇ»e quick brown"
1658        });
1659        cx.simulate_shared_keystrokes("g v").await;
1660        cx.shared_state().await.assert_eq(indoc! {
1661            "The «ˇquick» brown"
1662        });
1663        cx.simulate_shared_keystrokes("g v").await;
1664        cx.shared_state().await.assert_eq(indoc! {
1665            "«Thˇ»e quick brown"
1666        });
1667
1668        cx.set_state(
1669            indoc! {"
1670            fiˇsh one
1671            fish two
1672            fish red
1673            fish blue
1674        "},
1675            Mode::Normal,
1676        );
1677        cx.simulate_keystrokes("4 g l escape escape g v");
1678        cx.assert_state(
1679            indoc! {"
1680                «fishˇ» one
1681                «fishˇ» two
1682                «fishˇ» red
1683                «fishˇ» blue
1684            "},
1685            Mode::Visual,
1686        );
1687        cx.simulate_keystrokes("y g v");
1688        cx.assert_state(
1689            indoc! {"
1690                «fishˇ» one
1691                «fishˇ» two
1692                «fishˇ» red
1693                «fishˇ» blue
1694            "},
1695            Mode::Visual,
1696        );
1697    }
1698}