command.rs

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