command.rs

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