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