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