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