command.rs

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