command.rs

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