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