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