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                close_pinned: false,
 632            },
 633        )
 634        .bang(workspace::CloseInactiveItems {
 635            save_intent: Some(SaveIntent::Skip),
 636            close_pinned: false,
 637        }),
 638        VimCommand::new(
 639            ("on", "ly"),
 640            workspace::CloseInactiveTabsAndPanes {
 641                save_intent: Some(SaveIntent::Close),
 642            },
 643        )
 644        .bang(workspace::CloseInactiveTabsAndPanes {
 645            save_intent: Some(SaveIntent::Skip),
 646        }),
 647        VimCommand::str(("cl", "ist"), "diagnostics::Deploy"),
 648        VimCommand::new(("cc", ""), editor::actions::Hover),
 649        VimCommand::new(("ll", ""), editor::actions::Hover),
 650        VimCommand::new(("cn", "ext"), editor::actions::GoToDiagnostic).range(wrap_count),
 651        VimCommand::new(("cp", "revious"), editor::actions::GoToPrevDiagnostic).range(wrap_count),
 652        VimCommand::new(("cN", "ext"), editor::actions::GoToPrevDiagnostic).range(wrap_count),
 653        VimCommand::new(("lp", "revious"), editor::actions::GoToPrevDiagnostic).range(wrap_count),
 654        VimCommand::new(("lN", "ext"), editor::actions::GoToPrevDiagnostic).range(wrap_count),
 655        VimCommand::new(("j", "oin"), JoinLines).range(select_range),
 656        VimCommand::new(("fo", "ld"), editor::actions::FoldSelectedRanges).range(act_on_range),
 657        VimCommand::new(("foldo", "pen"), editor::actions::UnfoldLines)
 658            .bang(editor::actions::UnfoldRecursive)
 659            .range(act_on_range),
 660        VimCommand::new(("foldc", "lose"), editor::actions::Fold)
 661            .bang(editor::actions::FoldRecursive)
 662            .range(act_on_range),
 663        VimCommand::new(("dif", "fupdate"), editor::actions::ToggleHunkDiff).range(act_on_range),
 664        VimCommand::new(("rev", "ert"), editor::actions::RevertSelectedHunks).range(act_on_range),
 665        VimCommand::new(("d", "elete"), VisualDeleteLine).range(select_range),
 666        VimCommand::new(("y", "ank"), gpui::NoAction).range(|_, range| {
 667            Some(
 668                YankCommand {
 669                    range: range.clone(),
 670                }
 671                .boxed_clone(),
 672            )
 673        }),
 674        VimCommand::new(("sor", "t"), SortLinesCaseSensitive).range(select_range),
 675        VimCommand::new(("sort i", ""), SortLinesCaseInsensitive).range(select_range),
 676        VimCommand::str(("E", "xplore"), "project_panel::ToggleFocus"),
 677        VimCommand::str(("H", "explore"), "project_panel::ToggleFocus"),
 678        VimCommand::str(("L", "explore"), "project_panel::ToggleFocus"),
 679        VimCommand::str(("S", "explore"), "project_panel::ToggleFocus"),
 680        VimCommand::str(("Ve", "xplore"), "project_panel::ToggleFocus"),
 681        VimCommand::str(("te", "rm"), "terminal_panel::ToggleFocus"),
 682        VimCommand::str(("T", "erm"), "terminal_panel::ToggleFocus"),
 683        VimCommand::str(("C", "ollab"), "collab_panel::ToggleFocus"),
 684        VimCommand::str(("Ch", "at"), "chat_panel::ToggleFocus"),
 685        VimCommand::str(("No", "tifications"), "notification_panel::ToggleFocus"),
 686        VimCommand::str(("A", "I"), "assistant::ToggleFocus"),
 687        VimCommand::new(("noh", "lsearch"), search::buffer_search::Dismiss),
 688        VimCommand::new(("$", ""), EndOfDocument),
 689        VimCommand::new(("%", ""), EndOfDocument),
 690        VimCommand::new(("0", ""), StartOfDocument),
 691        VimCommand::new(("e", "dit"), editor::actions::ReloadFile)
 692            .bang(editor::actions::ReloadFile),
 693        VimCommand::new(("cpp", "link"), editor::actions::CopyPermalinkToLine).range(act_on_range),
 694    ]
 695}
 696
 697struct VimCommands(Vec<VimCommand>);
 698// safety: we only ever access this from the main thread (as ensured by the cx argument)
 699// actions are not Sync so we can't otherwise use a OnceLock.
 700unsafe impl Sync for VimCommands {}
 701impl Global for VimCommands {}
 702
 703fn commands(cx: &AppContext) -> &Vec<VimCommand> {
 704    static COMMANDS: OnceLock<VimCommands> = OnceLock::new();
 705    &COMMANDS
 706        .get_or_init(|| VimCommands(generate_commands(cx)))
 707        .0
 708}
 709
 710fn act_on_range(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
 711    Some(
 712        WithRange {
 713            restore_selection: true,
 714            range: range.clone(),
 715            action: WrappedAction(action),
 716        }
 717        .boxed_clone(),
 718    )
 719}
 720
 721fn select_range(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
 722    Some(
 723        WithRange {
 724            restore_selection: false,
 725            range: range.clone(),
 726            action: WrappedAction(action),
 727        }
 728        .boxed_clone(),
 729    )
 730}
 731
 732fn wrap_count(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
 733    range.as_count().map(|count| {
 734        WithCount {
 735            count,
 736            action: WrappedAction(action),
 737        }
 738        .boxed_clone()
 739    })
 740}
 741
 742pub fn command_interceptor(mut input: &str, cx: &AppContext) -> Option<CommandInterceptResult> {
 743    // NOTE: We also need to support passing arguments to commands like :w
 744    // (ideally with filename autocompletion).
 745    while input.starts_with(':') {
 746        input = &input[1..];
 747    }
 748
 749    let (range, query) = VimCommand::parse_range(input);
 750    let range_prefix = input[0..(input.len() - query.len())].to_string();
 751    let query = query.as_str().trim();
 752
 753    let action = if range.is_some() && query.is_empty() {
 754        Some(
 755            GoToLine {
 756                range: range.clone().unwrap(),
 757            }
 758            .boxed_clone(),
 759        )
 760    } else if query.starts_with('/') || query.starts_with('?') {
 761        Some(
 762            FindCommand {
 763                query: query[1..].to_string(),
 764                backwards: query.starts_with('?'),
 765            }
 766            .boxed_clone(),
 767        )
 768    } else if query.starts_with('s') {
 769        let mut substitute = "substitute".chars().peekable();
 770        let mut query = query.chars().peekable();
 771        while substitute
 772            .peek()
 773            .is_some_and(|char| Some(char) == query.peek())
 774        {
 775            substitute.next();
 776            query.next();
 777        }
 778        if let Some(replacement) = Replacement::parse(query) {
 779            let range = range.clone().unwrap_or(CommandRange {
 780                start: Position::CurrentLine { offset: 0 },
 781                end: None,
 782            });
 783            Some(ReplaceCommand { replacement, range }.boxed_clone())
 784        } else {
 785            None
 786        }
 787    } else {
 788        None
 789    };
 790    if let Some(action) = action {
 791        let string = input.to_string();
 792        let positions = generate_positions(&string, &(range_prefix + query));
 793        return Some(CommandInterceptResult {
 794            action,
 795            string,
 796            positions,
 797        });
 798    }
 799
 800    for command in commands(cx).iter() {
 801        if let Some(action) = command.parse(query, &range, cx) {
 802            let mut string = ":".to_owned() + &range_prefix + command.prefix + command.suffix;
 803            if query.ends_with('!') {
 804                string.push('!');
 805            }
 806            let positions = generate_positions(&string, &(range_prefix + query));
 807
 808            return Some(CommandInterceptResult {
 809                action,
 810                string,
 811                positions,
 812            });
 813        }
 814    }
 815    None
 816}
 817
 818fn generate_positions(string: &str, query: &str) -> Vec<usize> {
 819    let mut positions = Vec::new();
 820    let mut chars = query.chars();
 821
 822    let Some(mut current) = chars.next() else {
 823        return positions;
 824    };
 825
 826    for (i, c) in string.char_indices() {
 827        if c == current {
 828            positions.push(i);
 829            if let Some(c) = chars.next() {
 830                current = c;
 831            } else {
 832                break;
 833            }
 834        }
 835    }
 836
 837    positions
 838}
 839
 840#[cfg(test)]
 841mod test {
 842    use std::path::Path;
 843
 844    use crate::{
 845        state::Mode,
 846        test::{NeovimBackedTestContext, VimTestContext},
 847    };
 848    use editor::Editor;
 849    use gpui::TestAppContext;
 850    use indoc::indoc;
 851    use ui::ViewContext;
 852    use workspace::Workspace;
 853
 854    #[gpui::test]
 855    async fn test_command_basics(cx: &mut TestAppContext) {
 856        let mut cx = NeovimBackedTestContext::new(cx).await;
 857
 858        cx.set_shared_state(indoc! {"
 859            ˇa
 860            b
 861            c"})
 862            .await;
 863
 864        cx.simulate_shared_keystrokes(": j enter").await;
 865
 866        // hack: our cursor positioning after a join command is wrong
 867        cx.simulate_shared_keystrokes("^").await;
 868        cx.shared_state().await.assert_eq(indoc! {
 869            "ˇa b
 870            c"
 871        });
 872    }
 873
 874    #[gpui::test]
 875    async fn test_command_goto(cx: &mut TestAppContext) {
 876        let mut cx = NeovimBackedTestContext::new(cx).await;
 877
 878        cx.set_shared_state(indoc! {"
 879            ˇa
 880            b
 881            c"})
 882            .await;
 883        cx.simulate_shared_keystrokes(": 3 enter").await;
 884        cx.shared_state().await.assert_eq(indoc! {"
 885            a
 886            b
 887            ˇc"});
 888    }
 889
 890    #[gpui::test]
 891    async fn test_command_replace(cx: &mut TestAppContext) {
 892        let mut cx = NeovimBackedTestContext::new(cx).await;
 893
 894        cx.set_shared_state(indoc! {"
 895            ˇa
 896            b
 897            b
 898            c"})
 899            .await;
 900        cx.simulate_shared_keystrokes(": % s / b / d enter").await;
 901        cx.shared_state().await.assert_eq(indoc! {"
 902            a
 903            d
 904            ˇd
 905            c"});
 906        cx.simulate_shared_keystrokes(": % s : . : \\ 0 \\ 0 enter")
 907            .await;
 908        cx.shared_state().await.assert_eq(indoc! {"
 909            aa
 910            dd
 911            dd
 912            ˇcc"});
 913        cx.simulate_shared_keystrokes("k : s / dd / ee enter").await;
 914        cx.shared_state().await.assert_eq(indoc! {"
 915            aa
 916            dd
 917            ˇee
 918            cc"});
 919    }
 920
 921    #[gpui::test]
 922    async fn test_command_search(cx: &mut TestAppContext) {
 923        let mut cx = NeovimBackedTestContext::new(cx).await;
 924
 925        cx.set_shared_state(indoc! {"
 926                ˇa
 927                b
 928                a
 929                c"})
 930            .await;
 931        cx.simulate_shared_keystrokes(": / b enter").await;
 932        cx.shared_state().await.assert_eq(indoc! {"
 933                a
 934                ˇb
 935                a
 936                c"});
 937        cx.simulate_shared_keystrokes(": ? a enter").await;
 938        cx.shared_state().await.assert_eq(indoc! {"
 939                ˇa
 940                b
 941                a
 942                c"});
 943    }
 944
 945    #[gpui::test]
 946    async fn test_command_write(cx: &mut TestAppContext) {
 947        let mut cx = VimTestContext::new(cx, true).await;
 948        let path = Path::new("/root/dir/file.rs");
 949        let fs = cx.workspace(|workspace, cx| workspace.project().read(cx).fs().clone());
 950
 951        cx.simulate_keystrokes("i @ escape");
 952        cx.simulate_keystrokes(": w enter");
 953
 954        assert_eq!(fs.load(path).await.unwrap(), "@\n");
 955
 956        fs.as_fake().insert_file(path, b"oops\n".to_vec()).await;
 957
 958        // conflict!
 959        cx.simulate_keystrokes("i @ escape");
 960        cx.simulate_keystrokes(": w enter");
 961        assert!(cx.has_pending_prompt());
 962        // "Cancel"
 963        cx.simulate_prompt_answer(0);
 964        assert_eq!(fs.load(path).await.unwrap(), "oops\n");
 965        assert!(!cx.has_pending_prompt());
 966        // force overwrite
 967        cx.simulate_keystrokes(": w ! enter");
 968        assert!(!cx.has_pending_prompt());
 969        assert_eq!(fs.load(path).await.unwrap(), "@@\n");
 970    }
 971
 972    #[gpui::test]
 973    async fn test_command_quit(cx: &mut TestAppContext) {
 974        let mut cx = VimTestContext::new(cx, true).await;
 975
 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 enter");
 979        cx.workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 1));
 980        cx.simulate_keystrokes(": n e w enter");
 981        cx.workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 2));
 982        cx.simulate_keystrokes(": q a enter");
 983        cx.workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 0));
 984    }
 985
 986    #[gpui::test]
 987    async fn test_offsets(cx: &mut TestAppContext) {
 988        let mut cx = NeovimBackedTestContext::new(cx).await;
 989
 990        cx.set_shared_state("ˇ1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n")
 991            .await;
 992
 993        cx.simulate_shared_keystrokes(": + enter").await;
 994        cx.shared_state()
 995            .await
 996            .assert_eq("1\nˇ2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n");
 997
 998        cx.simulate_shared_keystrokes(": 1 0 - enter").await;
 999        cx.shared_state()
1000            .await
1001            .assert_eq("1\n2\n3\n4\n5\n6\n7\n8\nˇ9\n10\n11\n");
1002
1003        cx.simulate_shared_keystrokes(": . - 2 enter").await;
1004        cx.shared_state()
1005            .await
1006            .assert_eq("1\n2\n3\n4\n5\n6\nˇ7\n8\n9\n10\n11\n");
1007
1008        cx.simulate_shared_keystrokes(": % enter").await;
1009        cx.shared_state()
1010            .await
1011            .assert_eq("1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\nˇ");
1012    }
1013
1014    #[gpui::test]
1015    async fn test_command_ranges(cx: &mut TestAppContext) {
1016        let mut cx = NeovimBackedTestContext::new(cx).await;
1017
1018        cx.set_shared_state("ˇ1\n2\n3\n4\n4\n3\n2\n1").await;
1019
1020        cx.simulate_shared_keystrokes(": 2 , 4 d enter").await;
1021        cx.shared_state().await.assert_eq("1\nˇ4\n3\n2\n1");
1022
1023        cx.simulate_shared_keystrokes(": 2 , 4 s o r t enter").await;
1024        cx.shared_state().await.assert_eq("1\nˇ2\n3\n4\n1");
1025
1026        cx.simulate_shared_keystrokes(": 2 , 4 j o i n enter").await;
1027        cx.shared_state().await.assert_eq("1\nˇ2 3 4\n1");
1028    }
1029
1030    #[gpui::test]
1031    async fn test_command_visual_replace(cx: &mut TestAppContext) {
1032        let mut cx = NeovimBackedTestContext::new(cx).await;
1033
1034        cx.set_shared_state("ˇ1\n2\n3\n4\n4\n3\n2\n1").await;
1035
1036        cx.simulate_shared_keystrokes("v 2 j : s / . / k enter")
1037            .await;
1038        cx.shared_state().await.assert_eq("k\nk\nˇk\n4\n4\n3\n2\n1");
1039    }
1040
1041    fn assert_active_item(
1042        workspace: &mut Workspace,
1043        expected_path: &str,
1044        expected_text: &str,
1045        cx: &mut ViewContext<Workspace>,
1046    ) {
1047        let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();
1048
1049        let buffer = active_editor
1050            .read(cx)
1051            .buffer()
1052            .read(cx)
1053            .as_singleton()
1054            .unwrap();
1055
1056        let text = buffer.read(cx).text();
1057        let file = buffer.read(cx).file().unwrap();
1058        let file_path = file.as_local().unwrap().abs_path(cx);
1059
1060        assert_eq!(text, expected_text);
1061        assert_eq!(file_path.to_str().unwrap(), expected_path);
1062    }
1063
1064    #[gpui::test]
1065    async fn test_command_gf(cx: &mut TestAppContext) {
1066        let mut cx = VimTestContext::new(cx, true).await;
1067
1068        // Assert base state, that we're in /root/dir/file.rs
1069        cx.workspace(|workspace, cx| {
1070            assert_active_item(workspace, "/root/dir/file.rs", "", cx);
1071        });
1072
1073        // Insert a new file
1074        let fs = cx.workspace(|workspace, cx| workspace.project().read(cx).fs().clone());
1075        fs.as_fake()
1076            .insert_file("/root/dir/file2.rs", "This is file2.rs".as_bytes().to_vec())
1077            .await;
1078        fs.as_fake()
1079            .insert_file("/root/dir/file3.rs", "go to file3".as_bytes().to_vec())
1080            .await;
1081
1082        // Put the path to the second file into the currently open buffer
1083        cx.set_state(indoc! {"go to fiˇle2.rs"}, Mode::Normal);
1084
1085        // Go to file2.rs
1086        cx.simulate_keystrokes("g f");
1087
1088        // We now have two items
1089        cx.workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 2));
1090        cx.workspace(|workspace, cx| {
1091            assert_active_item(workspace, "/root/dir/file2.rs", "This is file2.rs", cx);
1092        });
1093
1094        // Update editor to point to `file2.rs`
1095        cx.editor = cx.workspace(|workspace, cx| workspace.active_item_as::<Editor>(cx).unwrap());
1096
1097        // Put the path to the third file into the currently open buffer,
1098        // but remove its suffix, because we want that lookup to happen automatically.
1099        cx.set_state(indoc! {"go to fiˇle3"}, Mode::Normal);
1100
1101        // Go to file3.rs
1102        cx.simulate_keystrokes("g f");
1103
1104        // We now have three items
1105        cx.workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 3));
1106        cx.workspace(|workspace, cx| {
1107            assert_active_item(workspace, "/root/dir/file3.rs", "go to file3", cx);
1108        });
1109    }
1110}