command.rs

   1use anyhow::{Result, anyhow};
   2use collections::{HashMap, HashSet};
   3use command_palette_hooks::{CommandInterceptItem, CommandInterceptResult};
   4use editor::{
   5    Bias, Editor, EditorSettings, SelectionEffects, ToPoint,
   6    actions::{SortLinesCaseInsensitive, SortLinesCaseSensitive},
   7    display_map::ToDisplayPoint,
   8};
   9use futures::AsyncWriteExt as _;
  10use gpui::{
  11    Action, App, AppContext as _, Context, Global, Keystroke, Task, WeakEntity, Window, actions,
  12};
  13use itertools::Itertools;
  14use language::Point;
  15use multi_buffer::MultiBufferRow;
  16use project::ProjectPath;
  17use regex::Regex;
  18use schemars::JsonSchema;
  19use search::{BufferSearchBar, SearchOptions};
  20use serde::Deserialize;
  21use settings::{Settings, SettingsStore};
  22use std::{
  23    iter::Peekable,
  24    ops::{Deref, Range},
  25    path::{Path, PathBuf},
  26    process::Stdio,
  27    str::Chars,
  28    sync::OnceLock,
  29    time::Instant,
  30};
  31use task::{HideStrategy, RevealStrategy, SpawnInTerminal, TaskId};
  32use ui::ActiveTheme;
  33use util::{
  34    ResultExt,
  35    paths::PathStyle,
  36    rel_path::{RelPath, RelPathBuf},
  37};
  38use workspace::{Item, SaveIntent, Workspace, notifications::NotifyResultExt};
  39use workspace::{
  40    SplitDirection,
  41    notifications::{DetachAndPromptErr, NotificationSource},
  42};
  43use zed_actions::{OpenDocs, RevealTarget};
  44
  45use crate::{
  46    ToggleMarksView, ToggleRegistersView, Vim, VimSettings,
  47    motion::{EndOfDocument, Motion, MotionKind, StartOfDocument},
  48    normal::{
  49        JoinLines,
  50        search::{FindCommand, ReplaceCommand, Replacement},
  51    },
  52    object::Object,
  53    state::{Mark, Mode},
  54    visual::VisualDeleteLine,
  55};
  56
  57/// Goes to the specified line number in the editor.
  58#[derive(Clone, Debug, PartialEq, Action)]
  59#[action(namespace = vim, no_json, no_register)]
  60pub struct GoToLine {
  61    range: CommandRange,
  62}
  63
  64/// Yanks (copies) text based on the specified range.
  65#[derive(Clone, Debug, PartialEq, Action)]
  66#[action(namespace = vim, no_json, no_register)]
  67pub struct YankCommand {
  68    range: CommandRange,
  69}
  70
  71/// Executes a command with the specified range.
  72#[derive(Clone, Debug, PartialEq, Action)]
  73#[action(namespace = vim, no_json, no_register)]
  74pub struct WithRange {
  75    restore_selection: bool,
  76    range: CommandRange,
  77    action: WrappedAction,
  78}
  79
  80/// Executes a command with the specified count.
  81#[derive(Clone, Debug, PartialEq, Action)]
  82#[action(namespace = vim, no_json, no_register)]
  83pub struct WithCount {
  84    count: u32,
  85    action: WrappedAction,
  86}
  87
  88#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
  89pub enum VimOption {
  90    Wrap(bool),
  91    Number(bool),
  92    RelativeNumber(bool),
  93    IgnoreCase(bool),
  94    GDefault(bool),
  95}
  96
  97impl VimOption {
  98    fn possible_commands(query: &str) -> Vec<CommandInterceptItem> {
  99        let mut prefix_of_options = Vec::new();
 100        let mut options = query.split(" ").collect::<Vec<_>>();
 101        let prefix = options.pop().unwrap_or_default();
 102        for option in options {
 103            if let Some(opt) = Self::from(option) {
 104                prefix_of_options.push(opt)
 105            } else {
 106                return vec![];
 107            }
 108        }
 109
 110        Self::possibilities(prefix)
 111            .map(|possible| {
 112                let mut options = prefix_of_options.clone();
 113                options.push(possible);
 114
 115                CommandInterceptItem {
 116                    string: format!(
 117                        ":set {}",
 118                        options.iter().map(|opt| opt.to_string()).join(" ")
 119                    ),
 120                    action: VimSet { options }.boxed_clone(),
 121                    positions: vec![],
 122                }
 123            })
 124            .collect()
 125    }
 126
 127    fn possibilities(query: &str) -> impl Iterator<Item = Self> + '_ {
 128        [
 129            (None, VimOption::Wrap(true)),
 130            (None, VimOption::Wrap(false)),
 131            (None, VimOption::Number(true)),
 132            (None, VimOption::Number(false)),
 133            (None, VimOption::RelativeNumber(true)),
 134            (None, VimOption::RelativeNumber(false)),
 135            (Some("rnu"), VimOption::RelativeNumber(true)),
 136            (Some("nornu"), VimOption::RelativeNumber(false)),
 137            (None, VimOption::IgnoreCase(true)),
 138            (None, VimOption::IgnoreCase(false)),
 139            (Some("ic"), VimOption::IgnoreCase(true)),
 140            (Some("noic"), VimOption::IgnoreCase(false)),
 141            (None, VimOption::GDefault(true)),
 142            (Some("gd"), VimOption::GDefault(true)),
 143            (None, VimOption::GDefault(false)),
 144            (Some("nogd"), VimOption::GDefault(false)),
 145        ]
 146        .into_iter()
 147        .filter(move |(prefix, option)| prefix.unwrap_or(option.to_string()).starts_with(query))
 148        .map(|(_, option)| option)
 149    }
 150
 151    fn from(option: &str) -> Option<Self> {
 152        match option {
 153            "wrap" => Some(Self::Wrap(true)),
 154            "nowrap" => Some(Self::Wrap(false)),
 155
 156            "number" => Some(Self::Number(true)),
 157            "nu" => Some(Self::Number(true)),
 158            "nonumber" => Some(Self::Number(false)),
 159            "nonu" => Some(Self::Number(false)),
 160
 161            "relativenumber" => Some(Self::RelativeNumber(true)),
 162            "rnu" => Some(Self::RelativeNumber(true)),
 163            "norelativenumber" => Some(Self::RelativeNumber(false)),
 164            "nornu" => Some(Self::RelativeNumber(false)),
 165
 166            "ignorecase" => Some(Self::IgnoreCase(true)),
 167            "ic" => Some(Self::IgnoreCase(true)),
 168            "noignorecase" => Some(Self::IgnoreCase(false)),
 169            "noic" => Some(Self::IgnoreCase(false)),
 170
 171            "gdefault" => Some(Self::GDefault(true)),
 172            "gd" => Some(Self::GDefault(true)),
 173            "nogdefault" => Some(Self::GDefault(false)),
 174            "nogd" => Some(Self::GDefault(false)),
 175
 176            _ => None,
 177        }
 178    }
 179
 180    fn to_string(&self) -> &'static str {
 181        match self {
 182            VimOption::Wrap(true) => "wrap",
 183            VimOption::Wrap(false) => "nowrap",
 184            VimOption::Number(true) => "number",
 185            VimOption::Number(false) => "nonumber",
 186            VimOption::RelativeNumber(true) => "relativenumber",
 187            VimOption::RelativeNumber(false) => "norelativenumber",
 188            VimOption::IgnoreCase(true) => "ignorecase",
 189            VimOption::IgnoreCase(false) => "noignorecase",
 190            VimOption::GDefault(true) => "gdefault",
 191            VimOption::GDefault(false) => "nogdefault",
 192        }
 193    }
 194}
 195
 196/// Sets vim options and configuration values.
 197#[derive(Clone, PartialEq, Action)]
 198#[action(namespace = vim, no_json, no_register)]
 199pub struct VimSet {
 200    options: Vec<VimOption>,
 201}
 202
 203/// Saves the current file with optional save intent.
 204#[derive(Clone, PartialEq, Action)]
 205#[action(namespace = vim, no_json, no_register)]
 206struct VimSave {
 207    pub range: Option<CommandRange>,
 208    pub save_intent: Option<SaveIntent>,
 209    pub filename: String,
 210}
 211
 212/// Deletes the specified marks from the editor.
 213#[derive(Clone, PartialEq, Action)]
 214#[action(namespace = vim, no_json, no_register)]
 215struct VimSplit {
 216    pub vertical: bool,
 217    pub filename: String,
 218}
 219
 220#[derive(Clone, PartialEq, Action)]
 221#[action(namespace = vim, no_json, no_register)]
 222enum DeleteMarks {
 223    Marks(String),
 224    AllLocal,
 225}
 226
 227actions!(
 228    vim,
 229    [
 230        /// Executes a command in visual mode.
 231        VisualCommand,
 232        /// Executes a command with a count prefix.
 233        CountCommand,
 234        /// Executes a shell command.
 235        ShellCommand,
 236        /// Indicates that an argument is required for the command.
 237        ArgumentRequired
 238    ]
 239);
 240
 241/// Opens the specified file for editing.
 242#[derive(Clone, PartialEq, Action)]
 243#[action(namespace = vim, no_json, no_register)]
 244struct VimEdit {
 245    pub filename: String,
 246}
 247
 248/// Pastes the specified file's contents.
 249#[derive(Clone, PartialEq, Action)]
 250#[action(namespace = vim, no_json, no_register)]
 251struct VimRead {
 252    pub range: Option<CommandRange>,
 253    pub filename: String,
 254}
 255
 256#[derive(Clone, PartialEq, Action)]
 257#[action(namespace = vim, no_json, no_register)]
 258struct VimNorm {
 259    pub range: Option<CommandRange>,
 260    pub command: String,
 261    /// Places cursors at beginning of each given row.
 262    /// Overrides given range and current cursor.
 263    pub override_rows: Option<Vec<u32>>,
 264}
 265
 266#[derive(Debug)]
 267struct WrappedAction(Box<dyn Action>);
 268
 269impl PartialEq for WrappedAction {
 270    fn eq(&self, other: &Self) -> bool {
 271        self.0.partial_eq(&*other.0)
 272    }
 273}
 274
 275impl Clone for WrappedAction {
 276    fn clone(&self) -> Self {
 277        Self(self.0.boxed_clone())
 278    }
 279}
 280
 281impl Deref for WrappedAction {
 282    type Target = dyn Action;
 283    fn deref(&self) -> &dyn Action {
 284        &*self.0
 285    }
 286}
 287
 288pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
 289    Vim::action(editor, cx, |vim, action: &VimSet, _, cx| {
 290        for option in action.options.iter() {
 291            vim.update_editor(cx, |_, editor, cx| match option {
 292                VimOption::Wrap(true) => {
 293                    editor
 294                        .set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
 295                }
 296                VimOption::Wrap(false) => {
 297                    editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);
 298                }
 299                VimOption::Number(enabled) => {
 300                    editor.set_show_line_numbers(*enabled, cx);
 301                }
 302                VimOption::RelativeNumber(enabled) => {
 303                    editor.set_relative_line_number(Some(*enabled), cx);
 304                }
 305                VimOption::IgnoreCase(enabled) => {
 306                    let mut settings = EditorSettings::get_global(cx).clone();
 307                    settings.search.case_sensitive = !*enabled;
 308                    SettingsStore::update(cx, |store, _| {
 309                        store.override_global(settings);
 310                    });
 311                }
 312                VimOption::GDefault(enabled) => {
 313                    let mut settings = VimSettings::get_global(cx).clone();
 314                    settings.gdefault = *enabled;
 315
 316                    SettingsStore::update(cx, |store, _| {
 317                        store.override_global(settings);
 318                    })
 319                }
 320            });
 321        }
 322    });
 323    Vim::action(editor, cx, |vim, _: &VisualCommand, window, cx| {
 324        let Some(workspace) = vim.workspace(window) else {
 325            return;
 326        };
 327        workspace.update(cx, |workspace, cx| {
 328            command_palette::CommandPalette::toggle(workspace, "'<,'>", window, cx);
 329        })
 330    });
 331
 332    Vim::action(editor, cx, |vim, _: &ShellCommand, window, cx| {
 333        let Some(workspace) = vim.workspace(window) else {
 334            return;
 335        };
 336        workspace.update(cx, |workspace, cx| {
 337            command_palette::CommandPalette::toggle(workspace, "'<,'>!", window, cx);
 338        })
 339    });
 340
 341    Vim::action(editor, cx, |_, _: &ArgumentRequired, window, cx| {
 342        let _ = window.prompt(
 343            gpui::PromptLevel::Critical,
 344            "Argument required",
 345            None,
 346            &["Cancel"],
 347            cx,
 348        );
 349    });
 350
 351    Vim::action(editor, cx, |vim, _: &ShellCommand, window, cx| {
 352        let Some(workspace) = vim.workspace(window) else {
 353            return;
 354        };
 355        workspace.update(cx, |workspace, cx| {
 356            command_palette::CommandPalette::toggle(workspace, "'<,'>!", window, cx);
 357        })
 358    });
 359
 360    Vim::action(editor, cx, |vim, action: &VimSave, window, cx| {
 361        if let Some(range) = &action.range {
 362            vim.update_editor(cx, |vim, editor, cx| {
 363                let Some(range) = range.buffer_range(vim, editor, window, cx).ok() else {
 364                    return;
 365                };
 366                let Some((line_ending, encoding, has_bom, text, whole_buffer)) = editor.buffer().update(cx, |multi, cx| {
 367                    Some(multi.as_singleton()?.update(cx, |buffer, _| {
 368                        (
 369                            buffer.line_ending(),
 370                            buffer.encoding(),
 371                            buffer.has_bom(),
 372                            buffer.as_rope().slice_rows(range.start.0..range.end.0 + 1),
 373                            range.start.0 == 0 && range.end.0 + 1 >= buffer.row_count(),
 374                        )
 375                    }))
 376                }) else {
 377                    return;
 378                };
 379
 380                let filename = action.filename.clone();
 381                let filename = if filename.is_empty() {
 382                    let Some(file) = editor
 383                        .buffer()
 384                        .read(cx)
 385                        .as_singleton()
 386                        .and_then(|buffer| buffer.read(cx).file())
 387                    else {
 388                        let _ = window.prompt(
 389                            gpui::PromptLevel::Warning,
 390                            "No file name",
 391                            Some("Partial buffer write requires file name."),
 392                            &["Cancel"],
 393                            cx,
 394                        );
 395                        return;
 396                    };
 397                    file.path().display(file.path_style(cx)).to_string()
 398                } else {
 399                    filename
 400                };
 401
 402                if action.filename.is_empty() {
 403                    if whole_buffer {
 404                        if let Some(workspace) = vim.workspace(window) {
 405                            workspace.update(cx, |workspace, cx| {
 406                                workspace
 407                                    .save_active_item(
 408                                        action.save_intent.unwrap_or(SaveIntent::Save),
 409                                        window,
 410                                        cx,
 411                                    )
 412                                    .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
 413                            });
 414                        }
 415                        return;
 416                    }
 417                    if Some(SaveIntent::Overwrite) != action.save_intent {
 418                        let _ = window.prompt(
 419                            gpui::PromptLevel::Warning,
 420                            "Use ! to write partial buffer",
 421                            Some("Overwriting the current file with selected buffer content requires '!'."),
 422                            &["Cancel"],
 423                            cx,
 424                        );
 425                        return;
 426                    }
 427                    editor.buffer().update(cx, |multi, cx| {
 428                        if let Some(buffer) = multi.as_singleton() {
 429                            buffer.update(cx, |buffer, _| buffer.set_conflict());
 430                        }
 431                    });
 432                };
 433
 434                editor.project().unwrap().update(cx, |project, cx| {
 435                    let worktree = project.visible_worktrees(cx).next().unwrap();
 436
 437                    worktree.update(cx, |worktree, cx| {
 438                        let path_style = worktree.path_style();
 439                        let Some(path) = RelPath::new(Path::new(&filename), path_style).ok() else {
 440                            return;
 441                        };
 442
 443                        let rx = (worktree.entry_for_path(&path).is_some() && Some(SaveIntent::Overwrite) != action.save_intent).then(|| {
 444                            window.prompt(
 445                                gpui::PromptLevel::Warning,
 446                                &format!("{path:?} already exists. Do you want to replace it?"),
 447                                Some(
 448                                    "A file or folder with the same name already exists. Replacing it will overwrite its current contents.",
 449                                ),
 450                                &["Replace", "Cancel"],
 451                                cx
 452                            )
 453                        });
 454                        let filename = filename.clone();
 455                        cx.spawn_in(window, async move |this, cx| {
 456                            if let Some(rx) = rx
 457                                && Ok(0) != rx.await
 458                            {
 459                                return;
 460                            }
 461
 462                            let _ = this.update_in(cx, |worktree, window, cx| {
 463                                let Some(path) = RelPath::new(Path::new(&filename), path_style).ok() else {
 464                                    return;
 465                                };
 466                                worktree
 467                                    .write_file(path.into_arc(), text.clone(), line_ending, encoding, has_bom, cx)
 468                                    .detach_and_prompt_err("Failed to write lines", window, cx, |_, _, _| None);
 469                            });
 470                        })
 471                        .detach();
 472                    });
 473                });
 474            });
 475            return;
 476        }
 477        if action.filename.is_empty() {
 478            if let Some(workspace) = vim.workspace(window) {
 479                workspace.update(cx, |workspace, cx| {
 480                    workspace
 481                        .save_active_item(
 482                            action.save_intent.unwrap_or(SaveIntent::Save),
 483                            window,
 484                            cx,
 485                        )
 486                        .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
 487                });
 488            }
 489            return;
 490        }
 491        vim.update_editor(cx, |_, editor, cx| {
 492            let Some(project) = editor.project().cloned() else {
 493                return;
 494            };
 495            let Some(worktree) = project.read(cx).visible_worktrees(cx).next() else {
 496                return;
 497            };
 498            let path_style = worktree.read(cx).path_style();
 499            let Ok(project_path) =
 500                RelPath::new(Path::new(&action.filename), path_style).map(|path| ProjectPath {
 501                    worktree_id: worktree.read(cx).id(),
 502                    path: path.into_arc(),
 503                })
 504            else {
 505                // TODO implement save_as with absolute path
 506                Task::ready(Err::<(), _>(anyhow!(
 507                    "Cannot save buffer with absolute path"
 508                )))
 509                .detach_and_prompt_err(
 510                    "Failed to save",
 511                    window,
 512                    cx,
 513                    |_, _, _| None,
 514                );
 515                return;
 516            };
 517
 518            if project.read(cx).entry_for_path(&project_path, cx).is_some()
 519                && action.save_intent != Some(SaveIntent::Overwrite)
 520            {
 521                let answer = window.prompt(
 522                    gpui::PromptLevel::Critical,
 523                    &format!(
 524                        "{} already exists. Do you want to replace it?",
 525                        project_path.path.display(path_style)
 526                    ),
 527                    Some(
 528                        "A file or folder with the same name already exists. \
 529                        Replacing it will overwrite its current contents.",
 530                    ),
 531                    &["Replace", "Cancel"],
 532                    cx,
 533                );
 534                cx.spawn_in(window, async move |editor, cx| {
 535                    if answer.await.ok() != Some(0) {
 536                        return;
 537                    }
 538
 539                    let _ = editor.update_in(cx, |editor, window, cx| {
 540                        editor
 541                            .save_as(project, project_path, window, cx)
 542                            .detach_and_prompt_err("Failed to :w", window, cx, |_, _, _| None);
 543                    });
 544                })
 545                .detach();
 546            } else {
 547                editor
 548                    .save_as(project, project_path, window, cx)
 549                    .detach_and_prompt_err("Failed to :w", window, cx, |_, _, _| None);
 550            }
 551        });
 552    });
 553
 554    Vim::action(editor, cx, |vim, action: &VimSplit, window, cx| {
 555        let Some(workspace) = vim.workspace(window) else {
 556            return;
 557        };
 558
 559        workspace.update(cx, |workspace, cx| {
 560            let project = workspace.project().clone();
 561            let Some(worktree) = project.read(cx).visible_worktrees(cx).next() else {
 562                return;
 563            };
 564            let path_style = worktree.read(cx).path_style();
 565            let Some(path) = RelPath::new(Path::new(&action.filename), path_style).log_err() else {
 566                return;
 567            };
 568            let project_path = ProjectPath {
 569                worktree_id: worktree.read(cx).id(),
 570                path: path.into_arc(),
 571            };
 572
 573            let direction = if action.vertical {
 574                SplitDirection::vertical(cx)
 575            } else {
 576                SplitDirection::horizontal(cx)
 577            };
 578
 579            workspace
 580                .split_path_preview(project_path, false, Some(direction), window, cx)
 581                .detach_and_log_err(cx);
 582        })
 583    });
 584
 585    Vim::action(editor, cx, |vim, action: &DeleteMarks, window, cx| {
 586        fn err(s: String, window: &mut Window, cx: &mut Context<Editor>) {
 587            let _ = window.prompt(
 588                gpui::PromptLevel::Critical,
 589                &format!("Invalid argument: {}", s),
 590                None,
 591                &["Cancel"],
 592                cx,
 593            );
 594        }
 595        vim.update_editor(cx, |vim, editor, cx| match action {
 596            DeleteMarks::Marks(s) => {
 597                if s.starts_with('-') || s.ends_with('-') || s.contains(['\'', '`']) {
 598                    err(s.clone(), window, cx);
 599                    return;
 600                }
 601
 602                let to_delete = if s.len() < 3 {
 603                    Some(s.clone())
 604                } else {
 605                    s.chars()
 606                        .tuple_windows::<(_, _, _)>()
 607                        .map(|(a, b, c)| {
 608                            if b == '-' {
 609                                if match a {
 610                                    'a'..='z' => a <= c && c <= 'z',
 611                                    'A'..='Z' => a <= c && c <= 'Z',
 612                                    '0'..='9' => a <= c && c <= '9',
 613                                    _ => false,
 614                                } {
 615                                    Some((a..=c).collect_vec())
 616                                } else {
 617                                    None
 618                                }
 619                            } else if a == '-' {
 620                                if c == '-' { None } else { Some(vec![c]) }
 621                            } else if c == '-' {
 622                                if a == '-' { None } else { Some(vec![a]) }
 623                            } else {
 624                                Some(vec![a, b, c])
 625                            }
 626                        })
 627                        .fold_options(HashSet::<char>::default(), |mut set, chars| {
 628                            set.extend(chars.iter().copied());
 629                            set
 630                        })
 631                        .map(|set| set.iter().collect::<String>())
 632                };
 633
 634                let Some(to_delete) = to_delete else {
 635                    err(s.clone(), window, cx);
 636                    return;
 637                };
 638
 639                for c in to_delete.chars().filter(|c| !c.is_whitespace()) {
 640                    vim.delete_mark(c.to_string(), editor, window, cx);
 641                }
 642            }
 643            DeleteMarks::AllLocal => {
 644                for s in 'a'..='z' {
 645                    vim.delete_mark(s.to_string(), editor, window, cx);
 646                }
 647            }
 648        });
 649    });
 650
 651    Vim::action(editor, cx, |vim, action: &VimEdit, window, cx| {
 652        vim.update_editor(cx, |vim, editor, cx| {
 653            let Some(workspace) = vim.workspace(window) else {
 654                return;
 655            };
 656            let Some(project) = editor.project().cloned() else {
 657                return;
 658            };
 659            let Some(worktree) = project.read(cx).visible_worktrees(cx).next() else {
 660                return;
 661            };
 662            let path_style = worktree.read(cx).path_style();
 663            let Some(path) = RelPath::new(Path::new(&action.filename), path_style).log_err() else {
 664                return;
 665            };
 666            let project_path = ProjectPath {
 667                worktree_id: worktree.read(cx).id(),
 668                path: path.into_arc(),
 669            };
 670
 671            let _ = workspace.update(cx, |workspace, cx| {
 672                workspace
 673                    .open_path(project_path, None, true, window, cx)
 674                    .detach_and_log_err(cx);
 675            });
 676        });
 677    });
 678
 679    Vim::action(editor, cx, |vim, action: &VimRead, window, cx| {
 680        vim.update_editor(cx, |vim, editor, cx| {
 681            let snapshot = editor.buffer().read(cx).snapshot(cx);
 682            let end = if let Some(range) = action.range.clone() {
 683                let Some(multi_range) = range.buffer_range(vim, editor, window, cx).log_err()
 684                else {
 685                    return;
 686                };
 687
 688                match &range.start {
 689                    // inserting text above the first line uses the command ":0r {name}"
 690                    Position::Line { row: 0, offset: 0 } if range.end.is_none() => {
 691                        snapshot.clip_point(Point::new(0, 0), Bias::Right)
 692                    }
 693                    _ => snapshot.clip_point(Point::new(multi_range.end.0 + 1, 0), Bias::Right),
 694                }
 695            } else {
 696                let end_row = editor
 697                    .selections
 698                    .newest::<Point>(&editor.display_snapshot(cx))
 699                    .range()
 700                    .end
 701                    .row;
 702                snapshot.clip_point(Point::new(end_row + 1, 0), Bias::Right)
 703            };
 704            let is_end_of_file = end == snapshot.max_point();
 705            let edit_range = snapshot.anchor_before(end)..snapshot.anchor_before(end);
 706
 707            let mut text = if is_end_of_file {
 708                String::from('\n')
 709            } else {
 710                String::new()
 711            };
 712
 713            let mut task = None;
 714            if action.filename.is_empty() {
 715                text.push_str(
 716                    &editor
 717                        .buffer()
 718                        .read(cx)
 719                        .as_singleton()
 720                        .map(|buffer| buffer.read(cx).text())
 721                        .unwrap_or_default(),
 722                );
 723            } else {
 724                if let Some(project) = editor.project().cloned() {
 725                    project.update(cx, |project, cx| {
 726                        let Some(worktree) = project.visible_worktrees(cx).next() else {
 727                            return;
 728                        };
 729                        let path_style = worktree.read(cx).path_style();
 730                        let Some(path) =
 731                            RelPath::new(Path::new(&action.filename), path_style).log_err()
 732                        else {
 733                            return;
 734                        };
 735                        task =
 736                            Some(worktree.update(cx, |worktree, cx| worktree.load_file(&path, cx)));
 737                    });
 738                } else {
 739                    return;
 740                }
 741            };
 742
 743            cx.spawn_in(window, async move |editor, cx| {
 744                if let Some(task) = task {
 745                    text.push_str(
 746                        &task
 747                            .await
 748                            .log_err()
 749                            .map(|loaded_file| loaded_file.text)
 750                            .unwrap_or_default(),
 751                    );
 752                }
 753
 754                if !text.is_empty() && !is_end_of_file {
 755                    text.push('\n');
 756                }
 757
 758                let _ = editor.update_in(cx, |editor, window, cx| {
 759                    editor.transact(window, cx, |editor, window, cx| {
 760                        editor.edit([(edit_range.clone(), text)], cx);
 761                        let snapshot = editor.buffer().read(cx).snapshot(cx);
 762                        editor.change_selections(Default::default(), window, cx, |s| {
 763                            let point = if is_end_of_file {
 764                                Point::new(
 765                                    edit_range.start.to_point(&snapshot).row.saturating_add(1),
 766                                    0,
 767                                )
 768                            } else {
 769                                Point::new(edit_range.start.to_point(&snapshot).row, 0)
 770                            };
 771                            s.select_ranges([point..point]);
 772                        })
 773                    });
 774                });
 775            })
 776            .detach();
 777        });
 778    });
 779
 780    Vim::action(editor, cx, |vim, action: &VimNorm, window, cx| {
 781        let keystrokes = action
 782            .command
 783            .chars()
 784            .map(|c| Keystroke::parse(&c.to_string()).unwrap())
 785            .collect();
 786        vim.switch_mode(Mode::Normal, true, window, cx);
 787        if let Some(override_rows) = &action.override_rows {
 788            vim.update_editor(cx, |_, editor, cx| {
 789                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
 790                    s.replace_cursors_with(|map| {
 791                        override_rows
 792                            .iter()
 793                            .map(|row| Point::new(*row, 0).to_display_point(map))
 794                            .collect()
 795                    });
 796                });
 797            });
 798        } else if let Some(range) = &action.range {
 799            let result = vim.update_editor(cx, |vim, editor, cx| {
 800                let range = range.buffer_range(vim, editor, window, cx)?;
 801                editor.change_selections(
 802                    SelectionEffects::no_scroll().nav_history(false),
 803                    window,
 804                    cx,
 805                    |s| {
 806                        s.select_ranges(
 807                            (range.start.0..=range.end.0)
 808                                .map(|line| Point::new(line, 0)..Point::new(line, 0)),
 809                        );
 810                    },
 811                );
 812                anyhow::Ok(())
 813            });
 814            if let Some(Err(err)) = result {
 815                log::error!("Error selecting range: {}", err);
 816                return;
 817            }
 818        };
 819
 820        let Some(workspace) = vim.workspace(window) else {
 821            return;
 822        };
 823        let task = workspace.update(cx, |workspace, cx| {
 824            workspace.send_keystrokes_impl(keystrokes, window, cx)
 825        });
 826        let had_range = action.range.is_some();
 827        let had_override = action.override_rows.is_some();
 828
 829        cx.spawn_in(window, async move |vim, cx| {
 830            task.await;
 831            vim.update_in(cx, |vim, window, cx| {
 832                if matches!(vim.mode, Mode::Insert | Mode::Replace) {
 833                    vim.normal_before(&Default::default(), window, cx);
 834                } else {
 835                    vim.switch_mode(Mode::Normal, true, window, cx);
 836                }
 837                if had_override || had_range {
 838                    vim.update_editor(cx, |_, editor, cx| {
 839                        editor.change_selections(SelectionEffects::default(), window, cx, |s| {
 840                            s.select_anchor_ranges([s.newest_anchor().range()]);
 841                        });
 842                        if let Some(tx_id) = editor
 843                            .buffer()
 844                            .update(cx, |multi, cx| multi.last_transaction_id(cx))
 845                        {
 846                            let last_sel = editor.selections.disjoint_anchors_arc();
 847                            editor.modify_transaction_selection_history(tx_id, |old| {
 848                                old.0 = old.0.get(..1).unwrap_or(&[]).into();
 849                                old.1 = Some(last_sel);
 850                            });
 851                        }
 852                    });
 853                }
 854            })
 855            .log_err();
 856        })
 857        .detach();
 858    });
 859
 860    Vim::action(editor, cx, |vim, _: &CountCommand, window, cx| {
 861        let Some(workspace) = vim.workspace(window) else {
 862            return;
 863        };
 864        let count = Vim::take_count(cx).unwrap_or(1);
 865        Vim::take_forced_motion(cx);
 866        let n = if count > 1 {
 867            format!(".,.+{}", count.saturating_sub(1))
 868        } else {
 869            ".".to_string()
 870        };
 871        workspace.update(cx, |workspace, cx| {
 872            command_palette::CommandPalette::toggle(workspace, &n, window, cx);
 873        })
 874    });
 875
 876    Vim::action(editor, cx, |vim, action: &GoToLine, window, cx| {
 877        vim.switch_mode(Mode::Normal, false, window, cx);
 878        let result = vim.update_editor(cx, |vim, editor, cx| {
 879            let snapshot = editor.snapshot(window, cx);
 880            let buffer_row = action.range.head().buffer_row(vim, editor, window, cx)?;
 881            let current = editor
 882                .selections
 883                .newest::<Point>(&editor.display_snapshot(cx));
 884            let target = snapshot
 885                .buffer_snapshot()
 886                .clip_point(Point::new(buffer_row.0, current.head().column), Bias::Left);
 887            editor.change_selections(Default::default(), window, cx, |s| {
 888                s.select_ranges([target..target]);
 889            });
 890
 891            anyhow::Ok(())
 892        });
 893        if let Some(e @ Err(_)) = result {
 894            let Some(workspace) = vim.workspace(window) else {
 895                return;
 896            };
 897            workspace.update(cx, |workspace, cx| {
 898                e.notify_err(workspace, NotificationSource::Editor, cx);
 899            });
 900        }
 901    });
 902
 903    Vim::action(editor, cx, |vim, action: &YankCommand, window, cx| {
 904        vim.update_editor(cx, |vim, editor, cx| {
 905            let snapshot = editor.snapshot(window, cx);
 906            if let Ok(range) = action.range.buffer_range(vim, editor, window, cx) {
 907                let end = if range.end < snapshot.buffer_snapshot().max_row() {
 908                    Point::new(range.end.0 + 1, 0)
 909                } else {
 910                    snapshot.buffer_snapshot().max_point()
 911                };
 912                vim.copy_ranges(
 913                    editor,
 914                    MotionKind::Linewise,
 915                    true,
 916                    vec![Point::new(range.start.0, 0)..end],
 917                    window,
 918                    cx,
 919                )
 920            }
 921        });
 922    });
 923
 924    Vim::action(editor, cx, |_, action: &WithCount, window, cx| {
 925        for _ in 0..action.count {
 926            window.dispatch_action(action.action.boxed_clone(), cx)
 927        }
 928    });
 929
 930    Vim::action(editor, cx, |vim, action: &WithRange, window, cx| {
 931        let result = vim.update_editor(cx, |vim, editor, cx| {
 932            action.range.buffer_range(vim, editor, window, cx)
 933        });
 934
 935        let range = match result {
 936            None => return,
 937            Some(e @ Err(_)) => {
 938                let Some(workspace) = vim.workspace(window) else {
 939                    return;
 940                };
 941                workspace.update(cx, |workspace, cx| {
 942                    e.notify_err(workspace, NotificationSource::Editor, cx);
 943                });
 944                return;
 945            }
 946            Some(Ok(result)) => result,
 947        };
 948
 949        let previous_selections = vim
 950            .update_editor(cx, |_, editor, cx| {
 951                let selections = action.restore_selection.then(|| {
 952                    editor
 953                        .selections
 954                        .disjoint_anchor_ranges()
 955                        .collect::<Vec<_>>()
 956                });
 957                let snapshot = editor.buffer().read(cx).snapshot(cx);
 958                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
 959                    let end = Point::new(range.end.0, snapshot.line_len(range.end));
 960                    s.select_ranges([end..Point::new(range.start.0, 0)]);
 961                });
 962                selections
 963            })
 964            .flatten();
 965        window.dispatch_action(action.action.boxed_clone(), cx);
 966        cx.defer_in(window, move |vim, window, cx| {
 967            vim.update_editor(cx, |_, editor, cx| {
 968                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
 969                    if let Some(previous_selections) = previous_selections {
 970                        s.select_ranges(previous_selections);
 971                    } else {
 972                        s.select_ranges([
 973                            Point::new(range.start.0, 0)..Point::new(range.start.0, 0)
 974                        ]);
 975                    }
 976                })
 977            });
 978        });
 979    });
 980
 981    Vim::action(editor, cx, |vim, action: &OnMatchingLines, window, cx| {
 982        action.run(vim, window, cx)
 983    });
 984
 985    Vim::action(editor, cx, |vim, action: &ShellExec, window, cx| {
 986        action.run(vim, window, cx)
 987    })
 988}
 989
 990#[derive(Default)]
 991struct VimCommand {
 992    prefix: &'static str,
 993    suffix: &'static str,
 994    action: Option<Box<dyn Action>>,
 995    action_name: Option<&'static str>,
 996    bang_action: Option<Box<dyn Action>>,
 997    args: Option<
 998        Box<dyn Fn(Box<dyn Action>, String) -> Option<Box<dyn Action>> + Send + Sync + 'static>,
 999    >,
1000    /// Optional range Range to use if no range is specified.
1001    default_range: Option<CommandRange>,
1002    range: Option<
1003        Box<
1004            dyn Fn(Box<dyn Action>, &CommandRange) -> Option<Box<dyn Action>>
1005                + Send
1006                + Sync
1007                + 'static,
1008        >,
1009    >,
1010    has_count: bool,
1011    has_filename: bool,
1012}
1013
1014struct ParsedQuery {
1015    args: String,
1016    has_bang: bool,
1017    has_space: bool,
1018}
1019
1020impl VimCommand {
1021    fn new(pattern: (&'static str, &'static str), action: impl Action) -> Self {
1022        Self {
1023            prefix: pattern.0,
1024            suffix: pattern.1,
1025            action: Some(action.boxed_clone()),
1026            ..Default::default()
1027        }
1028    }
1029
1030    // from_str is used for actions in other crates.
1031    fn str(pattern: (&'static str, &'static str), action_name: &'static str) -> Self {
1032        Self {
1033            prefix: pattern.0,
1034            suffix: pattern.1,
1035            action_name: Some(action_name),
1036            ..Default::default()
1037        }
1038    }
1039
1040    fn bang(mut self, bang_action: impl Action) -> Self {
1041        self.bang_action = Some(bang_action.boxed_clone());
1042        self
1043    }
1044
1045    /// Set argument handler. Trailing whitespace in arguments will be preserved.
1046    fn args(
1047        mut self,
1048        f: impl Fn(Box<dyn Action>, String) -> Option<Box<dyn Action>> + Send + Sync + 'static,
1049    ) -> Self {
1050        self.args = Some(Box::new(f));
1051        self
1052    }
1053
1054    /// Set argument handler. Trailing whitespace in arguments will be trimmed.
1055    /// Supports filename autocompletion.
1056    fn filename(
1057        mut self,
1058        f: impl Fn(Box<dyn Action>, String) -> Option<Box<dyn Action>> + Send + Sync + 'static,
1059    ) -> Self {
1060        self.args = Some(Box::new(f));
1061        self.has_filename = true;
1062        self
1063    }
1064
1065    fn range(
1066        mut self,
1067        f: impl Fn(Box<dyn Action>, &CommandRange) -> Option<Box<dyn Action>> + Send + Sync + 'static,
1068    ) -> Self {
1069        self.range = Some(Box::new(f));
1070        self
1071    }
1072
1073    fn default_range(mut self, range: CommandRange) -> Self {
1074        self.default_range = Some(range);
1075        self
1076    }
1077
1078    fn count(mut self) -> Self {
1079        self.has_count = true;
1080        self
1081    }
1082
1083    fn generate_filename_completions(
1084        parsed_query: &ParsedQuery,
1085        workspace: WeakEntity<Workspace>,
1086        cx: &mut App,
1087    ) -> Task<Vec<String>> {
1088        let ParsedQuery {
1089            args,
1090            has_bang: _,
1091            has_space: _,
1092        } = parsed_query;
1093        let Some(workspace) = workspace.upgrade() else {
1094            return Task::ready(Vec::new());
1095        };
1096
1097        let (task, args_path) = workspace.update(cx, |workspace, cx| {
1098            let prefix = workspace
1099                .project()
1100                .read(cx)
1101                .visible_worktrees(cx)
1102                .map(|worktree| worktree.read(cx).abs_path().to_path_buf())
1103                .next()
1104                .or_else(std::env::home_dir)
1105                .unwrap_or_else(|| PathBuf::from(""));
1106
1107            let rel_path = match RelPath::new(Path::new(&args), PathStyle::local()) {
1108                Ok(path) => path.to_rel_path_buf(),
1109                Err(_) => {
1110                    return (Task::ready(Ok(Vec::new())), RelPathBuf::new());
1111                }
1112            };
1113
1114            let rel_path = if args.ends_with(PathStyle::local().primary_separator()) {
1115                rel_path
1116            } else {
1117                rel_path
1118                    .parent()
1119                    .map(|rel_path| rel_path.to_rel_path_buf())
1120                    .unwrap_or(RelPathBuf::new())
1121            };
1122
1123            let task = workspace.project().update(cx, |project, cx| {
1124                let path = prefix
1125                    .join(rel_path.as_std_path())
1126                    .to_string_lossy()
1127                    .to_string();
1128                project.list_directory(path, cx)
1129            });
1130
1131            (task, rel_path)
1132        });
1133
1134        cx.background_spawn(async move {
1135            let directories = task.await.unwrap_or_default();
1136            directories
1137                .iter()
1138                .map(|dir| {
1139                    let path = RelPath::new(dir.path.as_path(), PathStyle::local())
1140                        .map(|cow| cow.into_owned())
1141                        .unwrap_or(RelPathBuf::new());
1142                    let mut path_string = args_path
1143                        .join(&path)
1144                        .display(PathStyle::local())
1145                        .to_string();
1146                    if dir.is_dir {
1147                        path_string.push_str(PathStyle::local().primary_separator());
1148                    }
1149                    path_string
1150                })
1151                .collect()
1152        })
1153    }
1154
1155    fn get_parsed_query(&self, query: String) -> Option<ParsedQuery> {
1156        let rest = query
1157            .strip_prefix(self.prefix)?
1158            .to_string()
1159            .chars()
1160            .zip_longest(self.suffix.to_string().chars())
1161            .skip_while(|e| e.clone().both().map(|(s, q)| s == q).unwrap_or(false))
1162            .filter_map(|e| e.left())
1163            .collect::<String>();
1164        let has_bang = rest.starts_with('!');
1165        let has_space = rest.starts_with("! ") || rest.starts_with(' ');
1166        let args = if has_bang {
1167            rest.strip_prefix('!')?.trim_start().to_string()
1168        } else if rest.is_empty() {
1169            "".into()
1170        } else {
1171            rest.strip_prefix(' ')?.trim_start().to_string()
1172        };
1173        Some(ParsedQuery {
1174            args,
1175            has_bang,
1176            has_space,
1177        })
1178    }
1179
1180    fn parse(
1181        &self,
1182        query: &str,
1183        range: &Option<CommandRange>,
1184        cx: &App,
1185    ) -> Option<Box<dyn Action>> {
1186        let ParsedQuery {
1187            args,
1188            has_bang,
1189            has_space: _,
1190        } = self.get_parsed_query(query.to_string())?;
1191        let action = if has_bang && let Some(bang_action) = self.bang_action.as_ref() {
1192            bang_action.boxed_clone()
1193        } else if let Some(action) = self.action.as_ref() {
1194            action.boxed_clone()
1195        } else if let Some(action_name) = self.action_name {
1196            cx.build_action(action_name, None).log_err()?
1197        } else {
1198            return None;
1199        };
1200
1201        // If the command does not accept args and we have args, we should do no
1202        // action.
1203        let action = if args.is_empty() {
1204            action
1205        } else if self.has_filename {
1206            self.args.as_ref()?(action, args.trim().into())?
1207        } else {
1208            self.args.as_ref()?(action, args)?
1209        };
1210
1211        let range = range.as_ref().or(self.default_range.as_ref());
1212        if let Some(range) = range {
1213            self.range.as_ref().and_then(|f| f(action, range))
1214        } else {
1215            Some(action)
1216        }
1217    }
1218
1219    // TODO: ranges with search queries
1220    fn parse_range(query: &str) -> (Option<CommandRange>, String) {
1221        let mut chars = query.chars().peekable();
1222
1223        match chars.peek() {
1224            Some('%') => {
1225                chars.next();
1226                return (
1227                    Some(CommandRange {
1228                        start: Position::Line { row: 1, offset: 0 },
1229                        end: Some(Position::LastLine { offset: 0 }),
1230                    }),
1231                    chars.collect(),
1232                );
1233            }
1234            Some('*') => {
1235                chars.next();
1236                return (
1237                    Some(CommandRange {
1238                        start: Position::Mark {
1239                            name: '<',
1240                            offset: 0,
1241                        },
1242                        end: Some(Position::Mark {
1243                            name: '>',
1244                            offset: 0,
1245                        }),
1246                    }),
1247                    chars.collect(),
1248                );
1249            }
1250            _ => {}
1251        }
1252
1253        let start = Self::parse_position(&mut chars);
1254
1255        match chars.peek() {
1256            Some(',' | ';') => {
1257                chars.next();
1258                (
1259                    Some(CommandRange {
1260                        start: start.unwrap_or(Position::CurrentLine { offset: 0 }),
1261                        end: Self::parse_position(&mut chars),
1262                    }),
1263                    chars.collect(),
1264                )
1265            }
1266            _ => (
1267                start.map(|start| CommandRange { start, end: None }),
1268                chars.collect(),
1269            ),
1270        }
1271    }
1272
1273    fn parse_position(chars: &mut Peekable<Chars>) -> Option<Position> {
1274        match chars.peek()? {
1275            '0'..='9' => {
1276                let row = Self::parse_u32(chars);
1277                Some(Position::Line {
1278                    row,
1279                    offset: Self::parse_offset(chars),
1280                })
1281            }
1282            '\'' => {
1283                chars.next();
1284                let name = chars.next()?;
1285                Some(Position::Mark {
1286                    name,
1287                    offset: Self::parse_offset(chars),
1288                })
1289            }
1290            '.' => {
1291                chars.next();
1292                Some(Position::CurrentLine {
1293                    offset: Self::parse_offset(chars),
1294                })
1295            }
1296            '+' | '-' => Some(Position::CurrentLine {
1297                offset: Self::parse_offset(chars),
1298            }),
1299            '$' => {
1300                chars.next();
1301                Some(Position::LastLine {
1302                    offset: Self::parse_offset(chars),
1303                })
1304            }
1305            _ => None,
1306        }
1307    }
1308
1309    fn parse_offset(chars: &mut Peekable<Chars>) -> i32 {
1310        let mut res: i32 = 0;
1311        while matches!(chars.peek(), Some('+' | '-')) {
1312            let sign = if chars.next().unwrap() == '+' { 1 } else { -1 };
1313            let amount = if matches!(chars.peek(), Some('0'..='9')) {
1314                (Self::parse_u32(chars) as i32).saturating_mul(sign)
1315            } else {
1316                sign
1317            };
1318            res = res.saturating_add(amount)
1319        }
1320        res
1321    }
1322
1323    fn parse_u32(chars: &mut Peekable<Chars>) -> u32 {
1324        let mut res: u32 = 0;
1325        while matches!(chars.peek(), Some('0'..='9')) {
1326            res = res
1327                .saturating_mul(10)
1328                .saturating_add(chars.next().unwrap() as u32 - '0' as u32);
1329        }
1330        res
1331    }
1332}
1333
1334#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq)]
1335enum Position {
1336    Line { row: u32, offset: i32 },
1337    Mark { name: char, offset: i32 },
1338    LastLine { offset: i32 },
1339    CurrentLine { offset: i32 },
1340}
1341
1342impl Position {
1343    fn buffer_row(
1344        &self,
1345        vim: &Vim,
1346        editor: &mut Editor,
1347        window: &mut Window,
1348        cx: &mut App,
1349    ) -> Result<MultiBufferRow> {
1350        let snapshot = editor.snapshot(window, cx);
1351        let target = match self {
1352            Position::Line { row, offset } => {
1353                if let Some(anchor) = editor.active_excerpt(cx).and_then(|(_, buffer, _)| {
1354                    editor.buffer().read(cx).buffer_point_to_anchor(
1355                        &buffer,
1356                        Point::new(row.saturating_sub(1), 0),
1357                        cx,
1358                    )
1359                }) {
1360                    anchor
1361                        .to_point(&snapshot.buffer_snapshot())
1362                        .row
1363                        .saturating_add_signed(*offset)
1364                } else {
1365                    row.saturating_add_signed(offset.saturating_sub(1))
1366                }
1367            }
1368            Position::Mark { name, offset } => {
1369                let Some(Mark::Local(anchors)) =
1370                    vim.get_mark(&name.to_string(), editor, window, cx)
1371                else {
1372                    anyhow::bail!("mark {name} not set");
1373                };
1374                let Some(mark) = anchors.last() else {
1375                    anyhow::bail!("mark {name} contains empty anchors");
1376                };
1377                mark.to_point(&snapshot.buffer_snapshot())
1378                    .row
1379                    .saturating_add_signed(*offset)
1380            }
1381            Position::LastLine { offset } => snapshot
1382                .buffer_snapshot()
1383                .max_row()
1384                .0
1385                .saturating_add_signed(*offset),
1386            Position::CurrentLine { offset } => editor
1387                .selections
1388                .newest_anchor()
1389                .head()
1390                .to_point(&snapshot.buffer_snapshot())
1391                .row
1392                .saturating_add_signed(*offset),
1393        };
1394
1395        Ok(MultiBufferRow(target).min(snapshot.buffer_snapshot().max_row()))
1396    }
1397}
1398
1399#[derive(Clone, Debug, PartialEq)]
1400pub(crate) struct CommandRange {
1401    start: Position,
1402    end: Option<Position>,
1403}
1404
1405impl CommandRange {
1406    fn head(&self) -> &Position {
1407        self.end.as_ref().unwrap_or(&self.start)
1408    }
1409
1410    /// Convert the `CommandRange` into a `Range<MultiBufferRow>`.
1411    pub(crate) fn buffer_range(
1412        &self,
1413        vim: &Vim,
1414        editor: &mut Editor,
1415        window: &mut Window,
1416        cx: &mut App,
1417    ) -> Result<Range<MultiBufferRow>> {
1418        let start = self.start.buffer_row(vim, editor, window, cx)?;
1419        let end = if let Some(end) = self.end.as_ref() {
1420            end.buffer_row(vim, editor, window, cx)?
1421        } else {
1422            start
1423        };
1424        if end < start {
1425            anyhow::Ok(end..start)
1426        } else {
1427            anyhow::Ok(start..end)
1428        }
1429    }
1430
1431    pub fn as_count(&self) -> Option<u32> {
1432        if let CommandRange {
1433            start: Position::Line { row, offset: 0 },
1434            end: None,
1435        } = &self
1436        {
1437            Some(*row)
1438        } else {
1439            None
1440        }
1441    }
1442
1443    /// The `CommandRange` representing the entire buffer.
1444    fn buffer() -> Self {
1445        Self {
1446            start: Position::Line { row: 1, offset: 0 },
1447            end: Some(Position::LastLine { offset: 0 }),
1448        }
1449    }
1450}
1451
1452fn generate_commands(_: &App) -> Vec<VimCommand> {
1453    vec![
1454        VimCommand::new(
1455            ("w", "rite"),
1456            VimSave {
1457                save_intent: Some(SaveIntent::Save),
1458                filename: "".into(),
1459                range: None,
1460            },
1461        )
1462        .bang(VimSave {
1463            save_intent: Some(SaveIntent::Overwrite),
1464            filename: "".into(),
1465            range: None,
1466        })
1467        .filename(|action, filename| {
1468            Some(
1469                VimSave {
1470                    save_intent: action
1471                        .as_any()
1472                        .downcast_ref::<VimSave>()
1473                        .and_then(|action| action.save_intent),
1474                    filename,
1475                    range: None,
1476                }
1477                .boxed_clone(),
1478            )
1479        })
1480        .range(|action, range| {
1481            let mut action: VimSave = action.as_any().downcast_ref::<VimSave>().unwrap().clone();
1482            action.range.replace(range.clone());
1483            Some(Box::new(action))
1484        }),
1485        VimCommand::new(("e", "dit"), editor::actions::ReloadFile)
1486            .bang(editor::actions::ReloadFile)
1487            .filename(|_, filename| Some(VimEdit { filename }.boxed_clone())),
1488        VimCommand::new(
1489            ("r", "ead"),
1490            VimRead {
1491                range: None,
1492                filename: "".into(),
1493            },
1494        )
1495        .filename(|_, filename| {
1496            Some(
1497                VimRead {
1498                    range: None,
1499                    filename,
1500                }
1501                .boxed_clone(),
1502            )
1503        })
1504        .range(|action, range| {
1505            let mut action: VimRead = action.as_any().downcast_ref::<VimRead>().unwrap().clone();
1506            action.range.replace(range.clone());
1507            Some(Box::new(action))
1508        }),
1509        VimCommand::new(("sp", "lit"), workspace::SplitHorizontal::default()).filename(
1510            |_, filename| {
1511                Some(
1512                    VimSplit {
1513                        vertical: false,
1514                        filename,
1515                    }
1516                    .boxed_clone(),
1517                )
1518            },
1519        ),
1520        VimCommand::new(("vs", "plit"), workspace::SplitVertical::default()).filename(
1521            |_, filename| {
1522                Some(
1523                    VimSplit {
1524                        vertical: true,
1525                        filename,
1526                    }
1527                    .boxed_clone(),
1528                )
1529            },
1530        ),
1531        VimCommand::new(("tabe", "dit"), workspace::NewFile)
1532            .filename(|_action, filename| Some(VimEdit { filename }.boxed_clone())),
1533        VimCommand::new(("tabnew", ""), workspace::NewFile)
1534            .filename(|_action, filename| Some(VimEdit { filename }.boxed_clone())),
1535        VimCommand::new(
1536            ("q", "uit"),
1537            workspace::CloseActiveItem {
1538                save_intent: Some(SaveIntent::Close),
1539                close_pinned: false,
1540            },
1541        )
1542        .bang(workspace::CloseActiveItem {
1543            save_intent: Some(SaveIntent::Skip),
1544            close_pinned: true,
1545        }),
1546        VimCommand::new(
1547            ("wq", ""),
1548            workspace::CloseActiveItem {
1549                save_intent: Some(SaveIntent::Save),
1550                close_pinned: false,
1551            },
1552        )
1553        .bang(workspace::CloseActiveItem {
1554            save_intent: Some(SaveIntent::Overwrite),
1555            close_pinned: true,
1556        }),
1557        VimCommand::new(
1558            ("x", "it"),
1559            workspace::CloseActiveItem {
1560                save_intent: Some(SaveIntent::SaveAll),
1561                close_pinned: false,
1562            },
1563        )
1564        .bang(workspace::CloseActiveItem {
1565            save_intent: Some(SaveIntent::Overwrite),
1566            close_pinned: true,
1567        }),
1568        VimCommand::new(
1569            ("exi", "t"),
1570            workspace::CloseActiveItem {
1571                save_intent: Some(SaveIntent::SaveAll),
1572                close_pinned: false,
1573            },
1574        )
1575        .bang(workspace::CloseActiveItem {
1576            save_intent: Some(SaveIntent::Overwrite),
1577            close_pinned: true,
1578        }),
1579        VimCommand::new(
1580            ("up", "date"),
1581            workspace::Save {
1582                save_intent: Some(SaveIntent::SaveAll),
1583            },
1584        ),
1585        VimCommand::new(
1586            ("wa", "ll"),
1587            workspace::SaveAll {
1588                save_intent: Some(SaveIntent::SaveAll),
1589            },
1590        )
1591        .bang(workspace::SaveAll {
1592            save_intent: Some(SaveIntent::Overwrite),
1593        }),
1594        VimCommand::new(
1595            ("qa", "ll"),
1596            workspace::CloseAllItemsAndPanes {
1597                save_intent: Some(SaveIntent::Close),
1598            },
1599        )
1600        .bang(workspace::CloseAllItemsAndPanes {
1601            save_intent: Some(SaveIntent::Skip),
1602        }),
1603        VimCommand::new(
1604            ("quita", "ll"),
1605            workspace::CloseAllItemsAndPanes {
1606                save_intent: Some(SaveIntent::Close),
1607            },
1608        )
1609        .bang(workspace::CloseAllItemsAndPanes {
1610            save_intent: Some(SaveIntent::Skip),
1611        }),
1612        VimCommand::new(
1613            ("xa", "ll"),
1614            workspace::CloseAllItemsAndPanes {
1615                save_intent: Some(SaveIntent::SaveAll),
1616            },
1617        )
1618        .bang(workspace::CloseAllItemsAndPanes {
1619            save_intent: Some(SaveIntent::Overwrite),
1620        }),
1621        VimCommand::new(
1622            ("wqa", "ll"),
1623            workspace::CloseAllItemsAndPanes {
1624                save_intent: Some(SaveIntent::SaveAll),
1625            },
1626        )
1627        .bang(workspace::CloseAllItemsAndPanes {
1628            save_intent: Some(SaveIntent::Overwrite),
1629        }),
1630        VimCommand::new(("cq", "uit"), zed_actions::Quit),
1631        VimCommand::new(
1632            ("bd", "elete"),
1633            workspace::CloseActiveItem {
1634                save_intent: Some(SaveIntent::Close),
1635                close_pinned: false,
1636            },
1637        )
1638        .bang(workspace::CloseActiveItem {
1639            save_intent: Some(SaveIntent::Skip),
1640            close_pinned: true,
1641        }),
1642        VimCommand::new(
1643            ("norm", "al"),
1644            VimNorm {
1645                command: "".into(),
1646                range: None,
1647                override_rows: None,
1648            },
1649        )
1650        .args(|_, args| {
1651            Some(
1652                VimNorm {
1653                    command: args,
1654                    range: None,
1655                    override_rows: None,
1656                }
1657                .boxed_clone(),
1658            )
1659        })
1660        .range(|action, range| {
1661            let mut action: VimNorm = action.as_any().downcast_ref::<VimNorm>().unwrap().clone();
1662            action.range.replace(range.clone());
1663            Some(Box::new(action))
1664        }),
1665        VimCommand::new(("bn", "ext"), workspace::ActivateNextItem).count(),
1666        VimCommand::new(("bN", "ext"), workspace::ActivatePreviousItem).count(),
1667        VimCommand::new(("bp", "revious"), workspace::ActivatePreviousItem).count(),
1668        VimCommand::new(("bf", "irst"), workspace::ActivateItem(0)),
1669        VimCommand::new(("br", "ewind"), workspace::ActivateItem(0)),
1670        VimCommand::new(("bl", "ast"), workspace::ActivateLastItem),
1671        VimCommand::str(("buffers", ""), "tab_switcher::ToggleAll"),
1672        VimCommand::str(("ls", ""), "tab_switcher::ToggleAll"),
1673        VimCommand::new(("new", ""), workspace::NewFileSplitHorizontal),
1674        VimCommand::new(("vne", "w"), workspace::NewFileSplitVertical),
1675        VimCommand::new(("tabn", "ext"), workspace::ActivateNextItem).count(),
1676        VimCommand::new(("tabp", "revious"), workspace::ActivatePreviousItem).count(),
1677        VimCommand::new(("tabN", "ext"), workspace::ActivatePreviousItem).count(),
1678        VimCommand::new(
1679            ("tabc", "lose"),
1680            workspace::CloseActiveItem {
1681                save_intent: Some(SaveIntent::Close),
1682                close_pinned: false,
1683            },
1684        ),
1685        VimCommand::new(
1686            ("tabo", "nly"),
1687            workspace::CloseOtherItems {
1688                save_intent: Some(SaveIntent::Close),
1689                close_pinned: false,
1690            },
1691        )
1692        .bang(workspace::CloseOtherItems {
1693            save_intent: Some(SaveIntent::Skip),
1694            close_pinned: false,
1695        }),
1696        VimCommand::new(
1697            ("on", "ly"),
1698            workspace::CloseInactiveTabsAndPanes {
1699                save_intent: Some(SaveIntent::Close),
1700            },
1701        )
1702        .bang(workspace::CloseInactiveTabsAndPanes {
1703            save_intent: Some(SaveIntent::Skip),
1704        }),
1705        VimCommand::str(("cl", "ist"), "diagnostics::Deploy"),
1706        VimCommand::new(("cc", ""), editor::actions::Hover),
1707        VimCommand::new(("ll", ""), editor::actions::Hover),
1708        VimCommand::new(("cn", "ext"), editor::actions::GoToDiagnostic::default())
1709            .range(wrap_count),
1710        VimCommand::new(
1711            ("cp", "revious"),
1712            editor::actions::GoToPreviousDiagnostic::default(),
1713        )
1714        .range(wrap_count),
1715        VimCommand::new(
1716            ("cN", "ext"),
1717            editor::actions::GoToPreviousDiagnostic::default(),
1718        )
1719        .range(wrap_count),
1720        VimCommand::new(
1721            ("lp", "revious"),
1722            editor::actions::GoToPreviousDiagnostic::default(),
1723        )
1724        .range(wrap_count),
1725        VimCommand::new(
1726            ("lN", "ext"),
1727            editor::actions::GoToPreviousDiagnostic::default(),
1728        )
1729        .range(wrap_count),
1730        VimCommand::new(("j", "oin"), JoinLines).range(select_range),
1731        VimCommand::new(("fo", "ld"), editor::actions::FoldSelectedRanges).range(act_on_range),
1732        VimCommand::new(("foldo", "pen"), editor::actions::UnfoldLines)
1733            .bang(editor::actions::UnfoldRecursive)
1734            .range(act_on_range),
1735        VimCommand::new(("foldc", "lose"), editor::actions::Fold)
1736            .bang(editor::actions::FoldRecursive)
1737            .range(act_on_range),
1738        VimCommand::new(("dif", "fupdate"), editor::actions::ToggleSelectedDiffHunks)
1739            .range(act_on_range),
1740        VimCommand::str(("rev", "ert"), "git::Restore").range(act_on_range),
1741        VimCommand::new(("d", "elete"), VisualDeleteLine).range(select_range),
1742        VimCommand::new(("y", "ank"), gpui::NoAction).range(|_, range| {
1743            Some(
1744                YankCommand {
1745                    range: range.clone(),
1746                }
1747                .boxed_clone(),
1748            )
1749        }),
1750        VimCommand::new(("reg", "isters"), ToggleRegistersView).bang(ToggleRegistersView),
1751        VimCommand::new(("di", "splay"), ToggleRegistersView).bang(ToggleRegistersView),
1752        VimCommand::new(("marks", ""), ToggleMarksView).bang(ToggleMarksView),
1753        VimCommand::new(("delm", "arks"), ArgumentRequired)
1754            .bang(DeleteMarks::AllLocal)
1755            .args(|_, args| Some(DeleteMarks::Marks(args).boxed_clone())),
1756        VimCommand::new(("sor", "t"), SortLinesCaseSensitive)
1757            .range(select_range)
1758            .default_range(CommandRange::buffer()),
1759        VimCommand::new(("sort i", ""), SortLinesCaseInsensitive)
1760            .range(select_range)
1761            .default_range(CommandRange::buffer()),
1762        VimCommand::str(("E", "xplore"), "project_panel::ToggleFocus"),
1763        VimCommand::str(("H", "explore"), "project_panel::ToggleFocus"),
1764        VimCommand::str(("L", "explore"), "project_panel::ToggleFocus"),
1765        VimCommand::str(("S", "explore"), "project_panel::ToggleFocus"),
1766        VimCommand::str(("Ve", "xplore"), "project_panel::ToggleFocus"),
1767        VimCommand::str(("te", "rm"), "terminal_panel::Toggle"),
1768        VimCommand::str(("T", "erm"), "terminal_panel::Toggle"),
1769        VimCommand::str(("C", "ollab"), "collab_panel::ToggleFocus"),
1770        VimCommand::str(("No", "tifications"), "notification_panel::ToggleFocus"),
1771        VimCommand::str(("A", "I"), "agent::ToggleFocus"),
1772        VimCommand::str(("G", "it"), "git_panel::ToggleFocus"),
1773        VimCommand::str(("D", "ebug"), "debug_panel::ToggleFocus"),
1774        VimCommand::new(("noh", "lsearch"), search::buffer_search::Dismiss),
1775        VimCommand::new(("$", ""), EndOfDocument),
1776        VimCommand::new(("%", ""), EndOfDocument),
1777        VimCommand::new(("0", ""), StartOfDocument),
1778        VimCommand::new(("ex", ""), editor::actions::ReloadFile).bang(editor::actions::ReloadFile),
1779        VimCommand::new(("cpp", "link"), editor::actions::CopyPermalinkToLine).range(act_on_range),
1780        VimCommand::str(("opt", "ions"), "zed::OpenDefaultSettings"),
1781        VimCommand::str(("map", ""), "vim::OpenDefaultKeymap"),
1782        VimCommand::new(("h", "elp"), OpenDocs),
1783    ]
1784}
1785
1786struct VimCommands(Vec<VimCommand>);
1787// safety: we only ever access this from the main thread (as ensured by the cx argument)
1788// actions are not Sync so we can't otherwise use a OnceLock.
1789unsafe impl Sync for VimCommands {}
1790impl Global for VimCommands {}
1791
1792fn commands(cx: &App) -> &Vec<VimCommand> {
1793    static COMMANDS: OnceLock<VimCommands> = OnceLock::new();
1794    &COMMANDS
1795        .get_or_init(|| VimCommands(generate_commands(cx)))
1796        .0
1797}
1798
1799fn act_on_range(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
1800    Some(
1801        WithRange {
1802            restore_selection: true,
1803            range: range.clone(),
1804            action: WrappedAction(action),
1805        }
1806        .boxed_clone(),
1807    )
1808}
1809
1810fn select_range(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
1811    Some(
1812        WithRange {
1813            restore_selection: false,
1814            range: range.clone(),
1815            action: WrappedAction(action),
1816        }
1817        .boxed_clone(),
1818    )
1819}
1820
1821fn wrap_count(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
1822    range.as_count().map(|count| {
1823        WithCount {
1824            count,
1825            action: WrappedAction(action),
1826        }
1827        .boxed_clone()
1828    })
1829}
1830
1831pub fn command_interceptor(
1832    mut input: &str,
1833    workspace: WeakEntity<Workspace>,
1834    cx: &mut App,
1835) -> Task<CommandInterceptResult> {
1836    while input.starts_with(':') {
1837        input = &input[1..];
1838    }
1839
1840    let (range, query) = VimCommand::parse_range(input);
1841    let range_prefix = input[0..(input.len() - query.len())].to_string();
1842    let has_trailing_space = query.ends_with(" ");
1843    let mut query = query.as_str().trim_start();
1844
1845    let on_matching_lines = (query.starts_with('g') || query.starts_with('v'))
1846        .then(|| {
1847            let (pattern, range, search, invert) = OnMatchingLines::parse(query, &range)?;
1848            let start_idx = query.len() - pattern.len();
1849            query = query[start_idx..].trim();
1850            Some((range, search, invert))
1851        })
1852        .flatten();
1853
1854    let mut action = if range.is_some() && query.is_empty() {
1855        Some(
1856            GoToLine {
1857                range: range.clone().unwrap(),
1858            }
1859            .boxed_clone(),
1860        )
1861    } else if query.starts_with('/') || query.starts_with('?') {
1862        Some(
1863            FindCommand {
1864                query: query[1..].to_string(),
1865                backwards: query.starts_with('?'),
1866            }
1867            .boxed_clone(),
1868        )
1869    } else if query.starts_with("se ") || query.starts_with("set ") {
1870        let (prefix, option) = query.split_once(' ').unwrap();
1871        let mut commands = VimOption::possible_commands(option);
1872        if !commands.is_empty() {
1873            let query = prefix.to_string() + " " + option;
1874            for command in &mut commands {
1875                command.positions = generate_positions(&command.string, &query);
1876            }
1877        }
1878        return Task::ready(CommandInterceptResult {
1879            results: commands,
1880            exclusive: false,
1881        });
1882    } else if query.starts_with('s') {
1883        let mut substitute = "substitute".chars().peekable();
1884        let mut query = query.chars().peekable();
1885        while substitute
1886            .peek()
1887            .is_some_and(|char| Some(char) == query.peek())
1888        {
1889            substitute.next();
1890            query.next();
1891        }
1892        if let Some(replacement) = Replacement::parse(query) {
1893            let range = range.clone().unwrap_or(CommandRange {
1894                start: Position::CurrentLine { offset: 0 },
1895                end: None,
1896            });
1897            Some(ReplaceCommand { replacement, range }.boxed_clone())
1898        } else {
1899            None
1900        }
1901    } else if query.contains('!') {
1902        ShellExec::parse(query, range.clone())
1903    } else if on_matching_lines.is_some() {
1904        commands(cx)
1905            .iter()
1906            .find_map(|command| command.parse(query, &None, cx))
1907    } else {
1908        None
1909    };
1910
1911    if let Some((range, search, invert)) = on_matching_lines
1912        && let Some(ref inner) = action
1913    {
1914        action = Some(Box::new(OnMatchingLines {
1915            range,
1916            search,
1917            action: WrappedAction(inner.boxed_clone()),
1918            invert,
1919        }));
1920    };
1921
1922    if let Some(action) = action {
1923        let string = input.to_string();
1924        let positions = generate_positions(&string, &(range_prefix + query));
1925        return Task::ready(CommandInterceptResult {
1926            results: vec![CommandInterceptItem {
1927                action,
1928                string,
1929                positions,
1930            }],
1931            exclusive: false,
1932        });
1933    }
1934
1935    let Some((mut results, filenames)) =
1936        commands(cx).iter().enumerate().find_map(|(idx, command)| {
1937            let action = command.parse(query, &range, cx)?;
1938            let parsed_query = command.get_parsed_query(query.into())?;
1939            let display_string = ":".to_owned()
1940                + &range_prefix
1941                + command.prefix
1942                + command.suffix
1943                + if parsed_query.has_bang { "!" } else { "" };
1944            let space = if parsed_query.has_space { " " } else { "" };
1945
1946            let string = format!("{}{}{}", &display_string, &space, &parsed_query.args);
1947            let positions = generate_positions(&string, &(range_prefix.clone() + query));
1948
1949            let results = vec![CommandInterceptItem {
1950                action,
1951                string,
1952                positions,
1953            }];
1954
1955            let no_args_positions =
1956                generate_positions(&display_string, &(range_prefix.clone() + query));
1957
1958            // The following are valid autocomplete scenarios:
1959            // :w!filename.txt
1960            // :w filename.txt
1961            // :w[space]
1962            if !command.has_filename
1963                || (!has_trailing_space && !parsed_query.has_bang && parsed_query.args.is_empty())
1964            {
1965                return Some((results, None));
1966            }
1967
1968            Some((
1969                results,
1970                Some((idx, parsed_query, display_string, no_args_positions)),
1971            ))
1972        })
1973    else {
1974        return Task::ready(CommandInterceptResult::default());
1975    };
1976
1977    if let Some((cmd_idx, parsed_query, display_string, no_args_positions)) = filenames {
1978        let filenames = VimCommand::generate_filename_completions(&parsed_query, workspace, cx);
1979        cx.spawn(async move |cx| {
1980            let filenames = filenames.await;
1981            const MAX_RESULTS: usize = 100;
1982            let executor = cx.background_executor().clone();
1983            let mut candidates = Vec::with_capacity(filenames.len());
1984
1985            for (idx, filename) in filenames.iter().enumerate() {
1986                candidates.push(fuzzy::StringMatchCandidate::new(idx, &filename));
1987            }
1988            let filenames = fuzzy::match_strings(
1989                &candidates,
1990                &parsed_query.args,
1991                false,
1992                true,
1993                MAX_RESULTS,
1994                &Default::default(),
1995                executor,
1996            )
1997            .await;
1998
1999            for fuzzy::StringMatch {
2000                candidate_id: _,
2001                score: _,
2002                positions,
2003                string,
2004            } in filenames
2005            {
2006                let offset = display_string.len() + 1;
2007                let mut positions: Vec<_> = positions.iter().map(|&pos| pos + offset).collect();
2008                positions.splice(0..0, no_args_positions.clone());
2009                let string = format!("{display_string} {string}");
2010                let (range, query) = VimCommand::parse_range(&string[1..]);
2011                let action =
2012                    match cx.update(|cx| commands(cx).get(cmd_idx)?.parse(&query, &range, cx)) {
2013                        Some(action) => action,
2014                        _ => continue,
2015                    };
2016                results.push(CommandInterceptItem {
2017                    action,
2018                    string,
2019                    positions,
2020                });
2021            }
2022            CommandInterceptResult {
2023                results,
2024                exclusive: true,
2025            }
2026        })
2027    } else {
2028        Task::ready(CommandInterceptResult {
2029            results,
2030            exclusive: false,
2031        })
2032    }
2033}
2034
2035fn generate_positions(string: &str, query: &str) -> Vec<usize> {
2036    let mut positions = Vec::new();
2037    let mut chars = query.chars();
2038
2039    let Some(mut current) = chars.next() else {
2040        return positions;
2041    };
2042
2043    for (i, c) in string.char_indices() {
2044        if c == current {
2045            positions.push(i);
2046            if let Some(c) = chars.next() {
2047                current = c;
2048            } else {
2049                break;
2050            }
2051        }
2052    }
2053
2054    positions
2055}
2056
2057/// Applies a command to all lines matching a pattern.
2058#[derive(Debug, PartialEq, Clone, Action)]
2059#[action(namespace = vim, no_json, no_register)]
2060pub(crate) struct OnMatchingLines {
2061    range: CommandRange,
2062    search: String,
2063    action: WrappedAction,
2064    invert: bool,
2065}
2066
2067impl OnMatchingLines {
2068    // convert a vim query into something more usable by zed.
2069    // we don't attempt to fully convert between the two regex syntaxes,
2070    // but we do flip \( and \) to ( and ) (and vice-versa) in the pattern,
2071    // and convert \0..\9 to $0..$9 in the replacement so that common idioms work.
2072    pub(crate) fn parse(
2073        query: &str,
2074        range: &Option<CommandRange>,
2075    ) -> Option<(String, CommandRange, String, bool)> {
2076        let mut global = "global".chars().peekable();
2077        let mut query_chars = query.chars().peekable();
2078        let mut invert = false;
2079        if query_chars.peek() == Some(&'v') {
2080            invert = true;
2081            query_chars.next();
2082        }
2083        while global
2084            .peek()
2085            .is_some_and(|char| Some(char) == query_chars.peek())
2086        {
2087            global.next();
2088            query_chars.next();
2089        }
2090        if !invert && query_chars.peek() == Some(&'!') {
2091            invert = true;
2092            query_chars.next();
2093        }
2094        let range = range.clone().unwrap_or(CommandRange {
2095            start: Position::Line { row: 0, offset: 0 },
2096            end: Some(Position::LastLine { offset: 0 }),
2097        });
2098
2099        let delimiter = query_chars.next().filter(|c| {
2100            !c.is_alphanumeric() && *c != '"' && *c != '|' && *c != '\'' && *c != '!'
2101        })?;
2102
2103        let mut search = String::new();
2104        let mut escaped = false;
2105
2106        for c in query_chars.by_ref() {
2107            if escaped {
2108                escaped = false;
2109                // unescape escaped parens
2110                if c != '(' && c != ')' && c != delimiter {
2111                    search.push('\\')
2112                }
2113                search.push(c)
2114            } else if c == '\\' {
2115                escaped = true;
2116            } else if c == delimiter {
2117                break;
2118            } else {
2119                // escape unescaped parens
2120                if c == '(' || c == ')' {
2121                    search.push('\\')
2122                }
2123                search.push(c)
2124            }
2125        }
2126
2127        Some((query_chars.collect::<String>(), range, search, invert))
2128    }
2129
2130    pub fn run(&self, vim: &mut Vim, window: &mut Window, cx: &mut Context<Vim>) {
2131        let result = vim.update_editor(cx, |vim, editor, cx| {
2132            self.range.buffer_range(vim, editor, window, cx)
2133        });
2134
2135        let range = match result {
2136            None => return,
2137            Some(e @ Err(_)) => {
2138                let Some(workspace) = vim.workspace(window) else {
2139                    return;
2140                };
2141                workspace.update(cx, |workspace, cx| {
2142                    e.notify_err(workspace, NotificationSource::Editor, cx);
2143                });
2144                return;
2145            }
2146            Some(Ok(result)) => result,
2147        };
2148
2149        let mut action = self.action.boxed_clone();
2150        let mut last_pattern = self.search.clone();
2151
2152        let mut regexes = match Regex::new(&self.search) {
2153            Ok(regex) => vec![(regex, !self.invert)],
2154            e @ Err(_) => {
2155                let Some(workspace) = vim.workspace(window) else {
2156                    return;
2157                };
2158                workspace.update(cx, |workspace, cx| {
2159                    e.notify_err(workspace, NotificationSource::Editor, cx);
2160                });
2161                return;
2162            }
2163        };
2164        while let Some(inner) = action
2165            .boxed_clone()
2166            .as_any()
2167            .downcast_ref::<OnMatchingLines>()
2168        {
2169            let Some(regex) = Regex::new(&inner.search).ok() else {
2170                break;
2171            };
2172            last_pattern = inner.search.clone();
2173            action = inner.action.boxed_clone();
2174            regexes.push((regex, !inner.invert))
2175        }
2176
2177        if let Some(pane) = vim.pane(window, cx) {
2178            pane.update(cx, |pane, cx| {
2179                if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()
2180                {
2181                    search_bar.update(cx, |search_bar, cx| {
2182                        if search_bar.show(window, cx) {
2183                            let _ = search_bar.search(
2184                                &last_pattern,
2185                                Some(SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE),
2186                                false,
2187                                window,
2188                                cx,
2189                            );
2190                        }
2191                    });
2192                }
2193            });
2194        };
2195
2196        vim.update_editor(cx, |_, editor, cx| {
2197            let snapshot = editor.snapshot(window, cx);
2198            let mut row = range.start.0;
2199
2200            let point_range = Point::new(range.start.0, 0)
2201                ..snapshot
2202                    .buffer_snapshot()
2203                    .clip_point(Point::new(range.end.0 + 1, 0), Bias::Left);
2204            cx.spawn_in(window, async move |editor, cx| {
2205                let new_selections = cx
2206                    .background_spawn(async move {
2207                        let mut line = String::new();
2208                        let mut new_selections = Vec::new();
2209                        let chunks = snapshot
2210                            .buffer_snapshot()
2211                            .text_for_range(point_range)
2212                            .chain(["\n"]);
2213
2214                        for chunk in chunks {
2215                            for (newline_ix, text) in chunk.split('\n').enumerate() {
2216                                if newline_ix > 0 {
2217                                    if regexes.iter().all(|(regex, should_match)| {
2218                                        regex.is_match(&line) == *should_match
2219                                    }) {
2220                                        new_selections
2221                                            .push(Point::new(row, 0).to_display_point(&snapshot))
2222                                    }
2223                                    row += 1;
2224                                    line.clear();
2225                                }
2226                                line.push_str(text)
2227                            }
2228                        }
2229
2230                        new_selections
2231                    })
2232                    .await;
2233
2234                if new_selections.is_empty() {
2235                    return;
2236                }
2237
2238                if let Some(vim_norm) = action.as_any().downcast_ref::<VimNorm>() {
2239                    let mut vim_norm = vim_norm.clone();
2240                    vim_norm.override_rows =
2241                        Some(new_selections.iter().map(|point| point.row().0).collect());
2242                    editor
2243                        .update_in(cx, |_, window, cx| {
2244                            window.dispatch_action(vim_norm.boxed_clone(), cx);
2245                        })
2246                        .log_err();
2247                    return;
2248                }
2249
2250                editor
2251                    .update_in(cx, |editor, window, cx| {
2252                        editor.start_transaction_at(Instant::now(), window, cx);
2253                        editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2254                            s.replace_cursors_with(|_| new_selections);
2255                        });
2256                        window.dispatch_action(action, cx);
2257
2258                        cx.defer_in(window, move |editor, window, cx| {
2259                            let newest = editor
2260                                .selections
2261                                .newest::<Point>(&editor.display_snapshot(cx));
2262                            editor.change_selections(
2263                                SelectionEffects::no_scroll(),
2264                                window,
2265                                cx,
2266                                |s| {
2267                                    s.select(vec![newest]);
2268                                },
2269                            );
2270                            editor.end_transaction_at(Instant::now(), cx);
2271                        })
2272                    })
2273                    .log_err();
2274            })
2275            .detach();
2276        });
2277    }
2278}
2279
2280/// Executes a shell command and returns the output.
2281#[derive(Clone, Debug, PartialEq, Action)]
2282#[action(namespace = vim, no_json, no_register)]
2283pub struct ShellExec {
2284    command: String,
2285    range: Option<CommandRange>,
2286    is_read: bool,
2287}
2288
2289impl Vim {
2290    pub fn cancel_running_command(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2291        if self.running_command.take().is_some() {
2292            self.update_editor(cx, |_, editor, cx| {
2293                editor.transact(window, cx, |editor, _window, _cx| {
2294                    editor.clear_row_highlights::<ShellExec>();
2295                })
2296            });
2297        }
2298    }
2299
2300    fn prepare_shell_command(
2301        &mut self,
2302        command: &str,
2303        _: &mut Window,
2304        cx: &mut Context<Self>,
2305    ) -> String {
2306        let mut ret = String::new();
2307        // N.B. non-standard escaping rules:
2308        // * !echo % => "echo README.md"
2309        // * !echo \% => "echo %"
2310        // * !echo \\% => echo \%
2311        // * !echo \\\% => echo \\%
2312        for c in command.chars() {
2313            if c != '%' && c != '!' {
2314                ret.push(c);
2315                continue;
2316            } else if ret.chars().last() == Some('\\') {
2317                ret.pop();
2318                ret.push(c);
2319                continue;
2320            }
2321            match c {
2322                '%' => {
2323                    self.update_editor(cx, |_, editor, cx| {
2324                        if let Some((_, buffer, _)) = editor.active_excerpt(cx)
2325                            && let Some(file) = buffer.read(cx).file()
2326                            && let Some(local) = file.as_local()
2327                        {
2328                            ret.push_str(&local.path().display(local.path_style(cx)));
2329                        }
2330                    });
2331                }
2332                '!' => {
2333                    if let Some(command) = &self.last_command {
2334                        ret.push_str(command)
2335                    }
2336                }
2337                _ => {}
2338            }
2339        }
2340        self.last_command = Some(ret.clone());
2341        ret
2342    }
2343
2344    pub fn shell_command_motion(
2345        &mut self,
2346        motion: Motion,
2347        times: Option<usize>,
2348        forced_motion: bool,
2349        window: &mut Window,
2350        cx: &mut Context<Vim>,
2351    ) {
2352        self.stop_recording(cx);
2353        let Some(workspace) = self.workspace(window) else {
2354            return;
2355        };
2356        let command = self.update_editor(cx, |_, editor, cx| {
2357            let snapshot = editor.snapshot(window, cx);
2358            let start = editor
2359                .selections
2360                .newest_display(&editor.display_snapshot(cx));
2361            let text_layout_details = editor.text_layout_details(window, cx);
2362            let (mut range, _) = motion
2363                .range(
2364                    &snapshot,
2365                    start.clone(),
2366                    times,
2367                    &text_layout_details,
2368                    forced_motion,
2369                )
2370                .unwrap_or((start.range(), MotionKind::Exclusive));
2371            if range.start != start.start {
2372                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2373                    s.select_ranges([
2374                        range.start.to_point(&snapshot)..range.start.to_point(&snapshot)
2375                    ]);
2376                })
2377            }
2378            if range.end.row() > range.start.row() && range.end.column() != 0 {
2379                *range.end.row_mut() -= 1
2380            }
2381            if range.end.row() == range.start.row() {
2382                ".!".to_string()
2383            } else {
2384                format!(".,.+{}!", (range.end.row() - range.start.row()).0)
2385            }
2386        });
2387        if let Some(command) = command {
2388            workspace.update(cx, |workspace, cx| {
2389                command_palette::CommandPalette::toggle(workspace, &command, window, cx);
2390            });
2391        }
2392    }
2393
2394    pub fn shell_command_object(
2395        &mut self,
2396        object: Object,
2397        around: bool,
2398        window: &mut Window,
2399        cx: &mut Context<Vim>,
2400    ) {
2401        self.stop_recording(cx);
2402        let Some(workspace) = self.workspace(window) else {
2403            return;
2404        };
2405        let command = self.update_editor(cx, |_, editor, cx| {
2406            let snapshot = editor.snapshot(window, cx);
2407            let start = editor
2408                .selections
2409                .newest_display(&editor.display_snapshot(cx));
2410            let range = object
2411                .range(&snapshot, start.clone(), around, None)
2412                .unwrap_or(start.range());
2413            if range.start != start.start {
2414                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2415                    s.select_ranges([
2416                        range.start.to_point(&snapshot)..range.start.to_point(&snapshot)
2417                    ]);
2418                })
2419            }
2420            if range.end.row() == range.start.row() {
2421                ".!".to_string()
2422            } else {
2423                format!(".,.+{}!", (range.end.row() - range.start.row()).0)
2424            }
2425        });
2426        if let Some(command) = command {
2427            workspace.update(cx, |workspace, cx| {
2428                command_palette::CommandPalette::toggle(workspace, &command, window, cx);
2429            });
2430        }
2431    }
2432}
2433
2434impl ShellExec {
2435    pub fn parse(query: &str, range: Option<CommandRange>) -> Option<Box<dyn Action>> {
2436        let (before, after) = query.split_once('!')?;
2437        let before = before.trim();
2438
2439        if !"read".starts_with(before) {
2440            return None;
2441        }
2442
2443        Some(
2444            ShellExec {
2445                command: after.trim().to_string(),
2446                range,
2447                is_read: !before.is_empty(),
2448            }
2449            .boxed_clone(),
2450        )
2451    }
2452
2453    pub fn run(&self, vim: &mut Vim, window: &mut Window, cx: &mut Context<Vim>) {
2454        let Some(workspace) = vim.workspace(window) else {
2455            return;
2456        };
2457
2458        let project = workspace.read(cx).project().clone();
2459        let command = vim.prepare_shell_command(&self.command, window, cx);
2460
2461        if self.range.is_none() && !self.is_read {
2462            workspace.update(cx, |workspace, cx| {
2463                let project = workspace.project().read(cx);
2464                let cwd = project.first_project_directory(cx);
2465                let shell = project.terminal_settings(&cwd, cx).shell.clone();
2466
2467                let spawn_in_terminal = SpawnInTerminal {
2468                    id: TaskId("vim".to_string()),
2469                    full_label: command.clone(),
2470                    label: command.clone(),
2471                    command: Some(command.clone()),
2472                    args: Vec::new(),
2473                    command_label: command.clone(),
2474                    cwd,
2475                    env: HashMap::default(),
2476                    use_new_terminal: true,
2477                    allow_concurrent_runs: true,
2478                    reveal: RevealStrategy::NoFocus,
2479                    reveal_target: RevealTarget::Dock,
2480                    hide: HideStrategy::Never,
2481                    shell,
2482                    show_summary: false,
2483                    show_command: false,
2484                    show_rerun: false,
2485                };
2486
2487                let task_status = workspace.spawn_in_terminal(spawn_in_terminal, window, cx);
2488                cx.background_spawn(async move {
2489                    match task_status.await {
2490                        Some(Ok(status)) => {
2491                            if status.success() {
2492                                log::debug!("Vim shell exec succeeded");
2493                            } else {
2494                                log::debug!("Vim shell exec failed, code: {:?}", status.code());
2495                            }
2496                        }
2497                        Some(Err(e)) => log::error!("Vim shell exec failed: {e}"),
2498                        None => log::debug!("Vim shell exec got cancelled"),
2499                    }
2500                })
2501                .detach();
2502            });
2503            return;
2504        };
2505
2506        let mut input_snapshot = None;
2507        let mut input_range = None;
2508        let mut needs_newline_prefix = false;
2509        vim.update_editor(cx, |vim, editor, cx| {
2510            let snapshot = editor.buffer().read(cx).snapshot(cx);
2511            let range = if let Some(range) = self.range.clone() {
2512                let Some(range) = range.buffer_range(vim, editor, window, cx).log_err() else {
2513                    return;
2514                };
2515                Point::new(range.start.0, 0)
2516                    ..snapshot.clip_point(Point::new(range.end.0 + 1, 0), Bias::Right)
2517            } else {
2518                let mut end = editor
2519                    .selections
2520                    .newest::<Point>(&editor.display_snapshot(cx))
2521                    .range()
2522                    .end;
2523                end = snapshot.clip_point(Point::new(end.row + 1, 0), Bias::Right);
2524                needs_newline_prefix = end == snapshot.max_point();
2525                end..end
2526            };
2527            if self.is_read {
2528                input_range =
2529                    Some(snapshot.anchor_after(range.end)..snapshot.anchor_after(range.end));
2530            } else {
2531                input_range =
2532                    Some(snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end));
2533            }
2534            editor.highlight_rows::<ShellExec>(
2535                input_range.clone().unwrap(),
2536                cx.theme().status().unreachable_background,
2537                Default::default(),
2538                cx,
2539            );
2540
2541            if !self.is_read {
2542                input_snapshot = Some(snapshot)
2543            }
2544        });
2545
2546        let Some(range) = input_range else { return };
2547
2548        let process_task = project.update(cx, |project, cx| project.exec_in_shell(command, cx));
2549
2550        let is_read = self.is_read;
2551
2552        let task = cx.spawn_in(window, async move |vim, cx| {
2553            let Some(mut process) = process_task.await.log_err() else {
2554                return;
2555            };
2556            process.stdout(Stdio::piped());
2557            process.stderr(Stdio::piped());
2558
2559            if input_snapshot.is_some() {
2560                process.stdin(Stdio::piped());
2561            } else {
2562                process.stdin(Stdio::null());
2563            };
2564
2565            let Some(mut running) = process.spawn().log_err() else {
2566                vim.update_in(cx, |vim, window, cx| {
2567                    vim.cancel_running_command(window, cx);
2568                })
2569                .log_err();
2570                return;
2571            };
2572
2573            if let Some(mut stdin) = running.stdin.take()
2574                && let Some(snapshot) = input_snapshot
2575            {
2576                let range = range.clone();
2577                cx.background_spawn(async move {
2578                    for chunk in snapshot.text_for_range(range) {
2579                        if stdin.write_all(chunk.as_bytes()).await.log_err().is_none() {
2580                            return;
2581                        }
2582                    }
2583                    stdin.flush().await.log_err();
2584                })
2585                .detach();
2586            };
2587
2588            let output = cx.background_spawn(running.output()).await;
2589
2590            let Some(output) = output.log_err() else {
2591                vim.update_in(cx, |vim, window, cx| {
2592                    vim.cancel_running_command(window, cx);
2593                })
2594                .log_err();
2595                return;
2596            };
2597            let mut text = String::new();
2598            if needs_newline_prefix {
2599                text.push('\n');
2600            }
2601            text.push_str(&String::from_utf8_lossy(&output.stdout));
2602            text.push_str(&String::from_utf8_lossy(&output.stderr));
2603            if !text.is_empty() && text.chars().last() != Some('\n') {
2604                text.push('\n');
2605            }
2606
2607            vim.update_in(cx, |vim, window, cx| {
2608                vim.update_editor(cx, |_, editor, cx| {
2609                    editor.transact(window, cx, |editor, window, cx| {
2610                        editor.edit([(range.clone(), text)], cx);
2611                        let snapshot = editor.buffer().read(cx).snapshot(cx);
2612                        editor.change_selections(Default::default(), window, cx, |s| {
2613                            let point = if is_read {
2614                                let point = range.end.to_point(&snapshot);
2615                                Point::new(point.row.saturating_sub(1), 0)
2616                            } else {
2617                                let point = range.start.to_point(&snapshot);
2618                                Point::new(point.row, 0)
2619                            };
2620                            s.select_ranges([point..point]);
2621                        })
2622                    })
2623                });
2624                vim.cancel_running_command(window, cx);
2625            })
2626            .log_err();
2627        });
2628        vim.running_command.replace(task);
2629    }
2630}
2631
2632#[cfg(test)]
2633mod test {
2634    use std::path::{Path, PathBuf};
2635
2636    use crate::{
2637        VimAddon,
2638        state::Mode,
2639        test::{NeovimBackedTestContext, VimTestContext},
2640    };
2641    use editor::{Editor, EditorSettings};
2642    use gpui::{Context, TestAppContext};
2643    use indoc::indoc;
2644    use settings::Settings;
2645    use util::path;
2646    use workspace::{OpenOptions, Workspace};
2647
2648    #[gpui::test]
2649    async fn test_command_basics(cx: &mut TestAppContext) {
2650        let mut cx = NeovimBackedTestContext::new(cx).await;
2651
2652        cx.set_shared_state(indoc! {"
2653            ˇa
2654            b
2655            c"})
2656            .await;
2657
2658        cx.simulate_shared_keystrokes(": j enter").await;
2659
2660        // hack: our cursor positioning after a join command is wrong
2661        cx.simulate_shared_keystrokes("^").await;
2662        cx.shared_state().await.assert_eq(indoc! {
2663            "ˇa b
2664            c"
2665        });
2666    }
2667
2668    #[gpui::test]
2669    async fn test_command_goto(cx: &mut TestAppContext) {
2670        let mut cx = NeovimBackedTestContext::new(cx).await;
2671
2672        cx.set_shared_state(indoc! {"
2673            ˇa
2674            b
2675            c"})
2676            .await;
2677        cx.simulate_shared_keystrokes(": 3 enter").await;
2678        cx.shared_state().await.assert_eq(indoc! {"
2679            a
2680            b
2681            ˇc"});
2682    }
2683
2684    #[gpui::test]
2685    async fn test_command_replace(cx: &mut TestAppContext) {
2686        let mut cx = NeovimBackedTestContext::new(cx).await;
2687
2688        cx.set_shared_state(indoc! {"
2689            ˇa
2690            b
2691            b
2692            c"})
2693            .await;
2694        cx.simulate_shared_keystrokes(": % s / b / d enter").await;
2695        cx.shared_state().await.assert_eq(indoc! {"
2696            a
2697            d
2698            ˇd
2699            c"});
2700        cx.simulate_shared_keystrokes(": % s : . : \\ 0 \\ 0 enter")
2701            .await;
2702        cx.shared_state().await.assert_eq(indoc! {"
2703            aa
2704            dd
2705            dd
2706            ˇcc"});
2707        cx.simulate_shared_keystrokes("k : s / d d / e e enter")
2708            .await;
2709        cx.shared_state().await.assert_eq(indoc! {"
2710            aa
2711            dd
2712            ˇee
2713            cc"});
2714    }
2715
2716    #[gpui::test]
2717    async fn test_command_search(cx: &mut TestAppContext) {
2718        let mut cx = NeovimBackedTestContext::new(cx).await;
2719
2720        cx.set_shared_state(indoc! {"
2721                ˇa
2722                b
2723                a
2724                c"})
2725            .await;
2726        cx.simulate_shared_keystrokes(": / b enter").await;
2727        cx.shared_state().await.assert_eq(indoc! {"
2728                a
2729                ˇb
2730                a
2731                c"});
2732        cx.simulate_shared_keystrokes(": ? a enter").await;
2733        cx.shared_state().await.assert_eq(indoc! {"
2734                ˇa
2735                b
2736                a
2737                c"});
2738    }
2739
2740    #[gpui::test]
2741    async fn test_command_write(cx: &mut TestAppContext) {
2742        let mut cx = VimTestContext::new(cx, true).await;
2743        let path = Path::new(path!("/root/dir/file.rs"));
2744        let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
2745
2746        cx.simulate_keystrokes("i @ escape");
2747        cx.simulate_keystrokes(": w enter");
2748
2749        assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "@\n");
2750
2751        fs.as_fake().insert_file(path, b"oops\n".to_vec()).await;
2752
2753        // conflict!
2754        cx.simulate_keystrokes("i @ escape");
2755        cx.simulate_keystrokes(": w enter");
2756        cx.simulate_prompt_answer("Cancel");
2757
2758        assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "oops\n");
2759        assert!(!cx.has_pending_prompt());
2760        cx.simulate_keystrokes(": w !");
2761        cx.simulate_keystrokes("enter");
2762        assert!(!cx.has_pending_prompt());
2763        assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "@@\n");
2764    }
2765
2766    #[gpui::test]
2767    async fn test_command_read(cx: &mut TestAppContext) {
2768        let mut cx = VimTestContext::new(cx, true).await;
2769
2770        let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
2771        let path = Path::new(path!("/root/dir/other.rs"));
2772        fs.as_fake().insert_file(path, "1\n2\n3".into()).await;
2773
2774        cx.workspace(|workspace, _, cx| {
2775            assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
2776        });
2777
2778        // File without trailing newline
2779        cx.set_state("one\ntwo\nthreeˇ", Mode::Normal);
2780        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2781        cx.simulate_keystrokes("enter");
2782        cx.assert_state("one\ntwo\nthree\nˇ1\n2\n3", Mode::Normal);
2783
2784        cx.set_state("oneˇ\ntwo\nthree", Mode::Normal);
2785        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2786        cx.simulate_keystrokes("enter");
2787        cx.assert_state("one\nˇ1\n2\n3\ntwo\nthree", Mode::Normal);
2788
2789        cx.set_state("one\nˇtwo\nthree", Mode::Normal);
2790        cx.simulate_keystrokes(": 0 r space d i r / o t h e r . r s");
2791        cx.simulate_keystrokes("enter");
2792        cx.assert_state("ˇ1\n2\n3\none\ntwo\nthree", Mode::Normal);
2793
2794        cx.set_state("one\n«ˇtwo\nthree\nfour»\nfive", Mode::Visual);
2795        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2796        cx.simulate_keystrokes("enter");
2797        cx.run_until_parked();
2798        cx.assert_state("one\ntwo\nthree\nfour\nˇ1\n2\n3\nfive", Mode::Normal);
2799
2800        // Empty filename
2801        cx.set_state("oneˇ\ntwo\nthree", Mode::Normal);
2802        cx.simulate_keystrokes(": r");
2803        cx.simulate_keystrokes("enter");
2804        cx.assert_state("one\nˇone\ntwo\nthree\ntwo\nthree", Mode::Normal);
2805
2806        // File with trailing newline
2807        fs.as_fake().insert_file(path, "1\n2\n3\n".into()).await;
2808        cx.set_state("one\ntwo\nthreeˇ", Mode::Normal);
2809        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2810        cx.simulate_keystrokes("enter");
2811        cx.assert_state("one\ntwo\nthree\nˇ1\n2\n3\n", Mode::Normal);
2812
2813        cx.set_state("oneˇ\ntwo\nthree", Mode::Normal);
2814        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2815        cx.simulate_keystrokes("enter");
2816        cx.assert_state("one\nˇ1\n2\n3\n\ntwo\nthree", Mode::Normal);
2817
2818        cx.set_state("one\n«ˇtwo\nthree\nfour»\nfive", Mode::Visual);
2819        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2820        cx.simulate_keystrokes("enter");
2821        cx.assert_state("one\ntwo\nthree\nfour\nˇ1\n2\n3\n\nfive", Mode::Normal);
2822
2823        cx.set_state("«one\ntwo\nthreeˇ»", Mode::Visual);
2824        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2825        cx.simulate_keystrokes("enter");
2826        cx.assert_state("one\ntwo\nthree\nˇ1\n2\n3\n", Mode::Normal);
2827
2828        // Empty file
2829        fs.as_fake().insert_file(path, "".into()).await;
2830        cx.set_state("ˇone\ntwo\nthree", Mode::Normal);
2831        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2832        cx.simulate_keystrokes("enter");
2833        cx.assert_state("one\nˇtwo\nthree", Mode::Normal);
2834    }
2835
2836    #[gpui::test]
2837    async fn test_command_quit(cx: &mut TestAppContext) {
2838        let mut cx = VimTestContext::new(cx, true).await;
2839
2840        cx.simulate_keystrokes(": n e w enter");
2841        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
2842        cx.simulate_keystrokes(": q enter");
2843        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 1));
2844        cx.simulate_keystrokes(": n e w enter");
2845        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
2846        cx.simulate_keystrokes(": q a enter");
2847        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 0));
2848    }
2849
2850    #[gpui::test]
2851    async fn test_offsets(cx: &mut TestAppContext) {
2852        let mut cx = NeovimBackedTestContext::new(cx).await;
2853
2854        cx.set_shared_state("ˇ1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n")
2855            .await;
2856
2857        cx.simulate_shared_keystrokes(": + enter").await;
2858        cx.shared_state()
2859            .await
2860            .assert_eq("1\nˇ2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n");
2861
2862        cx.simulate_shared_keystrokes(": 1 0 - enter").await;
2863        cx.shared_state()
2864            .await
2865            .assert_eq("1\n2\n3\n4\n5\n6\n7\n8\nˇ9\n10\n11\n");
2866
2867        cx.simulate_shared_keystrokes(": . - 2 enter").await;
2868        cx.shared_state()
2869            .await
2870            .assert_eq("1\n2\n3\n4\n5\n6\nˇ7\n8\n9\n10\n11\n");
2871
2872        cx.simulate_shared_keystrokes(": % enter").await;
2873        cx.shared_state()
2874            .await
2875            .assert_eq("1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\nˇ");
2876    }
2877
2878    #[gpui::test]
2879    async fn test_command_ranges(cx: &mut TestAppContext) {
2880        let mut cx = NeovimBackedTestContext::new(cx).await;
2881
2882        cx.set_shared_state("ˇ1\n2\n3\n4\n4\n3\n2\n1").await;
2883
2884        cx.simulate_shared_keystrokes(": 2 , 4 d enter").await;
2885        cx.shared_state().await.assert_eq("1\nˇ4\n3\n2\n1");
2886
2887        cx.simulate_shared_keystrokes(": 2 , 4 s o r t enter").await;
2888        cx.shared_state().await.assert_eq("1\nˇ2\n3\n4\n1");
2889
2890        cx.simulate_shared_keystrokes(": 2 , 4 j o i n enter").await;
2891        cx.shared_state().await.assert_eq("1\nˇ2 3 4\n1");
2892    }
2893
2894    #[gpui::test]
2895    async fn test_command_visual_replace(cx: &mut TestAppContext) {
2896        let mut cx = NeovimBackedTestContext::new(cx).await;
2897
2898        cx.set_shared_state("ˇ1\n2\n3\n4\n4\n3\n2\n1").await;
2899
2900        cx.simulate_shared_keystrokes("v 2 j : s / . / k enter")
2901            .await;
2902        cx.shared_state().await.assert_eq("k\nk\nˇk\n4\n4\n3\n2\n1");
2903    }
2904
2905    #[track_caller]
2906    fn assert_active_item(
2907        workspace: &mut Workspace,
2908        expected_path: &str,
2909        expected_text: &str,
2910        cx: &mut Context<Workspace>,
2911    ) {
2912        let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();
2913
2914        let buffer = active_editor
2915            .read(cx)
2916            .buffer()
2917            .read(cx)
2918            .as_singleton()
2919            .unwrap();
2920
2921        let text = buffer.read(cx).text();
2922        let file = buffer.read(cx).file().unwrap();
2923        let file_path = file.as_local().unwrap().abs_path(cx);
2924
2925        assert_eq!(text, expected_text);
2926        assert_eq!(file_path, Path::new(expected_path));
2927    }
2928
2929    #[gpui::test]
2930    async fn test_command_gf(cx: &mut TestAppContext) {
2931        let mut cx = VimTestContext::new(cx, true).await;
2932
2933        // Assert base state, that we're in /root/dir/file.rs
2934        cx.workspace(|workspace, _, cx| {
2935            assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
2936        });
2937
2938        // Insert a new file
2939        let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
2940        fs.as_fake()
2941            .insert_file(
2942                path!("/root/dir/file2.rs"),
2943                "This is file2.rs".as_bytes().to_vec(),
2944            )
2945            .await;
2946        fs.as_fake()
2947            .insert_file(
2948                path!("/root/dir/file3.rs"),
2949                "go to file3".as_bytes().to_vec(),
2950            )
2951            .await;
2952
2953        // Put the path to the second file into the currently open buffer
2954        cx.set_state(indoc! {"go to fiˇle2.rs"}, Mode::Normal);
2955
2956        // Go to file2.rs
2957        cx.simulate_keystrokes("g f");
2958
2959        // We now have two items
2960        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
2961        cx.workspace(|workspace, _, cx| {
2962            assert_active_item(
2963                workspace,
2964                path!("/root/dir/file2.rs"),
2965                "This is file2.rs",
2966                cx,
2967            );
2968        });
2969
2970        // Update editor to point to `file2.rs`
2971        cx.editor =
2972            cx.workspace(|workspace, _, cx| workspace.active_item_as::<Editor>(cx).unwrap());
2973
2974        // Put the path to the third file into the currently open buffer,
2975        // but remove its suffix, because we want that lookup to happen automatically.
2976        cx.set_state(indoc! {"go to fiˇle3"}, Mode::Normal);
2977
2978        // Go to file3.rs
2979        cx.simulate_keystrokes("g f");
2980
2981        // We now have three items
2982        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 3));
2983        cx.workspace(|workspace, _, cx| {
2984            assert_active_item(workspace, path!("/root/dir/file3.rs"), "go to file3", cx);
2985        });
2986    }
2987
2988    #[gpui::test]
2989    async fn test_command_write_filename(cx: &mut TestAppContext) {
2990        let mut cx = VimTestContext::new(cx, true).await;
2991
2992        cx.workspace(|workspace, _, cx| {
2993            assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
2994        });
2995
2996        cx.simulate_keystrokes(": w space other.rs");
2997        cx.simulate_keystrokes("enter");
2998
2999        cx.workspace(|workspace, _, cx| {
3000            assert_active_item(workspace, path!("/root/other.rs"), "", cx);
3001        });
3002
3003        cx.simulate_keystrokes(": w space dir/file.rs");
3004        cx.simulate_keystrokes("enter");
3005
3006        cx.simulate_prompt_answer("Replace");
3007        cx.run_until_parked();
3008
3009        cx.workspace(|workspace, _, cx| {
3010            assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
3011        });
3012
3013        cx.simulate_keystrokes(": w ! space other.rs");
3014        cx.simulate_keystrokes("enter");
3015
3016        cx.workspace(|workspace, _, cx| {
3017            assert_active_item(workspace, path!("/root/other.rs"), "", cx);
3018        });
3019    }
3020
3021    #[gpui::test]
3022    async fn test_command_write_range(cx: &mut TestAppContext) {
3023        let mut cx = VimTestContext::new(cx, true).await;
3024
3025        cx.workspace(|workspace, _, cx| {
3026            assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
3027        });
3028
3029        cx.set_state(
3030            indoc! {"
3031                    The quick
3032                    brown« fox
3033                    jumpsˇ» over
3034                    the lazy dog
3035                "},
3036            Mode::Visual,
3037        );
3038
3039        cx.simulate_keystrokes(": w space dir/other.rs");
3040        cx.simulate_keystrokes("enter");
3041
3042        let other = path!("/root/dir/other.rs");
3043
3044        let _ = cx
3045            .workspace(|workspace, window, cx| {
3046                workspace.open_abs_path(PathBuf::from(other), OpenOptions::default(), window, cx)
3047            })
3048            .await;
3049
3050        cx.workspace(|workspace, _, cx| {
3051            assert_active_item(
3052                workspace,
3053                other,
3054                indoc! {"
3055                        brown fox
3056                        jumps over
3057                    "},
3058                cx,
3059            );
3060        });
3061    }
3062
3063    #[gpui::test]
3064    async fn test_command_matching_lines(cx: &mut TestAppContext) {
3065        let mut cx = NeovimBackedTestContext::new(cx).await;
3066
3067        cx.set_shared_state(indoc! {"
3068            ˇa
3069            b
3070            a
3071            b
3072            a
3073        "})
3074            .await;
3075
3076        cx.simulate_shared_keystrokes(":").await;
3077        cx.simulate_shared_keystrokes("g / a / d").await;
3078        cx.simulate_shared_keystrokes("enter").await;
3079
3080        cx.shared_state().await.assert_eq(indoc! {"
3081            b
3082            b
3083            ˇ"});
3084
3085        cx.simulate_shared_keystrokes("u").await;
3086
3087        cx.shared_state().await.assert_eq(indoc! {"
3088            ˇa
3089            b
3090            a
3091            b
3092            a
3093        "});
3094
3095        cx.simulate_shared_keystrokes(":").await;
3096        cx.simulate_shared_keystrokes("v / a / d").await;
3097        cx.simulate_shared_keystrokes("enter").await;
3098
3099        cx.shared_state().await.assert_eq(indoc! {"
3100            a
3101            a
3102            ˇa"});
3103    }
3104
3105    #[gpui::test]
3106    async fn test_del_marks(cx: &mut TestAppContext) {
3107        let mut cx = NeovimBackedTestContext::new(cx).await;
3108
3109        cx.set_shared_state(indoc! {"
3110            ˇa
3111            b
3112            a
3113            b
3114            a
3115        "})
3116            .await;
3117
3118        cx.simulate_shared_keystrokes("m a").await;
3119
3120        let mark = cx.update_editor(|editor, window, cx| {
3121            let vim = editor.addon::<VimAddon>().unwrap().entity.clone();
3122            vim.update(cx, |vim, cx| vim.get_mark("a", editor, window, cx))
3123        });
3124        assert!(mark.is_some());
3125
3126        cx.simulate_shared_keystrokes(": d e l m space a").await;
3127        cx.simulate_shared_keystrokes("enter").await;
3128
3129        let mark = cx.update_editor(|editor, window, cx| {
3130            let vim = editor.addon::<VimAddon>().unwrap().entity.clone();
3131            vim.update(cx, |vim, cx| vim.get_mark("a", editor, window, cx))
3132        });
3133        assert!(mark.is_none())
3134    }
3135
3136    #[gpui::test]
3137    async fn test_normal_command(cx: &mut TestAppContext) {
3138        let mut cx = NeovimBackedTestContext::new(cx).await;
3139
3140        cx.set_shared_state(indoc! {"
3141            The quick
3142            brown« fox
3143            jumpsˇ» over
3144            the lazy dog
3145        "})
3146            .await;
3147
3148        cx.simulate_shared_keystrokes(": n o r m space w C w o r d")
3149            .await;
3150        cx.simulate_shared_keystrokes("enter").await;
3151
3152        cx.shared_state().await.assert_eq(indoc! {"
3153            The quick
3154            brown word
3155            jumps worˇd
3156            the lazy dog
3157        "});
3158
3159        cx.simulate_shared_keystrokes(": n o r m space _ w c i w t e s t")
3160            .await;
3161        cx.simulate_shared_keystrokes("enter").await;
3162
3163        cx.shared_state().await.assert_eq(indoc! {"
3164            The quick
3165            brown word
3166            jumps tesˇt
3167            the lazy dog
3168        "});
3169
3170        cx.simulate_shared_keystrokes("_ l v l : n o r m space s l a")
3171            .await;
3172        cx.simulate_shared_keystrokes("enter").await;
3173
3174        cx.shared_state().await.assert_eq(indoc! {"
3175            The quick
3176            brown word
3177            lˇaumps test
3178            the lazy dog
3179        "});
3180
3181        cx.set_shared_state(indoc! {"
3182            ˇThe quick
3183            brown fox
3184            jumps over
3185            the lazy dog
3186        "})
3187            .await;
3188
3189        cx.simulate_shared_keystrokes("c i w M y escape").await;
3190
3191        cx.shared_state().await.assert_eq(indoc! {"
3192            Mˇy quick
3193            brown fox
3194            jumps over
3195            the lazy dog
3196        "});
3197
3198        cx.simulate_shared_keystrokes(": n o r m space u").await;
3199        cx.simulate_shared_keystrokes("enter").await;
3200
3201        cx.shared_state().await.assert_eq(indoc! {"
3202            ˇThe quick
3203            brown fox
3204            jumps over
3205            the lazy dog
3206        "});
3207
3208        cx.set_shared_state(indoc! {"
3209            The« quick
3210            brownˇ» fox
3211            jumps over
3212            the lazy dog
3213        "})
3214            .await;
3215
3216        cx.simulate_shared_keystrokes(": n o r m space I 1 2 3")
3217            .await;
3218        cx.simulate_shared_keystrokes("enter").await;
3219        cx.simulate_shared_keystrokes("u").await;
3220
3221        cx.shared_state().await.assert_eq(indoc! {"
3222            ˇThe quick
3223            brown fox
3224            jumps over
3225            the lazy dog
3226        "});
3227
3228        cx.set_shared_state(indoc! {"
3229            ˇquick
3230            brown fox
3231            jumps over
3232            the lazy dog
3233        "})
3234            .await;
3235
3236        cx.simulate_shared_keystrokes(": n o r m space I T h e space")
3237            .await;
3238        cx.simulate_shared_keystrokes("enter").await;
3239
3240        cx.shared_state().await.assert_eq(indoc! {"
3241            Theˇ quick
3242            brown fox
3243            jumps over
3244            the lazy dog
3245        "});
3246
3247        // Once ctrl-v to input character literals is added there should be a test for redo
3248    }
3249
3250    #[gpui::test]
3251    async fn test_command_g_normal(cx: &mut TestAppContext) {
3252        let mut cx = NeovimBackedTestContext::new(cx).await;
3253
3254        cx.set_shared_state(indoc! {"
3255            ˇfoo
3256
3257            foo
3258        "})
3259            .await;
3260
3261        cx.simulate_shared_keystrokes(": % g / f o o / n o r m space A b a r")
3262            .await;
3263        cx.simulate_shared_keystrokes("enter").await;
3264        cx.run_until_parked();
3265
3266        cx.shared_state().await.assert_eq(indoc! {"
3267            foobar
3268
3269            foobaˇr
3270        "});
3271
3272        cx.simulate_shared_keystrokes("u").await;
3273
3274        cx.shared_state().await.assert_eq(indoc! {"
3275            foˇo
3276
3277            foo
3278        "});
3279    }
3280
3281    #[gpui::test]
3282    async fn test_command_tabnew(cx: &mut TestAppContext) {
3283        let mut cx = VimTestContext::new(cx, true).await;
3284
3285        // Create a new file to ensure that, when the filename is used with
3286        // `:tabnew`, it opens the existing file in a new tab.
3287        let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
3288        fs.as_fake()
3289            .insert_file(path!("/root/dir/file_2.rs"), "file_2".as_bytes().to_vec())
3290            .await;
3291
3292        cx.simulate_keystrokes(": tabnew");
3293        cx.simulate_keystrokes("enter");
3294        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
3295
3296        // Assert that the new tab is empty and not associated with any file, as
3297        // no file path was provided to the `:tabnew` command.
3298        cx.workspace(|workspace, _window, cx| {
3299            let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();
3300            let buffer = active_editor
3301                .read(cx)
3302                .buffer()
3303                .read(cx)
3304                .as_singleton()
3305                .unwrap();
3306
3307            assert!(&buffer.read(cx).file().is_none());
3308        });
3309
3310        // Leverage the filename as an argument to the `:tabnew` command,
3311        // ensuring that the file, instead of an empty buffer, is opened in a
3312        // new tab.
3313        cx.simulate_keystrokes(": tabnew space dir/file_2.rs");
3314        cx.simulate_keystrokes("enter");
3315
3316        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 3));
3317        cx.workspace(|workspace, _, cx| {
3318            assert_active_item(workspace, path!("/root/dir/file_2.rs"), "file_2", cx);
3319        });
3320
3321        // If the `filename` argument provided to the `:tabnew` command is for a
3322        // file that doesn't yet exist, it should still associate the buffer
3323        // with that file path, so that when the buffer contents are saved, the
3324        // file is created.
3325        cx.simulate_keystrokes(": tabnew space dir/file_3.rs");
3326        cx.simulate_keystrokes("enter");
3327
3328        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 4));
3329        cx.workspace(|workspace, _, cx| {
3330            assert_active_item(workspace, path!("/root/dir/file_3.rs"), "", cx);
3331        });
3332    }
3333
3334    #[gpui::test]
3335    async fn test_command_tabedit(cx: &mut TestAppContext) {
3336        let mut cx = VimTestContext::new(cx, true).await;
3337
3338        // Create a new file to ensure that, when the filename is used with
3339        // `:tabedit`, it opens the existing file in a new tab.
3340        let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
3341        fs.as_fake()
3342            .insert_file(path!("/root/dir/file_2.rs"), "file_2".as_bytes().to_vec())
3343            .await;
3344
3345        cx.simulate_keystrokes(": tabedit");
3346        cx.simulate_keystrokes("enter");
3347        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
3348
3349        // Assert that the new tab is empty and not associated with any file, as
3350        // no file path was provided to the `:tabedit` command.
3351        cx.workspace(|workspace, _window, cx| {
3352            let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();
3353            let buffer = active_editor
3354                .read(cx)
3355                .buffer()
3356                .read(cx)
3357                .as_singleton()
3358                .unwrap();
3359
3360            assert!(&buffer.read(cx).file().is_none());
3361        });
3362
3363        // Leverage the filename as an argument to the `:tabedit` command,
3364        // ensuring that the file, instead of an empty buffer, is opened in a
3365        // new tab.
3366        cx.simulate_keystrokes(": tabedit space dir/file_2.rs");
3367        cx.simulate_keystrokes("enter");
3368
3369        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 3));
3370        cx.workspace(|workspace, _, cx| {
3371            assert_active_item(workspace, path!("/root/dir/file_2.rs"), "file_2", cx);
3372        });
3373
3374        // If the `filename` argument provided to the `:tabedit` command is for a
3375        // file that doesn't yet exist, it should still associate the buffer
3376        // with that file path, so that when the buffer contents are saved, the
3377        // file is created.
3378        cx.simulate_keystrokes(": tabedit space dir/file_3.rs");
3379        cx.simulate_keystrokes("enter");
3380
3381        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 4));
3382        cx.workspace(|workspace, _, cx| {
3383            assert_active_item(workspace, path!("/root/dir/file_3.rs"), "", cx);
3384        });
3385    }
3386
3387    #[gpui::test]
3388    async fn test_ignorecase_command(cx: &mut TestAppContext) {
3389        let mut cx = VimTestContext::new(cx, true).await;
3390        cx.read(|cx| {
3391            assert_eq!(
3392                EditorSettings::get_global(cx).search.case_sensitive,
3393                false,
3394                "The `case_sensitive` setting should be `false` by default."
3395            );
3396        });
3397        cx.simulate_keystrokes(": set space noignorecase");
3398        cx.simulate_keystrokes("enter");
3399        cx.read(|cx| {
3400            assert_eq!(
3401                EditorSettings::get_global(cx).search.case_sensitive,
3402                true,
3403                "The `case_sensitive` setting should have been enabled with `:set noignorecase`."
3404            );
3405        });
3406        cx.simulate_keystrokes(": set space ignorecase");
3407        cx.simulate_keystrokes("enter");
3408        cx.read(|cx| {
3409            assert_eq!(
3410                EditorSettings::get_global(cx).search.case_sensitive,
3411                false,
3412                "The `case_sensitive` setting should have been disabled with `:set ignorecase`."
3413            );
3414        });
3415        cx.simulate_keystrokes(": set space noic");
3416        cx.simulate_keystrokes("enter");
3417        cx.read(|cx| {
3418            assert_eq!(
3419                EditorSettings::get_global(cx).search.case_sensitive,
3420                true,
3421                "The `case_sensitive` setting should have been enabled with `:set noic`."
3422            );
3423        });
3424        cx.simulate_keystrokes(": set space ic");
3425        cx.simulate_keystrokes("enter");
3426        cx.read(|cx| {
3427            assert_eq!(
3428                EditorSettings::get_global(cx).search.case_sensitive,
3429                false,
3430                "The `case_sensitive` setting should have been disabled with `:set ic`."
3431            );
3432        });
3433    }
3434
3435    #[gpui::test]
3436    async fn test_sort_commands(cx: &mut TestAppContext) {
3437        let mut cx = VimTestContext::new(cx, true).await;
3438
3439        cx.set_state(
3440            indoc! {"
3441                «hornet
3442                quirrel
3443                elderbug
3444                cornifer
3445                idaˇ»
3446            "},
3447            Mode::Visual,
3448        );
3449
3450        cx.simulate_keystrokes(": sort");
3451        cx.simulate_keystrokes("enter");
3452
3453        cx.assert_state(
3454            indoc! {"
3455                ˇcornifer
3456                elderbug
3457                hornet
3458                ida
3459                quirrel
3460            "},
3461            Mode::Normal,
3462        );
3463
3464        // Assert that, by default, `:sort` takes case into consideration.
3465        cx.set_state(
3466            indoc! {"
3467                «hornet
3468                quirrel
3469                Elderbug
3470                cornifer
3471                idaˇ»
3472            "},
3473            Mode::Visual,
3474        );
3475
3476        cx.simulate_keystrokes(": sort");
3477        cx.simulate_keystrokes("enter");
3478
3479        cx.assert_state(
3480            indoc! {"
3481                ˇElderbug
3482                cornifer
3483                hornet
3484                ida
3485                quirrel
3486            "},
3487            Mode::Normal,
3488        );
3489
3490        // Assert that, if the `i` option is passed, `:sort` ignores case.
3491        cx.set_state(
3492            indoc! {"
3493                «hornet
3494                quirrel
3495                Elderbug
3496                cornifer
3497                idaˇ»
3498            "},
3499            Mode::Visual,
3500        );
3501
3502        cx.simulate_keystrokes(": sort space i");
3503        cx.simulate_keystrokes("enter");
3504
3505        cx.assert_state(
3506            indoc! {"
3507                ˇcornifer
3508                Elderbug
3509                hornet
3510                ida
3511                quirrel
3512            "},
3513            Mode::Normal,
3514        );
3515
3516        // When no range is provided, sorts the whole buffer.
3517        cx.set_state(
3518            indoc! {"
3519                ˇhornet
3520                quirrel
3521                elderbug
3522                cornifer
3523                ida
3524            "},
3525            Mode::Normal,
3526        );
3527
3528        cx.simulate_keystrokes(": sort");
3529        cx.simulate_keystrokes("enter");
3530
3531        cx.assert_state(
3532            indoc! {"
3533                ˇcornifer
3534                elderbug
3535                hornet
3536                ida
3537                quirrel
3538            "},
3539            Mode::Normal,
3540        );
3541    }
3542}