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