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::{actions, impl_internal_actions, Action, App, Context, Global, Window};
  11use language::Point;
  12use multi_buffer::MultiBufferRow;
  13use regex::Regex;
  14use schemars::JsonSchema;
  15use search::{BufferSearchBar, SearchOptions};
  16use serde::Deserialize;
  17use std::{
  18    io::Write,
  19    iter::Peekable,
  20    ops::{Deref, Range},
  21    process::Stdio,
  22    str::Chars,
  23    sync::OnceLock,
  24    time::Instant,
  25};
  26use task::{HideStrategy, RevealStrategy, SpawnInTerminal, TaskId};
  27use ui::ActiveTheme;
  28use util::ResultExt;
  29use workspace::{notifications::NotifyResultExt, SaveIntent};
  30use zed_actions::RevealTarget;
  31
  32use crate::{
  33    motion::{EndOfDocument, Motion, StartOfDocument},
  34    normal::{
  35        search::{FindCommand, ReplaceCommand, Replacement},
  36        JoinLines,
  37    },
  38    object::Object,
  39    state::Mode,
  40    visual::VisualDeleteLine,
  41    Vim,
  42};
  43
  44#[derive(Clone, Debug, PartialEq)]
  45pub struct GoToLine {
  46    range: CommandRange,
  47}
  48
  49#[derive(Clone, Debug, PartialEq)]
  50pub struct YankCommand {
  51    range: CommandRange,
  52}
  53
  54#[derive(Clone, Debug, PartialEq)]
  55pub struct WithRange {
  56    restore_selection: bool,
  57    range: CommandRange,
  58    action: WrappedAction,
  59}
  60
  61#[derive(Clone, Debug, PartialEq)]
  62pub struct WithCount {
  63    count: u32,
  64    action: WrappedAction,
  65}
  66
  67#[derive(Debug)]
  68struct WrappedAction(Box<dyn Action>);
  69
  70actions!(vim, [VisualCommand, CountCommand, ShellCommand]);
  71impl_internal_actions!(
  72    vim,
  73    [
  74        GoToLine,
  75        YankCommand,
  76        WithRange,
  77        WithCount,
  78        OnMatchingLines,
  79        ShellExec
  80    ]
  81);
  82
  83impl PartialEq for WrappedAction {
  84    fn eq(&self, other: &Self) -> bool {
  85        self.0.partial_eq(&*other.0)
  86    }
  87}
  88
  89impl Clone for WrappedAction {
  90    fn clone(&self) -> Self {
  91        Self(self.0.boxed_clone())
  92    }
  93}
  94
  95impl Deref for WrappedAction {
  96    type Target = dyn Action;
  97    fn deref(&self) -> &dyn Action {
  98        &*self.0
  99    }
 100}
 101
 102pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
 103    Vim::action(editor, cx, |vim, _: &VisualCommand, window, cx| {
 104        let Some(workspace) = vim.workspace(window) else {
 105            return;
 106        };
 107        workspace.update(cx, |workspace, cx| {
 108            command_palette::CommandPalette::toggle(workspace, "'<,'>", window, cx);
 109        })
 110    });
 111
 112    Vim::action(editor, cx, |vim, _: &ShellCommand, window, cx| {
 113        let Some(workspace) = vim.workspace(window) else {
 114            return;
 115        };
 116        workspace.update(cx, |workspace, cx| {
 117            command_palette::CommandPalette::toggle(workspace, "'<,'>!", window, cx);
 118        })
 119    });
 120
 121    Vim::action(editor, cx, |vim, _: &CountCommand, window, cx| {
 122        let Some(workspace) = vim.workspace(window) else {
 123            return;
 124        };
 125        let count = Vim::take_count(cx).unwrap_or(1);
 126        let n = if count > 1 {
 127            format!(".,.+{}", count.saturating_sub(1))
 128        } else {
 129            ".".to_string()
 130        };
 131        workspace.update(cx, |workspace, cx| {
 132            command_palette::CommandPalette::toggle(workspace, &n, window, cx);
 133        })
 134    });
 135
 136    Vim::action(editor, cx, |vim, action: &GoToLine, window, cx| {
 137        vim.switch_mode(Mode::Normal, false, window, cx);
 138        let result = vim.update_editor(window, cx, |vim, editor, window, cx| {
 139            let snapshot = editor.snapshot(window, cx);
 140            let buffer_row = action.range.head().buffer_row(vim, editor, window, cx)?;
 141            let current = editor.selections.newest::<Point>(cx);
 142            let target = snapshot
 143                .buffer_snapshot
 144                .clip_point(Point::new(buffer_row.0, current.head().column), Bias::Left);
 145            editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 146                s.select_ranges([target..target]);
 147            });
 148
 149            anyhow::Ok(())
 150        });
 151        if let Some(e @ Err(_)) = result {
 152            let Some(workspace) = vim.workspace(window) else {
 153                return;
 154            };
 155            workspace.update(cx, |workspace, cx| {
 156                e.notify_err(workspace, cx);
 157            });
 158            return;
 159        }
 160    });
 161
 162    Vim::action(editor, cx, |vim, action: &YankCommand, window, cx| {
 163        vim.update_editor(window, cx, |vim, editor, window, cx| {
 164            let snapshot = editor.snapshot(window, cx);
 165            if let Ok(range) = action.range.buffer_range(vim, editor, window, cx) {
 166                let end = if range.end < snapshot.buffer_snapshot.max_row() {
 167                    Point::new(range.end.0 + 1, 0)
 168                } else {
 169                    snapshot.buffer_snapshot.max_point()
 170                };
 171                vim.copy_ranges(
 172                    editor,
 173                    true,
 174                    true,
 175                    vec![Point::new(range.start.0, 0)..end],
 176                    cx,
 177                )
 178            }
 179        });
 180    });
 181
 182    Vim::action(editor, cx, |_, action: &WithCount, window, cx| {
 183        for _ in 0..action.count {
 184            window.dispatch_action(action.action.boxed_clone(), cx)
 185        }
 186    });
 187
 188    Vim::action(editor, cx, |vim, action: &WithRange, window, cx| {
 189        let result = vim.update_editor(window, cx, |vim, editor, window, cx| {
 190            action.range.buffer_range(vim, editor, window, cx)
 191        });
 192
 193        let range = match result {
 194            None => return,
 195            Some(e @ Err(_)) => {
 196                let Some(workspace) = vim.workspace(window) else {
 197                    return;
 198                };
 199                workspace.update(cx, |workspace, cx| {
 200                    e.notify_err(workspace, cx);
 201                });
 202                return;
 203            }
 204            Some(Ok(result)) => result,
 205        };
 206
 207        let previous_selections = vim
 208            .update_editor(window, cx, |_, editor, window, cx| {
 209                let selections = action.restore_selection.then(|| {
 210                    editor
 211                        .selections
 212                        .disjoint_anchor_ranges()
 213                        .collect::<Vec<_>>()
 214                });
 215                editor.change_selections(None, window, cx, |s| {
 216                    let end = Point::new(range.end.0, s.buffer().line_len(range.end));
 217                    s.select_ranges([end..Point::new(range.start.0, 0)]);
 218                });
 219                selections
 220            })
 221            .flatten();
 222        window.dispatch_action(action.action.boxed_clone(), cx);
 223        cx.defer_in(window, move |vim, window, cx| {
 224            vim.update_editor(window, cx, |_, editor, window, cx| {
 225                editor.change_selections(None, window, cx, |s| {
 226                    if let Some(previous_selections) = previous_selections {
 227                        s.select_ranges(previous_selections);
 228                    } else {
 229                        s.select_ranges([
 230                            Point::new(range.start.0, 0)..Point::new(range.start.0, 0)
 231                        ]);
 232                    }
 233                })
 234            });
 235        });
 236    });
 237
 238    Vim::action(editor, cx, |vim, action: &OnMatchingLines, window, cx| {
 239        action.run(vim, window, cx)
 240    });
 241
 242    Vim::action(editor, cx, |vim, action: &ShellExec, window, cx| {
 243        action.run(vim, window, cx)
 244    })
 245}
 246
 247#[derive(Default)]
 248struct VimCommand {
 249    prefix: &'static str,
 250    suffix: &'static str,
 251    action: Option<Box<dyn Action>>,
 252    action_name: Option<&'static str>,
 253    bang_action: Option<Box<dyn Action>>,
 254    range: Option<
 255        Box<
 256            dyn Fn(Box<dyn Action>, &CommandRange) -> Option<Box<dyn Action>>
 257                + Send
 258                + Sync
 259                + 'static,
 260        >,
 261    >,
 262    has_count: bool,
 263}
 264
 265impl VimCommand {
 266    fn new(pattern: (&'static str, &'static str), action: impl Action) -> Self {
 267        Self {
 268            prefix: pattern.0,
 269            suffix: pattern.1,
 270            action: Some(action.boxed_clone()),
 271            ..Default::default()
 272        }
 273    }
 274
 275    // from_str is used for actions in other crates.
 276    fn str(pattern: (&'static str, &'static str), action_name: &'static str) -> Self {
 277        Self {
 278            prefix: pattern.0,
 279            suffix: pattern.1,
 280            action_name: Some(action_name),
 281            ..Default::default()
 282        }
 283    }
 284
 285    fn bang(mut self, bang_action: impl Action) -> Self {
 286        self.bang_action = Some(bang_action.boxed_clone());
 287        self
 288    }
 289
 290    fn range(
 291        mut self,
 292        f: impl Fn(Box<dyn Action>, &CommandRange) -> Option<Box<dyn Action>> + Send + Sync + 'static,
 293    ) -> Self {
 294        self.range = Some(Box::new(f));
 295        self
 296    }
 297
 298    fn count(mut self) -> Self {
 299        self.has_count = true;
 300        self
 301    }
 302
 303    fn parse(
 304        &self,
 305        mut query: &str,
 306        range: &Option<CommandRange>,
 307        cx: &App,
 308    ) -> Option<Box<dyn Action>> {
 309        let has_bang = query.ends_with('!');
 310        if has_bang {
 311            query = &query[..query.len() - 1];
 312        }
 313
 314        let suffix = query.strip_prefix(self.prefix)?;
 315        if !self.suffix.starts_with(suffix) {
 316            return None;
 317        }
 318
 319        let action = if has_bang && self.bang_action.is_some() {
 320            self.bang_action.as_ref().unwrap().boxed_clone()
 321        } else if let Some(action) = self.action.as_ref() {
 322            action.boxed_clone()
 323        } else if let Some(action_name) = self.action_name {
 324            cx.build_action(action_name, None).log_err()?
 325        } else {
 326            return None;
 327        };
 328
 329        if let Some(range) = range {
 330            self.range.as_ref().and_then(|f| f(action, range))
 331        } else {
 332            Some(action)
 333        }
 334    }
 335
 336    // TODO: ranges with search queries
 337    fn parse_range(query: &str) -> (Option<CommandRange>, String) {
 338        let mut chars = query.chars().peekable();
 339
 340        match chars.peek() {
 341            Some('%') => {
 342                chars.next();
 343                return (
 344                    Some(CommandRange {
 345                        start: Position::Line { row: 1, offset: 0 },
 346                        end: Some(Position::LastLine { offset: 0 }),
 347                    }),
 348                    chars.collect(),
 349                );
 350            }
 351            Some('*') => {
 352                chars.next();
 353                return (
 354                    Some(CommandRange {
 355                        start: Position::Mark {
 356                            name: '<',
 357                            offset: 0,
 358                        },
 359                        end: Some(Position::Mark {
 360                            name: '>',
 361                            offset: 0,
 362                        }),
 363                    }),
 364                    chars.collect(),
 365                );
 366            }
 367            _ => {}
 368        }
 369
 370        let start = Self::parse_position(&mut chars);
 371
 372        match chars.peek() {
 373            Some(',' | ';') => {
 374                chars.next();
 375                (
 376                    Some(CommandRange {
 377                        start: start.unwrap_or(Position::CurrentLine { offset: 0 }),
 378                        end: Self::parse_position(&mut chars),
 379                    }),
 380                    chars.collect(),
 381                )
 382            }
 383            _ => (
 384                start.map(|start| CommandRange { start, end: None }),
 385                chars.collect(),
 386            ),
 387        }
 388    }
 389
 390    fn parse_position(chars: &mut Peekable<Chars>) -> Option<Position> {
 391        match chars.peek()? {
 392            '0'..='9' => {
 393                let row = Self::parse_u32(chars);
 394                Some(Position::Line {
 395                    row,
 396                    offset: Self::parse_offset(chars),
 397                })
 398            }
 399            '\'' => {
 400                chars.next();
 401                let name = chars.next()?;
 402                Some(Position::Mark {
 403                    name,
 404                    offset: Self::parse_offset(chars),
 405                })
 406            }
 407            '.' => {
 408                chars.next();
 409                Some(Position::CurrentLine {
 410                    offset: Self::parse_offset(chars),
 411                })
 412            }
 413            '+' | '-' => Some(Position::CurrentLine {
 414                offset: Self::parse_offset(chars),
 415            }),
 416            '$' => {
 417                chars.next();
 418                Some(Position::LastLine {
 419                    offset: Self::parse_offset(chars),
 420                })
 421            }
 422            _ => None,
 423        }
 424    }
 425
 426    fn parse_offset(chars: &mut Peekable<Chars>) -> i32 {
 427        let mut res: i32 = 0;
 428        while matches!(chars.peek(), Some('+' | '-')) {
 429            let sign = if chars.next().unwrap() == '+' { 1 } else { -1 };
 430            let amount = if matches!(chars.peek(), Some('0'..='9')) {
 431                (Self::parse_u32(chars) as i32).saturating_mul(sign)
 432            } else {
 433                sign
 434            };
 435            res = res.saturating_add(amount)
 436        }
 437        res
 438    }
 439
 440    fn parse_u32(chars: &mut Peekable<Chars>) -> u32 {
 441        let mut res: u32 = 0;
 442        while matches!(chars.peek(), Some('0'..='9')) {
 443            res = res
 444                .saturating_mul(10)
 445                .saturating_add(chars.next().unwrap() as u32 - '0' as u32);
 446        }
 447        res
 448    }
 449}
 450
 451#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq)]
 452enum Position {
 453    Line { row: u32, offset: i32 },
 454    Mark { name: char, offset: i32 },
 455    LastLine { offset: i32 },
 456    CurrentLine { offset: i32 },
 457}
 458
 459impl Position {
 460    fn buffer_row(
 461        &self,
 462        vim: &Vim,
 463        editor: &mut Editor,
 464        window: &mut Window,
 465        cx: &mut App,
 466    ) -> Result<MultiBufferRow> {
 467        let snapshot = editor.snapshot(window, cx);
 468        let target = match self {
 469            Position::Line { row, offset } => {
 470                if let Some(anchor) = editor.active_excerpt(cx).and_then(|(_, buffer, _)| {
 471                    editor.buffer().read(cx).buffer_point_to_anchor(
 472                        &buffer,
 473                        Point::new(row.saturating_sub(1), 0),
 474                        cx,
 475                    )
 476                }) {
 477                    anchor
 478                        .to_point(&snapshot.buffer_snapshot)
 479                        .row
 480                        .saturating_add_signed(*offset)
 481                } else {
 482                    row.saturating_add_signed(offset.saturating_sub(1))
 483                }
 484            }
 485            Position::Mark { name, offset } => {
 486                let Some(mark) = vim.marks.get(&name.to_string()).and_then(|vec| vec.last()) else {
 487                    return Err(anyhow!("mark {} not set", name));
 488                };
 489                mark.to_point(&snapshot.buffer_snapshot)
 490                    .row
 491                    .saturating_add_signed(*offset)
 492            }
 493            Position::LastLine { offset } => snapshot
 494                .buffer_snapshot
 495                .max_row()
 496                .0
 497                .saturating_add_signed(*offset),
 498            Position::CurrentLine { offset } => editor
 499                .selections
 500                .newest_anchor()
 501                .head()
 502                .to_point(&snapshot.buffer_snapshot)
 503                .row
 504                .saturating_add_signed(*offset),
 505        };
 506
 507        Ok(MultiBufferRow(target).min(snapshot.buffer_snapshot.max_row()))
 508    }
 509}
 510
 511#[derive(Clone, Debug, PartialEq)]
 512pub(crate) struct CommandRange {
 513    start: Position,
 514    end: Option<Position>,
 515}
 516
 517impl CommandRange {
 518    fn head(&self) -> &Position {
 519        self.end.as_ref().unwrap_or(&self.start)
 520    }
 521
 522    pub(crate) fn buffer_range(
 523        &self,
 524        vim: &Vim,
 525        editor: &mut Editor,
 526        window: &mut Window,
 527        cx: &mut App,
 528    ) -> Result<Range<MultiBufferRow>> {
 529        let start = self.start.buffer_row(vim, editor, window, cx)?;
 530        let end = if let Some(end) = self.end.as_ref() {
 531            end.buffer_row(vim, editor, window, 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(_: &App) -> 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: &App) -> &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: &App) -> 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: &App,
 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, window: &mut Window, cx: &mut Context<Vim>) {
 985        let result = vim.update_editor(window, cx, |vim, editor, window, cx| {
 986            self.range.buffer_range(vim, editor, window, cx)
 987        });
 988
 989        let range = match result {
 990            None => return,
 991            Some(e @ Err(_)) => {
 992                let Some(workspace) = vim.workspace(window) 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(window) 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(window, 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(window, cx) {
1037                            let _ = search_bar.search(
1038                                &last_pattern,
1039                                Some(SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE),
1040                                window,
1041                                cx,
1042                            );
1043                        }
1044                    });
1045                }
1046            });
1047        };
1048
1049        vim.update_editor(window, cx, |_, editor, window, cx| {
1050            let snapshot = editor.snapshot(window, cx);
1051            let mut row = range.start.0;
1052
1053            let point_range = Point::new(range.start.0, 0)
1054                ..snapshot
1055                    .buffer_snapshot
1056                    .clip_point(Point::new(range.end.0 + 1, 0), Bias::Left);
1057            cx.spawn_in(window, |editor, mut cx| async move {
1058                let new_selections = cx
1059                    .background_executor()
1060                    .spawn(async move {
1061                        let mut line = String::new();
1062                        let mut new_selections = Vec::new();
1063                        let chunks = snapshot
1064                            .buffer_snapshot
1065                            .text_for_range(point_range)
1066                            .chain(["\n"]);
1067
1068                        for chunk in chunks {
1069                            for (newline_ix, text) in chunk.split('\n').enumerate() {
1070                                if newline_ix > 0 {
1071                                    if regexes.iter().all(|(regex, should_match)| {
1072                                        regex.is_match(&line) == *should_match
1073                                    }) {
1074                                        new_selections
1075                                            .push(Point::new(row, 0).to_display_point(&snapshot))
1076                                    }
1077                                    row += 1;
1078                                    line.clear();
1079                                }
1080                                line.push_str(text)
1081                            }
1082                        }
1083
1084                        new_selections
1085                    })
1086                    .await;
1087
1088                if new_selections.is_empty() {
1089                    return;
1090                }
1091                editor
1092                    .update_in(&mut cx, |editor, window, cx| {
1093                        editor.start_transaction_at(Instant::now(), window, cx);
1094                        editor.change_selections(None, window, cx, |s| {
1095                            s.replace_cursors_with(|_| new_selections);
1096                        });
1097                        window.dispatch_action(action, cx);
1098                        cx.defer_in(window, move |editor, window, cx| {
1099                            let newest = editor.selections.newest::<Point>(cx).clone();
1100                            editor.change_selections(None, window, cx, |s| {
1101                                s.select(vec![newest]);
1102                            });
1103                            editor.end_transaction_at(Instant::now(), cx);
1104                        })
1105                    })
1106                    .ok();
1107            })
1108            .detach();
1109        });
1110    }
1111}
1112
1113#[derive(Clone, Debug, PartialEq)]
1114pub struct ShellExec {
1115    command: String,
1116    range: Option<CommandRange>,
1117    is_read: bool,
1118}
1119
1120impl Vim {
1121    pub fn cancel_running_command(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1122        if self.running_command.take().is_some() {
1123            self.update_editor(window, cx, |_, editor, window, cx| {
1124                editor.transact(window, cx, |editor, _window, _cx| {
1125                    editor.clear_row_highlights::<ShellExec>();
1126                })
1127            });
1128        }
1129    }
1130
1131    fn prepare_shell_command(
1132        &mut self,
1133        command: &str,
1134        window: &mut Window,
1135        cx: &mut Context<Self>,
1136    ) -> String {
1137        let mut ret = String::new();
1138        // N.B. non-standard escaping rules:
1139        // * !echo % => "echo README.md"
1140        // * !echo \% => "echo %"
1141        // * !echo \\% => echo \%
1142        // * !echo \\\% => echo \\%
1143        for c in command.chars() {
1144            if c != '%' && c != '!' {
1145                ret.push(c);
1146                continue;
1147            } else if ret.chars().last() == Some('\\') {
1148                ret.pop();
1149                ret.push(c);
1150                continue;
1151            }
1152            match c {
1153                '%' => {
1154                    self.update_editor(window, cx, |_, editor, _window, cx| {
1155                        if let Some((_, buffer, _)) = editor.active_excerpt(cx) {
1156                            if let Some(file) = buffer.read(cx).file() {
1157                                if let Some(local) = file.as_local() {
1158                                    if let Some(str) = local.path().to_str() {
1159                                        ret.push_str(str)
1160                                    }
1161                                }
1162                            }
1163                        }
1164                    });
1165                }
1166                '!' => {
1167                    if let Some(command) = &self.last_command {
1168                        ret.push_str(command)
1169                    }
1170                }
1171                _ => {}
1172            }
1173        }
1174        self.last_command = Some(ret.clone());
1175        ret
1176    }
1177
1178    pub fn shell_command_motion(
1179        &mut self,
1180        motion: Motion,
1181        times: Option<usize>,
1182        window: &mut Window,
1183        cx: &mut Context<Vim>,
1184    ) {
1185        self.stop_recording(cx);
1186        let Some(workspace) = self.workspace(window) else {
1187            return;
1188        };
1189        let command = self.update_editor(window, cx, |_, editor, window, cx| {
1190            let snapshot = editor.snapshot(window, cx);
1191            let start = editor.selections.newest_display(cx);
1192            let text_layout_details = editor.text_layout_details(window);
1193            let mut range = motion
1194                .range(&snapshot, start.clone(), times, false, &text_layout_details)
1195                .unwrap_or(start.range());
1196            if range.start != start.start {
1197                editor.change_selections(None, window, cx, |s| {
1198                    s.select_ranges([
1199                        range.start.to_point(&snapshot)..range.start.to_point(&snapshot)
1200                    ]);
1201                })
1202            }
1203            if range.end.row() > range.start.row() && range.end.column() != 0 {
1204                *range.end.row_mut() -= 1
1205            }
1206            if range.end.row() == range.start.row() {
1207                ".!".to_string()
1208            } else {
1209                format!(".,.+{}!", (range.end.row() - range.start.row()).0)
1210            }
1211        });
1212        if let Some(command) = command {
1213            workspace.update(cx, |workspace, cx| {
1214                command_palette::CommandPalette::toggle(workspace, &command, window, cx);
1215            });
1216        }
1217    }
1218
1219    pub fn shell_command_object(
1220        &mut self,
1221        object: Object,
1222        around: bool,
1223        window: &mut Window,
1224        cx: &mut Context<Vim>,
1225    ) {
1226        self.stop_recording(cx);
1227        let Some(workspace) = self.workspace(window) else {
1228            return;
1229        };
1230        let command = self.update_editor(window, cx, |_, editor, window, cx| {
1231            let snapshot = editor.snapshot(window, cx);
1232            let start = editor.selections.newest_display(cx);
1233            let range = object
1234                .range(&snapshot, start.clone(), around)
1235                .unwrap_or(start.range());
1236            if range.start != start.start {
1237                editor.change_selections(None, window, cx, |s| {
1238                    s.select_ranges([
1239                        range.start.to_point(&snapshot)..range.start.to_point(&snapshot)
1240                    ]);
1241                })
1242            }
1243            if range.end.row() == range.start.row() {
1244                ".!".to_string()
1245            } else {
1246                format!(".,.+{}!", (range.end.row() - range.start.row()).0)
1247            }
1248        });
1249        if let Some(command) = command {
1250            workspace.update(cx, |workspace, cx| {
1251                command_palette::CommandPalette::toggle(workspace, &command, window, cx);
1252            });
1253        }
1254    }
1255}
1256
1257impl ShellExec {
1258    pub fn parse(query: &str, range: Option<CommandRange>) -> Option<Box<dyn Action>> {
1259        let (before, after) = query.split_once('!')?;
1260        let before = before.trim();
1261
1262        if !"read".starts_with(before) {
1263            return None;
1264        }
1265
1266        Some(
1267            ShellExec {
1268                command: after.trim().to_string(),
1269                range,
1270                is_read: !before.is_empty(),
1271            }
1272            .boxed_clone(),
1273        )
1274    }
1275
1276    pub fn run(&self, vim: &mut Vim, window: &mut Window, cx: &mut Context<Vim>) {
1277        let Some(workspace) = vim.workspace(window) else {
1278            return;
1279        };
1280
1281        let project = workspace.read(cx).project().clone();
1282        let command = vim.prepare_shell_command(&self.command, window, cx);
1283
1284        if self.range.is_none() && !self.is_read {
1285            workspace.update(cx, |workspace, cx| {
1286                let project = workspace.project().read(cx);
1287                let cwd = project.first_project_directory(cx);
1288                let shell = project.terminal_settings(&cwd, cx).shell.clone();
1289                cx.emit(workspace::Event::SpawnTask {
1290                    action: Box::new(SpawnInTerminal {
1291                        id: TaskId("vim".to_string()),
1292                        full_label: self.command.clone(),
1293                        label: self.command.clone(),
1294                        command: command.clone(),
1295                        args: Vec::new(),
1296                        command_label: self.command.clone(),
1297                        cwd,
1298                        env: HashMap::default(),
1299                        use_new_terminal: true,
1300                        allow_concurrent_runs: true,
1301                        reveal: RevealStrategy::NoFocus,
1302                        reveal_target: RevealTarget::Dock,
1303                        hide: HideStrategy::Never,
1304                        shell,
1305                        show_summary: false,
1306                        show_command: false,
1307                    }),
1308                });
1309            });
1310            return;
1311        };
1312
1313        let mut input_snapshot = None;
1314        let mut input_range = None;
1315        let mut needs_newline_prefix = false;
1316        vim.update_editor(window, cx, |vim, editor, window, cx| {
1317            let snapshot = editor.buffer().read(cx).snapshot(cx);
1318            let range = if let Some(range) = self.range.clone() {
1319                let Some(range) = range.buffer_range(vim, editor, window, cx).log_err() else {
1320                    return;
1321                };
1322                Point::new(range.start.0, 0)
1323                    ..snapshot.clip_point(Point::new(range.end.0 + 1, 0), Bias::Right)
1324            } else {
1325                let mut end = editor.selections.newest::<Point>(cx).range().end;
1326                end = snapshot.clip_point(Point::new(end.row + 1, 0), Bias::Right);
1327                needs_newline_prefix = end == snapshot.max_point();
1328                end..end
1329            };
1330            if self.is_read {
1331                input_range =
1332                    Some(snapshot.anchor_after(range.end)..snapshot.anchor_after(range.end));
1333            } else {
1334                input_range =
1335                    Some(snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end));
1336            }
1337            editor.highlight_rows::<ShellExec>(
1338                input_range.clone().unwrap(),
1339                cx.theme().status().unreachable_background,
1340                false,
1341                cx,
1342            );
1343
1344            if !self.is_read {
1345                input_snapshot = Some(snapshot)
1346            }
1347        });
1348
1349        let Some(range) = input_range else { return };
1350
1351        let mut process = project.read(cx).exec_in_shell(command, cx);
1352        process.stdout(Stdio::piped());
1353        process.stderr(Stdio::piped());
1354
1355        if input_snapshot.is_some() {
1356            process.stdin(Stdio::piped());
1357        } else {
1358            process.stdin(Stdio::null());
1359        };
1360
1361        // https://registerspill.thorstenball.com/p/how-to-lose-control-of-your-shell
1362        //
1363        // safety: code in pre_exec should be signal safe.
1364        // https://man7.org/linux/man-pages/man7/signal-safety.7.html
1365        #[cfg(not(target_os = "windows"))]
1366        unsafe {
1367            use std::os::unix::process::CommandExt;
1368            process.pre_exec(|| {
1369                libc::setsid();
1370                Ok(())
1371            });
1372        };
1373        let is_read = self.is_read;
1374
1375        let task = cx.spawn_in(window, |vim, mut cx| async move {
1376            let Some(mut running) = process.spawn().log_err() else {
1377                vim.update_in(&mut cx, |vim, window, cx| {
1378                    vim.cancel_running_command(window, cx);
1379                })
1380                .log_err();
1381                return;
1382            };
1383
1384            if let Some(mut stdin) = running.stdin.take() {
1385                if let Some(snapshot) = input_snapshot {
1386                    let range = range.clone();
1387                    cx.background_executor()
1388                        .spawn(async move {
1389                            for chunk in snapshot.text_for_range(range) {
1390                                if stdin.write_all(chunk.as_bytes()).log_err().is_none() {
1391                                    return;
1392                                }
1393                            }
1394                            stdin.flush().log_err();
1395                        })
1396                        .detach();
1397                }
1398            };
1399
1400            let output = cx
1401                .background_executor()
1402                .spawn(async move { running.wait_with_output() })
1403                .await;
1404
1405            let Some(output) = output.log_err() else {
1406                vim.update_in(&mut cx, |vim, window, cx| {
1407                    vim.cancel_running_command(window, cx);
1408                })
1409                .log_err();
1410                return;
1411            };
1412            let mut text = String::new();
1413            if needs_newline_prefix {
1414                text.push('\n');
1415            }
1416            text.push_str(&String::from_utf8_lossy(&output.stdout));
1417            text.push_str(&String::from_utf8_lossy(&output.stderr));
1418            if !text.is_empty() && text.chars().last() != Some('\n') {
1419                text.push('\n');
1420            }
1421
1422            vim.update_in(&mut cx, |vim, window, cx| {
1423                vim.update_editor(window, cx, |_, editor, window, cx| {
1424                    editor.transact(window, cx, |editor, window, cx| {
1425                        editor.edit([(range.clone(), text)], cx);
1426                        let snapshot = editor.buffer().read(cx).snapshot(cx);
1427                        editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
1428                            let point = if is_read {
1429                                let point = range.end.to_point(&snapshot);
1430                                Point::new(point.row.saturating_sub(1), 0)
1431                            } else {
1432                                let point = range.start.to_point(&snapshot);
1433                                Point::new(point.row, 0)
1434                            };
1435                            s.select_ranges([point..point]);
1436                        })
1437                    })
1438                });
1439                vim.cancel_running_command(window, cx);
1440            })
1441            .log_err();
1442        });
1443        vim.running_command.replace(task);
1444    }
1445}
1446
1447#[cfg(test)]
1448mod test {
1449    use std::path::Path;
1450
1451    use crate::{
1452        state::Mode,
1453        test::{NeovimBackedTestContext, VimTestContext},
1454    };
1455    use editor::Editor;
1456    use gpui::{Context, TestAppContext};
1457    use indoc::indoc;
1458    use util::path;
1459    use workspace::Workspace;
1460
1461    #[gpui::test]
1462    async fn test_command_basics(cx: &mut TestAppContext) {
1463        let mut cx = NeovimBackedTestContext::new(cx).await;
1464
1465        cx.set_shared_state(indoc! {"
1466            ˇa
1467            b
1468            c"})
1469            .await;
1470
1471        cx.simulate_shared_keystrokes(": j enter").await;
1472
1473        // hack: our cursor positioning after a join command is wrong
1474        cx.simulate_shared_keystrokes("^").await;
1475        cx.shared_state().await.assert_eq(indoc! {
1476            "ˇa b
1477            c"
1478        });
1479    }
1480
1481    #[gpui::test]
1482    async fn test_command_goto(cx: &mut TestAppContext) {
1483        let mut cx = NeovimBackedTestContext::new(cx).await;
1484
1485        cx.set_shared_state(indoc! {"
1486            ˇa
1487            b
1488            c"})
1489            .await;
1490        cx.simulate_shared_keystrokes(": 3 enter").await;
1491        cx.shared_state().await.assert_eq(indoc! {"
1492            a
1493            b
1494            ˇc"});
1495    }
1496
1497    #[gpui::test]
1498    async fn test_command_replace(cx: &mut TestAppContext) {
1499        let mut cx = NeovimBackedTestContext::new(cx).await;
1500
1501        cx.set_shared_state(indoc! {"
1502            ˇa
1503            b
1504            b
1505            c"})
1506            .await;
1507        cx.simulate_shared_keystrokes(": % s / b / d enter").await;
1508        cx.shared_state().await.assert_eq(indoc! {"
1509            a
1510            d
1511            ˇd
1512            c"});
1513        cx.simulate_shared_keystrokes(": % s : . : \\ 0 \\ 0 enter")
1514            .await;
1515        cx.shared_state().await.assert_eq(indoc! {"
1516            aa
1517            dd
1518            dd
1519            ˇcc"});
1520        cx.simulate_shared_keystrokes("k : s / d d / e e enter")
1521            .await;
1522        cx.shared_state().await.assert_eq(indoc! {"
1523            aa
1524            dd
1525            ˇee
1526            cc"});
1527    }
1528
1529    #[gpui::test]
1530    async fn test_command_search(cx: &mut TestAppContext) {
1531        let mut cx = NeovimBackedTestContext::new(cx).await;
1532
1533        cx.set_shared_state(indoc! {"
1534                ˇa
1535                b
1536                a
1537                c"})
1538            .await;
1539        cx.simulate_shared_keystrokes(": / b enter").await;
1540        cx.shared_state().await.assert_eq(indoc! {"
1541                a
1542                ˇb
1543                a
1544                c"});
1545        cx.simulate_shared_keystrokes(": ? a enter").await;
1546        cx.shared_state().await.assert_eq(indoc! {"
1547                ˇa
1548                b
1549                a
1550                c"});
1551    }
1552
1553    #[gpui::test]
1554    async fn test_command_write(cx: &mut TestAppContext) {
1555        let mut cx = VimTestContext::new(cx, true).await;
1556        let path = Path::new(path!("/root/dir/file.rs"));
1557        let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
1558
1559        cx.simulate_keystrokes("i @ escape");
1560        cx.simulate_keystrokes(": w enter");
1561
1562        assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "@\n");
1563
1564        fs.as_fake().insert_file(path, b"oops\n".to_vec()).await;
1565
1566        // conflict!
1567        cx.simulate_keystrokes("i @ escape");
1568        cx.simulate_keystrokes(": w enter");
1569        assert!(cx.has_pending_prompt());
1570        // "Cancel"
1571        cx.simulate_prompt_answer(0);
1572        assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "oops\n");
1573        assert!(!cx.has_pending_prompt());
1574        // force overwrite
1575        cx.simulate_keystrokes(": w ! enter");
1576        assert!(!cx.has_pending_prompt());
1577        assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "@@\n");
1578    }
1579
1580    #[gpui::test]
1581    async fn test_command_quit(cx: &mut TestAppContext) {
1582        let mut cx = VimTestContext::new(cx, true).await;
1583
1584        cx.simulate_keystrokes(": n e w enter");
1585        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
1586        cx.simulate_keystrokes(": q enter");
1587        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 1));
1588        cx.simulate_keystrokes(": n e w enter");
1589        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
1590        cx.simulate_keystrokes(": q a enter");
1591        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 0));
1592    }
1593
1594    #[gpui::test]
1595    async fn test_offsets(cx: &mut TestAppContext) {
1596        let mut cx = NeovimBackedTestContext::new(cx).await;
1597
1598        cx.set_shared_state("ˇ1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n")
1599            .await;
1600
1601        cx.simulate_shared_keystrokes(": + enter").await;
1602        cx.shared_state()
1603            .await
1604            .assert_eq("1\nˇ2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n");
1605
1606        cx.simulate_shared_keystrokes(": 1 0 - enter").await;
1607        cx.shared_state()
1608            .await
1609            .assert_eq("1\n2\n3\n4\n5\n6\n7\n8\nˇ9\n10\n11\n");
1610
1611        cx.simulate_shared_keystrokes(": . - 2 enter").await;
1612        cx.shared_state()
1613            .await
1614            .assert_eq("1\n2\n3\n4\n5\n6\nˇ7\n8\n9\n10\n11\n");
1615
1616        cx.simulate_shared_keystrokes(": % enter").await;
1617        cx.shared_state()
1618            .await
1619            .assert_eq("1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\nˇ");
1620    }
1621
1622    #[gpui::test]
1623    async fn test_command_ranges(cx: &mut TestAppContext) {
1624        let mut cx = NeovimBackedTestContext::new(cx).await;
1625
1626        cx.set_shared_state("ˇ1\n2\n3\n4\n4\n3\n2\n1").await;
1627
1628        cx.simulate_shared_keystrokes(": 2 , 4 d enter").await;
1629        cx.shared_state().await.assert_eq("1\nˇ4\n3\n2\n1");
1630
1631        cx.simulate_shared_keystrokes(": 2 , 4 s o r t enter").await;
1632        cx.shared_state().await.assert_eq("1\nˇ2\n3\n4\n1");
1633
1634        cx.simulate_shared_keystrokes(": 2 , 4 j o i n enter").await;
1635        cx.shared_state().await.assert_eq("1\nˇ2 3 4\n1");
1636    }
1637
1638    #[gpui::test]
1639    async fn test_command_visual_replace(cx: &mut TestAppContext) {
1640        let mut cx = NeovimBackedTestContext::new(cx).await;
1641
1642        cx.set_shared_state("ˇ1\n2\n3\n4\n4\n3\n2\n1").await;
1643
1644        cx.simulate_shared_keystrokes("v 2 j : s / . / k enter")
1645            .await;
1646        cx.shared_state().await.assert_eq("k\nk\nˇk\n4\n4\n3\n2\n1");
1647    }
1648
1649    fn assert_active_item(
1650        workspace: &mut Workspace,
1651        expected_path: &str,
1652        expected_text: &str,
1653        cx: &mut Context<Workspace>,
1654    ) {
1655        let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();
1656
1657        let buffer = active_editor
1658            .read(cx)
1659            .buffer()
1660            .read(cx)
1661            .as_singleton()
1662            .unwrap();
1663
1664        let text = buffer.read(cx).text();
1665        let file = buffer.read(cx).file().unwrap();
1666        let file_path = file.as_local().unwrap().abs_path(cx);
1667
1668        assert_eq!(text, expected_text);
1669        assert_eq!(file_path, Path::new(expected_path));
1670    }
1671
1672    #[gpui::test]
1673    async fn test_command_gf(cx: &mut TestAppContext) {
1674        let mut cx = VimTestContext::new(cx, true).await;
1675
1676        // Assert base state, that we're in /root/dir/file.rs
1677        cx.workspace(|workspace, _, cx| {
1678            assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
1679        });
1680
1681        // Insert a new file
1682        let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
1683        fs.as_fake()
1684            .insert_file(
1685                path!("/root/dir/file2.rs"),
1686                "This is file2.rs".as_bytes().to_vec(),
1687            )
1688            .await;
1689        fs.as_fake()
1690            .insert_file(
1691                path!("/root/dir/file3.rs"),
1692                "go to file3".as_bytes().to_vec(),
1693            )
1694            .await;
1695
1696        // Put the path to the second file into the currently open buffer
1697        cx.set_state(indoc! {"go to fiˇle2.rs"}, Mode::Normal);
1698
1699        // Go to file2.rs
1700        cx.simulate_keystrokes("g f");
1701
1702        // We now have two items
1703        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
1704        cx.workspace(|workspace, _, cx| {
1705            assert_active_item(
1706                workspace,
1707                path!("/root/dir/file2.rs"),
1708                "This is file2.rs",
1709                cx,
1710            );
1711        });
1712
1713        // Update editor to point to `file2.rs`
1714        cx.editor =
1715            cx.workspace(|workspace, _, cx| workspace.active_item_as::<Editor>(cx).unwrap());
1716
1717        // Put the path to the third file into the currently open buffer,
1718        // but remove its suffix, because we want that lookup to happen automatically.
1719        cx.set_state(indoc! {"go to fiˇle3"}, Mode::Normal);
1720
1721        // Go to file3.rs
1722        cx.simulate_keystrokes("g f");
1723
1724        // We now have three items
1725        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 3));
1726        cx.workspace(|workspace, _, cx| {
1727            assert_active_item(workspace, path!("/root/dir/file3.rs"), "go to file3", cx);
1728        });
1729    }
1730
1731    #[gpui::test]
1732    async fn test_command_matching_lines(cx: &mut TestAppContext) {
1733        let mut cx = NeovimBackedTestContext::new(cx).await;
1734
1735        cx.set_shared_state(indoc! {"
1736            ˇa
1737            b
1738            a
1739            b
1740            a
1741        "})
1742            .await;
1743
1744        cx.simulate_shared_keystrokes(":").await;
1745        cx.simulate_shared_keystrokes("g / a / d").await;
1746        cx.simulate_shared_keystrokes("enter").await;
1747
1748        cx.shared_state().await.assert_eq(indoc! {"
1749            b
1750            b
1751            ˇ"});
1752
1753        cx.simulate_shared_keystrokes("u").await;
1754
1755        cx.shared_state().await.assert_eq(indoc! {"
1756            ˇa
1757            b
1758            a
1759            b
1760            a
1761        "});
1762
1763        cx.simulate_shared_keystrokes(":").await;
1764        cx.simulate_shared_keystrokes("v / a / d").await;
1765        cx.simulate_shared_keystrokes("enter").await;
1766
1767        cx.shared_state().await.assert_eq(indoc! {"
1768            a
1769            a
1770            ˇa"});
1771    }
1772}