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