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