command.rs

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