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