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