Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:30:24.844Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

command.rs

3634 lines · 126.0 KB · rust
1use anyhow::{Result, anyhow};
2use collections::{HashMap, HashSet};
3use command_palette_hooks::{CommandInterceptItem, CommandInterceptResult};
4use editor::{
5    Bias, Editor, EditorSettings, SelectionEffects, ToPoint,
6    actions::{SortLinesCaseInsensitive, SortLinesCaseSensitive},
7    display_map::ToDisplayPoint,
8};
9use futures::AsyncWriteExt as _;
10use gpui::{
11    Action, App, AppContext as _, Context, Global, Keystroke, Task, TaskExt, WeakEntity, Window,
12    actions,
13};
14use itertools::Itertools;
15use language::Point;
16use multi_buffer::MultiBufferRow;
17use project::ProjectPath;
18use regex::Regex;
19use schemars::JsonSchema;
20use search::{BufferSearchBar, SearchOptions};
21use serde::Deserialize;
22use settings::{Settings, SettingsStore};
23use std::{
24    iter::Peekable,
25    ops::{Deref, Range},
26    path::{Path, PathBuf},
27    process::Stdio,
28    str::Chars,
29    sync::OnceLock,
30    time::Instant,
31};
32use task::{HideStrategy, RevealStrategy, SaveStrategy, Shell, SpawnInTerminal, TaskId};
33use ui::ActiveTheme;
34use util::{
35    ResultExt,
36    paths::PathStyle,
37    rel_path::{RelPath, RelPathBuf},
38};
39use workspace::{Item, SaveIntent, Workspace, notifications::NotifyResultExt};
40use workspace::{SplitDirection, notifications::DetachAndPromptErr};
41use zed_actions::{OpenDocs, RevealTarget};
42
43use crate::{
44    ToggleMarksView, ToggleRegistersView, Vim, VimSettings,
45    motion::{EndOfDocument, Motion, MotionKind, StartOfDocument},
46    normal::{
47        JoinLines,
48        search::{FindCommand, ReplaceCommand, Replacement},
49    },
50    object::Object,
51    rewrap::Rewrap,
52    state::{Mark, Mode},
53    visual::VisualDeleteLine,
54};
55
56/// Goes to the specified line number in the editor.
57#[derive(Clone, Debug, PartialEq, Action)]
58#[action(namespace = vim, no_json, no_register)]
59pub struct GoToLine {
60    range: CommandRange,
61}
62
63/// Yanks (copies) text based on the specified range.
64#[derive(Clone, Debug, PartialEq, Action)]
65#[action(namespace = vim, no_json, no_register)]
66pub struct YankCommand {
67    range: CommandRange,
68}
69
70/// Executes a command with the specified range.
71#[derive(Clone, Debug, PartialEq, Action)]
72#[action(namespace = vim, no_json, no_register)]
73pub struct WithRange {
74    restore_selection: bool,
75    range: CommandRange,
76    action: WrappedAction,
77}
78
79/// Executes a command with the specified count.
80#[derive(Clone, Debug, PartialEq, Action)]
81#[action(namespace = vim, no_json, no_register)]
82pub struct WithCount {
83    count: u32,
84    action: WrappedAction,
85}
86
87#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
88pub enum VimOption {
89    Wrap(bool),
90    Number(bool),
91    RelativeNumber(bool),
92    IgnoreCase(bool),
93    GDefault(bool),
94}
95
96impl VimOption {
97    fn possible_commands(query: &str) -> Vec<CommandInterceptItem> {
98        let mut prefix_of_options = Vec::new();
99        let mut options = query.split(" ").collect::<Vec<_>>();
100        let prefix = options.pop().unwrap_or_default();
101        for option in options {
102            if let Some(opt) = Self::from(option) {
103                prefix_of_options.push(opt)
104            } else {
105                return vec![];
106            }
107        }
108
109        Self::possibilities(prefix)
110            .map(|possible| {
111                let mut options = prefix_of_options.clone();
112                options.push(possible);
113
114                CommandInterceptItem {
115                    string: format!(
116                        ":set {}",
117                        options.iter().map(|opt| opt.to_string()).join(" ")
118                    ),
119                    action: VimSet { options }.boxed_clone(),
120                    positions: vec![],
121                }
122            })
123            .collect()
124    }
125
126    fn possibilities(query: &str) -> impl Iterator<Item = Self> + '_ {
127        [
128            (None, VimOption::Wrap(true)),
129            (None, VimOption::Wrap(false)),
130            (None, VimOption::Number(true)),
131            (None, VimOption::Number(false)),
132            (None, VimOption::RelativeNumber(true)),
133            (None, VimOption::RelativeNumber(false)),
134            (Some("rnu"), VimOption::RelativeNumber(true)),
135            (Some("nornu"), VimOption::RelativeNumber(false)),
136            (None, VimOption::IgnoreCase(true)),
137            (None, VimOption::IgnoreCase(false)),
138            (Some("ic"), VimOption::IgnoreCase(true)),
139            (Some("noic"), VimOption::IgnoreCase(false)),
140            (None, VimOption::GDefault(true)),
141            (Some("gd"), VimOption::GDefault(true)),
142            (None, VimOption::GDefault(false)),
143            (Some("nogd"), VimOption::GDefault(false)),
144        ]
145        .into_iter()
146        .filter(move |(prefix, option)| prefix.unwrap_or(option.to_string()).starts_with(query))
147        .map(|(_, option)| option)
148    }
149
150    fn from(option: &str) -> Option<Self> {
151        match option {
152            "wrap" => Some(Self::Wrap(true)),
153            "nowrap" => Some(Self::Wrap(false)),
154
155            "number" => Some(Self::Number(true)),
156            "nu" => Some(Self::Number(true)),
157            "nonumber" => Some(Self::Number(false)),
158            "nonu" => Some(Self::Number(false)),
159
160            "relativenumber" => Some(Self::RelativeNumber(true)),
161            "rnu" => Some(Self::RelativeNumber(true)),
162            "norelativenumber" => Some(Self::RelativeNumber(false)),
163            "nornu" => Some(Self::RelativeNumber(false)),
164
165            "ignorecase" => Some(Self::IgnoreCase(true)),
166            "ic" => Some(Self::IgnoreCase(true)),
167            "noignorecase" => Some(Self::IgnoreCase(false)),
168            "noic" => Some(Self::IgnoreCase(false)),
169
170            "gdefault" => Some(Self::GDefault(true)),
171            "gd" => Some(Self::GDefault(true)),
172            "nogdefault" => Some(Self::GDefault(false)),
173            "nogd" => Some(Self::GDefault(false)),
174
175            _ => None,
176        }
177    }
178
179    fn to_string(&self) -> &'static str {
180        match self {
181            VimOption::Wrap(true) => "wrap",
182            VimOption::Wrap(false) => "nowrap",
183            VimOption::Number(true) => "number",
184            VimOption::Number(false) => "nonumber",
185            VimOption::RelativeNumber(true) => "relativenumber",
186            VimOption::RelativeNumber(false) => "norelativenumber",
187            VimOption::IgnoreCase(true) => "ignorecase",
188            VimOption::IgnoreCase(false) => "noignorecase",
189            VimOption::GDefault(true) => "gdefault",
190            VimOption::GDefault(false) => "nogdefault",
191        }
192    }
193}
194
195/// Sets vim options and configuration values.
196#[derive(Clone, PartialEq, Action)]
197#[action(namespace = vim, no_json, no_register)]
198pub struct VimSet {
199    options: Vec<VimOption>,
200}
201
202/// Saves the current file with optional save intent.
203#[derive(Clone, PartialEq, Action)]
204#[action(namespace = vim, no_json, no_register)]
205struct VimSave {
206    pub range: Option<CommandRange>,
207    pub save_intent: Option<SaveIntent>,
208    pub filename: String,
209}
210
211/// Deletes the specified marks from the editor.
212#[derive(Clone, PartialEq, Action)]
213#[action(namespace = vim, no_json, no_register)]
214struct VimSplit {
215    pub vertical: bool,
216    pub filename: String,
217}
218
219#[derive(Clone, PartialEq, Action)]
220#[action(namespace = vim, no_json, no_register)]
221enum DeleteMarks {
222    Marks(String),
223    AllLocal,
224}
225
226actions!(
227    vim,
228    [
229        /// Executes a command in visual mode.
230        VisualCommand,
231        /// Executes a command with a count prefix.
232        CountCommand,
233        /// Executes a shell command.
234        ShellCommand,
235        /// Indicates that an argument is required for the command.
236        ArgumentRequired
237    ]
238);
239
240/// Opens the specified file for editing.
241#[derive(Clone, PartialEq, Action)]
242#[action(namespace = vim, no_json, no_register)]
243struct VimEdit {
244    pub filename: String,
245}
246
247/// Pastes the specified file's contents.
248#[derive(Clone, PartialEq, Action)]
249#[action(namespace = vim, no_json, no_register)]
250struct VimRead {
251    pub range: Option<CommandRange>,
252    pub filename: String,
253}
254
255#[derive(Clone, PartialEq, Action)]
256#[action(namespace = vim, no_json, no_register)]
257struct VimNorm {
258    pub range: Option<CommandRange>,
259    pub command: String,
260    /// Places cursors at beginning of each given row.
261    /// Overrides given range and current cursor.
262    pub override_rows: Option<Vec<u32>>,
263}
264
265#[derive(Debug)]
266struct WrappedAction(Box<dyn Action>);
267
268impl PartialEq for WrappedAction {
269    fn eq(&self, other: &Self) -> bool {
270        self.0.partial_eq(&*other.0)
271    }
272}
273
274impl Clone for WrappedAction {
275    fn clone(&self) -> Self {
276        Self(self.0.boxed_clone())
277    }
278}
279
280impl Deref for WrappedAction {
281    type Target = dyn Action;
282    fn deref(&self) -> &dyn Action {
283        &*self.0
284    }
285}
286
287pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
288    Vim::action(editor, cx, |vim, action: &VimSet, _, cx| {
289        for option in action.options.iter() {
290            vim.update_editor(cx, |_, editor, cx| match option {
291                VimOption::Wrap(true) => {
292                    editor
293                        .set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
294                }
295                VimOption::Wrap(false) => {
296                    editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);
297                }
298                VimOption::Number(enabled) => {
299                    editor.set_show_line_numbers(*enabled, cx);
300                }
301                VimOption::RelativeNumber(enabled) => {
302                    editor.set_relative_line_number(Some(*enabled), cx);
303                }
304                VimOption::IgnoreCase(enabled) => {
305                    let mut settings = EditorSettings::get_global(cx).clone();
306                    settings.search.case_sensitive = !*enabled;
307                    SettingsStore::update(cx, |store, _| {
308                        store.override_global(settings);
309                    });
310                }
311                VimOption::GDefault(enabled) => {
312                    let mut settings = VimSettings::get_global(cx).clone();
313                    settings.gdefault = *enabled;
314
315                    SettingsStore::update(cx, |store, _| {
316                        store.override_global(settings);
317                    })
318                }
319            });
320        }
321    });
322    Vim::action(editor, cx, |vim, _: &VisualCommand, window, cx| {
323        let Some(workspace) = vim.workspace(window, cx) else {
324            return;
325        };
326        workspace.update(cx, |workspace, cx| {
327            command_palette::CommandPalette::toggle(workspace, "'<,'>", window, cx);
328        })
329    });
330
331    Vim::action(editor, cx, |vim, _: &ShellCommand, window, cx| {
332        let Some(workspace) = vim.workspace(window, cx) else {
333            return;
334        };
335        workspace.update(cx, |workspace, cx| {
336            command_palette::CommandPalette::toggle(workspace, "'<,'>!", window, cx);
337        })
338    });
339
340    Vim::action(editor, cx, |_, _: &ArgumentRequired, window, cx| {
341        let _ = window.prompt(
342            gpui::PromptLevel::Critical,
343            "Argument required",
344            None,
345            &["Cancel"],
346            cx,
347        );
348    });
349
350    Vim::action(editor, cx, |vim, action: &VimSave, window, cx| {
351        if let Some(range) = &action.range {
352            vim.update_editor(cx, |vim, editor, cx| {
353                let Some(range) = range.buffer_range(vim, editor, window, cx).ok() else {
354                    return;
355                };
356                let Some((line_ending, encoding, has_bom, text, whole_buffer)) = editor.buffer().update(cx, |multi, cx| {
357                    Some(multi.as_singleton()?.update(cx, |buffer, _| {
358                        (
359                            buffer.line_ending(),
360                            buffer.encoding(),
361                            buffer.has_bom(),
362                            buffer.as_rope().slice_rows(range.start.0..range.end.0 + 1),
363                            range.start.0 == 0 && range.end.0 + 1 >= buffer.row_count(),
364                        )
365                    }))
366                }) else {
367                    return;
368                };
369
370                let filename = action.filename.clone();
371                let filename = if filename.is_empty() {
372                    let Some(file) = editor
373                        .buffer()
374                        .read(cx)
375                        .as_singleton()
376                        .and_then(|buffer| buffer.read(cx).file())
377                    else {
378                        let _ = window.prompt(
379                            gpui::PromptLevel::Warning,
380                            "No file name",
381                            Some("Partial buffer write requires file name."),
382                            &["Cancel"],
383                            cx,
384                        );
385                        return;
386                    };
387                    file.path().display(file.path_style(cx)).to_string()
388                } else {
389                    filename
390                };
391
392                if action.filename.is_empty() {
393                    if whole_buffer {
394                        if let Some(workspace) = vim.workspace(window, cx) {
395                            workspace.update(cx, |workspace, cx| {
396                                workspace
397                                    .save_active_item(
398                                        action.save_intent.unwrap_or(SaveIntent::Save),
399                                        window,
400                                        cx,
401                                    )
402                                    .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
403                            });
404                        }
405                        return;
406                    }
407                    if Some(SaveIntent::Overwrite) != action.save_intent {
408                        let _ = window.prompt(
409                            gpui::PromptLevel::Warning,
410                            "Use ! to write partial buffer",
411                            Some("Overwriting the current file with selected buffer content requires '!'."),
412                            &["Cancel"],
413                            cx,
414                        );
415                        return;
416                    }
417                    editor.buffer().update(cx, |multi, cx| {
418                        if let Some(buffer) = multi.as_singleton() {
419                            buffer.update(cx, |buffer, _| buffer.set_conflict());
420                        }
421                    });
422                };
423
424                editor.project().unwrap().update(cx, |project, cx| {
425                    let worktree = project.visible_worktrees(cx).next().unwrap();
426
427                    worktree.update(cx, |worktree, cx| {
428                        let path_style = worktree.path_style();
429                        let Some(path) = RelPath::new(Path::new(&filename), path_style).ok() else {
430                            return;
431                        };
432
433                        let rx = (worktree.entry_for_path(&path).is_some() && Some(SaveIntent::Overwrite) != action.save_intent).then(|| {
434                            window.prompt(
435                                gpui::PromptLevel::Warning,
436                                &format!("{path:?} already exists. Do you want to replace it?"),
437                                Some(
438                                    "A file or folder with the same name already exists. Replacing it will overwrite its current contents.",
439                                ),
440                                &["Replace", "Cancel"],
441                                cx
442                            )
443                        });
444                        let filename = filename.clone();
445                        cx.spawn_in(window, async move |this, cx| {
446                            if let Some(rx) = rx
447                                && Ok(0) != rx.await
448                            {
449                                return;
450                            }
451
452                            let _ = this.update_in(cx, |worktree, window, cx| {
453                                let Some(path) = RelPath::new(Path::new(&filename), path_style).ok() else {
454                                    return;
455                                };
456                                worktree
457                                    .write_file(path.into_arc(), text.clone(), line_ending, encoding, has_bom, cx)
458                                    .detach_and_prompt_err("Failed to write lines", window, cx, |_, _, _| None);
459                            });
460                        })
461                        .detach();
462                    });
463                });
464            });
465            return;
466        }
467        if action.filename.is_empty() {
468            if let Some(workspace) = vim.workspace(window, cx) {
469                workspace.update(cx, |workspace, cx| {
470                    workspace
471                        .save_active_item(
472                            action.save_intent.unwrap_or(SaveIntent::Save),
473                            window,
474                            cx,
475                        )
476                        .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
477                });
478            }
479            return;
480        }
481        vim.update_editor(cx, |_, editor, cx| {
482            let Some(project) = editor.project().cloned() else {
483                return;
484            };
485            let Some(worktree) = project.read(cx).visible_worktrees(cx).next() else {
486                return;
487            };
488            let path_style = worktree.read(cx).path_style();
489            let Ok(project_path) =
490                RelPath::new(Path::new(&action.filename), path_style).map(|path| ProjectPath {
491                    worktree_id: worktree.read(cx).id(),
492                    path: path.into_arc(),
493                })
494            else {
495                // TODO implement save_as with absolute path
496                Task::ready(Err::<(), _>(anyhow!(
497                    "Cannot save buffer with absolute path"
498                )))
499                .detach_and_prompt_err(
500                    "Failed to save",
501                    window,
502                    cx,
503                    |_, _, _| None,
504                );
505                return;
506            };
507
508            if project.read(cx).entry_for_path(&project_path, cx).is_some()
509                && action.save_intent != Some(SaveIntent::Overwrite)
510            {
511                let answer = window.prompt(
512                    gpui::PromptLevel::Critical,
513                    &format!(
514                        "{} already exists. Do you want to replace it?",
515                        project_path.path.display(path_style)
516                    ),
517                    Some(
518                        "A file or folder with the same name already exists. \
519                        Replacing it will overwrite its current contents.",
520                    ),
521                    &["Replace", "Cancel"],
522                    cx,
523                );
524                cx.spawn_in(window, async move |editor, cx| {
525                    if answer.await.ok() != Some(0) {
526                        return;
527                    }
528
529                    let _ = editor.update_in(cx, |editor, window, cx| {
530                        editor
531                            .save_as(project, project_path, window, cx)
532                            .detach_and_prompt_err("Failed to :w", window, cx, |_, _, _| None);
533                    });
534                })
535                .detach();
536            } else {
537                editor
538                    .save_as(project, project_path, window, cx)
539                    .detach_and_prompt_err("Failed to :w", window, cx, |_, _, _| None);
540            }
541        });
542    });
543
544    Vim::action(editor, cx, |vim, action: &VimSplit, window, cx| {
545        let Some(workspace) = vim.workspace(window, cx) else {
546            return;
547        };
548
549        workspace.update(cx, |workspace, cx| {
550            let project = workspace.project().clone();
551            let Some(worktree) = project.read(cx).visible_worktrees(cx).next() else {
552                return;
553            };
554            let path_style = worktree.read(cx).path_style();
555            let Some(path) = RelPath::new(Path::new(&action.filename), path_style).log_err() else {
556                return;
557            };
558            let project_path = ProjectPath {
559                worktree_id: worktree.read(cx).id(),
560                path: path.into_arc(),
561            };
562
563            let direction = if action.vertical {
564                SplitDirection::vertical(cx)
565            } else {
566                SplitDirection::horizontal(cx)
567            };
568
569            workspace
570                .split_path_preview(project_path, false, Some(direction), window, cx)
571                .detach_and_log_err(cx);
572        })
573    });
574
575    Vim::action(editor, cx, |vim, action: &DeleteMarks, window, cx| {
576        fn err(s: String, window: &mut Window, cx: &mut Context<Editor>) {
577            let _ = window.prompt(
578                gpui::PromptLevel::Critical,
579                &format!("Invalid argument: {}", s),
580                None,
581                &["Cancel"],
582                cx,
583            );
584        }
585        vim.update_editor(cx, |vim, editor, cx| match action {
586            DeleteMarks::Marks(s) => {
587                if s.starts_with('-') || s.ends_with('-') || s.contains(['\'', '`']) {
588                    err(s.clone(), window, cx);
589                    return;
590                }
591
592                let to_delete = if s.len() < 3 {
593                    Some(s.clone())
594                } else {
595                    s.chars()
596                        .tuple_windows::<(_, _, _)>()
597                        .map(|(a, b, c)| {
598                            if b == '-' {
599                                if match a {
600                                    'a'..='z' => a <= c && c <= 'z',
601                                    'A'..='Z' => a <= c && c <= 'Z',
602                                    '0'..='9' => a <= c && c <= '9',
603                                    _ => false,
604                                } {
605                                    Some((a..=c).collect_vec())
606                                } else {
607                                    None
608                                }
609                            } else if a == '-' {
610                                if c == '-' { None } else { Some(vec![c]) }
611                            } else if c == '-' {
612                                if a == '-' { None } else { Some(vec![a]) }
613                            } else {
614                                Some(vec![a, b, c])
615                            }
616                        })
617                        .fold_options(HashSet::<char>::default(), |mut set, chars| {
618                            set.extend(chars.iter().copied());
619                            set
620                        })
621                        .map(|set| set.iter().collect::<String>())
622                };
623
624                let Some(to_delete) = to_delete else {
625                    err(s.clone(), window, cx);
626                    return;
627                };
628
629                for c in to_delete.chars().filter(|c| !c.is_whitespace()) {
630                    vim.delete_mark(c.to_string(), editor, window, cx);
631                }
632            }
633            DeleteMarks::AllLocal => {
634                for s in 'a'..='z' {
635                    vim.delete_mark(s.to_string(), editor, window, cx);
636                }
637            }
638        });
639    });
640
641    Vim::action(editor, cx, |vim, action: &VimEdit, window, cx| {
642        vim.update_editor(cx, |vim, editor, cx| {
643            let Some(workspace) = vim.workspace(window, cx) else {
644                return;
645            };
646            let Some(project) = editor.project().cloned() else {
647                return;
648            };
649            let Some(worktree) = project.read(cx).visible_worktrees(cx).next() else {
650                return;
651            };
652            let path_style = worktree.read(cx).path_style();
653            let Some(path) = RelPath::new(Path::new(&action.filename), path_style).log_err() else {
654                return;
655            };
656            let project_path = ProjectPath {
657                worktree_id: worktree.read(cx).id(),
658                path: path.into_arc(),
659            };
660
661            let _ = workspace.update(cx, |workspace, cx| {
662                workspace
663                    .open_path(project_path, None, true, window, cx)
664                    .detach_and_log_err(cx);
665            });
666        });
667    });
668
669    Vim::action(editor, cx, |vim, action: &VimRead, window, cx| {
670        vim.update_editor(cx, |vim, editor, cx| {
671            let snapshot = editor.buffer().read(cx).snapshot(cx);
672            let end = if let Some(range) = action.range.clone() {
673                let Some(multi_range) = range.buffer_range(vim, editor, window, cx).log_err()
674                else {
675                    return;
676                };
677
678                match &range.start {
679                    // inserting text above the first line uses the command ":0r {name}"
680                    Position::Line { row: 0, offset: 0 } if range.end.is_none() => {
681                        snapshot.clip_point(Point::new(0, 0), Bias::Right)
682                    }
683                    _ => snapshot.clip_point(Point::new(multi_range.end.0 + 1, 0), Bias::Right),
684                }
685            } else {
686                let end_row = editor
687                    .selections
688                    .newest::<Point>(&editor.display_snapshot(cx))
689                    .range()
690                    .end
691                    .row;
692                snapshot.clip_point(Point::new(end_row + 1, 0), Bias::Right)
693            };
694            let is_end_of_file = end == snapshot.max_point();
695            let edit_range = snapshot.anchor_before(end)..snapshot.anchor_before(end);
696
697            let mut text = if is_end_of_file {
698                String::from('\n')
699            } else {
700                String::new()
701            };
702
703            let mut task = None;
704            if action.filename.is_empty() {
705                text.push_str(
706                    &editor
707                        .buffer()
708                        .read(cx)
709                        .as_singleton()
710                        .map(|buffer| buffer.read(cx).text())
711                        .unwrap_or_default(),
712                );
713            } else {
714                if let Some(project) = editor.project().cloned() {
715                    project.update(cx, |project, cx| {
716                        let Some(worktree) = project.visible_worktrees(cx).next() else {
717                            return;
718                        };
719                        let path_style = worktree.read(cx).path_style();
720                        let Some(path) =
721                            RelPath::new(Path::new(&action.filename), path_style).log_err()
722                        else {
723                            return;
724                        };
725                        task =
726                            Some(worktree.update(cx, |worktree, cx| worktree.load_file(&path, cx)));
727                    });
728                } else {
729                    return;
730                }
731            };
732
733            cx.spawn_in(window, async move |editor, cx| {
734                if let Some(task) = task {
735                    text.push_str(
736                        &task
737                            .await
738                            .log_err()
739                            .map(|loaded_file| loaded_file.text)
740                            .unwrap_or_default(),
741                    );
742                }
743
744                if !text.is_empty() && !is_end_of_file {
745                    text.push('\n');
746                }
747
748                let _ = editor.update_in(cx, |editor, window, cx| {
749                    editor.transact(window, cx, |editor, window, cx| {
750                        editor.edit([(edit_range.clone(), text)], cx);
751                        let snapshot = editor.buffer().read(cx).snapshot(cx);
752                        editor.change_selections(Default::default(), window, cx, |s| {
753                            let point = if is_end_of_file {
754                                Point::new(
755                                    edit_range.start.to_point(&snapshot).row.saturating_add(1),
756                                    0,
757                                )
758                            } else {
759                                Point::new(edit_range.start.to_point(&snapshot).row, 0)
760                            };
761                            s.select_ranges([point..point]);
762                        })
763                    });
764                });
765            })
766            .detach();
767        });
768    });
769
770    Vim::action(editor, cx, |vim, action: &VimNorm, window, cx| {
771        let keystrokes = action
772            .command
773            .chars()
774            .filter_map(|c| Keystroke::parse(&c.to_string()).ok())
775            .collect();
776        vim.switch_mode(Mode::Normal, true, window, cx);
777        if let Some(override_rows) = &action.override_rows {
778            vim.update_editor(cx, |_, editor, cx| {
779                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
780                    s.replace_cursors_with(|map| {
781                        override_rows
782                            .iter()
783                            .map(|row| Point::new(*row, 0).to_display_point(map))
784                            .collect()
785                    });
786                });
787            });
788        } else if let Some(range) = &action.range {
789            let result = vim.update_editor(cx, |vim, editor, cx| {
790                let range = range.buffer_range(vim, editor, window, cx)?;
791                editor.change_selections(
792                    SelectionEffects::no_scroll().nav_history(false),
793                    window,
794                    cx,
795                    |s| {
796                        s.select_ranges(
797                            (range.start.0..=range.end.0)
798                                .map(|line| Point::new(line, 0)..Point::new(line, 0)),
799                        );
800                    },
801                );
802                anyhow::Ok(())
803            });
804            if let Some(Err(err)) = result {
805                log::error!("Error selecting range: {}", err);
806                return;
807            }
808        };
809
810        let Some(workspace) = vim.workspace(window, cx) else {
811            return;
812        };
813        let task = workspace.update(cx, |workspace, cx| {
814            workspace.send_keystrokes_impl(keystrokes, window, cx)
815        });
816        let had_range = action.range.is_some();
817        let had_override = action.override_rows.is_some();
818
819        cx.spawn_in(window, async move |vim, cx| {
820            task.await;
821            vim.update_in(cx, |vim, window, cx| {
822                if matches!(vim.mode, Mode::Insert | Mode::Replace) {
823                    vim.normal_before(&Default::default(), window, cx);
824                } else {
825                    vim.switch_mode(Mode::Normal, true, window, cx);
826                }
827                if had_override || had_range {
828                    vim.update_editor(cx, |_, editor, cx| {
829                        editor.change_selections(SelectionEffects::default(), window, cx, |s| {
830                            s.select_anchor_ranges([s.newest_anchor().range()]);
831                        });
832                        if let Some(tx_id) = editor
833                            .buffer()
834                            .update(cx, |multi, cx| multi.last_transaction_id(cx))
835                        {
836                            let last_sel = editor.selections.disjoint_anchors_arc();
837                            editor.modify_transaction_selection_history(tx_id, |old| {
838                                old.undo = old.undo.get(..1).unwrap_or(&[]).into();
839                                old.redo = Some(last_sel);
840                            });
841                        }
842                    });
843                }
844            })
845            .log_err();
846        })
847        .detach();
848    });
849
850    Vim::action(editor, cx, |vim, _: &CountCommand, window, cx| {
851        let Some(workspace) = vim.workspace(window, cx) else {
852            return;
853        };
854        let count = Vim::take_count(cx).unwrap_or(1);
855        Vim::take_forced_motion(cx);
856        let n = if count > 1 {
857            format!(".,.+{}", count.saturating_sub(1))
858        } else {
859            ".".to_string()
860        };
861        workspace.update(cx, |workspace, cx| {
862            command_palette::CommandPalette::toggle(workspace, &n, window, cx);
863        })
864    });
865
866    Vim::action(editor, cx, |vim, action: &GoToLine, window, cx| {
867        vim.switch_mode(Mode::Normal, false, window, cx);
868        let result = vim.update_editor(cx, |vim, editor, cx| {
869            let snapshot = editor.snapshot(window, cx);
870            let buffer_row = action.range.head().buffer_row(vim, editor, window, cx)?;
871            let current = editor
872                .selections
873                .newest::<Point>(&editor.display_snapshot(cx));
874            let target = snapshot
875                .buffer_snapshot()
876                .clip_point(Point::new(buffer_row.0, current.head().column), Bias::Left);
877            editor.change_selections(Default::default(), window, cx, |s| {
878                s.select_ranges([target..target]);
879            });
880
881            anyhow::Ok(())
882        });
883        if let Some(e @ Err(_)) = result {
884            let Some(workspace) = vim.workspace(window, cx) else {
885                return;
886            };
887            workspace.update(cx, |workspace, cx| {
888                e.notify_err(workspace, cx);
889            });
890        }
891    });
892
893    Vim::action(editor, cx, |vim, action: &YankCommand, window, cx| {
894        vim.update_editor(cx, |vim, editor, cx| {
895            let snapshot = editor.snapshot(window, cx);
896            if let Ok(range) = action.range.buffer_range(vim, editor, window, cx) {
897                let end = if range.end < snapshot.buffer_snapshot().max_row() {
898                    Point::new(range.end.0 + 1, 0)
899                } else {
900                    snapshot.buffer_snapshot().max_point()
901                };
902                vim.copy_ranges(
903                    editor,
904                    MotionKind::Linewise,
905                    true,
906                    vec![Point::new(range.start.0, 0)..end],
907                    window,
908                    cx,
909                )
910            }
911        });
912    });
913
914    Vim::action(editor, cx, |_, action: &WithCount, window, cx| {
915        for _ in 0..action.count {
916            window.dispatch_action(action.action.boxed_clone(), cx)
917        }
918    });
919
920    Vim::action(editor, cx, |vim, action: &WithRange, window, cx| {
921        let result = vim.update_editor(cx, |vim, editor, cx| {
922            action.range.buffer_range(vim, editor, window, cx)
923        });
924
925        let range = match result {
926            None => return,
927            Some(e @ Err(_)) => {
928                let Some(workspace) = vim.workspace(window, cx) else {
929                    return;
930                };
931                workspace.update(cx, |workspace, cx| {
932                    e.notify_err(workspace, cx);
933                });
934                return;
935            }
936            Some(Ok(result)) => result,
937        };
938
939        let previous_selections = vim
940            .update_editor(cx, |_, editor, cx| {
941                let selections = action.restore_selection.then(|| {
942                    editor
943                        .selections
944                        .disjoint_anchor_ranges()
945                        .collect::<Vec<_>>()
946                });
947                let snapshot = editor.buffer().read(cx).snapshot(cx);
948                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
949                    let end = Point::new(range.end.0, snapshot.line_len(range.end));
950                    s.select_ranges([end..Point::new(range.start.0, 0)]);
951                });
952                selections
953            })
954            .flatten();
955        window.dispatch_action(action.action.boxed_clone(), cx);
956        cx.defer_in(window, move |vim, window, cx| {
957            vim.update_editor(cx, |_, editor, cx| {
958                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
959                    if let Some(previous_selections) = previous_selections {
960                        s.select_ranges(previous_selections);
961                    } else {
962                        s.select_ranges([
963                            Point::new(range.start.0, 0)..Point::new(range.start.0, 0)
964                        ]);
965                    }
966                })
967            });
968        });
969    });
970
971    Vim::action(editor, cx, |vim, action: &OnMatchingLines, window, cx| {
972        action.run(vim, window, cx)
973    });
974
975    Vim::action(editor, cx, |vim, action: &ShellExec, window, cx| {
976        action.run(vim, window, cx)
977    })
978}
979
980#[derive(Default)]
981struct VimCommand {
982    prefix: &'static str,
983    suffix: &'static str,
984    action: Option<Box<dyn Action>>,
985    action_name: Option<&'static str>,
986    bang_action: Option<Box<dyn Action>>,
987    args: Option<
988        Box<dyn Fn(Box<dyn Action>, String) -> Option<Box<dyn Action>> + Send + Sync + 'static>,
989    >,
990    /// Optional range Range to use if no range is specified.
991    default_range: Option<CommandRange>,
992    range: Option<
993        Box<
994            dyn Fn(Box<dyn Action>, &CommandRange) -> Option<Box<dyn Action>>
995                + Send
996                + Sync
997                + 'static,
998        >,
999    >,
1000    has_count: bool,
1001    has_filename: bool,
1002}
1003
1004struct ParsedQuery {
1005    args: String,
1006    has_bang: bool,
1007    has_space: bool,
1008}
1009
1010impl VimCommand {
1011    fn new(pattern: (&'static str, &'static str), action: impl Action) -> Self {
1012        Self {
1013            prefix: pattern.0,
1014            suffix: pattern.1,
1015            action: Some(action.boxed_clone()),
1016            ..Default::default()
1017        }
1018    }
1019
1020    // from_str is used for actions in other crates.
1021    fn str(pattern: (&'static str, &'static str), action_name: &'static str) -> Self {
1022        Self {
1023            prefix: pattern.0,
1024            suffix: pattern.1,
1025            action_name: Some(action_name),
1026            ..Default::default()
1027        }
1028    }
1029
1030    fn bang(mut self, bang_action: impl Action) -> Self {
1031        self.bang_action = Some(bang_action.boxed_clone());
1032        self
1033    }
1034
1035    /// Set argument handler. Trailing whitespace in arguments will be preserved.
1036    fn args(
1037        mut self,
1038        f: impl Fn(Box<dyn Action>, String) -> Option<Box<dyn Action>> + Send + Sync + 'static,
1039    ) -> Self {
1040        self.args = Some(Box::new(f));
1041        self
1042    }
1043
1044    /// Set argument handler. Trailing whitespace in arguments will be trimmed.
1045    /// Supports filename autocompletion.
1046    fn filename(
1047        mut self,
1048        f: impl Fn(Box<dyn Action>, String) -> Option<Box<dyn Action>> + Send + Sync + 'static,
1049    ) -> Self {
1050        self.args = Some(Box::new(f));
1051        self.has_filename = true;
1052        self
1053    }
1054
1055    fn range(
1056        mut self,
1057        f: impl Fn(Box<dyn Action>, &CommandRange) -> Option<Box<dyn Action>> + Send + Sync + 'static,
1058    ) -> Self {
1059        self.range = Some(Box::new(f));
1060        self
1061    }
1062
1063    fn default_range(mut self, range: CommandRange) -> Self {
1064        self.default_range = Some(range);
1065        self
1066    }
1067
1068    fn count(mut self) -> Self {
1069        self.has_count = true;
1070        self
1071    }
1072
1073    fn generate_filename_completions(
1074        parsed_query: &ParsedQuery,
1075        workspace: WeakEntity<Workspace>,
1076        cx: &mut App,
1077    ) -> Task<Vec<String>> {
1078        let ParsedQuery {
1079            args,
1080            has_bang: _,
1081            has_space: _,
1082        } = parsed_query;
1083        let Some(workspace) = workspace.upgrade() else {
1084            return Task::ready(Vec::new());
1085        };
1086
1087        let (task, args_path) = workspace.update(cx, |workspace, cx| {
1088            let prefix = workspace
1089                .project()
1090                .read(cx)
1091                .visible_worktrees(cx)
1092                .map(|worktree| worktree.read(cx).abs_path().to_path_buf())
1093                .next()
1094                .or_else(std::env::home_dir)
1095                .unwrap_or_else(|| PathBuf::from(""));
1096
1097            let rel_path = match RelPath::new(Path::new(&args), PathStyle::local()) {
1098                Ok(path) => path.to_rel_path_buf(),
1099                Err(_) => {
1100                    return (Task::ready(Ok(Vec::new())), RelPathBuf::new());
1101                }
1102            };
1103
1104            let rel_path = if args.ends_with(PathStyle::local().primary_separator()) {
1105                rel_path
1106            } else {
1107                rel_path
1108                    .parent()
1109                    .map(|rel_path| rel_path.to_rel_path_buf())
1110                    .unwrap_or(RelPathBuf::new())
1111            };
1112
1113            let task = workspace.project().update(cx, |project, cx| {
1114                let path = prefix
1115                    .join(rel_path.as_std_path())
1116                    .to_string_lossy()
1117                    .to_string();
1118                project.list_directory(path, cx)
1119            });
1120
1121            (task, rel_path)
1122        });
1123
1124        cx.background_spawn(async move {
1125            let directories = task.await.unwrap_or_default();
1126            directories
1127                .iter()
1128                .map(|dir| {
1129                    let path = RelPath::new(dir.path.as_path(), PathStyle::local())
1130                        .map(|cow| cow.into_owned())
1131                        .unwrap_or(RelPathBuf::new());
1132                    let mut path_string = args_path
1133                        .join(&path)
1134                        .display(PathStyle::local())
1135                        .to_string();
1136                    if dir.is_dir {
1137                        path_string.push_str(PathStyle::local().primary_separator());
1138                    }
1139                    path_string
1140                })
1141                .collect()
1142        })
1143    }
1144
1145    fn get_parsed_query(&self, query: String) -> Option<ParsedQuery> {
1146        let rest = query
1147            .strip_prefix(self.prefix)?
1148            .to_string()
1149            .chars()
1150            .zip_longest(self.suffix.to_string().chars())
1151            .skip_while(|e| e.clone().both().map(|(s, q)| s == q).unwrap_or(false))
1152            .filter_map(|e| e.left())
1153            .collect::<String>();
1154        let has_bang = rest.starts_with('!');
1155        let has_space = rest.starts_with("! ") || rest.starts_with(' ');
1156        let args = if has_bang {
1157            rest.strip_prefix('!')?.trim_start().to_string()
1158        } else if rest.is_empty() {
1159            "".into()
1160        } else {
1161            rest.strip_prefix(' ')?.trim_start().to_string()
1162        };
1163        Some(ParsedQuery {
1164            args,
1165            has_bang,
1166            has_space,
1167        })
1168    }
1169
1170    fn parse(
1171        &self,
1172        query: &str,
1173        range: &Option<CommandRange>,
1174        cx: &App,
1175    ) -> Option<Box<dyn Action>> {
1176        let ParsedQuery {
1177            args,
1178            has_bang,
1179            has_space: _,
1180        } = self.get_parsed_query(query.to_string())?;
1181        let action = if has_bang && let Some(bang_action) = self.bang_action.as_ref() {
1182            bang_action.boxed_clone()
1183        } else if let Some(action) = self.action.as_ref() {
1184            action.boxed_clone()
1185        } else if let Some(action_name) = self.action_name {
1186            cx.build_action(action_name, None).log_err()?
1187        } else {
1188            return None;
1189        };
1190
1191        // If the command does not accept args and we have args, we should do no
1192        // action.
1193        let action = if args.is_empty() {
1194            action
1195        } else if self.has_filename {
1196            self.args.as_ref()?(action, args.trim().into())?
1197        } else {
1198            self.args.as_ref()?(action, args)?
1199        };
1200
1201        let range = range.as_ref().or(self.default_range.as_ref());
1202        if let Some(range) = range {
1203            self.range.as_ref().and_then(|f| f(action, range))
1204        } else {
1205            Some(action)
1206        }
1207    }
1208
1209    // TODO: ranges with search queries
1210    fn parse_range(query: &str) -> (Option<CommandRange>, String) {
1211        let mut chars = query.chars().peekable();
1212
1213        match chars.peek() {
1214            Some('%') => {
1215                chars.next();
1216                return (
1217                    Some(CommandRange {
1218                        start: Position::Line { row: 1, offset: 0 },
1219                        end: Some(Position::LastLine { offset: 0 }),
1220                    }),
1221                    chars.collect(),
1222                );
1223            }
1224            Some('*') => {
1225                chars.next();
1226                return (
1227                    Some(CommandRange {
1228                        start: Position::Mark {
1229                            name: '<',
1230                            offset: 0,
1231                        },
1232                        end: Some(Position::Mark {
1233                            name: '>',
1234                            offset: 0,
1235                        }),
1236                    }),
1237                    chars.collect(),
1238                );
1239            }
1240            _ => {}
1241        }
1242
1243        let start = Self::parse_position(&mut chars);
1244
1245        match chars.peek() {
1246            Some(',' | ';') => {
1247                chars.next();
1248                (
1249                    Some(CommandRange {
1250                        start: start.unwrap_or(Position::CurrentLine { offset: 0 }),
1251                        end: Self::parse_position(&mut chars),
1252                    }),
1253                    chars.collect(),
1254                )
1255            }
1256            _ => (
1257                start.map(|start| CommandRange { start, end: None }),
1258                chars.collect(),
1259            ),
1260        }
1261    }
1262
1263    fn parse_position(chars: &mut Peekable<Chars>) -> Option<Position> {
1264        match chars.peek()? {
1265            '0'..='9' => {
1266                let row = Self::parse_u32(chars);
1267                Some(Position::Line {
1268                    row,
1269                    offset: Self::parse_offset(chars),
1270                })
1271            }
1272            '\'' => {
1273                chars.next();
1274                let name = chars.next()?;
1275                Some(Position::Mark {
1276                    name,
1277                    offset: Self::parse_offset(chars),
1278                })
1279            }
1280            '.' => {
1281                chars.next();
1282                Some(Position::CurrentLine {
1283                    offset: Self::parse_offset(chars),
1284                })
1285            }
1286            '+' | '-' => Some(Position::CurrentLine {
1287                offset: Self::parse_offset(chars),
1288            }),
1289            '$' => {
1290                chars.next();
1291                Some(Position::LastLine {
1292                    offset: Self::parse_offset(chars),
1293                })
1294            }
1295            _ => None,
1296        }
1297    }
1298
1299    fn parse_offset(chars: &mut Peekable<Chars>) -> i32 {
1300        let mut res: i32 = 0;
1301        while matches!(chars.peek(), Some('+' | '-')) {
1302            let sign = if chars.next().unwrap() == '+' { 1 } else { -1 };
1303            let amount = if matches!(chars.peek(), Some('0'..='9')) {
1304                (Self::parse_u32(chars) as i32).saturating_mul(sign)
1305            } else {
1306                sign
1307            };
1308            res = res.saturating_add(amount)
1309        }
1310        res
1311    }
1312
1313    fn parse_u32(chars: &mut Peekable<Chars>) -> u32 {
1314        let mut res: u32 = 0;
1315        while matches!(chars.peek(), Some('0'..='9')) {
1316            res = res
1317                .saturating_mul(10)
1318                .saturating_add(chars.next().unwrap() as u32 - '0' as u32);
1319        }
1320        res
1321    }
1322}
1323
1324#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq)]
1325enum Position {
1326    Line { row: u32, offset: i32 },
1327    Mark { name: char, offset: i32 },
1328    LastLine { offset: i32 },
1329    CurrentLine { offset: i32 },
1330}
1331
1332impl Position {
1333    fn buffer_row(
1334        &self,
1335        vim: &Vim,
1336        editor: &mut Editor,
1337        window: &mut Window,
1338        cx: &mut App,
1339    ) -> Result<MultiBufferRow> {
1340        let snapshot = editor.snapshot(window, cx);
1341        let target = match self {
1342            Position::Line { row, offset } => {
1343                if let Some(anchor) = editor.active_buffer(cx).and_then(|buffer| {
1344                    editor.buffer().read(cx).buffer_point_to_anchor(
1345                        &buffer,
1346                        Point::new(row.saturating_sub(1), 0),
1347                        cx,
1348                    )
1349                }) {
1350                    anchor
1351                        .to_point(&snapshot.buffer_snapshot())
1352                        .row
1353                        .saturating_add_signed(*offset)
1354                } else {
1355                    row.saturating_add_signed(offset.saturating_sub(1))
1356                }
1357            }
1358            Position::Mark { name, offset } => {
1359                let Some(Mark::Local(anchors)) =
1360                    vim.get_mark(&name.to_string(), editor, window, cx)
1361                else {
1362                    anyhow::bail!("mark {name} not set");
1363                };
1364                let Some(mark) = anchors.last() else {
1365                    anyhow::bail!("mark {name} contains empty anchors");
1366                };
1367                mark.to_point(&snapshot.buffer_snapshot())
1368                    .row
1369                    .saturating_add_signed(*offset)
1370            }
1371            Position::LastLine { offset } => snapshot
1372                .buffer_snapshot()
1373                .max_row()
1374                .0
1375                .saturating_add_signed(*offset),
1376            Position::CurrentLine { offset } => editor
1377                .selections
1378                .newest_anchor()
1379                .head()
1380                .to_point(&snapshot.buffer_snapshot())
1381                .row
1382                .saturating_add_signed(*offset),
1383        };
1384
1385        Ok(MultiBufferRow(target).min(snapshot.buffer_snapshot().max_row()))
1386    }
1387}
1388
1389#[derive(Clone, Debug, PartialEq)]
1390pub(crate) struct CommandRange {
1391    start: Position,
1392    end: Option<Position>,
1393}
1394
1395impl CommandRange {
1396    fn head(&self) -> &Position {
1397        self.end.as_ref().unwrap_or(&self.start)
1398    }
1399
1400    /// Convert the `CommandRange` into a `Range<MultiBufferRow>`.
1401    pub(crate) fn buffer_range(
1402        &self,
1403        vim: &Vim,
1404        editor: &mut Editor,
1405        window: &mut Window,
1406        cx: &mut App,
1407    ) -> Result<Range<MultiBufferRow>> {
1408        let start = self.start.buffer_row(vim, editor, window, cx)?;
1409        let end = if let Some(end) = self.end.as_ref() {
1410            end.buffer_row(vim, editor, window, cx)?
1411        } else {
1412            start
1413        };
1414        if end < start {
1415            anyhow::Ok(end..start)
1416        } else {
1417            anyhow::Ok(start..end)
1418        }
1419    }
1420
1421    pub fn as_count(&self) -> Option<u32> {
1422        if let CommandRange {
1423            start: Position::Line { row, offset: 0 },
1424            end: None,
1425        } = &self
1426        {
1427            Some(*row)
1428        } else {
1429            None
1430        }
1431    }
1432
1433    /// The `CommandRange` representing the entire buffer.
1434    fn buffer() -> Self {
1435        Self {
1436            start: Position::Line { row: 1, offset: 0 },
1437            end: Some(Position::LastLine { offset: 0 }),
1438        }
1439    }
1440}
1441
1442fn generate_commands(_: &App) -> Vec<VimCommand> {
1443    vec![
1444        VimCommand::new(
1445            ("w", "rite"),
1446            VimSave {
1447                save_intent: Some(SaveIntent::Save),
1448                filename: "".into(),
1449                range: None,
1450            },
1451        )
1452        .bang(VimSave {
1453            save_intent: Some(SaveIntent::Overwrite),
1454            filename: "".into(),
1455            range: None,
1456        })
1457        .filename(|action, filename| {
1458            Some(
1459                VimSave {
1460                    save_intent: action
1461                        .as_any()
1462                        .downcast_ref::<VimSave>()
1463                        .and_then(|action| action.save_intent),
1464                    filename,
1465                    range: None,
1466                }
1467                .boxed_clone(),
1468            )
1469        })
1470        .range(|action, range| {
1471            let mut action: VimSave = action.as_any().downcast_ref::<VimSave>().unwrap().clone();
1472            action.range.replace(range.clone());
1473            Some(Box::new(action))
1474        }),
1475        VimCommand::new(("e", "dit"), editor::actions::ReloadFile)
1476            .bang(editor::actions::ReloadFile)
1477            .filename(|_, filename| Some(VimEdit { filename }.boxed_clone())),
1478        VimCommand::new(
1479            ("r", "ead"),
1480            VimRead {
1481                range: None,
1482                filename: "".into(),
1483            },
1484        )
1485        .filename(|_, filename| {
1486            Some(
1487                VimRead {
1488                    range: None,
1489                    filename,
1490                }
1491                .boxed_clone(),
1492            )
1493        })
1494        .range(|action, range| {
1495            let mut action: VimRead = action.as_any().downcast_ref::<VimRead>().unwrap().clone();
1496            action.range.replace(range.clone());
1497            Some(Box::new(action))
1498        }),
1499        VimCommand::new(("sp", "lit"), workspace::SplitHorizontal::default()).filename(
1500            |_, filename| {
1501                Some(
1502                    VimSplit {
1503                        vertical: false,
1504                        filename,
1505                    }
1506                    .boxed_clone(),
1507                )
1508            },
1509        ),
1510        VimCommand::new(("vs", "plit"), workspace::SplitVertical::default()).filename(
1511            |_, filename| {
1512                Some(
1513                    VimSplit {
1514                        vertical: true,
1515                        filename,
1516                    }
1517                    .boxed_clone(),
1518                )
1519            },
1520        ),
1521        VimCommand::new(("tabe", "dit"), workspace::NewFile)
1522            .filename(|_action, filename| Some(VimEdit { filename }.boxed_clone())),
1523        VimCommand::new(("tabnew", ""), workspace::NewFile)
1524            .filename(|_action, filename| Some(VimEdit { filename }.boxed_clone())),
1525        VimCommand::new(
1526            ("q", "uit"),
1527            workspace::CloseActiveItem {
1528                save_intent: Some(SaveIntent::Close),
1529                close_pinned: false,
1530            },
1531        )
1532        .bang(workspace::CloseActiveItem {
1533            save_intent: Some(SaveIntent::Skip),
1534            close_pinned: true,
1535        }),
1536        VimCommand::new(
1537            ("wq", ""),
1538            workspace::CloseActiveItem {
1539                save_intent: Some(SaveIntent::Save),
1540                close_pinned: false,
1541            },
1542        )
1543        .bang(workspace::CloseActiveItem {
1544            save_intent: Some(SaveIntent::Overwrite),
1545            close_pinned: true,
1546        }),
1547        VimCommand::new(
1548            ("x", "it"),
1549            workspace::CloseActiveItem {
1550                save_intent: Some(SaveIntent::SaveAll),
1551                close_pinned: false,
1552            },
1553        )
1554        .bang(workspace::CloseActiveItem {
1555            save_intent: Some(SaveIntent::Overwrite),
1556            close_pinned: true,
1557        }),
1558        VimCommand::new(
1559            ("exi", "t"),
1560            workspace::CloseActiveItem {
1561                save_intent: Some(SaveIntent::SaveAll),
1562                close_pinned: false,
1563            },
1564        )
1565        .bang(workspace::CloseActiveItem {
1566            save_intent: Some(SaveIntent::Overwrite),
1567            close_pinned: true,
1568        }),
1569        VimCommand::new(
1570            ("up", "date"),
1571            workspace::Save {
1572                save_intent: Some(SaveIntent::SaveAll),
1573            },
1574        ),
1575        VimCommand::new(
1576            ("wa", "ll"),
1577            workspace::SaveAll {
1578                save_intent: Some(SaveIntent::SaveAll),
1579            },
1580        )
1581        .bang(workspace::SaveAll {
1582            save_intent: Some(SaveIntent::Overwrite),
1583        }),
1584        VimCommand::new(
1585            ("qa", "ll"),
1586            workspace::CloseAllItemsAndPanes {
1587                save_intent: Some(SaveIntent::Close),
1588            },
1589        )
1590        .bang(workspace::CloseAllItemsAndPanes {
1591            save_intent: Some(SaveIntent::Skip),
1592        }),
1593        VimCommand::new(
1594            ("quita", "ll"),
1595            workspace::CloseAllItemsAndPanes {
1596                save_intent: Some(SaveIntent::Close),
1597            },
1598        )
1599        .bang(workspace::CloseAllItemsAndPanes {
1600            save_intent: Some(SaveIntent::Skip),
1601        }),
1602        VimCommand::new(
1603            ("xa", "ll"),
1604            workspace::CloseAllItemsAndPanes {
1605                save_intent: Some(SaveIntent::SaveAll),
1606            },
1607        )
1608        .bang(workspace::CloseAllItemsAndPanes {
1609            save_intent: Some(SaveIntent::Overwrite),
1610        }),
1611        VimCommand::new(
1612            ("wqa", "ll"),
1613            workspace::CloseAllItemsAndPanes {
1614                save_intent: Some(SaveIntent::SaveAll),
1615            },
1616        )
1617        .bang(workspace::CloseAllItemsAndPanes {
1618            save_intent: Some(SaveIntent::Overwrite),
1619        }),
1620        VimCommand::new(("cq", "uit"), zed_actions::Quit),
1621        VimCommand::new(
1622            ("bd", "elete"),
1623            workspace::CloseItemInAllPanes {
1624                save_intent: Some(SaveIntent::Close),
1625                close_pinned: false,
1626            },
1627        )
1628        .bang(workspace::CloseItemInAllPanes {
1629            save_intent: Some(SaveIntent::Skip),
1630            close_pinned: true,
1631        }),
1632        VimCommand::new(
1633            ("norm", "al"),
1634            VimNorm {
1635                command: "".into(),
1636                range: None,
1637                override_rows: None,
1638            },
1639        )
1640        .args(|_, args| {
1641            Some(
1642                VimNorm {
1643                    command: args,
1644                    range: None,
1645                    override_rows: None,
1646                }
1647                .boxed_clone(),
1648            )
1649        })
1650        .range(|action, range| {
1651            let mut action: VimNorm = action.as_any().downcast_ref::<VimNorm>().unwrap().clone();
1652            action.range.replace(range.clone());
1653            Some(Box::new(action))
1654        }),
1655        VimCommand::new(("bn", "ext"), workspace::ActivateNextItem::default()).count(),
1656        VimCommand::new(("bN", "ext"), workspace::ActivatePreviousItem::default()).count(),
1657        VimCommand::new(
1658            ("bp", "revious"),
1659            workspace::ActivatePreviousItem::default(),
1660        )
1661        .count(),
1662        VimCommand::new(("bf", "irst"), workspace::ActivateItem(0)),
1663        VimCommand::new(("br", "ewind"), workspace::ActivateItem(0)),
1664        VimCommand::new(("bl", "ast"), workspace::ActivateLastItem),
1665        VimCommand::str(("buffers", ""), "tab_switcher::ToggleAll"),
1666        VimCommand::str(("ls", ""), "tab_switcher::ToggleAll"),
1667        VimCommand::new(("new", ""), workspace::NewFileSplitHorizontal),
1668        VimCommand::new(("vne", "w"), workspace::NewFileSplitVertical),
1669        VimCommand::new(("tabn", "ext"), workspace::ActivateNextItem::default()).count(),
1670        VimCommand::new(
1671            ("tabp", "revious"),
1672            workspace::ActivatePreviousItem::default(),
1673        )
1674        .count(),
1675        VimCommand::new(("tabN", "ext"), workspace::ActivatePreviousItem::default()).count(),
1676        VimCommand::new(
1677            ("tabc", "lose"),
1678            workspace::CloseActiveItem {
1679                save_intent: Some(SaveIntent::Close),
1680                close_pinned: false,
1681            },
1682        ),
1683        VimCommand::new(
1684            ("tabo", "nly"),
1685            workspace::CloseOtherItems {
1686                save_intent: Some(SaveIntent::Close),
1687                close_pinned: false,
1688            },
1689        )
1690        .bang(workspace::CloseOtherItems {
1691            save_intent: Some(SaveIntent::Skip),
1692            close_pinned: false,
1693        }),
1694        VimCommand::new(
1695            ("on", "ly"),
1696            workspace::CloseInactiveTabsAndPanes {
1697                save_intent: Some(SaveIntent::Close),
1698            },
1699        )
1700        .bang(workspace::CloseInactiveTabsAndPanes {
1701            save_intent: Some(SaveIntent::Skip),
1702        }),
1703        VimCommand::str(("cl", "ist"), "diagnostics::Deploy"),
1704        VimCommand::new(("cc", ""), editor::actions::Hover),
1705        VimCommand::new(("ll", ""), editor::actions::Hover),
1706        VimCommand::new(("cn", "ext"), editor::actions::GoToDiagnostic::default())
1707            .range(wrap_count),
1708        VimCommand::new(
1709            ("cp", "revious"),
1710            editor::actions::GoToPreviousDiagnostic::default(),
1711        )
1712        .range(wrap_count),
1713        VimCommand::new(
1714            ("cN", "ext"),
1715            editor::actions::GoToPreviousDiagnostic::default(),
1716        )
1717        .range(wrap_count),
1718        VimCommand::new(
1719            ("lp", "revious"),
1720            editor::actions::GoToPreviousDiagnostic::default(),
1721        )
1722        .range(wrap_count),
1723        VimCommand::new(
1724            ("lN", "ext"),
1725            editor::actions::GoToPreviousDiagnostic::default(),
1726        )
1727        .range(wrap_count),
1728        VimCommand::new(("j", "oin"), JoinLines).range(select_range),
1729        VimCommand::new(("reflow", ""), Rewrap { line_length: None })
1730            .range(select_range)
1731            .args(|_action, args| {
1732                args.parse::<usize>().map_or(None, |length| {
1733                    Some(Box::new(Rewrap {
1734                        line_length: Some(length),
1735                    }))
1736                })
1737            }),
1738        VimCommand::new(("fo", "ld"), editor::actions::FoldSelectedRanges).range(act_on_range),
1739        VimCommand::new(("foldo", "pen"), editor::actions::UnfoldLines)
1740            .bang(editor::actions::UnfoldRecursive)
1741            .range(act_on_range),
1742        VimCommand::new(("foldc", "lose"), editor::actions::Fold)
1743            .bang(editor::actions::FoldRecursive)
1744            .range(act_on_range),
1745        VimCommand::new(("dif", "fupdate"), editor::actions::ToggleSelectedDiffHunks)
1746            .range(act_on_range),
1747        VimCommand::str(("rev", "ert"), "git::Restore").range(act_on_range),
1748        VimCommand::new(("d", "elete"), VisualDeleteLine).range(select_range),
1749        VimCommand::new(("y", "ank"), gpui::NoAction).range(|_, range| {
1750            Some(
1751                YankCommand {
1752                    range: range.clone(),
1753                }
1754                .boxed_clone(),
1755            )
1756        }),
1757        VimCommand::new(("reg", "isters"), ToggleRegistersView).bang(ToggleRegistersView),
1758        VimCommand::new(("di", "splay"), ToggleRegistersView).bang(ToggleRegistersView),
1759        VimCommand::new(("marks", ""), ToggleMarksView).bang(ToggleMarksView),
1760        VimCommand::new(("delm", "arks"), ArgumentRequired)
1761            .bang(DeleteMarks::AllLocal)
1762            .args(|_, args| Some(DeleteMarks::Marks(args).boxed_clone())),
1763        VimCommand::new(("sor", "t"), SortLinesCaseSensitive)
1764            .range(select_range)
1765            .default_range(CommandRange::buffer()),
1766        VimCommand::new(("sort i", ""), SortLinesCaseInsensitive)
1767            .range(select_range)
1768            .default_range(CommandRange::buffer()),
1769        VimCommand::str(("E", "xplore"), "project_panel::ToggleFocus"),
1770        VimCommand::str(("H", "explore"), "project_panel::ToggleFocus"),
1771        VimCommand::str(("L", "explore"), "project_panel::ToggleFocus"),
1772        VimCommand::str(("S", "explore"), "project_panel::ToggleFocus"),
1773        VimCommand::str(("Ve", "xplore"), "project_panel::ToggleFocus"),
1774        VimCommand::str(("te", "rm"), "terminal_panel::Toggle"),
1775        VimCommand::str(("T", "erm"), "terminal_panel::Toggle"),
1776        VimCommand::str(("C", "ollab"), "collab_panel::ToggleFocus"),
1777        VimCommand::str(("A", "I"), "agent::ToggleFocus"),
1778        VimCommand::str(("G", "it"), "git_panel::ToggleFocus"),
1779        VimCommand::str(("D", "ebug"), "debug_panel::ToggleFocus"),
1780        VimCommand::new(("noh", "lsearch"), search::buffer_search::Dismiss),
1781        VimCommand::new(("$", ""), EndOfDocument),
1782        VimCommand::new(("%", ""), EndOfDocument),
1783        VimCommand::new(("0", ""), StartOfDocument),
1784        VimCommand::new(("ex", ""), editor::actions::ReloadFile).bang(editor::actions::ReloadFile),
1785        VimCommand::new(("cpp", "link"), editor::actions::CopyPermalinkToLine).range(act_on_range),
1786        VimCommand::str(("opt", "ions"), "omega::OpenDefaultSettings"),
1787        VimCommand::str(("map", ""), "vim::OpenDefaultKeymap"),
1788        VimCommand::new(("h", "elp"), OpenDocs),
1789    ]
1790}
1791
1792struct VimCommands(Vec<VimCommand>);
1793// safety: we only ever access this from the main thread (as ensured by the cx argument)
1794// actions are not Sync so we can't otherwise use a OnceLock.
1795unsafe impl Sync for VimCommands {}
1796impl Global for VimCommands {}
1797
1798fn commands(cx: &App) -> &Vec<VimCommand> {
1799    static COMMANDS: OnceLock<VimCommands> = OnceLock::new();
1800    &COMMANDS
1801        .get_or_init(|| VimCommands(generate_commands(cx)))
1802        .0
1803}
1804
1805fn act_on_range(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
1806    Some(
1807        WithRange {
1808            restore_selection: true,
1809            range: range.clone(),
1810            action: WrappedAction(action),
1811        }
1812        .boxed_clone(),
1813    )
1814}
1815
1816fn select_range(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
1817    Some(
1818        WithRange {
1819            restore_selection: false,
1820            range: range.clone(),
1821            action: WrappedAction(action),
1822        }
1823        .boxed_clone(),
1824    )
1825}
1826
1827fn wrap_count(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
1828    range.as_count().map(|count| {
1829        WithCount {
1830            count,
1831            action: WrappedAction(action),
1832        }
1833        .boxed_clone()
1834    })
1835}
1836
1837pub fn command_interceptor(
1838    mut input: &str,
1839    workspace: WeakEntity<Workspace>,
1840    cx: &mut App,
1841) -> Task<CommandInterceptResult> {
1842    while input.starts_with(':') {
1843        input = &input[1..];
1844    }
1845
1846    let (range, query) = VimCommand::parse_range(input);
1847    let range_prefix = input[0..(input.len() - query.len())].to_string();
1848    let has_trailing_space = query.ends_with(" ");
1849    let mut query = query.as_str().trim_start();
1850
1851    let on_matching_lines = (query.starts_with('g') || query.starts_with('v'))
1852        .then(|| {
1853            let (pattern, range, search, invert) = OnMatchingLines::parse(query, &range)?;
1854            let start_idx = query.len() - pattern.len();
1855            query = query[start_idx..].trim();
1856            Some((range, search, invert))
1857        })
1858        .flatten();
1859
1860    let mut action = if range.is_some() && query.is_empty() {
1861        Some(
1862            GoToLine {
1863                range: range.clone().unwrap(),
1864            }
1865            .boxed_clone(),
1866        )
1867    } else if query.starts_with('/') || query.starts_with('?') {
1868        Some(
1869            FindCommand {
1870                query: query[1..].to_string(),
1871                backwards: query.starts_with('?'),
1872            }
1873            .boxed_clone(),
1874        )
1875    } else if query.starts_with("se ") || query.starts_with("set ") {
1876        let (prefix, option) = query.split_once(' ').unwrap();
1877        let mut commands = VimOption::possible_commands(option);
1878        if !commands.is_empty() {
1879            let query = prefix.to_string() + " " + option;
1880            for command in &mut commands {
1881                command.positions = generate_positions(&command.string, &query);
1882            }
1883        }
1884        return Task::ready(CommandInterceptResult {
1885            results: commands,
1886            exclusive: false,
1887        });
1888    } else if query.starts_with('s') {
1889        let mut substitute = "substitute".chars().peekable();
1890        let mut query = query.chars().peekable();
1891        while substitute
1892            .peek()
1893            .is_some_and(|char| Some(char) == query.peek())
1894        {
1895            substitute.next();
1896            query.next();
1897        }
1898        if let Some(replacement) = Replacement::parse(query) {
1899            let range = range.clone().unwrap_or(CommandRange {
1900                start: Position::CurrentLine { offset: 0 },
1901                end: None,
1902            });
1903            Some(ReplaceCommand { replacement, range }.boxed_clone())
1904        } else {
1905            None
1906        }
1907    } else if query.contains('!') {
1908        ShellExec::parse(query, range.clone())
1909    } else if on_matching_lines.is_some() {
1910        commands(cx)
1911            .iter()
1912            .find_map(|command| command.parse(query, &None, cx))
1913    } else {
1914        None
1915    };
1916
1917    if let Some((range, search, invert)) = on_matching_lines
1918        && let Some(ref inner) = action
1919    {
1920        action = Some(Box::new(OnMatchingLines {
1921            range,
1922            search,
1923            action: WrappedAction(inner.boxed_clone()),
1924            invert,
1925        }));
1926    };
1927
1928    if let Some(action) = action {
1929        let string = input.to_string();
1930        let positions = generate_positions(&string, &(range_prefix + query));
1931        return Task::ready(CommandInterceptResult {
1932            results: vec![CommandInterceptItem {
1933                action,
1934                string,
1935                positions,
1936            }],
1937            exclusive: false,
1938        });
1939    }
1940
1941    let Some((mut results, filenames)) =
1942        commands(cx).iter().enumerate().find_map(|(idx, command)| {
1943            let action = command.parse(query, &range, cx)?;
1944            let parsed_query = command.get_parsed_query(query.into())?;
1945            let display_string = ":".to_owned()
1946                + &range_prefix
1947                + command.prefix
1948                + command.suffix
1949                + if parsed_query.has_bang { "!" } else { "" };
1950            let space = if parsed_query.has_space { " " } else { "" };
1951
1952            let string = format!("{}{}{}", &display_string, &space, &parsed_query.args);
1953            let positions = generate_positions(&string, &(range_prefix.clone() + query));
1954
1955            let results = vec![CommandInterceptItem {
1956                action,
1957                string,
1958                positions,
1959            }];
1960
1961            let no_args_positions =
1962                generate_positions(&display_string, &(range_prefix.clone() + query));
1963
1964            // The following are valid autocomplete scenarios:
1965            // :w!filename.txt
1966            // :w filename.txt
1967            // :w[space]
1968            if !command.has_filename
1969                || (!has_trailing_space && !parsed_query.has_bang && parsed_query.args.is_empty())
1970            {
1971                return Some((results, None));
1972            }
1973
1974            Some((
1975                results,
1976                Some((idx, parsed_query, display_string, no_args_positions)),
1977            ))
1978        })
1979    else {
1980        return Task::ready(CommandInterceptResult::default());
1981    };
1982
1983    if let Some((cmd_idx, parsed_query, display_string, no_args_positions)) = filenames {
1984        let filenames = VimCommand::generate_filename_completions(&parsed_query, workspace, cx);
1985        cx.spawn(async move |cx| {
1986            let filenames = filenames.await;
1987            const MAX_RESULTS: usize = 100;
1988            let executor = cx.background_executor().clone();
1989            let mut candidates = Vec::with_capacity(filenames.len());
1990
1991            for (idx, filename) in filenames.iter().enumerate() {
1992                candidates.push(fuzzy::StringMatchCandidate::new(idx, &filename));
1993            }
1994            let filenames = fuzzy::match_strings(
1995                &candidates,
1996                &parsed_query.args,
1997                false,
1998                true,
1999                MAX_RESULTS,
2000                &Default::default(),
2001                executor,
2002            )
2003            .await;
2004
2005            for fuzzy::StringMatch {
2006                candidate_id: _,
2007                score: _,
2008                positions,
2009                string,
2010            } in filenames
2011            {
2012                let offset = display_string.len() + 1;
2013                let mut positions: Vec<_> = positions.iter().map(|&pos| pos + offset).collect();
2014                positions.splice(0..0, no_args_positions.clone());
2015                let string = format!("{display_string} {string}");
2016                let (range, query) = VimCommand::parse_range(&string[1..]);
2017                let action =
2018                    match cx.update(|cx| commands(cx).get(cmd_idx)?.parse(&query, &range, cx)) {
2019                        Some(action) => action,
2020                        _ => continue,
2021                    };
2022                results.push(CommandInterceptItem {
2023                    action,
2024                    string,
2025                    positions,
2026                });
2027            }
2028            CommandInterceptResult {
2029                results,
2030                exclusive: true,
2031            }
2032        })
2033    } else {
2034        Task::ready(CommandInterceptResult {
2035            results,
2036            exclusive: false,
2037        })
2038    }
2039}
2040
2041fn generate_positions(string: &str, query: &str) -> Vec<usize> {
2042    let mut positions = Vec::new();
2043    let mut chars = query.chars();
2044
2045    let Some(mut current) = chars.next() else {
2046        return positions;
2047    };
2048
2049    for (i, c) in string.char_indices() {
2050        if c == current {
2051            positions.push(i);
2052            if let Some(c) = chars.next() {
2053                current = c;
2054            } else {
2055                break;
2056            }
2057        }
2058    }
2059
2060    positions
2061}
2062
2063/// Applies a command to all lines matching a pattern.
2064#[derive(Debug, PartialEq, Clone, Action)]
2065#[action(namespace = vim, no_json, no_register)]
2066pub(crate) struct OnMatchingLines {
2067    range: CommandRange,
2068    search: String,
2069    action: WrappedAction,
2070    invert: bool,
2071}
2072
2073impl OnMatchingLines {
2074    // convert a vim query into something more usable by zed.
2075    // we don't attempt to fully convert between the two regex syntaxes,
2076    // but we do flip \( and \) to ( and ) (and vice-versa) in the pattern,
2077    // and convert \0..\9 to $0..$9 in the replacement so that common idioms work.
2078    pub(crate) fn parse(
2079        query: &str,
2080        range: &Option<CommandRange>,
2081    ) -> Option<(String, CommandRange, String, bool)> {
2082        let mut global = "global".chars().peekable();
2083        let mut query_chars = query.chars().peekable();
2084        let mut invert = false;
2085        if query_chars.peek() == Some(&'v') {
2086            invert = true;
2087            query_chars.next();
2088        }
2089        while global
2090            .peek()
2091            .is_some_and(|char| Some(char) == query_chars.peek())
2092        {
2093            global.next();
2094            query_chars.next();
2095        }
2096        if !invert && query_chars.peek() == Some(&'!') {
2097            invert = true;
2098            query_chars.next();
2099        }
2100        let range = range.clone().unwrap_or(CommandRange {
2101            start: Position::Line { row: 0, offset: 0 },
2102            end: Some(Position::LastLine { offset: 0 }),
2103        });
2104
2105        let delimiter = query_chars.next().filter(|c| {
2106            !c.is_alphanumeric() && *c != '"' && *c != '|' && *c != '\'' && *c != '!'
2107        })?;
2108
2109        let mut search = String::new();
2110        let mut escaped = false;
2111
2112        for c in query_chars.by_ref() {
2113            if escaped {
2114                escaped = false;
2115                // unescape escaped parens
2116                if c != '(' && c != ')' && c != delimiter {
2117                    search.push('\\')
2118                }
2119                search.push(c)
2120            } else if c == '\\' {
2121                escaped = true;
2122            } else if c == delimiter {
2123                break;
2124            } else {
2125                // escape unescaped parens
2126                if c == '(' || c == ')' {
2127                    search.push('\\')
2128                }
2129                search.push(c)
2130            }
2131        }
2132
2133        Some((query_chars.collect::<String>(), range, search, invert))
2134    }
2135
2136    pub fn run(&self, vim: &mut Vim, window: &mut Window, cx: &mut Context<Vim>) {
2137        let result = vim.update_editor(cx, |vim, editor, cx| {
2138            self.range.buffer_range(vim, editor, window, cx)
2139        });
2140
2141        let range = match result {
2142            None => return,
2143            Some(e @ Err(_)) => {
2144                let Some(workspace) = vim.workspace(window, cx) else {
2145                    return;
2146                };
2147                workspace.update(cx, |workspace, cx| {
2148                    e.notify_err(workspace, cx);
2149                });
2150                return;
2151            }
2152            Some(Ok(result)) => result,
2153        };
2154
2155        let mut action = self.action.boxed_clone();
2156        let mut last_pattern = self.search.clone();
2157
2158        let mut regexes = match Regex::new(&self.search) {
2159            Ok(regex) => vec![(regex, !self.invert)],
2160            e @ Err(_) => {
2161                let Some(workspace) = vim.workspace(window, cx) else {
2162                    return;
2163                };
2164                workspace.update(cx, |workspace, cx| {
2165                    e.notify_err(workspace, cx);
2166                });
2167                return;
2168            }
2169        };
2170        while let Some(inner) = action
2171            .boxed_clone()
2172            .as_any()
2173            .downcast_ref::<OnMatchingLines>()
2174        {
2175            let Some(regex) = Regex::new(&inner.search).ok() else {
2176                break;
2177            };
2178            last_pattern = inner.search.clone();
2179            action = inner.action.boxed_clone();
2180            regexes.push((regex, !inner.invert))
2181        }
2182
2183        if let Some(pane) = vim.pane(window, cx) {
2184            pane.update(cx, |pane, cx| {
2185                if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()
2186                {
2187                    search_bar.update(cx, |search_bar, cx| {
2188                        if search_bar.show(window, cx) {
2189                            let _ = search_bar.search(
2190                                &last_pattern,
2191                                Some(SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE),
2192                                false,
2193                                window,
2194                                cx,
2195                            );
2196                        }
2197                    });
2198                }
2199            });
2200        };
2201
2202        vim.update_editor(cx, |_, editor, cx| {
2203            let snapshot = editor.snapshot(window, cx);
2204            let mut row = range.start.0;
2205
2206            let point_range = Point::new(range.start.0, 0)
2207                ..snapshot
2208                    .buffer_snapshot()
2209                    .clip_point(Point::new(range.end.0 + 1, 0), Bias::Left);
2210            cx.spawn_in(window, async move |editor, cx| {
2211                let new_selections = cx
2212                    .background_spawn(async move {
2213                        let mut line = String::new();
2214                        let mut new_selections = Vec::new();
2215                        let chunks = snapshot
2216                            .buffer_snapshot()
2217                            .text_for_range(point_range)
2218                            .chain(["\n"]);
2219
2220                        for chunk in chunks {
2221                            for (newline_ix, text) in chunk.split('\n').enumerate() {
2222                                if newline_ix > 0 {
2223                                    if regexes.iter().all(|(regex, should_match)| {
2224                                        regex.is_match(&line) == *should_match
2225                                    }) {
2226                                        new_selections
2227                                            .push(Point::new(row, 0).to_display_point(&snapshot))
2228                                    }
2229                                    row += 1;
2230                                    line.clear();
2231                                }
2232                                line.push_str(text)
2233                            }
2234                        }
2235
2236                        new_selections
2237                    })
2238                    .await;
2239
2240                if new_selections.is_empty() {
2241                    return;
2242                }
2243
2244                if let Some(vim_norm) = action.as_any().downcast_ref::<VimNorm>() {
2245                    let mut vim_norm = vim_norm.clone();
2246                    vim_norm.override_rows =
2247                        Some(new_selections.iter().map(|point| point.row().0).collect());
2248                    editor
2249                        .update_in(cx, |_, window, cx| {
2250                            window.dispatch_action(vim_norm.boxed_clone(), cx);
2251                        })
2252                        .log_err();
2253                    return;
2254                }
2255
2256                editor
2257                    .update_in(cx, |editor, window, cx| {
2258                        editor.start_transaction_at(Instant::now(), window, cx);
2259                        editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2260                            s.replace_cursors_with(|_| new_selections);
2261                        });
2262                        window.dispatch_action(action, cx);
2263
2264                        cx.defer_in(window, move |editor, window, cx| {
2265                            let newest = editor
2266                                .selections
2267                                .newest::<Point>(&editor.display_snapshot(cx));
2268                            editor.change_selections(
2269                                SelectionEffects::no_scroll(),
2270                                window,
2271                                cx,
2272                                |s| {
2273                                    s.select(vec![newest]);
2274                                },
2275                            );
2276                            editor.end_transaction_at(Instant::now(), cx);
2277                        })
2278                    })
2279                    .log_err();
2280            })
2281            .detach();
2282        });
2283    }
2284}
2285
2286/// Executes a shell command and returns the output.
2287#[derive(Clone, Debug, PartialEq, Action)]
2288#[action(namespace = vim, no_json, no_register)]
2289pub struct ShellExec {
2290    command: String,
2291    range: Option<CommandRange>,
2292    is_read: bool,
2293}
2294
2295impl Vim {
2296    pub fn cancel_running_command(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2297        if self.running_command.take().is_some() {
2298            self.update_editor(cx, |_, editor, cx| {
2299                editor.transact(window, cx, |editor, _window, _cx| {
2300                    editor.clear_row_highlights::<ShellExec>();
2301                })
2302            });
2303        }
2304    }
2305
2306    fn prepare_shell_command(
2307        &mut self,
2308        command: &str,
2309        _: &mut Window,
2310        cx: &mut Context<Self>,
2311    ) -> String {
2312        let mut ret = String::new();
2313        // N.B. non-standard escaping rules:
2314        // * !echo % => "echo README.md"
2315        // * !echo \% => "echo %"
2316        // * !echo \\% => echo \%
2317        // * !echo \\\% => echo \\%
2318        for c in command.chars() {
2319            if c != '%' && c != '!' {
2320                ret.push(c);
2321                continue;
2322            } else if ret.chars().last() == Some('\\') {
2323                ret.pop();
2324                ret.push(c);
2325                continue;
2326            }
2327            match c {
2328                '%' => {
2329                    self.update_editor(cx, |_, editor, cx| {
2330                        if let Some(buffer) = editor.active_buffer(cx)
2331                            && let Some(file) = buffer.read(cx).file()
2332                            && let Some(local) = file.as_local()
2333                        {
2334                            ret.push_str(&local.path().display(local.path_style(cx)));
2335                        }
2336                    });
2337                }
2338                '!' => {
2339                    if let Some(command) = &self.last_command {
2340                        ret.push_str(command)
2341                    }
2342                }
2343                _ => {}
2344            }
2345        }
2346        self.last_command = Some(ret.clone());
2347        ret
2348    }
2349
2350    pub fn shell_command_motion(
2351        &mut self,
2352        motion: Motion,
2353        times: Option<usize>,
2354        forced_motion: bool,
2355        window: &mut Window,
2356        cx: &mut Context<Vim>,
2357    ) {
2358        self.stop_recording(cx);
2359        let Some(workspace) = self.workspace(window, cx) else {
2360            return;
2361        };
2362        let command = self.update_editor(cx, |_, editor, cx| {
2363            let snapshot = editor.snapshot(window, cx);
2364            let start = editor
2365                .selections
2366                .newest_display(&editor.display_snapshot(cx));
2367            let text_layout_details = editor.text_layout_details(window, cx);
2368            let (mut range, _) = motion
2369                .range(
2370                    &snapshot,
2371                    start.clone(),
2372                    times,
2373                    &text_layout_details,
2374                    forced_motion,
2375                )
2376                .unwrap_or((start.range(), MotionKind::Exclusive));
2377            if range.start != start.start {
2378                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2379                    s.select_ranges([
2380                        range.start.to_point(&snapshot)..range.start.to_point(&snapshot)
2381                    ]);
2382                })
2383            }
2384            if range.end.row() > range.start.row() && range.end.column() != 0 {
2385                *range.end.row_mut() -= 1
2386            }
2387            if range.end.row() == range.start.row() {
2388                ".!".to_string()
2389            } else {
2390                format!(".,.+{}!", (range.end.row() - range.start.row()).0)
2391            }
2392        });
2393        if let Some(command) = command {
2394            workspace.update(cx, |workspace, cx| {
2395                command_palette::CommandPalette::toggle(workspace, &command, window, cx);
2396            });
2397        }
2398    }
2399
2400    pub fn shell_command_object(
2401        &mut self,
2402        object: Object,
2403        around: bool,
2404        window: &mut Window,
2405        cx: &mut Context<Vim>,
2406    ) {
2407        self.stop_recording(cx);
2408        let Some(workspace) = self.workspace(window, cx) else {
2409            return;
2410        };
2411        let command = self.update_editor(cx, |_, editor, cx| {
2412            let snapshot = editor.snapshot(window, cx);
2413            let start = editor
2414                .selections
2415                .newest_display(&editor.display_snapshot(cx));
2416            let range = object
2417                .range(&snapshot, start.clone(), around, None)
2418                .unwrap_or(start.range());
2419            if range.start != start.start {
2420                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2421                    s.select_ranges([
2422                        range.start.to_point(&snapshot)..range.start.to_point(&snapshot)
2423                    ]);
2424                })
2425            }
2426            if range.end.row() == range.start.row() {
2427                ".!".to_string()
2428            } else {
2429                format!(".,.+{}!", (range.end.row() - range.start.row()).0)
2430            }
2431        });
2432        if let Some(command) = command {
2433            workspace.update(cx, |workspace, cx| {
2434                command_palette::CommandPalette::toggle(workspace, &command, window, cx);
2435            });
2436        }
2437    }
2438}
2439
2440impl ShellExec {
2441    pub fn parse(query: &str, range: Option<CommandRange>) -> Option<Box<dyn Action>> {
2442        let (before, after) = query.split_once('!')?;
2443        let before = before.trim();
2444
2445        if !"read".starts_with(before) {
2446            return None;
2447        }
2448
2449        Some(
2450            ShellExec {
2451                command: after.trim().to_string(),
2452                range,
2453                is_read: !before.is_empty(),
2454            }
2455            .boxed_clone(),
2456        )
2457    }
2458
2459    pub fn run(&self, vim: &mut Vim, window: &mut Window, cx: &mut Context<Vim>) {
2460        let Some(workspace) = vim.workspace(window, cx) else {
2461            return;
2462        };
2463
2464        let project = workspace.read(cx).project().clone();
2465        let command = vim.prepare_shell_command(&self.command, window, cx);
2466
2467        if self.range.is_none() && !self.is_read {
2468            workspace.update(cx, |workspace, cx| {
2469                let project = workspace.project().read(cx);
2470                let cwd = project.first_project_directory(cx);
2471                let shell = Shell::System;
2472
2473                let spawn_in_terminal = SpawnInTerminal {
2474                    id: TaskId("vim".to_string()),
2475                    full_label: command.clone(),
2476                    label: command.clone(),
2477                    command: Some(command.clone()),
2478                    args: Vec::new(),
2479                    command_label: command.clone(),
2480                    cwd,
2481                    env: HashMap::default(),
2482                    use_new_terminal: true,
2483                    allow_concurrent_runs: true,
2484                    reveal: RevealStrategy::NoFocus,
2485                    reveal_target: RevealTarget::Dock,
2486                    hide: HideStrategy::Never,
2487                    shell,
2488                    show_summary: false,
2489                    show_command: false,
2490                    show_rerun: false,
2491                    save: SaveStrategy::default(),
2492                };
2493
2494                let task_status = workspace.spawn_in_terminal(spawn_in_terminal, window, cx);
2495                cx.background_spawn(async move {
2496                    match task_status.await {
2497                        Some(Ok(status)) => {
2498                            if status.success() {
2499                                log::debug!("Vim shell exec succeeded");
2500                            } else {
2501                                log::debug!("Vim shell exec failed, code: {:?}", status.code());
2502                            }
2503                        }
2504                        Some(Err(e)) => log::error!("Vim shell exec failed: {e}"),
2505                        None => log::debug!("Vim shell exec got cancelled"),
2506                    }
2507                })
2508                .detach();
2509            });
2510            return;
2511        };
2512
2513        let mut input_snapshot = None;
2514        let mut input_range = None;
2515        let mut needs_newline_prefix = false;
2516        vim.update_editor(cx, |vim, editor, cx| {
2517            let snapshot = editor.buffer().read(cx).snapshot(cx);
2518            let range = if let Some(range) = self.range.clone() {
2519                let Some(range) = range.buffer_range(vim, editor, window, cx).log_err() else {
2520                    return;
2521                };
2522                Point::new(range.start.0, 0)
2523                    ..snapshot.clip_point(Point::new(range.end.0 + 1, 0), Bias::Right)
2524            } else {
2525                let mut end = editor
2526                    .selections
2527                    .newest::<Point>(&editor.display_snapshot(cx))
2528                    .range()
2529                    .end;
2530                end = snapshot.clip_point(Point::new(end.row + 1, 0), Bias::Right);
2531                needs_newline_prefix = end == snapshot.max_point();
2532                end..end
2533            };
2534            if self.is_read {
2535                input_range =
2536                    Some(snapshot.anchor_after(range.end)..snapshot.anchor_after(range.end));
2537            } else {
2538                input_range =
2539                    Some(snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end));
2540            }
2541            editor.highlight_rows::<ShellExec>(
2542                input_range.clone().unwrap(),
2543                |cx| cx.theme().status().unreachable_background,
2544                Default::default(),
2545                cx,
2546            );
2547
2548            if !self.is_read {
2549                input_snapshot = Some(snapshot)
2550            }
2551        });
2552
2553        let Some(range) = input_range else { return };
2554
2555        let process_task = project.update(cx, |project, cx| project.exec_in_shell(command, cx));
2556
2557        let is_read = self.is_read;
2558
2559        let task = cx.spawn_in(window, async move |vim, cx| {
2560            let Some(mut process) = process_task.await.log_err() else {
2561                return;
2562            };
2563            process.stdout(Stdio::piped());
2564            process.stderr(Stdio::piped());
2565
2566            if input_snapshot.is_some() {
2567                process.stdin(Stdio::piped());
2568            } else {
2569                process.stdin(Stdio::null());
2570            };
2571
2572            let Some(mut running) = process.spawn().log_err() else {
2573                vim.update_in(cx, |vim, window, cx| {
2574                    vim.cancel_running_command(window, cx);
2575                })
2576                .log_err();
2577                return;
2578            };
2579
2580            if let Some(mut stdin) = running.stdin.take()
2581                && let Some(snapshot) = input_snapshot
2582            {
2583                let range = range.clone();
2584                cx.background_spawn(async move {
2585                    for chunk in snapshot.text_for_range(range) {
2586                        if stdin.write_all(chunk.as_bytes()).await.log_err().is_none() {
2587                            return;
2588                        }
2589                    }
2590                    stdin.flush().await.log_err();
2591                })
2592                .detach();
2593            };
2594
2595            let output = cx.background_spawn(running.output()).await;
2596
2597            let Some(output) = output.log_err() else {
2598                vim.update_in(cx, |vim, window, cx| {
2599                    vim.cancel_running_command(window, cx);
2600                })
2601                .log_err();
2602                return;
2603            };
2604            let mut text = String::new();
2605            if needs_newline_prefix {
2606                text.push('\n');
2607            }
2608            text.push_str(&String::from_utf8_lossy(&output.stdout));
2609            text.push_str(&String::from_utf8_lossy(&output.stderr));
2610            if !text.is_empty() && text.chars().last() != Some('\n') {
2611                text.push('\n');
2612            }
2613
2614            vim.update_in(cx, |vim, window, cx| {
2615                vim.update_editor(cx, |_, editor, cx| {
2616                    editor.transact(window, cx, |editor, window, cx| {
2617                        editor.edit([(range.clone(), text)], cx);
2618                        let snapshot = editor.buffer().read(cx).snapshot(cx);
2619                        editor.change_selections(Default::default(), window, cx, |s| {
2620                            let point = if is_read {
2621                                let point = range.end.to_point(&snapshot);
2622                                Point::new(point.row.saturating_sub(1), 0)
2623                            } else {
2624                                let point = range.start.to_point(&snapshot);
2625                                Point::new(point.row, 0)
2626                            };
2627                            s.select_ranges([point..point]);
2628                        })
2629                    })
2630                });
2631                vim.cancel_running_command(window, cx);
2632            })
2633            .log_err();
2634        });
2635        vim.running_command.replace(task);
2636    }
2637}
2638
2639#[cfg(test)]
2640mod test {
2641    use std::path::{Path, PathBuf};
2642
2643    use crate::{
2644        VimAddon,
2645        state::Mode,
2646        test::{NeovimBackedTestContext, VimTestContext},
2647    };
2648    use editor::{Editor, EditorSettings};
2649    use gpui::{Context, TestAppContext};
2650    use indoc::indoc;
2651    use settings::Settings;
2652    use util::path;
2653    use workspace::{OpenOptions, Workspace};
2654
2655    #[gpui::test]
2656    async fn test_command_basics(cx: &mut TestAppContext) {
2657        let mut cx = NeovimBackedTestContext::new(cx).await;
2658
2659        cx.set_shared_state(indoc! {"
2660            ˇa
2661            b
2662            c"})
2663            .await;
2664
2665        cx.simulate_shared_keystrokes(": j enter").await;
2666
2667        // hack: our cursor positioning after a join command is wrong
2668        cx.simulate_shared_keystrokes("^").await;
2669        cx.shared_state().await.assert_eq(indoc! {
2670            "ˇa b
2671            c"
2672        });
2673    }
2674
2675    #[gpui::test]
2676    async fn test_command_goto(cx: &mut TestAppContext) {
2677        let mut cx = NeovimBackedTestContext::new(cx).await;
2678
2679        cx.set_shared_state(indoc! {"
2680            ˇa
2681            b
2682            c"})
2683            .await;
2684        cx.simulate_shared_keystrokes(": 3 enter").await;
2685        cx.shared_state().await.assert_eq(indoc! {"
2686            a
2687            b
2688            ˇc"});
2689    }
2690
2691    #[gpui::test]
2692    async fn test_command_replace(cx: &mut TestAppContext) {
2693        let mut cx = NeovimBackedTestContext::new(cx).await;
2694
2695        cx.set_shared_state(indoc! {"
2696            ˇa
2697            b
2698            b
2699            c"})
2700            .await;
2701        cx.simulate_shared_keystrokes(": % s / b / d enter").await;
2702        cx.shared_state().await.assert_eq(indoc! {"
2703            a
2704            d
2705            ˇd
2706            c"});
2707        cx.simulate_shared_keystrokes(": % s : . : \\ 0 \\ 0 enter")
2708            .await;
2709        cx.shared_state().await.assert_eq(indoc! {"
2710            aa
2711            dd
2712            dd
2713            ˇcc"});
2714        cx.simulate_shared_keystrokes("k : s / d d / e e enter")
2715            .await;
2716        cx.shared_state().await.assert_eq(indoc! {"
2717            aa
2718            dd
2719            ˇee
2720            cc"});
2721    }
2722
2723    #[gpui::test]
2724    async fn test_command_search(cx: &mut TestAppContext) {
2725        let mut cx = NeovimBackedTestContext::new(cx).await;
2726
2727        cx.set_shared_state(indoc! {"
2728                ˇa
2729                b
2730                a
2731                c"})
2732            .await;
2733        cx.simulate_shared_keystrokes(": / b enter").await;
2734        cx.shared_state().await.assert_eq(indoc! {"
2735                a
2736                ˇb
2737                a
2738                c"});
2739        cx.simulate_shared_keystrokes(": ? a enter").await;
2740        cx.shared_state().await.assert_eq(indoc! {"
2741                ˇa
2742                b
2743                a
2744                c"});
2745    }
2746
2747    #[gpui::test]
2748    async fn test_command_write(cx: &mut TestAppContext) {
2749        let mut cx = VimTestContext::new(cx, true).await;
2750        let path = Path::new(path!("/root/dir/file.rs"));
2751        let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
2752
2753        cx.simulate_keystrokes("i @ escape");
2754        cx.simulate_keystrokes(": w enter");
2755
2756        assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "@\n");
2757
2758        fs.as_fake().insert_file(path, b"oops\n".to_vec()).await;
2759
2760        // conflict!
2761        cx.simulate_keystrokes("i @ escape");
2762        cx.simulate_keystrokes(": w enter");
2763        cx.simulate_prompt_answer("Cancel");
2764
2765        assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "oops\n");
2766        assert!(!cx.has_pending_prompt());
2767        cx.simulate_keystrokes(": w !");
2768        cx.simulate_keystrokes("enter");
2769        assert!(!cx.has_pending_prompt());
2770        assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "@@\n");
2771    }
2772
2773    #[gpui::test]
2774    async fn test_command_read(cx: &mut TestAppContext) {
2775        let mut cx = VimTestContext::new(cx, true).await;
2776
2777        let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
2778        let path = Path::new(path!("/root/dir/other.rs"));
2779        fs.as_fake().insert_file(path, "1\n2\n3".into()).await;
2780
2781        cx.workspace(|workspace, _, cx| {
2782            assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
2783        });
2784
2785        // File without trailing newline
2786        cx.set_state("one\ntwo\nthreeˇ", Mode::Normal);
2787        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2788        cx.simulate_keystrokes("enter");
2789        cx.assert_state("one\ntwo\nthree\nˇ1\n2\n3", Mode::Normal);
2790
2791        cx.set_state("oneˇ\ntwo\nthree", Mode::Normal);
2792        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2793        cx.simulate_keystrokes("enter");
2794        cx.assert_state("one\nˇ1\n2\n3\ntwo\nthree", Mode::Normal);
2795
2796        cx.set_state("one\nˇtwo\nthree", Mode::Normal);
2797        cx.simulate_keystrokes(": 0 r space d i r / o t h e r . r s");
2798        cx.simulate_keystrokes("enter");
2799        cx.assert_state("ˇ1\n2\n3\none\ntwo\nthree", Mode::Normal);
2800
2801        cx.set_state("one\n«ˇtwo\nthree\nfour»\nfive", Mode::Visual);
2802        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2803        cx.simulate_keystrokes("enter");
2804        cx.run_until_parked();
2805        cx.assert_state("one\ntwo\nthree\nfour\nˇ1\n2\n3\nfive", Mode::Normal);
2806
2807        // Empty filename
2808        cx.set_state("oneˇ\ntwo\nthree", Mode::Normal);
2809        cx.simulate_keystrokes(": r");
2810        cx.simulate_keystrokes("enter");
2811        cx.assert_state("one\nˇone\ntwo\nthree\ntwo\nthree", Mode::Normal);
2812
2813        // File with trailing newline
2814        fs.as_fake().insert_file(path, "1\n2\n3\n".into()).await;
2815        cx.set_state("one\ntwo\nthreeˇ", Mode::Normal);
2816        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2817        cx.simulate_keystrokes("enter");
2818        cx.assert_state("one\ntwo\nthree\nˇ1\n2\n3\n", Mode::Normal);
2819
2820        cx.set_state("oneˇ\ntwo\nthree", Mode::Normal);
2821        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2822        cx.simulate_keystrokes("enter");
2823        cx.assert_state("one\nˇ1\n2\n3\n\ntwo\nthree", Mode::Normal);
2824
2825        cx.set_state("one\n«ˇtwo\nthree\nfour»\nfive", Mode::Visual);
2826        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2827        cx.simulate_keystrokes("enter");
2828        cx.assert_state("one\ntwo\nthree\nfour\nˇ1\n2\n3\n\nfive", Mode::Normal);
2829
2830        cx.set_state("«one\ntwo\nthreeˇ»", Mode::Visual);
2831        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2832        cx.simulate_keystrokes("enter");
2833        cx.assert_state("one\ntwo\nthree\nˇ1\n2\n3\n", Mode::Normal);
2834
2835        // Empty file
2836        fs.as_fake().insert_file(path, "".into()).await;
2837        cx.set_state("ˇone\ntwo\nthree", Mode::Normal);
2838        cx.simulate_keystrokes(": r space d i r / o t h e r . r s");
2839        cx.simulate_keystrokes("enter");
2840        cx.assert_state("one\nˇtwo\nthree", Mode::Normal);
2841    }
2842
2843    #[gpui::test]
2844    async fn test_command_quit(cx: &mut TestAppContext) {
2845        let mut cx = VimTestContext::new(cx, true).await;
2846
2847        cx.simulate_keystrokes(": n e w enter");
2848        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
2849        cx.simulate_keystrokes(": q enter");
2850        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 1));
2851        cx.simulate_keystrokes(": n e w enter");
2852        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
2853        cx.simulate_keystrokes(": q a enter");
2854        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 0));
2855    }
2856
2857    #[gpui::test]
2858    async fn test_offsets(cx: &mut TestAppContext) {
2859        let mut cx = NeovimBackedTestContext::new(cx).await;
2860
2861        cx.set_shared_state("ˇ1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n")
2862            .await;
2863
2864        cx.simulate_shared_keystrokes(": + enter").await;
2865        cx.shared_state()
2866            .await
2867            .assert_eq("1\nˇ2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n");
2868
2869        cx.simulate_shared_keystrokes(": 1 0 - enter").await;
2870        cx.shared_state()
2871            .await
2872            .assert_eq("1\n2\n3\n4\n5\n6\n7\n8\nˇ9\n10\n11\n");
2873
2874        cx.simulate_shared_keystrokes(": . - 2 enter").await;
2875        cx.shared_state()
2876            .await
2877            .assert_eq("1\n2\n3\n4\n5\n6\nˇ7\n8\n9\n10\n11\n");
2878
2879        cx.simulate_shared_keystrokes(": % enter").await;
2880        cx.shared_state()
2881            .await
2882            .assert_eq("1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\nˇ");
2883    }
2884
2885    #[gpui::test]
2886    async fn test_command_ranges(cx: &mut TestAppContext) {
2887        let mut cx = NeovimBackedTestContext::new(cx).await;
2888
2889        cx.set_shared_state("ˇ1\n2\n3\n4\n4\n3\n2\n1").await;
2890
2891        cx.simulate_shared_keystrokes(": 2 , 4 d enter").await;
2892        cx.shared_state().await.assert_eq("1\nˇ4\n3\n2\n1");
2893
2894        cx.simulate_shared_keystrokes(": 2 , 4 s o r t enter").await;
2895        cx.shared_state().await.assert_eq("1\nˇ2\n3\n4\n1");
2896
2897        cx.simulate_shared_keystrokes(": 2 , 4 j o i n enter").await;
2898        cx.shared_state().await.assert_eq("1\nˇ2 3 4\n1");
2899    }
2900
2901    #[gpui::test]
2902    async fn test_command_visual_replace(cx: &mut TestAppContext) {
2903        let mut cx = NeovimBackedTestContext::new(cx).await;
2904
2905        cx.set_shared_state("ˇ1\n2\n3\n4\n4\n3\n2\n1").await;
2906
2907        cx.simulate_shared_keystrokes("v 2 j : s / . / k enter")
2908            .await;
2909        cx.shared_state().await.assert_eq("k\nk\nˇk\n4\n4\n3\n2\n1");
2910    }
2911
2912    #[track_caller]
2913    fn assert_active_item(
2914        workspace: &mut Workspace,
2915        expected_path: &str,
2916        expected_text: &str,
2917        cx: &mut Context<Workspace>,
2918    ) {
2919        let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();
2920
2921        let buffer = active_editor
2922            .read(cx)
2923            .buffer()
2924            .read(cx)
2925            .as_singleton()
2926            .unwrap();
2927
2928        let text = buffer.read(cx).text();
2929        let file = buffer.read(cx).file().unwrap();
2930        let file_path = file.as_local().unwrap().abs_path(cx);
2931
2932        assert_eq!(text, expected_text);
2933        assert_eq!(file_path, Path::new(expected_path));
2934    }
2935
2936    #[gpui::test]
2937    async fn test_command_gf(cx: &mut TestAppContext) {
2938        let mut cx = VimTestContext::new(cx, true).await;
2939
2940        // Assert base state, that we're in /root/dir/file.rs
2941        cx.workspace(|workspace, _, cx| {
2942            assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
2943        });
2944
2945        // Insert a new file
2946        let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
2947        fs.as_fake()
2948            .insert_file(
2949                path!("/root/dir/file2.rs"),
2950                "This is file2.rs".as_bytes().to_vec(),
2951            )
2952            .await;
2953        fs.as_fake()
2954            .insert_file(
2955                path!("/root/dir/file3.rs"),
2956                "go to file3".as_bytes().to_vec(),
2957            )
2958            .await;
2959
2960        // Put the path to the second file into the currently open buffer
2961        cx.set_state(indoc! {"go to fiˇle2.rs"}, Mode::Normal);
2962
2963        // Go to file2.rs
2964        cx.simulate_keystrokes("g f");
2965
2966        // We now have two items
2967        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
2968        cx.workspace(|workspace, _, cx| {
2969            assert_active_item(
2970                workspace,
2971                path!("/root/dir/file2.rs"),
2972                "This is file2.rs",
2973                cx,
2974            );
2975        });
2976
2977        // Update editor to point to `file2.rs`
2978        cx.editor =
2979            cx.workspace(|workspace, _, cx| workspace.active_item_as::<Editor>(cx).unwrap());
2980
2981        // Put the path to the third file into the currently open buffer,
2982        // but remove its suffix, because we want that lookup to happen automatically.
2983        cx.set_state(indoc! {"go to fiˇle3"}, Mode::Normal);
2984
2985        // Go to file3.rs
2986        cx.simulate_keystrokes("g f");
2987
2988        // We now have three items
2989        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 3));
2990        cx.workspace(|workspace, _, cx| {
2991            assert_active_item(workspace, path!("/root/dir/file3.rs"), "go to file3", cx);
2992        });
2993    }
2994
2995    #[gpui::test]
2996    async fn test_command_write_filename(cx: &mut TestAppContext) {
2997        let mut cx = VimTestContext::new(cx, true).await;
2998
2999        cx.workspace(|workspace, _, cx| {
3000            assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
3001        });
3002
3003        cx.simulate_keystrokes(": w space other.rs");
3004        cx.simulate_keystrokes("enter");
3005
3006        cx.workspace(|workspace, _, cx| {
3007            assert_active_item(workspace, path!("/root/other.rs"), "", cx);
3008        });
3009
3010        cx.simulate_keystrokes(": w space dir/file.rs");
3011        cx.simulate_keystrokes("enter");
3012
3013        cx.simulate_prompt_answer("Replace");
3014        cx.run_until_parked();
3015
3016        cx.workspace(|workspace, _, cx| {
3017            assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
3018        });
3019
3020        cx.simulate_keystrokes(": w ! space other.rs");
3021        cx.simulate_keystrokes("enter");
3022
3023        cx.workspace(|workspace, _, cx| {
3024            assert_active_item(workspace, path!("/root/other.rs"), "", cx);
3025        });
3026    }
3027
3028    #[gpui::test]
3029    async fn test_command_write_range(cx: &mut TestAppContext) {
3030        let mut cx = VimTestContext::new(cx, true).await;
3031
3032        cx.workspace(|workspace, _, cx| {
3033            assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
3034        });
3035
3036        cx.set_state(
3037            indoc! {"
3038                    The quick
3039                    brown« fox
3040                    jumpsˇ» over
3041                    the lazy dog
3042                "},
3043            Mode::Visual,
3044        );
3045
3046        cx.simulate_keystrokes(": w space dir/other.rs");
3047        cx.simulate_keystrokes("enter");
3048
3049        let other = path!("/root/dir/other.rs");
3050
3051        let _ = cx
3052            .workspace(|workspace, window, cx| {
3053                workspace.open_abs_path(PathBuf::from(other), OpenOptions::default(), window, cx)
3054            })
3055            .await;
3056
3057        cx.workspace(|workspace, _, cx| {
3058            assert_active_item(
3059                workspace,
3060                other,
3061                indoc! {"
3062                        brown fox
3063                        jumps over
3064                    "},
3065                cx,
3066            );
3067        });
3068    }
3069
3070    #[gpui::test]
3071    async fn test_command_matching_lines(cx: &mut TestAppContext) {
3072        let mut cx = NeovimBackedTestContext::new(cx).await;
3073
3074        cx.set_shared_state(indoc! {"
3075            ˇa
3076            b
3077            a
3078            b
3079            a
3080        "})
3081            .await;
3082
3083        cx.simulate_shared_keystrokes(":").await;
3084        cx.simulate_shared_keystrokes("g / a / d").await;
3085        cx.simulate_shared_keystrokes("enter").await;
3086
3087        cx.shared_state().await.assert_eq(indoc! {"
3088            b
3089            b
3090            ˇ"});
3091
3092        cx.simulate_shared_keystrokes("u").await;
3093
3094        cx.shared_state().await.assert_eq(indoc! {"
3095            ˇa
3096            b
3097            a
3098            b
3099            a
3100        "});
3101
3102        cx.simulate_shared_keystrokes(":").await;
3103        cx.simulate_shared_keystrokes("v / a / d").await;
3104        cx.simulate_shared_keystrokes("enter").await;
3105
3106        cx.shared_state().await.assert_eq(indoc! {"
3107            a
3108            a
3109            ˇa"});
3110    }
3111
3112    #[gpui::test]
3113    async fn test_del_marks(cx: &mut TestAppContext) {
3114        let mut cx = NeovimBackedTestContext::new(cx).await;
3115
3116        cx.set_shared_state(indoc! {"
3117            ˇa
3118            b
3119            a
3120            b
3121            a
3122        "})
3123            .await;
3124
3125        cx.simulate_shared_keystrokes("m a").await;
3126
3127        let mark = cx.update_editor(|editor, window, cx| {
3128            let vim = editor.addon::<VimAddon>().unwrap().entity.clone();
3129            vim.update(cx, |vim, cx| vim.get_mark("a", editor, window, cx))
3130        });
3131        assert!(mark.is_some());
3132
3133        cx.simulate_shared_keystrokes(": d e l m space a").await;
3134        cx.simulate_shared_keystrokes("enter").await;
3135
3136        let mark = cx.update_editor(|editor, window, cx| {
3137            let vim = editor.addon::<VimAddon>().unwrap().entity.clone();
3138            vim.update(cx, |vim, cx| vim.get_mark("a", editor, window, cx))
3139        });
3140        assert!(mark.is_none())
3141    }
3142
3143    #[gpui::test]
3144    async fn test_normal_command(cx: &mut TestAppContext) {
3145        let mut cx = NeovimBackedTestContext::new(cx).await;
3146
3147        cx.set_shared_state(indoc! {"
3148            The quick
3149            brown« fox
3150            jumpsˇ» over
3151            the lazy dog
3152        "})
3153            .await;
3154
3155        cx.simulate_shared_keystrokes(": n o r m space w C w o r d")
3156            .await;
3157        cx.simulate_shared_keystrokes("enter").await;
3158
3159        cx.shared_state().await.assert_eq(indoc! {"
3160            The quick
3161            brown word
3162            jumps worˇd
3163            the lazy dog
3164        "});
3165
3166        cx.simulate_shared_keystrokes(": n o r m space _ w c i w t e s t")
3167            .await;
3168        cx.simulate_shared_keystrokes("enter").await;
3169
3170        cx.shared_state().await.assert_eq(indoc! {"
3171            The quick
3172            brown word
3173            jumps tesˇt
3174            the lazy dog
3175        "});
3176
3177        cx.simulate_shared_keystrokes("_ l v l : n o r m space s l a")
3178            .await;
3179        cx.simulate_shared_keystrokes("enter").await;
3180
3181        cx.shared_state().await.assert_eq(indoc! {"
3182            The quick
3183            brown word
3184            lˇaumps test
3185            the lazy dog
3186        "});
3187
3188        cx.set_shared_state(indoc! {"
3189            ˇThe quick
3190            brown fox
3191            jumps over
3192            the lazy dog
3193        "})
3194            .await;
3195
3196        cx.simulate_shared_keystrokes("c i w M y escape").await;
3197
3198        cx.shared_state().await.assert_eq(indoc! {"
3199            Mˇy quick
3200            brown fox
3201            jumps over
3202            the lazy dog
3203        "});
3204
3205        cx.simulate_shared_keystrokes(": n o r m space u").await;
3206        cx.simulate_shared_keystrokes("enter").await;
3207
3208        cx.shared_state().await.assert_eq(indoc! {"
3209            ˇThe quick
3210            brown fox
3211            jumps over
3212            the lazy dog
3213        "});
3214
3215        cx.set_shared_state(indoc! {"
3216            The« quick
3217            brownˇ» fox
3218            jumps over
3219            the lazy dog
3220        "})
3221            .await;
3222
3223        cx.simulate_shared_keystrokes(": n o r m space I 1 2 3")
3224            .await;
3225        cx.simulate_shared_keystrokes("enter").await;
3226        cx.simulate_shared_keystrokes("u").await;
3227
3228        cx.shared_state().await.assert_eq(indoc! {"
3229            ˇThe quick
3230            brown fox
3231            jumps over
3232            the lazy dog
3233        "});
3234
3235        cx.set_shared_state(indoc! {"
3236            ˇquick
3237            brown fox
3238            jumps over
3239            the lazy dog
3240        "})
3241            .await;
3242
3243        cx.simulate_shared_keystrokes(": n o r m space I T h e space")
3244            .await;
3245        cx.simulate_shared_keystrokes("enter").await;
3246
3247        cx.shared_state().await.assert_eq(indoc! {"
3248            Theˇ quick
3249            brown fox
3250            jumps over
3251            the lazy dog
3252        "});
3253
3254        // Once ctrl-v to input character literals is added there should be a test for redo
3255    }
3256
3257    #[gpui::test]
3258    async fn test_command_g_normal(cx: &mut TestAppContext) {
3259        let mut cx = NeovimBackedTestContext::new(cx).await;
3260
3261        cx.set_shared_state(indoc! {"
3262            ˇfoo
3263
3264            foo
3265        "})
3266            .await;
3267
3268        cx.simulate_shared_keystrokes(": % g / f o o / n o r m space A b a r")
3269            .await;
3270        cx.simulate_shared_keystrokes("enter").await;
3271        cx.run_until_parked();
3272
3273        cx.shared_state().await.assert_eq(indoc! {"
3274            foobar
3275
3276            foobaˇr
3277        "});
3278
3279        cx.simulate_shared_keystrokes("u").await;
3280
3281        cx.shared_state().await.assert_eq(indoc! {"
3282            foˇo
3283
3284            foo
3285        "});
3286    }
3287
3288    #[gpui::test]
3289    async fn test_command_tabnew(cx: &mut TestAppContext) {
3290        let mut cx = VimTestContext::new(cx, true).await;
3291
3292        // Create a new file to ensure that, when the filename is used with
3293        // `:tabnew`, it opens the existing file in a new tab.
3294        let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
3295        fs.as_fake()
3296            .insert_file(path!("/root/dir/file_2.rs"), "file_2".as_bytes().to_vec())
3297            .await;
3298
3299        cx.simulate_keystrokes(": tabnew");
3300        cx.simulate_keystrokes("enter");
3301        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
3302
3303        // Assert that the new tab is empty and not associated with any file, as
3304        // no file path was provided to the `:tabnew` command.
3305        cx.workspace(|workspace, _window, cx| {
3306            let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();
3307            let buffer = active_editor
3308                .read(cx)
3309                .buffer()
3310                .read(cx)
3311                .as_singleton()
3312                .unwrap();
3313
3314            assert!(&buffer.read(cx).file().is_none());
3315        });
3316
3317        // Leverage the filename as an argument to the `:tabnew` command,
3318        // ensuring that the file, instead of an empty buffer, is opened in a
3319        // new tab.
3320        cx.simulate_keystrokes(": tabnew space dir/file_2.rs");
3321        cx.simulate_keystrokes("enter");
3322
3323        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 3));
3324        cx.workspace(|workspace, _, cx| {
3325            assert_active_item(workspace, path!("/root/dir/file_2.rs"), "file_2", cx);
3326        });
3327
3328        // If the `filename` argument provided to the `:tabnew` command is for a
3329        // file that doesn't yet exist, it should still associate the buffer
3330        // with that file path, so that when the buffer contents are saved, the
3331        // file is created.
3332        cx.simulate_keystrokes(": tabnew space dir/file_3.rs");
3333        cx.simulate_keystrokes("enter");
3334
3335        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 4));
3336        cx.workspace(|workspace, _, cx| {
3337            assert_active_item(workspace, path!("/root/dir/file_3.rs"), "", cx);
3338        });
3339    }
3340
3341    #[gpui::test]
3342    async fn test_command_tabedit(cx: &mut TestAppContext) {
3343        let mut cx = VimTestContext::new(cx, true).await;
3344
3345        // Create a new file to ensure that, when the filename is used with
3346        // `:tabedit`, it opens the existing file in a new tab.
3347        let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
3348        fs.as_fake()
3349            .insert_file(path!("/root/dir/file_2.rs"), "file_2".as_bytes().to_vec())
3350            .await;
3351
3352        cx.simulate_keystrokes(": tabedit");
3353        cx.simulate_keystrokes("enter");
3354        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
3355
3356        // Assert that the new tab is empty and not associated with any file, as
3357        // no file path was provided to the `:tabedit` command.
3358        cx.workspace(|workspace, _window, cx| {
3359            let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();
3360            let buffer = active_editor
3361                .read(cx)
3362                .buffer()
3363                .read(cx)
3364                .as_singleton()
3365                .unwrap();
3366
3367            assert!(&buffer.read(cx).file().is_none());
3368        });
3369
3370        // Leverage the filename as an argument to the `:tabedit` command,
3371        // ensuring that the file, instead of an empty buffer, is opened in a
3372        // new tab.
3373        cx.simulate_keystrokes(": tabedit space dir/file_2.rs");
3374        cx.simulate_keystrokes("enter");
3375
3376        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 3));
3377        cx.workspace(|workspace, _, cx| {
3378            assert_active_item(workspace, path!("/root/dir/file_2.rs"), "file_2", cx);
3379        });
3380
3381        // If the `filename` argument provided to the `:tabedit` command is for a
3382        // file that doesn't yet exist, it should still associate the buffer
3383        // with that file path, so that when the buffer contents are saved, the
3384        // file is created.
3385        cx.simulate_keystrokes(": tabedit space dir/file_3.rs");
3386        cx.simulate_keystrokes("enter");
3387
3388        cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 4));
3389        cx.workspace(|workspace, _, cx| {
3390            assert_active_item(workspace, path!("/root/dir/file_3.rs"), "", cx);
3391        });
3392    }
3393
3394    #[gpui::test]
3395    async fn test_ignorecase_command(cx: &mut TestAppContext) {
3396        let mut cx = VimTestContext::new(cx, true).await;
3397        cx.read(|cx| {
3398            assert_eq!(
3399                EditorSettings::get_global(cx).search.case_sensitive,
3400                false,
3401                "The `case_sensitive` setting should be `false` by default."
3402            );
3403        });
3404        cx.simulate_keystrokes(": set space noignorecase");
3405        cx.simulate_keystrokes("enter");
3406        cx.read(|cx| {
3407            assert_eq!(
3408                EditorSettings::get_global(cx).search.case_sensitive,
3409                true,
3410                "The `case_sensitive` setting should have been enabled with `:set noignorecase`."
3411            );
3412        });
3413        cx.simulate_keystrokes(": set space ignorecase");
3414        cx.simulate_keystrokes("enter");
3415        cx.read(|cx| {
3416            assert_eq!(
3417                EditorSettings::get_global(cx).search.case_sensitive,
3418                false,
3419                "The `case_sensitive` setting should have been disabled with `:set ignorecase`."
3420            );
3421        });
3422        cx.simulate_keystrokes(": set space noic");
3423        cx.simulate_keystrokes("enter");
3424        cx.read(|cx| {
3425            assert_eq!(
3426                EditorSettings::get_global(cx).search.case_sensitive,
3427                true,
3428                "The `case_sensitive` setting should have been enabled with `:set noic`."
3429            );
3430        });
3431        cx.simulate_keystrokes(": set space ic");
3432        cx.simulate_keystrokes("enter");
3433        cx.read(|cx| {
3434            assert_eq!(
3435                EditorSettings::get_global(cx).search.case_sensitive,
3436                false,
3437                "The `case_sensitive` setting should have been disabled with `:set ic`."
3438            );
3439        });
3440    }
3441
3442    #[gpui::test]
3443    async fn test_sort_commands(cx: &mut TestAppContext) {
3444        let mut cx = VimTestContext::new(cx, true).await;
3445
3446        cx.set_state(
3447            indoc! {"
3448                «hornet
3449                quirrel
3450                elderbug
3451                cornifer
3452                idaˇ»
3453            "},
3454            Mode::Visual,
3455        );
3456
3457        cx.simulate_keystrokes(": sort");
3458        cx.simulate_keystrokes("enter");
3459
3460        cx.assert_state(
3461            indoc! {"
3462                ˇcornifer
3463                elderbug
3464                hornet
3465                ida
3466                quirrel
3467            "},
3468            Mode::Normal,
3469        );
3470
3471        // Assert that, by default, `:sort` takes case into consideration.
3472        cx.set_state(
3473            indoc! {"
3474                «hornet
3475                quirrel
3476                Elderbug
3477                cornifer
3478                idaˇ»
3479            "},
3480            Mode::Visual,
3481        );
3482
3483        cx.simulate_keystrokes(": sort");
3484        cx.simulate_keystrokes("enter");
3485
3486        cx.assert_state(
3487            indoc! {"
3488                ˇElderbug
3489                cornifer
3490                hornet
3491                ida
3492                quirrel
3493            "},
3494            Mode::Normal,
3495        );
3496
3497        // Assert that, if the `i` option is passed, `:sort` ignores case.
3498        cx.set_state(
3499            indoc! {"
3500                «hornet
3501                quirrel
3502                Elderbug
3503                cornifer
3504                idaˇ»
3505            "},
3506            Mode::Visual,
3507        );
3508
3509        cx.simulate_keystrokes(": sort space i");
3510        cx.simulate_keystrokes("enter");
3511
3512        cx.assert_state(
3513            indoc! {"
3514                ˇcornifer
3515                Elderbug
3516                hornet
3517                ida
3518                quirrel
3519            "},
3520            Mode::Normal,
3521        );
3522
3523        // When no range is provided, sorts the whole buffer.
3524        cx.set_state(
3525            indoc! {"
3526                ˇhornet
3527                quirrel
3528                elderbug
3529                cornifer
3530                ida
3531            "},
3532            Mode::Normal,
3533        );
3534
3535        cx.simulate_keystrokes(": sort");
3536        cx.simulate_keystrokes("enter");
3537
3538        cx.assert_state(
3539            indoc! {"
3540                ˇcornifer
3541                elderbug
3542                hornet
3543                ida
3544                quirrel
3545            "},
3546            Mode::Normal,
3547        );
3548    }
3549
3550    #[gpui::test]
3551    async fn test_reflow(cx: &mut TestAppContext) {
3552        let mut cx = VimTestContext::new(cx, true).await;
3553
3554        cx.update_editor(|editor, _window, cx| {
3555            editor.set_hard_wrap(Some(10), cx);
3556        });
3557
3558        cx.set_state(
3559            indoc! {"
3560                ˇ0123456789 0123456789
3561            "},
3562            Mode::Normal,
3563        );
3564
3565        cx.simulate_keystrokes(": reflow");
3566        cx.simulate_keystrokes("enter");
3567
3568        cx.assert_state(
3569            indoc! {"
3570                0123456789
3571                ˇ0123456789
3572            "},
3573            Mode::Normal,
3574        );
3575
3576        cx.set_state(
3577            indoc! {"
3578                ˇ0123456789 0123456789
3579            "},
3580            Mode::VisualLine,
3581        );
3582
3583        cx.simulate_keystrokes("shift-v : reflow");
3584        cx.simulate_keystrokes("enter");
3585
3586        cx.assert_state(
3587            indoc! {"
3588                0123456789
3589                ˇ0123456789
3590            "},
3591            Mode::Normal,
3592        );
3593
3594        cx.set_state(
3595            indoc! {"
3596                ˇ0123 4567 0123 4567
3597            "},
3598            Mode::VisualLine,
3599        );
3600
3601        cx.simulate_keystrokes(": reflow space 7");
3602        cx.simulate_keystrokes("enter");
3603
3604        cx.assert_state(
3605            indoc! {"
3606                ˇ0123
3607                4567
3608                0123
3609                4567
3610            "},
3611            Mode::Normal,
3612        );
3613
3614        // Assert that, if `:reflow` is invoked with an invalid argument, it
3615        // does not actually have any effect in the buffer's contents.
3616        cx.set_state(
3617            indoc! {"
3618                ˇ0123 4567 0123 4567
3619            "},
3620            Mode::VisualLine,
3621        );
3622
3623        cx.simulate_keystrokes(": reflow space a");
3624        cx.simulate_keystrokes("enter");
3625
3626        cx.assert_state(
3627            indoc! {"
3628                ˇ0123 4567 0123 4567
3629            "},
3630            Mode::VisualLine,
3631        );
3632    }
3633}
3634
Served at tenant.openagents/omega Member data and write actions are omitted.