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