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::default()).filename(
1472            |_, filename| {
1473                Some(
1474                    VimSplit {
1475                        vertical: false,
1476                        filename,
1477                    }
1478                    .boxed_clone(),
1479                )
1480            },
1481        ),
1482        VimCommand::new(("vs", "plit"), workspace::SplitVertical::default()).filename(
1483            |_, filename| {
1484                Some(
1485                    VimSplit {
1486                        vertical: true,
1487                        filename,
1488                    }
1489                    .boxed_clone(),
1490                )
1491            },
1492        ),
1493        VimCommand::new(("tabe", "dit"), workspace::NewFile)
1494            .filename(|_action, filename| Some(VimEdit { filename }.boxed_clone())),
1495        VimCommand::new(("tabnew", ""), workspace::NewFile)
1496            .filename(|_action, filename| Some(VimEdit { filename }.boxed_clone())),
1497        VimCommand::new(
1498            ("q", "uit"),
1499            workspace::CloseActiveItem {
1500                save_intent: Some(SaveIntent::Close),
1501                close_pinned: false,
1502            },
1503        )
1504        .bang(workspace::CloseActiveItem {
1505            save_intent: Some(SaveIntent::Skip),
1506            close_pinned: true,
1507        }),
1508        VimCommand::new(
1509            ("wq", ""),
1510            workspace::CloseActiveItem {
1511                save_intent: Some(SaveIntent::Save),
1512                close_pinned: false,
1513            },
1514        )
1515        .bang(workspace::CloseActiveItem {
1516            save_intent: Some(SaveIntent::Overwrite),
1517            close_pinned: true,
1518        }),
1519        VimCommand::new(
1520            ("x", "it"),
1521            workspace::CloseActiveItem {
1522                save_intent: Some(SaveIntent::SaveAll),
1523                close_pinned: false,
1524            },
1525        )
1526        .bang(workspace::CloseActiveItem {
1527            save_intent: Some(SaveIntent::Overwrite),
1528            close_pinned: true,
1529        }),
1530        VimCommand::new(
1531            ("exi", "t"),
1532            workspace::CloseActiveItem {
1533                save_intent: Some(SaveIntent::SaveAll),
1534                close_pinned: false,
1535            },
1536        )
1537        .bang(workspace::CloseActiveItem {
1538            save_intent: Some(SaveIntent::Overwrite),
1539            close_pinned: true,
1540        }),
1541        VimCommand::new(
1542            ("up", "date"),
1543            workspace::Save {
1544                save_intent: Some(SaveIntent::SaveAll),
1545            },
1546        ),
1547        VimCommand::new(
1548            ("wa", "ll"),
1549            workspace::SaveAll {
1550                save_intent: Some(SaveIntent::SaveAll),
1551            },
1552        )
1553        .bang(workspace::SaveAll {
1554            save_intent: Some(SaveIntent::Overwrite),
1555        }),
1556        VimCommand::new(
1557            ("qa", "ll"),
1558            workspace::CloseAllItemsAndPanes {
1559                save_intent: Some(SaveIntent::Close),
1560            },
1561        )
1562        .bang(workspace::CloseAllItemsAndPanes {
1563            save_intent: Some(SaveIntent::Skip),
1564        }),
1565        VimCommand::new(
1566            ("quita", "ll"),
1567            workspace::CloseAllItemsAndPanes {
1568                save_intent: Some(SaveIntent::Close),
1569            },
1570        )
1571        .bang(workspace::CloseAllItemsAndPanes {
1572            save_intent: Some(SaveIntent::Skip),
1573        }),
1574        VimCommand::new(
1575            ("xa", "ll"),
1576            workspace::CloseAllItemsAndPanes {
1577                save_intent: Some(SaveIntent::SaveAll),
1578            },
1579        )
1580        .bang(workspace::CloseAllItemsAndPanes {
1581            save_intent: Some(SaveIntent::Overwrite),
1582        }),
1583        VimCommand::new(
1584            ("wqa", "ll"),
1585            workspace::CloseAllItemsAndPanes {
1586                save_intent: Some(SaveIntent::SaveAll),
1587            },
1588        )
1589        .bang(workspace::CloseAllItemsAndPanes {
1590            save_intent: Some(SaveIntent::Overwrite),
1591        }),
1592        VimCommand::new(("cq", "uit"), zed_actions::Quit),
1593        VimCommand::new(
1594            ("bd", "elete"),
1595            workspace::CloseActiveItem {
1596                save_intent: Some(SaveIntent::Close),
1597                close_pinned: false,
1598            },
1599        )
1600        .bang(workspace::CloseActiveItem {
1601            save_intent: Some(SaveIntent::Skip),
1602            close_pinned: true,
1603        }),
1604        VimCommand::new(
1605            ("norm", "al"),
1606            VimNorm {
1607                command: "".into(),
1608                range: None,
1609            },
1610        )
1611        .args(|_, args| {
1612            Some(
1613                VimNorm {
1614                    command: args,
1615                    range: None,
1616                }
1617                .boxed_clone(),
1618            )
1619        })
1620        .range(|action, range| {
1621            let mut action: VimNorm = action.as_any().downcast_ref::<VimNorm>().unwrap().clone();
1622            action.range.replace(range.clone());
1623            Some(Box::new(action))
1624        }),
1625        VimCommand::new(("bn", "ext"), workspace::ActivateNextItem).count(),
1626        VimCommand::new(("bN", "ext"), workspace::ActivatePreviousItem).count(),
1627        VimCommand::new(("bp", "revious"), workspace::ActivatePreviousItem).count(),
1628        VimCommand::new(("bf", "irst"), workspace::ActivateItem(0)),
1629        VimCommand::new(("br", "ewind"), workspace::ActivateItem(0)),
1630        VimCommand::new(("bl", "ast"), workspace::ActivateLastItem),
1631        VimCommand::str(("buffers", ""), "tab_switcher::ToggleAll"),
1632        VimCommand::str(("ls", ""), "tab_switcher::ToggleAll"),
1633        VimCommand::new(("new", ""), workspace::NewFileSplitHorizontal),
1634        VimCommand::new(("vne", "w"), workspace::NewFileSplitVertical),
1635        VimCommand::new(("tabn", "ext"), workspace::ActivateNextItem).count(),
1636        VimCommand::new(("tabp", "revious"), workspace::ActivatePreviousItem).count(),
1637        VimCommand::new(("tabN", "ext"), workspace::ActivatePreviousItem).count(),
1638        VimCommand::new(
1639            ("tabc", "lose"),
1640            workspace::CloseActiveItem {
1641                save_intent: Some(SaveIntent::Close),
1642                close_pinned: false,
1643            },
1644        ),
1645        VimCommand::new(
1646            ("tabo", "nly"),
1647            workspace::CloseOtherItems {
1648                save_intent: Some(SaveIntent::Close),
1649                close_pinned: false,
1650            },
1651        )
1652        .bang(workspace::CloseOtherItems {
1653            save_intent: Some(SaveIntent::Skip),
1654            close_pinned: false,
1655        }),
1656        VimCommand::new(
1657            ("on", "ly"),
1658            workspace::CloseInactiveTabsAndPanes {
1659                save_intent: Some(SaveIntent::Close),
1660            },
1661        )
1662        .bang(workspace::CloseInactiveTabsAndPanes {
1663            save_intent: Some(SaveIntent::Skip),
1664        }),
1665        VimCommand::str(("cl", "ist"), "diagnostics::Deploy"),
1666        VimCommand::new(("cc", ""), editor::actions::Hover),
1667        VimCommand::new(("ll", ""), editor::actions::Hover),
1668        VimCommand::new(("cn", "ext"), editor::actions::GoToDiagnostic::default())
1669            .range(wrap_count),
1670        VimCommand::new(
1671            ("cp", "revious"),
1672            editor::actions::GoToPreviousDiagnostic::default(),
1673        )
1674        .range(wrap_count),
1675        VimCommand::new(
1676            ("cN", "ext"),
1677            editor::actions::GoToPreviousDiagnostic::default(),
1678        )
1679        .range(wrap_count),
1680        VimCommand::new(
1681            ("lp", "revious"),
1682            editor::actions::GoToPreviousDiagnostic::default(),
1683        )
1684        .range(wrap_count),
1685        VimCommand::new(
1686            ("lN", "ext"),
1687            editor::actions::GoToPreviousDiagnostic::default(),
1688        )
1689        .range(wrap_count),
1690        VimCommand::new(("j", "oin"), JoinLines).range(select_range),
1691        VimCommand::new(("fo", "ld"), editor::actions::FoldSelectedRanges).range(act_on_range),
1692        VimCommand::new(("foldo", "pen"), editor::actions::UnfoldLines)
1693            .bang(editor::actions::UnfoldRecursive)
1694            .range(act_on_range),
1695        VimCommand::new(("foldc", "lose"), editor::actions::Fold)
1696            .bang(editor::actions::FoldRecursive)
1697            .range(act_on_range),
1698        VimCommand::new(("dif", "fupdate"), editor::actions::ToggleSelectedDiffHunks)
1699            .range(act_on_range),
1700        VimCommand::str(("rev", "ert"), "git::Restore").range(act_on_range),
1701        VimCommand::new(("d", "elete"), VisualDeleteLine).range(select_range),
1702        VimCommand::new(("y", "ank"), gpui::NoAction).range(|_, range| {
1703            Some(
1704                YankCommand {
1705                    range: range.clone(),
1706                }
1707                .boxed_clone(),
1708            )
1709        }),
1710        VimCommand::new(("reg", "isters"), ToggleRegistersView).bang(ToggleRegistersView),
1711        VimCommand::new(("di", "splay"), ToggleRegistersView).bang(ToggleRegistersView),
1712        VimCommand::new(("marks", ""), ToggleMarksView).bang(ToggleMarksView),
1713        VimCommand::new(("delm", "arks"), ArgumentRequired)
1714            .bang(DeleteMarks::AllLocal)
1715            .args(|_, args| Some(DeleteMarks::Marks(args).boxed_clone())),
1716        VimCommand::new(("sor", "t"), SortLinesCaseSensitive)
1717            .range(select_range)
1718            .default_range(CommandRange::buffer()),
1719        VimCommand::new(("sort i", ""), SortLinesCaseInsensitive)
1720            .range(select_range)
1721            .default_range(CommandRange::buffer()),
1722        VimCommand::str(("E", "xplore"), "project_panel::ToggleFocus"),
1723        VimCommand::str(("H", "explore"), "project_panel::ToggleFocus"),
1724        VimCommand::str(("L", "explore"), "project_panel::ToggleFocus"),
1725        VimCommand::str(("S", "explore"), "project_panel::ToggleFocus"),
1726        VimCommand::str(("Ve", "xplore"), "project_panel::ToggleFocus"),
1727        VimCommand::str(("te", "rm"), "terminal_panel::Toggle"),
1728        VimCommand::str(("T", "erm"), "terminal_panel::Toggle"),
1729        VimCommand::str(("C", "ollab"), "collab_panel::ToggleFocus"),
1730        VimCommand::str(("No", "tifications"), "notification_panel::ToggleFocus"),
1731        VimCommand::str(("A", "I"), "agent::ToggleFocus"),
1732        VimCommand::str(("G", "it"), "git_panel::ToggleFocus"),
1733        VimCommand::str(("D", "ebug"), "debug_panel::ToggleFocus"),
1734        VimCommand::new(("noh", "lsearch"), search::buffer_search::Dismiss),
1735        VimCommand::new(("$", ""), EndOfDocument),
1736        VimCommand::new(("%", ""), EndOfDocument),
1737        VimCommand::new(("0", ""), StartOfDocument),
1738        VimCommand::new(("ex", ""), editor::actions::ReloadFile).bang(editor::actions::ReloadFile),
1739        VimCommand::new(("cpp", "link"), editor::actions::CopyPermalinkToLine).range(act_on_range),
1740        VimCommand::str(("opt", "ions"), "zed::OpenDefaultSettings"),
1741        VimCommand::str(("map", ""), "vim::OpenDefaultKeymap"),
1742        VimCommand::new(("h", "elp"), OpenDocs),
1743    ]
1744}
1745
1746struct VimCommands(Vec<VimCommand>);
1747// safety: we only ever access this from the main thread (as ensured by the cx argument)
1748// actions are not Sync so we can't otherwise use a OnceLock.
1749unsafe impl Sync for VimCommands {}
1750impl Global for VimCommands {}
1751
1752fn commands(cx: &App) -> &Vec<VimCommand> {
1753    static COMMANDS: OnceLock<VimCommands> = OnceLock::new();
1754    &COMMANDS
1755        .get_or_init(|| VimCommands(generate_commands(cx)))
1756        .0
1757}
1758
1759fn act_on_range(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
1760    Some(
1761        WithRange {
1762            restore_selection: true,
1763            range: range.clone(),
1764            action: WrappedAction(action),
1765        }
1766        .boxed_clone(),
1767    )
1768}
1769
1770fn select_range(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
1771    Some(
1772        WithRange {
1773            restore_selection: false,
1774            range: range.clone(),
1775            action: WrappedAction(action),
1776        }
1777        .boxed_clone(),
1778    )
1779}
1780
1781fn wrap_count(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
1782    range.as_count().map(|count| {
1783        WithCount {
1784            count,
1785            action: WrappedAction(action),
1786        }
1787        .boxed_clone()
1788    })
1789}
1790
1791pub fn command_interceptor(
1792    mut input: &str,
1793    workspace: WeakEntity<Workspace>,
1794    cx: &mut App,
1795) -> Task<CommandInterceptResult> {
1796    while input.starts_with(':') {
1797        input = &input[1..];
1798    }
1799
1800    let (range, query) = VimCommand::parse_range(input);
1801    let range_prefix = input[0..(input.len() - query.len())].to_string();
1802    let has_trailing_space = query.ends_with(" ");
1803    let mut query = query.as_str().trim();
1804
1805    let on_matching_lines = (query.starts_with('g') || query.starts_with('v'))
1806        .then(|| {
1807            let (pattern, range, search, invert) = OnMatchingLines::parse(query, &range)?;
1808            let start_idx = query.len() - pattern.len();
1809            query = query[start_idx..].trim();
1810            Some((range, search, invert))
1811        })
1812        .flatten();
1813
1814    let mut action = if range.is_some() && query.is_empty() {
1815        Some(
1816            GoToLine {
1817                range: range.clone().unwrap(),
1818            }
1819            .boxed_clone(),
1820        )
1821    } else if query.starts_with('/') || query.starts_with('?') {
1822        Some(
1823            FindCommand {
1824                query: query[1..].to_string(),
1825                backwards: query.starts_with('?'),
1826            }
1827            .boxed_clone(),
1828        )
1829    } else if query.starts_with("se ") || query.starts_with("set ") {
1830        let (prefix, option) = query.split_once(' ').unwrap();
1831        let mut commands = VimOption::possible_commands(option);
1832        if !commands.is_empty() {
1833            let query = prefix.to_string() + " " + option;
1834            for command in &mut commands {
1835                command.positions = generate_positions(&command.string, &query);
1836            }
1837        }
1838        return Task::ready(CommandInterceptResult {
1839            results: commands,
1840            exclusive: false,
1841        });
1842    } else if query.starts_with('s') {
1843        let mut substitute = "substitute".chars().peekable();
1844        let mut query = query.chars().peekable();
1845        while substitute
1846            .peek()
1847            .is_some_and(|char| Some(char) == query.peek())
1848        {
1849            substitute.next();
1850            query.next();
1851        }
1852        if let Some(replacement) = Replacement::parse(query) {
1853            let range = range.clone().unwrap_or(CommandRange {
1854                start: Position::CurrentLine { offset: 0 },
1855                end: None,
1856            });
1857            Some(ReplaceCommand { replacement, range }.boxed_clone())
1858        } else {
1859            None
1860        }
1861    } else if query.contains('!') {
1862        ShellExec::parse(query, range.clone())
1863    } else if on_matching_lines.is_some() {
1864        commands(cx)
1865            .iter()
1866            .find_map(|command| command.parse(query, &range, cx))
1867    } else {
1868        None
1869    };
1870
1871    if let Some((range, search, invert)) = on_matching_lines
1872        && let Some(ref inner) = action
1873    {
1874        action = Some(Box::new(OnMatchingLines {
1875            range,
1876            search,
1877            action: WrappedAction(inner.boxed_clone()),
1878            invert,
1879        }));
1880    };
1881
1882    if let Some(action) = action {
1883        let string = input.to_string();
1884        let positions = generate_positions(&string, &(range_prefix + query));
1885        return Task::ready(CommandInterceptResult {
1886            results: vec![CommandInterceptItem {
1887                action,
1888                string,
1889                positions,
1890            }],
1891            exclusive: false,
1892        });
1893    }
1894
1895    let Some((mut results, filenames)) =
1896        commands(cx).iter().enumerate().find_map(|(idx, command)| {
1897            let action = command.parse(query, &range, cx)?;
1898            let parsed_query = command.get_parsed_query(query.into())?;
1899            let display_string = ":".to_owned()
1900                + &range_prefix
1901                + command.prefix
1902                + command.suffix
1903                + if parsed_query.has_bang { "!" } else { "" };
1904            let space = if parsed_query.has_space { " " } else { "" };
1905
1906            let string = format!("{}{}{}", &display_string, &space, &parsed_query.args);
1907            let positions = generate_positions(&string, &(range_prefix.clone() + query));
1908
1909            let results = vec![CommandInterceptItem {
1910                action,
1911                string,
1912                positions,
1913            }];
1914
1915            let no_args_positions =
1916                generate_positions(&display_string, &(range_prefix.clone() + query));
1917
1918            // The following are valid autocomplete scenarios:
1919            // :w!filename.txt
1920            // :w filename.txt
1921            // :w[space]
1922            if !command.has_filename
1923                || (!has_trailing_space && !parsed_query.has_bang && parsed_query.args.is_empty())
1924            {
1925                return Some((results, None));
1926            }
1927
1928            Some((
1929                results,
1930                Some((idx, parsed_query, display_string, no_args_positions)),
1931            ))
1932        })
1933    else {
1934        return Task::ready(CommandInterceptResult::default());
1935    };
1936
1937    if let Some((cmd_idx, parsed_query, display_string, no_args_positions)) = filenames {
1938        let filenames = VimCommand::generate_filename_completions(&parsed_query, workspace, cx);
1939        cx.spawn(async move |cx| {
1940            let filenames = filenames.await;
1941            const MAX_RESULTS: usize = 100;
1942            let executor = cx.background_executor().clone();
1943            let mut candidates = Vec::with_capacity(filenames.len());
1944
1945            for (idx, filename) in filenames.iter().enumerate() {
1946                candidates.push(fuzzy::StringMatchCandidate::new(idx, &filename));
1947            }
1948            let filenames = fuzzy::match_strings(
1949                &candidates,
1950                &parsed_query.args,
1951                false,
1952                true,
1953                MAX_RESULTS,
1954                &Default::default(),
1955                executor,
1956            )
1957            .await;
1958
1959            for fuzzy::StringMatch {
1960                candidate_id: _,
1961                score: _,
1962                positions,
1963                string,
1964            } in filenames
1965            {
1966                let offset = display_string.len() + 1;
1967                let mut positions: Vec<_> = positions.iter().map(|&pos| pos + offset).collect();
1968                positions.splice(0..0, no_args_positions.clone());
1969                let string = format!("{display_string} {string}");
1970                let (range, query) = VimCommand::parse_range(&string[1..]);
1971                let action =
1972                    match cx.update(|cx| commands(cx).get(cmd_idx)?.parse(&query, &range, cx)) {
1973                        Ok(Some(action)) => action,
1974                        _ => continue,
1975                    };
1976                results.push(CommandInterceptItem {
1977                    action,
1978                    string,
1979                    positions,
1980                });
1981            }
1982            CommandInterceptResult {
1983                results,
1984                exclusive: true,
1985            }
1986        })
1987    } else {
1988        Task::ready(CommandInterceptResult {
1989            results,
1990            exclusive: false,
1991        })
1992    }
1993}
1994
1995fn generate_positions(string: &str, query: &str) -> Vec<usize> {
1996    let mut positions = Vec::new();
1997    let mut chars = query.chars();
1998
1999    let Some(mut current) = chars.next() else {
2000        return positions;
2001    };
2002
2003    for (i, c) in string.char_indices() {
2004        if c == current {
2005            positions.push(i);
2006            if let Some(c) = chars.next() {
2007                current = c;
2008            } else {
2009                break;
2010            }
2011        }
2012    }
2013
2014    positions
2015}
2016
2017/// Applies a command to all lines matching a pattern.
2018#[derive(Debug, PartialEq, Clone, Action)]
2019#[action(namespace = vim, no_json, no_register)]
2020pub(crate) struct OnMatchingLines {
2021    range: CommandRange,
2022    search: String,
2023    action: WrappedAction,
2024    invert: bool,
2025}
2026
2027impl OnMatchingLines {
2028    // convert a vim query into something more usable by zed.
2029    // we don't attempt to fully convert between the two regex syntaxes,
2030    // but we do flip \( and \) to ( and ) (and vice-versa) in the pattern,
2031    // and convert \0..\9 to $0..$9 in the replacement so that common idioms work.
2032    pub(crate) fn parse(
2033        query: &str,
2034        range: &Option<CommandRange>,
2035    ) -> Option<(String, CommandRange, String, bool)> {
2036        let mut global = "global".chars().peekable();
2037        let mut query_chars = query.chars().peekable();
2038        let mut invert = false;
2039        if query_chars.peek() == Some(&'v') {
2040            invert = true;
2041            query_chars.next();
2042        }
2043        while global
2044            .peek()
2045            .is_some_and(|char| Some(char) == query_chars.peek())
2046        {
2047            global.next();
2048            query_chars.next();
2049        }
2050        if !invert && query_chars.peek() == Some(&'!') {
2051            invert = true;
2052            query_chars.next();
2053        }
2054        let range = range.clone().unwrap_or(CommandRange {
2055            start: Position::Line { row: 0, offset: 0 },
2056            end: Some(Position::LastLine { offset: 0 }),
2057        });
2058
2059        let delimiter = query_chars.next().filter(|c| {
2060            !c.is_alphanumeric() && *c != '"' && *c != '|' && *c != '\'' && *c != '!'
2061        })?;
2062
2063        let mut search = String::new();
2064        let mut escaped = false;
2065
2066        for c in query_chars.by_ref() {
2067            if escaped {
2068                escaped = false;
2069                // unescape escaped parens
2070                if c != '(' && c != ')' && c != delimiter {
2071                    search.push('\\')
2072                }
2073                search.push(c)
2074            } else if c == '\\' {
2075                escaped = true;
2076            } else if c == delimiter {
2077                break;
2078            } else {
2079                // escape unescaped parens
2080                if c == '(' || c == ')' {
2081                    search.push('\\')
2082                }
2083                search.push(c)
2084            }
2085        }
2086
2087        Some((query_chars.collect::<String>(), range, search, invert))
2088    }
2089
2090    pub fn run(&self, vim: &mut Vim, window: &mut Window, cx: &mut Context<Vim>) {
2091        let result = vim.update_editor(cx, |vim, editor, cx| {
2092            self.range.buffer_range(vim, editor, window, cx)
2093        });
2094
2095        let range = match result {
2096            None => return,
2097            Some(e @ Err(_)) => {
2098                let Some(workspace) = vim.workspace(window) else {
2099                    return;
2100                };
2101                workspace.update(cx, |workspace, cx| {
2102                    e.notify_err(workspace, cx);
2103                });
2104                return;
2105            }
2106            Some(Ok(result)) => result,
2107        };
2108
2109        let mut action = self.action.boxed_clone();
2110        let mut last_pattern = self.search.clone();
2111
2112        let mut regexes = match Regex::new(&self.search) {
2113            Ok(regex) => vec![(regex, !self.invert)],
2114            e @ Err(_) => {
2115                let Some(workspace) = vim.workspace(window) else {
2116                    return;
2117                };
2118                workspace.update(cx, |workspace, cx| {
2119                    e.notify_err(workspace, cx);
2120                });
2121                return;
2122            }
2123        };
2124        while let Some(inner) = action
2125            .boxed_clone()
2126            .as_any()
2127            .downcast_ref::<OnMatchingLines>()
2128        {
2129            let Some(regex) = Regex::new(&inner.search).ok() else {
2130                break;
2131            };
2132            last_pattern = inner.search.clone();
2133            action = inner.action.boxed_clone();
2134            regexes.push((regex, !inner.invert))
2135        }
2136
2137        if let Some(pane) = vim.pane(window, cx) {
2138            pane.update(cx, |pane, cx| {
2139                if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()
2140                {
2141                    search_bar.update(cx, |search_bar, cx| {
2142                        if search_bar.show(window, cx) {
2143                            let _ = search_bar.search(
2144                                &last_pattern,
2145                                Some(SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE),
2146                                false,
2147                                window,
2148                                cx,
2149                            );
2150                        }
2151                    });
2152                }
2153            });
2154        };
2155
2156        vim.update_editor(cx, |_, editor, cx| {
2157            let snapshot = editor.snapshot(window, cx);
2158            let mut row = range.start.0;
2159
2160            let point_range = Point::new(range.start.0, 0)
2161                ..snapshot
2162                    .buffer_snapshot()
2163                    .clip_point(Point::new(range.end.0 + 1, 0), Bias::Left);
2164            cx.spawn_in(window, async move |editor, cx| {
2165                let new_selections = cx
2166                    .background_spawn(async move {
2167                        let mut line = String::new();
2168                        let mut new_selections = Vec::new();
2169                        let chunks = snapshot
2170                            .buffer_snapshot()
2171                            .text_for_range(point_range)
2172                            .chain(["\n"]);
2173
2174                        for chunk in chunks {
2175                            for (newline_ix, text) in chunk.split('\n').enumerate() {
2176                                if newline_ix > 0 {
2177                                    if regexes.iter().all(|(regex, should_match)| {
2178                                        regex.is_match(&line) == *should_match
2179                                    }) {
2180                                        new_selections
2181                                            .push(Point::new(row, 0).to_display_point(&snapshot))
2182                                    }
2183                                    row += 1;
2184                                    line.clear();
2185                                }
2186                                line.push_str(text)
2187                            }
2188                        }
2189
2190                        new_selections
2191                    })
2192                    .await;
2193
2194                if new_selections.is_empty() {
2195                    return;
2196                }
2197                editor
2198                    .update_in(cx, |editor, window, cx| {
2199                        editor.start_transaction_at(Instant::now(), window, cx);
2200                        editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2201                            s.replace_cursors_with(|_| new_selections);
2202                        });
2203                        window.dispatch_action(action, cx);
2204                        cx.defer_in(window, move |editor, window, cx| {
2205                            let newest = editor
2206                                .selections
2207                                .newest::<Point>(&editor.display_snapshot(cx));
2208                            editor.change_selections(
2209                                SelectionEffects::no_scroll(),
2210                                window,
2211                                cx,
2212                                |s| {
2213                                    s.select(vec![newest]);
2214                                },
2215                            );
2216                            editor.end_transaction_at(Instant::now(), cx);
2217                        })
2218                    })
2219                    .ok();
2220            })
2221            .detach();
2222        });
2223    }
2224}
2225
2226/// Executes a shell command and returns the output.
2227#[derive(Clone, Debug, PartialEq, Action)]
2228#[action(namespace = vim, no_json, no_register)]
2229pub struct ShellExec {
2230    command: String,
2231    range: Option<CommandRange>,
2232    is_read: bool,
2233}
2234
2235impl Vim {
2236    pub fn cancel_running_command(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2237        if self.running_command.take().is_some() {
2238            self.update_editor(cx, |_, editor, cx| {
2239                editor.transact(window, cx, |editor, _window, _cx| {
2240                    editor.clear_row_highlights::<ShellExec>();
2241                })
2242            });
2243        }
2244    }
2245
2246    fn prepare_shell_command(
2247        &mut self,
2248        command: &str,
2249        _: &mut Window,
2250        cx: &mut Context<Self>,
2251    ) -> String {
2252        let mut ret = String::new();
2253        // N.B. non-standard escaping rules:
2254        // * !echo % => "echo README.md"
2255        // * !echo \% => "echo %"
2256        // * !echo \\% => echo \%
2257        // * !echo \\\% => echo \\%
2258        for c in command.chars() {
2259            if c != '%' && c != '!' {
2260                ret.push(c);
2261                continue;
2262            } else if ret.chars().last() == Some('\\') {
2263                ret.pop();
2264                ret.push(c);
2265                continue;
2266            }
2267            match c {
2268                '%' => {
2269                    self.update_editor(cx, |_, editor, cx| {
2270                        if let Some((_, buffer, _)) = editor.active_excerpt(cx)
2271                            && let Some(file) = buffer.read(cx).file()
2272                            && let Some(local) = file.as_local()
2273                        {
2274                            ret.push_str(&local.path().display(local.path_style(cx)));
2275                        }
2276                    });
2277                }
2278                '!' => {
2279                    if let Some(command) = &self.last_command {
2280                        ret.push_str(command)
2281                    }
2282                }
2283                _ => {}
2284            }
2285        }
2286        self.last_command = Some(ret.clone());
2287        ret
2288    }
2289
2290    pub fn shell_command_motion(
2291        &mut self,
2292        motion: Motion,
2293        times: Option<usize>,
2294        forced_motion: bool,
2295        window: &mut Window,
2296        cx: &mut Context<Vim>,
2297    ) {
2298        self.stop_recording(cx);
2299        let Some(workspace) = self.workspace(window) else {
2300            return;
2301        };
2302        let command = self.update_editor(cx, |_, editor, cx| {
2303            let snapshot = editor.snapshot(window, cx);
2304            let start = editor
2305                .selections
2306                .newest_display(&editor.display_snapshot(cx));
2307            let text_layout_details = editor.text_layout_details(window);
2308            let (mut range, _) = motion
2309                .range(
2310                    &snapshot,
2311                    start.clone(),
2312                    times,
2313                    &text_layout_details,
2314                    forced_motion,
2315                )
2316                .unwrap_or((start.range(), MotionKind::Exclusive));
2317            if range.start != start.start {
2318                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2319                    s.select_ranges([
2320                        range.start.to_point(&snapshot)..range.start.to_point(&snapshot)
2321                    ]);
2322                })
2323            }
2324            if range.end.row() > range.start.row() && range.end.column() != 0 {
2325                *range.end.row_mut() -= 1
2326            }
2327            if range.end.row() == range.start.row() {
2328                ".!".to_string()
2329            } else {
2330                format!(".,.+{}!", (range.end.row() - range.start.row()).0)
2331            }
2332        });
2333        if let Some(command) = command {
2334            workspace.update(cx, |workspace, cx| {
2335                command_palette::CommandPalette::toggle(workspace, &command, window, cx);
2336            });
2337        }
2338    }
2339
2340    pub fn shell_command_object(
2341        &mut self,
2342        object: Object,
2343        around: bool,
2344        window: &mut Window,
2345        cx: &mut Context<Vim>,
2346    ) {
2347        self.stop_recording(cx);
2348        let Some(workspace) = self.workspace(window) else {
2349            return;
2350        };
2351        let command = self.update_editor(cx, |_, editor, cx| {
2352            let snapshot = editor.snapshot(window, cx);
2353            let start = editor
2354                .selections
2355                .newest_display(&editor.display_snapshot(cx));
2356            let range = object
2357                .range(&snapshot, start.clone(), around, None)
2358                .unwrap_or(start.range());
2359            if range.start != start.start {
2360                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2361                    s.select_ranges([
2362                        range.start.to_point(&snapshot)..range.start.to_point(&snapshot)
2363                    ]);
2364                })
2365            }
2366            if range.end.row() == range.start.row() {
2367                ".!".to_string()
2368            } else {
2369                format!(".,.+{}!", (range.end.row() - range.start.row()).0)
2370            }
2371        });
2372        if let Some(command) = command {
2373            workspace.update(cx, |workspace, cx| {
2374                command_palette::CommandPalette::toggle(workspace, &command, window, cx);
2375            });
2376        }
2377    }
2378}
2379
2380impl ShellExec {
2381    pub fn parse(query: &str, range: Option<CommandRange>) -> Option<Box<dyn Action>> {
2382        let (before, after) = query.split_once('!')?;
2383        let before = before.trim();
2384
2385        if !"read".starts_with(before) {
2386            return None;
2387        }
2388
2389        Some(
2390            ShellExec {
2391                command: after.trim().to_string(),
2392                range,
2393                is_read: !before.is_empty(),
2394            }
2395            .boxed_clone(),
2396        )
2397    }
2398
2399    pub fn run(&self, vim: &mut Vim, window: &mut Window, cx: &mut Context<Vim>) {
2400        let Some(workspace) = vim.workspace(window) else {
2401            return;
2402        };
2403
2404        let project = workspace.read(cx).project().clone();
2405        let command = vim.prepare_shell_command(&self.command, window, cx);
2406
2407        if self.range.is_none() && !self.is_read {
2408            workspace.update(cx, |workspace, cx| {
2409                let project = workspace.project().read(cx);
2410                let cwd = project.first_project_directory(cx);
2411                let shell = project.terminal_settings(&cwd, cx).shell.clone();
2412
2413                let spawn_in_terminal = SpawnInTerminal {
2414                    id: TaskId("vim".to_string()),
2415                    full_label: command.clone(),
2416                    label: command.clone(),
2417                    command: Some(command.clone()),
2418                    args: Vec::new(),
2419                    command_label: command.clone(),
2420                    cwd,
2421                    env: HashMap::default(),
2422                    use_new_terminal: true,
2423                    allow_concurrent_runs: true,
2424                    reveal: RevealStrategy::NoFocus,
2425                    reveal_target: RevealTarget::Dock,
2426                    hide: HideStrategy::Never,
2427                    shell,
2428                    show_summary: false,
2429                    show_command: false,
2430                    show_rerun: false,
2431                };
2432
2433                let task_status = workspace.spawn_in_terminal(spawn_in_terminal, window, cx);
2434                cx.background_spawn(async move {
2435                    match task_status.await {
2436                        Some(Ok(status)) => {
2437                            if status.success() {
2438                                log::debug!("Vim shell exec succeeded");
2439                            } else {
2440                                log::debug!("Vim shell exec failed, code: {:?}", status.code());
2441                            }
2442                        }
2443                        Some(Err(e)) => log::error!("Vim shell exec failed: {e}"),
2444                        None => log::debug!("Vim shell exec got cancelled"),
2445                    }
2446                })
2447                .detach();
2448            });
2449            return;
2450        };
2451
2452        let mut input_snapshot = None;
2453        let mut input_range = None;
2454        let mut needs_newline_prefix = false;
2455        vim.update_editor(cx, |vim, editor, cx| {
2456            let snapshot = editor.buffer().read(cx).snapshot(cx);
2457            let range = if let Some(range) = self.range.clone() {
2458                let Some(range) = range.buffer_range(vim, editor, window, cx).log_err() else {
2459                    return;
2460                };
2461                Point::new(range.start.0, 0)
2462                    ..snapshot.clip_point(Point::new(range.end.0 + 1, 0), Bias::Right)
2463            } else {
2464                let mut end = editor
2465                    .selections
2466                    .newest::<Point>(&editor.display_snapshot(cx))
2467                    .range()
2468                    .end;
2469                end = snapshot.clip_point(Point::new(end.row + 1, 0), Bias::Right);
2470                needs_newline_prefix = end == snapshot.max_point();
2471                end..end
2472            };
2473            if self.is_read {
2474                input_range =
2475                    Some(snapshot.anchor_after(range.end)..snapshot.anchor_after(range.end));
2476            } else {
2477                input_range =
2478                    Some(snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end));
2479            }
2480            editor.highlight_rows::<ShellExec>(
2481                input_range.clone().unwrap(),
2482                cx.theme().status().unreachable_background,
2483                Default::default(),
2484                cx,
2485            );
2486
2487            if !self.is_read {
2488                input_snapshot = Some(snapshot)
2489            }
2490        });
2491
2492        let Some(range) = input_range else { return };
2493
2494        let process_task = project.update(cx, |project, cx| project.exec_in_shell(command, cx));
2495
2496        let is_read = self.is_read;
2497
2498        let task = cx.spawn_in(window, async move |vim, cx| {
2499            let Some(mut process) = process_task.await.log_err() else {
2500                return;
2501            };
2502            process.stdout(Stdio::piped());
2503            process.stderr(Stdio::piped());
2504
2505            if input_snapshot.is_some() {
2506                process.stdin(Stdio::piped());
2507            } else {
2508                process.stdin(Stdio::null());
2509            };
2510
2511            let Some(mut running) = process.spawn().log_err() else {
2512                vim.update_in(cx, |vim, window, cx| {
2513                    vim.cancel_running_command(window, cx);
2514                })
2515                .log_err();
2516                return;
2517            };
2518
2519            if let Some(mut stdin) = running.stdin.take()
2520                && let Some(snapshot) = input_snapshot
2521            {
2522                let range = range.clone();
2523                cx.background_spawn(async move {
2524                    for chunk in snapshot.text_for_range(range) {
2525                        if stdin.write_all(chunk.as_bytes()).await.log_err().is_none() {
2526                            return;
2527                        }
2528                    }
2529                    stdin.flush().await.log_err();
2530                })
2531                .detach();
2532            };
2533
2534            let output = cx.background_spawn(running.output()).await;
2535
2536            let Some(output) = output.log_err() else {
2537                vim.update_in(cx, |vim, window, cx| {
2538                    vim.cancel_running_command(window, cx);
2539                })
2540                .log_err();
2541                return;
2542            };
2543            let mut text = String::new();
2544            if needs_newline_prefix {
2545                text.push('\n');
2546            }
2547            text.push_str(&String::from_utf8_lossy(&output.stdout));
2548            text.push_str(&String::from_utf8_lossy(&output.stderr));
2549            if !text.is_empty() && text.chars().last() != Some('\n') {
2550                text.push('\n');
2551            }
2552
2553            vim.update_in(cx, |vim, window, cx| {
2554                vim.update_editor(cx, |_, editor, cx| {
2555                    editor.transact(window, cx, |editor, window, cx| {
2556                        editor.edit([(range.clone(), text)], cx);
2557                        let snapshot = editor.buffer().read(cx).snapshot(cx);
2558                        editor.change_selections(Default::default(), window, cx, |s| {
2559                            let point = if is_read {
2560                                let point = range.end.to_point(&snapshot);
2561                                Point::new(point.row.saturating_sub(1), 0)
2562                            } else {
2563                                let point = range.start.to_point(&snapshot);
2564                                Point::new(point.row, 0)
2565                            };
2566                            s.select_ranges([point..point]);
2567                        })
2568                    })
2569                });
2570                vim.cancel_running_command(window, cx);
2571            })
2572            .log_err();
2573        });
2574        vim.running_command.replace(task);
2575    }
2576}
2577
2578#[cfg(test)]
2579mod test {
2580    use std::path::{Path, PathBuf};
2581
2582    use crate::{
2583        VimAddon,
2584        state::Mode,
2585        test::{NeovimBackedTestContext, VimTestContext},
2586    };
2587    use editor::{Editor, EditorSettings};
2588    use gpui::{Context, TestAppContext};
2589    use indoc::indoc;
2590    use settings::Settings;
2591    use util::path;
2592    use workspace::{OpenOptions, Workspace};
2593
2594    #[gpui::test]
2595    async fn test_command_basics(cx: &mut TestAppContext) {
2596        let mut cx = NeovimBackedTestContext::new(cx).await;
2597
2598        cx.set_shared_state(indoc! {"
2599            ˇa
2600            b
2601            c"})
2602            .await;
2603
2604        cx.simulate_shared_keystrokes(": j enter").await;
2605
2606        // hack: our cursor positioning after a join command is wrong
2607        cx.simulate_shared_keystrokes("^").await;
2608        cx.shared_state().await.assert_eq(indoc! {
2609            "ˇa b
2610            c"
2611        });
2612    }
2613
2614    #[gpui::test]
2615    async fn test_command_goto(cx: &mut TestAppContext) {
2616        let mut cx = NeovimBackedTestContext::new(cx).await;
2617
2618        cx.set_shared_state(indoc! {"
2619            ˇa
2620            b
2621            c"})
2622            .await;
2623        cx.simulate_shared_keystrokes(": 3 enter").await;
2624        cx.shared_state().await.assert_eq(indoc! {"
2625            a
2626            b
2627            ˇc"});
2628    }
2629
2630    #[gpui::test]
2631    async fn test_command_replace(cx: &mut TestAppContext) {
2632        let mut cx = NeovimBackedTestContext::new(cx).await;
2633
2634        cx.set_shared_state(indoc! {"
2635            ˇa
2636            b
2637            b
2638            c"})
2639            .await;
2640        cx.simulate_shared_keystrokes(": % s / b / d enter").await;
2641        cx.shared_state().await.assert_eq(indoc! {"
2642            a
2643            d
2644            ˇd
2645            c"});
2646        cx.simulate_shared_keystrokes(": % s : . : \\ 0 \\ 0 enter")
2647            .await;
2648        cx.shared_state().await.assert_eq(indoc! {"
2649            aa
2650            dd
2651            dd
2652            ˇcc"});
2653        cx.simulate_shared_keystrokes("k : s / d d / e e enter")
2654            .await;
2655        cx.shared_state().await.assert_eq(indoc! {"
2656            aa
2657            dd
2658            ˇee
2659            cc"});
2660    }
2661
2662    #[gpui::test]
2663    async fn test_command_search(cx: &mut TestAppContext) {
2664        let mut cx = NeovimBackedTestContext::new(cx).await;
2665
2666        cx.set_shared_state(indoc! {"
2667                ˇa
2668                b
2669                a
2670                c"})
2671            .await;
2672        cx.simulate_shared_keystrokes(": / b enter").await;
2673        cx.shared_state().await.assert_eq(indoc! {"
2674                a
2675                ˇb
2676                a
2677                c"});
2678        cx.simulate_shared_keystrokes(": ? a enter").await;
2679        cx.shared_state().await.assert_eq(indoc! {"
2680                ˇa
2681                b
2682                a
2683                c"});
2684    }
2685
2686    #[gpui::test]
2687    async fn test_command_write(cx: &mut TestAppContext) {
2688        let mut cx = VimTestContext::new(cx, true).await;
2689        let path = Path::new(path!("/root/dir/file.rs"));
2690        let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
2691
2692        cx.simulate_keystrokes("i @ escape");
2693        cx.simulate_keystrokes(": w enter");
2694
2695        assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "@\n");
2696
2697        fs.as_fake().insert_file(path, b"oops\n".to_vec()).await;
2698
2699        // conflict!
2700        cx.simulate_keystrokes("i @ escape");
2701        cx.simulate_keystrokes(": w enter");
2702        cx.simulate_prompt_answer("Cancel");
2703
2704        assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "oops\n");
2705        assert!(!cx.has_pending_prompt());
2706        cx.simulate_keystrokes(": w !");
2707        cx.simulate_keystrokes("enter");
2708        assert!(!cx.has_pending_prompt());
2709        assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "@@\n");
2710    }
2711
2712    #[gpui::test]
2713    async fn test_command_read(cx: &mut TestAppContext) {
2714        let mut cx = VimTestContext::new(cx, true).await;
2715
2716        let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
2717        let path = Path::new(path!("/root/dir/other.rs"));
2718        fs.as_fake().insert_file(path, "1\n2\n3".into()).await;
2719
2720        cx.workspace(|workspace, _, cx| {
2721            assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
2722        });
2723
2724        // File without trailing newline
2725        cx.set_state("one\ntwo\nthreeˇ", Mode::Normal);
2726        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2727        cx.simulate_keystrokes("enter");
2728        cx.assert_state("one\ntwo\nthree\nˇ1\n2\n3", Mode::Normal);
2729
2730        cx.set_state("oneˇ\ntwo\nthree", Mode::Normal);
2731        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2732        cx.simulate_keystrokes("enter");
2733        cx.assert_state("one\nˇ1\n2\n3\ntwo\nthree", Mode::Normal);
2734
2735        cx.set_state("one\nˇtwo\nthree", Mode::Normal);
2736        cx.simulate_keystrokes(": 0 r space d i r / o t h e r . r s");
2737        cx.simulate_keystrokes("enter");
2738        cx.assert_state("ˇ1\n2\n3\none\ntwo\nthree", Mode::Normal);
2739
2740        cx.set_state("one\n«ˇtwo\nthree\nfour»\nfive", Mode::Visual);
2741        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2742        cx.simulate_keystrokes("enter");
2743        cx.run_until_parked();
2744        cx.assert_state("one\ntwo\nthree\nfour\nˇ1\n2\n3\nfive", Mode::Normal);
2745
2746        // Empty filename
2747        cx.set_state("oneˇ\ntwo\nthree", Mode::Normal);
2748        cx.simulate_keystrokes(": r");
2749        cx.simulate_keystrokes("enter");
2750        cx.assert_state("one\nˇone\ntwo\nthree\ntwo\nthree", Mode::Normal);
2751
2752        // File with trailing newline
2753        fs.as_fake().insert_file(path, "1\n2\n3\n".into()).await;
2754        cx.set_state("one\ntwo\nthreeˇ", Mode::Normal);
2755        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2756        cx.simulate_keystrokes("enter");
2757        cx.assert_state("one\ntwo\nthree\nˇ1\n2\n3\n", Mode::Normal);
2758
2759        cx.set_state("oneˇ\ntwo\nthree", Mode::Normal);
2760        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2761        cx.simulate_keystrokes("enter");
2762        cx.assert_state("one\nˇ1\n2\n3\n\ntwo\nthree", Mode::Normal);
2763
2764        cx.set_state("one\n«ˇtwo\nthree\nfour»\nfive", Mode::Visual);
2765        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2766        cx.simulate_keystrokes("enter");
2767        cx.assert_state("one\ntwo\nthree\nfour\nˇ1\n2\n3\n\nfive", Mode::Normal);
2768
2769        cx.set_state("«one\ntwo\nthreeˇ»", Mode::Visual);
2770        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2771        cx.simulate_keystrokes("enter");
2772        cx.assert_state("one\ntwo\nthree\nˇ1\n2\n3\n", Mode::Normal);
2773
2774        // Empty file
2775        fs.as_fake().insert_file(path, "".into()).await;
2776        cx.set_state("ˇone\ntwo\nthree", Mode::Normal);
2777        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2778        cx.simulate_keystrokes("enter");
2779        cx.assert_state("one\nˇtwo\nthree", Mode::Normal);
2780    }
2781
2782    #[gpui::test]
2783    async fn test_command_quit(cx: &mut TestAppContext) {
2784        let mut cx = VimTestContext::new(cx, true).await;
2785
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 enter");
2789        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 1));
2790        cx.simulate_keystrokes(": n e w enter");
2791        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
2792        cx.simulate_keystrokes(": q a enter");
2793        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 0));
2794    }
2795
2796    #[gpui::test]
2797    async fn test_offsets(cx: &mut TestAppContext) {
2798        let mut cx = NeovimBackedTestContext::new(cx).await;
2799
2800        cx.set_shared_state("ˇ1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n")
2801            .await;
2802
2803        cx.simulate_shared_keystrokes(": + enter").await;
2804        cx.shared_state()
2805            .await
2806            .assert_eq("1\nˇ2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n");
2807
2808        cx.simulate_shared_keystrokes(": 1 0 - enter").await;
2809        cx.shared_state()
2810            .await
2811            .assert_eq("1\n2\n3\n4\n5\n6\n7\n8\nˇ9\n10\n11\n");
2812
2813        cx.simulate_shared_keystrokes(": . - 2 enter").await;
2814        cx.shared_state()
2815            .await
2816            .assert_eq("1\n2\n3\n4\n5\n6\nˇ7\n8\n9\n10\n11\n");
2817
2818        cx.simulate_shared_keystrokes(": % enter").await;
2819        cx.shared_state()
2820            .await
2821            .assert_eq("1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\nˇ");
2822    }
2823
2824    #[gpui::test]
2825    async fn test_command_ranges(cx: &mut TestAppContext) {
2826        let mut cx = NeovimBackedTestContext::new(cx).await;
2827
2828        cx.set_shared_state("ˇ1\n2\n3\n4\n4\n3\n2\n1").await;
2829
2830        cx.simulate_shared_keystrokes(": 2 , 4 d enter").await;
2831        cx.shared_state().await.assert_eq("1\nˇ4\n3\n2\n1");
2832
2833        cx.simulate_shared_keystrokes(": 2 , 4 s o r t enter").await;
2834        cx.shared_state().await.assert_eq("1\nˇ2\n3\n4\n1");
2835
2836        cx.simulate_shared_keystrokes(": 2 , 4 j o i n enter").await;
2837        cx.shared_state().await.assert_eq("1\nˇ2 3 4\n1");
2838    }
2839
2840    #[gpui::test]
2841    async fn test_command_visual_replace(cx: &mut TestAppContext) {
2842        let mut cx = NeovimBackedTestContext::new(cx).await;
2843
2844        cx.set_shared_state("ˇ1\n2\n3\n4\n4\n3\n2\n1").await;
2845
2846        cx.simulate_shared_keystrokes("v 2 j : s / . / k enter")
2847            .await;
2848        cx.shared_state().await.assert_eq("k\nk\nˇk\n4\n4\n3\n2\n1");
2849    }
2850
2851    #[track_caller]
2852    fn assert_active_item(
2853        workspace: &mut Workspace,
2854        expected_path: &str,
2855        expected_text: &str,
2856        cx: &mut Context<Workspace>,
2857    ) {
2858        let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();
2859
2860        let buffer = active_editor
2861            .read(cx)
2862            .buffer()
2863            .read(cx)
2864            .as_singleton()
2865            .unwrap();
2866
2867        let text = buffer.read(cx).text();
2868        let file = buffer.read(cx).file().unwrap();
2869        let file_path = file.as_local().unwrap().abs_path(cx);
2870
2871        assert_eq!(text, expected_text);
2872        assert_eq!(file_path, Path::new(expected_path));
2873    }
2874
2875    #[gpui::test]
2876    async fn test_command_gf(cx: &mut TestAppContext) {
2877        let mut cx = VimTestContext::new(cx, true).await;
2878
2879        // Assert base state, that we're in /root/dir/file.rs
2880        cx.workspace(|workspace, _, cx| {
2881            assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
2882        });
2883
2884        // Insert a new file
2885        let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
2886        fs.as_fake()
2887            .insert_file(
2888                path!("/root/dir/file2.rs"),
2889                "This is file2.rs".as_bytes().to_vec(),
2890            )
2891            .await;
2892        fs.as_fake()
2893            .insert_file(
2894                path!("/root/dir/file3.rs"),
2895                "go to file3".as_bytes().to_vec(),
2896            )
2897            .await;
2898
2899        // Put the path to the second file into the currently open buffer
2900        cx.set_state(indoc! {"go to fiˇle2.rs"}, Mode::Normal);
2901
2902        // Go to file2.rs
2903        cx.simulate_keystrokes("g f");
2904
2905        // We now have two items
2906        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
2907        cx.workspace(|workspace, _, cx| {
2908            assert_active_item(
2909                workspace,
2910                path!("/root/dir/file2.rs"),
2911                "This is file2.rs",
2912                cx,
2913            );
2914        });
2915
2916        // Update editor to point to `file2.rs`
2917        cx.editor =
2918            cx.workspace(|workspace, _, cx| workspace.active_item_as::<Editor>(cx).unwrap());
2919
2920        // Put the path to the third file into the currently open buffer,
2921        // but remove its suffix, because we want that lookup to happen automatically.
2922        cx.set_state(indoc! {"go to fiˇle3"}, Mode::Normal);
2923
2924        // Go to file3.rs
2925        cx.simulate_keystrokes("g f");
2926
2927        // We now have three items
2928        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 3));
2929        cx.workspace(|workspace, _, cx| {
2930            assert_active_item(workspace, path!("/root/dir/file3.rs"), "go to file3", cx);
2931        });
2932    }
2933
2934    #[gpui::test]
2935    async fn test_command_write_filename(cx: &mut TestAppContext) {
2936        let mut cx = VimTestContext::new(cx, true).await;
2937
2938        cx.workspace(|workspace, _, cx| {
2939            assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
2940        });
2941
2942        cx.simulate_keystrokes(": w space other.rs");
2943        cx.simulate_keystrokes("enter");
2944
2945        cx.workspace(|workspace, _, cx| {
2946            assert_active_item(workspace, path!("/root/other.rs"), "", cx);
2947        });
2948
2949        cx.simulate_keystrokes(": w space dir/file.rs");
2950        cx.simulate_keystrokes("enter");
2951
2952        cx.simulate_prompt_answer("Replace");
2953        cx.run_until_parked();
2954
2955        cx.workspace(|workspace, _, cx| {
2956            assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
2957        });
2958
2959        cx.simulate_keystrokes(": w ! space other.rs");
2960        cx.simulate_keystrokes("enter");
2961
2962        cx.workspace(|workspace, _, cx| {
2963            assert_active_item(workspace, path!("/root/other.rs"), "", cx);
2964        });
2965    }
2966
2967    #[gpui::test]
2968    async fn test_command_write_range(cx: &mut TestAppContext) {
2969        let mut cx = VimTestContext::new(cx, true).await;
2970
2971        cx.workspace(|workspace, _, cx| {
2972            assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
2973        });
2974
2975        cx.set_state(
2976            indoc! {"
2977                    The quick
2978                    brown« fox
2979                    jumpsˇ» over
2980                    the lazy dog
2981                "},
2982            Mode::Visual,
2983        );
2984
2985        cx.simulate_keystrokes(": w space dir/other.rs");
2986        cx.simulate_keystrokes("enter");
2987
2988        let other = path!("/root/dir/other.rs");
2989
2990        let _ = cx
2991            .workspace(|workspace, window, cx| {
2992                workspace.open_abs_path(PathBuf::from(other), OpenOptions::default(), window, cx)
2993            })
2994            .await;
2995
2996        cx.workspace(|workspace, _, cx| {
2997            assert_active_item(
2998                workspace,
2999                other,
3000                indoc! {"
3001                        brown fox
3002                        jumps over
3003                    "},
3004                cx,
3005            );
3006        });
3007    }
3008
3009    #[gpui::test]
3010    async fn test_command_matching_lines(cx: &mut TestAppContext) {
3011        let mut cx = NeovimBackedTestContext::new(cx).await;
3012
3013        cx.set_shared_state(indoc! {"
3014            ˇa
3015            b
3016            a
3017            b
3018            a
3019        "})
3020            .await;
3021
3022        cx.simulate_shared_keystrokes(":").await;
3023        cx.simulate_shared_keystrokes("g / a / d").await;
3024        cx.simulate_shared_keystrokes("enter").await;
3025
3026        cx.shared_state().await.assert_eq(indoc! {"
3027            b
3028            b
3029            ˇ"});
3030
3031        cx.simulate_shared_keystrokes("u").await;
3032
3033        cx.shared_state().await.assert_eq(indoc! {"
3034            ˇa
3035            b
3036            a
3037            b
3038            a
3039        "});
3040
3041        cx.simulate_shared_keystrokes(":").await;
3042        cx.simulate_shared_keystrokes("v / a / d").await;
3043        cx.simulate_shared_keystrokes("enter").await;
3044
3045        cx.shared_state().await.assert_eq(indoc! {"
3046            a
3047            a
3048            ˇa"});
3049    }
3050
3051    #[gpui::test]
3052    async fn test_del_marks(cx: &mut TestAppContext) {
3053        let mut cx = NeovimBackedTestContext::new(cx).await;
3054
3055        cx.set_shared_state(indoc! {"
3056            ˇa
3057            b
3058            a
3059            b
3060            a
3061        "})
3062            .await;
3063
3064        cx.simulate_shared_keystrokes("m a").await;
3065
3066        let mark = cx.update_editor(|editor, window, cx| {
3067            let vim = editor.addon::<VimAddon>().unwrap().entity.clone();
3068            vim.update(cx, |vim, cx| vim.get_mark("a", editor, window, cx))
3069        });
3070        assert!(mark.is_some());
3071
3072        cx.simulate_shared_keystrokes(": d e l m space a").await;
3073        cx.simulate_shared_keystrokes("enter").await;
3074
3075        let mark = cx.update_editor(|editor, window, cx| {
3076            let vim = editor.addon::<VimAddon>().unwrap().entity.clone();
3077            vim.update(cx, |vim, cx| vim.get_mark("a", editor, window, cx))
3078        });
3079        assert!(mark.is_none())
3080    }
3081
3082    #[gpui::test]
3083    async fn test_normal_command(cx: &mut TestAppContext) {
3084        let mut cx = NeovimBackedTestContext::new(cx).await;
3085
3086        cx.set_shared_state(indoc! {"
3087            The quick
3088            brown« fox
3089            jumpsˇ» over
3090            the lazy dog
3091        "})
3092            .await;
3093
3094        cx.simulate_shared_keystrokes(": n o r m space w C w o r d")
3095            .await;
3096        cx.simulate_shared_keystrokes("enter").await;
3097
3098        cx.shared_state().await.assert_eq(indoc! {"
3099            The quick
3100            brown word
3101            jumps worˇd
3102            the lazy dog
3103        "});
3104
3105        cx.simulate_shared_keystrokes(": n o r m space _ w c i w t e s t")
3106            .await;
3107        cx.simulate_shared_keystrokes("enter").await;
3108
3109        cx.shared_state().await.assert_eq(indoc! {"
3110            The quick
3111            brown word
3112            jumps tesˇt
3113            the lazy dog
3114        "});
3115
3116        cx.simulate_shared_keystrokes("_ l v l : n o r m space s l a")
3117            .await;
3118        cx.simulate_shared_keystrokes("enter").await;
3119
3120        cx.shared_state().await.assert_eq(indoc! {"
3121            The quick
3122            brown word
3123            lˇaumps test
3124            the lazy dog
3125        "});
3126
3127        cx.set_shared_state(indoc! {"
3128            ˇThe quick
3129            brown fox
3130            jumps over
3131            the lazy dog
3132        "})
3133            .await;
3134
3135        cx.simulate_shared_keystrokes("c i w M y escape").await;
3136
3137        cx.shared_state().await.assert_eq(indoc! {"
3138            Mˇy quick
3139            brown fox
3140            jumps over
3141            the lazy dog
3142        "});
3143
3144        cx.simulate_shared_keystrokes(": n o r m space u").await;
3145        cx.simulate_shared_keystrokes("enter").await;
3146
3147        cx.shared_state().await.assert_eq(indoc! {"
3148            ˇThe quick
3149            brown fox
3150            jumps over
3151            the lazy dog
3152        "});
3153        // Once ctrl-v to input character literals is added there should be a test for redo
3154    }
3155
3156    #[gpui::test]
3157    async fn test_command_tabnew(cx: &mut TestAppContext) {
3158        let mut cx = VimTestContext::new(cx, true).await;
3159
3160        // Create a new file to ensure that, when the filename is used with
3161        // `:tabnew`, it opens the existing file in a new tab.
3162        let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
3163        fs.as_fake()
3164            .insert_file(path!("/root/dir/file_2.rs"), "file_2".as_bytes().to_vec())
3165            .await;
3166
3167        cx.simulate_keystrokes(": tabnew");
3168        cx.simulate_keystrokes("enter");
3169        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
3170
3171        // Assert that the new tab is empty and not associated with any file, as
3172        // no file path was provided to the `:tabnew` command.
3173        cx.workspace(|workspace, _window, cx| {
3174            let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();
3175            let buffer = active_editor
3176                .read(cx)
3177                .buffer()
3178                .read(cx)
3179                .as_singleton()
3180                .unwrap();
3181
3182            assert!(&buffer.read(cx).file().is_none());
3183        });
3184
3185        // Leverage the filename as an argument to the `:tabnew` command,
3186        // ensuring that the file, instead of an empty buffer, is opened in a
3187        // new tab.
3188        cx.simulate_keystrokes(": tabnew space dir/file_2.rs");
3189        cx.simulate_keystrokes("enter");
3190
3191        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 3));
3192        cx.workspace(|workspace, _, cx| {
3193            assert_active_item(workspace, path!("/root/dir/file_2.rs"), "file_2", cx);
3194        });
3195
3196        // If the `filename` argument provided to the `:tabnew` command is for a
3197        // file that doesn't yet exist, it should still associate the buffer
3198        // with that file path, so that when the buffer contents are saved, the
3199        // file is created.
3200        cx.simulate_keystrokes(": tabnew space dir/file_3.rs");
3201        cx.simulate_keystrokes("enter");
3202
3203        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 4));
3204        cx.workspace(|workspace, _, cx| {
3205            assert_active_item(workspace, path!("/root/dir/file_3.rs"), "", cx);
3206        });
3207    }
3208
3209    #[gpui::test]
3210    async fn test_command_tabedit(cx: &mut TestAppContext) {
3211        let mut cx = VimTestContext::new(cx, true).await;
3212
3213        // Create a new file to ensure that, when the filename is used with
3214        // `:tabedit`, it opens the existing file in a new tab.
3215        let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
3216        fs.as_fake()
3217            .insert_file(path!("/root/dir/file_2.rs"), "file_2".as_bytes().to_vec())
3218            .await;
3219
3220        cx.simulate_keystrokes(": tabedit");
3221        cx.simulate_keystrokes("enter");
3222        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
3223
3224        // Assert that the new tab is empty and not associated with any file, as
3225        // no file path was provided to the `:tabedit` command.
3226        cx.workspace(|workspace, _window, cx| {
3227            let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();
3228            let buffer = active_editor
3229                .read(cx)
3230                .buffer()
3231                .read(cx)
3232                .as_singleton()
3233                .unwrap();
3234
3235            assert!(&buffer.read(cx).file().is_none());
3236        });
3237
3238        // Leverage the filename as an argument to the `:tabedit` command,
3239        // ensuring that the file, instead of an empty buffer, is opened in a
3240        // new tab.
3241        cx.simulate_keystrokes(": tabedit space dir/file_2.rs");
3242        cx.simulate_keystrokes("enter");
3243
3244        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 3));
3245        cx.workspace(|workspace, _, cx| {
3246            assert_active_item(workspace, path!("/root/dir/file_2.rs"), "file_2", cx);
3247        });
3248
3249        // If the `filename` argument provided to the `:tabedit` command is for a
3250        // file that doesn't yet exist, it should still associate the buffer
3251        // with that file path, so that when the buffer contents are saved, the
3252        // file is created.
3253        cx.simulate_keystrokes(": tabedit space dir/file_3.rs");
3254        cx.simulate_keystrokes("enter");
3255
3256        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 4));
3257        cx.workspace(|workspace, _, cx| {
3258            assert_active_item(workspace, path!("/root/dir/file_3.rs"), "", cx);
3259        });
3260    }
3261
3262    #[gpui::test]
3263    async fn test_ignorecase_command(cx: &mut TestAppContext) {
3264        let mut cx = VimTestContext::new(cx, true).await;
3265        cx.read(|cx| {
3266            assert_eq!(
3267                EditorSettings::get_global(cx).search.case_sensitive,
3268                false,
3269                "The `case_sensitive` setting should be `false` by default."
3270            );
3271        });
3272        cx.simulate_keystrokes(": set space noignorecase");
3273        cx.simulate_keystrokes("enter");
3274        cx.read(|cx| {
3275            assert_eq!(
3276                EditorSettings::get_global(cx).search.case_sensitive,
3277                true,
3278                "The `case_sensitive` setting should have been enabled with `:set noignorecase`."
3279            );
3280        });
3281        cx.simulate_keystrokes(": set space ignorecase");
3282        cx.simulate_keystrokes("enter");
3283        cx.read(|cx| {
3284            assert_eq!(
3285                EditorSettings::get_global(cx).search.case_sensitive,
3286                false,
3287                "The `case_sensitive` setting should have been disabled with `:set ignorecase`."
3288            );
3289        });
3290        cx.simulate_keystrokes(": set space noic");
3291        cx.simulate_keystrokes("enter");
3292        cx.read(|cx| {
3293            assert_eq!(
3294                EditorSettings::get_global(cx).search.case_sensitive,
3295                true,
3296                "The `case_sensitive` setting should have been enabled with `:set noic`."
3297            );
3298        });
3299        cx.simulate_keystrokes(": set space ic");
3300        cx.simulate_keystrokes("enter");
3301        cx.read(|cx| {
3302            assert_eq!(
3303                EditorSettings::get_global(cx).search.case_sensitive,
3304                false,
3305                "The `case_sensitive` setting should have been disabled with `:set ic`."
3306            );
3307        });
3308    }
3309
3310    #[gpui::test]
3311    async fn test_sort_commands(cx: &mut TestAppContext) {
3312        let mut cx = VimTestContext::new(cx, true).await;
3313
3314        cx.set_state(
3315            indoc! {"
3316                «hornet
3317                quirrel
3318                elderbug
3319                cornifer
3320                idaˇ»
3321            "},
3322            Mode::Visual,
3323        );
3324
3325        cx.simulate_keystrokes(": sort");
3326        cx.simulate_keystrokes("enter");
3327
3328        cx.assert_state(
3329            indoc! {"
3330                ˇcornifer
3331                elderbug
3332                hornet
3333                ida
3334                quirrel
3335            "},
3336            Mode::Normal,
3337        );
3338
3339        // Assert that, by default, `:sort` takes case into consideration.
3340        cx.set_state(
3341            indoc! {"
3342                «hornet
3343                quirrel
3344                Elderbug
3345                cornifer
3346                idaˇ»
3347            "},
3348            Mode::Visual,
3349        );
3350
3351        cx.simulate_keystrokes(": sort");
3352        cx.simulate_keystrokes("enter");
3353
3354        cx.assert_state(
3355            indoc! {"
3356                ˇElderbug
3357                cornifer
3358                hornet
3359                ida
3360                quirrel
3361            "},
3362            Mode::Normal,
3363        );
3364
3365        // Assert that, if the `i` option is passed, `:sort` ignores case.
3366        cx.set_state(
3367            indoc! {"
3368                «hornet
3369                quirrel
3370                Elderbug
3371                cornifer
3372                idaˇ»
3373            "},
3374            Mode::Visual,
3375        );
3376
3377        cx.simulate_keystrokes(": sort space i");
3378        cx.simulate_keystrokes("enter");
3379
3380        cx.assert_state(
3381            indoc! {"
3382                ˇcornifer
3383                Elderbug
3384                hornet
3385                ida
3386                quirrel
3387            "},
3388            Mode::Normal,
3389        );
3390
3391        // When no range is provided, sorts the whole buffer.
3392        cx.set_state(
3393            indoc! {"
3394                ˇhornet
3395                quirrel
3396                elderbug
3397                cornifer
3398                ida
3399            "},
3400            Mode::Normal,
3401        );
3402
3403        cx.simulate_keystrokes(": sort");
3404        cx.simulate_keystrokes("enter");
3405
3406        cx.assert_state(
3407            indoc! {"
3408                ˇcornifer
3409                elderbug
3410                hornet
3411                ida
3412                quirrel
3413            "},
3414            Mode::Normal,
3415        );
3416    }
3417}