command.rs

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