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