command.rs

   1use std::{
   2    iter::Peekable,
   3    ops::{Deref, Range},
   4    str::Chars,
   5    sync::OnceLock,
   6    time::Instant,
   7};
   8
   9use anyhow::{anyhow, Result};
  10use command_palette_hooks::CommandInterceptResult;
  11use editor::{
  12    actions::{SortLinesCaseInsensitive, SortLinesCaseSensitive},
  13    display_map::ToDisplayPoint,
  14    Bias, Editor, ToPoint,
  15};
  16use gpui::{actions, impl_actions, Action, AppContext, Global, ViewContext, WindowContext};
  17use language::Point;
  18use multi_buffer::MultiBufferRow;
  19use regex::Regex;
  20use search::{BufferSearchBar, SearchOptions};
  21use serde::Deserialize;
  22use util::ResultExt;
  23use workspace::{notifications::NotifyResultExt, SaveIntent};
  24
  25use crate::{
  26    motion::{EndOfDocument, Motion, StartOfDocument},
  27    normal::{
  28        search::{FindCommand, ReplaceCommand, Replacement},
  29        JoinLines,
  30    },
  31    state::Mode,
  32    visual::VisualDeleteLine,
  33    Vim,
  34};
  35
  36#[derive(Debug, Clone, PartialEq, Deserialize)]
  37pub struct GoToLine {
  38    range: CommandRange,
  39}
  40
  41#[derive(Debug, Clone, PartialEq, Deserialize)]
  42pub struct YankCommand {
  43    range: CommandRange,
  44}
  45
  46#[derive(Debug, Clone, PartialEq, Deserialize)]
  47pub struct WithRange {
  48    restore_selection: bool,
  49    range: CommandRange,
  50    action: WrappedAction,
  51}
  52
  53#[derive(Debug, Clone, PartialEq, Deserialize)]
  54pub struct WithCount {
  55    count: u32,
  56    action: WrappedAction,
  57}
  58
  59#[derive(Debug)]
  60struct WrappedAction(Box<dyn Action>);
  61
  62actions!(vim, [VisualCommand, CountCommand]);
  63impl_actions!(
  64    vim,
  65    [GoToLine, YankCommand, WithRange, WithCount, OnMatchingLines]
  66);
  67
  68impl<'de> Deserialize<'de> for WrappedAction {
  69    fn deserialize<D>(_: D) -> Result<Self, D::Error>
  70    where
  71        D: serde::Deserializer<'de>,
  72    {
  73        Err(serde::de::Error::custom("Cannot deserialize WrappedAction"))
  74    }
  75}
  76
  77impl PartialEq for WrappedAction {
  78    fn eq(&self, other: &Self) -> bool {
  79        self.0.partial_eq(&*other.0)
  80    }
  81}
  82
  83impl Clone for WrappedAction {
  84    fn clone(&self) -> Self {
  85        Self(self.0.boxed_clone())
  86    }
  87}
  88
  89impl Deref for WrappedAction {
  90    type Target = dyn Action;
  91    fn deref(&self) -> &dyn Action {
  92        &*self.0
  93    }
  94}
  95
  96pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
  97    Vim::action(editor, cx, |vim, _: &VisualCommand, cx| {
  98        let Some(workspace) = vim.workspace(cx) else {
  99            return;
 100        };
 101        workspace.update(cx, |workspace, cx| {
 102            command_palette::CommandPalette::toggle(workspace, "'<,'>", cx);
 103        })
 104    });
 105
 106    Vim::action(editor, cx, |vim, _: &CountCommand, cx| {
 107        let Some(workspace) = vim.workspace(cx) else {
 108            return;
 109        };
 110        let count = Vim::take_count(cx).unwrap_or(1);
 111        workspace.update(cx, |workspace, cx| {
 112            command_palette::CommandPalette::toggle(
 113                workspace,
 114                &format!(".,.+{}", count.saturating_sub(1)),
 115                cx,
 116            );
 117        })
 118    });
 119
 120    Vim::action(editor, cx, |vim, action: &GoToLine, cx| {
 121        vim.switch_mode(Mode::Normal, false, cx);
 122        let result = vim.update_editor(cx, |vim, editor, cx| {
 123            action.range.head().buffer_row(vim, editor, cx)
 124        });
 125        let buffer_row = match result {
 126            None => return,
 127            Some(e @ Err(_)) => {
 128                let Some(workspace) = vim.workspace(cx) else {
 129                    return;
 130                };
 131                workspace.update(cx, |workspace, cx| {
 132                    e.notify_err(workspace, cx);
 133                });
 134                return;
 135            }
 136            Some(Ok(result)) => result,
 137        };
 138        vim.move_cursor(Motion::StartOfDocument, Some(buffer_row.0 as usize + 1), cx);
 139    });
 140
 141    Vim::action(editor, cx, |vim, action: &YankCommand, cx| {
 142        vim.update_editor(cx, |vim, editor, cx| {
 143            let snapshot = editor.snapshot(cx);
 144            if let Ok(range) = action.range.buffer_range(vim, editor, cx) {
 145                let end = if range.end < snapshot.buffer_snapshot.max_row() {
 146                    Point::new(range.end.0 + 1, 0)
 147                } else {
 148                    snapshot.buffer_snapshot.max_point()
 149                };
 150                vim.copy_ranges(
 151                    editor,
 152                    true,
 153                    true,
 154                    vec![Point::new(range.start.0, 0)..end],
 155                    cx,
 156                )
 157            }
 158        });
 159    });
 160
 161    Vim::action(editor, cx, |_, action: &WithCount, cx| {
 162        for _ in 0..action.count {
 163            cx.dispatch_action(action.action.boxed_clone())
 164        }
 165    });
 166
 167    Vim::action(editor, cx, |vim, action: &WithRange, cx| {
 168        let result = vim.update_editor(cx, |vim, editor, cx| {
 169            action.range.buffer_range(vim, editor, cx)
 170        });
 171
 172        let range = match result {
 173            None => return,
 174            Some(e @ Err(_)) => {
 175                let Some(workspace) = vim.workspace(cx) else {
 176                    return;
 177                };
 178                workspace.update(cx, |workspace, cx| {
 179                    e.notify_err(workspace, cx);
 180                });
 181                return;
 182            }
 183            Some(Ok(result)) => result,
 184        };
 185
 186        let previous_selections = vim
 187            .update_editor(cx, |_, editor, cx| {
 188                let selections = action
 189                    .restore_selection
 190                    .then(|| editor.selections.disjoint_anchor_ranges());
 191                editor.change_selections(None, cx, |s| {
 192                    let end = Point::new(range.end.0, s.buffer().line_len(range.end));
 193                    s.select_ranges([end..Point::new(range.start.0, 0)]);
 194                });
 195                selections
 196            })
 197            .flatten();
 198        cx.dispatch_action(action.action.boxed_clone());
 199        cx.defer(move |vim, cx| {
 200            vim.update_editor(cx, |_, editor, cx| {
 201                editor.change_selections(None, cx, |s| {
 202                    if let Some(previous_selections) = previous_selections {
 203                        s.select_ranges(previous_selections);
 204                    } else {
 205                        s.select_ranges([
 206                            Point::new(range.start.0, 0)..Point::new(range.start.0, 0)
 207                        ]);
 208                    }
 209                })
 210            });
 211        });
 212    });
 213
 214    Vim::action(editor, cx, |vim, action: &OnMatchingLines, cx| {
 215        action.run(vim, cx)
 216    })
 217}
 218
 219#[derive(Default)]
 220struct VimCommand {
 221    prefix: &'static str,
 222    suffix: &'static str,
 223    action: Option<Box<dyn Action>>,
 224    action_name: Option<&'static str>,
 225    bang_action: Option<Box<dyn Action>>,
 226    range: Option<
 227        Box<
 228            dyn Fn(Box<dyn Action>, &CommandRange) -> Option<Box<dyn Action>>
 229                + Send
 230                + Sync
 231                + 'static,
 232        >,
 233    >,
 234    has_count: bool,
 235}
 236
 237impl VimCommand {
 238    fn new(pattern: (&'static str, &'static str), action: impl Action) -> Self {
 239        Self {
 240            prefix: pattern.0,
 241            suffix: pattern.1,
 242            action: Some(action.boxed_clone()),
 243            ..Default::default()
 244        }
 245    }
 246
 247    // from_str is used for actions in other crates.
 248    fn str(pattern: (&'static str, &'static str), action_name: &'static str) -> Self {
 249        Self {
 250            prefix: pattern.0,
 251            suffix: pattern.1,
 252            action_name: Some(action_name),
 253            ..Default::default()
 254        }
 255    }
 256
 257    fn bang(mut self, bang_action: impl Action) -> Self {
 258        self.bang_action = Some(bang_action.boxed_clone());
 259        self
 260    }
 261
 262    fn range(
 263        mut self,
 264        f: impl Fn(Box<dyn Action>, &CommandRange) -> Option<Box<dyn Action>> + Send + Sync + 'static,
 265    ) -> Self {
 266        self.range = Some(Box::new(f));
 267        self
 268    }
 269
 270    fn count(mut self) -> Self {
 271        self.has_count = true;
 272        self
 273    }
 274
 275    fn parse(
 276        &self,
 277        mut query: &str,
 278        range: &Option<CommandRange>,
 279        cx: &AppContext,
 280    ) -> Option<Box<dyn Action>> {
 281        let has_bang = query.ends_with('!');
 282        if has_bang {
 283            query = &query[..query.len() - 1];
 284        }
 285
 286        let suffix = query.strip_prefix(self.prefix)?;
 287        if !self.suffix.starts_with(suffix) {
 288            return None;
 289        }
 290
 291        let action = if has_bang && self.bang_action.is_some() {
 292            self.bang_action.as_ref().unwrap().boxed_clone()
 293        } else if let Some(action) = self.action.as_ref() {
 294            action.boxed_clone()
 295        } else if let Some(action_name) = self.action_name {
 296            cx.build_action(action_name, None).log_err()?
 297        } else {
 298            return None;
 299        };
 300
 301        if let Some(range) = range {
 302            self.range.as_ref().and_then(|f| f(action, range))
 303        } else {
 304            Some(action)
 305        }
 306    }
 307
 308    // TODO: ranges with search queries
 309    fn parse_range(query: &str) -> (Option<CommandRange>, String) {
 310        let mut chars = query.chars().peekable();
 311
 312        match chars.peek() {
 313            Some('%') => {
 314                chars.next();
 315                return (
 316                    Some(CommandRange {
 317                        start: Position::Line { row: 1, offset: 0 },
 318                        end: Some(Position::LastLine { offset: 0 }),
 319                    }),
 320                    chars.collect(),
 321                );
 322            }
 323            Some('*') => {
 324                chars.next();
 325                return (
 326                    Some(CommandRange {
 327                        start: Position::Mark {
 328                            name: '<',
 329                            offset: 0,
 330                        },
 331                        end: Some(Position::Mark {
 332                            name: '>',
 333                            offset: 0,
 334                        }),
 335                    }),
 336                    chars.collect(),
 337                );
 338            }
 339            _ => {}
 340        }
 341
 342        let start = Self::parse_position(&mut chars);
 343
 344        match chars.peek() {
 345            Some(',' | ';') => {
 346                chars.next();
 347                (
 348                    Some(CommandRange {
 349                        start: start.unwrap_or(Position::CurrentLine { offset: 0 }),
 350                        end: Self::parse_position(&mut chars),
 351                    }),
 352                    chars.collect(),
 353                )
 354            }
 355            _ => (
 356                start.map(|start| CommandRange { start, end: None }),
 357                chars.collect(),
 358            ),
 359        }
 360    }
 361
 362    fn parse_position(chars: &mut Peekable<Chars>) -> Option<Position> {
 363        match chars.peek()? {
 364            '0'..='9' => {
 365                let row = Self::parse_u32(chars);
 366                Some(Position::Line {
 367                    row,
 368                    offset: Self::parse_offset(chars),
 369                })
 370            }
 371            '\'' => {
 372                chars.next();
 373                let name = chars.next()?;
 374                Some(Position::Mark {
 375                    name,
 376                    offset: Self::parse_offset(chars),
 377                })
 378            }
 379            '.' => {
 380                chars.next();
 381                Some(Position::CurrentLine {
 382                    offset: Self::parse_offset(chars),
 383                })
 384            }
 385            '+' | '-' => Some(Position::CurrentLine {
 386                offset: Self::parse_offset(chars),
 387            }),
 388            '$' => {
 389                chars.next();
 390                Some(Position::LastLine {
 391                    offset: Self::parse_offset(chars),
 392                })
 393            }
 394            _ => None,
 395        }
 396    }
 397
 398    fn parse_offset(chars: &mut Peekable<Chars>) -> i32 {
 399        let mut res: i32 = 0;
 400        while matches!(chars.peek(), Some('+' | '-')) {
 401            let sign = if chars.next().unwrap() == '+' { 1 } else { -1 };
 402            let amount = if matches!(chars.peek(), Some('0'..='9')) {
 403                (Self::parse_u32(chars) as i32).saturating_mul(sign)
 404            } else {
 405                sign
 406            };
 407            res = res.saturating_add(amount)
 408        }
 409        res
 410    }
 411
 412    fn parse_u32(chars: &mut Peekable<Chars>) -> u32 {
 413        let mut res: u32 = 0;
 414        while matches!(chars.peek(), Some('0'..='9')) {
 415            res = res
 416                .saturating_mul(10)
 417                .saturating_add(chars.next().unwrap() as u32 - '0' as u32);
 418        }
 419        res
 420    }
 421}
 422
 423#[derive(Debug, Clone, PartialEq, Deserialize)]
 424enum Position {
 425    Line { row: u32, offset: i32 },
 426    Mark { name: char, offset: i32 },
 427    LastLine { offset: i32 },
 428    CurrentLine { offset: i32 },
 429}
 430
 431impl Position {
 432    fn buffer_row(
 433        &self,
 434        vim: &Vim,
 435        editor: &mut Editor,
 436        cx: &mut WindowContext,
 437    ) -> Result<MultiBufferRow> {
 438        let snapshot = editor.snapshot(cx);
 439        let target = match self {
 440            Position::Line { row, offset } => row.saturating_add_signed(offset.saturating_sub(1)),
 441            Position::Mark { name, offset } => {
 442                let Some(mark) = vim.marks.get(&name.to_string()).and_then(|vec| vec.last()) else {
 443                    return Err(anyhow!("mark {} not set", name));
 444                };
 445                mark.to_point(&snapshot.buffer_snapshot)
 446                    .row
 447                    .saturating_add_signed(*offset)
 448            }
 449            Position::LastLine { offset } => snapshot
 450                .buffer_snapshot
 451                .max_row()
 452                .0
 453                .saturating_add_signed(*offset),
 454            Position::CurrentLine { offset } => editor
 455                .selections
 456                .newest_anchor()
 457                .head()
 458                .to_point(&snapshot.buffer_snapshot)
 459                .row
 460                .saturating_add_signed(*offset),
 461        };
 462
 463        Ok(MultiBufferRow(target).min(snapshot.buffer_snapshot.max_row()))
 464    }
 465}
 466
 467#[derive(Debug, Clone, PartialEq, Deserialize)]
 468pub(crate) struct CommandRange {
 469    start: Position,
 470    end: Option<Position>,
 471}
 472
 473impl CommandRange {
 474    fn head(&self) -> &Position {
 475        self.end.as_ref().unwrap_or(&self.start)
 476    }
 477
 478    pub(crate) fn buffer_range(
 479        &self,
 480        vim: &Vim,
 481        editor: &mut Editor,
 482        cx: &mut WindowContext,
 483    ) -> Result<Range<MultiBufferRow>> {
 484        let start = self.start.buffer_row(vim, editor, cx)?;
 485        let end = if let Some(end) = self.end.as_ref() {
 486            end.buffer_row(vim, editor, cx)?
 487        } else {
 488            start
 489        };
 490        if end < start {
 491            anyhow::Ok(end..start)
 492        } else {
 493            anyhow::Ok(start..end)
 494        }
 495    }
 496
 497    pub fn as_count(&self) -> Option<u32> {
 498        if let CommandRange {
 499            start: Position::Line { row, offset: 0 },
 500            end: None,
 501        } = &self
 502        {
 503            Some(*row)
 504        } else {
 505            None
 506        }
 507    }
 508}
 509
 510fn generate_commands(_: &AppContext) -> Vec<VimCommand> {
 511    vec![
 512        VimCommand::new(
 513            ("w", "rite"),
 514            workspace::Save {
 515                save_intent: Some(SaveIntent::Save),
 516            },
 517        )
 518        .bang(workspace::Save {
 519            save_intent: Some(SaveIntent::Overwrite),
 520        }),
 521        VimCommand::new(
 522            ("q", "uit"),
 523            workspace::CloseActiveItem {
 524                save_intent: Some(SaveIntent::Close),
 525            },
 526        )
 527        .bang(workspace::CloseActiveItem {
 528            save_intent: Some(SaveIntent::Skip),
 529        }),
 530        VimCommand::new(
 531            ("wq", ""),
 532            workspace::CloseActiveItem {
 533                save_intent: Some(SaveIntent::Save),
 534            },
 535        )
 536        .bang(workspace::CloseActiveItem {
 537            save_intent: Some(SaveIntent::Overwrite),
 538        }),
 539        VimCommand::new(
 540            ("x", "it"),
 541            workspace::CloseActiveItem {
 542                save_intent: Some(SaveIntent::SaveAll),
 543            },
 544        )
 545        .bang(workspace::CloseActiveItem {
 546            save_intent: Some(SaveIntent::Overwrite),
 547        }),
 548        VimCommand::new(
 549            ("ex", "it"),
 550            workspace::CloseActiveItem {
 551                save_intent: Some(SaveIntent::SaveAll),
 552            },
 553        )
 554        .bang(workspace::CloseActiveItem {
 555            save_intent: Some(SaveIntent::Overwrite),
 556        }),
 557        VimCommand::new(
 558            ("up", "date"),
 559            workspace::Save {
 560                save_intent: Some(SaveIntent::SaveAll),
 561            },
 562        ),
 563        VimCommand::new(
 564            ("wa", "ll"),
 565            workspace::SaveAll {
 566                save_intent: Some(SaveIntent::SaveAll),
 567            },
 568        )
 569        .bang(workspace::SaveAll {
 570            save_intent: Some(SaveIntent::Overwrite),
 571        }),
 572        VimCommand::new(
 573            ("qa", "ll"),
 574            workspace::CloseAllItemsAndPanes {
 575                save_intent: Some(SaveIntent::Close),
 576            },
 577        )
 578        .bang(workspace::CloseAllItemsAndPanes {
 579            save_intent: Some(SaveIntent::Skip),
 580        }),
 581        VimCommand::new(
 582            ("quita", "ll"),
 583            workspace::CloseAllItemsAndPanes {
 584                save_intent: Some(SaveIntent::Close),
 585            },
 586        )
 587        .bang(workspace::CloseAllItemsAndPanes {
 588            save_intent: Some(SaveIntent::Skip),
 589        }),
 590        VimCommand::new(
 591            ("xa", "ll"),
 592            workspace::CloseAllItemsAndPanes {
 593                save_intent: Some(SaveIntent::SaveAll),
 594            },
 595        )
 596        .bang(workspace::CloseAllItemsAndPanes {
 597            save_intent: Some(SaveIntent::Overwrite),
 598        }),
 599        VimCommand::new(
 600            ("wqa", "ll"),
 601            workspace::CloseAllItemsAndPanes {
 602                save_intent: Some(SaveIntent::SaveAll),
 603            },
 604        )
 605        .bang(workspace::CloseAllItemsAndPanes {
 606            save_intent: Some(SaveIntent::Overwrite),
 607        }),
 608        VimCommand::new(("cq", "uit"), zed_actions::Quit),
 609        VimCommand::new(("sp", "lit"), workspace::SplitHorizontal),
 610        VimCommand::new(("vs", "plit"), workspace::SplitVertical),
 611        VimCommand::new(
 612            ("bd", "elete"),
 613            workspace::CloseActiveItem {
 614                save_intent: Some(SaveIntent::Close),
 615            },
 616        )
 617        .bang(workspace::CloseActiveItem {
 618            save_intent: Some(SaveIntent::Skip),
 619        }),
 620        VimCommand::new(("bn", "ext"), workspace::ActivateNextItem).count(),
 621        VimCommand::new(("bN", "ext"), workspace::ActivatePrevItem).count(),
 622        VimCommand::new(("bp", "revious"), workspace::ActivatePrevItem).count(),
 623        VimCommand::new(("bf", "irst"), workspace::ActivateItem(0)),
 624        VimCommand::new(("br", "ewind"), workspace::ActivateItem(0)),
 625        VimCommand::new(("bl", "ast"), workspace::ActivateLastItem),
 626        VimCommand::new(("new", ""), workspace::NewFileSplitHorizontal),
 627        VimCommand::new(("vne", "w"), workspace::NewFileSplitVertical),
 628        VimCommand::new(("tabe", "dit"), workspace::NewFile),
 629        VimCommand::new(("tabnew", ""), workspace::NewFile),
 630        VimCommand::new(("tabn", "ext"), workspace::ActivateNextItem).count(),
 631        VimCommand::new(("tabp", "revious"), workspace::ActivatePrevItem).count(),
 632        VimCommand::new(("tabN", "ext"), workspace::ActivatePrevItem).count(),
 633        VimCommand::new(
 634            ("tabc", "lose"),
 635            workspace::CloseActiveItem {
 636                save_intent: Some(SaveIntent::Close),
 637            },
 638        ),
 639        VimCommand::new(
 640            ("tabo", "nly"),
 641            workspace::CloseInactiveItems {
 642                save_intent: Some(SaveIntent::Close),
 643                close_pinned: false,
 644            },
 645        )
 646        .bang(workspace::CloseInactiveItems {
 647            save_intent: Some(SaveIntent::Skip),
 648            close_pinned: false,
 649        }),
 650        VimCommand::new(
 651            ("on", "ly"),
 652            workspace::CloseInactiveTabsAndPanes {
 653                save_intent: Some(SaveIntent::Close),
 654            },
 655        )
 656        .bang(workspace::CloseInactiveTabsAndPanes {
 657            save_intent: Some(SaveIntent::Skip),
 658        }),
 659        VimCommand::str(("cl", "ist"), "diagnostics::Deploy"),
 660        VimCommand::new(("cc", ""), editor::actions::Hover),
 661        VimCommand::new(("ll", ""), editor::actions::Hover),
 662        VimCommand::new(("cn", "ext"), editor::actions::GoToDiagnostic).range(wrap_count),
 663        VimCommand::new(("cp", "revious"), editor::actions::GoToPrevDiagnostic).range(wrap_count),
 664        VimCommand::new(("cN", "ext"), editor::actions::GoToPrevDiagnostic).range(wrap_count),
 665        VimCommand::new(("lp", "revious"), editor::actions::GoToPrevDiagnostic).range(wrap_count),
 666        VimCommand::new(("lN", "ext"), editor::actions::GoToPrevDiagnostic).range(wrap_count),
 667        VimCommand::new(("j", "oin"), JoinLines).range(select_range),
 668        VimCommand::new(("fo", "ld"), editor::actions::FoldSelectedRanges).range(act_on_range),
 669        VimCommand::new(("foldo", "pen"), editor::actions::UnfoldLines)
 670            .bang(editor::actions::UnfoldRecursive)
 671            .range(act_on_range),
 672        VimCommand::new(("foldc", "lose"), editor::actions::Fold)
 673            .bang(editor::actions::FoldRecursive)
 674            .range(act_on_range),
 675        VimCommand::new(("dif", "fupdate"), editor::actions::ToggleHunkDiff).range(act_on_range),
 676        VimCommand::new(("rev", "ert"), editor::actions::RevertSelectedHunks).range(act_on_range),
 677        VimCommand::new(("d", "elete"), VisualDeleteLine).range(select_range),
 678        VimCommand::new(("y", "ank"), gpui::NoAction).range(|_, range| {
 679            Some(
 680                YankCommand {
 681                    range: range.clone(),
 682                }
 683                .boxed_clone(),
 684            )
 685        }),
 686        VimCommand::new(("sor", "t"), SortLinesCaseSensitive).range(select_range),
 687        VimCommand::new(("sort i", ""), SortLinesCaseInsensitive).range(select_range),
 688        VimCommand::str(("E", "xplore"), "project_panel::ToggleFocus"),
 689        VimCommand::str(("H", "explore"), "project_panel::ToggleFocus"),
 690        VimCommand::str(("L", "explore"), "project_panel::ToggleFocus"),
 691        VimCommand::str(("S", "explore"), "project_panel::ToggleFocus"),
 692        VimCommand::str(("Ve", "xplore"), "project_panel::ToggleFocus"),
 693        VimCommand::str(("te", "rm"), "terminal_panel::ToggleFocus"),
 694        VimCommand::str(("T", "erm"), "terminal_panel::ToggleFocus"),
 695        VimCommand::str(("C", "ollab"), "collab_panel::ToggleFocus"),
 696        VimCommand::str(("Ch", "at"), "chat_panel::ToggleFocus"),
 697        VimCommand::str(("No", "tifications"), "notification_panel::ToggleFocus"),
 698        VimCommand::str(("A", "I"), "assistant::ToggleFocus"),
 699        VimCommand::new(("noh", "lsearch"), search::buffer_search::Dismiss),
 700        VimCommand::new(("$", ""), EndOfDocument),
 701        VimCommand::new(("%", ""), EndOfDocument),
 702        VimCommand::new(("0", ""), StartOfDocument),
 703        VimCommand::new(("e", "dit"), editor::actions::ReloadFile)
 704            .bang(editor::actions::ReloadFile),
 705        VimCommand::new(("cpp", "link"), editor::actions::CopyPermalinkToLine).range(act_on_range),
 706    ]
 707}
 708
 709struct VimCommands(Vec<VimCommand>);
 710// safety: we only ever access this from the main thread (as ensured by the cx argument)
 711// actions are not Sync so we can't otherwise use a OnceLock.
 712unsafe impl Sync for VimCommands {}
 713impl Global for VimCommands {}
 714
 715fn commands(cx: &AppContext) -> &Vec<VimCommand> {
 716    static COMMANDS: OnceLock<VimCommands> = OnceLock::new();
 717    &COMMANDS
 718        .get_or_init(|| VimCommands(generate_commands(cx)))
 719        .0
 720}
 721
 722fn act_on_range(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
 723    Some(
 724        WithRange {
 725            restore_selection: true,
 726            range: range.clone(),
 727            action: WrappedAction(action),
 728        }
 729        .boxed_clone(),
 730    )
 731}
 732
 733fn select_range(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
 734    Some(
 735        WithRange {
 736            restore_selection: false,
 737            range: range.clone(),
 738            action: WrappedAction(action),
 739        }
 740        .boxed_clone(),
 741    )
 742}
 743
 744fn wrap_count(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
 745    range.as_count().map(|count| {
 746        WithCount {
 747            count,
 748            action: WrappedAction(action),
 749        }
 750        .boxed_clone()
 751    })
 752}
 753
 754pub fn command_interceptor(mut input: &str, cx: &AppContext) -> Option<CommandInterceptResult> {
 755    // NOTE: We also need to support passing arguments to commands like :w
 756    // (ideally with filename autocompletion).
 757    while input.starts_with(':') {
 758        input = &input[1..];
 759    }
 760
 761    let (range, query) = VimCommand::parse_range(input);
 762    let range_prefix = input[0..(input.len() - query.len())].to_string();
 763    let query = query.as_str().trim();
 764
 765    let action = if range.is_some() && query.is_empty() {
 766        Some(
 767            GoToLine {
 768                range: range.clone().unwrap(),
 769            }
 770            .boxed_clone(),
 771        )
 772    } else if query.starts_with('/') || query.starts_with('?') {
 773        Some(
 774            FindCommand {
 775                query: query[1..].to_string(),
 776                backwards: query.starts_with('?'),
 777            }
 778            .boxed_clone(),
 779        )
 780    } else if query.starts_with('s') {
 781        let mut substitute = "substitute".chars().peekable();
 782        let mut query = query.chars().peekable();
 783        while substitute
 784            .peek()
 785            .is_some_and(|char| Some(char) == query.peek())
 786        {
 787            substitute.next();
 788            query.next();
 789        }
 790        if let Some(replacement) = Replacement::parse(query) {
 791            let range = range.clone().unwrap_or(CommandRange {
 792                start: Position::CurrentLine { offset: 0 },
 793                end: None,
 794            });
 795            Some(ReplaceCommand { replacement, range }.boxed_clone())
 796        } else {
 797            None
 798        }
 799    } else if query.starts_with('g') || query.starts_with('v') {
 800        let mut global = "global".chars().peekable();
 801        let mut query = query.chars().peekable();
 802        let mut invert = false;
 803        if query.peek() == Some(&'v') {
 804            invert = true;
 805            query.next();
 806        }
 807        while global.peek().is_some_and(|char| Some(char) == query.peek()) {
 808            global.next();
 809            query.next();
 810        }
 811        if !invert && query.peek() == Some(&'!') {
 812            invert = true;
 813            query.next();
 814        }
 815        let range = range.clone().unwrap_or(CommandRange {
 816            start: Position::Line { row: 0, offset: 0 },
 817            end: Some(Position::LastLine { offset: 0 }),
 818        });
 819        if let Some(action) = OnMatchingLines::parse(query, invert, range, cx) {
 820            Some(action.boxed_clone())
 821        } else {
 822            None
 823        }
 824    } else {
 825        None
 826    };
 827    if let Some(action) = action {
 828        let string = input.to_string();
 829        let positions = generate_positions(&string, &(range_prefix + query));
 830        return Some(CommandInterceptResult {
 831            action,
 832            string,
 833            positions,
 834        });
 835    }
 836
 837    for command in commands(cx).iter() {
 838        if let Some(action) = command.parse(query, &range, cx) {
 839            let mut string = ":".to_owned() + &range_prefix + command.prefix + command.suffix;
 840            if query.ends_with('!') {
 841                string.push('!');
 842            }
 843            let positions = generate_positions(&string, &(range_prefix + query));
 844
 845            return Some(CommandInterceptResult {
 846                action,
 847                string,
 848                positions,
 849            });
 850        }
 851    }
 852    None
 853}
 854
 855fn generate_positions(string: &str, query: &str) -> Vec<usize> {
 856    let mut positions = Vec::new();
 857    let mut chars = query.chars();
 858
 859    let Some(mut current) = chars.next() else {
 860        return positions;
 861    };
 862
 863    for (i, c) in string.char_indices() {
 864        if c == current {
 865            positions.push(i);
 866            if let Some(c) = chars.next() {
 867                current = c;
 868            } else {
 869                break;
 870            }
 871        }
 872    }
 873
 874    positions
 875}
 876
 877#[derive(Debug, PartialEq, Deserialize, Clone)]
 878pub(crate) struct OnMatchingLines {
 879    range: CommandRange,
 880    search: String,
 881    action: WrappedAction,
 882    invert: bool,
 883}
 884
 885impl OnMatchingLines {
 886    // convert a vim query into something more usable by zed.
 887    // we don't attempt to fully convert between the two regex syntaxes,
 888    // but we do flip \( and \) to ( and ) (and vice-versa) in the pattern,
 889    // and convert \0..\9 to $0..$9 in the replacement so that common idioms work.
 890    pub(crate) fn parse(
 891        mut chars: Peekable<Chars>,
 892        invert: bool,
 893        range: CommandRange,
 894        cx: &AppContext,
 895    ) -> Option<Self> {
 896        let delimiter = chars.next().filter(|c| {
 897            !c.is_alphanumeric() && *c != '"' && *c != '|' && *c != '\'' && *c != '!'
 898        })?;
 899
 900        let mut search = String::new();
 901        let mut escaped = false;
 902
 903        while let Some(c) = chars.next() {
 904            if escaped {
 905                escaped = false;
 906                // unescape escaped parens
 907                if c != '(' && c != ')' && c != delimiter {
 908                    search.push('\\')
 909                }
 910                search.push(c)
 911            } else if c == '\\' {
 912                escaped = true;
 913            } else if c == delimiter {
 914                break;
 915            } else {
 916                // escape unescaped parens
 917                if c == '(' || c == ')' {
 918                    search.push('\\')
 919                }
 920                search.push(c)
 921            }
 922        }
 923
 924        let command: String = chars.collect();
 925
 926        let action = WrappedAction(command_interceptor(&command, cx)?.action);
 927
 928        Some(Self {
 929            range,
 930            search,
 931            invert,
 932            action,
 933        })
 934    }
 935
 936    pub fn run(&self, vim: &mut Vim, cx: &mut ViewContext<Vim>) {
 937        let result = vim.update_editor(cx, |vim, editor, cx| {
 938            self.range.buffer_range(vim, editor, cx)
 939        });
 940
 941        let range = match result {
 942            None => return,
 943            Some(e @ Err(_)) => {
 944                let Some(workspace) = vim.workspace(cx) else {
 945                    return;
 946                };
 947                workspace.update(cx, |workspace, cx| {
 948                    e.notify_err(workspace, cx);
 949                });
 950                return;
 951            }
 952            Some(Ok(result)) => result,
 953        };
 954
 955        let mut action = self.action.boxed_clone();
 956        let mut last_pattern = self.search.clone();
 957
 958        let mut regexes = match Regex::new(&self.search) {
 959            Ok(regex) => vec![(regex, !self.invert)],
 960            e @ Err(_) => {
 961                let Some(workspace) = vim.workspace(cx) else {
 962                    return;
 963                };
 964                workspace.update(cx, |workspace, cx| {
 965                    e.notify_err(workspace, cx);
 966                });
 967                return;
 968            }
 969        };
 970        while let Some(inner) = action
 971            .boxed_clone()
 972            .as_any()
 973            .downcast_ref::<OnMatchingLines>()
 974        {
 975            let Some(regex) = Regex::new(&inner.search).ok() else {
 976                break;
 977            };
 978            last_pattern = inner.search.clone();
 979            action = inner.action.boxed_clone();
 980            regexes.push((regex, !inner.invert))
 981        }
 982
 983        if let Some(pane) = vim.pane(cx) {
 984            pane.update(cx, |pane, cx| {
 985                if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()
 986                {
 987                    search_bar.update(cx, |search_bar, cx| {
 988                        if search_bar.show(cx) {
 989                            let _ = search_bar.search(
 990                                &last_pattern,
 991                                Some(SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE),
 992                                cx,
 993                            );
 994                        }
 995                    });
 996                }
 997            });
 998        };
 999
1000        vim.update_editor(cx, |_, editor, cx| {
1001            let snapshot = editor.snapshot(cx);
1002            let mut row = range.start.0;
1003
1004            let point_range = Point::new(range.start.0, 0)
1005                ..snapshot
1006                    .buffer_snapshot
1007                    .clip_point(Point::new(range.end.0 + 1, 0), Bias::Left);
1008            cx.spawn(|editor, mut cx| async move {
1009                let new_selections = cx
1010                    .background_executor()
1011                    .spawn(async move {
1012                        let mut line = String::new();
1013                        let mut new_selections = Vec::new();
1014                        let chunks = snapshot
1015                            .buffer_snapshot
1016                            .text_for_range(point_range)
1017                            .chain(["\n"]);
1018
1019                        for chunk in chunks {
1020                            for (newline_ix, text) in chunk.split('\n').enumerate() {
1021                                if newline_ix > 0 {
1022                                    if regexes.iter().all(|(regex, should_match)| {
1023                                        regex.is_match(&line) == *should_match
1024                                    }) {
1025                                        new_selections
1026                                            .push(Point::new(row, 0).to_display_point(&snapshot))
1027                                    }
1028                                    row += 1;
1029                                    line.clear();
1030                                }
1031                                line.push_str(text)
1032                            }
1033                        }
1034
1035                        new_selections
1036                    })
1037                    .await;
1038
1039                if new_selections.is_empty() {
1040                    return;
1041                }
1042                editor
1043                    .update(&mut cx, |editor, cx| {
1044                        editor.start_transaction_at(Instant::now(), cx);
1045                        editor.change_selections(None, cx, |s| {
1046                            s.replace_cursors_with(|_| new_selections);
1047                        });
1048                        cx.dispatch_action(action);
1049                        cx.defer(move |editor, cx| {
1050                            let newest = editor.selections.newest::<Point>(cx).clone();
1051                            editor.change_selections(None, cx, |s| {
1052                                s.select(vec![newest]);
1053                            });
1054                            editor.end_transaction_at(Instant::now(), cx);
1055                        })
1056                    })
1057                    .ok();
1058            })
1059            .detach();
1060        });
1061    }
1062}
1063
1064#[cfg(test)]
1065mod test {
1066    use std::path::Path;
1067
1068    use crate::{
1069        state::Mode,
1070        test::{NeovimBackedTestContext, VimTestContext},
1071    };
1072    use editor::Editor;
1073    use gpui::TestAppContext;
1074    use indoc::indoc;
1075    use ui::ViewContext;
1076    use workspace::Workspace;
1077
1078    #[gpui::test]
1079    async fn test_command_basics(cx: &mut TestAppContext) {
1080        let mut cx = NeovimBackedTestContext::new(cx).await;
1081
1082        cx.set_shared_state(indoc! {"
1083            ˇa
1084            b
1085            c"})
1086            .await;
1087
1088        cx.simulate_shared_keystrokes(": j enter").await;
1089
1090        // hack: our cursor positioning after a join command is wrong
1091        cx.simulate_shared_keystrokes("^").await;
1092        cx.shared_state().await.assert_eq(indoc! {
1093            "ˇa b
1094            c"
1095        });
1096    }
1097
1098    #[gpui::test]
1099    async fn test_command_goto(cx: &mut TestAppContext) {
1100        let mut cx = NeovimBackedTestContext::new(cx).await;
1101
1102        cx.set_shared_state(indoc! {"
1103            ˇa
1104            b
1105            c"})
1106            .await;
1107        cx.simulate_shared_keystrokes(": 3 enter").await;
1108        cx.shared_state().await.assert_eq(indoc! {"
1109            a
1110            b
1111            ˇc"});
1112    }
1113
1114    #[gpui::test]
1115    async fn test_command_replace(cx: &mut TestAppContext) {
1116        let mut cx = NeovimBackedTestContext::new(cx).await;
1117
1118        cx.set_shared_state(indoc! {"
1119            ˇa
1120            b
1121            b
1122            c"})
1123            .await;
1124        cx.simulate_shared_keystrokes(": % s / b / d enter").await;
1125        cx.shared_state().await.assert_eq(indoc! {"
1126            a
1127            d
1128            ˇd
1129            c"});
1130        cx.simulate_shared_keystrokes(": % s : . : \\ 0 \\ 0 enter")
1131            .await;
1132        cx.shared_state().await.assert_eq(indoc! {"
1133            aa
1134            dd
1135            dd
1136            ˇcc"});
1137        cx.simulate_shared_keystrokes("k : s / dd / ee enter").await;
1138        cx.shared_state().await.assert_eq(indoc! {"
1139            aa
1140            dd
1141            ˇee
1142            cc"});
1143    }
1144
1145    #[gpui::test]
1146    async fn test_command_search(cx: &mut TestAppContext) {
1147        let mut cx = NeovimBackedTestContext::new(cx).await;
1148
1149        cx.set_shared_state(indoc! {"
1150                ˇa
1151                b
1152                a
1153                c"})
1154            .await;
1155        cx.simulate_shared_keystrokes(": / b enter").await;
1156        cx.shared_state().await.assert_eq(indoc! {"
1157                a
1158                ˇb
1159                a
1160                c"});
1161        cx.simulate_shared_keystrokes(": ? a enter").await;
1162        cx.shared_state().await.assert_eq(indoc! {"
1163                ˇa
1164                b
1165                a
1166                c"});
1167    }
1168
1169    #[gpui::test]
1170    async fn test_command_write(cx: &mut TestAppContext) {
1171        let mut cx = VimTestContext::new(cx, true).await;
1172        let path = Path::new("/root/dir/file.rs");
1173        let fs = cx.workspace(|workspace, cx| workspace.project().read(cx).fs().clone());
1174
1175        cx.simulate_keystrokes("i @ escape");
1176        cx.simulate_keystrokes(": w enter");
1177
1178        assert_eq!(fs.load(path).await.unwrap(), "@\n");
1179
1180        fs.as_fake().insert_file(path, b"oops\n".to_vec()).await;
1181
1182        // conflict!
1183        cx.simulate_keystrokes("i @ escape");
1184        cx.simulate_keystrokes(": w enter");
1185        assert!(cx.has_pending_prompt());
1186        // "Cancel"
1187        cx.simulate_prompt_answer(0);
1188        assert_eq!(fs.load(path).await.unwrap(), "oops\n");
1189        assert!(!cx.has_pending_prompt());
1190        // force overwrite
1191        cx.simulate_keystrokes(": w ! enter");
1192        assert!(!cx.has_pending_prompt());
1193        assert_eq!(fs.load(path).await.unwrap(), "@@\n");
1194    }
1195
1196    #[gpui::test]
1197    async fn test_command_quit(cx: &mut TestAppContext) {
1198        let mut cx = VimTestContext::new(cx, true).await;
1199
1200        cx.simulate_keystrokes(": n e w enter");
1201        cx.workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 2));
1202        cx.simulate_keystrokes(": q enter");
1203        cx.workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 1));
1204        cx.simulate_keystrokes(": n e w enter");
1205        cx.workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 2));
1206        cx.simulate_keystrokes(": q a enter");
1207        cx.workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 0));
1208    }
1209
1210    #[gpui::test]
1211    async fn test_offsets(cx: &mut TestAppContext) {
1212        let mut cx = NeovimBackedTestContext::new(cx).await;
1213
1214        cx.set_shared_state("ˇ1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n")
1215            .await;
1216
1217        cx.simulate_shared_keystrokes(": + enter").await;
1218        cx.shared_state()
1219            .await
1220            .assert_eq("1\nˇ2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n");
1221
1222        cx.simulate_shared_keystrokes(": 1 0 - enter").await;
1223        cx.shared_state()
1224            .await
1225            .assert_eq("1\n2\n3\n4\n5\n6\n7\n8\nˇ9\n10\n11\n");
1226
1227        cx.simulate_shared_keystrokes(": . - 2 enter").await;
1228        cx.shared_state()
1229            .await
1230            .assert_eq("1\n2\n3\n4\n5\n6\nˇ7\n8\n9\n10\n11\n");
1231
1232        cx.simulate_shared_keystrokes(": % enter").await;
1233        cx.shared_state()
1234            .await
1235            .assert_eq("1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\nˇ");
1236    }
1237
1238    #[gpui::test]
1239    async fn test_command_ranges(cx: &mut TestAppContext) {
1240        let mut cx = NeovimBackedTestContext::new(cx).await;
1241
1242        cx.set_shared_state("ˇ1\n2\n3\n4\n4\n3\n2\n1").await;
1243
1244        cx.simulate_shared_keystrokes(": 2 , 4 d enter").await;
1245        cx.shared_state().await.assert_eq("1\nˇ4\n3\n2\n1");
1246
1247        cx.simulate_shared_keystrokes(": 2 , 4 s o r t enter").await;
1248        cx.shared_state().await.assert_eq("1\nˇ2\n3\n4\n1");
1249
1250        cx.simulate_shared_keystrokes(": 2 , 4 j o i n enter").await;
1251        cx.shared_state().await.assert_eq("1\nˇ2 3 4\n1");
1252    }
1253
1254    #[gpui::test]
1255    async fn test_command_visual_replace(cx: &mut TestAppContext) {
1256        let mut cx = NeovimBackedTestContext::new(cx).await;
1257
1258        cx.set_shared_state("ˇ1\n2\n3\n4\n4\n3\n2\n1").await;
1259
1260        cx.simulate_shared_keystrokes("v 2 j : s / . / k enter")
1261            .await;
1262        cx.shared_state().await.assert_eq("k\nk\nˇk\n4\n4\n3\n2\n1");
1263    }
1264
1265    fn assert_active_item(
1266        workspace: &mut Workspace,
1267        expected_path: &str,
1268        expected_text: &str,
1269        cx: &mut ViewContext<Workspace>,
1270    ) {
1271        let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();
1272
1273        let buffer = active_editor
1274            .read(cx)
1275            .buffer()
1276            .read(cx)
1277            .as_singleton()
1278            .unwrap();
1279
1280        let text = buffer.read(cx).text();
1281        let file = buffer.read(cx).file().unwrap();
1282        let file_path = file.as_local().unwrap().abs_path(cx);
1283
1284        assert_eq!(text, expected_text);
1285        assert_eq!(file_path.to_str().unwrap(), expected_path);
1286    }
1287
1288    #[gpui::test]
1289    async fn test_command_gf(cx: &mut TestAppContext) {
1290        let mut cx = VimTestContext::new(cx, true).await;
1291
1292        // Assert base state, that we're in /root/dir/file.rs
1293        cx.workspace(|workspace, cx| {
1294            assert_active_item(workspace, "/root/dir/file.rs", "", cx);
1295        });
1296
1297        // Insert a new file
1298        let fs = cx.workspace(|workspace, cx| workspace.project().read(cx).fs().clone());
1299        fs.as_fake()
1300            .insert_file("/root/dir/file2.rs", "This is file2.rs".as_bytes().to_vec())
1301            .await;
1302        fs.as_fake()
1303            .insert_file("/root/dir/file3.rs", "go to file3".as_bytes().to_vec())
1304            .await;
1305
1306        // Put the path to the second file into the currently open buffer
1307        cx.set_state(indoc! {"go to fiˇle2.rs"}, Mode::Normal);
1308
1309        // Go to file2.rs
1310        cx.simulate_keystrokes("g f");
1311
1312        // We now have two items
1313        cx.workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 2));
1314        cx.workspace(|workspace, cx| {
1315            assert_active_item(workspace, "/root/dir/file2.rs", "This is file2.rs", cx);
1316        });
1317
1318        // Update editor to point to `file2.rs`
1319        cx.editor = cx.workspace(|workspace, cx| workspace.active_item_as::<Editor>(cx).unwrap());
1320
1321        // Put the path to the third file into the currently open buffer,
1322        // but remove its suffix, because we want that lookup to happen automatically.
1323        cx.set_state(indoc! {"go to fiˇle3"}, Mode::Normal);
1324
1325        // Go to file3.rs
1326        cx.simulate_keystrokes("g f");
1327
1328        // We now have three items
1329        cx.workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 3));
1330        cx.workspace(|workspace, cx| {
1331            assert_active_item(workspace, "/root/dir/file3.rs", "go to file3", cx);
1332        });
1333    }
1334
1335    #[gpui::test]
1336    async fn test_command_matching_lines(cx: &mut TestAppContext) {
1337        let mut cx = NeovimBackedTestContext::new(cx).await;
1338
1339        cx.set_shared_state(indoc! {"
1340            ˇa
1341            b
1342            a
1343            b
1344            a
1345        "})
1346            .await;
1347
1348        cx.simulate_shared_keystrokes(":").await;
1349        cx.simulate_shared_keystrokes("g / a / d").await;
1350        cx.simulate_shared_keystrokes("enter").await;
1351
1352        cx.shared_state().await.assert_eq(indoc! {"
1353            b
1354            b
1355            ˇ"});
1356
1357        cx.simulate_shared_keystrokes("u").await;
1358
1359        cx.shared_state().await.assert_eq(indoc! {"
1360            ˇa
1361            b
1362            a
1363            b
1364            a
1365        "});
1366
1367        cx.simulate_shared_keystrokes(":").await;
1368        cx.simulate_shared_keystrokes("v / a / d").await;
1369        cx.simulate_shared_keystrokes("enter").await;
1370
1371        cx.shared_state().await.assert_eq(indoc! {"
1372            a
1373            a
1374            ˇa"});
1375    }
1376}