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