command.rs

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