command.rs

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