visual.rs

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