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