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.buffer_snapshot.max_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 } => snapshot
 440                .buffer_snapshot
 441                .max_row()
 442                .0
 443                .saturating_add_signed(*offset),
 444            Position::CurrentLine { offset } => editor
 445                .selections
 446                .newest_anchor()
 447                .head()
 448                .to_point(&snapshot.buffer_snapshot)
 449                .row
 450                .saturating_add_signed(*offset),
 451        };
 452
 453        Ok(MultiBufferRow(target).min(snapshot.buffer_snapshot.max_row()))
 454    }
 455}
 456
 457#[derive(Debug, Clone, PartialEq, Deserialize)]
 458pub(crate) struct CommandRange {
 459    start: Position,
 460    end: Option<Position>,
 461}
 462
 463impl CommandRange {
 464    fn head(&self) -> &Position {
 465        self.end.as_ref().unwrap_or(&self.start)
 466    }
 467
 468    pub(crate) fn buffer_range(
 469        &self,
 470        vim: &Vim,
 471        editor: &mut Editor,
 472        cx: &mut WindowContext,
 473    ) -> Result<Range<MultiBufferRow>> {
 474        let start = self.start.buffer_row(vim, editor, cx)?;
 475        let end = if let Some(end) = self.end.as_ref() {
 476            end.buffer_row(vim, editor, cx)?
 477        } else {
 478            start
 479        };
 480        if end < start {
 481            anyhow::Ok(end..start)
 482        } else {
 483            anyhow::Ok(start..end)
 484        }
 485    }
 486
 487    pub fn as_count(&self) -> Option<u32> {
 488        if let CommandRange {
 489            start: Position::Line { row, offset: 0 },
 490            end: None,
 491        } = &self
 492        {
 493            Some(*row)
 494        } else {
 495            None
 496        }
 497    }
 498}
 499
 500fn generate_commands(_: &AppContext) -> Vec<VimCommand> {
 501    vec![
 502        VimCommand::new(
 503            ("w", "rite"),
 504            workspace::Save {
 505                save_intent: Some(SaveIntent::Save),
 506            },
 507        )
 508        .bang(workspace::Save {
 509            save_intent: Some(SaveIntent::Overwrite),
 510        }),
 511        VimCommand::new(
 512            ("q", "uit"),
 513            workspace::CloseActiveItem {
 514                save_intent: Some(SaveIntent::Close),
 515            },
 516        )
 517        .bang(workspace::CloseActiveItem {
 518            save_intent: Some(SaveIntent::Skip),
 519        }),
 520        VimCommand::new(
 521            ("wq", ""),
 522            workspace::CloseActiveItem {
 523                save_intent: Some(SaveIntent::Save),
 524            },
 525        )
 526        .bang(workspace::CloseActiveItem {
 527            save_intent: Some(SaveIntent::Overwrite),
 528        }),
 529        VimCommand::new(
 530            ("x", "it"),
 531            workspace::CloseActiveItem {
 532                save_intent: Some(SaveIntent::SaveAll),
 533            },
 534        )
 535        .bang(workspace::CloseActiveItem {
 536            save_intent: Some(SaveIntent::Overwrite),
 537        }),
 538        VimCommand::new(
 539            ("ex", "it"),
 540            workspace::CloseActiveItem {
 541                save_intent: Some(SaveIntent::SaveAll),
 542            },
 543        )
 544        .bang(workspace::CloseActiveItem {
 545            save_intent: Some(SaveIntent::Overwrite),
 546        }),
 547        VimCommand::new(
 548            ("up", "date"),
 549            workspace::Save {
 550                save_intent: Some(SaveIntent::SaveAll),
 551            },
 552        ),
 553        VimCommand::new(
 554            ("wa", "ll"),
 555            workspace::SaveAll {
 556                save_intent: Some(SaveIntent::SaveAll),
 557            },
 558        )
 559        .bang(workspace::SaveAll {
 560            save_intent: Some(SaveIntent::Overwrite),
 561        }),
 562        VimCommand::new(
 563            ("qa", "ll"),
 564            workspace::CloseAllItemsAndPanes {
 565                save_intent: Some(SaveIntent::Close),
 566            },
 567        )
 568        .bang(workspace::CloseAllItemsAndPanes {
 569            save_intent: Some(SaveIntent::Skip),
 570        }),
 571        VimCommand::new(
 572            ("quita", "ll"),
 573            workspace::CloseAllItemsAndPanes {
 574                save_intent: Some(SaveIntent::Close),
 575            },
 576        )
 577        .bang(workspace::CloseAllItemsAndPanes {
 578            save_intent: Some(SaveIntent::Skip),
 579        }),
 580        VimCommand::new(
 581            ("xa", "ll"),
 582            workspace::CloseAllItemsAndPanes {
 583                save_intent: Some(SaveIntent::SaveAll),
 584            },
 585        )
 586        .bang(workspace::CloseAllItemsAndPanes {
 587            save_intent: Some(SaveIntent::Overwrite),
 588        }),
 589        VimCommand::new(
 590            ("wqa", "ll"),
 591            workspace::CloseAllItemsAndPanes {
 592                save_intent: Some(SaveIntent::SaveAll),
 593            },
 594        )
 595        .bang(workspace::CloseAllItemsAndPanes {
 596            save_intent: Some(SaveIntent::Overwrite),
 597        }),
 598        VimCommand::new(("cq", "uit"), zed_actions::Quit),
 599        VimCommand::new(("sp", "lit"), workspace::SplitHorizontal),
 600        VimCommand::new(("vs", "plit"), workspace::SplitVertical),
 601        VimCommand::new(
 602            ("bd", "elete"),
 603            workspace::CloseActiveItem {
 604                save_intent: Some(SaveIntent::Close),
 605            },
 606        )
 607        .bang(workspace::CloseActiveItem {
 608            save_intent: Some(SaveIntent::Skip),
 609        }),
 610        VimCommand::new(("bn", "ext"), workspace::ActivateNextItem).count(),
 611        VimCommand::new(("bN", "ext"), workspace::ActivatePrevItem).count(),
 612        VimCommand::new(("bp", "revious"), workspace::ActivatePrevItem).count(),
 613        VimCommand::new(("bf", "irst"), workspace::ActivateItem(0)),
 614        VimCommand::new(("br", "ewind"), workspace::ActivateItem(0)),
 615        VimCommand::new(("bl", "ast"), workspace::ActivateLastItem),
 616        VimCommand::new(("new", ""), workspace::NewFileSplitHorizontal),
 617        VimCommand::new(("vne", "w"), workspace::NewFileSplitVertical),
 618        VimCommand::new(("tabe", "dit"), workspace::NewFile),
 619        VimCommand::new(("tabnew", ""), workspace::NewFile),
 620        VimCommand::new(("tabn", "ext"), workspace::ActivateNextItem).count(),
 621        VimCommand::new(("tabp", "revious"), workspace::ActivatePrevItem).count(),
 622        VimCommand::new(("tabN", "ext"), workspace::ActivatePrevItem).count(),
 623        VimCommand::new(
 624            ("tabc", "lose"),
 625            workspace::CloseActiveItem {
 626                save_intent: Some(SaveIntent::Close),
 627            },
 628        ),
 629        VimCommand::new(
 630            ("tabo", "nly"),
 631            workspace::CloseInactiveItems {
 632                save_intent: Some(SaveIntent::Close),
 633                close_pinned: false,
 634            },
 635        )
 636        .bang(workspace::CloseInactiveItems {
 637            save_intent: Some(SaveIntent::Skip),
 638            close_pinned: false,
 639        }),
 640        VimCommand::new(
 641            ("on", "ly"),
 642            workspace::CloseInactiveTabsAndPanes {
 643                save_intent: Some(SaveIntent::Close),
 644            },
 645        )
 646        .bang(workspace::CloseInactiveTabsAndPanes {
 647            save_intent: Some(SaveIntent::Skip),
 648        }),
 649        VimCommand::str(("cl", "ist"), "diagnostics::Deploy"),
 650        VimCommand::new(("cc", ""), editor::actions::Hover),
 651        VimCommand::new(("ll", ""), editor::actions::Hover),
 652        VimCommand::new(("cn", "ext"), editor::actions::GoToDiagnostic).range(wrap_count),
 653        VimCommand::new(("cp", "revious"), editor::actions::GoToPrevDiagnostic).range(wrap_count),
 654        VimCommand::new(("cN", "ext"), editor::actions::GoToPrevDiagnostic).range(wrap_count),
 655        VimCommand::new(("lp", "revious"), editor::actions::GoToPrevDiagnostic).range(wrap_count),
 656        VimCommand::new(("lN", "ext"), editor::actions::GoToPrevDiagnostic).range(wrap_count),
 657        VimCommand::new(("j", "oin"), JoinLines).range(select_range),
 658        VimCommand::new(("fo", "ld"), editor::actions::FoldSelectedRanges).range(act_on_range),
 659        VimCommand::new(("foldo", "pen"), editor::actions::UnfoldLines)
 660            .bang(editor::actions::UnfoldRecursive)
 661            .range(act_on_range),
 662        VimCommand::new(("foldc", "lose"), editor::actions::Fold)
 663            .bang(editor::actions::FoldRecursive)
 664            .range(act_on_range),
 665        VimCommand::new(("dif", "fupdate"), editor::actions::ToggleHunkDiff).range(act_on_range),
 666        VimCommand::new(("rev", "ert"), editor::actions::RevertSelectedHunks).range(act_on_range),
 667        VimCommand::new(("d", "elete"), VisualDeleteLine).range(select_range),
 668        VimCommand::new(("y", "ank"), gpui::NoAction).range(|_, range| {
 669            Some(
 670                YankCommand {
 671                    range: range.clone(),
 672                }
 673                .boxed_clone(),
 674            )
 675        }),
 676        VimCommand::new(("sor", "t"), SortLinesCaseSensitive).range(select_range),
 677        VimCommand::new(("sort i", ""), SortLinesCaseInsensitive).range(select_range),
 678        VimCommand::str(("E", "xplore"), "project_panel::ToggleFocus"),
 679        VimCommand::str(("H", "explore"), "project_panel::ToggleFocus"),
 680        VimCommand::str(("L", "explore"), "project_panel::ToggleFocus"),
 681        VimCommand::str(("S", "explore"), "project_panel::ToggleFocus"),
 682        VimCommand::str(("Ve", "xplore"), "project_panel::ToggleFocus"),
 683        VimCommand::str(("te", "rm"), "terminal_panel::ToggleFocus"),
 684        VimCommand::str(("T", "erm"), "terminal_panel::ToggleFocus"),
 685        VimCommand::str(("C", "ollab"), "collab_panel::ToggleFocus"),
 686        VimCommand::str(("Ch", "at"), "chat_panel::ToggleFocus"),
 687        VimCommand::str(("No", "tifications"), "notification_panel::ToggleFocus"),
 688        VimCommand::str(("A", "I"), "assistant::ToggleFocus"),
 689        VimCommand::new(("noh", "lsearch"), search::buffer_search::Dismiss),
 690        VimCommand::new(("$", ""), EndOfDocument),
 691        VimCommand::new(("%", ""), EndOfDocument),
 692        VimCommand::new(("0", ""), StartOfDocument),
 693        VimCommand::new(("e", "dit"), editor::actions::ReloadFile)
 694            .bang(editor::actions::ReloadFile),
 695        VimCommand::new(("cpp", "link"), editor::actions::CopyPermalinkToLine).range(act_on_range),
 696    ]
 697}
 698
 699struct VimCommands(Vec<VimCommand>);
 700// safety: we only ever access this from the main thread (as ensured by the cx argument)
 701// actions are not Sync so we can't otherwise use a OnceLock.
 702unsafe impl Sync for VimCommands {}
 703impl Global for VimCommands {}
 704
 705fn commands(cx: &AppContext) -> &Vec<VimCommand> {
 706    static COMMANDS: OnceLock<VimCommands> = OnceLock::new();
 707    &COMMANDS
 708        .get_or_init(|| VimCommands(generate_commands(cx)))
 709        .0
 710}
 711
 712fn act_on_range(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
 713    Some(
 714        WithRange {
 715            restore_selection: true,
 716            range: range.clone(),
 717            action: WrappedAction(action),
 718        }
 719        .boxed_clone(),
 720    )
 721}
 722
 723fn select_range(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
 724    Some(
 725        WithRange {
 726            restore_selection: false,
 727            range: range.clone(),
 728            action: WrappedAction(action),
 729        }
 730        .boxed_clone(),
 731    )
 732}
 733
 734fn wrap_count(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
 735    range.as_count().map(|count| {
 736        WithCount {
 737            count,
 738            action: WrappedAction(action),
 739        }
 740        .boxed_clone()
 741    })
 742}
 743
 744pub fn command_interceptor(mut input: &str, cx: &AppContext) -> Option<CommandInterceptResult> {
 745    // NOTE: We also need to support passing arguments to commands like :w
 746    // (ideally with filename autocompletion).
 747    while input.starts_with(':') {
 748        input = &input[1..];
 749    }
 750
 751    let (range, query) = VimCommand::parse_range(input);
 752    let range_prefix = input[0..(input.len() - query.len())].to_string();
 753    let query = query.as_str().trim();
 754
 755    let action = if range.is_some() && query.is_empty() {
 756        Some(
 757            GoToLine {
 758                range: range.clone().unwrap(),
 759            }
 760            .boxed_clone(),
 761        )
 762    } else if query.starts_with('/') || query.starts_with('?') {
 763        Some(
 764            FindCommand {
 765                query: query[1..].to_string(),
 766                backwards: query.starts_with('?'),
 767            }
 768            .boxed_clone(),
 769        )
 770    } else if query.starts_with('s') {
 771        let mut substitute = "substitute".chars().peekable();
 772        let mut query = query.chars().peekable();
 773        while substitute
 774            .peek()
 775            .is_some_and(|char| Some(char) == query.peek())
 776        {
 777            substitute.next();
 778            query.next();
 779        }
 780        if let Some(replacement) = Replacement::parse(query) {
 781            let range = range.clone().unwrap_or(CommandRange {
 782                start: Position::CurrentLine { offset: 0 },
 783                end: None,
 784            });
 785            Some(ReplaceCommand { replacement, range }.boxed_clone())
 786        } else {
 787            None
 788        }
 789    } else {
 790        None
 791    };
 792    if let Some(action) = action {
 793        let string = input.to_string();
 794        let positions = generate_positions(&string, &(range_prefix + query));
 795        return Some(CommandInterceptResult {
 796            action,
 797            string,
 798            positions,
 799        });
 800    }
 801
 802    for command in commands(cx).iter() {
 803        if let Some(action) = command.parse(query, &range, cx) {
 804            let mut string = ":".to_owned() + &range_prefix + command.prefix + command.suffix;
 805            if query.ends_with('!') {
 806                string.push('!');
 807            }
 808            let positions = generate_positions(&string, &(range_prefix + query));
 809
 810            return Some(CommandInterceptResult {
 811                action,
 812                string,
 813                positions,
 814            });
 815        }
 816    }
 817    None
 818}
 819
 820fn generate_positions(string: &str, query: &str) -> Vec<usize> {
 821    let mut positions = Vec::new();
 822    let mut chars = query.chars();
 823
 824    let Some(mut current) = chars.next() else {
 825        return positions;
 826    };
 827
 828    for (i, c) in string.char_indices() {
 829        if c == current {
 830            positions.push(i);
 831            if let Some(c) = chars.next() {
 832                current = c;
 833            } else {
 834                break;
 835            }
 836        }
 837    }
 838
 839    positions
 840}
 841
 842#[cfg(test)]
 843mod test {
 844    use std::path::Path;
 845
 846    use crate::{
 847        state::Mode,
 848        test::{NeovimBackedTestContext, VimTestContext},
 849    };
 850    use editor::Editor;
 851    use gpui::TestAppContext;
 852    use indoc::indoc;
 853    use ui::ViewContext;
 854    use workspace::Workspace;
 855
 856    #[gpui::test]
 857    async fn test_command_basics(cx: &mut TestAppContext) {
 858        let mut cx = NeovimBackedTestContext::new(cx).await;
 859
 860        cx.set_shared_state(indoc! {"
 861            ˇa
 862            b
 863            c"})
 864            .await;
 865
 866        cx.simulate_shared_keystrokes(": j enter").await;
 867
 868        // hack: our cursor positioning after a join command is wrong
 869        cx.simulate_shared_keystrokes("^").await;
 870        cx.shared_state().await.assert_eq(indoc! {
 871            "ˇa b
 872            c"
 873        });
 874    }
 875
 876    #[gpui::test]
 877    async fn test_command_goto(cx: &mut TestAppContext) {
 878        let mut cx = NeovimBackedTestContext::new(cx).await;
 879
 880        cx.set_shared_state(indoc! {"
 881            ˇa
 882            b
 883            c"})
 884            .await;
 885        cx.simulate_shared_keystrokes(": 3 enter").await;
 886        cx.shared_state().await.assert_eq(indoc! {"
 887            a
 888            b
 889            ˇc"});
 890    }
 891
 892    #[gpui::test]
 893    async fn test_command_replace(cx: &mut TestAppContext) {
 894        let mut cx = NeovimBackedTestContext::new(cx).await;
 895
 896        cx.set_shared_state(indoc! {"
 897            ˇa
 898            b
 899            b
 900            c"})
 901            .await;
 902        cx.simulate_shared_keystrokes(": % s / b / d enter").await;
 903        cx.shared_state().await.assert_eq(indoc! {"
 904            a
 905            d
 906            ˇd
 907            c"});
 908        cx.simulate_shared_keystrokes(": % s : . : \\ 0 \\ 0 enter")
 909            .await;
 910        cx.shared_state().await.assert_eq(indoc! {"
 911            aa
 912            dd
 913            dd
 914            ˇcc"});
 915        cx.simulate_shared_keystrokes("k : s / dd / ee enter").await;
 916        cx.shared_state().await.assert_eq(indoc! {"
 917            aa
 918            dd
 919            ˇee
 920            cc"});
 921    }
 922
 923    #[gpui::test]
 924    async fn test_command_search(cx: &mut TestAppContext) {
 925        let mut cx = NeovimBackedTestContext::new(cx).await;
 926
 927        cx.set_shared_state(indoc! {"
 928                ˇa
 929                b
 930                a
 931                c"})
 932            .await;
 933        cx.simulate_shared_keystrokes(": / b enter").await;
 934        cx.shared_state().await.assert_eq(indoc! {"
 935                a
 936                ˇb
 937                a
 938                c"});
 939        cx.simulate_shared_keystrokes(": ? a enter").await;
 940        cx.shared_state().await.assert_eq(indoc! {"
 941                ˇa
 942                b
 943                a
 944                c"});
 945    }
 946
 947    #[gpui::test]
 948    async fn test_command_write(cx: &mut TestAppContext) {
 949        let mut cx = VimTestContext::new(cx, true).await;
 950        let path = Path::new("/root/dir/file.rs");
 951        let fs = cx.workspace(|workspace, cx| workspace.project().read(cx).fs().clone());
 952
 953        cx.simulate_keystrokes("i @ escape");
 954        cx.simulate_keystrokes(": w enter");
 955
 956        assert_eq!(fs.load(path).await.unwrap(), "@\n");
 957
 958        fs.as_fake().insert_file(path, b"oops\n".to_vec()).await;
 959
 960        // conflict!
 961        cx.simulate_keystrokes("i @ escape");
 962        cx.simulate_keystrokes(": w enter");
 963        assert!(cx.has_pending_prompt());
 964        // "Cancel"
 965        cx.simulate_prompt_answer(0);
 966        assert_eq!(fs.load(path).await.unwrap(), "oops\n");
 967        assert!(!cx.has_pending_prompt());
 968        // force overwrite
 969        cx.simulate_keystrokes(": w ! enter");
 970        assert!(!cx.has_pending_prompt());
 971        assert_eq!(fs.load(path).await.unwrap(), "@@\n");
 972    }
 973
 974    #[gpui::test]
 975    async fn test_command_quit(cx: &mut TestAppContext) {
 976        let mut cx = VimTestContext::new(cx, true).await;
 977
 978        cx.simulate_keystrokes(": n e w enter");
 979        cx.workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 2));
 980        cx.simulate_keystrokes(": q enter");
 981        cx.workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 1));
 982        cx.simulate_keystrokes(": n e w enter");
 983        cx.workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 2));
 984        cx.simulate_keystrokes(": q a enter");
 985        cx.workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 0));
 986    }
 987
 988    #[gpui::test]
 989    async fn test_offsets(cx: &mut TestAppContext) {
 990        let mut cx = NeovimBackedTestContext::new(cx).await;
 991
 992        cx.set_shared_state("ˇ1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n")
 993            .await;
 994
 995        cx.simulate_shared_keystrokes(": + enter").await;
 996        cx.shared_state()
 997            .await
 998            .assert_eq("1\nˇ2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n");
 999
1000        cx.simulate_shared_keystrokes(": 1 0 - enter").await;
1001        cx.shared_state()
1002            .await
1003            .assert_eq("1\n2\n3\n4\n5\n6\n7\n8\nˇ9\n10\n11\n");
1004
1005        cx.simulate_shared_keystrokes(": . - 2 enter").await;
1006        cx.shared_state()
1007            .await
1008            .assert_eq("1\n2\n3\n4\n5\n6\nˇ7\n8\n9\n10\n11\n");
1009
1010        cx.simulate_shared_keystrokes(": % enter").await;
1011        cx.shared_state()
1012            .await
1013            .assert_eq("1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\nˇ");
1014    }
1015
1016    #[gpui::test]
1017    async fn test_command_ranges(cx: &mut TestAppContext) {
1018        let mut cx = NeovimBackedTestContext::new(cx).await;
1019
1020        cx.set_shared_state("ˇ1\n2\n3\n4\n4\n3\n2\n1").await;
1021
1022        cx.simulate_shared_keystrokes(": 2 , 4 d enter").await;
1023        cx.shared_state().await.assert_eq("1\nˇ4\n3\n2\n1");
1024
1025        cx.simulate_shared_keystrokes(": 2 , 4 s o r t enter").await;
1026        cx.shared_state().await.assert_eq("1\nˇ2\n3\n4\n1");
1027
1028        cx.simulate_shared_keystrokes(": 2 , 4 j o i n enter").await;
1029        cx.shared_state().await.assert_eq("1\nˇ2 3 4\n1");
1030    }
1031
1032    #[gpui::test]
1033    async fn test_command_visual_replace(cx: &mut TestAppContext) {
1034        let mut cx = NeovimBackedTestContext::new(cx).await;
1035
1036        cx.set_shared_state("ˇ1\n2\n3\n4\n4\n3\n2\n1").await;
1037
1038        cx.simulate_shared_keystrokes("v 2 j : s / . / k enter")
1039            .await;
1040        cx.shared_state().await.assert_eq("k\nk\nˇk\n4\n4\n3\n2\n1");
1041    }
1042
1043    fn assert_active_item(
1044        workspace: &mut Workspace,
1045        expected_path: &str,
1046        expected_text: &str,
1047        cx: &mut ViewContext<Workspace>,
1048    ) {
1049        let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();
1050
1051        let buffer = active_editor
1052            .read(cx)
1053            .buffer()
1054            .read(cx)
1055            .as_singleton()
1056            .unwrap();
1057
1058        let text = buffer.read(cx).text();
1059        let file = buffer.read(cx).file().unwrap();
1060        let file_path = file.as_local().unwrap().abs_path(cx);
1061
1062        assert_eq!(text, expected_text);
1063        assert_eq!(file_path.to_str().unwrap(), expected_path);
1064    }
1065
1066    #[gpui::test]
1067    async fn test_command_gf(cx: &mut TestAppContext) {
1068        let mut cx = VimTestContext::new(cx, true).await;
1069
1070        // Assert base state, that we're in /root/dir/file.rs
1071        cx.workspace(|workspace, cx| {
1072            assert_active_item(workspace, "/root/dir/file.rs", "", cx);
1073        });
1074
1075        // Insert a new file
1076        let fs = cx.workspace(|workspace, cx| workspace.project().read(cx).fs().clone());
1077        fs.as_fake()
1078            .insert_file("/root/dir/file2.rs", "This is file2.rs".as_bytes().to_vec())
1079            .await;
1080        fs.as_fake()
1081            .insert_file("/root/dir/file3.rs", "go to file3".as_bytes().to_vec())
1082            .await;
1083
1084        // Put the path to the second file into the currently open buffer
1085        cx.set_state(indoc! {"go to fiˇle2.rs"}, Mode::Normal);
1086
1087        // Go to file2.rs
1088        cx.simulate_keystrokes("g f");
1089
1090        // We now have two items
1091        cx.workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 2));
1092        cx.workspace(|workspace, cx| {
1093            assert_active_item(workspace, "/root/dir/file2.rs", "This is file2.rs", cx);
1094        });
1095
1096        // Update editor to point to `file2.rs`
1097        cx.editor = cx.workspace(|workspace, cx| workspace.active_item_as::<Editor>(cx).unwrap());
1098
1099        // Put the path to the third file into the currently open buffer,
1100        // but remove its suffix, because we want that lookup to happen automatically.
1101        cx.set_state(indoc! {"go to fiˇle3"}, Mode::Normal);
1102
1103        // Go to file3.rs
1104        cx.simulate_keystrokes("g f");
1105
1106        // We now have three items
1107        cx.workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 3));
1108        cx.workspace(|workspace, cx| {
1109            assert_active_item(workspace, "/root/dir/file3.rs", "go to file3", cx);
1110        });
1111    }
1112}