command.rs

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