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, SaveStrategy, 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, cx) 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, cx) 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, cx) 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, cx) {
 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, cx) {
 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, cx) 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, cx) 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, cx) 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, cx) 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, cx) 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, cx) 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::CloseItemInAllPanes {
1631                save_intent: Some(SaveIntent::Close),
1632                close_pinned: false,
1633            },
1634        )
1635        .bang(workspace::CloseItemInAllPanes {
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, cx) 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, cx) 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, cx) 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, cx);
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, cx) 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, cx) 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                    save: SaveStrategy::default(),
2483                };
2484
2485                let task_status = workspace.spawn_in_terminal(spawn_in_terminal, window, cx);
2486                cx.background_spawn(async move {
2487                    match task_status.await {
2488                        Some(Ok(status)) => {
2489                            if status.success() {
2490                                log::debug!("Vim shell exec succeeded");
2491                            } else {
2492                                log::debug!("Vim shell exec failed, code: {:?}", status.code());
2493                            }
2494                        }
2495                        Some(Err(e)) => log::error!("Vim shell exec failed: {e}"),
2496                        None => log::debug!("Vim shell exec got cancelled"),
2497                    }
2498                })
2499                .detach();
2500            });
2501            return;
2502        };
2503
2504        let mut input_snapshot = None;
2505        let mut input_range = None;
2506        let mut needs_newline_prefix = false;
2507        vim.update_editor(cx, |vim, editor, cx| {
2508            let snapshot = editor.buffer().read(cx).snapshot(cx);
2509            let range = if let Some(range) = self.range.clone() {
2510                let Some(range) = range.buffer_range(vim, editor, window, cx).log_err() else {
2511                    return;
2512                };
2513                Point::new(range.start.0, 0)
2514                    ..snapshot.clip_point(Point::new(range.end.0 + 1, 0), Bias::Right)
2515            } else {
2516                let mut end = editor
2517                    .selections
2518                    .newest::<Point>(&editor.display_snapshot(cx))
2519                    .range()
2520                    .end;
2521                end = snapshot.clip_point(Point::new(end.row + 1, 0), Bias::Right);
2522                needs_newline_prefix = end == snapshot.max_point();
2523                end..end
2524            };
2525            if self.is_read {
2526                input_range =
2527                    Some(snapshot.anchor_after(range.end)..snapshot.anchor_after(range.end));
2528            } else {
2529                input_range =
2530                    Some(snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end));
2531            }
2532            editor.highlight_rows::<ShellExec>(
2533                input_range.clone().unwrap(),
2534                cx.theme().status().unreachable_background,
2535                Default::default(),
2536                cx,
2537            );
2538
2539            if !self.is_read {
2540                input_snapshot = Some(snapshot)
2541            }
2542        });
2543
2544        let Some(range) = input_range else { return };
2545
2546        let process_task = project.update(cx, |project, cx| project.exec_in_shell(command, cx));
2547
2548        let is_read = self.is_read;
2549
2550        let task = cx.spawn_in(window, async move |vim, cx| {
2551            let Some(mut process) = process_task.await.log_err() else {
2552                return;
2553            };
2554            process.stdout(Stdio::piped());
2555            process.stderr(Stdio::piped());
2556
2557            if input_snapshot.is_some() {
2558                process.stdin(Stdio::piped());
2559            } else {
2560                process.stdin(Stdio::null());
2561            };
2562
2563            let Some(mut running) = process.spawn().log_err() else {
2564                vim.update_in(cx, |vim, window, cx| {
2565                    vim.cancel_running_command(window, cx);
2566                })
2567                .log_err();
2568                return;
2569            };
2570
2571            if let Some(mut stdin) = running.stdin.take()
2572                && let Some(snapshot) = input_snapshot
2573            {
2574                let range = range.clone();
2575                cx.background_spawn(async move {
2576                    for chunk in snapshot.text_for_range(range) {
2577                        if stdin.write_all(chunk.as_bytes()).await.log_err().is_none() {
2578                            return;
2579                        }
2580                    }
2581                    stdin.flush().await.log_err();
2582                })
2583                .detach();
2584            };
2585
2586            let output = cx.background_spawn(running.output()).await;
2587
2588            let Some(output) = output.log_err() else {
2589                vim.update_in(cx, |vim, window, cx| {
2590                    vim.cancel_running_command(window, cx);
2591                })
2592                .log_err();
2593                return;
2594            };
2595            let mut text = String::new();
2596            if needs_newline_prefix {
2597                text.push('\n');
2598            }
2599            text.push_str(&String::from_utf8_lossy(&output.stdout));
2600            text.push_str(&String::from_utf8_lossy(&output.stderr));
2601            if !text.is_empty() && text.chars().last() != Some('\n') {
2602                text.push('\n');
2603            }
2604
2605            vim.update_in(cx, |vim, window, cx| {
2606                vim.update_editor(cx, |_, editor, cx| {
2607                    editor.transact(window, cx, |editor, window, cx| {
2608                        editor.edit([(range.clone(), text)], cx);
2609                        let snapshot = editor.buffer().read(cx).snapshot(cx);
2610                        editor.change_selections(Default::default(), window, cx, |s| {
2611                            let point = if is_read {
2612                                let point = range.end.to_point(&snapshot);
2613                                Point::new(point.row.saturating_sub(1), 0)
2614                            } else {
2615                                let point = range.start.to_point(&snapshot);
2616                                Point::new(point.row, 0)
2617                            };
2618                            s.select_ranges([point..point]);
2619                        })
2620                    })
2621                });
2622                vim.cancel_running_command(window, cx);
2623            })
2624            .log_err();
2625        });
2626        vim.running_command.replace(task);
2627    }
2628}
2629
2630#[cfg(test)]
2631mod test {
2632    use std::path::{Path, PathBuf};
2633
2634    use crate::{
2635        VimAddon,
2636        state::Mode,
2637        test::{NeovimBackedTestContext, VimTestContext},
2638    };
2639    use editor::{Editor, EditorSettings};
2640    use gpui::{Context, TestAppContext};
2641    use indoc::indoc;
2642    use settings::Settings;
2643    use util::path;
2644    use workspace::{OpenOptions, Workspace};
2645
2646    #[gpui::test]
2647    async fn test_command_basics(cx: &mut TestAppContext) {
2648        let mut cx = NeovimBackedTestContext::new(cx).await;
2649
2650        cx.set_shared_state(indoc! {"
2651            ˇa
2652            b
2653            c"})
2654            .await;
2655
2656        cx.simulate_shared_keystrokes(": j enter").await;
2657
2658        // hack: our cursor positioning after a join command is wrong
2659        cx.simulate_shared_keystrokes("^").await;
2660        cx.shared_state().await.assert_eq(indoc! {
2661            "ˇa b
2662            c"
2663        });
2664    }
2665
2666    #[gpui::test]
2667    async fn test_command_goto(cx: &mut TestAppContext) {
2668        let mut cx = NeovimBackedTestContext::new(cx).await;
2669
2670        cx.set_shared_state(indoc! {"
2671            ˇa
2672            b
2673            c"})
2674            .await;
2675        cx.simulate_shared_keystrokes(": 3 enter").await;
2676        cx.shared_state().await.assert_eq(indoc! {"
2677            a
2678            b
2679            ˇc"});
2680    }
2681
2682    #[gpui::test]
2683    async fn test_command_replace(cx: &mut TestAppContext) {
2684        let mut cx = NeovimBackedTestContext::new(cx).await;
2685
2686        cx.set_shared_state(indoc! {"
2687            ˇa
2688            b
2689            b
2690            c"})
2691            .await;
2692        cx.simulate_shared_keystrokes(": % s / b / d enter").await;
2693        cx.shared_state().await.assert_eq(indoc! {"
2694            a
2695            d
2696            ˇd
2697            c"});
2698        cx.simulate_shared_keystrokes(": % s : . : \\ 0 \\ 0 enter")
2699            .await;
2700        cx.shared_state().await.assert_eq(indoc! {"
2701            aa
2702            dd
2703            dd
2704            ˇcc"});
2705        cx.simulate_shared_keystrokes("k : s / d d / e e enter")
2706            .await;
2707        cx.shared_state().await.assert_eq(indoc! {"
2708            aa
2709            dd
2710            ˇee
2711            cc"});
2712    }
2713
2714    #[gpui::test]
2715    async fn test_command_search(cx: &mut TestAppContext) {
2716        let mut cx = NeovimBackedTestContext::new(cx).await;
2717
2718        cx.set_shared_state(indoc! {"
2719                ˇa
2720                b
2721                a
2722                c"})
2723            .await;
2724        cx.simulate_shared_keystrokes(": / b enter").await;
2725        cx.shared_state().await.assert_eq(indoc! {"
2726                a
2727                ˇb
2728                a
2729                c"});
2730        cx.simulate_shared_keystrokes(": ? a enter").await;
2731        cx.shared_state().await.assert_eq(indoc! {"
2732                ˇa
2733                b
2734                a
2735                c"});
2736    }
2737
2738    #[gpui::test]
2739    async fn test_command_write(cx: &mut TestAppContext) {
2740        let mut cx = VimTestContext::new(cx, true).await;
2741        let path = Path::new(path!("/root/dir/file.rs"));
2742        let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
2743
2744        cx.simulate_keystrokes("i @ escape");
2745        cx.simulate_keystrokes(": w enter");
2746
2747        assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "@\n");
2748
2749        fs.as_fake().insert_file(path, b"oops\n".to_vec()).await;
2750
2751        // conflict!
2752        cx.simulate_keystrokes("i @ escape");
2753        cx.simulate_keystrokes(": w enter");
2754        cx.simulate_prompt_answer("Cancel");
2755
2756        assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "oops\n");
2757        assert!(!cx.has_pending_prompt());
2758        cx.simulate_keystrokes(": w !");
2759        cx.simulate_keystrokes("enter");
2760        assert!(!cx.has_pending_prompt());
2761        assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "@@\n");
2762    }
2763
2764    #[gpui::test]
2765    async fn test_command_read(cx: &mut TestAppContext) {
2766        let mut cx = VimTestContext::new(cx, true).await;
2767
2768        let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
2769        let path = Path::new(path!("/root/dir/other.rs"));
2770        fs.as_fake().insert_file(path, "1\n2\n3".into()).await;
2771
2772        cx.workspace(|workspace, _, cx| {
2773            assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
2774        });
2775
2776        // File without trailing newline
2777        cx.set_state("one\ntwo\nthreeˇ", Mode::Normal);
2778        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2779        cx.simulate_keystrokes("enter");
2780        cx.assert_state("one\ntwo\nthree\nˇ1\n2\n3", Mode::Normal);
2781
2782        cx.set_state("oneˇ\ntwo\nthree", Mode::Normal);
2783        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2784        cx.simulate_keystrokes("enter");
2785        cx.assert_state("one\nˇ1\n2\n3\ntwo\nthree", Mode::Normal);
2786
2787        cx.set_state("one\nˇtwo\nthree", Mode::Normal);
2788        cx.simulate_keystrokes(": 0 r space d i r / o t h e r . r s");
2789        cx.simulate_keystrokes("enter");
2790        cx.assert_state("ˇ1\n2\n3\none\ntwo\nthree", Mode::Normal);
2791
2792        cx.set_state("one\n«ˇtwo\nthree\nfour»\nfive", Mode::Visual);
2793        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2794        cx.simulate_keystrokes("enter");
2795        cx.run_until_parked();
2796        cx.assert_state("one\ntwo\nthree\nfour\nˇ1\n2\n3\nfive", Mode::Normal);
2797
2798        // Empty filename
2799        cx.set_state("oneˇ\ntwo\nthree", Mode::Normal);
2800        cx.simulate_keystrokes(": r");
2801        cx.simulate_keystrokes("enter");
2802        cx.assert_state("one\nˇone\ntwo\nthree\ntwo\nthree", Mode::Normal);
2803
2804        // File with trailing newline
2805        fs.as_fake().insert_file(path, "1\n2\n3\n".into()).await;
2806        cx.set_state("one\ntwo\nthreeˇ", Mode::Normal);
2807        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2808        cx.simulate_keystrokes("enter");
2809        cx.assert_state("one\ntwo\nthree\nˇ1\n2\n3\n", Mode::Normal);
2810
2811        cx.set_state("oneˇ\ntwo\nthree", Mode::Normal);
2812        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2813        cx.simulate_keystrokes("enter");
2814        cx.assert_state("one\nˇ1\n2\n3\n\ntwo\nthree", Mode::Normal);
2815
2816        cx.set_state("one\n«ˇtwo\nthree\nfour»\nfive", Mode::Visual);
2817        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2818        cx.simulate_keystrokes("enter");
2819        cx.assert_state("one\ntwo\nthree\nfour\nˇ1\n2\n3\n\nfive", Mode::Normal);
2820
2821        cx.set_state("«one\ntwo\nthreeˇ»", Mode::Visual);
2822        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2823        cx.simulate_keystrokes("enter");
2824        cx.assert_state("one\ntwo\nthree\nˇ1\n2\n3\n", Mode::Normal);
2825
2826        // Empty file
2827        fs.as_fake().insert_file(path, "".into()).await;
2828        cx.set_state("ˇone\ntwo\nthree", Mode::Normal);
2829        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2830        cx.simulate_keystrokes("enter");
2831        cx.assert_state("one\nˇtwo\nthree", Mode::Normal);
2832    }
2833
2834    #[gpui::test]
2835    async fn test_command_quit(cx: &mut TestAppContext) {
2836        let mut cx = VimTestContext::new(cx, true).await;
2837
2838        cx.simulate_keystrokes(": n e w enter");
2839        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
2840        cx.simulate_keystrokes(": q enter");
2841        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 1));
2842        cx.simulate_keystrokes(": n e w enter");
2843        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
2844        cx.simulate_keystrokes(": q a enter");
2845        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 0));
2846    }
2847
2848    #[gpui::test]
2849    async fn test_offsets(cx: &mut TestAppContext) {
2850        let mut cx = NeovimBackedTestContext::new(cx).await;
2851
2852        cx.set_shared_state("ˇ1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n")
2853            .await;
2854
2855        cx.simulate_shared_keystrokes(": + enter").await;
2856        cx.shared_state()
2857            .await
2858            .assert_eq("1\nˇ2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n");
2859
2860        cx.simulate_shared_keystrokes(": 1 0 - enter").await;
2861        cx.shared_state()
2862            .await
2863            .assert_eq("1\n2\n3\n4\n5\n6\n7\n8\nˇ9\n10\n11\n");
2864
2865        cx.simulate_shared_keystrokes(": . - 2 enter").await;
2866        cx.shared_state()
2867            .await
2868            .assert_eq("1\n2\n3\n4\n5\n6\nˇ7\n8\n9\n10\n11\n");
2869
2870        cx.simulate_shared_keystrokes(": % enter").await;
2871        cx.shared_state()
2872            .await
2873            .assert_eq("1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\nˇ");
2874    }
2875
2876    #[gpui::test]
2877    async fn test_command_ranges(cx: &mut TestAppContext) {
2878        let mut cx = NeovimBackedTestContext::new(cx).await;
2879
2880        cx.set_shared_state("ˇ1\n2\n3\n4\n4\n3\n2\n1").await;
2881
2882        cx.simulate_shared_keystrokes(": 2 , 4 d enter").await;
2883        cx.shared_state().await.assert_eq("1\nˇ4\n3\n2\n1");
2884
2885        cx.simulate_shared_keystrokes(": 2 , 4 s o r t enter").await;
2886        cx.shared_state().await.assert_eq("1\nˇ2\n3\n4\n1");
2887
2888        cx.simulate_shared_keystrokes(": 2 , 4 j o i n enter").await;
2889        cx.shared_state().await.assert_eq("1\nˇ2 3 4\n1");
2890    }
2891
2892    #[gpui::test]
2893    async fn test_command_visual_replace(cx: &mut TestAppContext) {
2894        let mut cx = NeovimBackedTestContext::new(cx).await;
2895
2896        cx.set_shared_state("ˇ1\n2\n3\n4\n4\n3\n2\n1").await;
2897
2898        cx.simulate_shared_keystrokes("v 2 j : s / . / k enter")
2899            .await;
2900        cx.shared_state().await.assert_eq("k\nk\nˇk\n4\n4\n3\n2\n1");
2901    }
2902
2903    #[track_caller]
2904    fn assert_active_item(
2905        workspace: &mut Workspace,
2906        expected_path: &str,
2907        expected_text: &str,
2908        cx: &mut Context<Workspace>,
2909    ) {
2910        let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();
2911
2912        let buffer = active_editor
2913            .read(cx)
2914            .buffer()
2915            .read(cx)
2916            .as_singleton()
2917            .unwrap();
2918
2919        let text = buffer.read(cx).text();
2920        let file = buffer.read(cx).file().unwrap();
2921        let file_path = file.as_local().unwrap().abs_path(cx);
2922
2923        assert_eq!(text, expected_text);
2924        assert_eq!(file_path, Path::new(expected_path));
2925    }
2926
2927    #[gpui::test]
2928    async fn test_command_gf(cx: &mut TestAppContext) {
2929        let mut cx = VimTestContext::new(cx, true).await;
2930
2931        // Assert base state, that we're in /root/dir/file.rs
2932        cx.workspace(|workspace, _, cx| {
2933            assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
2934        });
2935
2936        // Insert a new file
2937        let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
2938        fs.as_fake()
2939            .insert_file(
2940                path!("/root/dir/file2.rs"),
2941                "This is file2.rs".as_bytes().to_vec(),
2942            )
2943            .await;
2944        fs.as_fake()
2945            .insert_file(
2946                path!("/root/dir/file3.rs"),
2947                "go to file3".as_bytes().to_vec(),
2948            )
2949            .await;
2950
2951        // Put the path to the second file into the currently open buffer
2952        cx.set_state(indoc! {"go to fiˇle2.rs"}, Mode::Normal);
2953
2954        // Go to file2.rs
2955        cx.simulate_keystrokes("g f");
2956
2957        // We now have two items
2958        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
2959        cx.workspace(|workspace, _, cx| {
2960            assert_active_item(
2961                workspace,
2962                path!("/root/dir/file2.rs"),
2963                "This is file2.rs",
2964                cx,
2965            );
2966        });
2967
2968        // Update editor to point to `file2.rs`
2969        cx.editor =
2970            cx.workspace(|workspace, _, cx| workspace.active_item_as::<Editor>(cx).unwrap());
2971
2972        // Put the path to the third file into the currently open buffer,
2973        // but remove its suffix, because we want that lookup to happen automatically.
2974        cx.set_state(indoc! {"go to fiˇle3"}, Mode::Normal);
2975
2976        // Go to file3.rs
2977        cx.simulate_keystrokes("g f");
2978
2979        // We now have three items
2980        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 3));
2981        cx.workspace(|workspace, _, cx| {
2982            assert_active_item(workspace, path!("/root/dir/file3.rs"), "go to file3", cx);
2983        });
2984    }
2985
2986    #[gpui::test]
2987    async fn test_command_write_filename(cx: &mut TestAppContext) {
2988        let mut cx = VimTestContext::new(cx, true).await;
2989
2990        cx.workspace(|workspace, _, cx| {
2991            assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
2992        });
2993
2994        cx.simulate_keystrokes(": w space other.rs");
2995        cx.simulate_keystrokes("enter");
2996
2997        cx.workspace(|workspace, _, cx| {
2998            assert_active_item(workspace, path!("/root/other.rs"), "", cx);
2999        });
3000
3001        cx.simulate_keystrokes(": w space dir/file.rs");
3002        cx.simulate_keystrokes("enter");
3003
3004        cx.simulate_prompt_answer("Replace");
3005        cx.run_until_parked();
3006
3007        cx.workspace(|workspace, _, cx| {
3008            assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
3009        });
3010
3011        cx.simulate_keystrokes(": w ! space other.rs");
3012        cx.simulate_keystrokes("enter");
3013
3014        cx.workspace(|workspace, _, cx| {
3015            assert_active_item(workspace, path!("/root/other.rs"), "", cx);
3016        });
3017    }
3018
3019    #[gpui::test]
3020    async fn test_command_write_range(cx: &mut TestAppContext) {
3021        let mut cx = VimTestContext::new(cx, true).await;
3022
3023        cx.workspace(|workspace, _, cx| {
3024            assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
3025        });
3026
3027        cx.set_state(
3028            indoc! {"
3029                    The quick
3030                    brown« fox
3031                    jumpsˇ» over
3032                    the lazy dog
3033                "},
3034            Mode::Visual,
3035        );
3036
3037        cx.simulate_keystrokes(": w space dir/other.rs");
3038        cx.simulate_keystrokes("enter");
3039
3040        let other = path!("/root/dir/other.rs");
3041
3042        let _ = cx
3043            .workspace(|workspace, window, cx| {
3044                workspace.open_abs_path(PathBuf::from(other), OpenOptions::default(), window, cx)
3045            })
3046            .await;
3047
3048        cx.workspace(|workspace, _, cx| {
3049            assert_active_item(
3050                workspace,
3051                other,
3052                indoc! {"
3053                        brown fox
3054                        jumps over
3055                    "},
3056                cx,
3057            );
3058        });
3059    }
3060
3061    #[gpui::test]
3062    async fn test_command_matching_lines(cx: &mut TestAppContext) {
3063        let mut cx = NeovimBackedTestContext::new(cx).await;
3064
3065        cx.set_shared_state(indoc! {"
3066            ˇa
3067            b
3068            a
3069            b
3070            a
3071        "})
3072            .await;
3073
3074        cx.simulate_shared_keystrokes(":").await;
3075        cx.simulate_shared_keystrokes("g / a / d").await;
3076        cx.simulate_shared_keystrokes("enter").await;
3077
3078        cx.shared_state().await.assert_eq(indoc! {"
3079            b
3080            b
3081            ˇ"});
3082
3083        cx.simulate_shared_keystrokes("u").await;
3084
3085        cx.shared_state().await.assert_eq(indoc! {"
3086            ˇa
3087            b
3088            a
3089            b
3090            a
3091        "});
3092
3093        cx.simulate_shared_keystrokes(":").await;
3094        cx.simulate_shared_keystrokes("v / a / d").await;
3095        cx.simulate_shared_keystrokes("enter").await;
3096
3097        cx.shared_state().await.assert_eq(indoc! {"
3098            a
3099            a
3100            ˇa"});
3101    }
3102
3103    #[gpui::test]
3104    async fn test_del_marks(cx: &mut TestAppContext) {
3105        let mut cx = NeovimBackedTestContext::new(cx).await;
3106
3107        cx.set_shared_state(indoc! {"
3108            ˇa
3109            b
3110            a
3111            b
3112            a
3113        "})
3114            .await;
3115
3116        cx.simulate_shared_keystrokes("m a").await;
3117
3118        let mark = cx.update_editor(|editor, window, cx| {
3119            let vim = editor.addon::<VimAddon>().unwrap().entity.clone();
3120            vim.update(cx, |vim, cx| vim.get_mark("a", editor, window, cx))
3121        });
3122        assert!(mark.is_some());
3123
3124        cx.simulate_shared_keystrokes(": d e l m space a").await;
3125        cx.simulate_shared_keystrokes("enter").await;
3126
3127        let mark = cx.update_editor(|editor, window, cx| {
3128            let vim = editor.addon::<VimAddon>().unwrap().entity.clone();
3129            vim.update(cx, |vim, cx| vim.get_mark("a", editor, window, cx))
3130        });
3131        assert!(mark.is_none())
3132    }
3133
3134    #[gpui::test]
3135    async fn test_normal_command(cx: &mut TestAppContext) {
3136        let mut cx = NeovimBackedTestContext::new(cx).await;
3137
3138        cx.set_shared_state(indoc! {"
3139            The quick
3140            brown« fox
3141            jumpsˇ» over
3142            the lazy dog
3143        "})
3144            .await;
3145
3146        cx.simulate_shared_keystrokes(": n o r m space w C w o r d")
3147            .await;
3148        cx.simulate_shared_keystrokes("enter").await;
3149
3150        cx.shared_state().await.assert_eq(indoc! {"
3151            The quick
3152            brown word
3153            jumps worˇd
3154            the lazy dog
3155        "});
3156
3157        cx.simulate_shared_keystrokes(": n o r m space _ w c i w t e s t")
3158            .await;
3159        cx.simulate_shared_keystrokes("enter").await;
3160
3161        cx.shared_state().await.assert_eq(indoc! {"
3162            The quick
3163            brown word
3164            jumps tesˇt
3165            the lazy dog
3166        "});
3167
3168        cx.simulate_shared_keystrokes("_ l v l : n o r m space s l a")
3169            .await;
3170        cx.simulate_shared_keystrokes("enter").await;
3171
3172        cx.shared_state().await.assert_eq(indoc! {"
3173            The quick
3174            brown word
3175            lˇaumps test
3176            the lazy dog
3177        "});
3178
3179        cx.set_shared_state(indoc! {"
3180            ˇThe quick
3181            brown fox
3182            jumps over
3183            the lazy dog
3184        "})
3185            .await;
3186
3187        cx.simulate_shared_keystrokes("c i w M y escape").await;
3188
3189        cx.shared_state().await.assert_eq(indoc! {"
3190            Mˇy quick
3191            brown fox
3192            jumps over
3193            the lazy dog
3194        "});
3195
3196        cx.simulate_shared_keystrokes(": n o r m space u").await;
3197        cx.simulate_shared_keystrokes("enter").await;
3198
3199        cx.shared_state().await.assert_eq(indoc! {"
3200            ˇThe quick
3201            brown fox
3202            jumps over
3203            the lazy dog
3204        "});
3205
3206        cx.set_shared_state(indoc! {"
3207            The« quick
3208            brownˇ» fox
3209            jumps over
3210            the lazy dog
3211        "})
3212            .await;
3213
3214        cx.simulate_shared_keystrokes(": n o r m space I 1 2 3")
3215            .await;
3216        cx.simulate_shared_keystrokes("enter").await;
3217        cx.simulate_shared_keystrokes("u").await;
3218
3219        cx.shared_state().await.assert_eq(indoc! {"
3220            ˇThe quick
3221            brown fox
3222            jumps over
3223            the lazy dog
3224        "});
3225
3226        cx.set_shared_state(indoc! {"
3227            ˇquick
3228            brown fox
3229            jumps over
3230            the lazy dog
3231        "})
3232            .await;
3233
3234        cx.simulate_shared_keystrokes(": n o r m space I T h e space")
3235            .await;
3236        cx.simulate_shared_keystrokes("enter").await;
3237
3238        cx.shared_state().await.assert_eq(indoc! {"
3239            Theˇ quick
3240            brown fox
3241            jumps over
3242            the lazy dog
3243        "});
3244
3245        // Once ctrl-v to input character literals is added there should be a test for redo
3246    }
3247
3248    #[gpui::test]
3249    async fn test_command_g_normal(cx: &mut TestAppContext) {
3250        let mut cx = NeovimBackedTestContext::new(cx).await;
3251
3252        cx.set_shared_state(indoc! {"
3253            ˇfoo
3254
3255            foo
3256        "})
3257            .await;
3258
3259        cx.simulate_shared_keystrokes(": % g / f o o / n o r m space A b a r")
3260            .await;
3261        cx.simulate_shared_keystrokes("enter").await;
3262        cx.run_until_parked();
3263
3264        cx.shared_state().await.assert_eq(indoc! {"
3265            foobar
3266
3267            foobaˇr
3268        "});
3269
3270        cx.simulate_shared_keystrokes("u").await;
3271
3272        cx.shared_state().await.assert_eq(indoc! {"
3273            foˇo
3274
3275            foo
3276        "});
3277    }
3278
3279    #[gpui::test]
3280    async fn test_command_tabnew(cx: &mut TestAppContext) {
3281        let mut cx = VimTestContext::new(cx, true).await;
3282
3283        // Create a new file to ensure that, when the filename is used with
3284        // `:tabnew`, it opens the existing file in a new tab.
3285        let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
3286        fs.as_fake()
3287            .insert_file(path!("/root/dir/file_2.rs"), "file_2".as_bytes().to_vec())
3288            .await;
3289
3290        cx.simulate_keystrokes(": tabnew");
3291        cx.simulate_keystrokes("enter");
3292        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
3293
3294        // Assert that the new tab is empty and not associated with any file, as
3295        // no file path was provided to the `:tabnew` command.
3296        cx.workspace(|workspace, _window, cx| {
3297            let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();
3298            let buffer = active_editor
3299                .read(cx)
3300                .buffer()
3301                .read(cx)
3302                .as_singleton()
3303                .unwrap();
3304
3305            assert!(&buffer.read(cx).file().is_none());
3306        });
3307
3308        // Leverage the filename as an argument to the `:tabnew` command,
3309        // ensuring that the file, instead of an empty buffer, is opened in a
3310        // new tab.
3311        cx.simulate_keystrokes(": tabnew space dir/file_2.rs");
3312        cx.simulate_keystrokes("enter");
3313
3314        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 3));
3315        cx.workspace(|workspace, _, cx| {
3316            assert_active_item(workspace, path!("/root/dir/file_2.rs"), "file_2", cx);
3317        });
3318
3319        // If the `filename` argument provided to the `:tabnew` command is for a
3320        // file that doesn't yet exist, it should still associate the buffer
3321        // with that file path, so that when the buffer contents are saved, the
3322        // file is created.
3323        cx.simulate_keystrokes(": tabnew space dir/file_3.rs");
3324        cx.simulate_keystrokes("enter");
3325
3326        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 4));
3327        cx.workspace(|workspace, _, cx| {
3328            assert_active_item(workspace, path!("/root/dir/file_3.rs"), "", cx);
3329        });
3330    }
3331
3332    #[gpui::test]
3333    async fn test_command_tabedit(cx: &mut TestAppContext) {
3334        let mut cx = VimTestContext::new(cx, true).await;
3335
3336        // Create a new file to ensure that, when the filename is used with
3337        // `:tabedit`, it opens the existing file in a new tab.
3338        let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
3339        fs.as_fake()
3340            .insert_file(path!("/root/dir/file_2.rs"), "file_2".as_bytes().to_vec())
3341            .await;
3342
3343        cx.simulate_keystrokes(": tabedit");
3344        cx.simulate_keystrokes("enter");
3345        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
3346
3347        // Assert that the new tab is empty and not associated with any file, as
3348        // no file path was provided to the `:tabedit` command.
3349        cx.workspace(|workspace, _window, cx| {
3350            let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();
3351            let buffer = active_editor
3352                .read(cx)
3353                .buffer()
3354                .read(cx)
3355                .as_singleton()
3356                .unwrap();
3357
3358            assert!(&buffer.read(cx).file().is_none());
3359        });
3360
3361        // Leverage the filename as an argument to the `:tabedit` command,
3362        // ensuring that the file, instead of an empty buffer, is opened in a
3363        // new tab.
3364        cx.simulate_keystrokes(": tabedit space dir/file_2.rs");
3365        cx.simulate_keystrokes("enter");
3366
3367        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 3));
3368        cx.workspace(|workspace, _, cx| {
3369            assert_active_item(workspace, path!("/root/dir/file_2.rs"), "file_2", cx);
3370        });
3371
3372        // If the `filename` argument provided to the `:tabedit` command is for a
3373        // file that doesn't yet exist, it should still associate the buffer
3374        // with that file path, so that when the buffer contents are saved, the
3375        // file is created.
3376        cx.simulate_keystrokes(": tabedit space dir/file_3.rs");
3377        cx.simulate_keystrokes("enter");
3378
3379        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 4));
3380        cx.workspace(|workspace, _, cx| {
3381            assert_active_item(workspace, path!("/root/dir/file_3.rs"), "", cx);
3382        });
3383    }
3384
3385    #[gpui::test]
3386    async fn test_ignorecase_command(cx: &mut TestAppContext) {
3387        let mut cx = VimTestContext::new(cx, true).await;
3388        cx.read(|cx| {
3389            assert_eq!(
3390                EditorSettings::get_global(cx).search.case_sensitive,
3391                false,
3392                "The `case_sensitive` setting should be `false` by default."
3393            );
3394        });
3395        cx.simulate_keystrokes(": set space noignorecase");
3396        cx.simulate_keystrokes("enter");
3397        cx.read(|cx| {
3398            assert_eq!(
3399                EditorSettings::get_global(cx).search.case_sensitive,
3400                true,
3401                "The `case_sensitive` setting should have been enabled with `:set noignorecase`."
3402            );
3403        });
3404        cx.simulate_keystrokes(": set space ignorecase");
3405        cx.simulate_keystrokes("enter");
3406        cx.read(|cx| {
3407            assert_eq!(
3408                EditorSettings::get_global(cx).search.case_sensitive,
3409                false,
3410                "The `case_sensitive` setting should have been disabled with `:set ignorecase`."
3411            );
3412        });
3413        cx.simulate_keystrokes(": set space noic");
3414        cx.simulate_keystrokes("enter");
3415        cx.read(|cx| {
3416            assert_eq!(
3417                EditorSettings::get_global(cx).search.case_sensitive,
3418                true,
3419                "The `case_sensitive` setting should have been enabled with `:set noic`."
3420            );
3421        });
3422        cx.simulate_keystrokes(": set space ic");
3423        cx.simulate_keystrokes("enter");
3424        cx.read(|cx| {
3425            assert_eq!(
3426                EditorSettings::get_global(cx).search.case_sensitive,
3427                false,
3428                "The `case_sensitive` setting should have been disabled with `:set ic`."
3429            );
3430        });
3431    }
3432
3433    #[gpui::test]
3434    async fn test_sort_commands(cx: &mut TestAppContext) {
3435        let mut cx = VimTestContext::new(cx, true).await;
3436
3437        cx.set_state(
3438            indoc! {"
3439                «hornet
3440                quirrel
3441                elderbug
3442                cornifer
3443                idaˇ»
3444            "},
3445            Mode::Visual,
3446        );
3447
3448        cx.simulate_keystrokes(": sort");
3449        cx.simulate_keystrokes("enter");
3450
3451        cx.assert_state(
3452            indoc! {"
3453                ˇcornifer
3454                elderbug
3455                hornet
3456                ida
3457                quirrel
3458            "},
3459            Mode::Normal,
3460        );
3461
3462        // Assert that, by default, `:sort` takes case into consideration.
3463        cx.set_state(
3464            indoc! {"
3465                «hornet
3466                quirrel
3467                Elderbug
3468                cornifer
3469                idaˇ»
3470            "},
3471            Mode::Visual,
3472        );
3473
3474        cx.simulate_keystrokes(": sort");
3475        cx.simulate_keystrokes("enter");
3476
3477        cx.assert_state(
3478            indoc! {"
3479                ˇElderbug
3480                cornifer
3481                hornet
3482                ida
3483                quirrel
3484            "},
3485            Mode::Normal,
3486        );
3487
3488        // Assert that, if the `i` option is passed, `:sort` ignores case.
3489        cx.set_state(
3490            indoc! {"
3491                «hornet
3492                quirrel
3493                Elderbug
3494                cornifer
3495                idaˇ»
3496            "},
3497            Mode::Visual,
3498        );
3499
3500        cx.simulate_keystrokes(": sort space i");
3501        cx.simulate_keystrokes("enter");
3502
3503        cx.assert_state(
3504            indoc! {"
3505                ˇcornifer
3506                Elderbug
3507                hornet
3508                ida
3509                quirrel
3510            "},
3511            Mode::Normal,
3512        );
3513
3514        // When no range is provided, sorts the whole buffer.
3515        cx.set_state(
3516            indoc! {"
3517                ˇhornet
3518                quirrel
3519                elderbug
3520                cornifer
3521                ida
3522            "},
3523            Mode::Normal,
3524        );
3525
3526        cx.simulate_keystrokes(": sort");
3527        cx.simulate_keystrokes("enter");
3528
3529        cx.assert_state(
3530            indoc! {"
3531                ˇcornifer
3532                elderbug
3533                hornet
3534                ida
3535                quirrel
3536            "},
3537            Mode::Normal,
3538        );
3539    }
3540}