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    /// Set argument handler. Trailing whitespace in arguments will be preserved.
1024    fn args(
1025        mut self,
1026        f: impl Fn(Box<dyn Action>, String) -> Option<Box<dyn Action>> + Send + Sync + 'static,
1027    ) -> Self {
1028        self.args = Some(Box::new(f));
1029        self
1030    }
1031
1032    /// Set argument handler. Trailing whitespace in arguments will be trimmed.
1033    /// Supports filename autocompletion.
1034    fn filename(
1035        mut self,
1036        f: impl Fn(Box<dyn Action>, String) -> Option<Box<dyn Action>> + Send + Sync + 'static,
1037    ) -> Self {
1038        self.args = Some(Box::new(f));
1039        self.has_filename = true;
1040        self
1041    }
1042
1043    fn range(
1044        mut self,
1045        f: impl Fn(Box<dyn Action>, &CommandRange) -> Option<Box<dyn Action>> + Send + Sync + 'static,
1046    ) -> Self {
1047        self.range = Some(Box::new(f));
1048        self
1049    }
1050
1051    fn default_range(mut self, range: CommandRange) -> Self {
1052        self.default_range = Some(range);
1053        self
1054    }
1055
1056    fn count(mut self) -> Self {
1057        self.has_count = true;
1058        self
1059    }
1060
1061    fn generate_filename_completions(
1062        parsed_query: &ParsedQuery,
1063        workspace: WeakEntity<Workspace>,
1064        cx: &mut App,
1065    ) -> Task<Vec<String>> {
1066        let ParsedQuery {
1067            args,
1068            has_bang: _,
1069            has_space: _,
1070        } = parsed_query;
1071        let Some(workspace) = workspace.upgrade() else {
1072            return Task::ready(Vec::new());
1073        };
1074
1075        let (task, args_path) = workspace.update(cx, |workspace, cx| {
1076            let prefix = workspace
1077                .project()
1078                .read(cx)
1079                .visible_worktrees(cx)
1080                .map(|worktree| worktree.read(cx).abs_path().to_path_buf())
1081                .next()
1082                .or_else(std::env::home_dir)
1083                .unwrap_or_else(|| PathBuf::from(""));
1084
1085            let rel_path = match RelPath::new(Path::new(&args), PathStyle::local()) {
1086                Ok(path) => path.to_rel_path_buf(),
1087                Err(_) => {
1088                    return (Task::ready(Ok(Vec::new())), RelPathBuf::new());
1089                }
1090            };
1091
1092            let rel_path = if args.ends_with(PathStyle::local().primary_separator()) {
1093                rel_path
1094            } else {
1095                rel_path
1096                    .parent()
1097                    .map(|rel_path| rel_path.to_rel_path_buf())
1098                    .unwrap_or(RelPathBuf::new())
1099            };
1100
1101            let task = workspace.project().update(cx, |project, cx| {
1102                let path = prefix
1103                    .join(rel_path.as_std_path())
1104                    .to_string_lossy()
1105                    .to_string();
1106                project.list_directory(path, cx)
1107            });
1108
1109            (task, rel_path)
1110        });
1111
1112        cx.background_spawn(async move {
1113            let directories = task.await.unwrap_or_default();
1114            directories
1115                .iter()
1116                .map(|dir| {
1117                    let path = RelPath::new(dir.path.as_path(), PathStyle::local())
1118                        .map(|cow| cow.into_owned())
1119                        .unwrap_or(RelPathBuf::new());
1120                    let mut path_string = args_path
1121                        .join(&path)
1122                        .display(PathStyle::local())
1123                        .to_string();
1124                    if dir.is_dir {
1125                        path_string.push_str(PathStyle::local().primary_separator());
1126                    }
1127                    path_string
1128                })
1129                .collect()
1130        })
1131    }
1132
1133    fn get_parsed_query(&self, query: String) -> Option<ParsedQuery> {
1134        let rest = query
1135            .strip_prefix(self.prefix)?
1136            .to_string()
1137            .chars()
1138            .zip_longest(self.suffix.to_string().chars())
1139            .skip_while(|e| e.clone().both().map(|(s, q)| s == q).unwrap_or(false))
1140            .filter_map(|e| e.left())
1141            .collect::<String>();
1142        let has_bang = rest.starts_with('!');
1143        let has_space = rest.starts_with("! ") || rest.starts_with(' ');
1144        let args = if has_bang {
1145            rest.strip_prefix('!')?.trim_start().to_string()
1146        } else if rest.is_empty() {
1147            "".into()
1148        } else {
1149            rest.strip_prefix(' ')?.trim_start().to_string()
1150        };
1151        Some(ParsedQuery {
1152            args,
1153            has_bang,
1154            has_space,
1155        })
1156    }
1157
1158    fn parse(
1159        &self,
1160        query: &str,
1161        range: &Option<CommandRange>,
1162        cx: &App,
1163    ) -> Option<Box<dyn Action>> {
1164        let ParsedQuery {
1165            args,
1166            has_bang,
1167            has_space: _,
1168        } = self.get_parsed_query(query.to_string())?;
1169        let action = if has_bang && self.bang_action.is_some() {
1170            self.bang_action.as_ref().unwrap().boxed_clone()
1171        } else if let Some(action) = self.action.as_ref() {
1172            action.boxed_clone()
1173        } else if let Some(action_name) = self.action_name {
1174            cx.build_action(action_name, None).log_err()?
1175        } else {
1176            return None;
1177        };
1178
1179        // If the command does not accept args and we have args, we should do no
1180        // action.
1181        let action = if args.is_empty() {
1182            action
1183        } else if self.has_filename {
1184            self.args.as_ref()?(action, args.trim().into())?
1185        } else {
1186            self.args.as_ref()?(action, args)?
1187        };
1188
1189        let range = range.as_ref().or(self.default_range.as_ref());
1190        if let Some(range) = range {
1191            self.range.as_ref().and_then(|f| f(action, range))
1192        } else {
1193            Some(action)
1194        }
1195    }
1196
1197    // TODO: ranges with search queries
1198    fn parse_range(query: &str) -> (Option<CommandRange>, String) {
1199        let mut chars = query.chars().peekable();
1200
1201        match chars.peek() {
1202            Some('%') => {
1203                chars.next();
1204                return (
1205                    Some(CommandRange {
1206                        start: Position::Line { row: 1, offset: 0 },
1207                        end: Some(Position::LastLine { offset: 0 }),
1208                    }),
1209                    chars.collect(),
1210                );
1211            }
1212            Some('*') => {
1213                chars.next();
1214                return (
1215                    Some(CommandRange {
1216                        start: Position::Mark {
1217                            name: '<',
1218                            offset: 0,
1219                        },
1220                        end: Some(Position::Mark {
1221                            name: '>',
1222                            offset: 0,
1223                        }),
1224                    }),
1225                    chars.collect(),
1226                );
1227            }
1228            _ => {}
1229        }
1230
1231        let start = Self::parse_position(&mut chars);
1232
1233        match chars.peek() {
1234            Some(',' | ';') => {
1235                chars.next();
1236                (
1237                    Some(CommandRange {
1238                        start: start.unwrap_or(Position::CurrentLine { offset: 0 }),
1239                        end: Self::parse_position(&mut chars),
1240                    }),
1241                    chars.collect(),
1242                )
1243            }
1244            _ => (
1245                start.map(|start| CommandRange { start, end: None }),
1246                chars.collect(),
1247            ),
1248        }
1249    }
1250
1251    fn parse_position(chars: &mut Peekable<Chars>) -> Option<Position> {
1252        match chars.peek()? {
1253            '0'..='9' => {
1254                let row = Self::parse_u32(chars);
1255                Some(Position::Line {
1256                    row,
1257                    offset: Self::parse_offset(chars),
1258                })
1259            }
1260            '\'' => {
1261                chars.next();
1262                let name = chars.next()?;
1263                Some(Position::Mark {
1264                    name,
1265                    offset: Self::parse_offset(chars),
1266                })
1267            }
1268            '.' => {
1269                chars.next();
1270                Some(Position::CurrentLine {
1271                    offset: Self::parse_offset(chars),
1272                })
1273            }
1274            '+' | '-' => Some(Position::CurrentLine {
1275                offset: Self::parse_offset(chars),
1276            }),
1277            '$' => {
1278                chars.next();
1279                Some(Position::LastLine {
1280                    offset: Self::parse_offset(chars),
1281                })
1282            }
1283            _ => None,
1284        }
1285    }
1286
1287    fn parse_offset(chars: &mut Peekable<Chars>) -> i32 {
1288        let mut res: i32 = 0;
1289        while matches!(chars.peek(), Some('+' | '-')) {
1290            let sign = if chars.next().unwrap() == '+' { 1 } else { -1 };
1291            let amount = if matches!(chars.peek(), Some('0'..='9')) {
1292                (Self::parse_u32(chars) as i32).saturating_mul(sign)
1293            } else {
1294                sign
1295            };
1296            res = res.saturating_add(amount)
1297        }
1298        res
1299    }
1300
1301    fn parse_u32(chars: &mut Peekable<Chars>) -> u32 {
1302        let mut res: u32 = 0;
1303        while matches!(chars.peek(), Some('0'..='9')) {
1304            res = res
1305                .saturating_mul(10)
1306                .saturating_add(chars.next().unwrap() as u32 - '0' as u32);
1307        }
1308        res
1309    }
1310}
1311
1312#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq)]
1313enum Position {
1314    Line { row: u32, offset: i32 },
1315    Mark { name: char, offset: i32 },
1316    LastLine { offset: i32 },
1317    CurrentLine { offset: i32 },
1318}
1319
1320impl Position {
1321    fn buffer_row(
1322        &self,
1323        vim: &Vim,
1324        editor: &mut Editor,
1325        window: &mut Window,
1326        cx: &mut App,
1327    ) -> Result<MultiBufferRow> {
1328        let snapshot = editor.snapshot(window, cx);
1329        let target = match self {
1330            Position::Line { row, offset } => {
1331                if let Some(anchor) = editor.active_excerpt(cx).and_then(|(_, buffer, _)| {
1332                    editor.buffer().read(cx).buffer_point_to_anchor(
1333                        &buffer,
1334                        Point::new(row.saturating_sub(1), 0),
1335                        cx,
1336                    )
1337                }) {
1338                    anchor
1339                        .to_point(&snapshot.buffer_snapshot())
1340                        .row
1341                        .saturating_add_signed(*offset)
1342                } else {
1343                    row.saturating_add_signed(offset.saturating_sub(1))
1344                }
1345            }
1346            Position::Mark { name, offset } => {
1347                let Some(Mark::Local(anchors)) =
1348                    vim.get_mark(&name.to_string(), editor, window, cx)
1349                else {
1350                    anyhow::bail!("mark {name} not set");
1351                };
1352                let Some(mark) = anchors.last() else {
1353                    anyhow::bail!("mark {name} contains empty anchors");
1354                };
1355                mark.to_point(&snapshot.buffer_snapshot())
1356                    .row
1357                    .saturating_add_signed(*offset)
1358            }
1359            Position::LastLine { offset } => snapshot
1360                .buffer_snapshot()
1361                .max_row()
1362                .0
1363                .saturating_add_signed(*offset),
1364            Position::CurrentLine { offset } => editor
1365                .selections
1366                .newest_anchor()
1367                .head()
1368                .to_point(&snapshot.buffer_snapshot())
1369                .row
1370                .saturating_add_signed(*offset),
1371        };
1372
1373        Ok(MultiBufferRow(target).min(snapshot.buffer_snapshot().max_row()))
1374    }
1375}
1376
1377#[derive(Clone, Debug, PartialEq)]
1378pub(crate) struct CommandRange {
1379    start: Position,
1380    end: Option<Position>,
1381}
1382
1383impl CommandRange {
1384    fn head(&self) -> &Position {
1385        self.end.as_ref().unwrap_or(&self.start)
1386    }
1387
1388    /// Convert the `CommandRange` into a `Range<MultiBufferRow>`.
1389    pub(crate) fn buffer_range(
1390        &self,
1391        vim: &Vim,
1392        editor: &mut Editor,
1393        window: &mut Window,
1394        cx: &mut App,
1395    ) -> Result<Range<MultiBufferRow>> {
1396        let start = self.start.buffer_row(vim, editor, window, cx)?;
1397        let end = if let Some(end) = self.end.as_ref() {
1398            end.buffer_row(vim, editor, window, cx)?
1399        } else {
1400            start
1401        };
1402        if end < start {
1403            anyhow::Ok(end..start)
1404        } else {
1405            anyhow::Ok(start..end)
1406        }
1407    }
1408
1409    pub fn as_count(&self) -> Option<u32> {
1410        if let CommandRange {
1411            start: Position::Line { row, offset: 0 },
1412            end: None,
1413        } = &self
1414        {
1415            Some(*row)
1416        } else {
1417            None
1418        }
1419    }
1420
1421    /// The `CommandRange` representing the entire buffer.
1422    fn buffer() -> Self {
1423        Self {
1424            start: Position::Line { row: 1, offset: 0 },
1425            end: Some(Position::LastLine { offset: 0 }),
1426        }
1427    }
1428}
1429
1430fn generate_commands(_: &App) -> Vec<VimCommand> {
1431    vec![
1432        VimCommand::new(
1433            ("w", "rite"),
1434            VimSave {
1435                save_intent: Some(SaveIntent::Save),
1436                filename: "".into(),
1437                range: None,
1438            },
1439        )
1440        .bang(VimSave {
1441            save_intent: Some(SaveIntent::Overwrite),
1442            filename: "".into(),
1443            range: None,
1444        })
1445        .filename(|action, filename| {
1446            Some(
1447                VimSave {
1448                    save_intent: action
1449                        .as_any()
1450                        .downcast_ref::<VimSave>()
1451                        .and_then(|action| action.save_intent),
1452                    filename,
1453                    range: None,
1454                }
1455                .boxed_clone(),
1456            )
1457        })
1458        .range(|action, range| {
1459            let mut action: VimSave = action.as_any().downcast_ref::<VimSave>().unwrap().clone();
1460            action.range.replace(range.clone());
1461            Some(Box::new(action))
1462        }),
1463        VimCommand::new(("e", "dit"), editor::actions::ReloadFile)
1464            .bang(editor::actions::ReloadFile)
1465            .filename(|_, filename| Some(VimEdit { filename }.boxed_clone())),
1466        VimCommand::new(
1467            ("r", "ead"),
1468            VimRead {
1469                range: None,
1470                filename: "".into(),
1471            },
1472        )
1473        .filename(|_, filename| {
1474            Some(
1475                VimRead {
1476                    range: None,
1477                    filename,
1478                }
1479                .boxed_clone(),
1480            )
1481        })
1482        .range(|action, range| {
1483            let mut action: VimRead = action.as_any().downcast_ref::<VimRead>().unwrap().clone();
1484            action.range.replace(range.clone());
1485            Some(Box::new(action))
1486        }),
1487        VimCommand::new(("sp", "lit"), workspace::SplitHorizontal::default()).filename(
1488            |_, filename| {
1489                Some(
1490                    VimSplit {
1491                        vertical: false,
1492                        filename,
1493                    }
1494                    .boxed_clone(),
1495                )
1496            },
1497        ),
1498        VimCommand::new(("vs", "plit"), workspace::SplitVertical::default()).filename(
1499            |_, filename| {
1500                Some(
1501                    VimSplit {
1502                        vertical: true,
1503                        filename,
1504                    }
1505                    .boxed_clone(),
1506                )
1507            },
1508        ),
1509        VimCommand::new(("tabe", "dit"), workspace::NewFile)
1510            .filename(|_action, filename| Some(VimEdit { filename }.boxed_clone())),
1511        VimCommand::new(("tabnew", ""), workspace::NewFile)
1512            .filename(|_action, filename| Some(VimEdit { filename }.boxed_clone())),
1513        VimCommand::new(
1514            ("q", "uit"),
1515            workspace::CloseActiveItem {
1516                save_intent: Some(SaveIntent::Close),
1517                close_pinned: false,
1518            },
1519        )
1520        .bang(workspace::CloseActiveItem {
1521            save_intent: Some(SaveIntent::Skip),
1522            close_pinned: true,
1523        }),
1524        VimCommand::new(
1525            ("wq", ""),
1526            workspace::CloseActiveItem {
1527                save_intent: Some(SaveIntent::Save),
1528                close_pinned: false,
1529            },
1530        )
1531        .bang(workspace::CloseActiveItem {
1532            save_intent: Some(SaveIntent::Overwrite),
1533            close_pinned: true,
1534        }),
1535        VimCommand::new(
1536            ("x", "it"),
1537            workspace::CloseActiveItem {
1538                save_intent: Some(SaveIntent::SaveAll),
1539                close_pinned: false,
1540            },
1541        )
1542        .bang(workspace::CloseActiveItem {
1543            save_intent: Some(SaveIntent::Overwrite),
1544            close_pinned: true,
1545        }),
1546        VimCommand::new(
1547            ("exi", "t"),
1548            workspace::CloseActiveItem {
1549                save_intent: Some(SaveIntent::SaveAll),
1550                close_pinned: false,
1551            },
1552        )
1553        .bang(workspace::CloseActiveItem {
1554            save_intent: Some(SaveIntent::Overwrite),
1555            close_pinned: true,
1556        }),
1557        VimCommand::new(
1558            ("up", "date"),
1559            workspace::Save {
1560                save_intent: Some(SaveIntent::SaveAll),
1561            },
1562        ),
1563        VimCommand::new(
1564            ("wa", "ll"),
1565            workspace::SaveAll {
1566                save_intent: Some(SaveIntent::SaveAll),
1567            },
1568        )
1569        .bang(workspace::SaveAll {
1570            save_intent: Some(SaveIntent::Overwrite),
1571        }),
1572        VimCommand::new(
1573            ("qa", "ll"),
1574            workspace::CloseAllItemsAndPanes {
1575                save_intent: Some(SaveIntent::Close),
1576            },
1577        )
1578        .bang(workspace::CloseAllItemsAndPanes {
1579            save_intent: Some(SaveIntent::Skip),
1580        }),
1581        VimCommand::new(
1582            ("quita", "ll"),
1583            workspace::CloseAllItemsAndPanes {
1584                save_intent: Some(SaveIntent::Close),
1585            },
1586        )
1587        .bang(workspace::CloseAllItemsAndPanes {
1588            save_intent: Some(SaveIntent::Skip),
1589        }),
1590        VimCommand::new(
1591            ("xa", "ll"),
1592            workspace::CloseAllItemsAndPanes {
1593                save_intent: Some(SaveIntent::SaveAll),
1594            },
1595        )
1596        .bang(workspace::CloseAllItemsAndPanes {
1597            save_intent: Some(SaveIntent::Overwrite),
1598        }),
1599        VimCommand::new(
1600            ("wqa", "ll"),
1601            workspace::CloseAllItemsAndPanes {
1602                save_intent: Some(SaveIntent::SaveAll),
1603            },
1604        )
1605        .bang(workspace::CloseAllItemsAndPanes {
1606            save_intent: Some(SaveIntent::Overwrite),
1607        }),
1608        VimCommand::new(("cq", "uit"), zed_actions::Quit),
1609        VimCommand::new(
1610            ("bd", "elete"),
1611            workspace::CloseActiveItem {
1612                save_intent: Some(SaveIntent::Close),
1613                close_pinned: false,
1614            },
1615        )
1616        .bang(workspace::CloseActiveItem {
1617            save_intent: Some(SaveIntent::Skip),
1618            close_pinned: true,
1619        }),
1620        VimCommand::new(
1621            ("norm", "al"),
1622            VimNorm {
1623                command: "".into(),
1624                range: None,
1625                override_rows: None,
1626            },
1627        )
1628        .args(|_, args| {
1629            Some(
1630                VimNorm {
1631                    command: args,
1632                    range: None,
1633                    override_rows: None,
1634                }
1635                .boxed_clone(),
1636            )
1637        })
1638        .range(|action, range| {
1639            let mut action: VimNorm = action.as_any().downcast_ref::<VimNorm>().unwrap().clone();
1640            action.range.replace(range.clone());
1641            Some(Box::new(action))
1642        }),
1643        VimCommand::new(("bn", "ext"), workspace::ActivateNextItem).count(),
1644        VimCommand::new(("bN", "ext"), workspace::ActivatePreviousItem).count(),
1645        VimCommand::new(("bp", "revious"), workspace::ActivatePreviousItem).count(),
1646        VimCommand::new(("bf", "irst"), workspace::ActivateItem(0)),
1647        VimCommand::new(("br", "ewind"), workspace::ActivateItem(0)),
1648        VimCommand::new(("bl", "ast"), workspace::ActivateLastItem),
1649        VimCommand::str(("buffers", ""), "tab_switcher::ToggleAll"),
1650        VimCommand::str(("ls", ""), "tab_switcher::ToggleAll"),
1651        VimCommand::new(("new", ""), workspace::NewFileSplitHorizontal),
1652        VimCommand::new(("vne", "w"), workspace::NewFileSplitVertical),
1653        VimCommand::new(("tabn", "ext"), workspace::ActivateNextItem).count(),
1654        VimCommand::new(("tabp", "revious"), workspace::ActivatePreviousItem).count(),
1655        VimCommand::new(("tabN", "ext"), workspace::ActivatePreviousItem).count(),
1656        VimCommand::new(
1657            ("tabc", "lose"),
1658            workspace::CloseActiveItem {
1659                save_intent: Some(SaveIntent::Close),
1660                close_pinned: false,
1661            },
1662        ),
1663        VimCommand::new(
1664            ("tabo", "nly"),
1665            workspace::CloseOtherItems {
1666                save_intent: Some(SaveIntent::Close),
1667                close_pinned: false,
1668            },
1669        )
1670        .bang(workspace::CloseOtherItems {
1671            save_intent: Some(SaveIntent::Skip),
1672            close_pinned: false,
1673        }),
1674        VimCommand::new(
1675            ("on", "ly"),
1676            workspace::CloseInactiveTabsAndPanes {
1677                save_intent: Some(SaveIntent::Close),
1678            },
1679        )
1680        .bang(workspace::CloseInactiveTabsAndPanes {
1681            save_intent: Some(SaveIntent::Skip),
1682        }),
1683        VimCommand::str(("cl", "ist"), "diagnostics::Deploy"),
1684        VimCommand::new(("cc", ""), editor::actions::Hover),
1685        VimCommand::new(("ll", ""), editor::actions::Hover),
1686        VimCommand::new(("cn", "ext"), editor::actions::GoToDiagnostic::default())
1687            .range(wrap_count),
1688        VimCommand::new(
1689            ("cp", "revious"),
1690            editor::actions::GoToPreviousDiagnostic::default(),
1691        )
1692        .range(wrap_count),
1693        VimCommand::new(
1694            ("cN", "ext"),
1695            editor::actions::GoToPreviousDiagnostic::default(),
1696        )
1697        .range(wrap_count),
1698        VimCommand::new(
1699            ("lp", "revious"),
1700            editor::actions::GoToPreviousDiagnostic::default(),
1701        )
1702        .range(wrap_count),
1703        VimCommand::new(
1704            ("lN", "ext"),
1705            editor::actions::GoToPreviousDiagnostic::default(),
1706        )
1707        .range(wrap_count),
1708        VimCommand::new(("j", "oin"), JoinLines).range(select_range),
1709        VimCommand::new(("fo", "ld"), editor::actions::FoldSelectedRanges).range(act_on_range),
1710        VimCommand::new(("foldo", "pen"), editor::actions::UnfoldLines)
1711            .bang(editor::actions::UnfoldRecursive)
1712            .range(act_on_range),
1713        VimCommand::new(("foldc", "lose"), editor::actions::Fold)
1714            .bang(editor::actions::FoldRecursive)
1715            .range(act_on_range),
1716        VimCommand::new(("dif", "fupdate"), editor::actions::ToggleSelectedDiffHunks)
1717            .range(act_on_range),
1718        VimCommand::str(("rev", "ert"), "git::Restore").range(act_on_range),
1719        VimCommand::new(("d", "elete"), VisualDeleteLine).range(select_range),
1720        VimCommand::new(("y", "ank"), gpui::NoAction).range(|_, range| {
1721            Some(
1722                YankCommand {
1723                    range: range.clone(),
1724                }
1725                .boxed_clone(),
1726            )
1727        }),
1728        VimCommand::new(("reg", "isters"), ToggleRegistersView).bang(ToggleRegistersView),
1729        VimCommand::new(("di", "splay"), ToggleRegistersView).bang(ToggleRegistersView),
1730        VimCommand::new(("marks", ""), ToggleMarksView).bang(ToggleMarksView),
1731        VimCommand::new(("delm", "arks"), ArgumentRequired)
1732            .bang(DeleteMarks::AllLocal)
1733            .args(|_, args| Some(DeleteMarks::Marks(args).boxed_clone())),
1734        VimCommand::new(("sor", "t"), SortLinesCaseSensitive)
1735            .range(select_range)
1736            .default_range(CommandRange::buffer()),
1737        VimCommand::new(("sort i", ""), SortLinesCaseInsensitive)
1738            .range(select_range)
1739            .default_range(CommandRange::buffer()),
1740        VimCommand::str(("E", "xplore"), "project_panel::ToggleFocus"),
1741        VimCommand::str(("H", "explore"), "project_panel::ToggleFocus"),
1742        VimCommand::str(("L", "explore"), "project_panel::ToggleFocus"),
1743        VimCommand::str(("S", "explore"), "project_panel::ToggleFocus"),
1744        VimCommand::str(("Ve", "xplore"), "project_panel::ToggleFocus"),
1745        VimCommand::str(("te", "rm"), "terminal_panel::Toggle"),
1746        VimCommand::str(("T", "erm"), "terminal_panel::Toggle"),
1747        VimCommand::str(("C", "ollab"), "collab_panel::ToggleFocus"),
1748        VimCommand::str(("No", "tifications"), "notification_panel::ToggleFocus"),
1749        VimCommand::str(("A", "I"), "agent::ToggleFocus"),
1750        VimCommand::str(("G", "it"), "git_panel::ToggleFocus"),
1751        VimCommand::str(("D", "ebug"), "debug_panel::ToggleFocus"),
1752        VimCommand::new(("noh", "lsearch"), search::buffer_search::Dismiss),
1753        VimCommand::new(("$", ""), EndOfDocument),
1754        VimCommand::new(("%", ""), EndOfDocument),
1755        VimCommand::new(("0", ""), StartOfDocument),
1756        VimCommand::new(("ex", ""), editor::actions::ReloadFile).bang(editor::actions::ReloadFile),
1757        VimCommand::new(("cpp", "link"), editor::actions::CopyPermalinkToLine).range(act_on_range),
1758        VimCommand::str(("opt", "ions"), "zed::OpenDefaultSettings"),
1759        VimCommand::str(("map", ""), "vim::OpenDefaultKeymap"),
1760        VimCommand::new(("h", "elp"), OpenDocs),
1761    ]
1762}
1763
1764struct VimCommands(Vec<VimCommand>);
1765// safety: we only ever access this from the main thread (as ensured by the cx argument)
1766// actions are not Sync so we can't otherwise use a OnceLock.
1767unsafe impl Sync for VimCommands {}
1768impl Global for VimCommands {}
1769
1770fn commands(cx: &App) -> &Vec<VimCommand> {
1771    static COMMANDS: OnceLock<VimCommands> = OnceLock::new();
1772    &COMMANDS
1773        .get_or_init(|| VimCommands(generate_commands(cx)))
1774        .0
1775}
1776
1777fn act_on_range(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
1778    Some(
1779        WithRange {
1780            restore_selection: true,
1781            range: range.clone(),
1782            action: WrappedAction(action),
1783        }
1784        .boxed_clone(),
1785    )
1786}
1787
1788fn select_range(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
1789    Some(
1790        WithRange {
1791            restore_selection: false,
1792            range: range.clone(),
1793            action: WrappedAction(action),
1794        }
1795        .boxed_clone(),
1796    )
1797}
1798
1799fn wrap_count(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
1800    range.as_count().map(|count| {
1801        WithCount {
1802            count,
1803            action: WrappedAction(action),
1804        }
1805        .boxed_clone()
1806    })
1807}
1808
1809pub fn command_interceptor(
1810    mut input: &str,
1811    workspace: WeakEntity<Workspace>,
1812    cx: &mut App,
1813) -> Task<CommandInterceptResult> {
1814    while input.starts_with(':') {
1815        input = &input[1..];
1816    }
1817
1818    let (range, query) = VimCommand::parse_range(input);
1819    let range_prefix = input[0..(input.len() - query.len())].to_string();
1820    let has_trailing_space = query.ends_with(" ");
1821    let mut query = query.as_str().trim_start();
1822
1823    let on_matching_lines = (query.starts_with('g') || query.starts_with('v'))
1824        .then(|| {
1825            let (pattern, range, search, invert) = OnMatchingLines::parse(query, &range)?;
1826            let start_idx = query.len() - pattern.len();
1827            query = query[start_idx..].trim();
1828            Some((range, search, invert))
1829        })
1830        .flatten();
1831
1832    let mut action = if range.is_some() && query.is_empty() {
1833        Some(
1834            GoToLine {
1835                range: range.clone().unwrap(),
1836            }
1837            .boxed_clone(),
1838        )
1839    } else if query.starts_with('/') || query.starts_with('?') {
1840        Some(
1841            FindCommand {
1842                query: query[1..].to_string(),
1843                backwards: query.starts_with('?'),
1844            }
1845            .boxed_clone(),
1846        )
1847    } else if query.starts_with("se ") || query.starts_with("set ") {
1848        let (prefix, option) = query.split_once(' ').unwrap();
1849        let mut commands = VimOption::possible_commands(option);
1850        if !commands.is_empty() {
1851            let query = prefix.to_string() + " " + option;
1852            for command in &mut commands {
1853                command.positions = generate_positions(&command.string, &query);
1854            }
1855        }
1856        return Task::ready(CommandInterceptResult {
1857            results: commands,
1858            exclusive: false,
1859        });
1860    } else if query.starts_with('s') {
1861        let mut substitute = "substitute".chars().peekable();
1862        let mut query = query.chars().peekable();
1863        while substitute
1864            .peek()
1865            .is_some_and(|char| Some(char) == query.peek())
1866        {
1867            substitute.next();
1868            query.next();
1869        }
1870        if let Some(replacement) = Replacement::parse(query) {
1871            let range = range.clone().unwrap_or(CommandRange {
1872                start: Position::CurrentLine { offset: 0 },
1873                end: None,
1874            });
1875            Some(ReplaceCommand { replacement, range }.boxed_clone())
1876        } else {
1877            None
1878        }
1879    } else if query.contains('!') {
1880        ShellExec::parse(query, range.clone())
1881    } else if on_matching_lines.is_some() {
1882        commands(cx)
1883            .iter()
1884            .find_map(|command| command.parse(query, &None, cx))
1885    } else {
1886        None
1887    };
1888
1889    if let Some((range, search, invert)) = on_matching_lines
1890        && let Some(ref inner) = action
1891    {
1892        action = Some(Box::new(OnMatchingLines {
1893            range,
1894            search,
1895            action: WrappedAction(inner.boxed_clone()),
1896            invert,
1897        }));
1898    };
1899
1900    if let Some(action) = action {
1901        let string = input.to_string();
1902        let positions = generate_positions(&string, &(range_prefix + query));
1903        return Task::ready(CommandInterceptResult {
1904            results: vec![CommandInterceptItem {
1905                action,
1906                string,
1907                positions,
1908            }],
1909            exclusive: false,
1910        });
1911    }
1912
1913    let Some((mut results, filenames)) =
1914        commands(cx).iter().enumerate().find_map(|(idx, command)| {
1915            let action = command.parse(query, &range, cx)?;
1916            let parsed_query = command.get_parsed_query(query.into())?;
1917            let display_string = ":".to_owned()
1918                + &range_prefix
1919                + command.prefix
1920                + command.suffix
1921                + if parsed_query.has_bang { "!" } else { "" };
1922            let space = if parsed_query.has_space { " " } else { "" };
1923
1924            let string = format!("{}{}{}", &display_string, &space, &parsed_query.args);
1925            let positions = generate_positions(&string, &(range_prefix.clone() + query));
1926
1927            let results = vec![CommandInterceptItem {
1928                action,
1929                string,
1930                positions,
1931            }];
1932
1933            let no_args_positions =
1934                generate_positions(&display_string, &(range_prefix.clone() + query));
1935
1936            // The following are valid autocomplete scenarios:
1937            // :w!filename.txt
1938            // :w filename.txt
1939            // :w[space]
1940            if !command.has_filename
1941                || (!has_trailing_space && !parsed_query.has_bang && parsed_query.args.is_empty())
1942            {
1943                return Some((results, None));
1944            }
1945
1946            Some((
1947                results,
1948                Some((idx, parsed_query, display_string, no_args_positions)),
1949            ))
1950        })
1951    else {
1952        return Task::ready(CommandInterceptResult::default());
1953    };
1954
1955    if let Some((cmd_idx, parsed_query, display_string, no_args_positions)) = filenames {
1956        let filenames = VimCommand::generate_filename_completions(&parsed_query, workspace, cx);
1957        cx.spawn(async move |cx| {
1958            let filenames = filenames.await;
1959            const MAX_RESULTS: usize = 100;
1960            let executor = cx.background_executor().clone();
1961            let mut candidates = Vec::with_capacity(filenames.len());
1962
1963            for (idx, filename) in filenames.iter().enumerate() {
1964                candidates.push(fuzzy::StringMatchCandidate::new(idx, &filename));
1965            }
1966            let filenames = fuzzy::match_strings(
1967                &candidates,
1968                &parsed_query.args,
1969                false,
1970                true,
1971                MAX_RESULTS,
1972                &Default::default(),
1973                executor,
1974            )
1975            .await;
1976
1977            for fuzzy::StringMatch {
1978                candidate_id: _,
1979                score: _,
1980                positions,
1981                string,
1982            } in filenames
1983            {
1984                let offset = display_string.len() + 1;
1985                let mut positions: Vec<_> = positions.iter().map(|&pos| pos + offset).collect();
1986                positions.splice(0..0, no_args_positions.clone());
1987                let string = format!("{display_string} {string}");
1988                let (range, query) = VimCommand::parse_range(&string[1..]);
1989                let action =
1990                    match cx.update(|cx| commands(cx).get(cmd_idx)?.parse(&query, &range, cx)) {
1991                        Some(action) => action,
1992                        _ => continue,
1993                    };
1994                results.push(CommandInterceptItem {
1995                    action,
1996                    string,
1997                    positions,
1998                });
1999            }
2000            CommandInterceptResult {
2001                results,
2002                exclusive: true,
2003            }
2004        })
2005    } else {
2006        Task::ready(CommandInterceptResult {
2007            results,
2008            exclusive: false,
2009        })
2010    }
2011}
2012
2013fn generate_positions(string: &str, query: &str) -> Vec<usize> {
2014    let mut positions = Vec::new();
2015    let mut chars = query.chars();
2016
2017    let Some(mut current) = chars.next() else {
2018        return positions;
2019    };
2020
2021    for (i, c) in string.char_indices() {
2022        if c == current {
2023            positions.push(i);
2024            if let Some(c) = chars.next() {
2025                current = c;
2026            } else {
2027                break;
2028            }
2029        }
2030    }
2031
2032    positions
2033}
2034
2035/// Applies a command to all lines matching a pattern.
2036#[derive(Debug, PartialEq, Clone, Action)]
2037#[action(namespace = vim, no_json, no_register)]
2038pub(crate) struct OnMatchingLines {
2039    range: CommandRange,
2040    search: String,
2041    action: WrappedAction,
2042    invert: bool,
2043}
2044
2045impl OnMatchingLines {
2046    // convert a vim query into something more usable by zed.
2047    // we don't attempt to fully convert between the two regex syntaxes,
2048    // but we do flip \( and \) to ( and ) (and vice-versa) in the pattern,
2049    // and convert \0..\9 to $0..$9 in the replacement so that common idioms work.
2050    pub(crate) fn parse(
2051        query: &str,
2052        range: &Option<CommandRange>,
2053    ) -> Option<(String, CommandRange, String, bool)> {
2054        let mut global = "global".chars().peekable();
2055        let mut query_chars = query.chars().peekable();
2056        let mut invert = false;
2057        if query_chars.peek() == Some(&'v') {
2058            invert = true;
2059            query_chars.next();
2060        }
2061        while global
2062            .peek()
2063            .is_some_and(|char| Some(char) == query_chars.peek())
2064        {
2065            global.next();
2066            query_chars.next();
2067        }
2068        if !invert && query_chars.peek() == Some(&'!') {
2069            invert = true;
2070            query_chars.next();
2071        }
2072        let range = range.clone().unwrap_or(CommandRange {
2073            start: Position::Line { row: 0, offset: 0 },
2074            end: Some(Position::LastLine { offset: 0 }),
2075        });
2076
2077        let delimiter = query_chars.next().filter(|c| {
2078            !c.is_alphanumeric() && *c != '"' && *c != '|' && *c != '\'' && *c != '!'
2079        })?;
2080
2081        let mut search = String::new();
2082        let mut escaped = false;
2083
2084        for c in query_chars.by_ref() {
2085            if escaped {
2086                escaped = false;
2087                // unescape escaped parens
2088                if c != '(' && c != ')' && c != delimiter {
2089                    search.push('\\')
2090                }
2091                search.push(c)
2092            } else if c == '\\' {
2093                escaped = true;
2094            } else if c == delimiter {
2095                break;
2096            } else {
2097                // escape unescaped parens
2098                if c == '(' || c == ')' {
2099                    search.push('\\')
2100                }
2101                search.push(c)
2102            }
2103        }
2104
2105        Some((query_chars.collect::<String>(), range, search, invert))
2106    }
2107
2108    pub fn run(&self, vim: &mut Vim, window: &mut Window, cx: &mut Context<Vim>) {
2109        let result = vim.update_editor(cx, |vim, editor, cx| {
2110            self.range.buffer_range(vim, editor, window, cx)
2111        });
2112
2113        let range = match result {
2114            None => return,
2115            Some(e @ Err(_)) => {
2116                let Some(workspace) = vim.workspace(window) else {
2117                    return;
2118                };
2119                workspace.update(cx, |workspace, cx| {
2120                    e.notify_err(workspace, cx);
2121                });
2122                return;
2123            }
2124            Some(Ok(result)) => result,
2125        };
2126
2127        let mut action = self.action.boxed_clone();
2128        let mut last_pattern = self.search.clone();
2129
2130        let mut regexes = match Regex::new(&self.search) {
2131            Ok(regex) => vec![(regex, !self.invert)],
2132            e @ Err(_) => {
2133                let Some(workspace) = vim.workspace(window) else {
2134                    return;
2135                };
2136                workspace.update(cx, |workspace, cx| {
2137                    e.notify_err(workspace, cx);
2138                });
2139                return;
2140            }
2141        };
2142        while let Some(inner) = action
2143            .boxed_clone()
2144            .as_any()
2145            .downcast_ref::<OnMatchingLines>()
2146        {
2147            let Some(regex) = Regex::new(&inner.search).ok() else {
2148                break;
2149            };
2150            last_pattern = inner.search.clone();
2151            action = inner.action.boxed_clone();
2152            regexes.push((regex, !inner.invert))
2153        }
2154
2155        if let Some(pane) = vim.pane(window, cx) {
2156            pane.update(cx, |pane, cx| {
2157                if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()
2158                {
2159                    search_bar.update(cx, |search_bar, cx| {
2160                        if search_bar.show(window, cx) {
2161                            let _ = search_bar.search(
2162                                &last_pattern,
2163                                Some(SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE),
2164                                false,
2165                                window,
2166                                cx,
2167                            );
2168                        }
2169                    });
2170                }
2171            });
2172        };
2173
2174        vim.update_editor(cx, |_, editor, cx| {
2175            let snapshot = editor.snapshot(window, cx);
2176            let mut row = range.start.0;
2177
2178            let point_range = Point::new(range.start.0, 0)
2179                ..snapshot
2180                    .buffer_snapshot()
2181                    .clip_point(Point::new(range.end.0 + 1, 0), Bias::Left);
2182            cx.spawn_in(window, async move |editor, cx| {
2183                let new_selections = cx
2184                    .background_spawn(async move {
2185                        let mut line = String::new();
2186                        let mut new_selections = Vec::new();
2187                        let chunks = snapshot
2188                            .buffer_snapshot()
2189                            .text_for_range(point_range)
2190                            .chain(["\n"]);
2191
2192                        for chunk in chunks {
2193                            for (newline_ix, text) in chunk.split('\n').enumerate() {
2194                                if newline_ix > 0 {
2195                                    if regexes.iter().all(|(regex, should_match)| {
2196                                        regex.is_match(&line) == *should_match
2197                                    }) {
2198                                        new_selections
2199                                            .push(Point::new(row, 0).to_display_point(&snapshot))
2200                                    }
2201                                    row += 1;
2202                                    line.clear();
2203                                }
2204                                line.push_str(text)
2205                            }
2206                        }
2207
2208                        new_selections
2209                    })
2210                    .await;
2211
2212                if new_selections.is_empty() {
2213                    return;
2214                }
2215
2216                if let Some(vim_norm) = action.as_any().downcast_ref::<VimNorm>() {
2217                    let mut vim_norm = vim_norm.clone();
2218                    vim_norm.override_rows =
2219                        Some(new_selections.iter().map(|point| point.row().0).collect());
2220                    editor
2221                        .update_in(cx, |_, window, cx| {
2222                            window.dispatch_action(vim_norm.boxed_clone(), cx);
2223                        })
2224                        .log_err();
2225                    return;
2226                }
2227
2228                editor
2229                    .update_in(cx, |editor, window, cx| {
2230                        editor.start_transaction_at(Instant::now(), window, cx);
2231                        editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2232                            s.replace_cursors_with(|_| new_selections);
2233                        });
2234                        window.dispatch_action(action, cx);
2235
2236                        cx.defer_in(window, move |editor, window, cx| {
2237                            let newest = editor
2238                                .selections
2239                                .newest::<Point>(&editor.display_snapshot(cx));
2240                            editor.change_selections(
2241                                SelectionEffects::no_scroll(),
2242                                window,
2243                                cx,
2244                                |s| {
2245                                    s.select(vec![newest]);
2246                                },
2247                            );
2248                            editor.end_transaction_at(Instant::now(), cx);
2249                        })
2250                    })
2251                    .log_err();
2252            })
2253            .detach();
2254        });
2255    }
2256}
2257
2258/// Executes a shell command and returns the output.
2259#[derive(Clone, Debug, PartialEq, Action)]
2260#[action(namespace = vim, no_json, no_register)]
2261pub struct ShellExec {
2262    command: String,
2263    range: Option<CommandRange>,
2264    is_read: bool,
2265}
2266
2267impl Vim {
2268    pub fn cancel_running_command(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2269        if self.running_command.take().is_some() {
2270            self.update_editor(cx, |_, editor, cx| {
2271                editor.transact(window, cx, |editor, _window, _cx| {
2272                    editor.clear_row_highlights::<ShellExec>();
2273                })
2274            });
2275        }
2276    }
2277
2278    fn prepare_shell_command(
2279        &mut self,
2280        command: &str,
2281        _: &mut Window,
2282        cx: &mut Context<Self>,
2283    ) -> String {
2284        let mut ret = String::new();
2285        // N.B. non-standard escaping rules:
2286        // * !echo % => "echo README.md"
2287        // * !echo \% => "echo %"
2288        // * !echo \\% => echo \%
2289        // * !echo \\\% => echo \\%
2290        for c in command.chars() {
2291            if c != '%' && c != '!' {
2292                ret.push(c);
2293                continue;
2294            } else if ret.chars().last() == Some('\\') {
2295                ret.pop();
2296                ret.push(c);
2297                continue;
2298            }
2299            match c {
2300                '%' => {
2301                    self.update_editor(cx, |_, editor, cx| {
2302                        if let Some((_, buffer, _)) = editor.active_excerpt(cx)
2303                            && let Some(file) = buffer.read(cx).file()
2304                            && let Some(local) = file.as_local()
2305                        {
2306                            ret.push_str(&local.path().display(local.path_style(cx)));
2307                        }
2308                    });
2309                }
2310                '!' => {
2311                    if let Some(command) = &self.last_command {
2312                        ret.push_str(command)
2313                    }
2314                }
2315                _ => {}
2316            }
2317        }
2318        self.last_command = Some(ret.clone());
2319        ret
2320    }
2321
2322    pub fn shell_command_motion(
2323        &mut self,
2324        motion: Motion,
2325        times: Option<usize>,
2326        forced_motion: bool,
2327        window: &mut Window,
2328        cx: &mut Context<Vim>,
2329    ) {
2330        self.stop_recording(cx);
2331        let Some(workspace) = self.workspace(window) else {
2332            return;
2333        };
2334        let command = self.update_editor(cx, |_, editor, cx| {
2335            let snapshot = editor.snapshot(window, cx);
2336            let start = editor
2337                .selections
2338                .newest_display(&editor.display_snapshot(cx));
2339            let text_layout_details = editor.text_layout_details(window);
2340            let (mut range, _) = motion
2341                .range(
2342                    &snapshot,
2343                    start.clone(),
2344                    times,
2345                    &text_layout_details,
2346                    forced_motion,
2347                )
2348                .unwrap_or((start.range(), MotionKind::Exclusive));
2349            if range.start != start.start {
2350                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2351                    s.select_ranges([
2352                        range.start.to_point(&snapshot)..range.start.to_point(&snapshot)
2353                    ]);
2354                })
2355            }
2356            if range.end.row() > range.start.row() && range.end.column() != 0 {
2357                *range.end.row_mut() -= 1
2358            }
2359            if range.end.row() == range.start.row() {
2360                ".!".to_string()
2361            } else {
2362                format!(".,.+{}!", (range.end.row() - range.start.row()).0)
2363            }
2364        });
2365        if let Some(command) = command {
2366            workspace.update(cx, |workspace, cx| {
2367                command_palette::CommandPalette::toggle(workspace, &command, window, cx);
2368            });
2369        }
2370    }
2371
2372    pub fn shell_command_object(
2373        &mut self,
2374        object: Object,
2375        around: bool,
2376        window: &mut Window,
2377        cx: &mut Context<Vim>,
2378    ) {
2379        self.stop_recording(cx);
2380        let Some(workspace) = self.workspace(window) else {
2381            return;
2382        };
2383        let command = self.update_editor(cx, |_, editor, cx| {
2384            let snapshot = editor.snapshot(window, cx);
2385            let start = editor
2386                .selections
2387                .newest_display(&editor.display_snapshot(cx));
2388            let range = object
2389                .range(&snapshot, start.clone(), around, None)
2390                .unwrap_or(start.range());
2391            if range.start != start.start {
2392                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2393                    s.select_ranges([
2394                        range.start.to_point(&snapshot)..range.start.to_point(&snapshot)
2395                    ]);
2396                })
2397            }
2398            if range.end.row() == range.start.row() {
2399                ".!".to_string()
2400            } else {
2401                format!(".,.+{}!", (range.end.row() - range.start.row()).0)
2402            }
2403        });
2404        if let Some(command) = command {
2405            workspace.update(cx, |workspace, cx| {
2406                command_palette::CommandPalette::toggle(workspace, &command, window, cx);
2407            });
2408        }
2409    }
2410}
2411
2412impl ShellExec {
2413    pub fn parse(query: &str, range: Option<CommandRange>) -> Option<Box<dyn Action>> {
2414        let (before, after) = query.split_once('!')?;
2415        let before = before.trim();
2416
2417        if !"read".starts_with(before) {
2418            return None;
2419        }
2420
2421        Some(
2422            ShellExec {
2423                command: after.trim().to_string(),
2424                range,
2425                is_read: !before.is_empty(),
2426            }
2427            .boxed_clone(),
2428        )
2429    }
2430
2431    pub fn run(&self, vim: &mut Vim, window: &mut Window, cx: &mut Context<Vim>) {
2432        let Some(workspace) = vim.workspace(window) else {
2433            return;
2434        };
2435
2436        let project = workspace.read(cx).project().clone();
2437        let command = vim.prepare_shell_command(&self.command, window, cx);
2438
2439        if self.range.is_none() && !self.is_read {
2440            workspace.update(cx, |workspace, cx| {
2441                let project = workspace.project().read(cx);
2442                let cwd = project.first_project_directory(cx);
2443                let shell = project.terminal_settings(&cwd, cx).shell.clone();
2444
2445                let spawn_in_terminal = SpawnInTerminal {
2446                    id: TaskId("vim".to_string()),
2447                    full_label: command.clone(),
2448                    label: command.clone(),
2449                    command: Some(command.clone()),
2450                    args: Vec::new(),
2451                    command_label: command.clone(),
2452                    cwd,
2453                    env: HashMap::default(),
2454                    use_new_terminal: true,
2455                    allow_concurrent_runs: true,
2456                    reveal: RevealStrategy::NoFocus,
2457                    reveal_target: RevealTarget::Dock,
2458                    hide: HideStrategy::Never,
2459                    shell,
2460                    show_summary: false,
2461                    show_command: false,
2462                    show_rerun: false,
2463                };
2464
2465                let task_status = workspace.spawn_in_terminal(spawn_in_terminal, window, cx);
2466                cx.background_spawn(async move {
2467                    match task_status.await {
2468                        Some(Ok(status)) => {
2469                            if status.success() {
2470                                log::debug!("Vim shell exec succeeded");
2471                            } else {
2472                                log::debug!("Vim shell exec failed, code: {:?}", status.code());
2473                            }
2474                        }
2475                        Some(Err(e)) => log::error!("Vim shell exec failed: {e}"),
2476                        None => log::debug!("Vim shell exec got cancelled"),
2477                    }
2478                })
2479                .detach();
2480            });
2481            return;
2482        };
2483
2484        let mut input_snapshot = None;
2485        let mut input_range = None;
2486        let mut needs_newline_prefix = false;
2487        vim.update_editor(cx, |vim, editor, cx| {
2488            let snapshot = editor.buffer().read(cx).snapshot(cx);
2489            let range = if let Some(range) = self.range.clone() {
2490                let Some(range) = range.buffer_range(vim, editor, window, cx).log_err() else {
2491                    return;
2492                };
2493                Point::new(range.start.0, 0)
2494                    ..snapshot.clip_point(Point::new(range.end.0 + 1, 0), Bias::Right)
2495            } else {
2496                let mut end = editor
2497                    .selections
2498                    .newest::<Point>(&editor.display_snapshot(cx))
2499                    .range()
2500                    .end;
2501                end = snapshot.clip_point(Point::new(end.row + 1, 0), Bias::Right);
2502                needs_newline_prefix = end == snapshot.max_point();
2503                end..end
2504            };
2505            if self.is_read {
2506                input_range =
2507                    Some(snapshot.anchor_after(range.end)..snapshot.anchor_after(range.end));
2508            } else {
2509                input_range =
2510                    Some(snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end));
2511            }
2512            editor.highlight_rows::<ShellExec>(
2513                input_range.clone().unwrap(),
2514                cx.theme().status().unreachable_background,
2515                Default::default(),
2516                cx,
2517            );
2518
2519            if !self.is_read {
2520                input_snapshot = Some(snapshot)
2521            }
2522        });
2523
2524        let Some(range) = input_range else { return };
2525
2526        let process_task = project.update(cx, |project, cx| project.exec_in_shell(command, cx));
2527
2528        let is_read = self.is_read;
2529
2530        let task = cx.spawn_in(window, async move |vim, cx| {
2531            let Some(mut process) = process_task.await.log_err() else {
2532                return;
2533            };
2534            process.stdout(Stdio::piped());
2535            process.stderr(Stdio::piped());
2536
2537            if input_snapshot.is_some() {
2538                process.stdin(Stdio::piped());
2539            } else {
2540                process.stdin(Stdio::null());
2541            };
2542
2543            let Some(mut running) = process.spawn().log_err() else {
2544                vim.update_in(cx, |vim, window, cx| {
2545                    vim.cancel_running_command(window, cx);
2546                })
2547                .log_err();
2548                return;
2549            };
2550
2551            if let Some(mut stdin) = running.stdin.take()
2552                && let Some(snapshot) = input_snapshot
2553            {
2554                let range = range.clone();
2555                cx.background_spawn(async move {
2556                    for chunk in snapshot.text_for_range(range) {
2557                        if stdin.write_all(chunk.as_bytes()).await.log_err().is_none() {
2558                            return;
2559                        }
2560                    }
2561                    stdin.flush().await.log_err();
2562                })
2563                .detach();
2564            };
2565
2566            let output = cx.background_spawn(running.output()).await;
2567
2568            let Some(output) = output.log_err() else {
2569                vim.update_in(cx, |vim, window, cx| {
2570                    vim.cancel_running_command(window, cx);
2571                })
2572                .log_err();
2573                return;
2574            };
2575            let mut text = String::new();
2576            if needs_newline_prefix {
2577                text.push('\n');
2578            }
2579            text.push_str(&String::from_utf8_lossy(&output.stdout));
2580            text.push_str(&String::from_utf8_lossy(&output.stderr));
2581            if !text.is_empty() && text.chars().last() != Some('\n') {
2582                text.push('\n');
2583            }
2584
2585            vim.update_in(cx, |vim, window, cx| {
2586                vim.update_editor(cx, |_, editor, cx| {
2587                    editor.transact(window, cx, |editor, window, cx| {
2588                        editor.edit([(range.clone(), text)], cx);
2589                        let snapshot = editor.buffer().read(cx).snapshot(cx);
2590                        editor.change_selections(Default::default(), window, cx, |s| {
2591                            let point = if is_read {
2592                                let point = range.end.to_point(&snapshot);
2593                                Point::new(point.row.saturating_sub(1), 0)
2594                            } else {
2595                                let point = range.start.to_point(&snapshot);
2596                                Point::new(point.row, 0)
2597                            };
2598                            s.select_ranges([point..point]);
2599                        })
2600                    })
2601                });
2602                vim.cancel_running_command(window, cx);
2603            })
2604            .log_err();
2605        });
2606        vim.running_command.replace(task);
2607    }
2608}
2609
2610#[cfg(test)]
2611mod test {
2612    use std::path::{Path, PathBuf};
2613
2614    use crate::{
2615        VimAddon,
2616        state::Mode,
2617        test::{NeovimBackedTestContext, VimTestContext},
2618    };
2619    use editor::{Editor, EditorSettings};
2620    use gpui::{Context, TestAppContext};
2621    use indoc::indoc;
2622    use settings::Settings;
2623    use util::path;
2624    use workspace::{OpenOptions, Workspace};
2625
2626    #[gpui::test]
2627    async fn test_command_basics(cx: &mut TestAppContext) {
2628        let mut cx = NeovimBackedTestContext::new(cx).await;
2629
2630        cx.set_shared_state(indoc! {"
2631            ˇa
2632            b
2633            c"})
2634            .await;
2635
2636        cx.simulate_shared_keystrokes(": j enter").await;
2637
2638        // hack: our cursor positioning after a join command is wrong
2639        cx.simulate_shared_keystrokes("^").await;
2640        cx.shared_state().await.assert_eq(indoc! {
2641            "ˇa b
2642            c"
2643        });
2644    }
2645
2646    #[gpui::test]
2647    async fn test_command_goto(cx: &mut TestAppContext) {
2648        let mut cx = NeovimBackedTestContext::new(cx).await;
2649
2650        cx.set_shared_state(indoc! {"
2651            ˇa
2652            b
2653            c"})
2654            .await;
2655        cx.simulate_shared_keystrokes(": 3 enter").await;
2656        cx.shared_state().await.assert_eq(indoc! {"
2657            a
2658            b
2659            ˇc"});
2660    }
2661
2662    #[gpui::test]
2663    async fn test_command_replace(cx: &mut TestAppContext) {
2664        let mut cx = NeovimBackedTestContext::new(cx).await;
2665
2666        cx.set_shared_state(indoc! {"
2667            ˇa
2668            b
2669            b
2670            c"})
2671            .await;
2672        cx.simulate_shared_keystrokes(": % s / b / d enter").await;
2673        cx.shared_state().await.assert_eq(indoc! {"
2674            a
2675            d
2676            ˇd
2677            c"});
2678        cx.simulate_shared_keystrokes(": % s : . : \\ 0 \\ 0 enter")
2679            .await;
2680        cx.shared_state().await.assert_eq(indoc! {"
2681            aa
2682            dd
2683            dd
2684            ˇcc"});
2685        cx.simulate_shared_keystrokes("k : s / d d / e e enter")
2686            .await;
2687        cx.shared_state().await.assert_eq(indoc! {"
2688            aa
2689            dd
2690            ˇee
2691            cc"});
2692    }
2693
2694    #[gpui::test]
2695    async fn test_command_search(cx: &mut TestAppContext) {
2696        let mut cx = NeovimBackedTestContext::new(cx).await;
2697
2698        cx.set_shared_state(indoc! {"
2699                ˇa
2700                b
2701                a
2702                c"})
2703            .await;
2704        cx.simulate_shared_keystrokes(": / b enter").await;
2705        cx.shared_state().await.assert_eq(indoc! {"
2706                a
2707                ˇb
2708                a
2709                c"});
2710        cx.simulate_shared_keystrokes(": ? a enter").await;
2711        cx.shared_state().await.assert_eq(indoc! {"
2712                ˇa
2713                b
2714                a
2715                c"});
2716    }
2717
2718    #[gpui::test]
2719    async fn test_command_write(cx: &mut TestAppContext) {
2720        let mut cx = VimTestContext::new(cx, true).await;
2721        let path = Path::new(path!("/root/dir/file.rs"));
2722        let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
2723
2724        cx.simulate_keystrokes("i @ escape");
2725        cx.simulate_keystrokes(": w enter");
2726
2727        assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "@\n");
2728
2729        fs.as_fake().insert_file(path, b"oops\n".to_vec()).await;
2730
2731        // conflict!
2732        cx.simulate_keystrokes("i @ escape");
2733        cx.simulate_keystrokes(": w enter");
2734        cx.simulate_prompt_answer("Cancel");
2735
2736        assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "oops\n");
2737        assert!(!cx.has_pending_prompt());
2738        cx.simulate_keystrokes(": w !");
2739        cx.simulate_keystrokes("enter");
2740        assert!(!cx.has_pending_prompt());
2741        assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "@@\n");
2742    }
2743
2744    #[gpui::test]
2745    async fn test_command_read(cx: &mut TestAppContext) {
2746        let mut cx = VimTestContext::new(cx, true).await;
2747
2748        let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
2749        let path = Path::new(path!("/root/dir/other.rs"));
2750        fs.as_fake().insert_file(path, "1\n2\n3".into()).await;
2751
2752        cx.workspace(|workspace, _, cx| {
2753            assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
2754        });
2755
2756        // File without trailing newline
2757        cx.set_state("one\ntwo\nthreeˇ", Mode::Normal);
2758        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2759        cx.simulate_keystrokes("enter");
2760        cx.assert_state("one\ntwo\nthree\nˇ1\n2\n3", Mode::Normal);
2761
2762        cx.set_state("oneˇ\ntwo\nthree", Mode::Normal);
2763        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2764        cx.simulate_keystrokes("enter");
2765        cx.assert_state("one\nˇ1\n2\n3\ntwo\nthree", Mode::Normal);
2766
2767        cx.set_state("one\nˇtwo\nthree", Mode::Normal);
2768        cx.simulate_keystrokes(": 0 r space d i r / o t h e r . r s");
2769        cx.simulate_keystrokes("enter");
2770        cx.assert_state("ˇ1\n2\n3\none\ntwo\nthree", Mode::Normal);
2771
2772        cx.set_state("one\n«ˇtwo\nthree\nfour»\nfive", Mode::Visual);
2773        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2774        cx.simulate_keystrokes("enter");
2775        cx.run_until_parked();
2776        cx.assert_state("one\ntwo\nthree\nfour\nˇ1\n2\n3\nfive", Mode::Normal);
2777
2778        // Empty filename
2779        cx.set_state("oneˇ\ntwo\nthree", Mode::Normal);
2780        cx.simulate_keystrokes(": r");
2781        cx.simulate_keystrokes("enter");
2782        cx.assert_state("one\nˇone\ntwo\nthree\ntwo\nthree", Mode::Normal);
2783
2784        // File with trailing newline
2785        fs.as_fake().insert_file(path, "1\n2\n3\n".into()).await;
2786        cx.set_state("one\ntwo\nthreeˇ", Mode::Normal);
2787        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2788        cx.simulate_keystrokes("enter");
2789        cx.assert_state("one\ntwo\nthree\nˇ1\n2\n3\n", Mode::Normal);
2790
2791        cx.set_state("oneˇ\ntwo\nthree", Mode::Normal);
2792        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2793        cx.simulate_keystrokes("enter");
2794        cx.assert_state("one\nˇ1\n2\n3\n\ntwo\nthree", Mode::Normal);
2795
2796        cx.set_state("one\n«ˇtwo\nthree\nfour»\nfive", Mode::Visual);
2797        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2798        cx.simulate_keystrokes("enter");
2799        cx.assert_state("one\ntwo\nthree\nfour\nˇ1\n2\n3\n\nfive", Mode::Normal);
2800
2801        cx.set_state("«one\ntwo\nthreeˇ»", Mode::Visual);
2802        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2803        cx.simulate_keystrokes("enter");
2804        cx.assert_state("one\ntwo\nthree\nˇ1\n2\n3\n", Mode::Normal);
2805
2806        // Empty file
2807        fs.as_fake().insert_file(path, "".into()).await;
2808        cx.set_state("ˇone\ntwo\nthree", Mode::Normal);
2809        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2810        cx.simulate_keystrokes("enter");
2811        cx.assert_state("one\nˇtwo\nthree", Mode::Normal);
2812    }
2813
2814    #[gpui::test]
2815    async fn test_command_quit(cx: &mut TestAppContext) {
2816        let mut cx = VimTestContext::new(cx, true).await;
2817
2818        cx.simulate_keystrokes(": n e w enter");
2819        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
2820        cx.simulate_keystrokes(": q enter");
2821        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 1));
2822        cx.simulate_keystrokes(": n e w enter");
2823        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
2824        cx.simulate_keystrokes(": q a enter");
2825        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 0));
2826    }
2827
2828    #[gpui::test]
2829    async fn test_offsets(cx: &mut TestAppContext) {
2830        let mut cx = NeovimBackedTestContext::new(cx).await;
2831
2832        cx.set_shared_state("ˇ1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n")
2833            .await;
2834
2835        cx.simulate_shared_keystrokes(": + enter").await;
2836        cx.shared_state()
2837            .await
2838            .assert_eq("1\nˇ2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n");
2839
2840        cx.simulate_shared_keystrokes(": 1 0 - enter").await;
2841        cx.shared_state()
2842            .await
2843            .assert_eq("1\n2\n3\n4\n5\n6\n7\n8\nˇ9\n10\n11\n");
2844
2845        cx.simulate_shared_keystrokes(": . - 2 enter").await;
2846        cx.shared_state()
2847            .await
2848            .assert_eq("1\n2\n3\n4\n5\n6\nˇ7\n8\n9\n10\n11\n");
2849
2850        cx.simulate_shared_keystrokes(": % enter").await;
2851        cx.shared_state()
2852            .await
2853            .assert_eq("1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\nˇ");
2854    }
2855
2856    #[gpui::test]
2857    async fn test_command_ranges(cx: &mut TestAppContext) {
2858        let mut cx = NeovimBackedTestContext::new(cx).await;
2859
2860        cx.set_shared_state("ˇ1\n2\n3\n4\n4\n3\n2\n1").await;
2861
2862        cx.simulate_shared_keystrokes(": 2 , 4 d enter").await;
2863        cx.shared_state().await.assert_eq("1\nˇ4\n3\n2\n1");
2864
2865        cx.simulate_shared_keystrokes(": 2 , 4 s o r t enter").await;
2866        cx.shared_state().await.assert_eq("1\nˇ2\n3\n4\n1");
2867
2868        cx.simulate_shared_keystrokes(": 2 , 4 j o i n enter").await;
2869        cx.shared_state().await.assert_eq("1\nˇ2 3 4\n1");
2870    }
2871
2872    #[gpui::test]
2873    async fn test_command_visual_replace(cx: &mut TestAppContext) {
2874        let mut cx = NeovimBackedTestContext::new(cx).await;
2875
2876        cx.set_shared_state("ˇ1\n2\n3\n4\n4\n3\n2\n1").await;
2877
2878        cx.simulate_shared_keystrokes("v 2 j : s / . / k enter")
2879            .await;
2880        cx.shared_state().await.assert_eq("k\nk\nˇk\n4\n4\n3\n2\n1");
2881    }
2882
2883    #[track_caller]
2884    fn assert_active_item(
2885        workspace: &mut Workspace,
2886        expected_path: &str,
2887        expected_text: &str,
2888        cx: &mut Context<Workspace>,
2889    ) {
2890        let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();
2891
2892        let buffer = active_editor
2893            .read(cx)
2894            .buffer()
2895            .read(cx)
2896            .as_singleton()
2897            .unwrap();
2898
2899        let text = buffer.read(cx).text();
2900        let file = buffer.read(cx).file().unwrap();
2901        let file_path = file.as_local().unwrap().abs_path(cx);
2902
2903        assert_eq!(text, expected_text);
2904        assert_eq!(file_path, Path::new(expected_path));
2905    }
2906
2907    #[gpui::test]
2908    async fn test_command_gf(cx: &mut TestAppContext) {
2909        let mut cx = VimTestContext::new(cx, true).await;
2910
2911        // Assert base state, that we're in /root/dir/file.rs
2912        cx.workspace(|workspace, _, cx| {
2913            assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
2914        });
2915
2916        // Insert a new file
2917        let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
2918        fs.as_fake()
2919            .insert_file(
2920                path!("/root/dir/file2.rs"),
2921                "This is file2.rs".as_bytes().to_vec(),
2922            )
2923            .await;
2924        fs.as_fake()
2925            .insert_file(
2926                path!("/root/dir/file3.rs"),
2927                "go to file3".as_bytes().to_vec(),
2928            )
2929            .await;
2930
2931        // Put the path to the second file into the currently open buffer
2932        cx.set_state(indoc! {"go to fiˇle2.rs"}, Mode::Normal);
2933
2934        // Go to file2.rs
2935        cx.simulate_keystrokes("g f");
2936
2937        // We now have two items
2938        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
2939        cx.workspace(|workspace, _, cx| {
2940            assert_active_item(
2941                workspace,
2942                path!("/root/dir/file2.rs"),
2943                "This is file2.rs",
2944                cx,
2945            );
2946        });
2947
2948        // Update editor to point to `file2.rs`
2949        cx.editor =
2950            cx.workspace(|workspace, _, cx| workspace.active_item_as::<Editor>(cx).unwrap());
2951
2952        // Put the path to the third file into the currently open buffer,
2953        // but remove its suffix, because we want that lookup to happen automatically.
2954        cx.set_state(indoc! {"go to fiˇle3"}, Mode::Normal);
2955
2956        // Go to file3.rs
2957        cx.simulate_keystrokes("g f");
2958
2959        // We now have three items
2960        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 3));
2961        cx.workspace(|workspace, _, cx| {
2962            assert_active_item(workspace, path!("/root/dir/file3.rs"), "go to file3", cx);
2963        });
2964    }
2965
2966    #[gpui::test]
2967    async fn test_command_write_filename(cx: &mut TestAppContext) {
2968        let mut cx = VimTestContext::new(cx, true).await;
2969
2970        cx.workspace(|workspace, _, cx| {
2971            assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
2972        });
2973
2974        cx.simulate_keystrokes(": w space other.rs");
2975        cx.simulate_keystrokes("enter");
2976
2977        cx.workspace(|workspace, _, cx| {
2978            assert_active_item(workspace, path!("/root/other.rs"), "", cx);
2979        });
2980
2981        cx.simulate_keystrokes(": w space dir/file.rs");
2982        cx.simulate_keystrokes("enter");
2983
2984        cx.simulate_prompt_answer("Replace");
2985        cx.run_until_parked();
2986
2987        cx.workspace(|workspace, _, cx| {
2988            assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
2989        });
2990
2991        cx.simulate_keystrokes(": w ! space other.rs");
2992        cx.simulate_keystrokes("enter");
2993
2994        cx.workspace(|workspace, _, cx| {
2995            assert_active_item(workspace, path!("/root/other.rs"), "", cx);
2996        });
2997    }
2998
2999    #[gpui::test]
3000    async fn test_command_write_range(cx: &mut TestAppContext) {
3001        let mut cx = VimTestContext::new(cx, true).await;
3002
3003        cx.workspace(|workspace, _, cx| {
3004            assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
3005        });
3006
3007        cx.set_state(
3008            indoc! {"
3009                    The quick
3010                    brown« fox
3011                    jumpsˇ» over
3012                    the lazy dog
3013                "},
3014            Mode::Visual,
3015        );
3016
3017        cx.simulate_keystrokes(": w space dir/other.rs");
3018        cx.simulate_keystrokes("enter");
3019
3020        let other = path!("/root/dir/other.rs");
3021
3022        let _ = cx
3023            .workspace(|workspace, window, cx| {
3024                workspace.open_abs_path(PathBuf::from(other), OpenOptions::default(), window, cx)
3025            })
3026            .await;
3027
3028        cx.workspace(|workspace, _, cx| {
3029            assert_active_item(
3030                workspace,
3031                other,
3032                indoc! {"
3033                        brown fox
3034                        jumps over
3035                    "},
3036                cx,
3037            );
3038        });
3039    }
3040
3041    #[gpui::test]
3042    async fn test_command_matching_lines(cx: &mut TestAppContext) {
3043        let mut cx = NeovimBackedTestContext::new(cx).await;
3044
3045        cx.set_shared_state(indoc! {"
3046            ˇa
3047            b
3048            a
3049            b
3050            a
3051        "})
3052            .await;
3053
3054        cx.simulate_shared_keystrokes(":").await;
3055        cx.simulate_shared_keystrokes("g / a / d").await;
3056        cx.simulate_shared_keystrokes("enter").await;
3057
3058        cx.shared_state().await.assert_eq(indoc! {"
3059            b
3060            b
3061            ˇ"});
3062
3063        cx.simulate_shared_keystrokes("u").await;
3064
3065        cx.shared_state().await.assert_eq(indoc! {"
3066            ˇa
3067            b
3068            a
3069            b
3070            a
3071        "});
3072
3073        cx.simulate_shared_keystrokes(":").await;
3074        cx.simulate_shared_keystrokes("v / a / d").await;
3075        cx.simulate_shared_keystrokes("enter").await;
3076
3077        cx.shared_state().await.assert_eq(indoc! {"
3078            a
3079            a
3080            ˇa"});
3081    }
3082
3083    #[gpui::test]
3084    async fn test_del_marks(cx: &mut TestAppContext) {
3085        let mut cx = NeovimBackedTestContext::new(cx).await;
3086
3087        cx.set_shared_state(indoc! {"
3088            ˇa
3089            b
3090            a
3091            b
3092            a
3093        "})
3094            .await;
3095
3096        cx.simulate_shared_keystrokes("m a").await;
3097
3098        let mark = cx.update_editor(|editor, window, cx| {
3099            let vim = editor.addon::<VimAddon>().unwrap().entity.clone();
3100            vim.update(cx, |vim, cx| vim.get_mark("a", editor, window, cx))
3101        });
3102        assert!(mark.is_some());
3103
3104        cx.simulate_shared_keystrokes(": d e l m space a").await;
3105        cx.simulate_shared_keystrokes("enter").await;
3106
3107        let mark = cx.update_editor(|editor, window, cx| {
3108            let vim = editor.addon::<VimAddon>().unwrap().entity.clone();
3109            vim.update(cx, |vim, cx| vim.get_mark("a", editor, window, cx))
3110        });
3111        assert!(mark.is_none())
3112    }
3113
3114    #[gpui::test]
3115    async fn test_normal_command(cx: &mut TestAppContext) {
3116        let mut cx = NeovimBackedTestContext::new(cx).await;
3117
3118        cx.set_shared_state(indoc! {"
3119            The quick
3120            brown« fox
3121            jumpsˇ» over
3122            the lazy dog
3123        "})
3124            .await;
3125
3126        cx.simulate_shared_keystrokes(": n o r m space w C w o r d")
3127            .await;
3128        cx.simulate_shared_keystrokes("enter").await;
3129
3130        cx.shared_state().await.assert_eq(indoc! {"
3131            The quick
3132            brown word
3133            jumps worˇd
3134            the lazy dog
3135        "});
3136
3137        cx.simulate_shared_keystrokes(": n o r m space _ w c i w t e s t")
3138            .await;
3139        cx.simulate_shared_keystrokes("enter").await;
3140
3141        cx.shared_state().await.assert_eq(indoc! {"
3142            The quick
3143            brown word
3144            jumps tesˇt
3145            the lazy dog
3146        "});
3147
3148        cx.simulate_shared_keystrokes("_ l v l : n o r m space s l a")
3149            .await;
3150        cx.simulate_shared_keystrokes("enter").await;
3151
3152        cx.shared_state().await.assert_eq(indoc! {"
3153            The quick
3154            brown word
3155            lˇaumps test
3156            the lazy dog
3157        "});
3158
3159        cx.set_shared_state(indoc! {"
3160            ˇThe quick
3161            brown fox
3162            jumps over
3163            the lazy dog
3164        "})
3165            .await;
3166
3167        cx.simulate_shared_keystrokes("c i w M y escape").await;
3168
3169        cx.shared_state().await.assert_eq(indoc! {"
3170            Mˇy quick
3171            brown fox
3172            jumps over
3173            the lazy dog
3174        "});
3175
3176        cx.simulate_shared_keystrokes(": n o r m space u").await;
3177        cx.simulate_shared_keystrokes("enter").await;
3178
3179        cx.shared_state().await.assert_eq(indoc! {"
3180            ˇThe quick
3181            brown fox
3182            jumps over
3183            the lazy dog
3184        "});
3185
3186        cx.set_shared_state(indoc! {"
3187            The« quick
3188            brownˇ» fox
3189            jumps over
3190            the lazy dog
3191        "})
3192            .await;
3193
3194        cx.simulate_shared_keystrokes(": n o r m space I 1 2 3")
3195            .await;
3196        cx.simulate_shared_keystrokes("enter").await;
3197        cx.simulate_shared_keystrokes("u").await;
3198
3199        cx.shared_state().await.assert_eq(indoc! {"
3200            ˇThe quick
3201            brown fox
3202            jumps over
3203            the lazy dog
3204        "});
3205
3206        cx.set_shared_state(indoc! {"
3207            ˇquick
3208            brown fox
3209            jumps over
3210            the lazy dog
3211        "})
3212            .await;
3213
3214        cx.simulate_shared_keystrokes(": n o r m space I T h e space")
3215            .await;
3216        cx.simulate_shared_keystrokes("enter").await;
3217
3218        cx.shared_state().await.assert_eq(indoc! {"
3219            Theˇ quick
3220            brown fox
3221            jumps over
3222            the lazy dog
3223        "});
3224
3225        // Once ctrl-v to input character literals is added there should be a test for redo
3226    }
3227
3228    #[gpui::test]
3229    async fn test_command_g_normal(cx: &mut TestAppContext) {
3230        let mut cx = NeovimBackedTestContext::new(cx).await;
3231
3232        cx.set_shared_state(indoc! {"
3233            ˇfoo
3234
3235            foo
3236        "})
3237            .await;
3238
3239        cx.simulate_shared_keystrokes(": % g / f o o / n o r m space A b a r")
3240            .await;
3241        cx.simulate_shared_keystrokes("enter").await;
3242        cx.run_until_parked();
3243
3244        cx.shared_state().await.assert_eq(indoc! {"
3245            foobar
3246
3247            foobaˇr
3248        "});
3249
3250        cx.simulate_shared_keystrokes("u").await;
3251
3252        cx.shared_state().await.assert_eq(indoc! {"
3253            foˇo
3254
3255            foo
3256        "});
3257    }
3258
3259    #[gpui::test]
3260    async fn test_command_tabnew(cx: &mut TestAppContext) {
3261        let mut cx = VimTestContext::new(cx, true).await;
3262
3263        // Create a new file to ensure that, when the filename is used with
3264        // `:tabnew`, it opens the existing file in a new tab.
3265        let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
3266        fs.as_fake()
3267            .insert_file(path!("/root/dir/file_2.rs"), "file_2".as_bytes().to_vec())
3268            .await;
3269
3270        cx.simulate_keystrokes(": tabnew");
3271        cx.simulate_keystrokes("enter");
3272        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
3273
3274        // Assert that the new tab is empty and not associated with any file, as
3275        // no file path was provided to the `:tabnew` command.
3276        cx.workspace(|workspace, _window, cx| {
3277            let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();
3278            let buffer = active_editor
3279                .read(cx)
3280                .buffer()
3281                .read(cx)
3282                .as_singleton()
3283                .unwrap();
3284
3285            assert!(&buffer.read(cx).file().is_none());
3286        });
3287
3288        // Leverage the filename as an argument to the `:tabnew` command,
3289        // ensuring that the file, instead of an empty buffer, is opened in a
3290        // new tab.
3291        cx.simulate_keystrokes(": tabnew space dir/file_2.rs");
3292        cx.simulate_keystrokes("enter");
3293
3294        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 3));
3295        cx.workspace(|workspace, _, cx| {
3296            assert_active_item(workspace, path!("/root/dir/file_2.rs"), "file_2", cx);
3297        });
3298
3299        // If the `filename` argument provided to the `:tabnew` command is for a
3300        // file that doesn't yet exist, it should still associate the buffer
3301        // with that file path, so that when the buffer contents are saved, the
3302        // file is created.
3303        cx.simulate_keystrokes(": tabnew space dir/file_3.rs");
3304        cx.simulate_keystrokes("enter");
3305
3306        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 4));
3307        cx.workspace(|workspace, _, cx| {
3308            assert_active_item(workspace, path!("/root/dir/file_3.rs"), "", cx);
3309        });
3310    }
3311
3312    #[gpui::test]
3313    async fn test_command_tabedit(cx: &mut TestAppContext) {
3314        let mut cx = VimTestContext::new(cx, true).await;
3315
3316        // Create a new file to ensure that, when the filename is used with
3317        // `:tabedit`, it opens the existing file in a new tab.
3318        let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
3319        fs.as_fake()
3320            .insert_file(path!("/root/dir/file_2.rs"), "file_2".as_bytes().to_vec())
3321            .await;
3322
3323        cx.simulate_keystrokes(": tabedit");
3324        cx.simulate_keystrokes("enter");
3325        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
3326
3327        // Assert that the new tab is empty and not associated with any file, as
3328        // no file path was provided to the `:tabedit` command.
3329        cx.workspace(|workspace, _window, cx| {
3330            let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();
3331            let buffer = active_editor
3332                .read(cx)
3333                .buffer()
3334                .read(cx)
3335                .as_singleton()
3336                .unwrap();
3337
3338            assert!(&buffer.read(cx).file().is_none());
3339        });
3340
3341        // Leverage the filename as an argument to the `:tabedit` command,
3342        // ensuring that the file, instead of an empty buffer, is opened in a
3343        // new tab.
3344        cx.simulate_keystrokes(": tabedit space dir/file_2.rs");
3345        cx.simulate_keystrokes("enter");
3346
3347        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 3));
3348        cx.workspace(|workspace, _, cx| {
3349            assert_active_item(workspace, path!("/root/dir/file_2.rs"), "file_2", cx);
3350        });
3351
3352        // If the `filename` argument provided to the `:tabedit` command is for a
3353        // file that doesn't yet exist, it should still associate the buffer
3354        // with that file path, so that when the buffer contents are saved, the
3355        // file is created.
3356        cx.simulate_keystrokes(": tabedit space dir/file_3.rs");
3357        cx.simulate_keystrokes("enter");
3358
3359        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 4));
3360        cx.workspace(|workspace, _, cx| {
3361            assert_active_item(workspace, path!("/root/dir/file_3.rs"), "", cx);
3362        });
3363    }
3364
3365    #[gpui::test]
3366    async fn test_ignorecase_command(cx: &mut TestAppContext) {
3367        let mut cx = VimTestContext::new(cx, true).await;
3368        cx.read(|cx| {
3369            assert_eq!(
3370                EditorSettings::get_global(cx).search.case_sensitive,
3371                false,
3372                "The `case_sensitive` setting should be `false` by default."
3373            );
3374        });
3375        cx.simulate_keystrokes(": set space noignorecase");
3376        cx.simulate_keystrokes("enter");
3377        cx.read(|cx| {
3378            assert_eq!(
3379                EditorSettings::get_global(cx).search.case_sensitive,
3380                true,
3381                "The `case_sensitive` setting should have been enabled with `:set noignorecase`."
3382            );
3383        });
3384        cx.simulate_keystrokes(": set space ignorecase");
3385        cx.simulate_keystrokes("enter");
3386        cx.read(|cx| {
3387            assert_eq!(
3388                EditorSettings::get_global(cx).search.case_sensitive,
3389                false,
3390                "The `case_sensitive` setting should have been disabled with `:set ignorecase`."
3391            );
3392        });
3393        cx.simulate_keystrokes(": set space noic");
3394        cx.simulate_keystrokes("enter");
3395        cx.read(|cx| {
3396            assert_eq!(
3397                EditorSettings::get_global(cx).search.case_sensitive,
3398                true,
3399                "The `case_sensitive` setting should have been enabled with `:set noic`."
3400            );
3401        });
3402        cx.simulate_keystrokes(": set space ic");
3403        cx.simulate_keystrokes("enter");
3404        cx.read(|cx| {
3405            assert_eq!(
3406                EditorSettings::get_global(cx).search.case_sensitive,
3407                false,
3408                "The `case_sensitive` setting should have been disabled with `:set ic`."
3409            );
3410        });
3411    }
3412
3413    #[gpui::test]
3414    async fn test_sort_commands(cx: &mut TestAppContext) {
3415        let mut cx = VimTestContext::new(cx, true).await;
3416
3417        cx.set_state(
3418            indoc! {"
3419                «hornet
3420                quirrel
3421                elderbug
3422                cornifer
3423                idaˇ»
3424            "},
3425            Mode::Visual,
3426        );
3427
3428        cx.simulate_keystrokes(": sort");
3429        cx.simulate_keystrokes("enter");
3430
3431        cx.assert_state(
3432            indoc! {"
3433                ˇcornifer
3434                elderbug
3435                hornet
3436                ida
3437                quirrel
3438            "},
3439            Mode::Normal,
3440        );
3441
3442        // Assert that, by default, `:sort` takes case into consideration.
3443        cx.set_state(
3444            indoc! {"
3445                «hornet
3446                quirrel
3447                Elderbug
3448                cornifer
3449                idaˇ»
3450            "},
3451            Mode::Visual,
3452        );
3453
3454        cx.simulate_keystrokes(": sort");
3455        cx.simulate_keystrokes("enter");
3456
3457        cx.assert_state(
3458            indoc! {"
3459                ˇElderbug
3460                cornifer
3461                hornet
3462                ida
3463                quirrel
3464            "},
3465            Mode::Normal,
3466        );
3467
3468        // Assert that, if the `i` option is passed, `:sort` ignores case.
3469        cx.set_state(
3470            indoc! {"
3471                «hornet
3472                quirrel
3473                Elderbug
3474                cornifer
3475                idaˇ»
3476            "},
3477            Mode::Visual,
3478        );
3479
3480        cx.simulate_keystrokes(": sort space i");
3481        cx.simulate_keystrokes("enter");
3482
3483        cx.assert_state(
3484            indoc! {"
3485                ˇcornifer
3486                Elderbug
3487                hornet
3488                ida
3489                quirrel
3490            "},
3491            Mode::Normal,
3492        );
3493
3494        // When no range is provided, sorts the whole buffer.
3495        cx.set_state(
3496            indoc! {"
3497                ˇhornet
3498                quirrel
3499                elderbug
3500                cornifer
3501                ida
3502            "},
3503            Mode::Normal,
3504        );
3505
3506        cx.simulate_keystrokes(": sort");
3507        cx.simulate_keystrokes("enter");
3508
3509        cx.assert_state(
3510            indoc! {"
3511                ˇcornifer
3512                elderbug
3513                hornet
3514                ida
3515                quirrel
3516            "},
3517            Mode::Normal,
3518        );
3519    }
3520}