command.rs

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