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).range(select_range),
1730        VimCommand::new(("fo", "ld"), editor::actions::FoldSelectedRanges).range(act_on_range),
1731        VimCommand::new(("foldo", "pen"), editor::actions::UnfoldLines)
1732            .bang(editor::actions::UnfoldRecursive)
1733            .range(act_on_range),
1734        VimCommand::new(("foldc", "lose"), editor::actions::Fold)
1735            .bang(editor::actions::FoldRecursive)
1736            .range(act_on_range),
1737        VimCommand::new(("dif", "fupdate"), editor::actions::ToggleSelectedDiffHunks)
1738            .range(act_on_range),
1739        VimCommand::str(("rev", "ert"), "git::Restore").range(act_on_range),
1740        VimCommand::new(("d", "elete"), VisualDeleteLine).range(select_range),
1741        VimCommand::new(("y", "ank"), gpui::NoAction).range(|_, range| {
1742            Some(
1743                YankCommand {
1744                    range: range.clone(),
1745                }
1746                .boxed_clone(),
1747            )
1748        }),
1749        VimCommand::new(("reg", "isters"), ToggleRegistersView).bang(ToggleRegistersView),
1750        VimCommand::new(("di", "splay"), ToggleRegistersView).bang(ToggleRegistersView),
1751        VimCommand::new(("marks", ""), ToggleMarksView).bang(ToggleMarksView),
1752        VimCommand::new(("delm", "arks"), ArgumentRequired)
1753            .bang(DeleteMarks::AllLocal)
1754            .args(|_, args| Some(DeleteMarks::Marks(args).boxed_clone())),
1755        VimCommand::new(("sor", "t"), SortLinesCaseSensitive)
1756            .range(select_range)
1757            .default_range(CommandRange::buffer()),
1758        VimCommand::new(("sort i", ""), SortLinesCaseInsensitive)
1759            .range(select_range)
1760            .default_range(CommandRange::buffer()),
1761        VimCommand::str(("E", "xplore"), "project_panel::ToggleFocus"),
1762        VimCommand::str(("H", "explore"), "project_panel::ToggleFocus"),
1763        VimCommand::str(("L", "explore"), "project_panel::ToggleFocus"),
1764        VimCommand::str(("S", "explore"), "project_panel::ToggleFocus"),
1765        VimCommand::str(("Ve", "xplore"), "project_panel::ToggleFocus"),
1766        VimCommand::str(("te", "rm"), "terminal_panel::Toggle"),
1767        VimCommand::str(("T", "erm"), "terminal_panel::Toggle"),
1768        VimCommand::str(("C", "ollab"), "collab_panel::ToggleFocus"),
1769        VimCommand::str(("No", "tifications"), "notification_panel::ToggleFocus"),
1770        VimCommand::str(("A", "I"), "agent::ToggleFocus"),
1771        VimCommand::str(("G", "it"), "git_panel::ToggleFocus"),
1772        VimCommand::str(("D", "ebug"), "debug_panel::ToggleFocus"),
1773        VimCommand::new(("noh", "lsearch"), search::buffer_search::Dismiss),
1774        VimCommand::new(("$", ""), EndOfDocument),
1775        VimCommand::new(("%", ""), EndOfDocument),
1776        VimCommand::new(("0", ""), StartOfDocument),
1777        VimCommand::new(("ex", ""), editor::actions::ReloadFile).bang(editor::actions::ReloadFile),
1778        VimCommand::new(("cpp", "link"), editor::actions::CopyPermalinkToLine).range(act_on_range),
1779        VimCommand::str(("opt", "ions"), "zed::OpenDefaultSettings"),
1780        VimCommand::str(("map", ""), "vim::OpenDefaultKeymap"),
1781        VimCommand::new(("h", "elp"), OpenDocs),
1782    ]
1783}
1784
1785struct VimCommands(Vec<VimCommand>);
1786// safety: we only ever access this from the main thread (as ensured by the cx argument)
1787// actions are not Sync so we can't otherwise use a OnceLock.
1788unsafe impl Sync for VimCommands {}
1789impl Global for VimCommands {}
1790
1791fn commands(cx: &App) -> &Vec<VimCommand> {
1792    static COMMANDS: OnceLock<VimCommands> = OnceLock::new();
1793    &COMMANDS
1794        .get_or_init(|| VimCommands(generate_commands(cx)))
1795        .0
1796}
1797
1798fn act_on_range(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
1799    Some(
1800        WithRange {
1801            restore_selection: true,
1802            range: range.clone(),
1803            action: WrappedAction(action),
1804        }
1805        .boxed_clone(),
1806    )
1807}
1808
1809fn select_range(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
1810    Some(
1811        WithRange {
1812            restore_selection: false,
1813            range: range.clone(),
1814            action: WrappedAction(action),
1815        }
1816        .boxed_clone(),
1817    )
1818}
1819
1820fn wrap_count(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
1821    range.as_count().map(|count| {
1822        WithCount {
1823            count,
1824            action: WrappedAction(action),
1825        }
1826        .boxed_clone()
1827    })
1828}
1829
1830pub fn command_interceptor(
1831    mut input: &str,
1832    workspace: WeakEntity<Workspace>,
1833    cx: &mut App,
1834) -> Task<CommandInterceptResult> {
1835    while input.starts_with(':') {
1836        input = &input[1..];
1837    }
1838
1839    let (range, query) = VimCommand::parse_range(input);
1840    let range_prefix = input[0..(input.len() - query.len())].to_string();
1841    let has_trailing_space = query.ends_with(" ");
1842    let mut query = query.as_str().trim_start();
1843
1844    let on_matching_lines = (query.starts_with('g') || query.starts_with('v'))
1845        .then(|| {
1846            let (pattern, range, search, invert) = OnMatchingLines::parse(query, &range)?;
1847            let start_idx = query.len() - pattern.len();
1848            query = query[start_idx..].trim();
1849            Some((range, search, invert))
1850        })
1851        .flatten();
1852
1853    let mut action = if range.is_some() && query.is_empty() {
1854        Some(
1855            GoToLine {
1856                range: range.clone().unwrap(),
1857            }
1858            .boxed_clone(),
1859        )
1860    } else if query.starts_with('/') || query.starts_with('?') {
1861        Some(
1862            FindCommand {
1863                query: query[1..].to_string(),
1864                backwards: query.starts_with('?'),
1865            }
1866            .boxed_clone(),
1867        )
1868    } else if query.starts_with("se ") || query.starts_with("set ") {
1869        let (prefix, option) = query.split_once(' ').unwrap();
1870        let mut commands = VimOption::possible_commands(option);
1871        if !commands.is_empty() {
1872            let query = prefix.to_string() + " " + option;
1873            for command in &mut commands {
1874                command.positions = generate_positions(&command.string, &query);
1875            }
1876        }
1877        return Task::ready(CommandInterceptResult {
1878            results: commands,
1879            exclusive: false,
1880        });
1881    } else if query.starts_with('s') {
1882        let mut substitute = "substitute".chars().peekable();
1883        let mut query = query.chars().peekable();
1884        while substitute
1885            .peek()
1886            .is_some_and(|char| Some(char) == query.peek())
1887        {
1888            substitute.next();
1889            query.next();
1890        }
1891        if let Some(replacement) = Replacement::parse(query) {
1892            let range = range.clone().unwrap_or(CommandRange {
1893                start: Position::CurrentLine { offset: 0 },
1894                end: None,
1895            });
1896            Some(ReplaceCommand { replacement, range }.boxed_clone())
1897        } else {
1898            None
1899        }
1900    } else if query.contains('!') {
1901        ShellExec::parse(query, range.clone())
1902    } else if on_matching_lines.is_some() {
1903        commands(cx)
1904            .iter()
1905            .find_map(|command| command.parse(query, &None, cx))
1906    } else {
1907        None
1908    };
1909
1910    if let Some((range, search, invert)) = on_matching_lines
1911        && let Some(ref inner) = action
1912    {
1913        action = Some(Box::new(OnMatchingLines {
1914            range,
1915            search,
1916            action: WrappedAction(inner.boxed_clone()),
1917            invert,
1918        }));
1919    };
1920
1921    if let Some(action) = action {
1922        let string = input.to_string();
1923        let positions = generate_positions(&string, &(range_prefix + query));
1924        return Task::ready(CommandInterceptResult {
1925            results: vec![CommandInterceptItem {
1926                action,
1927                string,
1928                positions,
1929            }],
1930            exclusive: false,
1931        });
1932    }
1933
1934    let Some((mut results, filenames)) =
1935        commands(cx).iter().enumerate().find_map(|(idx, command)| {
1936            let action = command.parse(query, &range, cx)?;
1937            let parsed_query = command.get_parsed_query(query.into())?;
1938            let display_string = ":".to_owned()
1939                + &range_prefix
1940                + command.prefix
1941                + command.suffix
1942                + if parsed_query.has_bang { "!" } else { "" };
1943            let space = if parsed_query.has_space { " " } else { "" };
1944
1945            let string = format!("{}{}{}", &display_string, &space, &parsed_query.args);
1946            let positions = generate_positions(&string, &(range_prefix.clone() + query));
1947
1948            let results = vec![CommandInterceptItem {
1949                action,
1950                string,
1951                positions,
1952            }];
1953
1954            let no_args_positions =
1955                generate_positions(&display_string, &(range_prefix.clone() + query));
1956
1957            // The following are valid autocomplete scenarios:
1958            // :w!filename.txt
1959            // :w filename.txt
1960            // :w[space]
1961            if !command.has_filename
1962                || (!has_trailing_space && !parsed_query.has_bang && parsed_query.args.is_empty())
1963            {
1964                return Some((results, None));
1965            }
1966
1967            Some((
1968                results,
1969                Some((idx, parsed_query, display_string, no_args_positions)),
1970            ))
1971        })
1972    else {
1973        return Task::ready(CommandInterceptResult::default());
1974    };
1975
1976    if let Some((cmd_idx, parsed_query, display_string, no_args_positions)) = filenames {
1977        let filenames = VimCommand::generate_filename_completions(&parsed_query, workspace, cx);
1978        cx.spawn(async move |cx| {
1979            let filenames = filenames.await;
1980            const MAX_RESULTS: usize = 100;
1981            let executor = cx.background_executor().clone();
1982            let mut candidates = Vec::with_capacity(filenames.len());
1983
1984            for (idx, filename) in filenames.iter().enumerate() {
1985                candidates.push(fuzzy::StringMatchCandidate::new(idx, &filename));
1986            }
1987            let filenames = fuzzy::match_strings(
1988                &candidates,
1989                &parsed_query.args,
1990                false,
1991                true,
1992                MAX_RESULTS,
1993                &Default::default(),
1994                executor,
1995            )
1996            .await;
1997
1998            for fuzzy::StringMatch {
1999                candidate_id: _,
2000                score: _,
2001                positions,
2002                string,
2003            } in filenames
2004            {
2005                let offset = display_string.len() + 1;
2006                let mut positions: Vec<_> = positions.iter().map(|&pos| pos + offset).collect();
2007                positions.splice(0..0, no_args_positions.clone());
2008                let string = format!("{display_string} {string}");
2009                let (range, query) = VimCommand::parse_range(&string[1..]);
2010                let action =
2011                    match cx.update(|cx| commands(cx).get(cmd_idx)?.parse(&query, &range, cx)) {
2012                        Some(action) => action,
2013                        _ => continue,
2014                    };
2015                results.push(CommandInterceptItem {
2016                    action,
2017                    string,
2018                    positions,
2019                });
2020            }
2021            CommandInterceptResult {
2022                results,
2023                exclusive: true,
2024            }
2025        })
2026    } else {
2027        Task::ready(CommandInterceptResult {
2028            results,
2029            exclusive: false,
2030        })
2031    }
2032}
2033
2034fn generate_positions(string: &str, query: &str) -> Vec<usize> {
2035    let mut positions = Vec::new();
2036    let mut chars = query.chars();
2037
2038    let Some(mut current) = chars.next() else {
2039        return positions;
2040    };
2041
2042    for (i, c) in string.char_indices() {
2043        if c == current {
2044            positions.push(i);
2045            if let Some(c) = chars.next() {
2046                current = c;
2047            } else {
2048                break;
2049            }
2050        }
2051    }
2052
2053    positions
2054}
2055
2056/// Applies a command to all lines matching a pattern.
2057#[derive(Debug, PartialEq, Clone, Action)]
2058#[action(namespace = vim, no_json, no_register)]
2059pub(crate) struct OnMatchingLines {
2060    range: CommandRange,
2061    search: String,
2062    action: WrappedAction,
2063    invert: bool,
2064}
2065
2066impl OnMatchingLines {
2067    // convert a vim query into something more usable by zed.
2068    // we don't attempt to fully convert between the two regex syntaxes,
2069    // but we do flip \( and \) to ( and ) (and vice-versa) in the pattern,
2070    // and convert \0..\9 to $0..$9 in the replacement so that common idioms work.
2071    pub(crate) fn parse(
2072        query: &str,
2073        range: &Option<CommandRange>,
2074    ) -> Option<(String, CommandRange, String, bool)> {
2075        let mut global = "global".chars().peekable();
2076        let mut query_chars = query.chars().peekable();
2077        let mut invert = false;
2078        if query_chars.peek() == Some(&'v') {
2079            invert = true;
2080            query_chars.next();
2081        }
2082        while global
2083            .peek()
2084            .is_some_and(|char| Some(char) == query_chars.peek())
2085        {
2086            global.next();
2087            query_chars.next();
2088        }
2089        if !invert && query_chars.peek() == Some(&'!') {
2090            invert = true;
2091            query_chars.next();
2092        }
2093        let range = range.clone().unwrap_or(CommandRange {
2094            start: Position::Line { row: 0, offset: 0 },
2095            end: Some(Position::LastLine { offset: 0 }),
2096        });
2097
2098        let delimiter = query_chars.next().filter(|c| {
2099            !c.is_alphanumeric() && *c != '"' && *c != '|' && *c != '\'' && *c != '!'
2100        })?;
2101
2102        let mut search = String::new();
2103        let mut escaped = false;
2104
2105        for c in query_chars.by_ref() {
2106            if escaped {
2107                escaped = false;
2108                // unescape escaped parens
2109                if c != '(' && c != ')' && c != delimiter {
2110                    search.push('\\')
2111                }
2112                search.push(c)
2113            } else if c == '\\' {
2114                escaped = true;
2115            } else if c == delimiter {
2116                break;
2117            } else {
2118                // escape unescaped parens
2119                if c == '(' || c == ')' {
2120                    search.push('\\')
2121                }
2122                search.push(c)
2123            }
2124        }
2125
2126        Some((query_chars.collect::<String>(), range, search, invert))
2127    }
2128
2129    pub fn run(&self, vim: &mut Vim, window: &mut Window, cx: &mut Context<Vim>) {
2130        let result = vim.update_editor(cx, |vim, editor, cx| {
2131            self.range.buffer_range(vim, editor, window, cx)
2132        });
2133
2134        let range = match result {
2135            None => return,
2136            Some(e @ Err(_)) => {
2137                let Some(workspace) = vim.workspace(window, cx) else {
2138                    return;
2139                };
2140                workspace.update(cx, |workspace, cx| {
2141                    e.notify_err(workspace, cx);
2142                });
2143                return;
2144            }
2145            Some(Ok(result)) => result,
2146        };
2147
2148        let mut action = self.action.boxed_clone();
2149        let mut last_pattern = self.search.clone();
2150
2151        let mut regexes = match Regex::new(&self.search) {
2152            Ok(regex) => vec![(regex, !self.invert)],
2153            e @ Err(_) => {
2154                let Some(workspace) = vim.workspace(window, cx) else {
2155                    return;
2156                };
2157                workspace.update(cx, |workspace, cx| {
2158                    e.notify_err(workspace, cx);
2159                });
2160                return;
2161            }
2162        };
2163        while let Some(inner) = action
2164            .boxed_clone()
2165            .as_any()
2166            .downcast_ref::<OnMatchingLines>()
2167        {
2168            let Some(regex) = Regex::new(&inner.search).ok() else {
2169                break;
2170            };
2171            last_pattern = inner.search.clone();
2172            action = inner.action.boxed_clone();
2173            regexes.push((regex, !inner.invert))
2174        }
2175
2176        if let Some(pane) = vim.pane(window, cx) {
2177            pane.update(cx, |pane, cx| {
2178                if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()
2179                {
2180                    search_bar.update(cx, |search_bar, cx| {
2181                        if search_bar.show(window, cx) {
2182                            let _ = search_bar.search(
2183                                &last_pattern,
2184                                Some(SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE),
2185                                false,
2186                                window,
2187                                cx,
2188                            );
2189                        }
2190                    });
2191                }
2192            });
2193        };
2194
2195        vim.update_editor(cx, |_, editor, cx| {
2196            let snapshot = editor.snapshot(window, cx);
2197            let mut row = range.start.0;
2198
2199            let point_range = Point::new(range.start.0, 0)
2200                ..snapshot
2201                    .buffer_snapshot()
2202                    .clip_point(Point::new(range.end.0 + 1, 0), Bias::Left);
2203            cx.spawn_in(window, async move |editor, cx| {
2204                let new_selections = cx
2205                    .background_spawn(async move {
2206                        let mut line = String::new();
2207                        let mut new_selections = Vec::new();
2208                        let chunks = snapshot
2209                            .buffer_snapshot()
2210                            .text_for_range(point_range)
2211                            .chain(["\n"]);
2212
2213                        for chunk in chunks {
2214                            for (newline_ix, text) in chunk.split('\n').enumerate() {
2215                                if newline_ix > 0 {
2216                                    if regexes.iter().all(|(regex, should_match)| {
2217                                        regex.is_match(&line) == *should_match
2218                                    }) {
2219                                        new_selections
2220                                            .push(Point::new(row, 0).to_display_point(&snapshot))
2221                                    }
2222                                    row += 1;
2223                                    line.clear();
2224                                }
2225                                line.push_str(text)
2226                            }
2227                        }
2228
2229                        new_selections
2230                    })
2231                    .await;
2232
2233                if new_selections.is_empty() {
2234                    return;
2235                }
2236
2237                if let Some(vim_norm) = action.as_any().downcast_ref::<VimNorm>() {
2238                    let mut vim_norm = vim_norm.clone();
2239                    vim_norm.override_rows =
2240                        Some(new_selections.iter().map(|point| point.row().0).collect());
2241                    editor
2242                        .update_in(cx, |_, window, cx| {
2243                            window.dispatch_action(vim_norm.boxed_clone(), cx);
2244                        })
2245                        .log_err();
2246                    return;
2247                }
2248
2249                editor
2250                    .update_in(cx, |editor, window, cx| {
2251                        editor.start_transaction_at(Instant::now(), window, cx);
2252                        editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2253                            s.replace_cursors_with(|_| new_selections);
2254                        });
2255                        window.dispatch_action(action, cx);
2256
2257                        cx.defer_in(window, move |editor, window, cx| {
2258                            let newest = editor
2259                                .selections
2260                                .newest::<Point>(&editor.display_snapshot(cx));
2261                            editor.change_selections(
2262                                SelectionEffects::no_scroll(),
2263                                window,
2264                                cx,
2265                                |s| {
2266                                    s.select(vec![newest]);
2267                                },
2268                            );
2269                            editor.end_transaction_at(Instant::now(), cx);
2270                        })
2271                    })
2272                    .log_err();
2273            })
2274            .detach();
2275        });
2276    }
2277}
2278
2279/// Executes a shell command and returns the output.
2280#[derive(Clone, Debug, PartialEq, Action)]
2281#[action(namespace = vim, no_json, no_register)]
2282pub struct ShellExec {
2283    command: String,
2284    range: Option<CommandRange>,
2285    is_read: bool,
2286}
2287
2288impl Vim {
2289    pub fn cancel_running_command(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2290        if self.running_command.take().is_some() {
2291            self.update_editor(cx, |_, editor, cx| {
2292                editor.transact(window, cx, |editor, _window, _cx| {
2293                    editor.clear_row_highlights::<ShellExec>();
2294                })
2295            });
2296        }
2297    }
2298
2299    fn prepare_shell_command(
2300        &mut self,
2301        command: &str,
2302        _: &mut Window,
2303        cx: &mut Context<Self>,
2304    ) -> String {
2305        let mut ret = String::new();
2306        // N.B. non-standard escaping rules:
2307        // * !echo % => "echo README.md"
2308        // * !echo \% => "echo %"
2309        // * !echo \\% => echo \%
2310        // * !echo \\\% => echo \\%
2311        for c in command.chars() {
2312            if c != '%' && c != '!' {
2313                ret.push(c);
2314                continue;
2315            } else if ret.chars().last() == Some('\\') {
2316                ret.pop();
2317                ret.push(c);
2318                continue;
2319            }
2320            match c {
2321                '%' => {
2322                    self.update_editor(cx, |_, editor, cx| {
2323                        if let Some((_, buffer, _)) = editor.active_excerpt(cx)
2324                            && let Some(file) = buffer.read(cx).file()
2325                            && let Some(local) = file.as_local()
2326                        {
2327                            ret.push_str(&local.path().display(local.path_style(cx)));
2328                        }
2329                    });
2330                }
2331                '!' => {
2332                    if let Some(command) = &self.last_command {
2333                        ret.push_str(command)
2334                    }
2335                }
2336                _ => {}
2337            }
2338        }
2339        self.last_command = Some(ret.clone());
2340        ret
2341    }
2342
2343    pub fn shell_command_motion(
2344        &mut self,
2345        motion: Motion,
2346        times: Option<usize>,
2347        forced_motion: bool,
2348        window: &mut Window,
2349        cx: &mut Context<Vim>,
2350    ) {
2351        self.stop_recording(cx);
2352        let Some(workspace) = self.workspace(window, cx) else {
2353            return;
2354        };
2355        let command = self.update_editor(cx, |_, editor, cx| {
2356            let snapshot = editor.snapshot(window, cx);
2357            let start = editor
2358                .selections
2359                .newest_display(&editor.display_snapshot(cx));
2360            let text_layout_details = editor.text_layout_details(window, cx);
2361            let (mut range, _) = motion
2362                .range(
2363                    &snapshot,
2364                    start.clone(),
2365                    times,
2366                    &text_layout_details,
2367                    forced_motion,
2368                )
2369                .unwrap_or((start.range(), MotionKind::Exclusive));
2370            if range.start != start.start {
2371                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2372                    s.select_ranges([
2373                        range.start.to_point(&snapshot)..range.start.to_point(&snapshot)
2374                    ]);
2375                })
2376            }
2377            if range.end.row() > range.start.row() && range.end.column() != 0 {
2378                *range.end.row_mut() -= 1
2379            }
2380            if range.end.row() == range.start.row() {
2381                ".!".to_string()
2382            } else {
2383                format!(".,.+{}!", (range.end.row() - range.start.row()).0)
2384            }
2385        });
2386        if let Some(command) = command {
2387            workspace.update(cx, |workspace, cx| {
2388                command_palette::CommandPalette::toggle(workspace, &command, window, cx);
2389            });
2390        }
2391    }
2392
2393    pub fn shell_command_object(
2394        &mut self,
2395        object: Object,
2396        around: bool,
2397        window: &mut Window,
2398        cx: &mut Context<Vim>,
2399    ) {
2400        self.stop_recording(cx);
2401        let Some(workspace) = self.workspace(window, cx) else {
2402            return;
2403        };
2404        let command = self.update_editor(cx, |_, editor, cx| {
2405            let snapshot = editor.snapshot(window, cx);
2406            let start = editor
2407                .selections
2408                .newest_display(&editor.display_snapshot(cx));
2409            let range = object
2410                .range(&snapshot, start.clone(), around, None)
2411                .unwrap_or(start.range());
2412            if range.start != start.start {
2413                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2414                    s.select_ranges([
2415                        range.start.to_point(&snapshot)..range.start.to_point(&snapshot)
2416                    ]);
2417                })
2418            }
2419            if range.end.row() == range.start.row() {
2420                ".!".to_string()
2421            } else {
2422                format!(".,.+{}!", (range.end.row() - range.start.row()).0)
2423            }
2424        });
2425        if let Some(command) = command {
2426            workspace.update(cx, |workspace, cx| {
2427                command_palette::CommandPalette::toggle(workspace, &command, window, cx);
2428            });
2429        }
2430    }
2431}
2432
2433impl ShellExec {
2434    pub fn parse(query: &str, range: Option<CommandRange>) -> Option<Box<dyn Action>> {
2435        let (before, after) = query.split_once('!')?;
2436        let before = before.trim();
2437
2438        if !"read".starts_with(before) {
2439            return None;
2440        }
2441
2442        Some(
2443            ShellExec {
2444                command: after.trim().to_string(),
2445                range,
2446                is_read: !before.is_empty(),
2447            }
2448            .boxed_clone(),
2449        )
2450    }
2451
2452    pub fn run(&self, vim: &mut Vim, window: &mut Window, cx: &mut Context<Vim>) {
2453        let Some(workspace) = vim.workspace(window, cx) else {
2454            return;
2455        };
2456
2457        let project = workspace.read(cx).project().clone();
2458        let command = vim.prepare_shell_command(&self.command, window, cx);
2459
2460        if self.range.is_none() && !self.is_read {
2461            workspace.update(cx, |workspace, cx| {
2462                let project = workspace.project().read(cx);
2463                let cwd = project.first_project_directory(cx);
2464                let shell = project.terminal_settings(&cwd, cx).shell.clone();
2465
2466                let spawn_in_terminal = SpawnInTerminal {
2467                    id: TaskId("vim".to_string()),
2468                    full_label: command.clone(),
2469                    label: command.clone(),
2470                    command: Some(command.clone()),
2471                    args: Vec::new(),
2472                    command_label: command.clone(),
2473                    cwd,
2474                    env: HashMap::default(),
2475                    use_new_terminal: true,
2476                    allow_concurrent_runs: true,
2477                    reveal: RevealStrategy::NoFocus,
2478                    reveal_target: RevealTarget::Dock,
2479                    hide: HideStrategy::Never,
2480                    shell,
2481                    show_summary: false,
2482                    show_command: false,
2483                    show_rerun: false,
2484                    save: SaveStrategy::default(),
2485                };
2486
2487                let task_status = workspace.spawn_in_terminal(spawn_in_terminal, window, cx);
2488                cx.background_spawn(async move {
2489                    match task_status.await {
2490                        Some(Ok(status)) => {
2491                            if status.success() {
2492                                log::debug!("Vim shell exec succeeded");
2493                            } else {
2494                                log::debug!("Vim shell exec failed, code: {:?}", status.code());
2495                            }
2496                        }
2497                        Some(Err(e)) => log::error!("Vim shell exec failed: {e}"),
2498                        None => log::debug!("Vim shell exec got cancelled"),
2499                    }
2500                })
2501                .detach();
2502            });
2503            return;
2504        };
2505
2506        let mut input_snapshot = None;
2507        let mut input_range = None;
2508        let mut needs_newline_prefix = false;
2509        vim.update_editor(cx, |vim, editor, cx| {
2510            let snapshot = editor.buffer().read(cx).snapshot(cx);
2511            let range = if let Some(range) = self.range.clone() {
2512                let Some(range) = range.buffer_range(vim, editor, window, cx).log_err() else {
2513                    return;
2514                };
2515                Point::new(range.start.0, 0)
2516                    ..snapshot.clip_point(Point::new(range.end.0 + 1, 0), Bias::Right)
2517            } else {
2518                let mut end = editor
2519                    .selections
2520                    .newest::<Point>(&editor.display_snapshot(cx))
2521                    .range()
2522                    .end;
2523                end = snapshot.clip_point(Point::new(end.row + 1, 0), Bias::Right);
2524                needs_newline_prefix = end == snapshot.max_point();
2525                end..end
2526            };
2527            if self.is_read {
2528                input_range =
2529                    Some(snapshot.anchor_after(range.end)..snapshot.anchor_after(range.end));
2530            } else {
2531                input_range =
2532                    Some(snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end));
2533            }
2534            editor.highlight_rows::<ShellExec>(
2535                input_range.clone().unwrap(),
2536                cx.theme().status().unreachable_background,
2537                Default::default(),
2538                cx,
2539            );
2540
2541            if !self.is_read {
2542                input_snapshot = Some(snapshot)
2543            }
2544        });
2545
2546        let Some(range) = input_range else { return };
2547
2548        let process_task = project.update(cx, |project, cx| project.exec_in_shell(command, cx));
2549
2550        let is_read = self.is_read;
2551
2552        let task = cx.spawn_in(window, async move |vim, cx| {
2553            let Some(mut process) = process_task.await.log_err() else {
2554                return;
2555            };
2556            process.stdout(Stdio::piped());
2557            process.stderr(Stdio::piped());
2558
2559            if input_snapshot.is_some() {
2560                process.stdin(Stdio::piped());
2561            } else {
2562                process.stdin(Stdio::null());
2563            };
2564
2565            let Some(mut running) = process.spawn().log_err() else {
2566                vim.update_in(cx, |vim, window, cx| {
2567                    vim.cancel_running_command(window, cx);
2568                })
2569                .log_err();
2570                return;
2571            };
2572
2573            if let Some(mut stdin) = running.stdin.take()
2574                && let Some(snapshot) = input_snapshot
2575            {
2576                let range = range.clone();
2577                cx.background_spawn(async move {
2578                    for chunk in snapshot.text_for_range(range) {
2579                        if stdin.write_all(chunk.as_bytes()).await.log_err().is_none() {
2580                            return;
2581                        }
2582                    }
2583                    stdin.flush().await.log_err();
2584                })
2585                .detach();
2586            };
2587
2588            let output = cx.background_spawn(running.output()).await;
2589
2590            let Some(output) = output.log_err() else {
2591                vim.update_in(cx, |vim, window, cx| {
2592                    vim.cancel_running_command(window, cx);
2593                })
2594                .log_err();
2595                return;
2596            };
2597            let mut text = String::new();
2598            if needs_newline_prefix {
2599                text.push('\n');
2600            }
2601            text.push_str(&String::from_utf8_lossy(&output.stdout));
2602            text.push_str(&String::from_utf8_lossy(&output.stderr));
2603            if !text.is_empty() && text.chars().last() != Some('\n') {
2604                text.push('\n');
2605            }
2606
2607            vim.update_in(cx, |vim, window, cx| {
2608                vim.update_editor(cx, |_, editor, cx| {
2609                    editor.transact(window, cx, |editor, window, cx| {
2610                        editor.edit([(range.clone(), text)], cx);
2611                        let snapshot = editor.buffer().read(cx).snapshot(cx);
2612                        editor.change_selections(Default::default(), window, cx, |s| {
2613                            let point = if is_read {
2614                                let point = range.end.to_point(&snapshot);
2615                                Point::new(point.row.saturating_sub(1), 0)
2616                            } else {
2617                                let point = range.start.to_point(&snapshot);
2618                                Point::new(point.row, 0)
2619                            };
2620                            s.select_ranges([point..point]);
2621                        })
2622                    })
2623                });
2624                vim.cancel_running_command(window, cx);
2625            })
2626            .log_err();
2627        });
2628        vim.running_command.replace(task);
2629    }
2630}
2631
2632#[cfg(test)]
2633mod test {
2634    use std::path::{Path, PathBuf};
2635
2636    use crate::{
2637        VimAddon,
2638        state::Mode,
2639        test::{NeovimBackedTestContext, VimTestContext},
2640    };
2641    use editor::{Editor, EditorSettings};
2642    use gpui::{Context, TestAppContext};
2643    use indoc::indoc;
2644    use settings::Settings;
2645    use util::path;
2646    use workspace::{OpenOptions, Workspace};
2647
2648    #[gpui::test]
2649    async fn test_command_basics(cx: &mut TestAppContext) {
2650        let mut cx = NeovimBackedTestContext::new(cx).await;
2651
2652        cx.set_shared_state(indoc! {"
2653            ˇa
2654            b
2655            c"})
2656            .await;
2657
2658        cx.simulate_shared_keystrokes(": j enter").await;
2659
2660        // hack: our cursor positioning after a join command is wrong
2661        cx.simulate_shared_keystrokes("^").await;
2662        cx.shared_state().await.assert_eq(indoc! {
2663            "ˇa b
2664            c"
2665        });
2666    }
2667
2668    #[gpui::test]
2669    async fn test_command_goto(cx: &mut TestAppContext) {
2670        let mut cx = NeovimBackedTestContext::new(cx).await;
2671
2672        cx.set_shared_state(indoc! {"
2673            ˇa
2674            b
2675            c"})
2676            .await;
2677        cx.simulate_shared_keystrokes(": 3 enter").await;
2678        cx.shared_state().await.assert_eq(indoc! {"
2679            a
2680            b
2681            ˇc"});
2682    }
2683
2684    #[gpui::test]
2685    async fn test_command_replace(cx: &mut TestAppContext) {
2686        let mut cx = NeovimBackedTestContext::new(cx).await;
2687
2688        cx.set_shared_state(indoc! {"
2689            ˇa
2690            b
2691            b
2692            c"})
2693            .await;
2694        cx.simulate_shared_keystrokes(": % s / b / d enter").await;
2695        cx.shared_state().await.assert_eq(indoc! {"
2696            a
2697            d
2698            ˇd
2699            c"});
2700        cx.simulate_shared_keystrokes(": % s : . : \\ 0 \\ 0 enter")
2701            .await;
2702        cx.shared_state().await.assert_eq(indoc! {"
2703            aa
2704            dd
2705            dd
2706            ˇcc"});
2707        cx.simulate_shared_keystrokes("k : s / d d / e e enter")
2708            .await;
2709        cx.shared_state().await.assert_eq(indoc! {"
2710            aa
2711            dd
2712            ˇee
2713            cc"});
2714    }
2715
2716    #[gpui::test]
2717    async fn test_command_search(cx: &mut TestAppContext) {
2718        let mut cx = NeovimBackedTestContext::new(cx).await;
2719
2720        cx.set_shared_state(indoc! {"
2721                ˇa
2722                b
2723                a
2724                c"})
2725            .await;
2726        cx.simulate_shared_keystrokes(": / b enter").await;
2727        cx.shared_state().await.assert_eq(indoc! {"
2728                a
2729                ˇb
2730                a
2731                c"});
2732        cx.simulate_shared_keystrokes(": ? a enter").await;
2733        cx.shared_state().await.assert_eq(indoc! {"
2734                ˇa
2735                b
2736                a
2737                c"});
2738    }
2739
2740    #[gpui::test]
2741    async fn test_command_write(cx: &mut TestAppContext) {
2742        let mut cx = VimTestContext::new(cx, true).await;
2743        let path = Path::new(path!("/root/dir/file.rs"));
2744        let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
2745
2746        cx.simulate_keystrokes("i @ escape");
2747        cx.simulate_keystrokes(": w enter");
2748
2749        assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "@\n");
2750
2751        fs.as_fake().insert_file(path, b"oops\n".to_vec()).await;
2752
2753        // conflict!
2754        cx.simulate_keystrokes("i @ escape");
2755        cx.simulate_keystrokes(": w enter");
2756        cx.simulate_prompt_answer("Cancel");
2757
2758        assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "oops\n");
2759        assert!(!cx.has_pending_prompt());
2760        cx.simulate_keystrokes(": w !");
2761        cx.simulate_keystrokes("enter");
2762        assert!(!cx.has_pending_prompt());
2763        assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "@@\n");
2764    }
2765
2766    #[gpui::test]
2767    async fn test_command_read(cx: &mut TestAppContext) {
2768        let mut cx = VimTestContext::new(cx, true).await;
2769
2770        let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
2771        let path = Path::new(path!("/root/dir/other.rs"));
2772        fs.as_fake().insert_file(path, "1\n2\n3".into()).await;
2773
2774        cx.workspace(|workspace, _, cx| {
2775            assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
2776        });
2777
2778        // File without trailing newline
2779        cx.set_state("one\ntwo\nthreeˇ", Mode::Normal);
2780        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2781        cx.simulate_keystrokes("enter");
2782        cx.assert_state("one\ntwo\nthree\nˇ1\n2\n3", Mode::Normal);
2783
2784        cx.set_state("oneˇ\ntwo\nthree", Mode::Normal);
2785        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2786        cx.simulate_keystrokes("enter");
2787        cx.assert_state("one\nˇ1\n2\n3\ntwo\nthree", Mode::Normal);
2788
2789        cx.set_state("one\nˇtwo\nthree", Mode::Normal);
2790        cx.simulate_keystrokes(": 0 r space d i r / o t h e r . r s");
2791        cx.simulate_keystrokes("enter");
2792        cx.assert_state("ˇ1\n2\n3\none\ntwo\nthree", Mode::Normal);
2793
2794        cx.set_state("one\n«ˇtwo\nthree\nfour»\nfive", Mode::Visual);
2795        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2796        cx.simulate_keystrokes("enter");
2797        cx.run_until_parked();
2798        cx.assert_state("one\ntwo\nthree\nfour\nˇ1\n2\n3\nfive", Mode::Normal);
2799
2800        // Empty filename
2801        cx.set_state("oneˇ\ntwo\nthree", Mode::Normal);
2802        cx.simulate_keystrokes(": r");
2803        cx.simulate_keystrokes("enter");
2804        cx.assert_state("one\nˇone\ntwo\nthree\ntwo\nthree", Mode::Normal);
2805
2806        // File with trailing newline
2807        fs.as_fake().insert_file(path, "1\n2\n3\n".into()).await;
2808        cx.set_state("one\ntwo\nthreeˇ", Mode::Normal);
2809        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2810        cx.simulate_keystrokes("enter");
2811        cx.assert_state("one\ntwo\nthree\nˇ1\n2\n3\n", Mode::Normal);
2812
2813        cx.set_state("oneˇ\ntwo\nthree", Mode::Normal);
2814        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2815        cx.simulate_keystrokes("enter");
2816        cx.assert_state("one\nˇ1\n2\n3\n\ntwo\nthree", Mode::Normal);
2817
2818        cx.set_state("one\n«ˇtwo\nthree\nfour»\nfive", Mode::Visual);
2819        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2820        cx.simulate_keystrokes("enter");
2821        cx.assert_state("one\ntwo\nthree\nfour\nˇ1\n2\n3\n\nfive", Mode::Normal);
2822
2823        cx.set_state("«one\ntwo\nthreeˇ»", Mode::Visual);
2824        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2825        cx.simulate_keystrokes("enter");
2826        cx.assert_state("one\ntwo\nthree\nˇ1\n2\n3\n", Mode::Normal);
2827
2828        // Empty file
2829        fs.as_fake().insert_file(path, "".into()).await;
2830        cx.set_state("ˇone\ntwo\nthree", Mode::Normal);
2831        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2832        cx.simulate_keystrokes("enter");
2833        cx.assert_state("one\nˇtwo\nthree", Mode::Normal);
2834    }
2835
2836    #[gpui::test]
2837    async fn test_command_quit(cx: &mut TestAppContext) {
2838        let mut cx = VimTestContext::new(cx, true).await;
2839
2840        cx.simulate_keystrokes(": n e w enter");
2841        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
2842        cx.simulate_keystrokes(": q enter");
2843        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 1));
2844        cx.simulate_keystrokes(": n e w enter");
2845        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
2846        cx.simulate_keystrokes(": q a enter");
2847        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 0));
2848    }
2849
2850    #[gpui::test]
2851    async fn test_offsets(cx: &mut TestAppContext) {
2852        let mut cx = NeovimBackedTestContext::new(cx).await;
2853
2854        cx.set_shared_state("ˇ1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n")
2855            .await;
2856
2857        cx.simulate_shared_keystrokes(": + enter").await;
2858        cx.shared_state()
2859            .await
2860            .assert_eq("1\nˇ2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n");
2861
2862        cx.simulate_shared_keystrokes(": 1 0 - enter").await;
2863        cx.shared_state()
2864            .await
2865            .assert_eq("1\n2\n3\n4\n5\n6\n7\n8\nˇ9\n10\n11\n");
2866
2867        cx.simulate_shared_keystrokes(": . - 2 enter").await;
2868        cx.shared_state()
2869            .await
2870            .assert_eq("1\n2\n3\n4\n5\n6\nˇ7\n8\n9\n10\n11\n");
2871
2872        cx.simulate_shared_keystrokes(": % enter").await;
2873        cx.shared_state()
2874            .await
2875            .assert_eq("1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\nˇ");
2876    }
2877
2878    #[gpui::test]
2879    async fn test_command_ranges(cx: &mut TestAppContext) {
2880        let mut cx = NeovimBackedTestContext::new(cx).await;
2881
2882        cx.set_shared_state("ˇ1\n2\n3\n4\n4\n3\n2\n1").await;
2883
2884        cx.simulate_shared_keystrokes(": 2 , 4 d enter").await;
2885        cx.shared_state().await.assert_eq("1\nˇ4\n3\n2\n1");
2886
2887        cx.simulate_shared_keystrokes(": 2 , 4 s o r t enter").await;
2888        cx.shared_state().await.assert_eq("1\nˇ2\n3\n4\n1");
2889
2890        cx.simulate_shared_keystrokes(": 2 , 4 j o i n enter").await;
2891        cx.shared_state().await.assert_eq("1\nˇ2 3 4\n1");
2892    }
2893
2894    #[gpui::test]
2895    async fn test_command_visual_replace(cx: &mut TestAppContext) {
2896        let mut cx = NeovimBackedTestContext::new(cx).await;
2897
2898        cx.set_shared_state("ˇ1\n2\n3\n4\n4\n3\n2\n1").await;
2899
2900        cx.simulate_shared_keystrokes("v 2 j : s / . / k enter")
2901            .await;
2902        cx.shared_state().await.assert_eq("k\nk\nˇk\n4\n4\n3\n2\n1");
2903    }
2904
2905    #[track_caller]
2906    fn assert_active_item(
2907        workspace: &mut Workspace,
2908        expected_path: &str,
2909        expected_text: &str,
2910        cx: &mut Context<Workspace>,
2911    ) {
2912        let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();
2913
2914        let buffer = active_editor
2915            .read(cx)
2916            .buffer()
2917            .read(cx)
2918            .as_singleton()
2919            .unwrap();
2920
2921        let text = buffer.read(cx).text();
2922        let file = buffer.read(cx).file().unwrap();
2923        let file_path = file.as_local().unwrap().abs_path(cx);
2924
2925        assert_eq!(text, expected_text);
2926        assert_eq!(file_path, Path::new(expected_path));
2927    }
2928
2929    #[gpui::test]
2930    async fn test_command_gf(cx: &mut TestAppContext) {
2931        let mut cx = VimTestContext::new(cx, true).await;
2932
2933        // Assert base state, that we're in /root/dir/file.rs
2934        cx.workspace(|workspace, _, cx| {
2935            assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
2936        });
2937
2938        // Insert a new file
2939        let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
2940        fs.as_fake()
2941            .insert_file(
2942                path!("/root/dir/file2.rs"),
2943                "This is file2.rs".as_bytes().to_vec(),
2944            )
2945            .await;
2946        fs.as_fake()
2947            .insert_file(
2948                path!("/root/dir/file3.rs"),
2949                "go to file3".as_bytes().to_vec(),
2950            )
2951            .await;
2952
2953        // Put the path to the second file into the currently open buffer
2954        cx.set_state(indoc! {"go to fiˇle2.rs"}, Mode::Normal);
2955
2956        // Go to file2.rs
2957        cx.simulate_keystrokes("g f");
2958
2959        // We now have two items
2960        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
2961        cx.workspace(|workspace, _, cx| {
2962            assert_active_item(
2963                workspace,
2964                path!("/root/dir/file2.rs"),
2965                "This is file2.rs",
2966                cx,
2967            );
2968        });
2969
2970        // Update editor to point to `file2.rs`
2971        cx.editor =
2972            cx.workspace(|workspace, _, cx| workspace.active_item_as::<Editor>(cx).unwrap());
2973
2974        // Put the path to the third file into the currently open buffer,
2975        // but remove its suffix, because we want that lookup to happen automatically.
2976        cx.set_state(indoc! {"go to fiˇle3"}, Mode::Normal);
2977
2978        // Go to file3.rs
2979        cx.simulate_keystrokes("g f");
2980
2981        // We now have three items
2982        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 3));
2983        cx.workspace(|workspace, _, cx| {
2984            assert_active_item(workspace, path!("/root/dir/file3.rs"), "go to file3", cx);
2985        });
2986    }
2987
2988    #[gpui::test]
2989    async fn test_command_write_filename(cx: &mut TestAppContext) {
2990        let mut cx = VimTestContext::new(cx, true).await;
2991
2992        cx.workspace(|workspace, _, cx| {
2993            assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
2994        });
2995
2996        cx.simulate_keystrokes(": w space other.rs");
2997        cx.simulate_keystrokes("enter");
2998
2999        cx.workspace(|workspace, _, cx| {
3000            assert_active_item(workspace, path!("/root/other.rs"), "", cx);
3001        });
3002
3003        cx.simulate_keystrokes(": w space dir/file.rs");
3004        cx.simulate_keystrokes("enter");
3005
3006        cx.simulate_prompt_answer("Replace");
3007        cx.run_until_parked();
3008
3009        cx.workspace(|workspace, _, cx| {
3010            assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
3011        });
3012
3013        cx.simulate_keystrokes(": w ! space other.rs");
3014        cx.simulate_keystrokes("enter");
3015
3016        cx.workspace(|workspace, _, cx| {
3017            assert_active_item(workspace, path!("/root/other.rs"), "", cx);
3018        });
3019    }
3020
3021    #[gpui::test]
3022    async fn test_command_write_range(cx: &mut TestAppContext) {
3023        let mut cx = VimTestContext::new(cx, true).await;
3024
3025        cx.workspace(|workspace, _, cx| {
3026            assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
3027        });
3028
3029        cx.set_state(
3030            indoc! {"
3031                    The quick
3032                    brown« fox
3033                    jumpsˇ» over
3034                    the lazy dog
3035                "},
3036            Mode::Visual,
3037        );
3038
3039        cx.simulate_keystrokes(": w space dir/other.rs");
3040        cx.simulate_keystrokes("enter");
3041
3042        let other = path!("/root/dir/other.rs");
3043
3044        let _ = cx
3045            .workspace(|workspace, window, cx| {
3046                workspace.open_abs_path(PathBuf::from(other), OpenOptions::default(), window, cx)
3047            })
3048            .await;
3049
3050        cx.workspace(|workspace, _, cx| {
3051            assert_active_item(
3052                workspace,
3053                other,
3054                indoc! {"
3055                        brown fox
3056                        jumps over
3057                    "},
3058                cx,
3059            );
3060        });
3061    }
3062
3063    #[gpui::test]
3064    async fn test_command_matching_lines(cx: &mut TestAppContext) {
3065        let mut cx = NeovimBackedTestContext::new(cx).await;
3066
3067        cx.set_shared_state(indoc! {"
3068            ˇa
3069            b
3070            a
3071            b
3072            a
3073        "})
3074            .await;
3075
3076        cx.simulate_shared_keystrokes(":").await;
3077        cx.simulate_shared_keystrokes("g / a / d").await;
3078        cx.simulate_shared_keystrokes("enter").await;
3079
3080        cx.shared_state().await.assert_eq(indoc! {"
3081            b
3082            b
3083            ˇ"});
3084
3085        cx.simulate_shared_keystrokes("u").await;
3086
3087        cx.shared_state().await.assert_eq(indoc! {"
3088            ˇa
3089            b
3090            a
3091            b
3092            a
3093        "});
3094
3095        cx.simulate_shared_keystrokes(":").await;
3096        cx.simulate_shared_keystrokes("v / a / d").await;
3097        cx.simulate_shared_keystrokes("enter").await;
3098
3099        cx.shared_state().await.assert_eq(indoc! {"
3100            a
3101            a
3102            ˇa"});
3103    }
3104
3105    #[gpui::test]
3106    async fn test_del_marks(cx: &mut TestAppContext) {
3107        let mut cx = NeovimBackedTestContext::new(cx).await;
3108
3109        cx.set_shared_state(indoc! {"
3110            ˇa
3111            b
3112            a
3113            b
3114            a
3115        "})
3116            .await;
3117
3118        cx.simulate_shared_keystrokes("m a").await;
3119
3120        let mark = cx.update_editor(|editor, window, cx| {
3121            let vim = editor.addon::<VimAddon>().unwrap().entity.clone();
3122            vim.update(cx, |vim, cx| vim.get_mark("a", editor, window, cx))
3123        });
3124        assert!(mark.is_some());
3125
3126        cx.simulate_shared_keystrokes(": d e l m space a").await;
3127        cx.simulate_shared_keystrokes("enter").await;
3128
3129        let mark = cx.update_editor(|editor, window, cx| {
3130            let vim = editor.addon::<VimAddon>().unwrap().entity.clone();
3131            vim.update(cx, |vim, cx| vim.get_mark("a", editor, window, cx))
3132        });
3133        assert!(mark.is_none())
3134    }
3135
3136    #[gpui::test]
3137    async fn test_normal_command(cx: &mut TestAppContext) {
3138        let mut cx = NeovimBackedTestContext::new(cx).await;
3139
3140        cx.set_shared_state(indoc! {"
3141            The quick
3142            brown« fox
3143            jumpsˇ» over
3144            the lazy dog
3145        "})
3146            .await;
3147
3148        cx.simulate_shared_keystrokes(": n o r m space w C w o r d")
3149            .await;
3150        cx.simulate_shared_keystrokes("enter").await;
3151
3152        cx.shared_state().await.assert_eq(indoc! {"
3153            The quick
3154            brown word
3155            jumps worˇd
3156            the lazy dog
3157        "});
3158
3159        cx.simulate_shared_keystrokes(": n o r m space _ w c i w t e s t")
3160            .await;
3161        cx.simulate_shared_keystrokes("enter").await;
3162
3163        cx.shared_state().await.assert_eq(indoc! {"
3164            The quick
3165            brown word
3166            jumps tesˇt
3167            the lazy dog
3168        "});
3169
3170        cx.simulate_shared_keystrokes("_ l v l : n o r m space s l a")
3171            .await;
3172        cx.simulate_shared_keystrokes("enter").await;
3173
3174        cx.shared_state().await.assert_eq(indoc! {"
3175            The quick
3176            brown word
3177            lˇaumps test
3178            the lazy dog
3179        "});
3180
3181        cx.set_shared_state(indoc! {"
3182            ˇThe quick
3183            brown fox
3184            jumps over
3185            the lazy dog
3186        "})
3187            .await;
3188
3189        cx.simulate_shared_keystrokes("c i w M y escape").await;
3190
3191        cx.shared_state().await.assert_eq(indoc! {"
3192            Mˇy quick
3193            brown fox
3194            jumps over
3195            the lazy dog
3196        "});
3197
3198        cx.simulate_shared_keystrokes(": n o r m space u").await;
3199        cx.simulate_shared_keystrokes("enter").await;
3200
3201        cx.shared_state().await.assert_eq(indoc! {"
3202            ˇThe quick
3203            brown fox
3204            jumps over
3205            the lazy dog
3206        "});
3207
3208        cx.set_shared_state(indoc! {"
3209            The« quick
3210            brownˇ» fox
3211            jumps over
3212            the lazy dog
3213        "})
3214            .await;
3215
3216        cx.simulate_shared_keystrokes(": n o r m space I 1 2 3")
3217            .await;
3218        cx.simulate_shared_keystrokes("enter").await;
3219        cx.simulate_shared_keystrokes("u").await;
3220
3221        cx.shared_state().await.assert_eq(indoc! {"
3222            ˇThe quick
3223            brown fox
3224            jumps over
3225            the lazy dog
3226        "});
3227
3228        cx.set_shared_state(indoc! {"
3229            ˇquick
3230            brown fox
3231            jumps over
3232            the lazy dog
3233        "})
3234            .await;
3235
3236        cx.simulate_shared_keystrokes(": n o r m space I T h e space")
3237            .await;
3238        cx.simulate_shared_keystrokes("enter").await;
3239
3240        cx.shared_state().await.assert_eq(indoc! {"
3241            Theˇ quick
3242            brown fox
3243            jumps over
3244            the lazy dog
3245        "});
3246
3247        // Once ctrl-v to input character literals is added there should be a test for redo
3248    }
3249
3250    #[gpui::test]
3251    async fn test_command_g_normal(cx: &mut TestAppContext) {
3252        let mut cx = NeovimBackedTestContext::new(cx).await;
3253
3254        cx.set_shared_state(indoc! {"
3255            ˇfoo
3256
3257            foo
3258        "})
3259            .await;
3260
3261        cx.simulate_shared_keystrokes(": % g / f o o / n o r m space A b a r")
3262            .await;
3263        cx.simulate_shared_keystrokes("enter").await;
3264        cx.run_until_parked();
3265
3266        cx.shared_state().await.assert_eq(indoc! {"
3267            foobar
3268
3269            foobaˇr
3270        "});
3271
3272        cx.simulate_shared_keystrokes("u").await;
3273
3274        cx.shared_state().await.assert_eq(indoc! {"
3275            foˇo
3276
3277            foo
3278        "});
3279    }
3280
3281    #[gpui::test]
3282    async fn test_command_tabnew(cx: &mut TestAppContext) {
3283        let mut cx = VimTestContext::new(cx, true).await;
3284
3285        // Create a new file to ensure that, when the filename is used with
3286        // `:tabnew`, it opens the existing file in a new tab.
3287        let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
3288        fs.as_fake()
3289            .insert_file(path!("/root/dir/file_2.rs"), "file_2".as_bytes().to_vec())
3290            .await;
3291
3292        cx.simulate_keystrokes(": tabnew");
3293        cx.simulate_keystrokes("enter");
3294        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
3295
3296        // Assert that the new tab is empty and not associated with any file, as
3297        // no file path was provided to the `:tabnew` command.
3298        cx.workspace(|workspace, _window, cx| {
3299            let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();
3300            let buffer = active_editor
3301                .read(cx)
3302                .buffer()
3303                .read(cx)
3304                .as_singleton()
3305                .unwrap();
3306
3307            assert!(&buffer.read(cx).file().is_none());
3308        });
3309
3310        // Leverage the filename as an argument to the `:tabnew` command,
3311        // ensuring that the file, instead of an empty buffer, is opened in a
3312        // new tab.
3313        cx.simulate_keystrokes(": tabnew space dir/file_2.rs");
3314        cx.simulate_keystrokes("enter");
3315
3316        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 3));
3317        cx.workspace(|workspace, _, cx| {
3318            assert_active_item(workspace, path!("/root/dir/file_2.rs"), "file_2", cx);
3319        });
3320
3321        // If the `filename` argument provided to the `:tabnew` command is for a
3322        // file that doesn't yet exist, it should still associate the buffer
3323        // with that file path, so that when the buffer contents are saved, the
3324        // file is created.
3325        cx.simulate_keystrokes(": tabnew space dir/file_3.rs");
3326        cx.simulate_keystrokes("enter");
3327
3328        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 4));
3329        cx.workspace(|workspace, _, cx| {
3330            assert_active_item(workspace, path!("/root/dir/file_3.rs"), "", cx);
3331        });
3332    }
3333
3334    #[gpui::test]
3335    async fn test_command_tabedit(cx: &mut TestAppContext) {
3336        let mut cx = VimTestContext::new(cx, true).await;
3337
3338        // Create a new file to ensure that, when the filename is used with
3339        // `:tabedit`, it opens the existing file in a new tab.
3340        let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
3341        fs.as_fake()
3342            .insert_file(path!("/root/dir/file_2.rs"), "file_2".as_bytes().to_vec())
3343            .await;
3344
3345        cx.simulate_keystrokes(": tabedit");
3346        cx.simulate_keystrokes("enter");
3347        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
3348
3349        // Assert that the new tab is empty and not associated with any file, as
3350        // no file path was provided to the `:tabedit` command.
3351        cx.workspace(|workspace, _window, cx| {
3352            let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();
3353            let buffer = active_editor
3354                .read(cx)
3355                .buffer()
3356                .read(cx)
3357                .as_singleton()
3358                .unwrap();
3359
3360            assert!(&buffer.read(cx).file().is_none());
3361        });
3362
3363        // Leverage the filename as an argument to the `:tabedit` command,
3364        // ensuring that the file, instead of an empty buffer, is opened in a
3365        // new tab.
3366        cx.simulate_keystrokes(": tabedit space dir/file_2.rs");
3367        cx.simulate_keystrokes("enter");
3368
3369        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 3));
3370        cx.workspace(|workspace, _, cx| {
3371            assert_active_item(workspace, path!("/root/dir/file_2.rs"), "file_2", cx);
3372        });
3373
3374        // If the `filename` argument provided to the `:tabedit` command is for a
3375        // file that doesn't yet exist, it should still associate the buffer
3376        // with that file path, so that when the buffer contents are saved, the
3377        // file is created.
3378        cx.simulate_keystrokes(": tabedit space dir/file_3.rs");
3379        cx.simulate_keystrokes("enter");
3380
3381        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 4));
3382        cx.workspace(|workspace, _, cx| {
3383            assert_active_item(workspace, path!("/root/dir/file_3.rs"), "", cx);
3384        });
3385    }
3386
3387    #[gpui::test]
3388    async fn test_ignorecase_command(cx: &mut TestAppContext) {
3389        let mut cx = VimTestContext::new(cx, true).await;
3390        cx.read(|cx| {
3391            assert_eq!(
3392                EditorSettings::get_global(cx).search.case_sensitive,
3393                false,
3394                "The `case_sensitive` setting should be `false` by default."
3395            );
3396        });
3397        cx.simulate_keystrokes(": set space noignorecase");
3398        cx.simulate_keystrokes("enter");
3399        cx.read(|cx| {
3400            assert_eq!(
3401                EditorSettings::get_global(cx).search.case_sensitive,
3402                true,
3403                "The `case_sensitive` setting should have been enabled with `:set noignorecase`."
3404            );
3405        });
3406        cx.simulate_keystrokes(": set space ignorecase");
3407        cx.simulate_keystrokes("enter");
3408        cx.read(|cx| {
3409            assert_eq!(
3410                EditorSettings::get_global(cx).search.case_sensitive,
3411                false,
3412                "The `case_sensitive` setting should have been disabled with `:set ignorecase`."
3413            );
3414        });
3415        cx.simulate_keystrokes(": set space noic");
3416        cx.simulate_keystrokes("enter");
3417        cx.read(|cx| {
3418            assert_eq!(
3419                EditorSettings::get_global(cx).search.case_sensitive,
3420                true,
3421                "The `case_sensitive` setting should have been enabled with `:set noic`."
3422            );
3423        });
3424        cx.simulate_keystrokes(": set space ic");
3425        cx.simulate_keystrokes("enter");
3426        cx.read(|cx| {
3427            assert_eq!(
3428                EditorSettings::get_global(cx).search.case_sensitive,
3429                false,
3430                "The `case_sensitive` setting should have been disabled with `:set ic`."
3431            );
3432        });
3433    }
3434
3435    #[gpui::test]
3436    async fn test_sort_commands(cx: &mut TestAppContext) {
3437        let mut cx = VimTestContext::new(cx, true).await;
3438
3439        cx.set_state(
3440            indoc! {"
3441                «hornet
3442                quirrel
3443                elderbug
3444                cornifer
3445                idaˇ»
3446            "},
3447            Mode::Visual,
3448        );
3449
3450        cx.simulate_keystrokes(": sort");
3451        cx.simulate_keystrokes("enter");
3452
3453        cx.assert_state(
3454            indoc! {"
3455                ˇcornifer
3456                elderbug
3457                hornet
3458                ida
3459                quirrel
3460            "},
3461            Mode::Normal,
3462        );
3463
3464        // Assert that, by default, `:sort` takes case into consideration.
3465        cx.set_state(
3466            indoc! {"
3467                «hornet
3468                quirrel
3469                Elderbug
3470                cornifer
3471                idaˇ»
3472            "},
3473            Mode::Visual,
3474        );
3475
3476        cx.simulate_keystrokes(": sort");
3477        cx.simulate_keystrokes("enter");
3478
3479        cx.assert_state(
3480            indoc! {"
3481                ˇElderbug
3482                cornifer
3483                hornet
3484                ida
3485                quirrel
3486            "},
3487            Mode::Normal,
3488        );
3489
3490        // Assert that, if the `i` option is passed, `:sort` ignores case.
3491        cx.set_state(
3492            indoc! {"
3493                «hornet
3494                quirrel
3495                Elderbug
3496                cornifer
3497                idaˇ»
3498            "},
3499            Mode::Visual,
3500        );
3501
3502        cx.simulate_keystrokes(": sort space i");
3503        cx.simulate_keystrokes("enter");
3504
3505        cx.assert_state(
3506            indoc! {"
3507                ˇcornifer
3508                Elderbug
3509                hornet
3510                ida
3511                quirrel
3512            "},
3513            Mode::Normal,
3514        );
3515
3516        // When no range is provided, sorts the whole buffer.
3517        cx.set_state(
3518            indoc! {"
3519                ˇhornet
3520                quirrel
3521                elderbug
3522                cornifer
3523                ida
3524            "},
3525            Mode::Normal,
3526        );
3527
3528        cx.simulate_keystrokes(": sort");
3529        cx.simulate_keystrokes("enter");
3530
3531        cx.assert_state(
3532            indoc! {"
3533                ˇcornifer
3534                elderbug
3535                hornet
3536                ida
3537                quirrel
3538            "},
3539            Mode::Normal,
3540        );
3541    }
3542
3543    #[gpui::test]
3544    async fn test_reflow(cx: &mut TestAppContext) {
3545        let mut cx = VimTestContext::new(cx, true).await;
3546
3547        cx.update_editor(|editor, _window, cx| {
3548            editor.set_hard_wrap(Some(10), cx);
3549        });
3550
3551        cx.set_state(
3552            indoc! {"
3553                ˇ0123456789 0123456789 0123456789 0123456789
3554            "},
3555            Mode::Normal,
3556        );
3557
3558        cx.simulate_keystrokes(": reflow");
3559        cx.simulate_keystrokes("enter");
3560
3561        cx.assert_state(
3562            indoc! {"
3563                0123456789
3564                0123456789
3565                0123456789
3566                ˇ0123456789
3567            "},
3568            Mode::Normal,
3569        );
3570
3571        cx.set_state(
3572            indoc! {"
3573                «0123456789 0123456789ˇ»
3574                0123456789 0123456789
3575            "},
3576            Mode::VisualLine,
3577        );
3578
3579        cx.simulate_keystrokes(": reflow");
3580        cx.simulate_keystrokes("enter");
3581
3582        cx.assert_state(
3583            indoc! {"
3584                ˇ0123456789
3585                0123456789
3586                0123456789 0123456789
3587            "},
3588            Mode::Normal,
3589        );
3590    }
3591}