command.rs

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