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