command.rs

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