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