command.rs

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