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_map();
 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_map();
 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 = DisplayPoint::new(
 375                    row,
 376                    laid_out_line.closest_index_for_x(positions.start) as u32,
 377                );
 378                let mut end =
 379                    DisplayPoint::new(row, laid_out_line.closest_index_for_x(positions.end) as u32);
 380                if end <= start {
 381                    if start.column() == map.line_len(start.row()) {
 382                        end = start;
 383                    } else {
 384                        end = movement::saturating_right(map, start);
 385                    }
 386                }
 387
 388                if positions.start <= laid_out_line.width {
 389                    let selection = Selection {
 390                        id: s.new_selection_id(),
 391                        start: start.to_point(map),
 392                        end: end.to_point(map),
 393                        reversed: is_reversed &&
 394                                    // For neovim parity: cursor is not reversed when column is a single character
 395                                    end.column() - start.column() > 1,
 396                        goal,
 397                    };
 398
 399                    selections.push(selection);
 400                }
 401
 402                // When dealing with soft wrapped lines, it's possible that
 403                // `row` ends up being set to a value other than `head.row()` as
 404                // `head.row()` might be a `DisplayPoint` mapped to a soft
 405                // wrapped line, hence the need for `<=` and `>=` instead of
 406                // `==`.
 407                if going_up && row <= head.row() || !going_up && row >= head.row() {
 408                    break;
 409                }
 410
 411                // Find the next or previous buffer row where the `row` should
 412                // be moved to, so that wrapped lines are skipped.
 413                row = map
 414                    .start_of_relative_buffer_row(DisplayPoint::new(row, 0), direction)
 415                    .row();
 416            }
 417
 418            s.select(selections);
 419        })
 420    }
 421
 422    pub fn visual_object(
 423        &mut self,
 424        object: Object,
 425        count: Option<usize>,
 426        window: &mut Window,
 427        cx: &mut Context<Vim>,
 428    ) {
 429        if let Some(Operator::Object { around, .. }) = self.active_operator() {
 430            self.pop_operator(window, cx);
 431            let current_mode = self.mode;
 432            let target_mode = object.target_visual_mode(current_mode, around);
 433            if target_mode != current_mode {
 434                self.switch_mode(target_mode, true, window, cx);
 435            }
 436
 437            self.update_editor(cx, |_, editor, cx| {
 438                editor.change_selections(Default::default(), window, cx, |s| {
 439                    s.move_with(|map, selection| {
 440                        let mut mut_selection = selection.clone();
 441
 442                        // all our motions assume that the current character is
 443                        // after the cursor; however in the case of a visual selection
 444                        // the current character is before the cursor.
 445                        // But this will affect the judgment of the html tag
 446                        // so the html tag needs to skip this logic.
 447                        if !selection.reversed && object != Object::Tag {
 448                            mut_selection.set_head(
 449                                movement::left(map, mut_selection.head()),
 450                                mut_selection.goal,
 451                            );
 452                        }
 453
 454                        let original_point = selection.tail().to_point(map);
 455
 456                        if let Some(range) = object.range(map, mut_selection, around, true, count) {
 457                            if !range.is_empty() {
 458                                let expand_both_ways = object.always_expands_both_ways()
 459                                    || selection.is_empty()
 460                                    || movement::right(map, selection.start) == selection.end;
 461
 462                                if expand_both_ways {
 463                                    if selection.start == range.start
 464                                        && selection.end == range.end
 465                                        && object.always_expands_both_ways()
 466                                    {
 467                                        if let Some(range) = object.range(
 468                                            map,
 469                                            selection.clone(),
 470                                            around,
 471                                            true,
 472                                            count,
 473                                        ) {
 474                                            selection.start = range.start;
 475                                            selection.end = range.end;
 476                                        }
 477                                    } else {
 478                                        selection.start = range.start;
 479                                        selection.end = range.end;
 480                                    }
 481                                } else if selection.reversed {
 482                                    selection.start = range.start;
 483                                } else {
 484                                    selection.end = range.end;
 485                                }
 486                            }
 487
 488                            // In the visual selection result of a paragraph object, the cursor is
 489                            // placed at the start of the last line. And in the visual mode, the
 490                            // selection end is located after the end character. So, adjustment of
 491                            // selection end is needed.
 492                            //
 493                            // We don't do this adjustment for a one-line blank paragraph since the
 494                            // trailing newline is included in its selection from the beginning.
 495                            if object == Object::Paragraph && range.start != range.end {
 496                                let row_of_selection_end_line = selection.end.to_point(map).row;
 497                                let new_selection_end = if map
 498                                    .buffer_snapshot()
 499                                    .line_len(MultiBufferRow(row_of_selection_end_line))
 500                                    == 0
 501                                {
 502                                    Point::new(row_of_selection_end_line + 1, 0)
 503                                } else {
 504                                    Point::new(row_of_selection_end_line, 1)
 505                                };
 506                                selection.end = new_selection_end.to_display_point(map);
 507                            }
 508
 509                            // To match vim, if the range starts of the same line as it originally
 510                            // did, we keep the tail of the selection in the same place instead of
 511                            // snapping it to the start of the line
 512                            if target_mode == Mode::VisualLine {
 513                                let new_start_point = selection.start.to_point(map);
 514                                if new_start_point.row == original_point.row {
 515                                    if selection.end.to_point(map).row > new_start_point.row {
 516                                        if original_point.column
 517                                            == map
 518                                                .buffer_snapshot()
 519                                                .line_len(MultiBufferRow(original_point.row))
 520                                        {
 521                                            selection.start = movement::saturating_left(
 522                                                map,
 523                                                original_point.to_display_point(map),
 524                                            )
 525                                        } else {
 526                                            selection.start = original_point.to_display_point(map)
 527                                        }
 528                                    } else {
 529                                        selection.end = movement::saturating_right(
 530                                            map,
 531                                            original_point.to_display_point(map),
 532                                        );
 533                                        if original_point.column > 0 {
 534                                            selection.reversed = true
 535                                        }
 536                                    }
 537                                }
 538                            }
 539                        }
 540                    });
 541                });
 542            });
 543        }
 544    }
 545
 546    fn visual_insert_end_of_line(
 547        &mut self,
 548        _: &VisualInsertEndOfLine,
 549        window: &mut Window,
 550        cx: &mut Context<Self>,
 551    ) {
 552        self.update_editor(cx, |_, editor, cx| {
 553            editor.split_selection_into_lines(&Default::default(), window, cx);
 554            editor.change_selections(Default::default(), window, cx, |s| {
 555                s.move_cursors_with(|map, cursor, _| {
 556                    (next_line_end(map, cursor, 1), SelectionGoal::None)
 557                });
 558            });
 559        });
 560
 561        self.switch_mode(Mode::Insert, false, window, cx);
 562    }
 563
 564    fn visual_insert_first_non_white_space(
 565        &mut self,
 566        _: &VisualInsertFirstNonWhiteSpace,
 567        window: &mut Window,
 568        cx: &mut Context<Self>,
 569    ) {
 570        self.update_editor(cx, |_, editor, cx| {
 571            editor.split_selection_into_lines(&Default::default(), window, cx);
 572            editor.change_selections(Default::default(), window, cx, |s| {
 573                s.move_cursors_with(|map, cursor, _| {
 574                    (
 575                        first_non_whitespace(map, false, cursor),
 576                        SelectionGoal::None,
 577                    )
 578                });
 579            });
 580        });
 581
 582        self.switch_mode(Mode::Insert, false, window, cx);
 583    }
 584
 585    fn toggle_mode(&mut self, mode: Mode, window: &mut Window, cx: &mut Context<Self>) {
 586        if self.mode == mode {
 587            self.switch_mode(Mode::Normal, false, window, cx);
 588        } else {
 589            self.switch_mode(mode, false, window, cx);
 590        }
 591    }
 592
 593    pub fn other_end(&mut self, _: &OtherEnd, window: &mut Window, cx: &mut Context<Self>) {
 594        self.update_editor(cx, |_, editor, cx| {
 595            editor.change_selections(Default::default(), window, cx, |s| {
 596                s.move_with(|_, selection| {
 597                    selection.reversed = !selection.reversed;
 598                });
 599            })
 600        });
 601    }
 602
 603    pub fn other_end_row_aware(
 604        &mut self,
 605        _: &OtherEndRowAware,
 606        window: &mut Window,
 607        cx: &mut Context<Self>,
 608    ) {
 609        let mode = self.mode;
 610        self.update_editor(cx, |_, editor, cx| {
 611            editor.change_selections(Default::default(), window, cx, |s| {
 612                s.move_with(|_, selection| {
 613                    selection.reversed = !selection.reversed;
 614                });
 615                if mode == Mode::VisualBlock {
 616                    s.reverse_selections();
 617                }
 618            })
 619        });
 620    }
 621
 622    pub fn visual_delete(&mut self, line_mode: bool, window: &mut Window, cx: &mut Context<Self>) {
 623        self.store_visual_marks(window, cx);
 624        self.update_editor(cx, |vim, editor, cx| {
 625            let mut original_columns: HashMap<_, _> = Default::default();
 626            let line_mode = line_mode || editor.selections.line_mode();
 627            editor.selections.set_line_mode(false);
 628
 629            editor.transact(window, cx, |editor, window, cx| {
 630                editor.change_selections(Default::default(), window, cx, |s| {
 631                    s.move_with(|map, selection| {
 632                        if line_mode {
 633                            let mut position = selection.head();
 634                            if !selection.reversed {
 635                                position = movement::left(map, position);
 636                            }
 637                            original_columns.insert(selection.id, position.to_point(map).column);
 638                            if vim.mode == Mode::VisualBlock {
 639                                *selection.end.column_mut() = map.line_len(selection.end.row())
 640                            } else {
 641                                let start = selection.start.to_point(map);
 642                                let end = selection.end.to_point(map);
 643                                selection.start = map.prev_line_boundary(start).1;
 644                                if end.column == 0 && end > start {
 645                                    let row = end.row.saturating_sub(1);
 646                                    selection.end = Point::new(
 647                                        row,
 648                                        map.buffer_snapshot().line_len(MultiBufferRow(row)),
 649                                    )
 650                                    .to_display_point(map)
 651                                } else {
 652                                    selection.end = map.next_line_boundary(end).1;
 653                                }
 654                            }
 655                        }
 656                        selection.goal = SelectionGoal::None;
 657                    });
 658                });
 659                let kind = if line_mode {
 660                    MotionKind::Linewise
 661                } else {
 662                    MotionKind::Exclusive
 663                };
 664                vim.copy_selections_content(editor, kind, window, cx);
 665
 666                if line_mode && vim.mode != Mode::VisualBlock {
 667                    editor.change_selections(Default::default(), window, cx, |s| {
 668                        s.move_with(|map, selection| {
 669                            let end = selection.end.to_point(map);
 670                            let start = selection.start.to_point(map);
 671                            if end.row < map.buffer_snapshot().max_point().row {
 672                                selection.end = Point::new(end.row + 1, 0).to_display_point(map)
 673                            } else if start.row > 0 {
 674                                selection.start = Point::new(
 675                                    start.row - 1,
 676                                    map.buffer_snapshot()
 677                                        .line_len(MultiBufferRow(start.row - 1)),
 678                                )
 679                                .to_display_point(map)
 680                            }
 681                        });
 682                    });
 683                }
 684                editor.insert("", window, cx);
 685
 686                // Fixup cursor position after the deletion
 687                editor.set_clip_at_line_ends(true, cx);
 688                editor.change_selections(Default::default(), window, cx, |s| {
 689                    s.move_with(|map, selection| {
 690                        let mut cursor = selection.head().to_point(map);
 691
 692                        if let Some(column) = original_columns.get(&selection.id) {
 693                            cursor.column = *column
 694                        }
 695                        let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left);
 696                        selection.collapse_to(cursor, selection.goal)
 697                    });
 698                    if vim.mode == Mode::VisualBlock {
 699                        s.select_anchors(vec![s.first_anchor()])
 700                    }
 701                });
 702            })
 703        });
 704        self.switch_mode(Mode::Normal, true, window, cx);
 705    }
 706
 707    pub fn visual_yank(&mut self, line_mode: bool, window: &mut Window, cx: &mut Context<Self>) {
 708        self.store_visual_marks(window, cx);
 709        self.update_editor(cx, |vim, editor, cx| {
 710            let line_mode = line_mode || editor.selections.line_mode();
 711
 712            // For visual line mode, adjust selections to avoid yanking the next line when on \n
 713            if line_mode && vim.mode != Mode::VisualBlock {
 714                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
 715                    s.move_with(|map, selection| {
 716                        let start = selection.start.to_point(map);
 717                        let end = selection.end.to_point(map);
 718                        if end.column == 0 && end > start {
 719                            let row = end.row.saturating_sub(1);
 720                            selection.end = Point::new(
 721                                row,
 722                                map.buffer_snapshot().line_len(MultiBufferRow(row)),
 723                            )
 724                            .to_display_point(map);
 725                        }
 726                    });
 727                });
 728            }
 729
 730            editor.selections.set_line_mode(line_mode);
 731            let kind = if line_mode {
 732                MotionKind::Linewise
 733            } else {
 734                MotionKind::Exclusive
 735            };
 736            vim.yank_selections_content(editor, kind, window, cx);
 737            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
 738                s.move_with(|map, selection| {
 739                    if line_mode {
 740                        selection.start = start_of_line(map, false, selection.start);
 741                    };
 742                    selection.collapse_to(selection.start, SelectionGoal::None)
 743                });
 744                if vim.mode == Mode::VisualBlock {
 745                    s.select_anchors(vec![s.first_anchor()])
 746                }
 747            });
 748        });
 749        self.switch_mode(Mode::Normal, true, window, cx);
 750    }
 751
 752    pub(crate) fn visual_replace(
 753        &mut self,
 754        text: Arc<str>,
 755        window: &mut Window,
 756        cx: &mut Context<Self>,
 757    ) {
 758        self.stop_recording(cx);
 759        self.update_editor(cx, |_, editor, cx| {
 760            editor.transact(window, cx, |editor, window, cx| {
 761                let display_map = editor.display_snapshot(cx);
 762                let selections = editor.selections.all_adjusted_display(&display_map);
 763
 764                // Selections are biased right at the start. So we need to store
 765                // anchors that are biased left so that we can restore the selections
 766                // after the change
 767                let stable_anchors = editor
 768                    .selections
 769                    .disjoint_anchors_arc()
 770                    .iter()
 771                    .map(|selection| {
 772                        let start = selection.start.bias_left(&display_map.buffer_snapshot());
 773                        start..start
 774                    })
 775                    .collect::<Vec<_>>();
 776
 777                let mut edits = Vec::new();
 778                for selection in selections.iter() {
 779                    let selection = selection.clone();
 780                    for row_range in
 781                        movement::split_display_range_by_lines(&display_map, selection.range())
 782                    {
 783                        let range = row_range.start.to_offset(&display_map, Bias::Right)
 784                            ..row_range.end.to_offset(&display_map, Bias::Right);
 785                        let text = text.repeat(range.len());
 786                        edits.push((range, text));
 787                    }
 788                }
 789
 790                editor.edit(edits, cx);
 791                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
 792                    s.select_ranges(stable_anchors)
 793                });
 794            });
 795        });
 796        self.switch_mode(Mode::Normal, false, window, cx);
 797    }
 798
 799    pub fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
 800        Vim::take_forced_motion(cx);
 801        let count =
 802            Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 });
 803        self.update_editor(cx, |_, editor, cx| {
 804            editor.set_clip_at_line_ends(false, cx);
 805            for _ in 0..count {
 806                if editor
 807                    .select_next(&Default::default(), window, cx)
 808                    .log_err()
 809                    .is_none()
 810                {
 811                    break;
 812                }
 813            }
 814        });
 815    }
 816
 817    pub fn select_previous(
 818        &mut self,
 819        _: &SelectPrevious,
 820        window: &mut Window,
 821        cx: &mut Context<Self>,
 822    ) {
 823        Vim::take_forced_motion(cx);
 824        let count =
 825            Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 });
 826        self.update_editor(cx, |_, editor, cx| {
 827            for _ in 0..count {
 828                if editor
 829                    .select_previous(&Default::default(), window, cx)
 830                    .log_err()
 831                    .is_none()
 832                {
 833                    break;
 834                }
 835            }
 836        });
 837    }
 838
 839    pub fn select_match(
 840        &mut self,
 841        direction: Direction,
 842        window: &mut Window,
 843        cx: &mut Context<Self>,
 844    ) {
 845        Vim::take_forced_motion(cx);
 846        let count = Vim::take_count(cx).unwrap_or(1);
 847        let Some(pane) = self.pane(window, cx) else {
 848            return;
 849        };
 850        let vim_is_normal = self.mode == Mode::Normal;
 851        let mut start_selection = 0usize;
 852        let mut end_selection = 0usize;
 853
 854        self.update_editor(cx, |_, editor, _| {
 855            editor.set_collapse_matches(false);
 856        });
 857        if vim_is_normal {
 858            pane.update(cx, |pane, cx| {
 859                if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()
 860                {
 861                    search_bar.update(cx, |search_bar, cx| {
 862                        if !search_bar.has_active_match() || !search_bar.show(window, cx) {
 863                            return;
 864                        }
 865                        // without update_match_index there is a bug when the cursor is before the first match
 866                        search_bar.update_match_index(window, cx);
 867                        search_bar.select_match(direction.opposite(), 1, window, cx);
 868                    });
 869                }
 870            });
 871        }
 872        self.update_editor(cx, |_, editor, cx| {
 873            let latest = editor
 874                .selections
 875                .newest::<usize>(&editor.display_snapshot(cx));
 876            start_selection = latest.start;
 877            end_selection = latest.end;
 878        });
 879
 880        let mut match_exists = false;
 881        pane.update(cx, |pane, cx| {
 882            if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 883                search_bar.update(cx, |search_bar, cx| {
 884                    search_bar.update_match_index(window, cx);
 885                    search_bar.select_match(direction, count, window, cx);
 886                    match_exists = search_bar.match_exists(window, cx);
 887                });
 888            }
 889        });
 890        if !match_exists {
 891            self.clear_operator(window, cx);
 892            self.stop_replaying(cx);
 893            return;
 894        }
 895        self.update_editor(cx, |_, editor, cx| {
 896            let latest = editor
 897                .selections
 898                .newest::<usize>(&editor.display_snapshot(cx));
 899            if vim_is_normal {
 900                start_selection = latest.start;
 901                end_selection = latest.end;
 902            } else {
 903                start_selection = start_selection.min(latest.start);
 904                end_selection = end_selection.max(latest.end);
 905            }
 906            if direction == Direction::Prev {
 907                std::mem::swap(&mut start_selection, &mut end_selection);
 908            }
 909            editor.change_selections(Default::default(), window, cx, |s| {
 910                s.select_ranges([start_selection..end_selection]);
 911            });
 912            editor.set_collapse_matches(true);
 913        });
 914
 915        match self.maybe_pop_operator() {
 916            Some(Operator::Change) => self.substitute(None, false, window, cx),
 917            Some(Operator::Delete) => {
 918                self.stop_recording(cx);
 919                self.visual_delete(false, window, cx)
 920            }
 921            Some(Operator::Yank) => self.visual_yank(false, window, cx),
 922            _ => {} // Ignoring other operators
 923        }
 924    }
 925}
 926#[cfg(test)]
 927mod test {
 928    use indoc::indoc;
 929    use workspace::item::Item;
 930
 931    use crate::{
 932        state::Mode,
 933        test::{NeovimBackedTestContext, VimTestContext},
 934    };
 935
 936    #[gpui::test]
 937    async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
 938        let mut cx = NeovimBackedTestContext::new(cx).await;
 939
 940        cx.set_shared_state(indoc! {
 941            "The ˇquick brown
 942            fox jumps over
 943            the lazy dog"
 944        })
 945        .await;
 946        let cursor = cx.update_editor(|editor, _, cx| editor.pixel_position_of_cursor(cx));
 947
 948        // entering visual mode should select the character
 949        // under cursor
 950        cx.simulate_shared_keystrokes("v").await;
 951        cx.shared_state()
 952            .await
 953            .assert_eq(indoc! { "The «qˇ»uick brown
 954            fox jumps over
 955            the lazy dog"});
 956        cx.update_editor(|editor, _, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
 957
 958        // forwards motions should extend the selection
 959        cx.simulate_shared_keystrokes("w j").await;
 960        cx.shared_state().await.assert_eq(indoc! { "The «quick brown
 961            fox jumps oˇ»ver
 962            the lazy dog"});
 963
 964        cx.simulate_shared_keystrokes("escape").await;
 965        cx.shared_state().await.assert_eq(indoc! { "The quick brown
 966            fox jumps ˇover
 967            the lazy dog"});
 968
 969        // motions work backwards
 970        cx.simulate_shared_keystrokes("v k b").await;
 971        cx.shared_state()
 972            .await
 973            .assert_eq(indoc! { "The «ˇquick brown
 974            fox jumps o»ver
 975            the lazy dog"});
 976
 977        // works on empty lines
 978        cx.set_shared_state(indoc! {"
 979            a
 980            ˇ
 981            b
 982            "})
 983            .await;
 984        let cursor = cx.update_editor(|editor, _, cx| editor.pixel_position_of_cursor(cx));
 985        cx.simulate_shared_keystrokes("v").await;
 986        cx.shared_state().await.assert_eq(indoc! {"
 987            a
 988            «
 989            ˇ»b
 990        "});
 991        cx.update_editor(|editor, _, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
 992
 993        // toggles off again
 994        cx.simulate_shared_keystrokes("v").await;
 995        cx.shared_state().await.assert_eq(indoc! {"
 996            a
 997            ˇ
 998            b
 999            "});
1000
1001        // works at the end of a document
1002        cx.set_shared_state(indoc! {"
1003            a
1004            b
1005            ˇ"})
1006            .await;
1007
1008        cx.simulate_shared_keystrokes("v").await;
1009        cx.shared_state().await.assert_eq(indoc! {"
1010            a
1011            b
1012            ˇ"});
1013    }
1014
1015    #[gpui::test]
1016    async fn test_visual_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
1017        let mut cx = VimTestContext::new(cx, true).await;
1018
1019        cx.set_state(
1020            indoc! {
1021                "«The quick brown
1022                fox jumps over
1023                the lazy dogˇ»"
1024            },
1025            Mode::Visual,
1026        );
1027        cx.simulate_keystrokes("g shift-i");
1028        cx.assert_state(
1029            indoc! {
1030                "ˇThe quick brown
1031                ˇfox jumps over
1032                ˇthe lazy dog"
1033            },
1034            Mode::Insert,
1035        );
1036    }
1037
1038    #[gpui::test]
1039    async fn test_visual_insert_end_of_line(cx: &mut gpui::TestAppContext) {
1040        let mut cx = VimTestContext::new(cx, true).await;
1041
1042        cx.set_state(
1043            indoc! {
1044                "«The quick brown
1045                fox jumps over
1046                the lazy dogˇ»"
1047            },
1048            Mode::Visual,
1049        );
1050        cx.simulate_keystrokes("g shift-a");
1051        cx.assert_state(
1052            indoc! {
1053                "The quick brownˇ
1054                fox jumps overˇ
1055                the lazy dogˇ"
1056            },
1057            Mode::Insert,
1058        );
1059    }
1060
1061    #[gpui::test]
1062    async fn test_enter_visual_line_mode(cx: &mut gpui::TestAppContext) {
1063        let mut cx = NeovimBackedTestContext::new(cx).await;
1064
1065        cx.set_shared_state(indoc! {
1066            "The ˇquick brown
1067            fox jumps over
1068            the lazy dog"
1069        })
1070        .await;
1071        cx.simulate_shared_keystrokes("shift-v").await;
1072        cx.shared_state()
1073            .await
1074            .assert_eq(indoc! { "The «qˇ»uick brown
1075            fox jumps over
1076            the lazy dog"});
1077        cx.simulate_shared_keystrokes("x").await;
1078        cx.shared_state().await.assert_eq(indoc! { "fox ˇjumps over
1079        the lazy dog"});
1080
1081        // it should work on empty lines
1082        cx.set_shared_state(indoc! {"
1083            a
1084            ˇ
1085            b"})
1086            .await;
1087        cx.simulate_shared_keystrokes("shift-v").await;
1088        cx.shared_state().await.assert_eq(indoc! {"
1089            a
1090            «
1091            ˇ»b"});
1092        cx.simulate_shared_keystrokes("x").await;
1093        cx.shared_state().await.assert_eq(indoc! {"
1094            a
1095            ˇb"});
1096
1097        // it should work at the end of the document
1098        cx.set_shared_state(indoc! {"
1099            a
1100            b
1101            ˇ"})
1102            .await;
1103        let cursor = cx.update_editor(|editor, _, cx| editor.pixel_position_of_cursor(cx));
1104        cx.simulate_shared_keystrokes("shift-v").await;
1105        cx.shared_state().await.assert_eq(indoc! {"
1106            a
1107            b
1108            ˇ"});
1109        cx.update_editor(|editor, _, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
1110        cx.simulate_shared_keystrokes("x").await;
1111        cx.shared_state().await.assert_eq(indoc! {"
1112            a
1113            ˇb"});
1114    }
1115
1116    #[gpui::test]
1117    async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
1118        let mut cx = NeovimBackedTestContext::new(cx).await;
1119
1120        cx.simulate("v w", "The quick ˇbrown")
1121            .await
1122            .assert_matches();
1123
1124        cx.simulate("v w x", "The quick ˇbrown")
1125            .await
1126            .assert_matches();
1127        cx.simulate(
1128            "v w j x",
1129            indoc! {"
1130                The ˇquick brown
1131                fox jumps over
1132                the lazy dog"},
1133        )
1134        .await
1135        .assert_matches();
1136        // Test pasting code copied on delete
1137        cx.simulate_shared_keystrokes("j p").await;
1138        cx.shared_state().await.assert_matches();
1139
1140        cx.simulate_at_each_offset(
1141            "v w j x",
1142            indoc! {"
1143                The ˇquick brown
1144                fox jumps over
1145                the ˇlazy dog"},
1146        )
1147        .await
1148        .assert_matches();
1149        cx.simulate_at_each_offset(
1150            "v b k x",
1151            indoc! {"
1152                The ˇquick brown
1153                fox jumps ˇover
1154                the ˇlazy dog"},
1155        )
1156        .await
1157        .assert_matches();
1158    }
1159
1160    #[gpui::test]
1161    async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
1162        let mut cx = NeovimBackedTestContext::new(cx).await;
1163
1164        cx.set_shared_state(indoc! {"
1165                The quˇick brown
1166                fox jumps over
1167                the lazy dog"})
1168            .await;
1169        cx.simulate_shared_keystrokes("shift-v x").await;
1170        cx.shared_state().await.assert_matches();
1171
1172        // Test pasting code copied on delete
1173        cx.simulate_shared_keystrokes("p").await;
1174        cx.shared_state().await.assert_matches();
1175
1176        cx.set_shared_state(indoc! {"
1177                The quick brown
1178                fox jumps over
1179                the laˇzy dog"})
1180            .await;
1181        cx.simulate_shared_keystrokes("shift-v x").await;
1182        cx.shared_state().await.assert_matches();
1183        cx.shared_clipboard().await.assert_eq("the lazy dog\n");
1184
1185        cx.set_shared_state(indoc! {"
1186                                The quˇick brown
1187                                fox jumps over
1188                                the lazy dog"})
1189            .await;
1190        cx.simulate_shared_keystrokes("shift-v j x").await;
1191        cx.shared_state().await.assert_matches();
1192        // Test pasting code copied on delete
1193        cx.simulate_shared_keystrokes("p").await;
1194        cx.shared_state().await.assert_matches();
1195
1196        cx.set_shared_state(indoc! {"
1197            The ˇlong line
1198            should not
1199            crash
1200            "})
1201            .await;
1202        cx.simulate_shared_keystrokes("shift-v $ x").await;
1203        cx.shared_state().await.assert_matches();
1204    }
1205
1206    #[gpui::test]
1207    async fn test_visual_yank(cx: &mut gpui::TestAppContext) {
1208        let mut cx = NeovimBackedTestContext::new(cx).await;
1209
1210        cx.set_shared_state("The quick ˇbrown").await;
1211        cx.simulate_shared_keystrokes("v w y").await;
1212        cx.shared_state().await.assert_eq("The quick ˇbrown");
1213        cx.shared_clipboard().await.assert_eq("brown");
1214
1215        cx.set_shared_state(indoc! {"
1216                The ˇquick brown
1217                fox jumps over
1218                the lazy dog"})
1219            .await;
1220        cx.simulate_shared_keystrokes("v w j y").await;
1221        cx.shared_state().await.assert_eq(indoc! {"
1222                    The ˇquick brown
1223                    fox jumps over
1224                    the lazy dog"});
1225        cx.shared_clipboard().await.assert_eq(indoc! {"
1226                quick brown
1227                fox jumps o"});
1228
1229        cx.set_shared_state(indoc! {"
1230                    The quick brown
1231                    fox jumps over
1232                    the ˇlazy dog"})
1233            .await;
1234        cx.simulate_shared_keystrokes("v w j y").await;
1235        cx.shared_state().await.assert_eq(indoc! {"
1236                    The quick brown
1237                    fox jumps over
1238                    the ˇlazy dog"});
1239        cx.shared_clipboard().await.assert_eq("lazy d");
1240        cx.simulate_shared_keystrokes("shift-v y").await;
1241        cx.shared_clipboard().await.assert_eq("the lazy dog\n");
1242
1243        cx.set_shared_state(indoc! {"
1244                    The ˇquick brown
1245                    fox jumps over
1246                    the lazy dog"})
1247            .await;
1248        cx.simulate_shared_keystrokes("v b k y").await;
1249        cx.shared_state().await.assert_eq(indoc! {"
1250                    ˇThe quick brown
1251                    fox jumps over
1252                    the lazy dog"});
1253        assert_eq!(
1254            cx.read_from_clipboard()
1255                .map(|item| item.text().unwrap())
1256                .unwrap(),
1257            "The q"
1258        );
1259
1260        cx.set_shared_state(indoc! {"
1261                    The quick brown
1262                    fox ˇjumps over
1263                    the lazy dog"})
1264            .await;
1265        cx.simulate_shared_keystrokes("shift-v shift-g shift-y")
1266            .await;
1267        cx.shared_state().await.assert_eq(indoc! {"
1268                    The quick brown
1269                    ˇfox jumps over
1270                    the lazy dog"});
1271        cx.shared_clipboard()
1272            .await
1273            .assert_eq("fox jumps over\nthe lazy dog\n");
1274
1275        cx.set_shared_state(indoc! {"
1276                    The quick brown
1277                    fox ˇjumps over
1278                    the lazy dog"})
1279            .await;
1280        cx.simulate_shared_keystrokes("shift-v $ shift-y").await;
1281        cx.shared_state().await.assert_eq(indoc! {"
1282                    The quick brown
1283                    ˇfox jumps over
1284                    the lazy dog"});
1285        cx.shared_clipboard().await.assert_eq("fox jumps over\n");
1286    }
1287
1288    #[gpui::test]
1289    async fn test_visual_block_mode(cx: &mut gpui::TestAppContext) {
1290        let mut cx = NeovimBackedTestContext::new(cx).await;
1291
1292        cx.set_shared_state(indoc! {
1293            "The ˇquick brown
1294             fox jumps over
1295             the lazy dog"
1296        })
1297        .await;
1298        cx.simulate_shared_keystrokes("ctrl-v").await;
1299        cx.shared_state().await.assert_eq(indoc! {
1300            "The «qˇ»uick brown
1301            fox jumps over
1302            the lazy dog"
1303        });
1304        cx.simulate_shared_keystrokes("2 down").await;
1305        cx.shared_state().await.assert_eq(indoc! {
1306            "The «qˇ»uick brown
1307            fox «jˇ»umps over
1308            the «lˇ»azy dog"
1309        });
1310        cx.simulate_shared_keystrokes("e").await;
1311        cx.shared_state().await.assert_eq(indoc! {
1312            "The «quicˇ»k brown
1313            fox «jumpˇ»s over
1314            the «lazyˇ» dog"
1315        });
1316        cx.simulate_shared_keystrokes("^").await;
1317        cx.shared_state().await.assert_eq(indoc! {
1318            "«ˇThe q»uick brown
1319            «ˇfox j»umps over
1320            «ˇthe l»azy dog"
1321        });
1322        cx.simulate_shared_keystrokes("$").await;
1323        cx.shared_state().await.assert_eq(indoc! {
1324            "The «quick brownˇ»
1325            fox «jumps overˇ»
1326            the «lazy dogˇ»"
1327        });
1328        cx.simulate_shared_keystrokes("shift-f space").await;
1329        cx.shared_state().await.assert_eq(indoc! {
1330            "The «quickˇ» brown
1331            fox «jumpsˇ» over
1332            the «lazy ˇ»dog"
1333        });
1334
1335        // toggling through visual mode works as expected
1336        cx.simulate_shared_keystrokes("v").await;
1337        cx.shared_state().await.assert_eq(indoc! {
1338            "The «quick brown
1339            fox jumps over
1340            the lazy ˇ»dog"
1341        });
1342        cx.simulate_shared_keystrokes("ctrl-v").await;
1343        cx.shared_state().await.assert_eq(indoc! {
1344            "The «quickˇ» brown
1345            fox «jumpsˇ» over
1346            the «lazy ˇ»dog"
1347        });
1348
1349        cx.set_shared_state(indoc! {
1350            "The ˇquick
1351             brown
1352             fox
1353             jumps over the
1354
1355             lazy dog
1356            "
1357        })
1358        .await;
1359        cx.simulate_shared_keystrokes("ctrl-v down down").await;
1360        cx.shared_state().await.assert_eq(indoc! {
1361            "The«ˇ q»uick
1362            bro«ˇwn»
1363            foxˇ
1364            jumps over the
1365
1366            lazy dog
1367            "
1368        });
1369        cx.simulate_shared_keystrokes("down").await;
1370        cx.shared_state().await.assert_eq(indoc! {
1371            "The «qˇ»uick
1372            brow«nˇ»
1373            fox
1374            jump«sˇ» over the
1375
1376            lazy dog
1377            "
1378        });
1379        cx.simulate_shared_keystrokes("left").await;
1380        cx.shared_state().await.assert_eq(indoc! {
1381            "The«ˇ q»uick
1382            bro«ˇwn»
1383            foxˇ
1384            jum«ˇps» over the
1385
1386            lazy dog
1387            "
1388        });
1389        cx.simulate_shared_keystrokes("s o escape").await;
1390        cx.shared_state().await.assert_eq(indoc! {
1391            "Theˇouick
1392            broo
1393            foxo
1394            jumo over the
1395
1396            lazy dog
1397            "
1398        });
1399
1400        // https://github.com/zed-industries/zed/issues/6274
1401        cx.set_shared_state(indoc! {
1402            "Theˇ quick brown
1403
1404            fox jumps over
1405            the lazy dog
1406            "
1407        })
1408        .await;
1409        cx.simulate_shared_keystrokes("l ctrl-v j j").await;
1410        cx.shared_state().await.assert_eq(indoc! {
1411            "The «qˇ»uick brown
1412
1413            fox «jˇ»umps over
1414            the lazy dog
1415            "
1416        });
1417    }
1418
1419    #[gpui::test]
1420    async fn test_visual_block_issue_2123(cx: &mut gpui::TestAppContext) {
1421        let mut cx = NeovimBackedTestContext::new(cx).await;
1422
1423        cx.set_shared_state(indoc! {
1424            "The ˇquick brown
1425            fox jumps over
1426            the lazy dog
1427            "
1428        })
1429        .await;
1430        cx.simulate_shared_keystrokes("ctrl-v right down").await;
1431        cx.shared_state().await.assert_eq(indoc! {
1432            "The «quˇ»ick brown
1433            fox «juˇ»mps over
1434            the lazy dog
1435            "
1436        });
1437    }
1438    #[gpui::test]
1439    async fn test_visual_block_mode_down_right(cx: &mut gpui::TestAppContext) {
1440        let mut cx = NeovimBackedTestContext::new(cx).await;
1441        cx.set_shared_state(indoc! {"
1442            The ˇquick brown
1443            fox jumps over
1444            the lazy dog"})
1445            .await;
1446        cx.simulate_shared_keystrokes("ctrl-v l l l l l j").await;
1447        cx.shared_state().await.assert_eq(indoc! {"
1448            The «quick ˇ»brown
1449            fox «jumps ˇ»over
1450            the lazy dog"});
1451    }
1452
1453    #[gpui::test]
1454    async fn test_visual_block_mode_up_left(cx: &mut gpui::TestAppContext) {
1455        let mut cx = NeovimBackedTestContext::new(cx).await;
1456        cx.set_shared_state(indoc! {"
1457            The quick brown
1458            fox jumpsˇ over
1459            the lazy dog"})
1460            .await;
1461        cx.simulate_shared_keystrokes("ctrl-v h h h h h k").await;
1462        cx.shared_state().await.assert_eq(indoc! {"
1463            The «ˇquick »brown
1464            fox «ˇjumps »over
1465            the lazy dog"});
1466    }
1467
1468    #[gpui::test]
1469    async fn test_visual_block_mode_other_end(cx: &mut gpui::TestAppContext) {
1470        let mut cx = NeovimBackedTestContext::new(cx).await;
1471        cx.set_shared_state(indoc! {"
1472            The quick brown
1473            fox jˇumps over
1474            the lazy dog"})
1475            .await;
1476        cx.simulate_shared_keystrokes("ctrl-v l l l l j").await;
1477        cx.shared_state().await.assert_eq(indoc! {"
1478            The quick brown
1479            fox j«umps ˇ»over
1480            the l«azy dˇ»og"});
1481        cx.simulate_shared_keystrokes("o k").await;
1482        cx.shared_state().await.assert_eq(indoc! {"
1483            The q«ˇuick »brown
1484            fox j«ˇumps »over
1485            the l«ˇazy d»og"});
1486    }
1487
1488    #[gpui::test]
1489    async fn test_visual_block_mode_shift_other_end(cx: &mut gpui::TestAppContext) {
1490        let mut cx = NeovimBackedTestContext::new(cx).await;
1491        cx.set_shared_state(indoc! {"
1492            The quick brown
1493            fox jˇumps over
1494            the lazy dog"})
1495            .await;
1496        cx.simulate_shared_keystrokes("ctrl-v l l l l j").await;
1497        cx.shared_state().await.assert_eq(indoc! {"
1498            The quick brown
1499            fox j«umps ˇ»over
1500            the l«azy dˇ»og"});
1501        cx.simulate_shared_keystrokes("shift-o k").await;
1502        cx.shared_state().await.assert_eq(indoc! {"
1503            The quick brown
1504            fox j«ˇumps »over
1505            the lazy dog"});
1506    }
1507
1508    #[gpui::test]
1509    async fn test_visual_block_insert(cx: &mut gpui::TestAppContext) {
1510        let mut cx = NeovimBackedTestContext::new(cx).await;
1511
1512        cx.set_shared_state(indoc! {
1513            "ˇThe quick brown
1514            fox jumps over
1515            the lazy dog
1516            "
1517        })
1518        .await;
1519        cx.simulate_shared_keystrokes("ctrl-v 9 down").await;
1520        cx.shared_state().await.assert_eq(indoc! {
1521            "«Tˇ»he quick brown
1522            «fˇ»ox jumps over
1523            «tˇ»he lazy dog
1524            ˇ"
1525        });
1526
1527        cx.simulate_shared_keystrokes("shift-i k escape").await;
1528        cx.shared_state().await.assert_eq(indoc! {
1529            "ˇkThe quick brown
1530            kfox jumps over
1531            kthe lazy dog
1532            k"
1533        });
1534
1535        cx.set_shared_state(indoc! {
1536            "ˇThe quick brown
1537            fox jumps over
1538            the lazy dog
1539            "
1540        })
1541        .await;
1542        cx.simulate_shared_keystrokes("ctrl-v 9 down").await;
1543        cx.shared_state().await.assert_eq(indoc! {
1544            "«Tˇ»he quick brown
1545            «fˇ»ox jumps over
1546            «tˇ»he lazy dog
1547            ˇ"
1548        });
1549        cx.simulate_shared_keystrokes("c k escape").await;
1550        cx.shared_state().await.assert_eq(indoc! {
1551            "ˇkhe quick brown
1552            kox jumps over
1553            khe lazy dog
1554            k"
1555        });
1556    }
1557
1558    #[gpui::test]
1559    async fn test_visual_block_wrapping_selection(cx: &mut gpui::TestAppContext) {
1560        let mut cx = NeovimBackedTestContext::new(cx).await;
1561
1562        // Ensure that the editor is wrapping lines at 12 columns so that each
1563        // of the lines ends up being wrapped.
1564        cx.set_shared_wrap(12).await;
1565        cx.set_shared_state(indoc! {
1566            "ˇ12345678901234567890
1567            12345678901234567890
1568            12345678901234567890
1569            "
1570        })
1571        .await;
1572        cx.simulate_shared_keystrokes("ctrl-v j").await;
1573        cx.shared_state().await.assert_eq(indoc! {
1574            "«1ˇ»2345678901234567890
1575            «1ˇ»2345678901234567890
1576            12345678901234567890
1577            "
1578        });
1579
1580        // Test with lines taking up different amounts of display rows to ensure
1581        // that, even in that case, only the buffer rows are taken into account.
1582        cx.set_shared_state(indoc! {
1583            "ˇ123456789012345678901234567890123456789012345678901234567890
1584            1234567890123456789012345678901234567890
1585            12345678901234567890
1586            "
1587        })
1588        .await;
1589        cx.simulate_shared_keystrokes("ctrl-v 2 j").await;
1590        cx.shared_state().await.assert_eq(indoc! {
1591            "«1ˇ»23456789012345678901234567890123456789012345678901234567890
1592            «1ˇ»234567890123456789012345678901234567890
1593            «1ˇ»2345678901234567890
1594            "
1595        });
1596
1597        // Same scenario as above, but using the up motion to ensure that the
1598        // result is the same.
1599        cx.set_shared_state(indoc! {
1600            "123456789012345678901234567890123456789012345678901234567890
1601            1234567890123456789012345678901234567890
1602            ˇ12345678901234567890
1603            "
1604        })
1605        .await;
1606        cx.simulate_shared_keystrokes("ctrl-v 2 k").await;
1607        cx.shared_state().await.assert_eq(indoc! {
1608            "«1ˇ»23456789012345678901234567890123456789012345678901234567890
1609            «1ˇ»234567890123456789012345678901234567890
1610            «1ˇ»2345678901234567890
1611            "
1612        });
1613    }
1614
1615    #[gpui::test]
1616    async fn test_visual_object(cx: &mut gpui::TestAppContext) {
1617        let mut cx = NeovimBackedTestContext::new(cx).await;
1618
1619        cx.set_shared_state("hello (in [parˇens] o)").await;
1620        cx.simulate_shared_keystrokes("ctrl-v l").await;
1621        cx.simulate_shared_keystrokes("a ]").await;
1622        cx.shared_state()
1623            .await
1624            .assert_eq("hello (in «[parens]ˇ» o)");
1625        cx.simulate_shared_keystrokes("i (").await;
1626        cx.shared_state()
1627            .await
1628            .assert_eq("hello («in [parens] oˇ»)");
1629
1630        cx.set_shared_state("hello in a wˇord again.").await;
1631        cx.simulate_shared_keystrokes("ctrl-v l i w").await;
1632        cx.shared_state()
1633            .await
1634            .assert_eq("hello in a w«ordˇ» again.");
1635        assert_eq!(cx.mode(), Mode::VisualBlock);
1636        cx.simulate_shared_keystrokes("o a s").await;
1637        cx.shared_state()
1638            .await
1639            .assert_eq("«ˇhello in a word» again.");
1640    }
1641
1642    #[gpui::test]
1643    async fn test_visual_object_expands(cx: &mut gpui::TestAppContext) {
1644        let mut cx = NeovimBackedTestContext::new(cx).await;
1645
1646        cx.set_shared_state(indoc! {
1647            "{
1648                {
1649               ˇ }
1650            }
1651            {
1652            }
1653            "
1654        })
1655        .await;
1656        cx.simulate_shared_keystrokes("v l").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        // cx.simulate_shared_keystrokes("a {").await;
1687        // cx.shared_state().await.assert_eq(indoc! {
1688        //     "{
1689        //         «{
1690        //         }ˇ»
1691        //     }
1692        //     {
1693        //     }
1694        //     "
1695        // });
1696    }
1697
1698    #[gpui::test]
1699    async fn test_mode_across_command(cx: &mut gpui::TestAppContext) {
1700        let mut cx = VimTestContext::new(cx, true).await;
1701
1702        cx.set_state("aˇbc", Mode::Normal);
1703        cx.simulate_keystrokes("ctrl-v");
1704        assert_eq!(cx.mode(), Mode::VisualBlock);
1705        cx.simulate_keystrokes("cmd-shift-p escape");
1706        assert_eq!(cx.mode(), Mode::VisualBlock);
1707    }
1708
1709    #[gpui::test]
1710    async fn test_gn(cx: &mut gpui::TestAppContext) {
1711        let mut cx = NeovimBackedTestContext::new(cx).await;
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("g n").await;
1717        cx.shared_state().await.assert_eq("aa «aaˇ» aa aa aa");
1718        cx.simulate_shared_keystrokes("g n").await;
1719        cx.shared_state().await.assert_eq("aa «aa aaˇ» aa aa");
1720        cx.simulate_shared_keystrokes("escape d g n").await;
1721        cx.shared_state().await.assert_eq("aa aa ˇ aa aa");
1722
1723        cx.set_shared_state("aaˇ aa aa aa aa").await;
1724        cx.simulate_shared_keystrokes("/ a a enter").await;
1725        cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1726        cx.simulate_shared_keystrokes("3 g n").await;
1727        cx.shared_state().await.assert_eq("aa aa aa «aaˇ» aa");
1728
1729        cx.set_shared_state("aaˇ aa aa aa aa").await;
1730        cx.simulate_shared_keystrokes("/ a a enter").await;
1731        cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1732        cx.simulate_shared_keystrokes("g shift-n").await;
1733        cx.shared_state().await.assert_eq("aa «ˇaa» aa aa aa");
1734        cx.simulate_shared_keystrokes("g shift-n").await;
1735        cx.shared_state().await.assert_eq("«ˇaa aa» aa aa aa");
1736    }
1737
1738    #[gpui::test]
1739    async fn test_gl(cx: &mut gpui::TestAppContext) {
1740        let mut cx = VimTestContext::new(cx, true).await;
1741
1742        cx.set_state("aaˇ aa\naa", Mode::Normal);
1743        cx.simulate_keystrokes("g l");
1744        cx.assert_state("«aaˇ» «aaˇ»\naa", Mode::Visual);
1745        cx.simulate_keystrokes("g >");
1746        cx.assert_state("«aaˇ» aa\n«aaˇ»", Mode::Visual);
1747    }
1748
1749    #[gpui::test]
1750    async fn test_dgn_repeat(cx: &mut gpui::TestAppContext) {
1751        let mut cx = NeovimBackedTestContext::new(cx).await;
1752
1753        cx.set_shared_state("aaˇ aa aa aa aa").await;
1754        cx.simulate_shared_keystrokes("/ a a enter").await;
1755        cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1756        cx.simulate_shared_keystrokes("d g n").await;
1757
1758        cx.shared_state().await.assert_eq("aa ˇ aa aa aa");
1759        cx.simulate_shared_keystrokes(".").await;
1760        cx.shared_state().await.assert_eq("aa  ˇ aa aa");
1761        cx.simulate_shared_keystrokes(".").await;
1762        cx.shared_state().await.assert_eq("aa   ˇ aa");
1763    }
1764
1765    #[gpui::test]
1766    async fn test_cgn_repeat(cx: &mut gpui::TestAppContext) {
1767        let mut cx = NeovimBackedTestContext::new(cx).await;
1768
1769        cx.set_shared_state("aaˇ aa aa aa aa").await;
1770        cx.simulate_shared_keystrokes("/ a a enter").await;
1771        cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1772        cx.simulate_shared_keystrokes("c g n x escape").await;
1773        cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1774        cx.simulate_shared_keystrokes(".").await;
1775        cx.shared_state().await.assert_eq("aa x ˇx aa aa");
1776    }
1777
1778    #[gpui::test]
1779    async fn test_cgn_nomatch(cx: &mut gpui::TestAppContext) {
1780        let mut cx = NeovimBackedTestContext::new(cx).await;
1781
1782        cx.set_shared_state("aaˇ aa aa aa aa").await;
1783        cx.simulate_shared_keystrokes("/ b b enter").await;
1784        cx.shared_state().await.assert_eq("aaˇ aa aa aa aa");
1785        cx.simulate_shared_keystrokes("c g n x escape").await;
1786        cx.shared_state().await.assert_eq("aaˇaa aa aa aa");
1787        cx.simulate_shared_keystrokes(".").await;
1788        cx.shared_state().await.assert_eq("aaˇa aa aa aa");
1789
1790        cx.set_shared_state("aaˇ bb aa aa aa").await;
1791        cx.simulate_shared_keystrokes("/ b b enter").await;
1792        cx.shared_state().await.assert_eq("aa ˇbb aa aa aa");
1793        cx.simulate_shared_keystrokes("c g n x escape").await;
1794        cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1795        cx.simulate_shared_keystrokes(".").await;
1796        cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1797    }
1798
1799    #[gpui::test]
1800    async fn test_visual_shift_d(cx: &mut gpui::TestAppContext) {
1801        let mut cx = NeovimBackedTestContext::new(cx).await;
1802
1803        cx.set_shared_state(indoc! {
1804            "The ˇquick brown
1805            fox jumps over
1806            the lazy dog
1807            "
1808        })
1809        .await;
1810        cx.simulate_shared_keystrokes("v down shift-d").await;
1811        cx.shared_state().await.assert_eq(indoc! {
1812            "the ˇlazy dog\n"
1813        });
1814
1815        cx.set_shared_state(indoc! {
1816            "The ˇquick brown
1817            fox jumps over
1818            the lazy dog
1819            "
1820        })
1821        .await;
1822        cx.simulate_shared_keystrokes("ctrl-v down shift-d").await;
1823        cx.shared_state().await.assert_eq(indoc! {
1824            "Theˇ•
1825            fox•
1826            the lazy dog
1827            "
1828        });
1829    }
1830
1831    #[gpui::test]
1832    async fn test_shift_y(cx: &mut gpui::TestAppContext) {
1833        let mut cx = NeovimBackedTestContext::new(cx).await;
1834
1835        cx.set_shared_state(indoc! {
1836            "The ˇquick brown\n"
1837        })
1838        .await;
1839        cx.simulate_shared_keystrokes("v i w shift-y").await;
1840        cx.shared_clipboard().await.assert_eq(indoc! {
1841            "The quick brown\n"
1842        });
1843    }
1844
1845    #[gpui::test]
1846    async fn test_gv(cx: &mut gpui::TestAppContext) {
1847        let mut cx = NeovimBackedTestContext::new(cx).await;
1848
1849        cx.set_shared_state(indoc! {
1850            "The ˇquick brown"
1851        })
1852        .await;
1853        cx.simulate_shared_keystrokes("v i w escape g v").await;
1854        cx.shared_state().await.assert_eq(indoc! {
1855            "The «quickˇ» brown"
1856        });
1857
1858        cx.simulate_shared_keystrokes("o escape g v").await;
1859        cx.shared_state().await.assert_eq(indoc! {
1860            "The «ˇquick» brown"
1861        });
1862
1863        cx.simulate_shared_keystrokes("escape ^ ctrl-v l").await;
1864        cx.shared_state().await.assert_eq(indoc! {
1865            "«Thˇ»e quick brown"
1866        });
1867        cx.simulate_shared_keystrokes("g v").await;
1868        cx.shared_state().await.assert_eq(indoc! {
1869            "The «ˇquick» brown"
1870        });
1871        cx.simulate_shared_keystrokes("g v").await;
1872        cx.shared_state().await.assert_eq(indoc! {
1873            "«Thˇ»e quick brown"
1874        });
1875
1876        cx.set_state(
1877            indoc! {"
1878            fiˇsh one
1879            fish two
1880            fish red
1881            fish blue
1882        "},
1883            Mode::Normal,
1884        );
1885        cx.simulate_keystrokes("4 g l escape escape 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        cx.simulate_keystrokes("y g v");
1896        cx.assert_state(
1897            indoc! {"
1898                «fishˇ» one
1899                «fishˇ» two
1900                «fishˇ» red
1901                «fishˇ» blue
1902            "},
1903            Mode::Visual,
1904        );
1905    }
1906
1907    #[gpui::test]
1908    async fn test_p_g_v_y(cx: &mut gpui::TestAppContext) {
1909        let mut cx = NeovimBackedTestContext::new(cx).await;
1910
1911        cx.set_shared_state(indoc! {
1912            "The
1913            quicˇk
1914            brown
1915            fox"
1916        })
1917        .await;
1918        cx.simulate_shared_keystrokes("y y j shift-v p g v y").await;
1919        cx.shared_state().await.assert_eq(indoc! {
1920            "The
1921            quick
1922            ˇquick
1923            fox"
1924        });
1925        cx.shared_clipboard().await.assert_eq("quick\n");
1926    }
1927
1928    #[gpui::test]
1929    async fn test_v2ap(cx: &mut gpui::TestAppContext) {
1930        let mut cx = NeovimBackedTestContext::new(cx).await;
1931
1932        cx.set_shared_state(indoc! {
1933            "The
1934            quicˇk
1935
1936            brown
1937            fox"
1938        })
1939        .await;
1940        cx.simulate_shared_keystrokes("v 2 a p").await;
1941        cx.shared_state().await.assert_eq(indoc! {
1942            "«The
1943            quick
1944
1945            brown
1946            fˇ»ox"
1947        });
1948    }
1949
1950    #[gpui::test]
1951    async fn test_visual_syntax_sibling_selection(cx: &mut gpui::TestAppContext) {
1952        let mut cx = VimTestContext::new(cx, true).await;
1953
1954        cx.set_state(
1955            indoc! {"
1956                fn test() {
1957                    let ˇa = 1;
1958                    let b = 2;
1959                    let c = 3;
1960                }
1961            "},
1962            Mode::Normal,
1963        );
1964
1965        // Enter visual mode and select the statement
1966        cx.simulate_keystrokes("v w w w");
1967        cx.assert_state(
1968            indoc! {"
1969                fn test() {
1970                    let «a = 1;ˇ»
1971                    let b = 2;
1972                    let c = 3;
1973                }
1974            "},
1975            Mode::Visual,
1976        );
1977
1978        // The specific behavior of syntax sibling selection in vim mode
1979        // would depend on the key bindings configured, but the actions
1980        // are now available for use
1981    }
1982}