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