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                close_pinned: false,
 571            },
 572        )
 573        .bang(workspace::CloseActiveItem {
 574            save_intent: Some(SaveIntent::Skip),
 575            close_pinned: true,
 576        }),
 577        VimCommand::new(
 578            ("wq", ""),
 579            workspace::CloseActiveItem {
 580                save_intent: Some(SaveIntent::Save),
 581                close_pinned: false,
 582            },
 583        )
 584        .bang(workspace::CloseActiveItem {
 585            save_intent: Some(SaveIntent::Overwrite),
 586            close_pinned: true,
 587        }),
 588        VimCommand::new(
 589            ("x", "it"),
 590            workspace::CloseActiveItem {
 591                save_intent: Some(SaveIntent::SaveAll),
 592                close_pinned: false,
 593            },
 594        )
 595        .bang(workspace::CloseActiveItem {
 596            save_intent: Some(SaveIntent::Overwrite),
 597            close_pinned: true,
 598        }),
 599        VimCommand::new(
 600            ("ex", "it"),
 601            workspace::CloseActiveItem {
 602                save_intent: Some(SaveIntent::SaveAll),
 603                close_pinned: false,
 604            },
 605        )
 606        .bang(workspace::CloseActiveItem {
 607            save_intent: Some(SaveIntent::Overwrite),
 608            close_pinned: true,
 609        }),
 610        VimCommand::new(
 611            ("up", "date"),
 612            workspace::Save {
 613                save_intent: Some(SaveIntent::SaveAll),
 614            },
 615        ),
 616        VimCommand::new(
 617            ("wa", "ll"),
 618            workspace::SaveAll {
 619                save_intent: Some(SaveIntent::SaveAll),
 620            },
 621        )
 622        .bang(workspace::SaveAll {
 623            save_intent: Some(SaveIntent::Overwrite),
 624        }),
 625        VimCommand::new(
 626            ("qa", "ll"),
 627            workspace::CloseAllItemsAndPanes {
 628                save_intent: Some(SaveIntent::Close),
 629            },
 630        )
 631        .bang(workspace::CloseAllItemsAndPanes {
 632            save_intent: Some(SaveIntent::Skip),
 633        }),
 634        VimCommand::new(
 635            ("quita", "ll"),
 636            workspace::CloseAllItemsAndPanes {
 637                save_intent: Some(SaveIntent::Close),
 638            },
 639        )
 640        .bang(workspace::CloseAllItemsAndPanes {
 641            save_intent: Some(SaveIntent::Skip),
 642        }),
 643        VimCommand::new(
 644            ("xa", "ll"),
 645            workspace::CloseAllItemsAndPanes {
 646                save_intent: Some(SaveIntent::SaveAll),
 647            },
 648        )
 649        .bang(workspace::CloseAllItemsAndPanes {
 650            save_intent: Some(SaveIntent::Overwrite),
 651        }),
 652        VimCommand::new(
 653            ("wqa", "ll"),
 654            workspace::CloseAllItemsAndPanes {
 655                save_intent: Some(SaveIntent::SaveAll),
 656            },
 657        )
 658        .bang(workspace::CloseAllItemsAndPanes {
 659            save_intent: Some(SaveIntent::Overwrite),
 660        }),
 661        VimCommand::new(("cq", "uit"), zed_actions::Quit),
 662        VimCommand::new(("sp", "lit"), workspace::SplitHorizontal),
 663        VimCommand::new(("vs", "plit"), workspace::SplitVertical),
 664        VimCommand::new(
 665            ("bd", "elete"),
 666            workspace::CloseActiveItem {
 667                save_intent: Some(SaveIntent::Close),
 668                close_pinned: false,
 669            },
 670        )
 671        .bang(workspace::CloseActiveItem {
 672            save_intent: Some(SaveIntent::Skip),
 673            close_pinned: true,
 674        }),
 675        VimCommand::new(("bn", "ext"), workspace::ActivateNextItem).count(),
 676        VimCommand::new(("bN", "ext"), workspace::ActivatePrevItem).count(),
 677        VimCommand::new(("bp", "revious"), workspace::ActivatePrevItem).count(),
 678        VimCommand::new(("bf", "irst"), workspace::ActivateItem(0)),
 679        VimCommand::new(("br", "ewind"), workspace::ActivateItem(0)),
 680        VimCommand::new(("bl", "ast"), workspace::ActivateLastItem),
 681        VimCommand::new(("new", ""), workspace::NewFileSplitHorizontal),
 682        VimCommand::new(("vne", "w"), workspace::NewFileSplitVertical),
 683        VimCommand::new(("tabe", "dit"), workspace::NewFile),
 684        VimCommand::new(("tabnew", ""), workspace::NewFile),
 685        VimCommand::new(("tabn", "ext"), workspace::ActivateNextItem).count(),
 686        VimCommand::new(("tabp", "revious"), workspace::ActivatePrevItem).count(),
 687        VimCommand::new(("tabN", "ext"), workspace::ActivatePrevItem).count(),
 688        VimCommand::new(
 689            ("tabc", "lose"),
 690            workspace::CloseActiveItem {
 691                save_intent: Some(SaveIntent::Close),
 692                close_pinned: false,
 693            },
 694        ),
 695        VimCommand::new(
 696            ("tabo", "nly"),
 697            workspace::CloseInactiveItems {
 698                save_intent: Some(SaveIntent::Close),
 699                close_pinned: false,
 700            },
 701        )
 702        .bang(workspace::CloseInactiveItems {
 703            save_intent: Some(SaveIntent::Skip),
 704            close_pinned: false,
 705        }),
 706        VimCommand::new(
 707            ("on", "ly"),
 708            workspace::CloseInactiveTabsAndPanes {
 709                save_intent: Some(SaveIntent::Close),
 710            },
 711        )
 712        .bang(workspace::CloseInactiveTabsAndPanes {
 713            save_intent: Some(SaveIntent::Skip),
 714        }),
 715        VimCommand::str(("cl", "ist"), "diagnostics::Deploy"),
 716        VimCommand::new(("cc", ""), editor::actions::Hover),
 717        VimCommand::new(("ll", ""), editor::actions::Hover),
 718        VimCommand::new(("cn", "ext"), editor::actions::GoToDiagnostic).range(wrap_count),
 719        VimCommand::new(("cp", "revious"), editor::actions::GoToPrevDiagnostic).range(wrap_count),
 720        VimCommand::new(("cN", "ext"), editor::actions::GoToPrevDiagnostic).range(wrap_count),
 721        VimCommand::new(("lp", "revious"), editor::actions::GoToPrevDiagnostic).range(wrap_count),
 722        VimCommand::new(("lN", "ext"), editor::actions::GoToPrevDiagnostic).range(wrap_count),
 723        VimCommand::new(("j", "oin"), JoinLines).range(select_range),
 724        VimCommand::new(("fo", "ld"), editor::actions::FoldSelectedRanges).range(act_on_range),
 725        VimCommand::new(("foldo", "pen"), editor::actions::UnfoldLines)
 726            .bang(editor::actions::UnfoldRecursive)
 727            .range(act_on_range),
 728        VimCommand::new(("foldc", "lose"), editor::actions::Fold)
 729            .bang(editor::actions::FoldRecursive)
 730            .range(act_on_range),
 731        VimCommand::new(("dif", "fupdate"), editor::actions::ToggleSelectedDiffHunks)
 732            .range(act_on_range),
 733        VimCommand::new(("rev", "ert"), editor::actions::RevertSelectedHunks).range(act_on_range),
 734        VimCommand::new(("d", "elete"), VisualDeleteLine).range(select_range),
 735        VimCommand::new(("y", "ank"), gpui::NoAction).range(|_, range| {
 736            Some(
 737                YankCommand {
 738                    range: range.clone(),
 739                }
 740                .boxed_clone(),
 741            )
 742        }),
 743        VimCommand::new(("sor", "t"), SortLinesCaseSensitive).range(select_range),
 744        VimCommand::new(("sort i", ""), SortLinesCaseInsensitive).range(select_range),
 745        VimCommand::str(("E", "xplore"), "project_panel::ToggleFocus"),
 746        VimCommand::str(("H", "explore"), "project_panel::ToggleFocus"),
 747        VimCommand::str(("L", "explore"), "project_panel::ToggleFocus"),
 748        VimCommand::str(("S", "explore"), "project_panel::ToggleFocus"),
 749        VimCommand::str(("Ve", "xplore"), "project_panel::ToggleFocus"),
 750        VimCommand::str(("te", "rm"), "terminal_panel::ToggleFocus"),
 751        VimCommand::str(("T", "erm"), "terminal_panel::ToggleFocus"),
 752        VimCommand::str(("C", "ollab"), "collab_panel::ToggleFocus"),
 753        VimCommand::str(("Ch", "at"), "chat_panel::ToggleFocus"),
 754        VimCommand::str(("No", "tifications"), "notification_panel::ToggleFocus"),
 755        VimCommand::str(("A", "I"), "assistant::ToggleFocus"),
 756        VimCommand::new(("noh", "lsearch"), search::buffer_search::Dismiss),
 757        VimCommand::new(("$", ""), EndOfDocument),
 758        VimCommand::new(("%", ""), EndOfDocument),
 759        VimCommand::new(("0", ""), StartOfDocument),
 760        VimCommand::new(("e", "dit"), editor::actions::ReloadFile)
 761            .bang(editor::actions::ReloadFile),
 762        VimCommand::new(("cpp", "link"), editor::actions::CopyPermalinkToLine).range(act_on_range),
 763    ]
 764}
 765
 766struct VimCommands(Vec<VimCommand>);
 767// safety: we only ever access this from the main thread (as ensured by the cx argument)
 768// actions are not Sync so we can't otherwise use a OnceLock.
 769unsafe impl Sync for VimCommands {}
 770impl Global for VimCommands {}
 771
 772fn commands(cx: &App) -> &Vec<VimCommand> {
 773    static COMMANDS: OnceLock<VimCommands> = OnceLock::new();
 774    &COMMANDS
 775        .get_or_init(|| VimCommands(generate_commands(cx)))
 776        .0
 777}
 778
 779fn act_on_range(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
 780    Some(
 781        WithRange {
 782            restore_selection: true,
 783            range: range.clone(),
 784            action: WrappedAction(action),
 785        }
 786        .boxed_clone(),
 787    )
 788}
 789
 790fn select_range(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
 791    Some(
 792        WithRange {
 793            restore_selection: false,
 794            range: range.clone(),
 795            action: WrappedAction(action),
 796        }
 797        .boxed_clone(),
 798    )
 799}
 800
 801fn wrap_count(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
 802    range.as_count().map(|count| {
 803        WithCount {
 804            count,
 805            action: WrappedAction(action),
 806        }
 807        .boxed_clone()
 808    })
 809}
 810
 811pub fn command_interceptor(mut input: &str, cx: &App) -> Option<CommandInterceptResult> {
 812    // NOTE: We also need to support passing arguments to commands like :w
 813    // (ideally with filename autocompletion).
 814    while input.starts_with(':') {
 815        input = &input[1..];
 816    }
 817
 818    let (range, query) = VimCommand::parse_range(input);
 819    let range_prefix = input[0..(input.len() - query.len())].to_string();
 820    let query = query.as_str().trim();
 821
 822    let action = if range.is_some() && query.is_empty() {
 823        Some(
 824            GoToLine {
 825                range: range.clone().unwrap(),
 826            }
 827            .boxed_clone(),
 828        )
 829    } else if query.starts_with('/') || query.starts_with('?') {
 830        Some(
 831            FindCommand {
 832                query: query[1..].to_string(),
 833                backwards: query.starts_with('?'),
 834            }
 835            .boxed_clone(),
 836        )
 837    } else if query.starts_with('s') {
 838        let mut substitute = "substitute".chars().peekable();
 839        let mut query = query.chars().peekable();
 840        while substitute
 841            .peek()
 842            .is_some_and(|char| Some(char) == query.peek())
 843        {
 844            substitute.next();
 845            query.next();
 846        }
 847        if let Some(replacement) = Replacement::parse(query) {
 848            let range = range.clone().unwrap_or(CommandRange {
 849                start: Position::CurrentLine { offset: 0 },
 850                end: None,
 851            });
 852            Some(ReplaceCommand { replacement, range }.boxed_clone())
 853        } else {
 854            None
 855        }
 856    } else if query.starts_with('g') || query.starts_with('v') {
 857        let mut global = "global".chars().peekable();
 858        let mut query = query.chars().peekable();
 859        let mut invert = false;
 860        if query.peek() == Some(&'v') {
 861            invert = true;
 862            query.next();
 863        }
 864        while global.peek().is_some_and(|char| Some(char) == query.peek()) {
 865            global.next();
 866            query.next();
 867        }
 868        if !invert && query.peek() == Some(&'!') {
 869            invert = true;
 870            query.next();
 871        }
 872        let range = range.clone().unwrap_or(CommandRange {
 873            start: Position::Line { row: 0, offset: 0 },
 874            end: Some(Position::LastLine { offset: 0 }),
 875        });
 876        if let Some(action) = OnMatchingLines::parse(query, invert, range, cx) {
 877            Some(action.boxed_clone())
 878        } else {
 879            None
 880        }
 881    } else if query.contains('!') {
 882        ShellExec::parse(query, range.clone())
 883    } else {
 884        None
 885    };
 886    if let Some(action) = action {
 887        let string = input.to_string();
 888        let positions = generate_positions(&string, &(range_prefix + query));
 889        return Some(CommandInterceptResult {
 890            action,
 891            string,
 892            positions,
 893        });
 894    }
 895
 896    for command in commands(cx).iter() {
 897        if let Some(action) = command.parse(query, &range, cx) {
 898            let mut string = ":".to_owned() + &range_prefix + command.prefix + command.suffix;
 899            if query.ends_with('!') {
 900                string.push('!');
 901            }
 902            let positions = generate_positions(&string, &(range_prefix + query));
 903
 904            return Some(CommandInterceptResult {
 905                action,
 906                string,
 907                positions,
 908            });
 909        }
 910    }
 911    None
 912}
 913
 914fn generate_positions(string: &str, query: &str) -> Vec<usize> {
 915    let mut positions = Vec::new();
 916    let mut chars = query.chars();
 917
 918    let Some(mut current) = chars.next() else {
 919        return positions;
 920    };
 921
 922    for (i, c) in string.char_indices() {
 923        if c == current {
 924            positions.push(i);
 925            if let Some(c) = chars.next() {
 926                current = c;
 927            } else {
 928                break;
 929            }
 930        }
 931    }
 932
 933    positions
 934}
 935
 936#[derive(Debug, PartialEq, Clone)]
 937pub(crate) struct OnMatchingLines {
 938    range: CommandRange,
 939    search: String,
 940    action: WrappedAction,
 941    invert: bool,
 942}
 943
 944impl OnMatchingLines {
 945    // convert a vim query into something more usable by zed.
 946    // we don't attempt to fully convert between the two regex syntaxes,
 947    // but we do flip \( and \) to ( and ) (and vice-versa) in the pattern,
 948    // and convert \0..\9 to $0..$9 in the replacement so that common idioms work.
 949    pub(crate) fn parse(
 950        mut chars: Peekable<Chars>,
 951        invert: bool,
 952        range: CommandRange,
 953        cx: &App,
 954    ) -> Option<Self> {
 955        let delimiter = chars.next().filter(|c| {
 956            !c.is_alphanumeric() && *c != '"' && *c != '|' && *c != '\'' && *c != '!'
 957        })?;
 958
 959        let mut search = String::new();
 960        let mut escaped = false;
 961
 962        while let Some(c) = chars.next() {
 963            if escaped {
 964                escaped = false;
 965                // unescape escaped parens
 966                if c != '(' && c != ')' && c != delimiter {
 967                    search.push('\\')
 968                }
 969                search.push(c)
 970            } else if c == '\\' {
 971                escaped = true;
 972            } else if c == delimiter {
 973                break;
 974            } else {
 975                // escape unescaped parens
 976                if c == '(' || c == ')' {
 977                    search.push('\\')
 978                }
 979                search.push(c)
 980            }
 981        }
 982
 983        let command: String = chars.collect();
 984
 985        let action = WrappedAction(command_interceptor(&command, cx)?.action);
 986
 987        Some(Self {
 988            range,
 989            search,
 990            invert,
 991            action,
 992        })
 993    }
 994
 995    pub fn run(&self, vim: &mut Vim, window: &mut Window, cx: &mut Context<Vim>) {
 996        let result = vim.update_editor(window, cx, |vim, editor, window, cx| {
 997            self.range.buffer_range(vim, editor, window, cx)
 998        });
 999
1000        let range = match result {
1001            None => return,
1002            Some(e @ Err(_)) => {
1003                let Some(workspace) = vim.workspace(window) else {
1004                    return;
1005                };
1006                workspace.update(cx, |workspace, cx| {
1007                    e.notify_err(workspace, cx);
1008                });
1009                return;
1010            }
1011            Some(Ok(result)) => result,
1012        };
1013
1014        let mut action = self.action.boxed_clone();
1015        let mut last_pattern = self.search.clone();
1016
1017        let mut regexes = match Regex::new(&self.search) {
1018            Ok(regex) => vec![(regex, !self.invert)],
1019            e @ Err(_) => {
1020                let Some(workspace) = vim.workspace(window) else {
1021                    return;
1022                };
1023                workspace.update(cx, |workspace, cx| {
1024                    e.notify_err(workspace, cx);
1025                });
1026                return;
1027            }
1028        };
1029        while let Some(inner) = action
1030            .boxed_clone()
1031            .as_any()
1032            .downcast_ref::<OnMatchingLines>()
1033        {
1034            let Some(regex) = Regex::new(&inner.search).ok() else {
1035                break;
1036            };
1037            last_pattern = inner.search.clone();
1038            action = inner.action.boxed_clone();
1039            regexes.push((regex, !inner.invert))
1040        }
1041
1042        if let Some(pane) = vim.pane(window, cx) {
1043            pane.update(cx, |pane, cx| {
1044                if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()
1045                {
1046                    search_bar.update(cx, |search_bar, cx| {
1047                        if search_bar.show(window, cx) {
1048                            let _ = search_bar.search(
1049                                &last_pattern,
1050                                Some(SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE),
1051                                window,
1052                                cx,
1053                            );
1054                        }
1055                    });
1056                }
1057            });
1058        };
1059
1060        vim.update_editor(window, cx, |_, editor, window, cx| {
1061            let snapshot = editor.snapshot(window, cx);
1062            let mut row = range.start.0;
1063
1064            let point_range = Point::new(range.start.0, 0)
1065                ..snapshot
1066                    .buffer_snapshot
1067                    .clip_point(Point::new(range.end.0 + 1, 0), Bias::Left);
1068            cx.spawn_in(window, |editor, mut cx| async move {
1069                let new_selections = cx
1070                    .background_executor()
1071                    .spawn(async move {
1072                        let mut line = String::new();
1073                        let mut new_selections = Vec::new();
1074                        let chunks = snapshot
1075                            .buffer_snapshot
1076                            .text_for_range(point_range)
1077                            .chain(["\n"]);
1078
1079                        for chunk in chunks {
1080                            for (newline_ix, text) in chunk.split('\n').enumerate() {
1081                                if newline_ix > 0 {
1082                                    if regexes.iter().all(|(regex, should_match)| {
1083                                        regex.is_match(&line) == *should_match
1084                                    }) {
1085                                        new_selections
1086                                            .push(Point::new(row, 0).to_display_point(&snapshot))
1087                                    }
1088                                    row += 1;
1089                                    line.clear();
1090                                }
1091                                line.push_str(text)
1092                            }
1093                        }
1094
1095                        new_selections
1096                    })
1097                    .await;
1098
1099                if new_selections.is_empty() {
1100                    return;
1101                }
1102                editor
1103                    .update_in(&mut cx, |editor, window, cx| {
1104                        editor.start_transaction_at(Instant::now(), window, cx);
1105                        editor.change_selections(None, window, cx, |s| {
1106                            s.replace_cursors_with(|_| new_selections);
1107                        });
1108                        window.dispatch_action(action, cx);
1109                        cx.defer_in(window, move |editor, window, cx| {
1110                            let newest = editor.selections.newest::<Point>(cx).clone();
1111                            editor.change_selections(None, window, cx, |s| {
1112                                s.select(vec![newest]);
1113                            });
1114                            editor.end_transaction_at(Instant::now(), cx);
1115                        })
1116                    })
1117                    .ok();
1118            })
1119            .detach();
1120        });
1121    }
1122}
1123
1124#[derive(Clone, Debug, PartialEq)]
1125pub struct ShellExec {
1126    command: String,
1127    range: Option<CommandRange>,
1128    is_read: bool,
1129}
1130
1131impl Vim {
1132    pub fn cancel_running_command(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1133        if self.running_command.take().is_some() {
1134            self.update_editor(window, cx, |_, editor, window, cx| {
1135                editor.transact(window, cx, |editor, _window, _cx| {
1136                    editor.clear_row_highlights::<ShellExec>();
1137                })
1138            });
1139        }
1140    }
1141
1142    fn prepare_shell_command(
1143        &mut self,
1144        command: &str,
1145        window: &mut Window,
1146        cx: &mut Context<Self>,
1147    ) -> String {
1148        let mut ret = String::new();
1149        // N.B. non-standard escaping rules:
1150        // * !echo % => "echo README.md"
1151        // * !echo \% => "echo %"
1152        // * !echo \\% => echo \%
1153        // * !echo \\\% => echo \\%
1154        for c in command.chars() {
1155            if c != '%' && c != '!' {
1156                ret.push(c);
1157                continue;
1158            } else if ret.chars().last() == Some('\\') {
1159                ret.pop();
1160                ret.push(c);
1161                continue;
1162            }
1163            match c {
1164                '%' => {
1165                    self.update_editor(window, cx, |_, editor, _window, cx| {
1166                        if let Some((_, buffer, _)) = editor.active_excerpt(cx) {
1167                            if let Some(file) = buffer.read(cx).file() {
1168                                if let Some(local) = file.as_local() {
1169                                    if let Some(str) = local.path().to_str() {
1170                                        ret.push_str(str)
1171                                    }
1172                                }
1173                            }
1174                        }
1175                    });
1176                }
1177                '!' => {
1178                    if let Some(command) = &self.last_command {
1179                        ret.push_str(command)
1180                    }
1181                }
1182                _ => {}
1183            }
1184        }
1185        self.last_command = Some(ret.clone());
1186        ret
1187    }
1188
1189    pub fn shell_command_motion(
1190        &mut self,
1191        motion: Motion,
1192        times: Option<usize>,
1193        window: &mut Window,
1194        cx: &mut Context<Vim>,
1195    ) {
1196        self.stop_recording(cx);
1197        let Some(workspace) = self.workspace(window) else {
1198            return;
1199        };
1200        let command = self.update_editor(window, cx, |_, editor, window, cx| {
1201            let snapshot = editor.snapshot(window, cx);
1202            let start = editor.selections.newest_display(cx);
1203            let text_layout_details = editor.text_layout_details(window);
1204            let mut range = motion
1205                .range(&snapshot, start.clone(), times, false, &text_layout_details)
1206                .unwrap_or(start.range());
1207            if range.start != start.start {
1208                editor.change_selections(None, window, cx, |s| {
1209                    s.select_ranges([
1210                        range.start.to_point(&snapshot)..range.start.to_point(&snapshot)
1211                    ]);
1212                })
1213            }
1214            if range.end.row() > range.start.row() && range.end.column() != 0 {
1215                *range.end.row_mut() -= 1
1216            }
1217            if range.end.row() == range.start.row() {
1218                ".!".to_string()
1219            } else {
1220                format!(".,.+{}!", (range.end.row() - range.start.row()).0)
1221            }
1222        });
1223        if let Some(command) = command {
1224            workspace.update(cx, |workspace, cx| {
1225                command_palette::CommandPalette::toggle(workspace, &command, window, cx);
1226            });
1227        }
1228    }
1229
1230    pub fn shell_command_object(
1231        &mut self,
1232        object: Object,
1233        around: bool,
1234        window: &mut Window,
1235        cx: &mut Context<Vim>,
1236    ) {
1237        self.stop_recording(cx);
1238        let Some(workspace) = self.workspace(window) else {
1239            return;
1240        };
1241        let command = self.update_editor(window, cx, |_, editor, window, cx| {
1242            let snapshot = editor.snapshot(window, cx);
1243            let start = editor.selections.newest_display(cx);
1244            let range = object
1245                .range(&snapshot, start.clone(), around)
1246                .unwrap_or(start.range());
1247            if range.start != start.start {
1248                editor.change_selections(None, window, cx, |s| {
1249                    s.select_ranges([
1250                        range.start.to_point(&snapshot)..range.start.to_point(&snapshot)
1251                    ]);
1252                })
1253            }
1254            if range.end.row() == range.start.row() {
1255                ".!".to_string()
1256            } else {
1257                format!(".,.+{}!", (range.end.row() - range.start.row()).0)
1258            }
1259        });
1260        if let Some(command) = command {
1261            workspace.update(cx, |workspace, cx| {
1262                command_palette::CommandPalette::toggle(workspace, &command, window, cx);
1263            });
1264        }
1265    }
1266}
1267
1268impl ShellExec {
1269    pub fn parse(query: &str, range: Option<CommandRange>) -> Option<Box<dyn Action>> {
1270        let (before, after) = query.split_once('!')?;
1271        let before = before.trim();
1272
1273        if !"read".starts_with(before) {
1274            return None;
1275        }
1276
1277        Some(
1278            ShellExec {
1279                command: after.trim().to_string(),
1280                range,
1281                is_read: !before.is_empty(),
1282            }
1283            .boxed_clone(),
1284        )
1285    }
1286
1287    pub fn run(&self, vim: &mut Vim, window: &mut Window, cx: &mut Context<Vim>) {
1288        let Some(workspace) = vim.workspace(window) else {
1289            return;
1290        };
1291
1292        let project = workspace.read(cx).project().clone();
1293        let command = vim.prepare_shell_command(&self.command, window, cx);
1294
1295        if self.range.is_none() && !self.is_read {
1296            workspace.update(cx, |workspace, cx| {
1297                let project = workspace.project().read(cx);
1298                let cwd = project.first_project_directory(cx);
1299                let shell = project.terminal_settings(&cwd, cx).shell.clone();
1300                cx.emit(workspace::Event::SpawnTask {
1301                    action: Box::new(SpawnInTerminal {
1302                        id: TaskId("vim".to_string()),
1303                        full_label: self.command.clone(),
1304                        label: self.command.clone(),
1305                        command: command.clone(),
1306                        args: Vec::new(),
1307                        command_label: self.command.clone(),
1308                        cwd,
1309                        env: HashMap::default(),
1310                        use_new_terminal: true,
1311                        allow_concurrent_runs: true,
1312                        reveal: RevealStrategy::NoFocus,
1313                        reveal_target: RevealTarget::Dock,
1314                        hide: HideStrategy::Never,
1315                        shell,
1316                        show_summary: false,
1317                        show_command: false,
1318                    }),
1319                });
1320            });
1321            return;
1322        };
1323
1324        let mut input_snapshot = None;
1325        let mut input_range = None;
1326        let mut needs_newline_prefix = false;
1327        vim.update_editor(window, cx, |vim, editor, window, cx| {
1328            let snapshot = editor.buffer().read(cx).snapshot(cx);
1329            let range = if let Some(range) = self.range.clone() {
1330                let Some(range) = range.buffer_range(vim, editor, window, cx).log_err() else {
1331                    return;
1332                };
1333                Point::new(range.start.0, 0)
1334                    ..snapshot.clip_point(Point::new(range.end.0 + 1, 0), Bias::Right)
1335            } else {
1336                let mut end = editor.selections.newest::<Point>(cx).range().end;
1337                end = snapshot.clip_point(Point::new(end.row + 1, 0), Bias::Right);
1338                needs_newline_prefix = end == snapshot.max_point();
1339                end..end
1340            };
1341            if self.is_read {
1342                input_range =
1343                    Some(snapshot.anchor_after(range.end)..snapshot.anchor_after(range.end));
1344            } else {
1345                input_range =
1346                    Some(snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end));
1347            }
1348            editor.highlight_rows::<ShellExec>(
1349                input_range.clone().unwrap(),
1350                cx.theme().status().unreachable_background,
1351                false,
1352                cx,
1353            );
1354
1355            if !self.is_read {
1356                input_snapshot = Some(snapshot)
1357            }
1358        });
1359
1360        let Some(range) = input_range else { return };
1361
1362        let mut process = project.read(cx).exec_in_shell(command, cx);
1363        process.stdout(Stdio::piped());
1364        process.stderr(Stdio::piped());
1365
1366        if input_snapshot.is_some() {
1367            process.stdin(Stdio::piped());
1368        } else {
1369            process.stdin(Stdio::null());
1370        };
1371
1372        // https://registerspill.thorstenball.com/p/how-to-lose-control-of-your-shell
1373        //
1374        // safety: code in pre_exec should be signal safe.
1375        // https://man7.org/linux/man-pages/man7/signal-safety.7.html
1376        #[cfg(not(target_os = "windows"))]
1377        unsafe {
1378            use std::os::unix::process::CommandExt;
1379            process.pre_exec(|| {
1380                libc::setsid();
1381                Ok(())
1382            });
1383        };
1384        let is_read = self.is_read;
1385
1386        let task = cx.spawn_in(window, |vim, mut cx| async move {
1387            let Some(mut running) = process.spawn().log_err() else {
1388                vim.update_in(&mut cx, |vim, window, cx| {
1389                    vim.cancel_running_command(window, cx);
1390                })
1391                .log_err();
1392                return;
1393            };
1394
1395            if let Some(mut stdin) = running.stdin.take() {
1396                if let Some(snapshot) = input_snapshot {
1397                    let range = range.clone();
1398                    cx.background_executor()
1399                        .spawn(async move {
1400                            for chunk in snapshot.text_for_range(range) {
1401                                if stdin.write_all(chunk.as_bytes()).log_err().is_none() {
1402                                    return;
1403                                }
1404                            }
1405                            stdin.flush().log_err();
1406                        })
1407                        .detach();
1408                }
1409            };
1410
1411            let output = cx
1412                .background_executor()
1413                .spawn(async move { running.wait_with_output() })
1414                .await;
1415
1416            let Some(output) = output.log_err() else {
1417                vim.update_in(&mut cx, |vim, window, cx| {
1418                    vim.cancel_running_command(window, cx);
1419                })
1420                .log_err();
1421                return;
1422            };
1423            let mut text = String::new();
1424            if needs_newline_prefix {
1425                text.push('\n');
1426            }
1427            text.push_str(&String::from_utf8_lossy(&output.stdout));
1428            text.push_str(&String::from_utf8_lossy(&output.stderr));
1429            if !text.is_empty() && text.chars().last() != Some('\n') {
1430                text.push('\n');
1431            }
1432
1433            vim.update_in(&mut cx, |vim, window, cx| {
1434                vim.update_editor(window, cx, |_, editor, window, cx| {
1435                    editor.transact(window, cx, |editor, window, cx| {
1436                        editor.edit([(range.clone(), text)], cx);
1437                        let snapshot = editor.buffer().read(cx).snapshot(cx);
1438                        editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
1439                            let point = if is_read {
1440                                let point = range.end.to_point(&snapshot);
1441                                Point::new(point.row.saturating_sub(1), 0)
1442                            } else {
1443                                let point = range.start.to_point(&snapshot);
1444                                Point::new(point.row, 0)
1445                            };
1446                            s.select_ranges([point..point]);
1447                        })
1448                    })
1449                });
1450                vim.cancel_running_command(window, cx);
1451            })
1452            .log_err();
1453        });
1454        vim.running_command.replace(task);
1455    }
1456}
1457
1458#[cfg(test)]
1459mod test {
1460    use std::path::Path;
1461
1462    use crate::{
1463        state::Mode,
1464        test::{NeovimBackedTestContext, VimTestContext},
1465    };
1466    use editor::Editor;
1467    use gpui::{Context, TestAppContext};
1468    use indoc::indoc;
1469    use util::path;
1470    use workspace::Workspace;
1471
1472    #[gpui::test]
1473    async fn test_command_basics(cx: &mut TestAppContext) {
1474        let mut cx = NeovimBackedTestContext::new(cx).await;
1475
1476        cx.set_shared_state(indoc! {"
1477            ˇa
1478            b
1479            c"})
1480            .await;
1481
1482        cx.simulate_shared_keystrokes(": j enter").await;
1483
1484        // hack: our cursor positioning after a join command is wrong
1485        cx.simulate_shared_keystrokes("^").await;
1486        cx.shared_state().await.assert_eq(indoc! {
1487            "ˇa b
1488            c"
1489        });
1490    }
1491
1492    #[gpui::test]
1493    async fn test_command_goto(cx: &mut TestAppContext) {
1494        let mut cx = NeovimBackedTestContext::new(cx).await;
1495
1496        cx.set_shared_state(indoc! {"
1497            ˇa
1498            b
1499            c"})
1500            .await;
1501        cx.simulate_shared_keystrokes(": 3 enter").await;
1502        cx.shared_state().await.assert_eq(indoc! {"
1503            a
1504            b
1505            ˇc"});
1506    }
1507
1508    #[gpui::test]
1509    async fn test_command_replace(cx: &mut TestAppContext) {
1510        let mut cx = NeovimBackedTestContext::new(cx).await;
1511
1512        cx.set_shared_state(indoc! {"
1513            ˇa
1514            b
1515            b
1516            c"})
1517            .await;
1518        cx.simulate_shared_keystrokes(": % s / b / d enter").await;
1519        cx.shared_state().await.assert_eq(indoc! {"
1520            a
1521            d
1522            ˇd
1523            c"});
1524        cx.simulate_shared_keystrokes(": % s : . : \\ 0 \\ 0 enter")
1525            .await;
1526        cx.shared_state().await.assert_eq(indoc! {"
1527            aa
1528            dd
1529            dd
1530            ˇcc"});
1531        cx.simulate_shared_keystrokes("k : s / d d / e e enter")
1532            .await;
1533        cx.shared_state().await.assert_eq(indoc! {"
1534            aa
1535            dd
1536            ˇee
1537            cc"});
1538    }
1539
1540    #[gpui::test]
1541    async fn test_command_search(cx: &mut TestAppContext) {
1542        let mut cx = NeovimBackedTestContext::new(cx).await;
1543
1544        cx.set_shared_state(indoc! {"
1545                ˇa
1546                b
1547                a
1548                c"})
1549            .await;
1550        cx.simulate_shared_keystrokes(": / b enter").await;
1551        cx.shared_state().await.assert_eq(indoc! {"
1552                a
1553                ˇb
1554                a
1555                c"});
1556        cx.simulate_shared_keystrokes(": ? a enter").await;
1557        cx.shared_state().await.assert_eq(indoc! {"
1558                ˇa
1559                b
1560                a
1561                c"});
1562    }
1563
1564    #[gpui::test]
1565    async fn test_command_write(cx: &mut TestAppContext) {
1566        let mut cx = VimTestContext::new(cx, true).await;
1567        let path = Path::new(path!("/root/dir/file.rs"));
1568        let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
1569
1570        cx.simulate_keystrokes("i @ escape");
1571        cx.simulate_keystrokes(": w enter");
1572
1573        assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "@\n");
1574
1575        fs.as_fake().insert_file(path, b"oops\n".to_vec()).await;
1576
1577        // conflict!
1578        cx.simulate_keystrokes("i @ escape");
1579        cx.simulate_keystrokes(": w enter");
1580        assert!(cx.has_pending_prompt());
1581        // "Cancel"
1582        cx.simulate_prompt_answer(0);
1583        assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "oops\n");
1584        assert!(!cx.has_pending_prompt());
1585        // force overwrite
1586        cx.simulate_keystrokes(": w ! enter");
1587        assert!(!cx.has_pending_prompt());
1588        assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "@@\n");
1589    }
1590
1591    #[gpui::test]
1592    async fn test_command_quit(cx: &mut TestAppContext) {
1593        let mut cx = VimTestContext::new(cx, true).await;
1594
1595        cx.simulate_keystrokes(": n e w enter");
1596        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
1597        cx.simulate_keystrokes(": q enter");
1598        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 1));
1599        cx.simulate_keystrokes(": n e w enter");
1600        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
1601        cx.simulate_keystrokes(": q a enter");
1602        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 0));
1603    }
1604
1605    #[gpui::test]
1606    async fn test_offsets(cx: &mut TestAppContext) {
1607        let mut cx = NeovimBackedTestContext::new(cx).await;
1608
1609        cx.set_shared_state("ˇ1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n")
1610            .await;
1611
1612        cx.simulate_shared_keystrokes(": + enter").await;
1613        cx.shared_state()
1614            .await
1615            .assert_eq("1\nˇ2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n");
1616
1617        cx.simulate_shared_keystrokes(": 1 0 - enter").await;
1618        cx.shared_state()
1619            .await
1620            .assert_eq("1\n2\n3\n4\n5\n6\n7\n8\nˇ9\n10\n11\n");
1621
1622        cx.simulate_shared_keystrokes(": . - 2 enter").await;
1623        cx.shared_state()
1624            .await
1625            .assert_eq("1\n2\n3\n4\n5\n6\nˇ7\n8\n9\n10\n11\n");
1626
1627        cx.simulate_shared_keystrokes(": % enter").await;
1628        cx.shared_state()
1629            .await
1630            .assert_eq("1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\nˇ");
1631    }
1632
1633    #[gpui::test]
1634    async fn test_command_ranges(cx: &mut TestAppContext) {
1635        let mut cx = NeovimBackedTestContext::new(cx).await;
1636
1637        cx.set_shared_state("ˇ1\n2\n3\n4\n4\n3\n2\n1").await;
1638
1639        cx.simulate_shared_keystrokes(": 2 , 4 d enter").await;
1640        cx.shared_state().await.assert_eq("1\nˇ4\n3\n2\n1");
1641
1642        cx.simulate_shared_keystrokes(": 2 , 4 s o r t enter").await;
1643        cx.shared_state().await.assert_eq("1\nˇ2\n3\n4\n1");
1644
1645        cx.simulate_shared_keystrokes(": 2 , 4 j o i n enter").await;
1646        cx.shared_state().await.assert_eq("1\nˇ2 3 4\n1");
1647    }
1648
1649    #[gpui::test]
1650    async fn test_command_visual_replace(cx: &mut TestAppContext) {
1651        let mut cx = NeovimBackedTestContext::new(cx).await;
1652
1653        cx.set_shared_state("ˇ1\n2\n3\n4\n4\n3\n2\n1").await;
1654
1655        cx.simulate_shared_keystrokes("v 2 j : s / . / k enter")
1656            .await;
1657        cx.shared_state().await.assert_eq("k\nk\nˇk\n4\n4\n3\n2\n1");
1658    }
1659
1660    fn assert_active_item(
1661        workspace: &mut Workspace,
1662        expected_path: &str,
1663        expected_text: &str,
1664        cx: &mut Context<Workspace>,
1665    ) {
1666        let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();
1667
1668        let buffer = active_editor
1669            .read(cx)
1670            .buffer()
1671            .read(cx)
1672            .as_singleton()
1673            .unwrap();
1674
1675        let text = buffer.read(cx).text();
1676        let file = buffer.read(cx).file().unwrap();
1677        let file_path = file.as_local().unwrap().abs_path(cx);
1678
1679        assert_eq!(text, expected_text);
1680        assert_eq!(file_path, Path::new(expected_path));
1681    }
1682
1683    #[gpui::test]
1684    async fn test_command_gf(cx: &mut TestAppContext) {
1685        let mut cx = VimTestContext::new(cx, true).await;
1686
1687        // Assert base state, that we're in /root/dir/file.rs
1688        cx.workspace(|workspace, _, cx| {
1689            assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
1690        });
1691
1692        // Insert a new file
1693        let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
1694        fs.as_fake()
1695            .insert_file(
1696                path!("/root/dir/file2.rs"),
1697                "This is file2.rs".as_bytes().to_vec(),
1698            )
1699            .await;
1700        fs.as_fake()
1701            .insert_file(
1702                path!("/root/dir/file3.rs"),
1703                "go to file3".as_bytes().to_vec(),
1704            )
1705            .await;
1706
1707        // Put the path to the second file into the currently open buffer
1708        cx.set_state(indoc! {"go to fiˇle2.rs"}, Mode::Normal);
1709
1710        // Go to file2.rs
1711        cx.simulate_keystrokes("g f");
1712
1713        // We now have two items
1714        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
1715        cx.workspace(|workspace, _, cx| {
1716            assert_active_item(
1717                workspace,
1718                path!("/root/dir/file2.rs"),
1719                "This is file2.rs",
1720                cx,
1721            );
1722        });
1723
1724        // Update editor to point to `file2.rs`
1725        cx.editor =
1726            cx.workspace(|workspace, _, cx| workspace.active_item_as::<Editor>(cx).unwrap());
1727
1728        // Put the path to the third file into the currently open buffer,
1729        // but remove its suffix, because we want that lookup to happen automatically.
1730        cx.set_state(indoc! {"go to fiˇle3"}, Mode::Normal);
1731
1732        // Go to file3.rs
1733        cx.simulate_keystrokes("g f");
1734
1735        // We now have three items
1736        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 3));
1737        cx.workspace(|workspace, _, cx| {
1738            assert_active_item(workspace, path!("/root/dir/file3.rs"), "go to file3", cx);
1739        });
1740    }
1741
1742    #[gpui::test]
1743    async fn test_command_matching_lines(cx: &mut TestAppContext) {
1744        let mut cx = NeovimBackedTestContext::new(cx).await;
1745
1746        cx.set_shared_state(indoc! {"
1747            ˇa
1748            b
1749            a
1750            b
1751            a
1752        "})
1753            .await;
1754
1755        cx.simulate_shared_keystrokes(":").await;
1756        cx.simulate_shared_keystrokes("g / a / d").await;
1757        cx.simulate_shared_keystrokes("enter").await;
1758
1759        cx.shared_state().await.assert_eq(indoc! {"
1760            b
1761            b
1762            ˇ"});
1763
1764        cx.simulate_shared_keystrokes("u").await;
1765
1766        cx.shared_state().await.assert_eq(indoc! {"
1767            ˇa
1768            b
1769            a
1770            b
1771            a
1772        "});
1773
1774        cx.simulate_shared_keystrokes(":").await;
1775        cx.simulate_shared_keystrokes("v / a / d").await;
1776        cx.simulate_shared_keystrokes("enter").await;
1777
1778        cx.shared_state().await.assert_eq(indoc! {"
1779            a
1780            a
1781            ˇa"});
1782    }
1783}