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