command.rs

   1use anyhow::Result;
   2use collections::HashMap;
   3use command_palette_hooks::CommandInterceptResult;
   4use editor::{
   5    Bias, Editor, ToPoint,
   6    actions::{SortLinesCaseInsensitive, SortLinesCaseSensitive},
   7    display_map::ToDisplayPoint,
   8    scroll::Autoscroll,
   9};
  10use gpui::{Action, App, AppContext as _, Context, Global, Window, actions, impl_internal_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    io::Write,
  21    iter::Peekable,
  22    ops::{Deref, Range},
  23    path::Path,
  24    process::Stdio,
  25    str::Chars,
  26    sync::{Arc, OnceLock},
  27    time::Instant,
  28};
  29use task::{HideStrategy, RevealStrategy, SpawnInTerminal, TaskId};
  30use ui::ActiveTheme;
  31use util::ResultExt;
  32use workspace::notifications::DetachAndPromptErr;
  33use workspace::{Item, SaveIntent, notifications::NotifyResultExt};
  34use zed_actions::{OpenDocs, RevealTarget};
  35
  36use crate::{
  37    ToggleMarksView, ToggleRegistersView, Vim,
  38    motion::{EndOfDocument, Motion, MotionKind, StartOfDocument},
  39    normal::{
  40        JoinLines,
  41        search::{FindCommand, ReplaceCommand, Replacement},
  42    },
  43    object::Object,
  44    state::{Mark, Mode},
  45    visual::VisualDeleteLine,
  46};
  47
  48#[derive(Clone, Debug, PartialEq)]
  49pub struct GoToLine {
  50    range: CommandRange,
  51}
  52
  53#[derive(Clone, Debug, PartialEq)]
  54pub struct YankCommand {
  55    range: CommandRange,
  56}
  57
  58#[derive(Clone, Debug, PartialEq)]
  59pub struct WithRange {
  60    restore_selection: bool,
  61    range: CommandRange,
  62    action: WrappedAction,
  63}
  64
  65#[derive(Clone, Debug, PartialEq)]
  66pub struct WithCount {
  67    count: u32,
  68    action: WrappedAction,
  69}
  70
  71#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
  72pub enum VimOption {
  73    Wrap(bool),
  74    Number(bool),
  75    RelativeNumber(bool),
  76}
  77
  78impl VimOption {
  79    fn possible_commands(query: &str) -> Vec<CommandInterceptResult> {
  80        let mut prefix_of_options = Vec::new();
  81        let mut options = query.split(" ").collect::<Vec<_>>();
  82        let prefix = options.pop().unwrap_or_default();
  83        for option in options {
  84            if let Some(opt) = Self::from(option) {
  85                prefix_of_options.push(opt)
  86            } else {
  87                return vec![];
  88            }
  89        }
  90
  91        Self::possibilities(&prefix)
  92            .map(|possible| {
  93                let mut options = prefix_of_options.clone();
  94                options.push(possible);
  95
  96                CommandInterceptResult {
  97                    string: format!(
  98                        ":set {}",
  99                        options.iter().map(|opt| opt.to_string()).join(" ")
 100                    ),
 101                    action: VimSet { options }.boxed_clone(),
 102                    positions: vec![],
 103                }
 104            })
 105            .collect()
 106    }
 107
 108    fn possibilities(query: &str) -> impl Iterator<Item = Self> + '_ {
 109        [
 110            (None, VimOption::Wrap(true)),
 111            (None, VimOption::Wrap(false)),
 112            (None, VimOption::Number(true)),
 113            (None, VimOption::Number(false)),
 114            (None, VimOption::RelativeNumber(true)),
 115            (None, VimOption::RelativeNumber(false)),
 116            (Some("rnu"), VimOption::RelativeNumber(true)),
 117            (Some("nornu"), VimOption::RelativeNumber(false)),
 118        ]
 119        .into_iter()
 120        .filter(move |(prefix, option)| prefix.unwrap_or(option.to_string()).starts_with(query))
 121        .map(|(_, option)| option)
 122    }
 123
 124    fn from(option: &str) -> Option<Self> {
 125        match option {
 126            "wrap" => Some(Self::Wrap(true)),
 127            "nowrap" => Some(Self::Wrap(false)),
 128
 129            "number" => Some(Self::Number(true)),
 130            "nu" => Some(Self::Number(true)),
 131            "nonumber" => Some(Self::Number(false)),
 132            "nonu" => Some(Self::Number(false)),
 133
 134            "relativenumber" => Some(Self::RelativeNumber(true)),
 135            "rnu" => Some(Self::RelativeNumber(true)),
 136            "norelativenumber" => Some(Self::RelativeNumber(false)),
 137            "nornu" => Some(Self::RelativeNumber(false)),
 138
 139            _ => None,
 140        }
 141    }
 142
 143    fn to_string(&self) -> &'static str {
 144        match self {
 145            VimOption::Wrap(true) => "wrap",
 146            VimOption::Wrap(false) => "nowrap",
 147            VimOption::Number(true) => "number",
 148            VimOption::Number(false) => "nonumber",
 149            VimOption::RelativeNumber(true) => "relativenumber",
 150            VimOption::RelativeNumber(false) => "norelativenumber",
 151        }
 152    }
 153}
 154
 155#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 156pub struct VimSet {
 157    options: Vec<VimOption>,
 158}
 159
 160#[derive(Debug)]
 161struct WrappedAction(Box<dyn Action>);
 162
 163#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 164struct VimSave {
 165    pub save_intent: Option<SaveIntent>,
 166    pub filename: String,
 167}
 168
 169actions!(vim, [VisualCommand, CountCommand, ShellCommand]);
 170impl_internal_actions!(
 171    vim,
 172    [
 173        GoToLine,
 174        YankCommand,
 175        WithRange,
 176        WithCount,
 177        OnMatchingLines,
 178        ShellExec,
 179        VimSet,
 180        VimSave,
 181    ]
 182);
 183
 184impl PartialEq for WrappedAction {
 185    fn eq(&self, other: &Self) -> bool {
 186        self.0.partial_eq(&*other.0)
 187    }
 188}
 189
 190impl Clone for WrappedAction {
 191    fn clone(&self) -> Self {
 192        Self(self.0.boxed_clone())
 193    }
 194}
 195
 196impl Deref for WrappedAction {
 197    type Target = dyn Action;
 198    fn deref(&self) -> &dyn Action {
 199        &*self.0
 200    }
 201}
 202
 203pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
 204    // Vim::action(editor, cx, |vim, action: &StartOfLine, window, cx| {
 205    Vim::action(editor, cx, |vim, action: &VimSet, window, cx| {
 206        for option in action.options.iter() {
 207            vim.update_editor(window, cx, |_, editor, _, cx| match option {
 208                VimOption::Wrap(true) => {
 209                    editor
 210                        .set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
 211                }
 212                VimOption::Wrap(false) => {
 213                    editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);
 214                }
 215                VimOption::Number(enabled) => {
 216                    editor.set_show_line_numbers(*enabled, cx);
 217                }
 218                VimOption::RelativeNumber(enabled) => {
 219                    editor.set_relative_line_number(Some(*enabled), cx);
 220                }
 221            });
 222        }
 223    });
 224    Vim::action(editor, cx, |vim, _: &VisualCommand, window, cx| {
 225        let Some(workspace) = vim.workspace(window) else {
 226            return;
 227        };
 228        workspace.update(cx, |workspace, cx| {
 229            command_palette::CommandPalette::toggle(workspace, "'<,'>", window, cx);
 230        })
 231    });
 232
 233    Vim::action(editor, cx, |vim, _: &ShellCommand, window, cx| {
 234        let Some(workspace) = vim.workspace(window) else {
 235            return;
 236        };
 237        workspace.update(cx, |workspace, cx| {
 238            command_palette::CommandPalette::toggle(workspace, "'<,'>!", window, cx);
 239        })
 240    });
 241
 242    Vim::action(editor, cx, |vim, action: &VimSave, window, cx| {
 243        vim.update_editor(window, cx, |_, editor, window, cx| {
 244            let Some(project) = editor.project.clone() else {
 245                return;
 246            };
 247            let Some(worktree) = project.read(cx).visible_worktrees(cx).next() else {
 248                return;
 249            };
 250            let project_path = ProjectPath {
 251                worktree_id: worktree.read(cx).id(),
 252                path: Arc::from(Path::new(&action.filename)),
 253            };
 254
 255            if project.read(cx).entry_for_path(&project_path, cx).is_some() && action.save_intent != Some(SaveIntent::Overwrite) {
 256                let answer = window.prompt(
 257                    gpui::PromptLevel::Critical,
 258                    &format!("{} already exists. Do you want to replace it?", project_path.path.to_string_lossy()),
 259                    Some(
 260                        "A file or folder with the same name already exists. Replacing it will overwrite its current contents.",
 261                    ),
 262                    &["Replace", "Cancel"],
 263                cx);
 264                cx.spawn_in(window, async move |editor, cx| {
 265                    if answer.await.ok() != Some(0) {
 266                        return;
 267                    }
 268
 269                    let _ = editor.update_in(cx, |editor, window, cx|{
 270                        editor
 271                            .save_as(project, project_path, window, cx)
 272                            .detach_and_prompt_err("Failed to :w", window, cx, |_, _, _| None);
 273                    });
 274                }).detach();
 275            } else {
 276                editor
 277                    .save_as(project, project_path, window, cx)
 278                    .detach_and_prompt_err("Failed to :w", window, cx, |_, _, _| None);
 279            }
 280        });
 281    });
 282
 283    Vim::action(editor, cx, |vim, _: &CountCommand, window, cx| {
 284        let Some(workspace) = vim.workspace(window) else {
 285            return;
 286        };
 287        let count = Vim::take_count(cx).unwrap_or(1);
 288        Vim::take_forced_motion(cx);
 289        let n = if count > 1 {
 290            format!(".,.+{}", count.saturating_sub(1))
 291        } else {
 292            ".".to_string()
 293        };
 294        workspace.update(cx, |workspace, cx| {
 295            command_palette::CommandPalette::toggle(workspace, &n, window, cx);
 296        })
 297    });
 298
 299    Vim::action(editor, cx, |vim, action: &GoToLine, window, cx| {
 300        vim.switch_mode(Mode::Normal, false, window, cx);
 301        let result = vim.update_editor(window, cx, |vim, editor, window, cx| {
 302            let snapshot = editor.snapshot(window, cx);
 303            let buffer_row = action.range.head().buffer_row(vim, editor, window, cx)?;
 304            let current = editor.selections.newest::<Point>(cx);
 305            let target = snapshot
 306                .buffer_snapshot
 307                .clip_point(Point::new(buffer_row.0, current.head().column), Bias::Left);
 308            editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 309                s.select_ranges([target..target]);
 310            });
 311
 312            anyhow::Ok(())
 313        });
 314        if let Some(e @ Err(_)) = result {
 315            let Some(workspace) = vim.workspace(window) else {
 316                return;
 317            };
 318            workspace.update(cx, |workspace, cx| {
 319                e.notify_err(workspace, cx);
 320            });
 321            return;
 322        }
 323    });
 324
 325    Vim::action(editor, cx, |vim, action: &YankCommand, window, cx| {
 326        vim.update_editor(window, cx, |vim, editor, window, cx| {
 327            let snapshot = editor.snapshot(window, cx);
 328            if let Ok(range) = action.range.buffer_range(vim, editor, window, cx) {
 329                let end = if range.end < snapshot.buffer_snapshot.max_row() {
 330                    Point::new(range.end.0 + 1, 0)
 331                } else {
 332                    snapshot.buffer_snapshot.max_point()
 333                };
 334                vim.copy_ranges(
 335                    editor,
 336                    MotionKind::Linewise,
 337                    true,
 338                    vec![Point::new(range.start.0, 0)..end],
 339                    window,
 340                    cx,
 341                )
 342            }
 343        });
 344    });
 345
 346    Vim::action(editor, cx, |_, action: &WithCount, window, cx| {
 347        for _ in 0..action.count {
 348            window.dispatch_action(action.action.boxed_clone(), cx)
 349        }
 350    });
 351
 352    Vim::action(editor, cx, |vim, action: &WithRange, window, cx| {
 353        let result = vim.update_editor(window, cx, |vim, editor, window, cx| {
 354            action.range.buffer_range(vim, editor, window, cx)
 355        });
 356
 357        let range = match result {
 358            None => return,
 359            Some(e @ Err(_)) => {
 360                let Some(workspace) = vim.workspace(window) else {
 361                    return;
 362                };
 363                workspace.update(cx, |workspace, cx| {
 364                    e.notify_err(workspace, cx);
 365                });
 366                return;
 367            }
 368            Some(Ok(result)) => result,
 369        };
 370
 371        let previous_selections = vim
 372            .update_editor(window, cx, |_, editor, window, cx| {
 373                let selections = action.restore_selection.then(|| {
 374                    editor
 375                        .selections
 376                        .disjoint_anchor_ranges()
 377                        .collect::<Vec<_>>()
 378                });
 379                editor.change_selections(None, window, cx, |s| {
 380                    let end = Point::new(range.end.0, s.buffer().line_len(range.end));
 381                    s.select_ranges([end..Point::new(range.start.0, 0)]);
 382                });
 383                selections
 384            })
 385            .flatten();
 386        window.dispatch_action(action.action.boxed_clone(), cx);
 387        cx.defer_in(window, move |vim, window, cx| {
 388            vim.update_editor(window, cx, |_, editor, window, cx| {
 389                editor.change_selections(None, window, cx, |s| {
 390                    if let Some(previous_selections) = previous_selections {
 391                        s.select_ranges(previous_selections);
 392                    } else {
 393                        s.select_ranges([
 394                            Point::new(range.start.0, 0)..Point::new(range.start.0, 0)
 395                        ]);
 396                    }
 397                })
 398            });
 399        });
 400    });
 401
 402    Vim::action(editor, cx, |vim, action: &OnMatchingLines, window, cx| {
 403        action.run(vim, window, cx)
 404    });
 405
 406    Vim::action(editor, cx, |vim, action: &ShellExec, window, cx| {
 407        action.run(vim, window, cx)
 408    })
 409}
 410
 411#[derive(Default)]
 412struct VimCommand {
 413    prefix: &'static str,
 414    suffix: &'static str,
 415    action: Option<Box<dyn Action>>,
 416    action_name: Option<&'static str>,
 417    bang_action: Option<Box<dyn Action>>,
 418    args: Option<
 419        Box<dyn Fn(Box<dyn Action>, String) -> Option<Box<dyn Action>> + Send + Sync + 'static>,
 420    >,
 421    range: Option<
 422        Box<
 423            dyn Fn(Box<dyn Action>, &CommandRange) -> Option<Box<dyn Action>>
 424                + Send
 425                + Sync
 426                + 'static,
 427        >,
 428    >,
 429    has_count: bool,
 430}
 431
 432impl VimCommand {
 433    fn new(pattern: (&'static str, &'static str), action: impl Action) -> Self {
 434        Self {
 435            prefix: pattern.0,
 436            suffix: pattern.1,
 437            action: Some(action.boxed_clone()),
 438            ..Default::default()
 439        }
 440    }
 441
 442    // from_str is used for actions in other crates.
 443    fn str(pattern: (&'static str, &'static str), action_name: &'static str) -> Self {
 444        Self {
 445            prefix: pattern.0,
 446            suffix: pattern.1,
 447            action_name: Some(action_name),
 448            ..Default::default()
 449        }
 450    }
 451
 452    fn bang(mut self, bang_action: impl Action) -> Self {
 453        self.bang_action = Some(bang_action.boxed_clone());
 454        self
 455    }
 456
 457    fn args(
 458        mut self,
 459        f: impl Fn(Box<dyn Action>, String) -> Option<Box<dyn Action>> + Send + Sync + 'static,
 460    ) -> Self {
 461        self.args = Some(Box::new(f));
 462        self
 463    }
 464
 465    fn range(
 466        mut self,
 467        f: impl Fn(Box<dyn Action>, &CommandRange) -> Option<Box<dyn Action>> + Send + Sync + 'static,
 468    ) -> Self {
 469        self.range = Some(Box::new(f));
 470        self
 471    }
 472
 473    fn count(mut self) -> Self {
 474        self.has_count = true;
 475        self
 476    }
 477
 478    fn parse(
 479        &self,
 480        query: &str,
 481        range: &Option<CommandRange>,
 482        cx: &App,
 483    ) -> Option<Box<dyn Action>> {
 484        let rest = query
 485            .to_string()
 486            .strip_prefix(self.prefix)?
 487            .to_string()
 488            .chars()
 489            .zip_longest(self.suffix.to_string().chars())
 490            .skip_while(|e| e.clone().both().map(|(s, q)| s == q).unwrap_or(false))
 491            .filter_map(|e| e.left())
 492            .collect::<String>();
 493        let has_bang = rest.starts_with('!');
 494        let args = if has_bang {
 495            rest.strip_prefix('!')?.trim().to_string()
 496        } else if rest.is_empty() {
 497            "".into()
 498        } else {
 499            rest.strip_prefix(' ')?.trim().to_string()
 500        };
 501
 502        let action = if has_bang && self.bang_action.is_some() {
 503            self.bang_action.as_ref().unwrap().boxed_clone()
 504        } else if let Some(action) = self.action.as_ref() {
 505            action.boxed_clone()
 506        } else if let Some(action_name) = self.action_name {
 507            cx.build_action(action_name, None).log_err()?
 508        } else {
 509            return None;
 510        };
 511        if !args.is_empty() {
 512            // if command does not accept args and we have args then we should do no action
 513            if let Some(args_fn) = &self.args {
 514                args_fn.deref()(action, args)
 515            } else {
 516                None
 517            }
 518        } else if let Some(range) = range {
 519            self.range.as_ref().and_then(|f| f(action, range))
 520        } else {
 521            Some(action)
 522        }
 523    }
 524
 525    // TODO: ranges with search queries
 526    fn parse_range(query: &str) -> (Option<CommandRange>, String) {
 527        let mut chars = query.chars().peekable();
 528
 529        match chars.peek() {
 530            Some('%') => {
 531                chars.next();
 532                return (
 533                    Some(CommandRange {
 534                        start: Position::Line { row: 1, offset: 0 },
 535                        end: Some(Position::LastLine { offset: 0 }),
 536                    }),
 537                    chars.collect(),
 538                );
 539            }
 540            Some('*') => {
 541                chars.next();
 542                return (
 543                    Some(CommandRange {
 544                        start: Position::Mark {
 545                            name: '<',
 546                            offset: 0,
 547                        },
 548                        end: Some(Position::Mark {
 549                            name: '>',
 550                            offset: 0,
 551                        }),
 552                    }),
 553                    chars.collect(),
 554                );
 555            }
 556            _ => {}
 557        }
 558
 559        let start = Self::parse_position(&mut chars);
 560
 561        match chars.peek() {
 562            Some(',' | ';') => {
 563                chars.next();
 564                (
 565                    Some(CommandRange {
 566                        start: start.unwrap_or(Position::CurrentLine { offset: 0 }),
 567                        end: Self::parse_position(&mut chars),
 568                    }),
 569                    chars.collect(),
 570                )
 571            }
 572            _ => (
 573                start.map(|start| CommandRange { start, end: None }),
 574                chars.collect(),
 575            ),
 576        }
 577    }
 578
 579    fn parse_position(chars: &mut Peekable<Chars>) -> Option<Position> {
 580        match chars.peek()? {
 581            '0'..='9' => {
 582                let row = Self::parse_u32(chars);
 583                Some(Position::Line {
 584                    row,
 585                    offset: Self::parse_offset(chars),
 586                })
 587            }
 588            '\'' => {
 589                chars.next();
 590                let name = chars.next()?;
 591                Some(Position::Mark {
 592                    name,
 593                    offset: Self::parse_offset(chars),
 594                })
 595            }
 596            '.' => {
 597                chars.next();
 598                Some(Position::CurrentLine {
 599                    offset: Self::parse_offset(chars),
 600                })
 601            }
 602            '+' | '-' => Some(Position::CurrentLine {
 603                offset: Self::parse_offset(chars),
 604            }),
 605            '$' => {
 606                chars.next();
 607                Some(Position::LastLine {
 608                    offset: Self::parse_offset(chars),
 609                })
 610            }
 611            _ => None,
 612        }
 613    }
 614
 615    fn parse_offset(chars: &mut Peekable<Chars>) -> i32 {
 616        let mut res: i32 = 0;
 617        while matches!(chars.peek(), Some('+' | '-')) {
 618            let sign = if chars.next().unwrap() == '+' { 1 } else { -1 };
 619            let amount = if matches!(chars.peek(), Some('0'..='9')) {
 620                (Self::parse_u32(chars) as i32).saturating_mul(sign)
 621            } else {
 622                sign
 623            };
 624            res = res.saturating_add(amount)
 625        }
 626        res
 627    }
 628
 629    fn parse_u32(chars: &mut Peekable<Chars>) -> u32 {
 630        let mut res: u32 = 0;
 631        while matches!(chars.peek(), Some('0'..='9')) {
 632            res = res
 633                .saturating_mul(10)
 634                .saturating_add(chars.next().unwrap() as u32 - '0' as u32);
 635        }
 636        res
 637    }
 638}
 639
 640#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq)]
 641enum Position {
 642    Line { row: u32, offset: i32 },
 643    Mark { name: char, offset: i32 },
 644    LastLine { offset: i32 },
 645    CurrentLine { offset: i32 },
 646}
 647
 648impl Position {
 649    fn buffer_row(
 650        &self,
 651        vim: &Vim,
 652        editor: &mut Editor,
 653        window: &mut Window,
 654        cx: &mut App,
 655    ) -> Result<MultiBufferRow> {
 656        let snapshot = editor.snapshot(window, cx);
 657        let target = match self {
 658            Position::Line { row, offset } => {
 659                if let Some(anchor) = editor.active_excerpt(cx).and_then(|(_, buffer, _)| {
 660                    editor.buffer().read(cx).buffer_point_to_anchor(
 661                        &buffer,
 662                        Point::new(row.saturating_sub(1), 0),
 663                        cx,
 664                    )
 665                }) {
 666                    anchor
 667                        .to_point(&snapshot.buffer_snapshot)
 668                        .row
 669                        .saturating_add_signed(*offset)
 670                } else {
 671                    row.saturating_add_signed(offset.saturating_sub(1))
 672                }
 673            }
 674            Position::Mark { name, offset } => {
 675                let Some(Mark::Local(anchors)) =
 676                    vim.get_mark(&name.to_string(), editor, window, cx)
 677                else {
 678                    anyhow::bail!("mark {name} not set");
 679                };
 680                let Some(mark) = anchors.last() else {
 681                    anyhow::bail!("mark {name} contains empty anchors");
 682                };
 683                mark.to_point(&snapshot.buffer_snapshot)
 684                    .row
 685                    .saturating_add_signed(*offset)
 686            }
 687            Position::LastLine { offset } => snapshot
 688                .buffer_snapshot
 689                .max_row()
 690                .0
 691                .saturating_add_signed(*offset),
 692            Position::CurrentLine { offset } => editor
 693                .selections
 694                .newest_anchor()
 695                .head()
 696                .to_point(&snapshot.buffer_snapshot)
 697                .row
 698                .saturating_add_signed(*offset),
 699        };
 700
 701        Ok(MultiBufferRow(target).min(snapshot.buffer_snapshot.max_row()))
 702    }
 703}
 704
 705#[derive(Clone, Debug, PartialEq)]
 706pub(crate) struct CommandRange {
 707    start: Position,
 708    end: Option<Position>,
 709}
 710
 711impl CommandRange {
 712    fn head(&self) -> &Position {
 713        self.end.as_ref().unwrap_or(&self.start)
 714    }
 715
 716    pub(crate) fn buffer_range(
 717        &self,
 718        vim: &Vim,
 719        editor: &mut Editor,
 720        window: &mut Window,
 721        cx: &mut App,
 722    ) -> Result<Range<MultiBufferRow>> {
 723        let start = self.start.buffer_row(vim, editor, window, cx)?;
 724        let end = if let Some(end) = self.end.as_ref() {
 725            end.buffer_row(vim, editor, window, cx)?
 726        } else {
 727            start
 728        };
 729        if end < start {
 730            anyhow::Ok(end..start)
 731        } else {
 732            anyhow::Ok(start..end)
 733        }
 734    }
 735
 736    pub fn as_count(&self) -> Option<u32> {
 737        if let CommandRange {
 738            start: Position::Line { row, offset: 0 },
 739            end: None,
 740        } = &self
 741        {
 742            Some(*row)
 743        } else {
 744            None
 745        }
 746    }
 747}
 748
 749fn generate_commands(_: &App) -> Vec<VimCommand> {
 750    vec![
 751        VimCommand::new(
 752            ("w", "rite"),
 753            workspace::Save {
 754                save_intent: Some(SaveIntent::Save),
 755            },
 756        )
 757        .bang(workspace::Save {
 758            save_intent: Some(SaveIntent::Overwrite),
 759        })
 760        .args(|action, args| {
 761            Some(
 762                VimSave {
 763                    save_intent: action
 764                        .as_any()
 765                        .downcast_ref::<workspace::Save>()
 766                        .and_then(|action| action.save_intent),
 767                    filename: args,
 768                }
 769                .boxed_clone(),
 770            )
 771        }),
 772        VimCommand::new(
 773            ("q", "uit"),
 774            workspace::CloseActiveItem {
 775                save_intent: Some(SaveIntent::Close),
 776                close_pinned: false,
 777            },
 778        )
 779        .bang(workspace::CloseActiveItem {
 780            save_intent: Some(SaveIntent::Skip),
 781            close_pinned: true,
 782        }),
 783        VimCommand::new(
 784            ("wq", ""),
 785            workspace::CloseActiveItem {
 786                save_intent: Some(SaveIntent::Save),
 787                close_pinned: false,
 788            },
 789        )
 790        .bang(workspace::CloseActiveItem {
 791            save_intent: Some(SaveIntent::Overwrite),
 792            close_pinned: true,
 793        }),
 794        VimCommand::new(
 795            ("x", "it"),
 796            workspace::CloseActiveItem {
 797                save_intent: Some(SaveIntent::SaveAll),
 798                close_pinned: false,
 799            },
 800        )
 801        .bang(workspace::CloseActiveItem {
 802            save_intent: Some(SaveIntent::Overwrite),
 803            close_pinned: true,
 804        }),
 805        VimCommand::new(
 806            ("exi", "t"),
 807            workspace::CloseActiveItem {
 808                save_intent: Some(SaveIntent::SaveAll),
 809                close_pinned: false,
 810            },
 811        )
 812        .bang(workspace::CloseActiveItem {
 813            save_intent: Some(SaveIntent::Overwrite),
 814            close_pinned: true,
 815        }),
 816        VimCommand::new(
 817            ("up", "date"),
 818            workspace::Save {
 819                save_intent: Some(SaveIntent::SaveAll),
 820            },
 821        ),
 822        VimCommand::new(
 823            ("wa", "ll"),
 824            workspace::SaveAll {
 825                save_intent: Some(SaveIntent::SaveAll),
 826            },
 827        )
 828        .bang(workspace::SaveAll {
 829            save_intent: Some(SaveIntent::Overwrite),
 830        }),
 831        VimCommand::new(
 832            ("qa", "ll"),
 833            workspace::CloseAllItemsAndPanes {
 834                save_intent: Some(SaveIntent::Close),
 835            },
 836        )
 837        .bang(workspace::CloseAllItemsAndPanes {
 838            save_intent: Some(SaveIntent::Skip),
 839        }),
 840        VimCommand::new(
 841            ("quita", "ll"),
 842            workspace::CloseAllItemsAndPanes {
 843                save_intent: Some(SaveIntent::Close),
 844            },
 845        )
 846        .bang(workspace::CloseAllItemsAndPanes {
 847            save_intent: Some(SaveIntent::Skip),
 848        }),
 849        VimCommand::new(
 850            ("xa", "ll"),
 851            workspace::CloseAllItemsAndPanes {
 852                save_intent: Some(SaveIntent::SaveAll),
 853            },
 854        )
 855        .bang(workspace::CloseAllItemsAndPanes {
 856            save_intent: Some(SaveIntent::Overwrite),
 857        }),
 858        VimCommand::new(
 859            ("wqa", "ll"),
 860            workspace::CloseAllItemsAndPanes {
 861                save_intent: Some(SaveIntent::SaveAll),
 862            },
 863        )
 864        .bang(workspace::CloseAllItemsAndPanes {
 865            save_intent: Some(SaveIntent::Overwrite),
 866        }),
 867        VimCommand::new(("cq", "uit"), zed_actions::Quit),
 868        VimCommand::new(("sp", "lit"), workspace::SplitHorizontal),
 869        VimCommand::new(("vs", "plit"), workspace::SplitVertical),
 870        VimCommand::new(
 871            ("bd", "elete"),
 872            workspace::CloseActiveItem {
 873                save_intent: Some(SaveIntent::Close),
 874                close_pinned: false,
 875            },
 876        )
 877        .bang(workspace::CloseActiveItem {
 878            save_intent: Some(SaveIntent::Skip),
 879            close_pinned: true,
 880        }),
 881        VimCommand::new(("bn", "ext"), workspace::ActivateNextItem).count(),
 882        VimCommand::new(("bN", "ext"), workspace::ActivatePreviousItem).count(),
 883        VimCommand::new(("bp", "revious"), workspace::ActivatePreviousItem).count(),
 884        VimCommand::new(("bf", "irst"), workspace::ActivateItem(0)),
 885        VimCommand::new(("br", "ewind"), workspace::ActivateItem(0)),
 886        VimCommand::new(("bl", "ast"), workspace::ActivateLastItem),
 887        VimCommand::str(("buffers", ""), "tab_switcher::ToggleAll"),
 888        VimCommand::str(("ls", ""), "tab_switcher::ToggleAll"),
 889        VimCommand::new(("new", ""), workspace::NewFileSplitHorizontal),
 890        VimCommand::new(("vne", "w"), workspace::NewFileSplitVertical),
 891        VimCommand::new(("tabe", "dit"), workspace::NewFile),
 892        VimCommand::new(("tabnew", ""), workspace::NewFile),
 893        VimCommand::new(("tabn", "ext"), workspace::ActivateNextItem).count(),
 894        VimCommand::new(("tabp", "revious"), workspace::ActivatePreviousItem).count(),
 895        VimCommand::new(("tabN", "ext"), workspace::ActivatePreviousItem).count(),
 896        VimCommand::new(
 897            ("tabc", "lose"),
 898            workspace::CloseActiveItem {
 899                save_intent: Some(SaveIntent::Close),
 900                close_pinned: false,
 901            },
 902        ),
 903        VimCommand::new(
 904            ("tabo", "nly"),
 905            workspace::CloseInactiveItems {
 906                save_intent: Some(SaveIntent::Close),
 907                close_pinned: false,
 908            },
 909        )
 910        .bang(workspace::CloseInactiveItems {
 911            save_intent: Some(SaveIntent::Skip),
 912            close_pinned: false,
 913        }),
 914        VimCommand::new(
 915            ("on", "ly"),
 916            workspace::CloseInactiveTabsAndPanes {
 917                save_intent: Some(SaveIntent::Close),
 918            },
 919        )
 920        .bang(workspace::CloseInactiveTabsAndPanes {
 921            save_intent: Some(SaveIntent::Skip),
 922        }),
 923        VimCommand::str(("cl", "ist"), "diagnostics::Deploy"),
 924        VimCommand::new(("cc", ""), editor::actions::Hover),
 925        VimCommand::new(("ll", ""), editor::actions::Hover),
 926        VimCommand::new(("cn", "ext"), editor::actions::GoToDiagnostic).range(wrap_count),
 927        VimCommand::new(("cp", "revious"), editor::actions::GoToPreviousDiagnostic)
 928            .range(wrap_count),
 929        VimCommand::new(("cN", "ext"), editor::actions::GoToPreviousDiagnostic).range(wrap_count),
 930        VimCommand::new(("lp", "revious"), editor::actions::GoToPreviousDiagnostic)
 931            .range(wrap_count),
 932        VimCommand::new(("lN", "ext"), editor::actions::GoToPreviousDiagnostic).range(wrap_count),
 933        VimCommand::new(("j", "oin"), JoinLines).range(select_range),
 934        VimCommand::new(("fo", "ld"), editor::actions::FoldSelectedRanges).range(act_on_range),
 935        VimCommand::new(("foldo", "pen"), editor::actions::UnfoldLines)
 936            .bang(editor::actions::UnfoldRecursive)
 937            .range(act_on_range),
 938        VimCommand::new(("foldc", "lose"), editor::actions::Fold)
 939            .bang(editor::actions::FoldRecursive)
 940            .range(act_on_range),
 941        VimCommand::new(("dif", "fupdate"), editor::actions::ToggleSelectedDiffHunks)
 942            .range(act_on_range),
 943        VimCommand::str(("rev", "ert"), "git::Restore").range(act_on_range),
 944        VimCommand::new(("d", "elete"), VisualDeleteLine).range(select_range),
 945        VimCommand::new(("y", "ank"), gpui::NoAction).range(|_, range| {
 946            Some(
 947                YankCommand {
 948                    range: range.clone(),
 949                }
 950                .boxed_clone(),
 951            )
 952        }),
 953        VimCommand::new(("reg", "isters"), ToggleRegistersView).bang(ToggleRegistersView),
 954        VimCommand::new(("marks", ""), ToggleMarksView).bang(ToggleMarksView),
 955        VimCommand::new(("sor", "t"), SortLinesCaseSensitive).range(select_range),
 956        VimCommand::new(("sort i", ""), SortLinesCaseInsensitive).range(select_range),
 957        VimCommand::str(("E", "xplore"), "project_panel::ToggleFocus"),
 958        VimCommand::str(("H", "explore"), "project_panel::ToggleFocus"),
 959        VimCommand::str(("L", "explore"), "project_panel::ToggleFocus"),
 960        VimCommand::str(("S", "explore"), "project_panel::ToggleFocus"),
 961        VimCommand::str(("Ve", "xplore"), "project_panel::ToggleFocus"),
 962        VimCommand::str(("te", "rm"), "terminal_panel::ToggleFocus"),
 963        VimCommand::str(("T", "erm"), "terminal_panel::ToggleFocus"),
 964        VimCommand::str(("C", "ollab"), "collab_panel::ToggleFocus"),
 965        VimCommand::str(("Ch", "at"), "chat_panel::ToggleFocus"),
 966        VimCommand::str(("No", "tifications"), "notification_panel::ToggleFocus"),
 967        VimCommand::str(("A", "I"), "agent::ToggleFocus"),
 968        VimCommand::str(("G", "it"), "git_panel::ToggleFocus"),
 969        VimCommand::new(("noh", "lsearch"), search::buffer_search::Dismiss),
 970        VimCommand::new(("$", ""), EndOfDocument),
 971        VimCommand::new(("%", ""), EndOfDocument),
 972        VimCommand::new(("0", ""), StartOfDocument),
 973        VimCommand::new(("e", "dit"), editor::actions::ReloadFile)
 974            .bang(editor::actions::ReloadFile),
 975        VimCommand::new(("ex", ""), editor::actions::ReloadFile).bang(editor::actions::ReloadFile),
 976        VimCommand::new(("cpp", "link"), editor::actions::CopyPermalinkToLine).range(act_on_range),
 977        VimCommand::str(("opt", "ions"), "zed::OpenDefaultSettings"),
 978        VimCommand::str(("map", ""), "vim::OpenDefaultKeymap"),
 979        VimCommand::new(("h", "elp"), OpenDocs),
 980    ]
 981}
 982
 983struct VimCommands(Vec<VimCommand>);
 984// safety: we only ever access this from the main thread (as ensured by the cx argument)
 985// actions are not Sync so we can't otherwise use a OnceLock.
 986unsafe impl Sync for VimCommands {}
 987impl Global for VimCommands {}
 988
 989fn commands(cx: &App) -> &Vec<VimCommand> {
 990    static COMMANDS: OnceLock<VimCommands> = OnceLock::new();
 991    &COMMANDS
 992        .get_or_init(|| VimCommands(generate_commands(cx)))
 993        .0
 994}
 995
 996fn act_on_range(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
 997    Some(
 998        WithRange {
 999            restore_selection: true,
1000            range: range.clone(),
1001            action: WrappedAction(action),
1002        }
1003        .boxed_clone(),
1004    )
1005}
1006
1007fn select_range(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
1008    Some(
1009        WithRange {
1010            restore_selection: false,
1011            range: range.clone(),
1012            action: WrappedAction(action),
1013        }
1014        .boxed_clone(),
1015    )
1016}
1017
1018fn wrap_count(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
1019    range.as_count().map(|count| {
1020        WithCount {
1021            count,
1022            action: WrappedAction(action),
1023        }
1024        .boxed_clone()
1025    })
1026}
1027
1028pub fn command_interceptor(mut input: &str, cx: &App) -> Vec<CommandInterceptResult> {
1029    // NOTE: We also need to support passing arguments to commands like :w
1030    // (ideally with filename autocompletion).
1031    while input.starts_with(':') {
1032        input = &input[1..];
1033    }
1034
1035    let (range, query) = VimCommand::parse_range(input);
1036    let range_prefix = input[0..(input.len() - query.len())].to_string();
1037    let query = query.as_str().trim();
1038
1039    let action = if range.is_some() && query.is_empty() {
1040        Some(
1041            GoToLine {
1042                range: range.clone().unwrap(),
1043            }
1044            .boxed_clone(),
1045        )
1046    } else if query.starts_with('/') || query.starts_with('?') {
1047        Some(
1048            FindCommand {
1049                query: query[1..].to_string(),
1050                backwards: query.starts_with('?'),
1051            }
1052            .boxed_clone(),
1053        )
1054    } else if query.starts_with("se ") || query.starts_with("set ") {
1055        let (prefix, option) = query.split_once(' ').unwrap();
1056        let mut commands = VimOption::possible_commands(option);
1057        if !commands.is_empty() {
1058            let query = prefix.to_string() + " " + option;
1059            for command in &mut commands {
1060                command.positions = generate_positions(&command.string, &query);
1061            }
1062        }
1063        return commands;
1064    } else if query.starts_with('s') {
1065        let mut substitute = "substitute".chars().peekable();
1066        let mut query = query.chars().peekable();
1067        while substitute
1068            .peek()
1069            .is_some_and(|char| Some(char) == query.peek())
1070        {
1071            substitute.next();
1072            query.next();
1073        }
1074        if let Some(replacement) = Replacement::parse(query) {
1075            let range = range.clone().unwrap_or(CommandRange {
1076                start: Position::CurrentLine { offset: 0 },
1077                end: None,
1078            });
1079            Some(ReplaceCommand { replacement, range }.boxed_clone())
1080        } else {
1081            None
1082        }
1083    } else if query.starts_with('g') || query.starts_with('v') {
1084        let mut global = "global".chars().peekable();
1085        let mut query = query.chars().peekable();
1086        let mut invert = false;
1087        if query.peek() == Some(&'v') {
1088            invert = true;
1089            query.next();
1090        }
1091        while global.peek().is_some_and(|char| Some(char) == query.peek()) {
1092            global.next();
1093            query.next();
1094        }
1095        if !invert && query.peek() == Some(&'!') {
1096            invert = true;
1097            query.next();
1098        }
1099        let range = range.clone().unwrap_or(CommandRange {
1100            start: Position::Line { row: 0, offset: 0 },
1101            end: Some(Position::LastLine { offset: 0 }),
1102        });
1103        if let Some(action) = OnMatchingLines::parse(query, invert, range, cx) {
1104            Some(action.boxed_clone())
1105        } else {
1106            None
1107        }
1108    } else if query.contains('!') {
1109        ShellExec::parse(query, range.clone())
1110    } else {
1111        None
1112    };
1113    if let Some(action) = action {
1114        let string = input.to_string();
1115        let positions = generate_positions(&string, &(range_prefix + query));
1116        return vec![CommandInterceptResult {
1117            action,
1118            string,
1119            positions,
1120        }];
1121    }
1122
1123    for command in commands(cx).iter() {
1124        if let Some(action) = command.parse(query, &range, cx) {
1125            let mut string = ":".to_owned() + &range_prefix + command.prefix + command.suffix;
1126            if query.contains('!') {
1127                string.push('!');
1128            }
1129            let positions = generate_positions(&string, &(range_prefix + query));
1130
1131            return vec![CommandInterceptResult {
1132                action,
1133                string,
1134                positions,
1135            }];
1136        }
1137    }
1138    return Vec::default();
1139}
1140
1141fn generate_positions(string: &str, query: &str) -> Vec<usize> {
1142    let mut positions = Vec::new();
1143    let mut chars = query.chars();
1144
1145    let Some(mut current) = chars.next() else {
1146        return positions;
1147    };
1148
1149    for (i, c) in string.char_indices() {
1150        if c == current {
1151            positions.push(i);
1152            if let Some(c) = chars.next() {
1153                current = c;
1154            } else {
1155                break;
1156            }
1157        }
1158    }
1159
1160    positions
1161}
1162
1163#[derive(Debug, PartialEq, Clone)]
1164pub(crate) struct OnMatchingLines {
1165    range: CommandRange,
1166    search: String,
1167    action: WrappedAction,
1168    invert: bool,
1169}
1170
1171impl OnMatchingLines {
1172    // convert a vim query into something more usable by zed.
1173    // we don't attempt to fully convert between the two regex syntaxes,
1174    // but we do flip \( and \) to ( and ) (and vice-versa) in the pattern,
1175    // and convert \0..\9 to $0..$9 in the replacement so that common idioms work.
1176    pub(crate) fn parse(
1177        mut chars: Peekable<Chars>,
1178        invert: bool,
1179        range: CommandRange,
1180        cx: &App,
1181    ) -> Option<Self> {
1182        let delimiter = chars.next().filter(|c| {
1183            !c.is_alphanumeric() && *c != '"' && *c != '|' && *c != '\'' && *c != '!'
1184        })?;
1185
1186        let mut search = String::new();
1187        let mut escaped = false;
1188
1189        while let Some(c) = chars.next() {
1190            if escaped {
1191                escaped = false;
1192                // unescape escaped parens
1193                if c != '(' && c != ')' && c != delimiter {
1194                    search.push('\\')
1195                }
1196                search.push(c)
1197            } else if c == '\\' {
1198                escaped = true;
1199            } else if c == delimiter {
1200                break;
1201            } else {
1202                // escape unescaped parens
1203                if c == '(' || c == ')' {
1204                    search.push('\\')
1205                }
1206                search.push(c)
1207            }
1208        }
1209
1210        let command: String = chars.collect();
1211
1212        let action = WrappedAction(
1213            command_interceptor(&command, cx)
1214                .first()?
1215                .action
1216                .boxed_clone(),
1217        );
1218
1219        Some(Self {
1220            range,
1221            search,
1222            invert,
1223            action,
1224        })
1225    }
1226
1227    pub fn run(&self, vim: &mut Vim, window: &mut Window, cx: &mut Context<Vim>) {
1228        let result = vim.update_editor(window, cx, |vim, editor, window, cx| {
1229            self.range.buffer_range(vim, editor, window, cx)
1230        });
1231
1232        let range = match result {
1233            None => return,
1234            Some(e @ Err(_)) => {
1235                let Some(workspace) = vim.workspace(window) else {
1236                    return;
1237                };
1238                workspace.update(cx, |workspace, cx| {
1239                    e.notify_err(workspace, cx);
1240                });
1241                return;
1242            }
1243            Some(Ok(result)) => result,
1244        };
1245
1246        let mut action = self.action.boxed_clone();
1247        let mut last_pattern = self.search.clone();
1248
1249        let mut regexes = match Regex::new(&self.search) {
1250            Ok(regex) => vec![(regex, !self.invert)],
1251            e @ Err(_) => {
1252                let Some(workspace) = vim.workspace(window) else {
1253                    return;
1254                };
1255                workspace.update(cx, |workspace, cx| {
1256                    e.notify_err(workspace, cx);
1257                });
1258                return;
1259            }
1260        };
1261        while let Some(inner) = action
1262            .boxed_clone()
1263            .as_any()
1264            .downcast_ref::<OnMatchingLines>()
1265        {
1266            let Some(regex) = Regex::new(&inner.search).ok() else {
1267                break;
1268            };
1269            last_pattern = inner.search.clone();
1270            action = inner.action.boxed_clone();
1271            regexes.push((regex, !inner.invert))
1272        }
1273
1274        if let Some(pane) = vim.pane(window, cx) {
1275            pane.update(cx, |pane, cx| {
1276                if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()
1277                {
1278                    search_bar.update(cx, |search_bar, cx| {
1279                        if search_bar.show(window, cx) {
1280                            let _ = search_bar.search(
1281                                &last_pattern,
1282                                Some(SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE),
1283                                window,
1284                                cx,
1285                            );
1286                        }
1287                    });
1288                }
1289            });
1290        };
1291
1292        vim.update_editor(window, cx, |_, editor, window, cx| {
1293            let snapshot = editor.snapshot(window, cx);
1294            let mut row = range.start.0;
1295
1296            let point_range = Point::new(range.start.0, 0)
1297                ..snapshot
1298                    .buffer_snapshot
1299                    .clip_point(Point::new(range.end.0 + 1, 0), Bias::Left);
1300            cx.spawn_in(window, async move |editor, cx| {
1301                let new_selections = cx
1302                    .background_spawn(async move {
1303                        let mut line = String::new();
1304                        let mut new_selections = Vec::new();
1305                        let chunks = snapshot
1306                            .buffer_snapshot
1307                            .text_for_range(point_range)
1308                            .chain(["\n"]);
1309
1310                        for chunk in chunks {
1311                            for (newline_ix, text) in chunk.split('\n').enumerate() {
1312                                if newline_ix > 0 {
1313                                    if regexes.iter().all(|(regex, should_match)| {
1314                                        regex.is_match(&line) == *should_match
1315                                    }) {
1316                                        new_selections
1317                                            .push(Point::new(row, 0).to_display_point(&snapshot))
1318                                    }
1319                                    row += 1;
1320                                    line.clear();
1321                                }
1322                                line.push_str(text)
1323                            }
1324                        }
1325
1326                        new_selections
1327                    })
1328                    .await;
1329
1330                if new_selections.is_empty() {
1331                    return;
1332                }
1333                editor
1334                    .update_in(cx, |editor, window, cx| {
1335                        editor.start_transaction_at(Instant::now(), window, cx);
1336                        editor.change_selections(None, window, cx, |s| {
1337                            s.replace_cursors_with(|_| new_selections);
1338                        });
1339                        window.dispatch_action(action, cx);
1340                        cx.defer_in(window, move |editor, window, cx| {
1341                            let newest = editor.selections.newest::<Point>(cx).clone();
1342                            editor.change_selections(None, window, cx, |s| {
1343                                s.select(vec![newest]);
1344                            });
1345                            editor.end_transaction_at(Instant::now(), cx);
1346                        })
1347                    })
1348                    .ok();
1349            })
1350            .detach();
1351        });
1352    }
1353}
1354
1355#[derive(Clone, Debug, PartialEq)]
1356pub struct ShellExec {
1357    command: String,
1358    range: Option<CommandRange>,
1359    is_read: bool,
1360}
1361
1362impl Vim {
1363    pub fn cancel_running_command(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1364        if self.running_command.take().is_some() {
1365            self.update_editor(window, cx, |_, editor, window, cx| {
1366                editor.transact(window, cx, |editor, _window, _cx| {
1367                    editor.clear_row_highlights::<ShellExec>();
1368                })
1369            });
1370        }
1371    }
1372
1373    fn prepare_shell_command(
1374        &mut self,
1375        command: &str,
1376        window: &mut Window,
1377        cx: &mut Context<Self>,
1378    ) -> String {
1379        let mut ret = String::new();
1380        // N.B. non-standard escaping rules:
1381        // * !echo % => "echo README.md"
1382        // * !echo \% => "echo %"
1383        // * !echo \\% => echo \%
1384        // * !echo \\\% => echo \\%
1385        for c in command.chars() {
1386            if c != '%' && c != '!' {
1387                ret.push(c);
1388                continue;
1389            } else if ret.chars().last() == Some('\\') {
1390                ret.pop();
1391                ret.push(c);
1392                continue;
1393            }
1394            match c {
1395                '%' => {
1396                    self.update_editor(window, cx, |_, editor, _window, cx| {
1397                        if let Some((_, buffer, _)) = editor.active_excerpt(cx) {
1398                            if let Some(file) = buffer.read(cx).file() {
1399                                if let Some(local) = file.as_local() {
1400                                    if let Some(str) = local.path().to_str() {
1401                                        ret.push_str(str)
1402                                    }
1403                                }
1404                            }
1405                        }
1406                    });
1407                }
1408                '!' => {
1409                    if let Some(command) = &self.last_command {
1410                        ret.push_str(command)
1411                    }
1412                }
1413                _ => {}
1414            }
1415        }
1416        self.last_command = Some(ret.clone());
1417        ret
1418    }
1419
1420    pub fn shell_command_motion(
1421        &mut self,
1422        motion: Motion,
1423        times: Option<usize>,
1424        forced_motion: bool,
1425        window: &mut Window,
1426        cx: &mut Context<Vim>,
1427    ) {
1428        self.stop_recording(cx);
1429        let Some(workspace) = self.workspace(window) else {
1430            return;
1431        };
1432        let command = self.update_editor(window, cx, |_, editor, window, cx| {
1433            let snapshot = editor.snapshot(window, cx);
1434            let start = editor.selections.newest_display(cx);
1435            let text_layout_details = editor.text_layout_details(window);
1436            let (mut range, _) = motion
1437                .range(
1438                    &snapshot,
1439                    start.clone(),
1440                    times,
1441                    &text_layout_details,
1442                    forced_motion,
1443                )
1444                .unwrap_or((start.range(), MotionKind::Exclusive));
1445            if range.start != start.start {
1446                editor.change_selections(None, window, cx, |s| {
1447                    s.select_ranges([
1448                        range.start.to_point(&snapshot)..range.start.to_point(&snapshot)
1449                    ]);
1450                })
1451            }
1452            if range.end.row() > range.start.row() && range.end.column() != 0 {
1453                *range.end.row_mut() -= 1
1454            }
1455            if range.end.row() == range.start.row() {
1456                ".!".to_string()
1457            } else {
1458                format!(".,.+{}!", (range.end.row() - range.start.row()).0)
1459            }
1460        });
1461        if let Some(command) = command {
1462            workspace.update(cx, |workspace, cx| {
1463                command_palette::CommandPalette::toggle(workspace, &command, window, cx);
1464            });
1465        }
1466    }
1467
1468    pub fn shell_command_object(
1469        &mut self,
1470        object: Object,
1471        around: bool,
1472        window: &mut Window,
1473        cx: &mut Context<Vim>,
1474    ) {
1475        self.stop_recording(cx);
1476        let Some(workspace) = self.workspace(window) else {
1477            return;
1478        };
1479        let command = self.update_editor(window, cx, |_, editor, window, cx| {
1480            let snapshot = editor.snapshot(window, cx);
1481            let start = editor.selections.newest_display(cx);
1482            let range = object
1483                .range(&snapshot, start.clone(), around)
1484                .unwrap_or(start.range());
1485            if range.start != start.start {
1486                editor.change_selections(None, window, cx, |s| {
1487                    s.select_ranges([
1488                        range.start.to_point(&snapshot)..range.start.to_point(&snapshot)
1489                    ]);
1490                })
1491            }
1492            if range.end.row() == range.start.row() {
1493                ".!".to_string()
1494            } else {
1495                format!(".,.+{}!", (range.end.row() - range.start.row()).0)
1496            }
1497        });
1498        if let Some(command) = command {
1499            workspace.update(cx, |workspace, cx| {
1500                command_palette::CommandPalette::toggle(workspace, &command, window, cx);
1501            });
1502        }
1503    }
1504}
1505
1506impl ShellExec {
1507    pub fn parse(query: &str, range: Option<CommandRange>) -> Option<Box<dyn Action>> {
1508        let (before, after) = query.split_once('!')?;
1509        let before = before.trim();
1510
1511        if !"read".starts_with(before) {
1512            return None;
1513        }
1514
1515        Some(
1516            ShellExec {
1517                command: after.trim().to_string(),
1518                range,
1519                is_read: !before.is_empty(),
1520            }
1521            .boxed_clone(),
1522        )
1523    }
1524
1525    pub fn run(&self, vim: &mut Vim, window: &mut Window, cx: &mut Context<Vim>) {
1526        let Some(workspace) = vim.workspace(window) else {
1527            return;
1528        };
1529
1530        let project = workspace.read(cx).project().clone();
1531        let command = vim.prepare_shell_command(&self.command, window, cx);
1532
1533        if self.range.is_none() && !self.is_read {
1534            workspace.update(cx, |workspace, cx| {
1535                let project = workspace.project().read(cx);
1536                let cwd = project.first_project_directory(cx);
1537                let shell = project.terminal_settings(&cwd, cx).shell.clone();
1538
1539                let spawn_in_terminal = SpawnInTerminal {
1540                    id: TaskId("vim".to_string()),
1541                    full_label: command.clone(),
1542                    label: command.clone(),
1543                    command: command.clone(),
1544                    args: Vec::new(),
1545                    command_label: command.clone(),
1546                    cwd,
1547                    env: HashMap::default(),
1548                    use_new_terminal: true,
1549                    allow_concurrent_runs: true,
1550                    reveal: RevealStrategy::NoFocus,
1551                    reveal_target: RevealTarget::Dock,
1552                    hide: HideStrategy::Never,
1553                    shell,
1554                    show_summary: false,
1555                    show_command: false,
1556                    show_rerun: false,
1557                };
1558
1559                let task_status = workspace.spawn_in_terminal(spawn_in_terminal, window, cx);
1560                cx.background_spawn(async move {
1561                    match task_status.await {
1562                        Some(Ok(status)) => {
1563                            if status.success() {
1564                                log::debug!("Vim shell exec succeeded");
1565                            } else {
1566                                log::debug!("Vim shell exec failed, code: {:?}", status.code());
1567                            }
1568                        }
1569                        Some(Err(e)) => log::error!("Vim shell exec failed: {e}"),
1570                        None => log::debug!("Vim shell exec got cancelled"),
1571                    }
1572                })
1573                .detach();
1574            });
1575            return;
1576        };
1577
1578        let mut input_snapshot = None;
1579        let mut input_range = None;
1580        let mut needs_newline_prefix = false;
1581        vim.update_editor(window, cx, |vim, editor, window, cx| {
1582            let snapshot = editor.buffer().read(cx).snapshot(cx);
1583            let range = if let Some(range) = self.range.clone() {
1584                let Some(range) = range.buffer_range(vim, editor, window, cx).log_err() else {
1585                    return;
1586                };
1587                Point::new(range.start.0, 0)
1588                    ..snapshot.clip_point(Point::new(range.end.0 + 1, 0), Bias::Right)
1589            } else {
1590                let mut end = editor.selections.newest::<Point>(cx).range().end;
1591                end = snapshot.clip_point(Point::new(end.row + 1, 0), Bias::Right);
1592                needs_newline_prefix = end == snapshot.max_point();
1593                end..end
1594            };
1595            if self.is_read {
1596                input_range =
1597                    Some(snapshot.anchor_after(range.end)..snapshot.anchor_after(range.end));
1598            } else {
1599                input_range =
1600                    Some(snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end));
1601            }
1602            editor.highlight_rows::<ShellExec>(
1603                input_range.clone().unwrap(),
1604                cx.theme().status().unreachable_background,
1605                Default::default(),
1606                cx,
1607            );
1608
1609            if !self.is_read {
1610                input_snapshot = Some(snapshot)
1611            }
1612        });
1613
1614        let Some(range) = input_range else { return };
1615
1616        let mut process = project.read(cx).exec_in_shell(command, cx);
1617        process.stdout(Stdio::piped());
1618        process.stderr(Stdio::piped());
1619
1620        if input_snapshot.is_some() {
1621            process.stdin(Stdio::piped());
1622        } else {
1623            process.stdin(Stdio::null());
1624        };
1625
1626        util::set_pre_exec_to_start_new_session(&mut process);
1627        let is_read = self.is_read;
1628
1629        let task = cx.spawn_in(window, async move |vim, cx| {
1630            let Some(mut running) = process.spawn().log_err() else {
1631                vim.update_in(cx, |vim, window, cx| {
1632                    vim.cancel_running_command(window, cx);
1633                })
1634                .log_err();
1635                return;
1636            };
1637
1638            if let Some(mut stdin) = running.stdin.take() {
1639                if let Some(snapshot) = input_snapshot {
1640                    let range = range.clone();
1641                    cx.background_spawn(async move {
1642                        for chunk in snapshot.text_for_range(range) {
1643                            if stdin.write_all(chunk.as_bytes()).log_err().is_none() {
1644                                return;
1645                            }
1646                        }
1647                        stdin.flush().log_err();
1648                    })
1649                    .detach();
1650                }
1651            };
1652
1653            let output = cx
1654                .background_spawn(async move { running.wait_with_output() })
1655                .await;
1656
1657            let Some(output) = output.log_err() else {
1658                vim.update_in(cx, |vim, window, cx| {
1659                    vim.cancel_running_command(window, cx);
1660                })
1661                .log_err();
1662                return;
1663            };
1664            let mut text = String::new();
1665            if needs_newline_prefix {
1666                text.push('\n');
1667            }
1668            text.push_str(&String::from_utf8_lossy(&output.stdout));
1669            text.push_str(&String::from_utf8_lossy(&output.stderr));
1670            if !text.is_empty() && text.chars().last() != Some('\n') {
1671                text.push('\n');
1672            }
1673
1674            vim.update_in(cx, |vim, window, cx| {
1675                vim.update_editor(window, cx, |_, editor, window, cx| {
1676                    editor.transact(window, cx, |editor, window, cx| {
1677                        editor.edit([(range.clone(), text)], cx);
1678                        let snapshot = editor.buffer().read(cx).snapshot(cx);
1679                        editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
1680                            let point = if is_read {
1681                                let point = range.end.to_point(&snapshot);
1682                                Point::new(point.row.saturating_sub(1), 0)
1683                            } else {
1684                                let point = range.start.to_point(&snapshot);
1685                                Point::new(point.row, 0)
1686                            };
1687                            s.select_ranges([point..point]);
1688                        })
1689                    })
1690                });
1691                vim.cancel_running_command(window, cx);
1692            })
1693            .log_err();
1694        });
1695        vim.running_command.replace(task);
1696    }
1697}
1698
1699#[cfg(test)]
1700mod test {
1701    use std::path::Path;
1702
1703    use crate::{
1704        state::Mode,
1705        test::{NeovimBackedTestContext, VimTestContext},
1706    };
1707    use editor::Editor;
1708    use gpui::{Context, TestAppContext};
1709    use indoc::indoc;
1710    use util::path;
1711    use workspace::Workspace;
1712
1713    #[gpui::test]
1714    async fn test_command_basics(cx: &mut TestAppContext) {
1715        let mut cx = NeovimBackedTestContext::new(cx).await;
1716
1717        cx.set_shared_state(indoc! {"
1718            ˇa
1719            b
1720            c"})
1721            .await;
1722
1723        cx.simulate_shared_keystrokes(": j enter").await;
1724
1725        // hack: our cursor positioning after a join command is wrong
1726        cx.simulate_shared_keystrokes("^").await;
1727        cx.shared_state().await.assert_eq(indoc! {
1728            "ˇa b
1729            c"
1730        });
1731    }
1732
1733    #[gpui::test]
1734    async fn test_command_goto(cx: &mut TestAppContext) {
1735        let mut cx = NeovimBackedTestContext::new(cx).await;
1736
1737        cx.set_shared_state(indoc! {"
1738            ˇa
1739            b
1740            c"})
1741            .await;
1742        cx.simulate_shared_keystrokes(": 3 enter").await;
1743        cx.shared_state().await.assert_eq(indoc! {"
1744            a
1745            b
1746            ˇc"});
1747    }
1748
1749    #[gpui::test]
1750    async fn test_command_replace(cx: &mut TestAppContext) {
1751        let mut cx = NeovimBackedTestContext::new(cx).await;
1752
1753        cx.set_shared_state(indoc! {"
1754            ˇa
1755            b
1756            b
1757            c"})
1758            .await;
1759        cx.simulate_shared_keystrokes(": % s / b / d enter").await;
1760        cx.shared_state().await.assert_eq(indoc! {"
1761            a
1762            d
1763            ˇd
1764            c"});
1765        cx.simulate_shared_keystrokes(": % s : . : \\ 0 \\ 0 enter")
1766            .await;
1767        cx.shared_state().await.assert_eq(indoc! {"
1768            aa
1769            dd
1770            dd
1771            ˇcc"});
1772        cx.simulate_shared_keystrokes("k : s / d d / e e enter")
1773            .await;
1774        cx.shared_state().await.assert_eq(indoc! {"
1775            aa
1776            dd
1777            ˇee
1778            cc"});
1779    }
1780
1781    #[gpui::test]
1782    async fn test_command_search(cx: &mut TestAppContext) {
1783        let mut cx = NeovimBackedTestContext::new(cx).await;
1784
1785        cx.set_shared_state(indoc! {"
1786                ˇa
1787                b
1788                a
1789                c"})
1790            .await;
1791        cx.simulate_shared_keystrokes(": / b enter").await;
1792        cx.shared_state().await.assert_eq(indoc! {"
1793                a
1794                ˇb
1795                a
1796                c"});
1797        cx.simulate_shared_keystrokes(": ? a enter").await;
1798        cx.shared_state().await.assert_eq(indoc! {"
1799                ˇa
1800                b
1801                a
1802                c"});
1803    }
1804
1805    #[gpui::test]
1806    async fn test_command_write(cx: &mut TestAppContext) {
1807        let mut cx = VimTestContext::new(cx, true).await;
1808        let path = Path::new(path!("/root/dir/file.rs"));
1809        let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
1810
1811        cx.simulate_keystrokes("i @ escape");
1812        cx.simulate_keystrokes(": w enter");
1813
1814        assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "@\n");
1815
1816        fs.as_fake().insert_file(path, b"oops\n".to_vec()).await;
1817
1818        // conflict!
1819        cx.simulate_keystrokes("i @ escape");
1820        cx.simulate_keystrokes(": w enter");
1821        cx.simulate_prompt_answer("Cancel");
1822
1823        assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "oops\n");
1824        assert!(!cx.has_pending_prompt());
1825        cx.simulate_keystrokes(": w ! enter");
1826        assert!(!cx.has_pending_prompt());
1827        assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "@@\n");
1828    }
1829
1830    #[gpui::test]
1831    async fn test_command_quit(cx: &mut TestAppContext) {
1832        let mut cx = VimTestContext::new(cx, true).await;
1833
1834        cx.simulate_keystrokes(": n e w enter");
1835        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
1836        cx.simulate_keystrokes(": q enter");
1837        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 1));
1838        cx.simulate_keystrokes(": n e w enter");
1839        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
1840        cx.simulate_keystrokes(": q a enter");
1841        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 0));
1842    }
1843
1844    #[gpui::test]
1845    async fn test_offsets(cx: &mut TestAppContext) {
1846        let mut cx = NeovimBackedTestContext::new(cx).await;
1847
1848        cx.set_shared_state("ˇ1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n")
1849            .await;
1850
1851        cx.simulate_shared_keystrokes(": + enter").await;
1852        cx.shared_state()
1853            .await
1854            .assert_eq("1\nˇ2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n");
1855
1856        cx.simulate_shared_keystrokes(": 1 0 - enter").await;
1857        cx.shared_state()
1858            .await
1859            .assert_eq("1\n2\n3\n4\n5\n6\n7\n8\nˇ9\n10\n11\n");
1860
1861        cx.simulate_shared_keystrokes(": . - 2 enter").await;
1862        cx.shared_state()
1863            .await
1864            .assert_eq("1\n2\n3\n4\n5\n6\nˇ7\n8\n9\n10\n11\n");
1865
1866        cx.simulate_shared_keystrokes(": % enter").await;
1867        cx.shared_state()
1868            .await
1869            .assert_eq("1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\nˇ");
1870    }
1871
1872    #[gpui::test]
1873    async fn test_command_ranges(cx: &mut TestAppContext) {
1874        let mut cx = NeovimBackedTestContext::new(cx).await;
1875
1876        cx.set_shared_state("ˇ1\n2\n3\n4\n4\n3\n2\n1").await;
1877
1878        cx.simulate_shared_keystrokes(": 2 , 4 d enter").await;
1879        cx.shared_state().await.assert_eq("1\nˇ4\n3\n2\n1");
1880
1881        cx.simulate_shared_keystrokes(": 2 , 4 s o r t enter").await;
1882        cx.shared_state().await.assert_eq("1\nˇ2\n3\n4\n1");
1883
1884        cx.simulate_shared_keystrokes(": 2 , 4 j o i n enter").await;
1885        cx.shared_state().await.assert_eq("1\nˇ2 3 4\n1");
1886    }
1887
1888    #[gpui::test]
1889    async fn test_command_visual_replace(cx: &mut TestAppContext) {
1890        let mut cx = NeovimBackedTestContext::new(cx).await;
1891
1892        cx.set_shared_state("ˇ1\n2\n3\n4\n4\n3\n2\n1").await;
1893
1894        cx.simulate_shared_keystrokes("v 2 j : s / . / k enter")
1895            .await;
1896        cx.shared_state().await.assert_eq("k\nk\nˇk\n4\n4\n3\n2\n1");
1897    }
1898
1899    #[track_caller]
1900    fn assert_active_item(
1901        workspace: &mut Workspace,
1902        expected_path: &str,
1903        expected_text: &str,
1904        cx: &mut Context<Workspace>,
1905    ) {
1906        let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();
1907
1908        let buffer = active_editor
1909            .read(cx)
1910            .buffer()
1911            .read(cx)
1912            .as_singleton()
1913            .unwrap();
1914
1915        let text = buffer.read(cx).text();
1916        let file = buffer.read(cx).file().unwrap();
1917        let file_path = file.as_local().unwrap().abs_path(cx);
1918
1919        assert_eq!(text, expected_text);
1920        assert_eq!(file_path, Path::new(expected_path));
1921    }
1922
1923    #[gpui::test]
1924    async fn test_command_gf(cx: &mut TestAppContext) {
1925        let mut cx = VimTestContext::new(cx, true).await;
1926
1927        // Assert base state, that we're in /root/dir/file.rs
1928        cx.workspace(|workspace, _, cx| {
1929            assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
1930        });
1931
1932        // Insert a new file
1933        let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
1934        fs.as_fake()
1935            .insert_file(
1936                path!("/root/dir/file2.rs"),
1937                "This is file2.rs".as_bytes().to_vec(),
1938            )
1939            .await;
1940        fs.as_fake()
1941            .insert_file(
1942                path!("/root/dir/file3.rs"),
1943                "go to file3".as_bytes().to_vec(),
1944            )
1945            .await;
1946
1947        // Put the path to the second file into the currently open buffer
1948        cx.set_state(indoc! {"go to fiˇle2.rs"}, Mode::Normal);
1949
1950        // Go to file2.rs
1951        cx.simulate_keystrokes("g f");
1952
1953        // We now have two items
1954        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
1955        cx.workspace(|workspace, _, cx| {
1956            assert_active_item(
1957                workspace,
1958                path!("/root/dir/file2.rs"),
1959                "This is file2.rs",
1960                cx,
1961            );
1962        });
1963
1964        // Update editor to point to `file2.rs`
1965        cx.editor =
1966            cx.workspace(|workspace, _, cx| workspace.active_item_as::<Editor>(cx).unwrap());
1967
1968        // Put the path to the third file into the currently open buffer,
1969        // but remove its suffix, because we want that lookup to happen automatically.
1970        cx.set_state(indoc! {"go to fiˇle3"}, Mode::Normal);
1971
1972        // Go to file3.rs
1973        cx.simulate_keystrokes("g f");
1974
1975        // We now have three items
1976        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 3));
1977        cx.workspace(|workspace, _, cx| {
1978            assert_active_item(workspace, path!("/root/dir/file3.rs"), "go to file3", cx);
1979        });
1980    }
1981
1982    #[gpui::test]
1983    async fn test_w_command(cx: &mut TestAppContext) {
1984        let mut cx = VimTestContext::new(cx, true).await;
1985
1986        cx.workspace(|workspace, _, cx| {
1987            assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
1988        });
1989
1990        cx.simulate_keystrokes(": w space other.rs");
1991        cx.simulate_keystrokes("enter");
1992
1993        cx.workspace(|workspace, _, cx| {
1994            assert_active_item(workspace, path!("/root/other.rs"), "", cx);
1995        });
1996
1997        cx.simulate_keystrokes(": w space dir/file.rs");
1998        cx.simulate_keystrokes("enter");
1999
2000        cx.simulate_prompt_answer("Replace");
2001        cx.run_until_parked();
2002
2003        cx.workspace(|workspace, _, cx| {
2004            assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
2005        });
2006
2007        cx.simulate_keystrokes(": w ! space other.rs");
2008        cx.simulate_keystrokes("enter");
2009
2010        cx.workspace(|workspace, _, cx| {
2011            assert_active_item(workspace, path!("/root/other.rs"), "", cx);
2012        });
2013    }
2014
2015    #[gpui::test]
2016    async fn test_command_matching_lines(cx: &mut TestAppContext) {
2017        let mut cx = NeovimBackedTestContext::new(cx).await;
2018
2019        cx.set_shared_state(indoc! {"
2020            ˇa
2021            b
2022            a
2023            b
2024            a
2025        "})
2026            .await;
2027
2028        cx.simulate_shared_keystrokes(":").await;
2029        cx.simulate_shared_keystrokes("g / a / d").await;
2030        cx.simulate_shared_keystrokes("enter").await;
2031
2032        cx.shared_state().await.assert_eq(indoc! {"
2033            b
2034            b
2035            ˇ"});
2036
2037        cx.simulate_shared_keystrokes("u").await;
2038
2039        cx.shared_state().await.assert_eq(indoc! {"
2040            ˇa
2041            b
2042            a
2043            b
2044            a
2045        "});
2046
2047        cx.simulate_shared_keystrokes(":").await;
2048        cx.simulate_shared_keystrokes("v / a / d").await;
2049        cx.simulate_shared_keystrokes("enter").await;
2050
2051        cx.shared_state().await.assert_eq(indoc! {"
2052            a
2053            a
2054            ˇa"});
2055    }
2056}