visual.rs

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