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