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