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