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