visual.rs

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