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