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