command.rs

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