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::{OpenDocs, 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        VimCommand::new(("h", "elp"), OpenDocs),
 892    ]
 893}
 894
 895struct VimCommands(Vec<VimCommand>);
 896// safety: we only ever access this from the main thread (as ensured by the cx argument)
 897// actions are not Sync so we can't otherwise use a OnceLock.
 898unsafe impl Sync for VimCommands {}
 899impl Global for VimCommands {}
 900
 901fn commands(cx: &App) -> &Vec<VimCommand> {
 902    static COMMANDS: OnceLock<VimCommands> = OnceLock::new();
 903    &COMMANDS
 904        .get_or_init(|| VimCommands(generate_commands(cx)))
 905        .0
 906}
 907
 908fn act_on_range(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
 909    Some(
 910        WithRange {
 911            restore_selection: true,
 912            range: range.clone(),
 913            action: WrappedAction(action),
 914        }
 915        .boxed_clone(),
 916    )
 917}
 918
 919fn select_range(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
 920    Some(
 921        WithRange {
 922            restore_selection: false,
 923            range: range.clone(),
 924            action: WrappedAction(action),
 925        }
 926        .boxed_clone(),
 927    )
 928}
 929
 930fn wrap_count(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
 931    range.as_count().map(|count| {
 932        WithCount {
 933            count,
 934            action: WrappedAction(action),
 935        }
 936        .boxed_clone()
 937    })
 938}
 939
 940pub fn command_interceptor(mut input: &str, cx: &App) -> Vec<CommandInterceptResult> {
 941    // NOTE: We also need to support passing arguments to commands like :w
 942    // (ideally with filename autocompletion).
 943    while input.starts_with(':') {
 944        input = &input[1..];
 945    }
 946
 947    let (range, query) = VimCommand::parse_range(input);
 948    let range_prefix = input[0..(input.len() - query.len())].to_string();
 949    let query = query.as_str().trim();
 950
 951    let action = if range.is_some() && query.is_empty() {
 952        Some(
 953            GoToLine {
 954                range: range.clone().unwrap(),
 955            }
 956            .boxed_clone(),
 957        )
 958    } else if query.starts_with('/') || query.starts_with('?') {
 959        Some(
 960            FindCommand {
 961                query: query[1..].to_string(),
 962                backwards: query.starts_with('?'),
 963            }
 964            .boxed_clone(),
 965        )
 966    } else if query.starts_with("se ") || query.starts_with("set ") {
 967        let (prefix, option) = query.split_once(' ').unwrap();
 968        let mut commands = VimOption::possible_commands(option);
 969        if !commands.is_empty() {
 970            let query = prefix.to_string() + " " + option;
 971            for command in &mut commands {
 972                command.positions = generate_positions(&command.string, &query);
 973            }
 974        }
 975        return commands;
 976    } else if query.starts_with('s') {
 977        let mut substitute = "substitute".chars().peekable();
 978        let mut query = query.chars().peekable();
 979        while substitute
 980            .peek()
 981            .is_some_and(|char| Some(char) == query.peek())
 982        {
 983            substitute.next();
 984            query.next();
 985        }
 986        if let Some(replacement) = Replacement::parse(query) {
 987            let range = range.clone().unwrap_or(CommandRange {
 988                start: Position::CurrentLine { offset: 0 },
 989                end: None,
 990            });
 991            Some(ReplaceCommand { replacement, range }.boxed_clone())
 992        } else {
 993            None
 994        }
 995    } else if query.starts_with('g') || query.starts_with('v') {
 996        let mut global = "global".chars().peekable();
 997        let mut query = query.chars().peekable();
 998        let mut invert = false;
 999        if query.peek() == Some(&'v') {
1000            invert = true;
1001            query.next();
1002        }
1003        while global.peek().is_some_and(|char| Some(char) == query.peek()) {
1004            global.next();
1005            query.next();
1006        }
1007        if !invert && query.peek() == Some(&'!') {
1008            invert = true;
1009            query.next();
1010        }
1011        let range = range.clone().unwrap_or(CommandRange {
1012            start: Position::Line { row: 0, offset: 0 },
1013            end: Some(Position::LastLine { offset: 0 }),
1014        });
1015        if let Some(action) = OnMatchingLines::parse(query, invert, range, cx) {
1016            Some(action.boxed_clone())
1017        } else {
1018            None
1019        }
1020    } else if query.contains('!') {
1021        ShellExec::parse(query, range.clone())
1022    } else {
1023        None
1024    };
1025    if let Some(action) = action {
1026        let string = input.to_string();
1027        let positions = generate_positions(&string, &(range_prefix + query));
1028        return vec![CommandInterceptResult {
1029            action,
1030            string,
1031            positions,
1032        }];
1033    }
1034
1035    for command in commands(cx).iter() {
1036        if let Some(action) = command.parse(query, &range, cx) {
1037            let mut string = ":".to_owned() + &range_prefix + command.prefix + command.suffix;
1038            if query.ends_with('!') {
1039                string.push('!');
1040            }
1041            let positions = generate_positions(&string, &(range_prefix + query));
1042
1043            return vec![CommandInterceptResult {
1044                action,
1045                string,
1046                positions,
1047            }];
1048        }
1049    }
1050    return Vec::default();
1051}
1052
1053fn generate_positions(string: &str, query: &str) -> Vec<usize> {
1054    let mut positions = Vec::new();
1055    let mut chars = query.chars();
1056
1057    let Some(mut current) = chars.next() else {
1058        return positions;
1059    };
1060
1061    for (i, c) in string.char_indices() {
1062        if c == current {
1063            positions.push(i);
1064            if let Some(c) = chars.next() {
1065                current = c;
1066            } else {
1067                break;
1068            }
1069        }
1070    }
1071
1072    positions
1073}
1074
1075#[derive(Debug, PartialEq, Clone)]
1076pub(crate) struct OnMatchingLines {
1077    range: CommandRange,
1078    search: String,
1079    action: WrappedAction,
1080    invert: bool,
1081}
1082
1083impl OnMatchingLines {
1084    // convert a vim query into something more usable by zed.
1085    // we don't attempt to fully convert between the two regex syntaxes,
1086    // but we do flip \( and \) to ( and ) (and vice-versa) in the pattern,
1087    // and convert \0..\9 to $0..$9 in the replacement so that common idioms work.
1088    pub(crate) fn parse(
1089        mut chars: Peekable<Chars>,
1090        invert: bool,
1091        range: CommandRange,
1092        cx: &App,
1093    ) -> Option<Self> {
1094        let delimiter = chars.next().filter(|c| {
1095            !c.is_alphanumeric() && *c != '"' && *c != '|' && *c != '\'' && *c != '!'
1096        })?;
1097
1098        let mut search = String::new();
1099        let mut escaped = false;
1100
1101        while let Some(c) = chars.next() {
1102            if escaped {
1103                escaped = false;
1104                // unescape escaped parens
1105                if c != '(' && c != ')' && c != delimiter {
1106                    search.push('\\')
1107                }
1108                search.push(c)
1109            } else if c == '\\' {
1110                escaped = true;
1111            } else if c == delimiter {
1112                break;
1113            } else {
1114                // escape unescaped parens
1115                if c == '(' || c == ')' {
1116                    search.push('\\')
1117                }
1118                search.push(c)
1119            }
1120        }
1121
1122        let command: String = chars.collect();
1123
1124        let action = WrappedAction(
1125            command_interceptor(&command, cx)
1126                .first()?
1127                .action
1128                .boxed_clone(),
1129        );
1130
1131        Some(Self {
1132            range,
1133            search,
1134            invert,
1135            action,
1136        })
1137    }
1138
1139    pub fn run(&self, vim: &mut Vim, window: &mut Window, cx: &mut Context<Vim>) {
1140        let result = vim.update_editor(window, cx, |vim, editor, window, cx| {
1141            self.range.buffer_range(vim, editor, window, cx)
1142        });
1143
1144        let range = match result {
1145            None => return,
1146            Some(e @ Err(_)) => {
1147                let Some(workspace) = vim.workspace(window) else {
1148                    return;
1149                };
1150                workspace.update(cx, |workspace, cx| {
1151                    e.notify_err(workspace, cx);
1152                });
1153                return;
1154            }
1155            Some(Ok(result)) => result,
1156        };
1157
1158        let mut action = self.action.boxed_clone();
1159        let mut last_pattern = self.search.clone();
1160
1161        let mut regexes = match Regex::new(&self.search) {
1162            Ok(regex) => vec![(regex, !self.invert)],
1163            e @ Err(_) => {
1164                let Some(workspace) = vim.workspace(window) else {
1165                    return;
1166                };
1167                workspace.update(cx, |workspace, cx| {
1168                    e.notify_err(workspace, cx);
1169                });
1170                return;
1171            }
1172        };
1173        while let Some(inner) = action
1174            .boxed_clone()
1175            .as_any()
1176            .downcast_ref::<OnMatchingLines>()
1177        {
1178            let Some(regex) = Regex::new(&inner.search).ok() else {
1179                break;
1180            };
1181            last_pattern = inner.search.clone();
1182            action = inner.action.boxed_clone();
1183            regexes.push((regex, !inner.invert))
1184        }
1185
1186        if let Some(pane) = vim.pane(window, cx) {
1187            pane.update(cx, |pane, cx| {
1188                if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()
1189                {
1190                    search_bar.update(cx, |search_bar, cx| {
1191                        if search_bar.show(window, cx) {
1192                            let _ = search_bar.search(
1193                                &last_pattern,
1194                                Some(SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE),
1195                                window,
1196                                cx,
1197                            );
1198                        }
1199                    });
1200                }
1201            });
1202        };
1203
1204        vim.update_editor(window, cx, |_, editor, window, cx| {
1205            let snapshot = editor.snapshot(window, cx);
1206            let mut row = range.start.0;
1207
1208            let point_range = Point::new(range.start.0, 0)
1209                ..snapshot
1210                    .buffer_snapshot
1211                    .clip_point(Point::new(range.end.0 + 1, 0), Bias::Left);
1212            cx.spawn_in(window, async move |editor, cx| {
1213                let new_selections = cx
1214                    .background_spawn(async move {
1215                        let mut line = String::new();
1216                        let mut new_selections = Vec::new();
1217                        let chunks = snapshot
1218                            .buffer_snapshot
1219                            .text_for_range(point_range)
1220                            .chain(["\n"]);
1221
1222                        for chunk in chunks {
1223                            for (newline_ix, text) in chunk.split('\n').enumerate() {
1224                                if newline_ix > 0 {
1225                                    if regexes.iter().all(|(regex, should_match)| {
1226                                        regex.is_match(&line) == *should_match
1227                                    }) {
1228                                        new_selections
1229                                            .push(Point::new(row, 0).to_display_point(&snapshot))
1230                                    }
1231                                    row += 1;
1232                                    line.clear();
1233                                }
1234                                line.push_str(text)
1235                            }
1236                        }
1237
1238                        new_selections
1239                    })
1240                    .await;
1241
1242                if new_selections.is_empty() {
1243                    return;
1244                }
1245                editor
1246                    .update_in(cx, |editor, window, cx| {
1247                        editor.start_transaction_at(Instant::now(), window, cx);
1248                        editor.change_selections(None, window, cx, |s| {
1249                            s.replace_cursors_with(|_| new_selections);
1250                        });
1251                        window.dispatch_action(action, cx);
1252                        cx.defer_in(window, move |editor, window, cx| {
1253                            let newest = editor.selections.newest::<Point>(cx).clone();
1254                            editor.change_selections(None, window, cx, |s| {
1255                                s.select(vec![newest]);
1256                            });
1257                            editor.end_transaction_at(Instant::now(), cx);
1258                        })
1259                    })
1260                    .ok();
1261            })
1262            .detach();
1263        });
1264    }
1265}
1266
1267#[derive(Clone, Debug, PartialEq)]
1268pub struct ShellExec {
1269    command: String,
1270    range: Option<CommandRange>,
1271    is_read: bool,
1272}
1273
1274impl Vim {
1275    pub fn cancel_running_command(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1276        if self.running_command.take().is_some() {
1277            self.update_editor(window, cx, |_, editor, window, cx| {
1278                editor.transact(window, cx, |editor, _window, _cx| {
1279                    editor.clear_row_highlights::<ShellExec>();
1280                })
1281            });
1282        }
1283    }
1284
1285    fn prepare_shell_command(
1286        &mut self,
1287        command: &str,
1288        window: &mut Window,
1289        cx: &mut Context<Self>,
1290    ) -> String {
1291        let mut ret = String::new();
1292        // N.B. non-standard escaping rules:
1293        // * !echo % => "echo README.md"
1294        // * !echo \% => "echo %"
1295        // * !echo \\% => echo \%
1296        // * !echo \\\% => echo \\%
1297        for c in command.chars() {
1298            if c != '%' && c != '!' {
1299                ret.push(c);
1300                continue;
1301            } else if ret.chars().last() == Some('\\') {
1302                ret.pop();
1303                ret.push(c);
1304                continue;
1305            }
1306            match c {
1307                '%' => {
1308                    self.update_editor(window, cx, |_, editor, _window, cx| {
1309                        if let Some((_, buffer, _)) = editor.active_excerpt(cx) {
1310                            if let Some(file) = buffer.read(cx).file() {
1311                                if let Some(local) = file.as_local() {
1312                                    if let Some(str) = local.path().to_str() {
1313                                        ret.push_str(str)
1314                                    }
1315                                }
1316                            }
1317                        }
1318                    });
1319                }
1320                '!' => {
1321                    if let Some(command) = &self.last_command {
1322                        ret.push_str(command)
1323                    }
1324                }
1325                _ => {}
1326            }
1327        }
1328        self.last_command = Some(ret.clone());
1329        ret
1330    }
1331
1332    pub fn shell_command_motion(
1333        &mut self,
1334        motion: Motion,
1335        times: Option<usize>,
1336        forced_motion: bool,
1337        window: &mut Window,
1338        cx: &mut Context<Vim>,
1339    ) {
1340        self.stop_recording(cx);
1341        let Some(workspace) = self.workspace(window) else {
1342            return;
1343        };
1344        let command = self.update_editor(window, cx, |_, editor, window, cx| {
1345            let snapshot = editor.snapshot(window, cx);
1346            let start = editor.selections.newest_display(cx);
1347            let text_layout_details = editor.text_layout_details(window);
1348            let (mut range, _) = motion
1349                .range(
1350                    &snapshot,
1351                    start.clone(),
1352                    times,
1353                    &text_layout_details,
1354                    forced_motion,
1355                )
1356                .unwrap_or((start.range(), MotionKind::Exclusive));
1357            if range.start != start.start {
1358                editor.change_selections(None, window, cx, |s| {
1359                    s.select_ranges([
1360                        range.start.to_point(&snapshot)..range.start.to_point(&snapshot)
1361                    ]);
1362                })
1363            }
1364            if range.end.row() > range.start.row() && range.end.column() != 0 {
1365                *range.end.row_mut() -= 1
1366            }
1367            if range.end.row() == range.start.row() {
1368                ".!".to_string()
1369            } else {
1370                format!(".,.+{}!", (range.end.row() - range.start.row()).0)
1371            }
1372        });
1373        if let Some(command) = command {
1374            workspace.update(cx, |workspace, cx| {
1375                command_palette::CommandPalette::toggle(workspace, &command, window, cx);
1376            });
1377        }
1378    }
1379
1380    pub fn shell_command_object(
1381        &mut self,
1382        object: Object,
1383        around: bool,
1384        window: &mut Window,
1385        cx: &mut Context<Vim>,
1386    ) {
1387        self.stop_recording(cx);
1388        let Some(workspace) = self.workspace(window) else {
1389            return;
1390        };
1391        let command = self.update_editor(window, cx, |_, editor, window, cx| {
1392            let snapshot = editor.snapshot(window, cx);
1393            let start = editor.selections.newest_display(cx);
1394            let range = object
1395                .range(&snapshot, start.clone(), around)
1396                .unwrap_or(start.range());
1397            if range.start != start.start {
1398                editor.change_selections(None, window, cx, |s| {
1399                    s.select_ranges([
1400                        range.start.to_point(&snapshot)..range.start.to_point(&snapshot)
1401                    ]);
1402                })
1403            }
1404            if range.end.row() == range.start.row() {
1405                ".!".to_string()
1406            } else {
1407                format!(".,.+{}!", (range.end.row() - range.start.row()).0)
1408            }
1409        });
1410        if let Some(command) = command {
1411            workspace.update(cx, |workspace, cx| {
1412                command_palette::CommandPalette::toggle(workspace, &command, window, cx);
1413            });
1414        }
1415    }
1416}
1417
1418impl ShellExec {
1419    pub fn parse(query: &str, range: Option<CommandRange>) -> Option<Box<dyn Action>> {
1420        let (before, after) = query.split_once('!')?;
1421        let before = before.trim();
1422
1423        if !"read".starts_with(before) {
1424            return None;
1425        }
1426
1427        Some(
1428            ShellExec {
1429                command: after.trim().to_string(),
1430                range,
1431                is_read: !before.is_empty(),
1432            }
1433            .boxed_clone(),
1434        )
1435    }
1436
1437    pub fn run(&self, vim: &mut Vim, window: &mut Window, cx: &mut Context<Vim>) {
1438        let Some(workspace) = vim.workspace(window) else {
1439            return;
1440        };
1441
1442        let project = workspace.read(cx).project().clone();
1443        let command = vim.prepare_shell_command(&self.command, window, cx);
1444
1445        if self.range.is_none() && !self.is_read {
1446            workspace.update(cx, |workspace, cx| {
1447                let project = workspace.project().read(cx);
1448                let cwd = project.first_project_directory(cx);
1449                let shell = project.terminal_settings(&cwd, cx).shell.clone();
1450
1451                let spawn_in_terminal = SpawnInTerminal {
1452                    id: TaskId("vim".to_string()),
1453                    full_label: command.clone(),
1454                    label: command.clone(),
1455                    command: command.clone(),
1456                    args: Vec::new(),
1457                    command_label: command.clone(),
1458                    cwd,
1459                    env: HashMap::default(),
1460                    use_new_terminal: true,
1461                    allow_concurrent_runs: true,
1462                    reveal: RevealStrategy::NoFocus,
1463                    reveal_target: RevealTarget::Dock,
1464                    hide: HideStrategy::Never,
1465                    shell,
1466                    show_summary: false,
1467                    show_command: false,
1468                    show_rerun: false,
1469                };
1470
1471                let task_status = workspace.spawn_in_terminal(spawn_in_terminal, window, cx);
1472                cx.background_spawn(async move {
1473                    match task_status.await {
1474                        Some(Ok(status)) => {
1475                            if status.success() {
1476                                log::debug!("Vim shell exec succeeded");
1477                            } else {
1478                                log::debug!("Vim shell exec failed, code: {:?}", status.code());
1479                            }
1480                        }
1481                        Some(Err(e)) => log::error!("Vim shell exec failed: {e}"),
1482                        None => log::debug!("Vim shell exec got cancelled"),
1483                    }
1484                })
1485                .detach();
1486            });
1487            return;
1488        };
1489
1490        let mut input_snapshot = None;
1491        let mut input_range = None;
1492        let mut needs_newline_prefix = false;
1493        vim.update_editor(window, cx, |vim, editor, window, cx| {
1494            let snapshot = editor.buffer().read(cx).snapshot(cx);
1495            let range = if let Some(range) = self.range.clone() {
1496                let Some(range) = range.buffer_range(vim, editor, window, cx).log_err() else {
1497                    return;
1498                };
1499                Point::new(range.start.0, 0)
1500                    ..snapshot.clip_point(Point::new(range.end.0 + 1, 0), Bias::Right)
1501            } else {
1502                let mut end = editor.selections.newest::<Point>(cx).range().end;
1503                end = snapshot.clip_point(Point::new(end.row + 1, 0), Bias::Right);
1504                needs_newline_prefix = end == snapshot.max_point();
1505                end..end
1506            };
1507            if self.is_read {
1508                input_range =
1509                    Some(snapshot.anchor_after(range.end)..snapshot.anchor_after(range.end));
1510            } else {
1511                input_range =
1512                    Some(snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end));
1513            }
1514            editor.highlight_rows::<ShellExec>(
1515                input_range.clone().unwrap(),
1516                cx.theme().status().unreachable_background,
1517                Default::default(),
1518                cx,
1519            );
1520
1521            if !self.is_read {
1522                input_snapshot = Some(snapshot)
1523            }
1524        });
1525
1526        let Some(range) = input_range else { return };
1527
1528        let mut process = project.read(cx).exec_in_shell(command, cx);
1529        process.stdout(Stdio::piped());
1530        process.stderr(Stdio::piped());
1531
1532        if input_snapshot.is_some() {
1533            process.stdin(Stdio::piped());
1534        } else {
1535            process.stdin(Stdio::null());
1536        };
1537
1538        util::set_pre_exec_to_start_new_session(&mut process);
1539        let is_read = self.is_read;
1540
1541        let task = cx.spawn_in(window, async move |vim, cx| {
1542            let Some(mut running) = process.spawn().log_err() else {
1543                vim.update_in(cx, |vim, window, cx| {
1544                    vim.cancel_running_command(window, cx);
1545                })
1546                .log_err();
1547                return;
1548            };
1549
1550            if let Some(mut stdin) = running.stdin.take() {
1551                if let Some(snapshot) = input_snapshot {
1552                    let range = range.clone();
1553                    cx.background_spawn(async move {
1554                        for chunk in snapshot.text_for_range(range) {
1555                            if stdin.write_all(chunk.as_bytes()).log_err().is_none() {
1556                                return;
1557                            }
1558                        }
1559                        stdin.flush().log_err();
1560                    })
1561                    .detach();
1562                }
1563            };
1564
1565            let output = cx
1566                .background_spawn(async move { running.wait_with_output() })
1567                .await;
1568
1569            let Some(output) = output.log_err() else {
1570                vim.update_in(cx, |vim, window, cx| {
1571                    vim.cancel_running_command(window, cx);
1572                })
1573                .log_err();
1574                return;
1575            };
1576            let mut text = String::new();
1577            if needs_newline_prefix {
1578                text.push('\n');
1579            }
1580            text.push_str(&String::from_utf8_lossy(&output.stdout));
1581            text.push_str(&String::from_utf8_lossy(&output.stderr));
1582            if !text.is_empty() && text.chars().last() != Some('\n') {
1583                text.push('\n');
1584            }
1585
1586            vim.update_in(cx, |vim, window, cx| {
1587                vim.update_editor(window, cx, |_, editor, window, cx| {
1588                    editor.transact(window, cx, |editor, window, cx| {
1589                        editor.edit([(range.clone(), text)], cx);
1590                        let snapshot = editor.buffer().read(cx).snapshot(cx);
1591                        editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
1592                            let point = if is_read {
1593                                let point = range.end.to_point(&snapshot);
1594                                Point::new(point.row.saturating_sub(1), 0)
1595                            } else {
1596                                let point = range.start.to_point(&snapshot);
1597                                Point::new(point.row, 0)
1598                            };
1599                            s.select_ranges([point..point]);
1600                        })
1601                    })
1602                });
1603                vim.cancel_running_command(window, cx);
1604            })
1605            .log_err();
1606        });
1607        vim.running_command.replace(task);
1608    }
1609}
1610
1611#[cfg(test)]
1612mod test {
1613    use std::path::Path;
1614
1615    use crate::{
1616        state::Mode,
1617        test::{NeovimBackedTestContext, VimTestContext},
1618    };
1619    use editor::Editor;
1620    use gpui::{Context, TestAppContext};
1621    use indoc::indoc;
1622    use util::path;
1623    use workspace::Workspace;
1624
1625    #[gpui::test]
1626    async fn test_command_basics(cx: &mut TestAppContext) {
1627        let mut cx = NeovimBackedTestContext::new(cx).await;
1628
1629        cx.set_shared_state(indoc! {"
1630            ˇa
1631            b
1632            c"})
1633            .await;
1634
1635        cx.simulate_shared_keystrokes(": j enter").await;
1636
1637        // hack: our cursor positioning after a join command is wrong
1638        cx.simulate_shared_keystrokes("^").await;
1639        cx.shared_state().await.assert_eq(indoc! {
1640            "ˇa b
1641            c"
1642        });
1643    }
1644
1645    #[gpui::test]
1646    async fn test_command_goto(cx: &mut TestAppContext) {
1647        let mut cx = NeovimBackedTestContext::new(cx).await;
1648
1649        cx.set_shared_state(indoc! {"
1650            ˇa
1651            b
1652            c"})
1653            .await;
1654        cx.simulate_shared_keystrokes(": 3 enter").await;
1655        cx.shared_state().await.assert_eq(indoc! {"
1656            a
1657            b
1658            ˇc"});
1659    }
1660
1661    #[gpui::test]
1662    async fn test_command_replace(cx: &mut TestAppContext) {
1663        let mut cx = NeovimBackedTestContext::new(cx).await;
1664
1665        cx.set_shared_state(indoc! {"
1666            ˇa
1667            b
1668            b
1669            c"})
1670            .await;
1671        cx.simulate_shared_keystrokes(": % s / b / d enter").await;
1672        cx.shared_state().await.assert_eq(indoc! {"
1673            a
1674            d
1675            ˇd
1676            c"});
1677        cx.simulate_shared_keystrokes(": % s : . : \\ 0 \\ 0 enter")
1678            .await;
1679        cx.shared_state().await.assert_eq(indoc! {"
1680            aa
1681            dd
1682            dd
1683            ˇcc"});
1684        cx.simulate_shared_keystrokes("k : s / d d / e e enter")
1685            .await;
1686        cx.shared_state().await.assert_eq(indoc! {"
1687            aa
1688            dd
1689            ˇee
1690            cc"});
1691    }
1692
1693    #[gpui::test]
1694    async fn test_command_search(cx: &mut TestAppContext) {
1695        let mut cx = NeovimBackedTestContext::new(cx).await;
1696
1697        cx.set_shared_state(indoc! {"
1698                ˇa
1699                b
1700                a
1701                c"})
1702            .await;
1703        cx.simulate_shared_keystrokes(": / b enter").await;
1704        cx.shared_state().await.assert_eq(indoc! {"
1705                a
1706                ˇb
1707                a
1708                c"});
1709        cx.simulate_shared_keystrokes(": ? a enter").await;
1710        cx.shared_state().await.assert_eq(indoc! {"
1711                ˇa
1712                b
1713                a
1714                c"});
1715    }
1716
1717    #[gpui::test]
1718    async fn test_command_write(cx: &mut TestAppContext) {
1719        let mut cx = VimTestContext::new(cx, true).await;
1720        let path = Path::new(path!("/root/dir/file.rs"));
1721        let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
1722
1723        cx.simulate_keystrokes("i @ escape");
1724        cx.simulate_keystrokes(": w enter");
1725
1726        assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "@\n");
1727
1728        fs.as_fake().insert_file(path, b"oops\n".to_vec()).await;
1729
1730        // conflict!
1731        cx.simulate_keystrokes("i @ escape");
1732        cx.simulate_keystrokes(": w enter");
1733        cx.simulate_prompt_answer("Cancel");
1734
1735        assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "oops\n");
1736        assert!(!cx.has_pending_prompt());
1737        cx.simulate_keystrokes(": w ! enter");
1738        assert!(!cx.has_pending_prompt());
1739        assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "@@\n");
1740    }
1741
1742    #[gpui::test]
1743    async fn test_command_quit(cx: &mut TestAppContext) {
1744        let mut cx = VimTestContext::new(cx, true).await;
1745
1746        cx.simulate_keystrokes(": n e w enter");
1747        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
1748        cx.simulate_keystrokes(": q enter");
1749        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 1));
1750        cx.simulate_keystrokes(": n e w enter");
1751        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
1752        cx.simulate_keystrokes(": q a enter");
1753        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 0));
1754    }
1755
1756    #[gpui::test]
1757    async fn test_offsets(cx: &mut TestAppContext) {
1758        let mut cx = NeovimBackedTestContext::new(cx).await;
1759
1760        cx.set_shared_state("ˇ1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n")
1761            .await;
1762
1763        cx.simulate_shared_keystrokes(": + enter").await;
1764        cx.shared_state()
1765            .await
1766            .assert_eq("1\nˇ2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n");
1767
1768        cx.simulate_shared_keystrokes(": 1 0 - enter").await;
1769        cx.shared_state()
1770            .await
1771            .assert_eq("1\n2\n3\n4\n5\n6\n7\n8\nˇ9\n10\n11\n");
1772
1773        cx.simulate_shared_keystrokes(": . - 2 enter").await;
1774        cx.shared_state()
1775            .await
1776            .assert_eq("1\n2\n3\n4\n5\n6\nˇ7\n8\n9\n10\n11\n");
1777
1778        cx.simulate_shared_keystrokes(": % enter").await;
1779        cx.shared_state()
1780            .await
1781            .assert_eq("1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\nˇ");
1782    }
1783
1784    #[gpui::test]
1785    async fn test_command_ranges(cx: &mut TestAppContext) {
1786        let mut cx = NeovimBackedTestContext::new(cx).await;
1787
1788        cx.set_shared_state("ˇ1\n2\n3\n4\n4\n3\n2\n1").await;
1789
1790        cx.simulate_shared_keystrokes(": 2 , 4 d enter").await;
1791        cx.shared_state().await.assert_eq("1\nˇ4\n3\n2\n1");
1792
1793        cx.simulate_shared_keystrokes(": 2 , 4 s o r t enter").await;
1794        cx.shared_state().await.assert_eq("1\nˇ2\n3\n4\n1");
1795
1796        cx.simulate_shared_keystrokes(": 2 , 4 j o i n enter").await;
1797        cx.shared_state().await.assert_eq("1\nˇ2 3 4\n1");
1798    }
1799
1800    #[gpui::test]
1801    async fn test_command_visual_replace(cx: &mut TestAppContext) {
1802        let mut cx = NeovimBackedTestContext::new(cx).await;
1803
1804        cx.set_shared_state("ˇ1\n2\n3\n4\n4\n3\n2\n1").await;
1805
1806        cx.simulate_shared_keystrokes("v 2 j : s / . / k enter")
1807            .await;
1808        cx.shared_state().await.assert_eq("k\nk\nˇk\n4\n4\n3\n2\n1");
1809    }
1810
1811    fn assert_active_item(
1812        workspace: &mut Workspace,
1813        expected_path: &str,
1814        expected_text: &str,
1815        cx: &mut Context<Workspace>,
1816    ) {
1817        let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();
1818
1819        let buffer = active_editor
1820            .read(cx)
1821            .buffer()
1822            .read(cx)
1823            .as_singleton()
1824            .unwrap();
1825
1826        let text = buffer.read(cx).text();
1827        let file = buffer.read(cx).file().unwrap();
1828        let file_path = file.as_local().unwrap().abs_path(cx);
1829
1830        assert_eq!(text, expected_text);
1831        assert_eq!(file_path, Path::new(expected_path));
1832    }
1833
1834    #[gpui::test]
1835    async fn test_command_gf(cx: &mut TestAppContext) {
1836        let mut cx = VimTestContext::new(cx, true).await;
1837
1838        // Assert base state, that we're in /root/dir/file.rs
1839        cx.workspace(|workspace, _, cx| {
1840            assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
1841        });
1842
1843        // Insert a new file
1844        let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
1845        fs.as_fake()
1846            .insert_file(
1847                path!("/root/dir/file2.rs"),
1848                "This is file2.rs".as_bytes().to_vec(),
1849            )
1850            .await;
1851        fs.as_fake()
1852            .insert_file(
1853                path!("/root/dir/file3.rs"),
1854                "go to file3".as_bytes().to_vec(),
1855            )
1856            .await;
1857
1858        // Put the path to the second file into the currently open buffer
1859        cx.set_state(indoc! {"go to fiˇle2.rs"}, Mode::Normal);
1860
1861        // Go to file2.rs
1862        cx.simulate_keystrokes("g f");
1863
1864        // We now have two items
1865        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
1866        cx.workspace(|workspace, _, cx| {
1867            assert_active_item(
1868                workspace,
1869                path!("/root/dir/file2.rs"),
1870                "This is file2.rs",
1871                cx,
1872            );
1873        });
1874
1875        // Update editor to point to `file2.rs`
1876        cx.editor =
1877            cx.workspace(|workspace, _, cx| workspace.active_item_as::<Editor>(cx).unwrap());
1878
1879        // Put the path to the third file into the currently open buffer,
1880        // but remove its suffix, because we want that lookup to happen automatically.
1881        cx.set_state(indoc! {"go to fiˇle3"}, Mode::Normal);
1882
1883        // Go to file3.rs
1884        cx.simulate_keystrokes("g f");
1885
1886        // We now have three items
1887        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 3));
1888        cx.workspace(|workspace, _, cx| {
1889            assert_active_item(workspace, path!("/root/dir/file3.rs"), "go to file3", cx);
1890        });
1891    }
1892
1893    #[gpui::test]
1894    async fn test_command_matching_lines(cx: &mut TestAppContext) {
1895        let mut cx = NeovimBackedTestContext::new(cx).await;
1896
1897        cx.set_shared_state(indoc! {"
1898            ˇa
1899            b
1900            a
1901            b
1902            a
1903        "})
1904            .await;
1905
1906        cx.simulate_shared_keystrokes(":").await;
1907        cx.simulate_shared_keystrokes("g / a / d").await;
1908        cx.simulate_shared_keystrokes("enter").await;
1909
1910        cx.shared_state().await.assert_eq(indoc! {"
1911            b
1912            b
1913            ˇ"});
1914
1915        cx.simulate_shared_keystrokes("u").await;
1916
1917        cx.shared_state().await.assert_eq(indoc! {"
1918            ˇa
1919            b
1920            a
1921            b
1922            a
1923        "});
1924
1925        cx.simulate_shared_keystrokes(":").await;
1926        cx.simulate_shared_keystrokes("v / a / d").await;
1927        cx.simulate_shared_keystrokes("enter").await;
1928
1929        cx.shared_state().await.assert_eq(indoc! {"
1930            a
1931            a
1932            ˇa"});
1933    }
1934}