command.rs

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