command.rs

   1use anyhow::Result;
   2use collections::{HashMap, HashSet};
   3use command_palette_hooks::CommandInterceptResult;
   4use editor::{
   5    Bias, Editor, SelectionEffects, ToPoint,
   6    actions::{SortLinesCaseInsensitive, SortLinesCaseSensitive},
   7    display_map::ToDisplayPoint,
   8};
   9use gpui::{Action, App, AppContext as _, Context, Global, Window, actions};
  10use itertools::Itertools;
  11use language::Point;
  12use multi_buffer::MultiBufferRow;
  13use project::ProjectPath;
  14use regex::Regex;
  15use schemars::JsonSchema;
  16use search::{BufferSearchBar, SearchOptions};
  17use serde::Deserialize;
  18use std::{
  19    io::Write,
  20    iter::Peekable,
  21    ops::{Deref, Range},
  22    path::Path,
  23    process::Stdio,
  24    str::Chars,
  25    sync::{Arc, OnceLock},
  26    time::Instant,
  27};
  28use task::{HideStrategy, RevealStrategy, SpawnInTerminal, TaskId};
  29use ui::ActiveTheme;
  30use util::ResultExt;
  31use workspace::{Item, SaveIntent, notifications::NotifyResultExt};
  32use workspace::{SplitDirection, notifications::DetachAndPromptErr};
  33use zed_actions::{OpenDocs, RevealTarget};
  34
  35use crate::{
  36    ToggleMarksView, ToggleRegistersView, Vim,
  37    motion::{EndOfDocument, Motion, MotionKind, StartOfDocument},
  38    normal::{
  39        JoinLines,
  40        search::{FindCommand, ReplaceCommand, Replacement},
  41    },
  42    object::Object,
  43    state::{Mark, Mode},
  44    visual::VisualDeleteLine,
  45};
  46
  47/// Goes to the specified line number in the editor.
  48#[derive(Clone, Debug, PartialEq, Action)]
  49#[action(namespace = vim, no_json, no_register)]
  50pub struct GoToLine {
  51    range: CommandRange,
  52}
  53
  54/// Yanks (copies) text based on the specified range.
  55#[derive(Clone, Debug, PartialEq, Action)]
  56#[action(namespace = vim, no_json, no_register)]
  57pub struct YankCommand {
  58    range: CommandRange,
  59}
  60
  61/// Executes a command with the specified range.
  62#[derive(Clone, Debug, PartialEq, Action)]
  63#[action(namespace = vim, no_json, no_register)]
  64pub struct WithRange {
  65    restore_selection: bool,
  66    range: CommandRange,
  67    action: WrappedAction,
  68}
  69
  70/// Executes a command with the specified count.
  71#[derive(Clone, Debug, PartialEq, Action)]
  72#[action(namespace = vim, no_json, no_register)]
  73pub struct WithCount {
  74    count: u32,
  75    action: WrappedAction,
  76}
  77
  78#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
  79pub enum VimOption {
  80    Wrap(bool),
  81    Number(bool),
  82    RelativeNumber(bool),
  83}
  84
  85impl VimOption {
  86    fn possible_commands(query: &str) -> Vec<CommandInterceptResult> {
  87        let mut prefix_of_options = Vec::new();
  88        let mut options = query.split(" ").collect::<Vec<_>>();
  89        let prefix = options.pop().unwrap_or_default();
  90        for option in options {
  91            if let Some(opt) = Self::from(option) {
  92                prefix_of_options.push(opt)
  93            } else {
  94                return vec![];
  95            }
  96        }
  97
  98        Self::possibilities(&prefix)
  99            .map(|possible| {
 100                let mut options = prefix_of_options.clone();
 101                options.push(possible);
 102
 103                CommandInterceptResult {
 104                    string: format!(
 105                        ":set {}",
 106                        options.iter().map(|opt| opt.to_string()).join(" ")
 107                    ),
 108                    action: VimSet { options }.boxed_clone(),
 109                    positions: vec![],
 110                }
 111            })
 112            .collect()
 113    }
 114
 115    fn possibilities(query: &str) -> impl Iterator<Item = Self> + '_ {
 116        [
 117            (None, VimOption::Wrap(true)),
 118            (None, VimOption::Wrap(false)),
 119            (None, VimOption::Number(true)),
 120            (None, VimOption::Number(false)),
 121            (None, VimOption::RelativeNumber(true)),
 122            (None, VimOption::RelativeNumber(false)),
 123            (Some("rnu"), VimOption::RelativeNumber(true)),
 124            (Some("nornu"), VimOption::RelativeNumber(false)),
 125        ]
 126        .into_iter()
 127        .filter(move |(prefix, option)| prefix.unwrap_or(option.to_string()).starts_with(query))
 128        .map(|(_, option)| option)
 129    }
 130
 131    fn from(option: &str) -> Option<Self> {
 132        match option {
 133            "wrap" => Some(Self::Wrap(true)),
 134            "nowrap" => Some(Self::Wrap(false)),
 135
 136            "number" => Some(Self::Number(true)),
 137            "nu" => Some(Self::Number(true)),
 138            "nonumber" => Some(Self::Number(false)),
 139            "nonu" => Some(Self::Number(false)),
 140
 141            "relativenumber" => Some(Self::RelativeNumber(true)),
 142            "rnu" => Some(Self::RelativeNumber(true)),
 143            "norelativenumber" => Some(Self::RelativeNumber(false)),
 144            "nornu" => Some(Self::RelativeNumber(false)),
 145
 146            _ => None,
 147        }
 148    }
 149
 150    fn to_string(&self) -> &'static str {
 151        match self {
 152            VimOption::Wrap(true) => "wrap",
 153            VimOption::Wrap(false) => "nowrap",
 154            VimOption::Number(true) => "number",
 155            VimOption::Number(false) => "nonumber",
 156            VimOption::RelativeNumber(true) => "relativenumber",
 157            VimOption::RelativeNumber(false) => "norelativenumber",
 158        }
 159    }
 160}
 161
 162/// Sets vim options and configuration values.
 163#[derive(Clone, PartialEq, Action)]
 164#[action(namespace = vim, no_json, no_register)]
 165pub struct VimSet {
 166    options: Vec<VimOption>,
 167}
 168
 169/// Saves the current file with optional save intent.
 170#[derive(Clone, PartialEq, Action)]
 171#[action(namespace = vim, no_json, no_register)]
 172struct VimSave {
 173    pub save_intent: Option<SaveIntent>,
 174    pub filename: String,
 175}
 176
 177/// Deletes the specified marks from the editor.
 178#[derive(Clone, PartialEq, Action)]
 179#[action(namespace = vim, no_json, no_register)]
 180struct VimSplit {
 181    pub vertical: bool,
 182    pub filename: String,
 183}
 184
 185#[derive(Clone, PartialEq, Action)]
 186#[action(namespace = vim, no_json, no_register)]
 187enum DeleteMarks {
 188    Marks(String),
 189    AllLocal,
 190}
 191
 192actions!(
 193    vim,
 194    [
 195        /// Executes a command in visual mode.
 196        VisualCommand,
 197        /// Executes a command with a count prefix.
 198        CountCommand,
 199        /// Executes a shell command.
 200        ShellCommand,
 201        /// Indicates that an argument is required for the command.
 202        ArgumentRequired
 203    ]
 204);
 205/// Opens the specified file for editing.
 206#[derive(Clone, PartialEq, Action)]
 207#[action(namespace = vim, no_json, no_register)]
 208struct VimEdit {
 209    pub filename: String,
 210}
 211
 212#[derive(Debug)]
 213struct WrappedAction(Box<dyn Action>);
 214
 215impl PartialEq for WrappedAction {
 216    fn eq(&self, other: &Self) -> bool {
 217        self.0.partial_eq(&*other.0)
 218    }
 219}
 220
 221impl Clone for WrappedAction {
 222    fn clone(&self) -> Self {
 223        Self(self.0.boxed_clone())
 224    }
 225}
 226
 227impl Deref for WrappedAction {
 228    type Target = dyn Action;
 229    fn deref(&self) -> &dyn Action {
 230        &*self.0
 231    }
 232}
 233
 234pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
 235    // Vim::action(editor, cx, |vim, action: &StartOfLine, window, cx| {
 236    Vim::action(editor, cx, |vim, action: &VimSet, window, cx| {
 237        for option in action.options.iter() {
 238            vim.update_editor(window, cx, |_, editor, _, cx| match option {
 239                VimOption::Wrap(true) => {
 240                    editor
 241                        .set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
 242                }
 243                VimOption::Wrap(false) => {
 244                    editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);
 245                }
 246                VimOption::Number(enabled) => {
 247                    editor.set_show_line_numbers(*enabled, cx);
 248                }
 249                VimOption::RelativeNumber(enabled) => {
 250                    editor.set_relative_line_number(Some(*enabled), cx);
 251                }
 252            });
 253        }
 254    });
 255    Vim::action(editor, cx, |vim, _: &VisualCommand, window, cx| {
 256        let Some(workspace) = vim.workspace(window) else {
 257            return;
 258        };
 259        workspace.update(cx, |workspace, cx| {
 260            command_palette::CommandPalette::toggle(workspace, "'<,'>", window, cx);
 261        })
 262    });
 263
 264    Vim::action(editor, cx, |vim, _: &ShellCommand, window, cx| {
 265        let Some(workspace) = vim.workspace(window) else {
 266            return;
 267        };
 268        workspace.update(cx, |workspace, cx| {
 269            command_palette::CommandPalette::toggle(workspace, "'<,'>!", window, cx);
 270        })
 271    });
 272
 273    Vim::action(editor, cx, |_, _: &ArgumentRequired, window, cx| {
 274        let _ = window.prompt(
 275            gpui::PromptLevel::Critical,
 276            "Argument required",
 277            None,
 278            &["Cancel"],
 279            cx,
 280        );
 281    });
 282
 283    Vim::action(editor, cx, |vim, _: &ShellCommand, window, cx| {
 284        let Some(workspace) = vim.workspace(window) else {
 285            return;
 286        };
 287        workspace.update(cx, |workspace, cx| {
 288            command_palette::CommandPalette::toggle(workspace, "'<,'>!", window, cx);
 289        })
 290    });
 291
 292    Vim::action(editor, cx, |vim, action: &VimSave, window, cx| {
 293        vim.update_editor(window, cx, |_, editor, window, cx| {
 294            let Some(project) = editor.project.clone() else {
 295                return;
 296            };
 297            let Some(worktree) = project.read(cx).visible_worktrees(cx).next() else {
 298                return;
 299            };
 300            let project_path = ProjectPath {
 301                worktree_id: worktree.read(cx).id(),
 302                path: Arc::from(Path::new(&action.filename)),
 303            };
 304
 305            if project.read(cx).entry_for_path(&project_path, cx).is_some() && action.save_intent != Some(SaveIntent::Overwrite) {
 306                let answer = window.prompt(
 307                    gpui::PromptLevel::Critical,
 308                    &format!("{} already exists. Do you want to replace it?", project_path.path.to_string_lossy()),
 309                    Some(
 310                        "A file or folder with the same name already exists. Replacing it will overwrite its current contents.",
 311                    ),
 312                    &["Replace", "Cancel"],
 313                cx);
 314                cx.spawn_in(window, async move |editor, cx| {
 315                    if answer.await.ok() != Some(0) {
 316                        return;
 317                    }
 318
 319                    let _ = editor.update_in(cx, |editor, window, cx|{
 320                        editor
 321                            .save_as(project, project_path, window, cx)
 322                            .detach_and_prompt_err("Failed to :w", window, cx, |_, _, _| None);
 323                    });
 324                }).detach();
 325            } else {
 326                editor
 327                    .save_as(project, project_path, window, cx)
 328                    .detach_and_prompt_err("Failed to :w", window, cx, |_, _, _| None);
 329            }
 330        });
 331    });
 332
 333    Vim::action(editor, cx, |vim, action: &VimSplit, window, cx| {
 334        let Some(workspace) = vim.workspace(window) else {
 335            return;
 336        };
 337
 338        workspace.update(cx, |workspace, cx| {
 339            let project = workspace.project().clone();
 340            let Some(worktree) = project.read(cx).visible_worktrees(cx).next() else {
 341                return;
 342            };
 343            let project_path = ProjectPath {
 344                worktree_id: worktree.read(cx).id(),
 345                path: Arc::from(Path::new(&action.filename)),
 346            };
 347
 348            let direction = if action.vertical {
 349                SplitDirection::vertical(cx)
 350            } else {
 351                SplitDirection::horizontal(cx)
 352            };
 353
 354            workspace
 355                .split_path_preview(project_path, false, Some(direction), window, cx)
 356                .detach_and_log_err(cx);
 357        })
 358    });
 359
 360    Vim::action(editor, cx, |vim, action: &DeleteMarks, window, cx| {
 361        fn err(s: String, window: &mut Window, cx: &mut Context<Editor>) {
 362            let _ = window.prompt(
 363                gpui::PromptLevel::Critical,
 364                &format!("Invalid argument: {}", s),
 365                None,
 366                &["Cancel"],
 367                cx,
 368            );
 369        }
 370        vim.update_editor(window, cx, |vim, editor, window, cx| match action {
 371            DeleteMarks::Marks(s) => {
 372                if s.starts_with('-') || s.ends_with('-') || s.contains(['\'', '`']) {
 373                    err(s.clone(), window, cx);
 374                    return;
 375                }
 376
 377                let to_delete = if s.len() < 3 {
 378                    Some(s.clone())
 379                } else {
 380                    s.chars()
 381                        .tuple_windows::<(_, _, _)>()
 382                        .map(|(a, b, c)| {
 383                            if b == '-' {
 384                                if match a {
 385                                    'a'..='z' => a <= c && c <= 'z',
 386                                    'A'..='Z' => a <= c && c <= 'Z',
 387                                    '0'..='9' => a <= c && c <= '9',
 388                                    _ => false,
 389                                } {
 390                                    Some((a..=c).collect_vec())
 391                                } else {
 392                                    None
 393                                }
 394                            } else if a == '-' {
 395                                if c == '-' { None } else { Some(vec![c]) }
 396                            } else if c == '-' {
 397                                if a == '-' { None } else { Some(vec![a]) }
 398                            } else {
 399                                Some(vec![a, b, c])
 400                            }
 401                        })
 402                        .fold_options(HashSet::<char>::default(), |mut set, chars| {
 403                            set.extend(chars.iter().copied());
 404                            set
 405                        })
 406                        .map(|set| set.iter().collect::<String>())
 407                };
 408
 409                let Some(to_delete) = to_delete else {
 410                    err(s.clone(), window, cx);
 411                    return;
 412                };
 413
 414                for c in to_delete.chars().filter(|c| !c.is_whitespace()) {
 415                    vim.delete_mark(c.to_string(), editor, window, cx);
 416                }
 417            }
 418            DeleteMarks::AllLocal => {
 419                for s in 'a'..='z' {
 420                    vim.delete_mark(s.to_string(), editor, window, cx);
 421                }
 422            }
 423        });
 424    });
 425
 426    Vim::action(editor, cx, |vim, action: &VimEdit, window, cx| {
 427        vim.update_editor(window, cx, |vim, editor, window, cx| {
 428            let Some(workspace) = vim.workspace(window) else {
 429                return;
 430            };
 431            let Some(project) = editor.project.clone() else {
 432                return;
 433            };
 434            let Some(worktree) = project.read(cx).visible_worktrees(cx).next() else {
 435                return;
 436            };
 437            let project_path = ProjectPath {
 438                worktree_id: worktree.read(cx).id(),
 439                path: Arc::from(Path::new(&action.filename)),
 440            };
 441
 442            let _ = workspace.update(cx, |workspace, cx| {
 443                workspace
 444                    .open_path(project_path, None, true, window, cx)
 445                    .detach_and_log_err(cx);
 446            });
 447        });
 448    });
 449
 450    Vim::action(editor, cx, |vim, _: &CountCommand, window, cx| {
 451        let Some(workspace) = vim.workspace(window) else {
 452            return;
 453        };
 454        let count = Vim::take_count(cx).unwrap_or(1);
 455        Vim::take_forced_motion(cx);
 456        let n = if count > 1 {
 457            format!(".,.+{}", count.saturating_sub(1))
 458        } else {
 459            ".".to_string()
 460        };
 461        workspace.update(cx, |workspace, cx| {
 462            command_palette::CommandPalette::toggle(workspace, &n, window, cx);
 463        })
 464    });
 465
 466    Vim::action(editor, cx, |vim, action: &GoToLine, window, cx| {
 467        vim.switch_mode(Mode::Normal, false, window, cx);
 468        let result = vim.update_editor(window, cx, |vim, editor, window, cx| {
 469            let snapshot = editor.snapshot(window, cx);
 470            let buffer_row = action.range.head().buffer_row(vim, editor, window, cx)?;
 471            let current = editor.selections.newest::<Point>(cx);
 472            let target = snapshot
 473                .buffer_snapshot
 474                .clip_point(Point::new(buffer_row.0, current.head().column), Bias::Left);
 475            editor.change_selections(Default::default(), window, cx, |s| {
 476                s.select_ranges([target..target]);
 477            });
 478
 479            anyhow::Ok(())
 480        });
 481        if let Some(e @ Err(_)) = result {
 482            let Some(workspace) = vim.workspace(window) else {
 483                return;
 484            };
 485            workspace.update(cx, |workspace, cx| {
 486                e.notify_err(workspace, cx);
 487            });
 488            return;
 489        }
 490    });
 491
 492    Vim::action(editor, cx, |vim, action: &YankCommand, window, cx| {
 493        vim.update_editor(window, cx, |vim, editor, window, cx| {
 494            let snapshot = editor.snapshot(window, cx);
 495            if let Ok(range) = action.range.buffer_range(vim, editor, window, cx) {
 496                let end = if range.end < snapshot.buffer_snapshot.max_row() {
 497                    Point::new(range.end.0 + 1, 0)
 498                } else {
 499                    snapshot.buffer_snapshot.max_point()
 500                };
 501                vim.copy_ranges(
 502                    editor,
 503                    MotionKind::Linewise,
 504                    true,
 505                    vec![Point::new(range.start.0, 0)..end],
 506                    window,
 507                    cx,
 508                )
 509            }
 510        });
 511    });
 512
 513    Vim::action(editor, cx, |_, action: &WithCount, window, cx| {
 514        for _ in 0..action.count {
 515            window.dispatch_action(action.action.boxed_clone(), cx)
 516        }
 517    });
 518
 519    Vim::action(editor, cx, |vim, action: &WithRange, window, cx| {
 520        let result = vim.update_editor(window, cx, |vim, editor, window, cx| {
 521            action.range.buffer_range(vim, editor, window, cx)
 522        });
 523
 524        let range = match result {
 525            None => return,
 526            Some(e @ Err(_)) => {
 527                let Some(workspace) = vim.workspace(window) else {
 528                    return;
 529                };
 530                workspace.update(cx, |workspace, cx| {
 531                    e.notify_err(workspace, cx);
 532                });
 533                return;
 534            }
 535            Some(Ok(result)) => result,
 536        };
 537
 538        let previous_selections = vim
 539            .update_editor(window, cx, |_, editor, window, cx| {
 540                let selections = action.restore_selection.then(|| {
 541                    editor
 542                        .selections
 543                        .disjoint_anchor_ranges()
 544                        .collect::<Vec<_>>()
 545                });
 546                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
 547                    let end = Point::new(range.end.0, s.buffer().line_len(range.end));
 548                    s.select_ranges([end..Point::new(range.start.0, 0)]);
 549                });
 550                selections
 551            })
 552            .flatten();
 553        window.dispatch_action(action.action.boxed_clone(), cx);
 554        cx.defer_in(window, move |vim, window, cx| {
 555            vim.update_editor(window, cx, |_, editor, window, cx| {
 556                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
 557                    if let Some(previous_selections) = previous_selections {
 558                        s.select_ranges(previous_selections);
 559                    } else {
 560                        s.select_ranges([
 561                            Point::new(range.start.0, 0)..Point::new(range.start.0, 0)
 562                        ]);
 563                    }
 564                })
 565            });
 566        });
 567    });
 568
 569    Vim::action(editor, cx, |vim, action: &OnMatchingLines, window, cx| {
 570        action.run(vim, window, cx)
 571    });
 572
 573    Vim::action(editor, cx, |vim, action: &ShellExec, window, cx| {
 574        action.run(vim, window, cx)
 575    })
 576}
 577
 578#[derive(Default)]
 579struct VimCommand {
 580    prefix: &'static str,
 581    suffix: &'static str,
 582    action: Option<Box<dyn Action>>,
 583    action_name: Option<&'static str>,
 584    bang_action: Option<Box<dyn Action>>,
 585    args: Option<
 586        Box<dyn Fn(Box<dyn Action>, String) -> Option<Box<dyn Action>> + Send + Sync + 'static>,
 587    >,
 588    range: Option<
 589        Box<
 590            dyn Fn(Box<dyn Action>, &CommandRange) -> Option<Box<dyn Action>>
 591                + Send
 592                + Sync
 593                + 'static,
 594        >,
 595    >,
 596    has_count: bool,
 597}
 598
 599impl VimCommand {
 600    fn new(pattern: (&'static str, &'static str), action: impl Action) -> Self {
 601        Self {
 602            prefix: pattern.0,
 603            suffix: pattern.1,
 604            action: Some(action.boxed_clone()),
 605            ..Default::default()
 606        }
 607    }
 608
 609    // from_str is used for actions in other crates.
 610    fn str(pattern: (&'static str, &'static str), action_name: &'static str) -> Self {
 611        Self {
 612            prefix: pattern.0,
 613            suffix: pattern.1,
 614            action_name: Some(action_name),
 615            ..Default::default()
 616        }
 617    }
 618
 619    fn bang(mut self, bang_action: impl Action) -> Self {
 620        self.bang_action = Some(bang_action.boxed_clone());
 621        self
 622    }
 623
 624    fn args(
 625        mut self,
 626        f: impl Fn(Box<dyn Action>, String) -> Option<Box<dyn Action>> + Send + Sync + 'static,
 627    ) -> Self {
 628        self.args = Some(Box::new(f));
 629        self
 630    }
 631
 632    fn range(
 633        mut self,
 634        f: impl Fn(Box<dyn Action>, &CommandRange) -> Option<Box<dyn Action>> + Send + Sync + 'static,
 635    ) -> Self {
 636        self.range = Some(Box::new(f));
 637        self
 638    }
 639
 640    fn count(mut self) -> Self {
 641        self.has_count = true;
 642        self
 643    }
 644
 645    fn parse(
 646        &self,
 647        query: &str,
 648        range: &Option<CommandRange>,
 649        cx: &App,
 650    ) -> Option<Box<dyn Action>> {
 651        let rest = query
 652            .to_string()
 653            .strip_prefix(self.prefix)?
 654            .to_string()
 655            .chars()
 656            .zip_longest(self.suffix.to_string().chars())
 657            .skip_while(|e| e.clone().both().map(|(s, q)| s == q).unwrap_or(false))
 658            .filter_map(|e| e.left())
 659            .collect::<String>();
 660        let has_bang = rest.starts_with('!');
 661        let args = if has_bang {
 662            rest.strip_prefix('!')?.trim().to_string()
 663        } else if rest.is_empty() {
 664            "".into()
 665        } else {
 666            rest.strip_prefix(' ')?.trim().to_string()
 667        };
 668
 669        let action = if has_bang && self.bang_action.is_some() {
 670            self.bang_action.as_ref().unwrap().boxed_clone()
 671        } else if let Some(action) = self.action.as_ref() {
 672            action.boxed_clone()
 673        } else if let Some(action_name) = self.action_name {
 674            cx.build_action(action_name, None).log_err()?
 675        } else {
 676            return None;
 677        };
 678        if !args.is_empty() {
 679            // if command does not accept args and we have args then we should do no action
 680            if let Some(args_fn) = &self.args {
 681                args_fn.deref()(action, args)
 682            } else {
 683                None
 684            }
 685        } else if let Some(range) = range {
 686            self.range.as_ref().and_then(|f| f(action, range))
 687        } else {
 688            Some(action)
 689        }
 690    }
 691
 692    // TODO: ranges with search queries
 693    fn parse_range(query: &str) -> (Option<CommandRange>, String) {
 694        let mut chars = query.chars().peekable();
 695
 696        match chars.peek() {
 697            Some('%') => {
 698                chars.next();
 699                return (
 700                    Some(CommandRange {
 701                        start: Position::Line { row: 1, offset: 0 },
 702                        end: Some(Position::LastLine { offset: 0 }),
 703                    }),
 704                    chars.collect(),
 705                );
 706            }
 707            Some('*') => {
 708                chars.next();
 709                return (
 710                    Some(CommandRange {
 711                        start: Position::Mark {
 712                            name: '<',
 713                            offset: 0,
 714                        },
 715                        end: Some(Position::Mark {
 716                            name: '>',
 717                            offset: 0,
 718                        }),
 719                    }),
 720                    chars.collect(),
 721                );
 722            }
 723            _ => {}
 724        }
 725
 726        let start = Self::parse_position(&mut chars);
 727
 728        match chars.peek() {
 729            Some(',' | ';') => {
 730                chars.next();
 731                (
 732                    Some(CommandRange {
 733                        start: start.unwrap_or(Position::CurrentLine { offset: 0 }),
 734                        end: Self::parse_position(&mut chars),
 735                    }),
 736                    chars.collect(),
 737                )
 738            }
 739            _ => (
 740                start.map(|start| CommandRange { start, end: None }),
 741                chars.collect(),
 742            ),
 743        }
 744    }
 745
 746    fn parse_position(chars: &mut Peekable<Chars>) -> Option<Position> {
 747        match chars.peek()? {
 748            '0'..='9' => {
 749                let row = Self::parse_u32(chars);
 750                Some(Position::Line {
 751                    row,
 752                    offset: Self::parse_offset(chars),
 753                })
 754            }
 755            '\'' => {
 756                chars.next();
 757                let name = chars.next()?;
 758                Some(Position::Mark {
 759                    name,
 760                    offset: Self::parse_offset(chars),
 761                })
 762            }
 763            '.' => {
 764                chars.next();
 765                Some(Position::CurrentLine {
 766                    offset: Self::parse_offset(chars),
 767                })
 768            }
 769            '+' | '-' => Some(Position::CurrentLine {
 770                offset: Self::parse_offset(chars),
 771            }),
 772            '$' => {
 773                chars.next();
 774                Some(Position::LastLine {
 775                    offset: Self::parse_offset(chars),
 776                })
 777            }
 778            _ => None,
 779        }
 780    }
 781
 782    fn parse_offset(chars: &mut Peekable<Chars>) -> i32 {
 783        let mut res: i32 = 0;
 784        while matches!(chars.peek(), Some('+' | '-')) {
 785            let sign = if chars.next().unwrap() == '+' { 1 } else { -1 };
 786            let amount = if matches!(chars.peek(), Some('0'..='9')) {
 787                (Self::parse_u32(chars) as i32).saturating_mul(sign)
 788            } else {
 789                sign
 790            };
 791            res = res.saturating_add(amount)
 792        }
 793        res
 794    }
 795
 796    fn parse_u32(chars: &mut Peekable<Chars>) -> u32 {
 797        let mut res: u32 = 0;
 798        while matches!(chars.peek(), Some('0'..='9')) {
 799            res = res
 800                .saturating_mul(10)
 801                .saturating_add(chars.next().unwrap() as u32 - '0' as u32);
 802        }
 803        res
 804    }
 805}
 806
 807#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq)]
 808enum Position {
 809    Line { row: u32, offset: i32 },
 810    Mark { name: char, offset: i32 },
 811    LastLine { offset: i32 },
 812    CurrentLine { offset: i32 },
 813}
 814
 815impl Position {
 816    fn buffer_row(
 817        &self,
 818        vim: &Vim,
 819        editor: &mut Editor,
 820        window: &mut Window,
 821        cx: &mut App,
 822    ) -> Result<MultiBufferRow> {
 823        let snapshot = editor.snapshot(window, cx);
 824        let target = match self {
 825            Position::Line { row, offset } => {
 826                if let Some(anchor) = editor.active_excerpt(cx).and_then(|(_, buffer, _)| {
 827                    editor.buffer().read(cx).buffer_point_to_anchor(
 828                        &buffer,
 829                        Point::new(row.saturating_sub(1), 0),
 830                        cx,
 831                    )
 832                }) {
 833                    anchor
 834                        .to_point(&snapshot.buffer_snapshot)
 835                        .row
 836                        .saturating_add_signed(*offset)
 837                } else {
 838                    row.saturating_add_signed(offset.saturating_sub(1))
 839                }
 840            }
 841            Position::Mark { name, offset } => {
 842                let Some(Mark::Local(anchors)) =
 843                    vim.get_mark(&name.to_string(), editor, window, cx)
 844                else {
 845                    anyhow::bail!("mark {name} not set");
 846                };
 847                let Some(mark) = anchors.last() else {
 848                    anyhow::bail!("mark {name} contains empty anchors");
 849                };
 850                mark.to_point(&snapshot.buffer_snapshot)
 851                    .row
 852                    .saturating_add_signed(*offset)
 853            }
 854            Position::LastLine { offset } => snapshot
 855                .buffer_snapshot
 856                .max_row()
 857                .0
 858                .saturating_add_signed(*offset),
 859            Position::CurrentLine { offset } => editor
 860                .selections
 861                .newest_anchor()
 862                .head()
 863                .to_point(&snapshot.buffer_snapshot)
 864                .row
 865                .saturating_add_signed(*offset),
 866        };
 867
 868        Ok(MultiBufferRow(target).min(snapshot.buffer_snapshot.max_row()))
 869    }
 870}
 871
 872#[derive(Clone, Debug, PartialEq)]
 873pub(crate) struct CommandRange {
 874    start: Position,
 875    end: Option<Position>,
 876}
 877
 878impl CommandRange {
 879    fn head(&self) -> &Position {
 880        self.end.as_ref().unwrap_or(&self.start)
 881    }
 882
 883    pub(crate) fn buffer_range(
 884        &self,
 885        vim: &Vim,
 886        editor: &mut Editor,
 887        window: &mut Window,
 888        cx: &mut App,
 889    ) -> Result<Range<MultiBufferRow>> {
 890        let start = self.start.buffer_row(vim, editor, window, cx)?;
 891        let end = if let Some(end) = self.end.as_ref() {
 892            end.buffer_row(vim, editor, window, cx)?
 893        } else {
 894            start
 895        };
 896        if end < start {
 897            anyhow::Ok(end..start)
 898        } else {
 899            anyhow::Ok(start..end)
 900        }
 901    }
 902
 903    pub fn as_count(&self) -> Option<u32> {
 904        if let CommandRange {
 905            start: Position::Line { row, offset: 0 },
 906            end: None,
 907        } = &self
 908        {
 909            Some(*row)
 910        } else {
 911            None
 912        }
 913    }
 914}
 915
 916fn generate_commands(_: &App) -> Vec<VimCommand> {
 917    vec![
 918        VimCommand::new(
 919            ("w", "rite"),
 920            workspace::Save {
 921                save_intent: Some(SaveIntent::Save),
 922            },
 923        )
 924        .bang(workspace::Save {
 925            save_intent: Some(SaveIntent::Overwrite),
 926        })
 927        .args(|action, args| {
 928            Some(
 929                VimSave {
 930                    save_intent: action
 931                        .as_any()
 932                        .downcast_ref::<workspace::Save>()
 933                        .and_then(|action| action.save_intent),
 934                    filename: args,
 935                }
 936                .boxed_clone(),
 937            )
 938        }),
 939        VimCommand::new(
 940            ("q", "uit"),
 941            workspace::CloseActiveItem {
 942                save_intent: Some(SaveIntent::Close),
 943                close_pinned: false,
 944            },
 945        )
 946        .bang(workspace::CloseActiveItem {
 947            save_intent: Some(SaveIntent::Skip),
 948            close_pinned: true,
 949        }),
 950        VimCommand::new(
 951            ("wq", ""),
 952            workspace::CloseActiveItem {
 953                save_intent: Some(SaveIntent::Save),
 954                close_pinned: false,
 955            },
 956        )
 957        .bang(workspace::CloseActiveItem {
 958            save_intent: Some(SaveIntent::Overwrite),
 959            close_pinned: true,
 960        }),
 961        VimCommand::new(
 962            ("x", "it"),
 963            workspace::CloseActiveItem {
 964                save_intent: Some(SaveIntent::SaveAll),
 965                close_pinned: false,
 966            },
 967        )
 968        .bang(workspace::CloseActiveItem {
 969            save_intent: Some(SaveIntent::Overwrite),
 970            close_pinned: true,
 971        }),
 972        VimCommand::new(
 973            ("exi", "t"),
 974            workspace::CloseActiveItem {
 975                save_intent: Some(SaveIntent::SaveAll),
 976                close_pinned: false,
 977            },
 978        )
 979        .bang(workspace::CloseActiveItem {
 980            save_intent: Some(SaveIntent::Overwrite),
 981            close_pinned: true,
 982        }),
 983        VimCommand::new(
 984            ("up", "date"),
 985            workspace::Save {
 986                save_intent: Some(SaveIntent::SaveAll),
 987            },
 988        ),
 989        VimCommand::new(
 990            ("wa", "ll"),
 991            workspace::SaveAll {
 992                save_intent: Some(SaveIntent::SaveAll),
 993            },
 994        )
 995        .bang(workspace::SaveAll {
 996            save_intent: Some(SaveIntent::Overwrite),
 997        }),
 998        VimCommand::new(
 999            ("qa", "ll"),
1000            workspace::CloseAllItemsAndPanes {
1001                save_intent: Some(SaveIntent::Close),
1002            },
1003        )
1004        .bang(workspace::CloseAllItemsAndPanes {
1005            save_intent: Some(SaveIntent::Skip),
1006        }),
1007        VimCommand::new(
1008            ("quita", "ll"),
1009            workspace::CloseAllItemsAndPanes {
1010                save_intent: Some(SaveIntent::Close),
1011            },
1012        )
1013        .bang(workspace::CloseAllItemsAndPanes {
1014            save_intent: Some(SaveIntent::Skip),
1015        }),
1016        VimCommand::new(
1017            ("xa", "ll"),
1018            workspace::CloseAllItemsAndPanes {
1019                save_intent: Some(SaveIntent::SaveAll),
1020            },
1021        )
1022        .bang(workspace::CloseAllItemsAndPanes {
1023            save_intent: Some(SaveIntent::Overwrite),
1024        }),
1025        VimCommand::new(
1026            ("wqa", "ll"),
1027            workspace::CloseAllItemsAndPanes {
1028                save_intent: Some(SaveIntent::SaveAll),
1029            },
1030        )
1031        .bang(workspace::CloseAllItemsAndPanes {
1032            save_intent: Some(SaveIntent::Overwrite),
1033        }),
1034        VimCommand::new(("cq", "uit"), zed_actions::Quit),
1035        VimCommand::new(("sp", "lit"), workspace::SplitHorizontal).args(|_, args| {
1036            Some(
1037                VimSplit {
1038                    vertical: false,
1039                    filename: args,
1040                }
1041                .boxed_clone(),
1042            )
1043        }),
1044        VimCommand::new(("vs", "plit"), workspace::SplitVertical).args(|_, args| {
1045            Some(
1046                VimSplit {
1047                    vertical: true,
1048                    filename: args,
1049                }
1050                .boxed_clone(),
1051            )
1052        }),
1053        VimCommand::new(
1054            ("bd", "elete"),
1055            workspace::CloseActiveItem {
1056                save_intent: Some(SaveIntent::Close),
1057                close_pinned: false,
1058            },
1059        )
1060        .bang(workspace::CloseActiveItem {
1061            save_intent: Some(SaveIntent::Skip),
1062            close_pinned: true,
1063        }),
1064        VimCommand::new(("bn", "ext"), workspace::ActivateNextItem).count(),
1065        VimCommand::new(("bN", "ext"), workspace::ActivatePreviousItem).count(),
1066        VimCommand::new(("bp", "revious"), workspace::ActivatePreviousItem).count(),
1067        VimCommand::new(("bf", "irst"), workspace::ActivateItem(0)),
1068        VimCommand::new(("br", "ewind"), workspace::ActivateItem(0)),
1069        VimCommand::new(("bl", "ast"), workspace::ActivateLastItem),
1070        VimCommand::str(("buffers", ""), "tab_switcher::ToggleAll"),
1071        VimCommand::str(("ls", ""), "tab_switcher::ToggleAll"),
1072        VimCommand::new(("new", ""), workspace::NewFileSplitHorizontal),
1073        VimCommand::new(("vne", "w"), workspace::NewFileSplitVertical),
1074        VimCommand::new(("tabe", "dit"), workspace::NewFile),
1075        VimCommand::new(("tabnew", ""), workspace::NewFile),
1076        VimCommand::new(("tabn", "ext"), workspace::ActivateNextItem).count(),
1077        VimCommand::new(("tabp", "revious"), workspace::ActivatePreviousItem).count(),
1078        VimCommand::new(("tabN", "ext"), workspace::ActivatePreviousItem).count(),
1079        VimCommand::new(
1080            ("tabc", "lose"),
1081            workspace::CloseActiveItem {
1082                save_intent: Some(SaveIntent::Close),
1083                close_pinned: false,
1084            },
1085        ),
1086        VimCommand::new(
1087            ("tabo", "nly"),
1088            workspace::CloseOtherItems {
1089                save_intent: Some(SaveIntent::Close),
1090                close_pinned: false,
1091            },
1092        )
1093        .bang(workspace::CloseOtherItems {
1094            save_intent: Some(SaveIntent::Skip),
1095            close_pinned: false,
1096        }),
1097        VimCommand::new(
1098            ("on", "ly"),
1099            workspace::CloseInactiveTabsAndPanes {
1100                save_intent: Some(SaveIntent::Close),
1101            },
1102        )
1103        .bang(workspace::CloseInactiveTabsAndPanes {
1104            save_intent: Some(SaveIntent::Skip),
1105        }),
1106        VimCommand::str(("cl", "ist"), "diagnostics::Deploy"),
1107        VimCommand::new(("cc", ""), editor::actions::Hover),
1108        VimCommand::new(("ll", ""), editor::actions::Hover),
1109        VimCommand::new(("cn", "ext"), editor::actions::GoToDiagnostic::default())
1110            .range(wrap_count),
1111        VimCommand::new(
1112            ("cp", "revious"),
1113            editor::actions::GoToPreviousDiagnostic::default(),
1114        )
1115        .range(wrap_count),
1116        VimCommand::new(
1117            ("cN", "ext"),
1118            editor::actions::GoToPreviousDiagnostic::default(),
1119        )
1120        .range(wrap_count),
1121        VimCommand::new(
1122            ("lp", "revious"),
1123            editor::actions::GoToPreviousDiagnostic::default(),
1124        )
1125        .range(wrap_count),
1126        VimCommand::new(
1127            ("lN", "ext"),
1128            editor::actions::GoToPreviousDiagnostic::default(),
1129        )
1130        .range(wrap_count),
1131        VimCommand::new(("j", "oin"), JoinLines).range(select_range),
1132        VimCommand::new(("fo", "ld"), editor::actions::FoldSelectedRanges).range(act_on_range),
1133        VimCommand::new(("foldo", "pen"), editor::actions::UnfoldLines)
1134            .bang(editor::actions::UnfoldRecursive)
1135            .range(act_on_range),
1136        VimCommand::new(("foldc", "lose"), editor::actions::Fold)
1137            .bang(editor::actions::FoldRecursive)
1138            .range(act_on_range),
1139        VimCommand::new(("dif", "fupdate"), editor::actions::ToggleSelectedDiffHunks)
1140            .range(act_on_range),
1141        VimCommand::str(("rev", "ert"), "git::Restore").range(act_on_range),
1142        VimCommand::new(("d", "elete"), VisualDeleteLine).range(select_range),
1143        VimCommand::new(("y", "ank"), gpui::NoAction).range(|_, range| {
1144            Some(
1145                YankCommand {
1146                    range: range.clone(),
1147                }
1148                .boxed_clone(),
1149            )
1150        }),
1151        VimCommand::new(("reg", "isters"), ToggleRegistersView).bang(ToggleRegistersView),
1152        VimCommand::new(("di", "splay"), ToggleRegistersView).bang(ToggleRegistersView),
1153        VimCommand::new(("marks", ""), ToggleMarksView).bang(ToggleMarksView),
1154        VimCommand::new(("delm", "arks"), ArgumentRequired)
1155            .bang(DeleteMarks::AllLocal)
1156            .args(|_, args| Some(DeleteMarks::Marks(args).boxed_clone())),
1157        VimCommand::new(("sor", "t"), SortLinesCaseSensitive).range(select_range),
1158        VimCommand::new(("sort i", ""), SortLinesCaseInsensitive).range(select_range),
1159        VimCommand::str(("E", "xplore"), "project_panel::ToggleFocus"),
1160        VimCommand::str(("H", "explore"), "project_panel::ToggleFocus"),
1161        VimCommand::str(("L", "explore"), "project_panel::ToggleFocus"),
1162        VimCommand::str(("S", "explore"), "project_panel::ToggleFocus"),
1163        VimCommand::str(("Ve", "xplore"), "project_panel::ToggleFocus"),
1164        VimCommand::str(("te", "rm"), "terminal_panel::ToggleFocus"),
1165        VimCommand::str(("T", "erm"), "terminal_panel::ToggleFocus"),
1166        VimCommand::str(("C", "ollab"), "collab_panel::ToggleFocus"),
1167        VimCommand::str(("Ch", "at"), "chat_panel::ToggleFocus"),
1168        VimCommand::str(("No", "tifications"), "notification_panel::ToggleFocus"),
1169        VimCommand::str(("A", "I"), "agent::ToggleFocus"),
1170        VimCommand::str(("G", "it"), "git_panel::ToggleFocus"),
1171        VimCommand::str(("D", "ebug"), "debug_panel::ToggleFocus"),
1172        VimCommand::new(("noh", "lsearch"), search::buffer_search::Dismiss),
1173        VimCommand::new(("$", ""), EndOfDocument),
1174        VimCommand::new(("%", ""), EndOfDocument),
1175        VimCommand::new(("0", ""), StartOfDocument),
1176        VimCommand::new(("e", "dit"), editor::actions::ReloadFile)
1177            .bang(editor::actions::ReloadFile)
1178            .args(|_, args| Some(VimEdit { filename: args }.boxed_clone())),
1179        VimCommand::new(("ex", ""), editor::actions::ReloadFile).bang(editor::actions::ReloadFile),
1180        VimCommand::new(("cpp", "link"), editor::actions::CopyPermalinkToLine).range(act_on_range),
1181        VimCommand::str(("opt", "ions"), "zed::OpenDefaultSettings"),
1182        VimCommand::str(("map", ""), "vim::OpenDefaultKeymap"),
1183        VimCommand::new(("h", "elp"), OpenDocs),
1184    ]
1185}
1186
1187struct VimCommands(Vec<VimCommand>);
1188// safety: we only ever access this from the main thread (as ensured by the cx argument)
1189// actions are not Sync so we can't otherwise use a OnceLock.
1190unsafe impl Sync for VimCommands {}
1191impl Global for VimCommands {}
1192
1193fn commands(cx: &App) -> &Vec<VimCommand> {
1194    static COMMANDS: OnceLock<VimCommands> = OnceLock::new();
1195    &COMMANDS
1196        .get_or_init(|| VimCommands(generate_commands(cx)))
1197        .0
1198}
1199
1200fn act_on_range(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
1201    Some(
1202        WithRange {
1203            restore_selection: true,
1204            range: range.clone(),
1205            action: WrappedAction(action),
1206        }
1207        .boxed_clone(),
1208    )
1209}
1210
1211fn select_range(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
1212    Some(
1213        WithRange {
1214            restore_selection: false,
1215            range: range.clone(),
1216            action: WrappedAction(action),
1217        }
1218        .boxed_clone(),
1219    )
1220}
1221
1222fn wrap_count(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
1223    range.as_count().map(|count| {
1224        WithCount {
1225            count,
1226            action: WrappedAction(action),
1227        }
1228        .boxed_clone()
1229    })
1230}
1231
1232pub fn command_interceptor(mut input: &str, cx: &App) -> Vec<CommandInterceptResult> {
1233    // NOTE: We also need to support passing arguments to commands like :w
1234    // (ideally with filename autocompletion).
1235    while input.starts_with(':') {
1236        input = &input[1..];
1237    }
1238
1239    let (range, query) = VimCommand::parse_range(input);
1240    let range_prefix = input[0..(input.len() - query.len())].to_string();
1241    let query = query.as_str().trim();
1242
1243    let action = if range.is_some() && query.is_empty() {
1244        Some(
1245            GoToLine {
1246                range: range.clone().unwrap(),
1247            }
1248            .boxed_clone(),
1249        )
1250    } else if query.starts_with('/') || query.starts_with('?') {
1251        Some(
1252            FindCommand {
1253                query: query[1..].to_string(),
1254                backwards: query.starts_with('?'),
1255            }
1256            .boxed_clone(),
1257        )
1258    } else if query.starts_with("se ") || query.starts_with("set ") {
1259        let (prefix, option) = query.split_once(' ').unwrap();
1260        let mut commands = VimOption::possible_commands(option);
1261        if !commands.is_empty() {
1262            let query = prefix.to_string() + " " + option;
1263            for command in &mut commands {
1264                command.positions = generate_positions(&command.string, &query);
1265            }
1266        }
1267        return commands;
1268    } else if query.starts_with('s') {
1269        let mut substitute = "substitute".chars().peekable();
1270        let mut query = query.chars().peekable();
1271        while substitute
1272            .peek()
1273            .is_some_and(|char| Some(char) == query.peek())
1274        {
1275            substitute.next();
1276            query.next();
1277        }
1278        if let Some(replacement) = Replacement::parse(query) {
1279            let range = range.clone().unwrap_or(CommandRange {
1280                start: Position::CurrentLine { offset: 0 },
1281                end: None,
1282            });
1283            Some(ReplaceCommand { replacement, range }.boxed_clone())
1284        } else {
1285            None
1286        }
1287    } else if query.starts_with('g') || query.starts_with('v') {
1288        let mut global = "global".chars().peekable();
1289        let mut query = query.chars().peekable();
1290        let mut invert = false;
1291        if query.peek() == Some(&'v') {
1292            invert = true;
1293            query.next();
1294        }
1295        while global.peek().is_some_and(|char| Some(char) == query.peek()) {
1296            global.next();
1297            query.next();
1298        }
1299        if !invert && query.peek() == Some(&'!') {
1300            invert = true;
1301            query.next();
1302        }
1303        let range = range.clone().unwrap_or(CommandRange {
1304            start: Position::Line { row: 0, offset: 0 },
1305            end: Some(Position::LastLine { offset: 0 }),
1306        });
1307        if let Some(action) = OnMatchingLines::parse(query, invert, range, cx) {
1308            Some(action.boxed_clone())
1309        } else {
1310            None
1311        }
1312    } else if query.contains('!') {
1313        ShellExec::parse(query, range.clone())
1314    } else {
1315        None
1316    };
1317    if let Some(action) = action {
1318        let string = input.to_string();
1319        let positions = generate_positions(&string, &(range_prefix + query));
1320        return vec![CommandInterceptResult {
1321            action,
1322            string,
1323            positions,
1324        }];
1325    }
1326
1327    for command in commands(cx).iter() {
1328        if let Some(action) = command.parse(query, &range, cx) {
1329            let mut string = ":".to_owned() + &range_prefix + command.prefix + command.suffix;
1330            if query.contains('!') {
1331                string.push('!');
1332            }
1333            let positions = generate_positions(&string, &(range_prefix + query));
1334
1335            return vec![CommandInterceptResult {
1336                action,
1337                string,
1338                positions,
1339            }];
1340        }
1341    }
1342    return Vec::default();
1343}
1344
1345fn generate_positions(string: &str, query: &str) -> Vec<usize> {
1346    let mut positions = Vec::new();
1347    let mut chars = query.chars();
1348
1349    let Some(mut current) = chars.next() else {
1350        return positions;
1351    };
1352
1353    for (i, c) in string.char_indices() {
1354        if c == current {
1355            positions.push(i);
1356            if let Some(c) = chars.next() {
1357                current = c;
1358            } else {
1359                break;
1360            }
1361        }
1362    }
1363
1364    positions
1365}
1366
1367/// Applies a command to all lines matching a pattern.
1368#[derive(Debug, PartialEq, Clone, Action)]
1369#[action(namespace = vim, no_json, no_register)]
1370pub(crate) struct OnMatchingLines {
1371    range: CommandRange,
1372    search: String,
1373    action: WrappedAction,
1374    invert: bool,
1375}
1376
1377impl OnMatchingLines {
1378    // convert a vim query into something more usable by zed.
1379    // we don't attempt to fully convert between the two regex syntaxes,
1380    // but we do flip \( and \) to ( and ) (and vice-versa) in the pattern,
1381    // and convert \0..\9 to $0..$9 in the replacement so that common idioms work.
1382    pub(crate) fn parse(
1383        mut chars: Peekable<Chars>,
1384        invert: bool,
1385        range: CommandRange,
1386        cx: &App,
1387    ) -> Option<Self> {
1388        let delimiter = chars.next().filter(|c| {
1389            !c.is_alphanumeric() && *c != '"' && *c != '|' && *c != '\'' && *c != '!'
1390        })?;
1391
1392        let mut search = String::new();
1393        let mut escaped = false;
1394
1395        while let Some(c) = chars.next() {
1396            if escaped {
1397                escaped = false;
1398                // unescape escaped parens
1399                if c != '(' && c != ')' && c != delimiter {
1400                    search.push('\\')
1401                }
1402                search.push(c)
1403            } else if c == '\\' {
1404                escaped = true;
1405            } else if c == delimiter {
1406                break;
1407            } else {
1408                // escape unescaped parens
1409                if c == '(' || c == ')' {
1410                    search.push('\\')
1411                }
1412                search.push(c)
1413            }
1414        }
1415
1416        let command: String = chars.collect();
1417
1418        let action = WrappedAction(
1419            command_interceptor(&command, cx)
1420                .first()?
1421                .action
1422                .boxed_clone(),
1423        );
1424
1425        Some(Self {
1426            range,
1427            search,
1428            invert,
1429            action,
1430        })
1431    }
1432
1433    pub fn run(&self, vim: &mut Vim, window: &mut Window, cx: &mut Context<Vim>) {
1434        let result = vim.update_editor(window, cx, |vim, editor, window, cx| {
1435            self.range.buffer_range(vim, editor, window, cx)
1436        });
1437
1438        let range = match result {
1439            None => return,
1440            Some(e @ Err(_)) => {
1441                let Some(workspace) = vim.workspace(window) else {
1442                    return;
1443                };
1444                workspace.update(cx, |workspace, cx| {
1445                    e.notify_err(workspace, cx);
1446                });
1447                return;
1448            }
1449            Some(Ok(result)) => result,
1450        };
1451
1452        let mut action = self.action.boxed_clone();
1453        let mut last_pattern = self.search.clone();
1454
1455        let mut regexes = match Regex::new(&self.search) {
1456            Ok(regex) => vec![(regex, !self.invert)],
1457            e @ Err(_) => {
1458                let Some(workspace) = vim.workspace(window) else {
1459                    return;
1460                };
1461                workspace.update(cx, |workspace, cx| {
1462                    e.notify_err(workspace, cx);
1463                });
1464                return;
1465            }
1466        };
1467        while let Some(inner) = action
1468            .boxed_clone()
1469            .as_any()
1470            .downcast_ref::<OnMatchingLines>()
1471        {
1472            let Some(regex) = Regex::new(&inner.search).ok() else {
1473                break;
1474            };
1475            last_pattern = inner.search.clone();
1476            action = inner.action.boxed_clone();
1477            regexes.push((regex, !inner.invert))
1478        }
1479
1480        if let Some(pane) = vim.pane(window, cx) {
1481            pane.update(cx, |pane, cx| {
1482                if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()
1483                {
1484                    search_bar.update(cx, |search_bar, cx| {
1485                        if search_bar.show(window, cx) {
1486                            let _ = search_bar.search(
1487                                &last_pattern,
1488                                Some(SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE),
1489                                window,
1490                                cx,
1491                            );
1492                        }
1493                    });
1494                }
1495            });
1496        };
1497
1498        vim.update_editor(window, cx, |_, editor, window, cx| {
1499            let snapshot = editor.snapshot(window, cx);
1500            let mut row = range.start.0;
1501
1502            let point_range = Point::new(range.start.0, 0)
1503                ..snapshot
1504                    .buffer_snapshot
1505                    .clip_point(Point::new(range.end.0 + 1, 0), Bias::Left);
1506            cx.spawn_in(window, async move |editor, cx| {
1507                let new_selections = cx
1508                    .background_spawn(async move {
1509                        let mut line = String::new();
1510                        let mut new_selections = Vec::new();
1511                        let chunks = snapshot
1512                            .buffer_snapshot
1513                            .text_for_range(point_range)
1514                            .chain(["\n"]);
1515
1516                        for chunk in chunks {
1517                            for (newline_ix, text) in chunk.split('\n').enumerate() {
1518                                if newline_ix > 0 {
1519                                    if regexes.iter().all(|(regex, should_match)| {
1520                                        regex.is_match(&line) == *should_match
1521                                    }) {
1522                                        new_selections
1523                                            .push(Point::new(row, 0).to_display_point(&snapshot))
1524                                    }
1525                                    row += 1;
1526                                    line.clear();
1527                                }
1528                                line.push_str(text)
1529                            }
1530                        }
1531
1532                        new_selections
1533                    })
1534                    .await;
1535
1536                if new_selections.is_empty() {
1537                    return;
1538                }
1539                editor
1540                    .update_in(cx, |editor, window, cx| {
1541                        editor.start_transaction_at(Instant::now(), window, cx);
1542                        editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1543                            s.replace_cursors_with(|_| new_selections);
1544                        });
1545                        window.dispatch_action(action, cx);
1546                        cx.defer_in(window, move |editor, window, cx| {
1547                            let newest = editor.selections.newest::<Point>(cx).clone();
1548                            editor.change_selections(
1549                                SelectionEffects::no_scroll(),
1550                                window,
1551                                cx,
1552                                |s| {
1553                                    s.select(vec![newest]);
1554                                },
1555                            );
1556                            editor.end_transaction_at(Instant::now(), cx);
1557                        })
1558                    })
1559                    .ok();
1560            })
1561            .detach();
1562        });
1563    }
1564}
1565
1566/// Executes a shell command and returns the output.
1567#[derive(Clone, Debug, PartialEq, Action)]
1568#[action(namespace = vim, no_json, no_register)]
1569pub struct ShellExec {
1570    command: String,
1571    range: Option<CommandRange>,
1572    is_read: bool,
1573}
1574
1575impl Vim {
1576    pub fn cancel_running_command(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1577        if self.running_command.take().is_some() {
1578            self.update_editor(window, cx, |_, editor, window, cx| {
1579                editor.transact(window, cx, |editor, _window, _cx| {
1580                    editor.clear_row_highlights::<ShellExec>();
1581                })
1582            });
1583        }
1584    }
1585
1586    fn prepare_shell_command(
1587        &mut self,
1588        command: &str,
1589        window: &mut Window,
1590        cx: &mut Context<Self>,
1591    ) -> String {
1592        let mut ret = String::new();
1593        // N.B. non-standard escaping rules:
1594        // * !echo % => "echo README.md"
1595        // * !echo \% => "echo %"
1596        // * !echo \\% => echo \%
1597        // * !echo \\\% => echo \\%
1598        for c in command.chars() {
1599            if c != '%' && c != '!' {
1600                ret.push(c);
1601                continue;
1602            } else if ret.chars().last() == Some('\\') {
1603                ret.pop();
1604                ret.push(c);
1605                continue;
1606            }
1607            match c {
1608                '%' => {
1609                    self.update_editor(window, cx, |_, editor, _window, cx| {
1610                        if let Some((_, buffer, _)) = editor.active_excerpt(cx) {
1611                            if let Some(file) = buffer.read(cx).file() {
1612                                if let Some(local) = file.as_local() {
1613                                    if let Some(str) = local.path().to_str() {
1614                                        ret.push_str(str)
1615                                    }
1616                                }
1617                            }
1618                        }
1619                    });
1620                }
1621                '!' => {
1622                    if let Some(command) = &self.last_command {
1623                        ret.push_str(command)
1624                    }
1625                }
1626                _ => {}
1627            }
1628        }
1629        self.last_command = Some(ret.clone());
1630        ret
1631    }
1632
1633    pub fn shell_command_motion(
1634        &mut self,
1635        motion: Motion,
1636        times: Option<usize>,
1637        forced_motion: bool,
1638        window: &mut Window,
1639        cx: &mut Context<Vim>,
1640    ) {
1641        self.stop_recording(cx);
1642        let Some(workspace) = self.workspace(window) else {
1643            return;
1644        };
1645        let command = self.update_editor(window, cx, |_, editor, window, cx| {
1646            let snapshot = editor.snapshot(window, cx);
1647            let start = editor.selections.newest_display(cx);
1648            let text_layout_details = editor.text_layout_details(window);
1649            let (mut range, _) = motion
1650                .range(
1651                    &snapshot,
1652                    start.clone(),
1653                    times,
1654                    &text_layout_details,
1655                    forced_motion,
1656                )
1657                .unwrap_or((start.range(), MotionKind::Exclusive));
1658            if range.start != start.start {
1659                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1660                    s.select_ranges([
1661                        range.start.to_point(&snapshot)..range.start.to_point(&snapshot)
1662                    ]);
1663                })
1664            }
1665            if range.end.row() > range.start.row() && range.end.column() != 0 {
1666                *range.end.row_mut() -= 1
1667            }
1668            if range.end.row() == range.start.row() {
1669                ".!".to_string()
1670            } else {
1671                format!(".,.+{}!", (range.end.row() - range.start.row()).0)
1672            }
1673        });
1674        if let Some(command) = command {
1675            workspace.update(cx, |workspace, cx| {
1676                command_palette::CommandPalette::toggle(workspace, &command, window, cx);
1677            });
1678        }
1679    }
1680
1681    pub fn shell_command_object(
1682        &mut self,
1683        object: Object,
1684        around: bool,
1685        window: &mut Window,
1686        cx: &mut Context<Vim>,
1687    ) {
1688        self.stop_recording(cx);
1689        let Some(workspace) = self.workspace(window) else {
1690            return;
1691        };
1692        let command = self.update_editor(window, cx, |_, editor, window, cx| {
1693            let snapshot = editor.snapshot(window, cx);
1694            let start = editor.selections.newest_display(cx);
1695            let range = object
1696                .range(&snapshot, start.clone(), around, None)
1697                .unwrap_or(start.range());
1698            if range.start != start.start {
1699                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1700                    s.select_ranges([
1701                        range.start.to_point(&snapshot)..range.start.to_point(&snapshot)
1702                    ]);
1703                })
1704            }
1705            if range.end.row() == range.start.row() {
1706                ".!".to_string()
1707            } else {
1708                format!(".,.+{}!", (range.end.row() - range.start.row()).0)
1709            }
1710        });
1711        if let Some(command) = command {
1712            workspace.update(cx, |workspace, cx| {
1713                command_palette::CommandPalette::toggle(workspace, &command, window, cx);
1714            });
1715        }
1716    }
1717}
1718
1719impl ShellExec {
1720    pub fn parse(query: &str, range: Option<CommandRange>) -> Option<Box<dyn Action>> {
1721        let (before, after) = query.split_once('!')?;
1722        let before = before.trim();
1723
1724        if !"read".starts_with(before) {
1725            return None;
1726        }
1727
1728        Some(
1729            ShellExec {
1730                command: after.trim().to_string(),
1731                range,
1732                is_read: !before.is_empty(),
1733            }
1734            .boxed_clone(),
1735        )
1736    }
1737
1738    pub fn run(&self, vim: &mut Vim, window: &mut Window, cx: &mut Context<Vim>) {
1739        let Some(workspace) = vim.workspace(window) else {
1740            return;
1741        };
1742
1743        let project = workspace.read(cx).project().clone();
1744        let command = vim.prepare_shell_command(&self.command, window, cx);
1745
1746        if self.range.is_none() && !self.is_read {
1747            workspace.update(cx, |workspace, cx| {
1748                let project = workspace.project().read(cx);
1749                let cwd = project.first_project_directory(cx);
1750                let shell = project.terminal_settings(&cwd, cx).shell.clone();
1751
1752                let spawn_in_terminal = SpawnInTerminal {
1753                    id: TaskId("vim".to_string()),
1754                    full_label: command.clone(),
1755                    label: command.clone(),
1756                    command: Some(command.clone()),
1757                    args: Vec::new(),
1758                    command_label: command.clone(),
1759                    cwd,
1760                    env: HashMap::default(),
1761                    use_new_terminal: true,
1762                    allow_concurrent_runs: true,
1763                    reveal: RevealStrategy::NoFocus,
1764                    reveal_target: RevealTarget::Dock,
1765                    hide: HideStrategy::Never,
1766                    shell,
1767                    show_summary: false,
1768                    show_command: false,
1769                    show_rerun: false,
1770                };
1771
1772                let task_status = workspace.spawn_in_terminal(spawn_in_terminal, window, cx);
1773                cx.background_spawn(async move {
1774                    match task_status.await {
1775                        Some(Ok(status)) => {
1776                            if status.success() {
1777                                log::debug!("Vim shell exec succeeded");
1778                            } else {
1779                                log::debug!("Vim shell exec failed, code: {:?}", status.code());
1780                            }
1781                        }
1782                        Some(Err(e)) => log::error!("Vim shell exec failed: {e}"),
1783                        None => log::debug!("Vim shell exec got cancelled"),
1784                    }
1785                })
1786                .detach();
1787            });
1788            return;
1789        };
1790
1791        let mut input_snapshot = None;
1792        let mut input_range = None;
1793        let mut needs_newline_prefix = false;
1794        vim.update_editor(window, cx, |vim, editor, window, cx| {
1795            let snapshot = editor.buffer().read(cx).snapshot(cx);
1796            let range = if let Some(range) = self.range.clone() {
1797                let Some(range) = range.buffer_range(vim, editor, window, cx).log_err() else {
1798                    return;
1799                };
1800                Point::new(range.start.0, 0)
1801                    ..snapshot.clip_point(Point::new(range.end.0 + 1, 0), Bias::Right)
1802            } else {
1803                let mut end = editor.selections.newest::<Point>(cx).range().end;
1804                end = snapshot.clip_point(Point::new(end.row + 1, 0), Bias::Right);
1805                needs_newline_prefix = end == snapshot.max_point();
1806                end..end
1807            };
1808            if self.is_read {
1809                input_range =
1810                    Some(snapshot.anchor_after(range.end)..snapshot.anchor_after(range.end));
1811            } else {
1812                input_range =
1813                    Some(snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end));
1814            }
1815            editor.highlight_rows::<ShellExec>(
1816                input_range.clone().unwrap(),
1817                cx.theme().status().unreachable_background,
1818                Default::default(),
1819                cx,
1820            );
1821
1822            if !self.is_read {
1823                input_snapshot = Some(snapshot)
1824            }
1825        });
1826
1827        let Some(range) = input_range else { return };
1828
1829        let mut process = project.read(cx).exec_in_shell(command, cx);
1830        process.stdout(Stdio::piped());
1831        process.stderr(Stdio::piped());
1832
1833        if input_snapshot.is_some() {
1834            process.stdin(Stdio::piped());
1835        } else {
1836            process.stdin(Stdio::null());
1837        };
1838
1839        util::set_pre_exec_to_start_new_session(&mut process);
1840        let is_read = self.is_read;
1841
1842        let task = cx.spawn_in(window, async move |vim, cx| {
1843            let Some(mut running) = process.spawn().log_err() else {
1844                vim.update_in(cx, |vim, window, cx| {
1845                    vim.cancel_running_command(window, cx);
1846                })
1847                .log_err();
1848                return;
1849            };
1850
1851            if let Some(mut stdin) = running.stdin.take() {
1852                if let Some(snapshot) = input_snapshot {
1853                    let range = range.clone();
1854                    cx.background_spawn(async move {
1855                        for chunk in snapshot.text_for_range(range) {
1856                            if stdin.write_all(chunk.as_bytes()).log_err().is_none() {
1857                                return;
1858                            }
1859                        }
1860                        stdin.flush().log_err();
1861                    })
1862                    .detach();
1863                }
1864            };
1865
1866            let output = cx
1867                .background_spawn(async move { running.wait_with_output() })
1868                .await;
1869
1870            let Some(output) = output.log_err() else {
1871                vim.update_in(cx, |vim, window, cx| {
1872                    vim.cancel_running_command(window, cx);
1873                })
1874                .log_err();
1875                return;
1876            };
1877            let mut text = String::new();
1878            if needs_newline_prefix {
1879                text.push('\n');
1880            }
1881            text.push_str(&String::from_utf8_lossy(&output.stdout));
1882            text.push_str(&String::from_utf8_lossy(&output.stderr));
1883            if !text.is_empty() && text.chars().last() != Some('\n') {
1884                text.push('\n');
1885            }
1886
1887            vim.update_in(cx, |vim, window, cx| {
1888                vim.update_editor(window, cx, |_, editor, window, cx| {
1889                    editor.transact(window, cx, |editor, window, cx| {
1890                        editor.edit([(range.clone(), text)], cx);
1891                        let snapshot = editor.buffer().read(cx).snapshot(cx);
1892                        editor.change_selections(Default::default(), window, cx, |s| {
1893                            let point = if is_read {
1894                                let point = range.end.to_point(&snapshot);
1895                                Point::new(point.row.saturating_sub(1), 0)
1896                            } else {
1897                                let point = range.start.to_point(&snapshot);
1898                                Point::new(point.row, 0)
1899                            };
1900                            s.select_ranges([point..point]);
1901                        })
1902                    })
1903                });
1904                vim.cancel_running_command(window, cx);
1905            })
1906            .log_err();
1907        });
1908        vim.running_command.replace(task);
1909    }
1910}
1911
1912#[cfg(test)]
1913mod test {
1914    use std::path::Path;
1915
1916    use crate::{
1917        VimAddon,
1918        state::Mode,
1919        test::{NeovimBackedTestContext, VimTestContext},
1920    };
1921    use editor::Editor;
1922    use gpui::{Context, TestAppContext};
1923    use indoc::indoc;
1924    use util::path;
1925    use workspace::Workspace;
1926
1927    #[gpui::test]
1928    async fn test_command_basics(cx: &mut TestAppContext) {
1929        let mut cx = NeovimBackedTestContext::new(cx).await;
1930
1931        cx.set_shared_state(indoc! {"
1932            ˇa
1933            b
1934            c"})
1935            .await;
1936
1937        cx.simulate_shared_keystrokes(": j enter").await;
1938
1939        // hack: our cursor positioning after a join command is wrong
1940        cx.simulate_shared_keystrokes("^").await;
1941        cx.shared_state().await.assert_eq(indoc! {
1942            "ˇa b
1943            c"
1944        });
1945    }
1946
1947    #[gpui::test]
1948    async fn test_command_goto(cx: &mut TestAppContext) {
1949        let mut cx = NeovimBackedTestContext::new(cx).await;
1950
1951        cx.set_shared_state(indoc! {"
1952            ˇa
1953            b
1954            c"})
1955            .await;
1956        cx.simulate_shared_keystrokes(": 3 enter").await;
1957        cx.shared_state().await.assert_eq(indoc! {"
1958            a
1959            b
1960            ˇc"});
1961    }
1962
1963    #[gpui::test]
1964    async fn test_command_replace(cx: &mut TestAppContext) {
1965        let mut cx = NeovimBackedTestContext::new(cx).await;
1966
1967        cx.set_shared_state(indoc! {"
1968            ˇa
1969            b
1970            b
1971            c"})
1972            .await;
1973        cx.simulate_shared_keystrokes(": % s / b / d enter").await;
1974        cx.shared_state().await.assert_eq(indoc! {"
1975            a
1976            d
1977            ˇd
1978            c"});
1979        cx.simulate_shared_keystrokes(": % s : . : \\ 0 \\ 0 enter")
1980            .await;
1981        cx.shared_state().await.assert_eq(indoc! {"
1982            aa
1983            dd
1984            dd
1985            ˇcc"});
1986        cx.simulate_shared_keystrokes("k : s / d d / e e enter")
1987            .await;
1988        cx.shared_state().await.assert_eq(indoc! {"
1989            aa
1990            dd
1991            ˇee
1992            cc"});
1993    }
1994
1995    #[gpui::test]
1996    async fn test_command_search(cx: &mut TestAppContext) {
1997        let mut cx = NeovimBackedTestContext::new(cx).await;
1998
1999        cx.set_shared_state(indoc! {"
2000                ˇa
2001                b
2002                a
2003                c"})
2004            .await;
2005        cx.simulate_shared_keystrokes(": / b enter").await;
2006        cx.shared_state().await.assert_eq(indoc! {"
2007                a
2008                ˇb
2009                a
2010                c"});
2011        cx.simulate_shared_keystrokes(": ? a enter").await;
2012        cx.shared_state().await.assert_eq(indoc! {"
2013                ˇa
2014                b
2015                a
2016                c"});
2017    }
2018
2019    #[gpui::test]
2020    async fn test_command_write(cx: &mut TestAppContext) {
2021        let mut cx = VimTestContext::new(cx, true).await;
2022        let path = Path::new(path!("/root/dir/file.rs"));
2023        let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
2024
2025        cx.simulate_keystrokes("i @ escape");
2026        cx.simulate_keystrokes(": w enter");
2027
2028        assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "@\n");
2029
2030        fs.as_fake().insert_file(path, b"oops\n".to_vec()).await;
2031
2032        // conflict!
2033        cx.simulate_keystrokes("i @ escape");
2034        cx.simulate_keystrokes(": w enter");
2035        cx.simulate_prompt_answer("Cancel");
2036
2037        assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "oops\n");
2038        assert!(!cx.has_pending_prompt());
2039        cx.simulate_keystrokes(": w ! enter");
2040        assert!(!cx.has_pending_prompt());
2041        assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "@@\n");
2042    }
2043
2044    #[gpui::test]
2045    async fn test_command_quit(cx: &mut TestAppContext) {
2046        let mut cx = VimTestContext::new(cx, true).await;
2047
2048        cx.simulate_keystrokes(": n e w enter");
2049        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
2050        cx.simulate_keystrokes(": q enter");
2051        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 1));
2052        cx.simulate_keystrokes(": n e w enter");
2053        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
2054        cx.simulate_keystrokes(": q a enter");
2055        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 0));
2056    }
2057
2058    #[gpui::test]
2059    async fn test_offsets(cx: &mut TestAppContext) {
2060        let mut cx = NeovimBackedTestContext::new(cx).await;
2061
2062        cx.set_shared_state("ˇ1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n")
2063            .await;
2064
2065        cx.simulate_shared_keystrokes(": + enter").await;
2066        cx.shared_state()
2067            .await
2068            .assert_eq("1\nˇ2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n");
2069
2070        cx.simulate_shared_keystrokes(": 1 0 - enter").await;
2071        cx.shared_state()
2072            .await
2073            .assert_eq("1\n2\n3\n4\n5\n6\n7\n8\nˇ9\n10\n11\n");
2074
2075        cx.simulate_shared_keystrokes(": . - 2 enter").await;
2076        cx.shared_state()
2077            .await
2078            .assert_eq("1\n2\n3\n4\n5\n6\nˇ7\n8\n9\n10\n11\n");
2079
2080        cx.simulate_shared_keystrokes(": % enter").await;
2081        cx.shared_state()
2082            .await
2083            .assert_eq("1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\nˇ");
2084    }
2085
2086    #[gpui::test]
2087    async fn test_command_ranges(cx: &mut TestAppContext) {
2088        let mut cx = NeovimBackedTestContext::new(cx).await;
2089
2090        cx.set_shared_state("ˇ1\n2\n3\n4\n4\n3\n2\n1").await;
2091
2092        cx.simulate_shared_keystrokes(": 2 , 4 d enter").await;
2093        cx.shared_state().await.assert_eq("1\nˇ4\n3\n2\n1");
2094
2095        cx.simulate_shared_keystrokes(": 2 , 4 s o r t enter").await;
2096        cx.shared_state().await.assert_eq("1\nˇ2\n3\n4\n1");
2097
2098        cx.simulate_shared_keystrokes(": 2 , 4 j o i n enter").await;
2099        cx.shared_state().await.assert_eq("1\nˇ2 3 4\n1");
2100    }
2101
2102    #[gpui::test]
2103    async fn test_command_visual_replace(cx: &mut TestAppContext) {
2104        let mut cx = NeovimBackedTestContext::new(cx).await;
2105
2106        cx.set_shared_state("ˇ1\n2\n3\n4\n4\n3\n2\n1").await;
2107
2108        cx.simulate_shared_keystrokes("v 2 j : s / . / k enter")
2109            .await;
2110        cx.shared_state().await.assert_eq("k\nk\nˇk\n4\n4\n3\n2\n1");
2111    }
2112
2113    #[track_caller]
2114    fn assert_active_item(
2115        workspace: &mut Workspace,
2116        expected_path: &str,
2117        expected_text: &str,
2118        cx: &mut Context<Workspace>,
2119    ) {
2120        let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();
2121
2122        let buffer = active_editor
2123            .read(cx)
2124            .buffer()
2125            .read(cx)
2126            .as_singleton()
2127            .unwrap();
2128
2129        let text = buffer.read(cx).text();
2130        let file = buffer.read(cx).file().unwrap();
2131        let file_path = file.as_local().unwrap().abs_path(cx);
2132
2133        assert_eq!(text, expected_text);
2134        assert_eq!(file_path, Path::new(expected_path));
2135    }
2136
2137    #[gpui::test]
2138    async fn test_command_gf(cx: &mut TestAppContext) {
2139        let mut cx = VimTestContext::new(cx, true).await;
2140
2141        // Assert base state, that we're in /root/dir/file.rs
2142        cx.workspace(|workspace, _, cx| {
2143            assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
2144        });
2145
2146        // Insert a new file
2147        let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
2148        fs.as_fake()
2149            .insert_file(
2150                path!("/root/dir/file2.rs"),
2151                "This is file2.rs".as_bytes().to_vec(),
2152            )
2153            .await;
2154        fs.as_fake()
2155            .insert_file(
2156                path!("/root/dir/file3.rs"),
2157                "go to file3".as_bytes().to_vec(),
2158            )
2159            .await;
2160
2161        // Put the path to the second file into the currently open buffer
2162        cx.set_state(indoc! {"go to fiˇle2.rs"}, Mode::Normal);
2163
2164        // Go to file2.rs
2165        cx.simulate_keystrokes("g f");
2166
2167        // We now have two items
2168        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
2169        cx.workspace(|workspace, _, cx| {
2170            assert_active_item(
2171                workspace,
2172                path!("/root/dir/file2.rs"),
2173                "This is file2.rs",
2174                cx,
2175            );
2176        });
2177
2178        // Update editor to point to `file2.rs`
2179        cx.editor =
2180            cx.workspace(|workspace, _, cx| workspace.active_item_as::<Editor>(cx).unwrap());
2181
2182        // Put the path to the third file into the currently open buffer,
2183        // but remove its suffix, because we want that lookup to happen automatically.
2184        cx.set_state(indoc! {"go to fiˇle3"}, Mode::Normal);
2185
2186        // Go to file3.rs
2187        cx.simulate_keystrokes("g f");
2188
2189        // We now have three items
2190        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 3));
2191        cx.workspace(|workspace, _, cx| {
2192            assert_active_item(workspace, path!("/root/dir/file3.rs"), "go to file3", cx);
2193        });
2194    }
2195
2196    #[gpui::test]
2197    async fn test_w_command(cx: &mut TestAppContext) {
2198        let mut cx = VimTestContext::new(cx, true).await;
2199
2200        cx.workspace(|workspace, _, cx| {
2201            assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
2202        });
2203
2204        cx.simulate_keystrokes(": w space other.rs");
2205        cx.simulate_keystrokes("enter");
2206
2207        cx.workspace(|workspace, _, cx| {
2208            assert_active_item(workspace, path!("/root/other.rs"), "", cx);
2209        });
2210
2211        cx.simulate_keystrokes(": w space dir/file.rs");
2212        cx.simulate_keystrokes("enter");
2213
2214        cx.simulate_prompt_answer("Replace");
2215        cx.run_until_parked();
2216
2217        cx.workspace(|workspace, _, cx| {
2218            assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
2219        });
2220
2221        cx.simulate_keystrokes(": w ! space other.rs");
2222        cx.simulate_keystrokes("enter");
2223
2224        cx.workspace(|workspace, _, cx| {
2225            assert_active_item(workspace, path!("/root/other.rs"), "", cx);
2226        });
2227    }
2228
2229    #[gpui::test]
2230    async fn test_command_matching_lines(cx: &mut TestAppContext) {
2231        let mut cx = NeovimBackedTestContext::new(cx).await;
2232
2233        cx.set_shared_state(indoc! {"
2234            ˇa
2235            b
2236            a
2237            b
2238            a
2239        "})
2240            .await;
2241
2242        cx.simulate_shared_keystrokes(":").await;
2243        cx.simulate_shared_keystrokes("g / a / d").await;
2244        cx.simulate_shared_keystrokes("enter").await;
2245
2246        cx.shared_state().await.assert_eq(indoc! {"
2247            b
2248            b
2249            ˇ"});
2250
2251        cx.simulate_shared_keystrokes("u").await;
2252
2253        cx.shared_state().await.assert_eq(indoc! {"
2254            ˇa
2255            b
2256            a
2257            b
2258            a
2259        "});
2260
2261        cx.simulate_shared_keystrokes(":").await;
2262        cx.simulate_shared_keystrokes("v / a / d").await;
2263        cx.simulate_shared_keystrokes("enter").await;
2264
2265        cx.shared_state().await.assert_eq(indoc! {"
2266            a
2267            a
2268            ˇa"});
2269    }
2270
2271    #[gpui::test]
2272    async fn test_del_marks(cx: &mut TestAppContext) {
2273        let mut cx = NeovimBackedTestContext::new(cx).await;
2274
2275        cx.set_shared_state(indoc! {"
2276            ˇa
2277            b
2278            a
2279            b
2280            a
2281        "})
2282            .await;
2283
2284        cx.simulate_shared_keystrokes("m a").await;
2285
2286        let mark = cx.update_editor(|editor, window, cx| {
2287            let vim = editor.addon::<VimAddon>().unwrap().entity.clone();
2288            vim.update(cx, |vim, cx| vim.get_mark("a", editor, window, cx))
2289        });
2290        assert!(mark.is_some());
2291
2292        cx.simulate_shared_keystrokes(": d e l m space a").await;
2293        cx.simulate_shared_keystrokes("enter").await;
2294
2295        let mark = cx.update_editor(|editor, window, cx| {
2296            let vim = editor.addon::<VimAddon>().unwrap().entity.clone();
2297            vim.update(cx, |vim, cx| vim.get_mark("a", editor, window, cx))
2298        });
2299        assert!(mark.is_none())
2300    }
2301}