command.rs

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