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