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