command.rs

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