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