1use std::{
2 iter::Peekable,
3 ops::{Deref, Range},
4 str::Chars,
5 sync::OnceLock,
6 time::Instant,
7};
8
9use anyhow::{anyhow, Result};
10use command_palette_hooks::CommandInterceptResult;
11use editor::{
12 actions::{SortLinesCaseInsensitive, SortLinesCaseSensitive},
13 display_map::ToDisplayPoint,
14 Bias, Editor, ToPoint,
15};
16use gpui::{actions, impl_actions, Action, AppContext, Global, ViewContext, WindowContext};
17use language::Point;
18use multi_buffer::MultiBufferRow;
19use regex::Regex;
20use search::{BufferSearchBar, SearchOptions};
21use serde::Deserialize;
22use util::ResultExt;
23use workspace::{notifications::NotifyResultExt, SaveIntent};
24
25use crate::{
26 motion::{EndOfDocument, Motion, StartOfDocument},
27 normal::{
28 search::{FindCommand, ReplaceCommand, Replacement},
29 JoinLines,
30 },
31 state::Mode,
32 visual::VisualDeleteLine,
33 Vim,
34};
35
36#[derive(Debug, Clone, PartialEq, Deserialize)]
37pub struct GoToLine {
38 range: CommandRange,
39}
40
41#[derive(Debug, Clone, PartialEq, Deserialize)]
42pub struct YankCommand {
43 range: CommandRange,
44}
45
46#[derive(Debug, Clone, PartialEq, Deserialize)]
47pub struct WithRange {
48 restore_selection: bool,
49 range: CommandRange,
50 action: WrappedAction,
51}
52
53#[derive(Debug, Clone, PartialEq, Deserialize)]
54pub struct WithCount {
55 count: u32,
56 action: WrappedAction,
57}
58
59#[derive(Debug)]
60struct WrappedAction(Box<dyn Action>);
61
62actions!(vim, [VisualCommand, CountCommand]);
63impl_actions!(
64 vim,
65 [GoToLine, YankCommand, WithRange, WithCount, OnMatchingLines]
66);
67
68impl<'de> Deserialize<'de> for WrappedAction {
69 fn deserialize<D>(_: D) -> Result<Self, D::Error>
70 where
71 D: serde::Deserializer<'de>,
72 {
73 Err(serde::de::Error::custom("Cannot deserialize WrappedAction"))
74 }
75}
76
77impl PartialEq for WrappedAction {
78 fn eq(&self, other: &Self) -> bool {
79 self.0.partial_eq(&*other.0)
80 }
81}
82
83impl Clone for WrappedAction {
84 fn clone(&self) -> Self {
85 Self(self.0.boxed_clone())
86 }
87}
88
89impl Deref for WrappedAction {
90 type Target = dyn Action;
91 fn deref(&self) -> &dyn Action {
92 &*self.0
93 }
94}
95
96pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
97 Vim::action(editor, cx, |vim, _: &VisualCommand, cx| {
98 let Some(workspace) = vim.workspace(cx) else {
99 return;
100 };
101 workspace.update(cx, |workspace, cx| {
102 command_palette::CommandPalette::toggle(workspace, "'<,'>", cx);
103 })
104 });
105
106 Vim::action(editor, cx, |vim, _: &CountCommand, cx| {
107 let Some(workspace) = vim.workspace(cx) else {
108 return;
109 };
110 let count = Vim::take_count(cx).unwrap_or(1);
111 workspace.update(cx, |workspace, cx| {
112 command_palette::CommandPalette::toggle(
113 workspace,
114 &format!(".,.+{}", count.saturating_sub(1)),
115 cx,
116 );
117 })
118 });
119
120 Vim::action(editor, cx, |vim, action: &GoToLine, cx| {
121 vim.switch_mode(Mode::Normal, false, cx);
122 let result = vim.update_editor(cx, |vim, editor, cx| {
123 action.range.head().buffer_row(vim, editor, cx)
124 });
125 let buffer_row = match result {
126 None => return,
127 Some(e @ Err(_)) => {
128 let Some(workspace) = vim.workspace(cx) else {
129 return;
130 };
131 workspace.update(cx, |workspace, cx| {
132 e.notify_err(workspace, cx);
133 });
134 return;
135 }
136 Some(Ok(result)) => result,
137 };
138 vim.move_cursor(Motion::StartOfDocument, Some(buffer_row.0 as usize + 1), cx);
139 });
140
141 Vim::action(editor, cx, |vim, action: &YankCommand, cx| {
142 vim.update_editor(cx, |vim, editor, cx| {
143 let snapshot = editor.snapshot(cx);
144 if let Ok(range) = action.range.buffer_range(vim, editor, cx) {
145 let end = if range.end < snapshot.buffer_snapshot.max_row() {
146 Point::new(range.end.0 + 1, 0)
147 } else {
148 snapshot.buffer_snapshot.max_point()
149 };
150 vim.copy_ranges(
151 editor,
152 true,
153 true,
154 vec![Point::new(range.start.0, 0)..end],
155 cx,
156 )
157 }
158 });
159 });
160
161 Vim::action(editor, cx, |_, action: &WithCount, cx| {
162 for _ in 0..action.count {
163 cx.dispatch_action(action.action.boxed_clone())
164 }
165 });
166
167 Vim::action(editor, cx, |vim, action: &WithRange, cx| {
168 let result = vim.update_editor(cx, |vim, editor, cx| {
169 action.range.buffer_range(vim, editor, cx)
170 });
171
172 let range = match result {
173 None => return,
174 Some(e @ Err(_)) => {
175 let Some(workspace) = vim.workspace(cx) else {
176 return;
177 };
178 workspace.update(cx, |workspace, cx| {
179 e.notify_err(workspace, cx);
180 });
181 return;
182 }
183 Some(Ok(result)) => result,
184 };
185
186 let previous_selections = vim
187 .update_editor(cx, |_, editor, cx| {
188 let selections = action
189 .restore_selection
190 .then(|| editor.selections.disjoint_anchor_ranges());
191 editor.change_selections(None, cx, |s| {
192 let end = Point::new(range.end.0, s.buffer().line_len(range.end));
193 s.select_ranges([end..Point::new(range.start.0, 0)]);
194 });
195 selections
196 })
197 .flatten();
198 cx.dispatch_action(action.action.boxed_clone());
199 cx.defer(move |vim, cx| {
200 vim.update_editor(cx, |_, editor, cx| {
201 editor.change_selections(None, cx, |s| {
202 if let Some(previous_selections) = previous_selections {
203 s.select_ranges(previous_selections);
204 } else {
205 s.select_ranges([
206 Point::new(range.start.0, 0)..Point::new(range.start.0, 0)
207 ]);
208 }
209 })
210 });
211 });
212 });
213
214 Vim::action(editor, cx, |vim, action: &OnMatchingLines, cx| {
215 action.run(vim, cx)
216 })
217}
218
219#[derive(Default)]
220struct VimCommand {
221 prefix: &'static str,
222 suffix: &'static str,
223 action: Option<Box<dyn Action>>,
224 action_name: Option<&'static str>,
225 bang_action: Option<Box<dyn Action>>,
226 range: Option<
227 Box<
228 dyn Fn(Box<dyn Action>, &CommandRange) -> Option<Box<dyn Action>>
229 + Send
230 + Sync
231 + 'static,
232 >,
233 >,
234 has_count: bool,
235}
236
237impl VimCommand {
238 fn new(pattern: (&'static str, &'static str), action: impl Action) -> Self {
239 Self {
240 prefix: pattern.0,
241 suffix: pattern.1,
242 action: Some(action.boxed_clone()),
243 ..Default::default()
244 }
245 }
246
247 // from_str is used for actions in other crates.
248 fn str(pattern: (&'static str, &'static str), action_name: &'static str) -> Self {
249 Self {
250 prefix: pattern.0,
251 suffix: pattern.1,
252 action_name: Some(action_name),
253 ..Default::default()
254 }
255 }
256
257 fn bang(mut self, bang_action: impl Action) -> Self {
258 self.bang_action = Some(bang_action.boxed_clone());
259 self
260 }
261
262 fn range(
263 mut self,
264 f: impl Fn(Box<dyn Action>, &CommandRange) -> Option<Box<dyn Action>> + Send + Sync + 'static,
265 ) -> Self {
266 self.range = Some(Box::new(f));
267 self
268 }
269
270 fn count(mut self) -> Self {
271 self.has_count = true;
272 self
273 }
274
275 fn parse(
276 &self,
277 mut query: &str,
278 range: &Option<CommandRange>,
279 cx: &AppContext,
280 ) -> Option<Box<dyn Action>> {
281 let has_bang = query.ends_with('!');
282 if has_bang {
283 query = &query[..query.len() - 1];
284 }
285
286 let suffix = query.strip_prefix(self.prefix)?;
287 if !self.suffix.starts_with(suffix) {
288 return None;
289 }
290
291 let action = if has_bang && self.bang_action.is_some() {
292 self.bang_action.as_ref().unwrap().boxed_clone()
293 } else if let Some(action) = self.action.as_ref() {
294 action.boxed_clone()
295 } else if let Some(action_name) = self.action_name {
296 cx.build_action(action_name, None).log_err()?
297 } else {
298 return None;
299 };
300
301 if let Some(range) = range {
302 self.range.as_ref().and_then(|f| f(action, range))
303 } else {
304 Some(action)
305 }
306 }
307
308 // TODO: ranges with search queries
309 fn parse_range(query: &str) -> (Option<CommandRange>, String) {
310 let mut chars = query.chars().peekable();
311
312 match chars.peek() {
313 Some('%') => {
314 chars.next();
315 return (
316 Some(CommandRange {
317 start: Position::Line { row: 1, offset: 0 },
318 end: Some(Position::LastLine { offset: 0 }),
319 }),
320 chars.collect(),
321 );
322 }
323 Some('*') => {
324 chars.next();
325 return (
326 Some(CommandRange {
327 start: Position::Mark {
328 name: '<',
329 offset: 0,
330 },
331 end: Some(Position::Mark {
332 name: '>',
333 offset: 0,
334 }),
335 }),
336 chars.collect(),
337 );
338 }
339 _ => {}
340 }
341
342 let start = Self::parse_position(&mut chars);
343
344 match chars.peek() {
345 Some(',' | ';') => {
346 chars.next();
347 (
348 Some(CommandRange {
349 start: start.unwrap_or(Position::CurrentLine { offset: 0 }),
350 end: Self::parse_position(&mut chars),
351 }),
352 chars.collect(),
353 )
354 }
355 _ => (
356 start.map(|start| CommandRange { start, end: None }),
357 chars.collect(),
358 ),
359 }
360 }
361
362 fn parse_position(chars: &mut Peekable<Chars>) -> Option<Position> {
363 match chars.peek()? {
364 '0'..='9' => {
365 let row = Self::parse_u32(chars);
366 Some(Position::Line {
367 row,
368 offset: Self::parse_offset(chars),
369 })
370 }
371 '\'' => {
372 chars.next();
373 let name = chars.next()?;
374 Some(Position::Mark {
375 name,
376 offset: Self::parse_offset(chars),
377 })
378 }
379 '.' => {
380 chars.next();
381 Some(Position::CurrentLine {
382 offset: Self::parse_offset(chars),
383 })
384 }
385 '+' | '-' => Some(Position::CurrentLine {
386 offset: Self::parse_offset(chars),
387 }),
388 '$' => {
389 chars.next();
390 Some(Position::LastLine {
391 offset: Self::parse_offset(chars),
392 })
393 }
394 _ => None,
395 }
396 }
397
398 fn parse_offset(chars: &mut Peekable<Chars>) -> i32 {
399 let mut res: i32 = 0;
400 while matches!(chars.peek(), Some('+' | '-')) {
401 let sign = if chars.next().unwrap() == '+' { 1 } else { -1 };
402 let amount = if matches!(chars.peek(), Some('0'..='9')) {
403 (Self::parse_u32(chars) as i32).saturating_mul(sign)
404 } else {
405 sign
406 };
407 res = res.saturating_add(amount)
408 }
409 res
410 }
411
412 fn parse_u32(chars: &mut Peekable<Chars>) -> u32 {
413 let mut res: u32 = 0;
414 while matches!(chars.peek(), Some('0'..='9')) {
415 res = res
416 .saturating_mul(10)
417 .saturating_add(chars.next().unwrap() as u32 - '0' as u32);
418 }
419 res
420 }
421}
422
423#[derive(Debug, Clone, PartialEq, Deserialize)]
424enum Position {
425 Line { row: u32, offset: i32 },
426 Mark { name: char, offset: i32 },
427 LastLine { offset: i32 },
428 CurrentLine { offset: i32 },
429}
430
431impl Position {
432 fn buffer_row(
433 &self,
434 vim: &Vim,
435 editor: &mut Editor,
436 cx: &mut WindowContext,
437 ) -> Result<MultiBufferRow> {
438 let snapshot = editor.snapshot(cx);
439 let target = match self {
440 Position::Line { row, offset } => row.saturating_add_signed(offset.saturating_sub(1)),
441 Position::Mark { name, offset } => {
442 let Some(mark) = vim.marks.get(&name.to_string()).and_then(|vec| vec.last()) else {
443 return Err(anyhow!("mark {} not set", name));
444 };
445 mark.to_point(&snapshot.buffer_snapshot)
446 .row
447 .saturating_add_signed(*offset)
448 }
449 Position::LastLine { offset } => snapshot
450 .buffer_snapshot
451 .max_row()
452 .0
453 .saturating_add_signed(*offset),
454 Position::CurrentLine { offset } => editor
455 .selections
456 .newest_anchor()
457 .head()
458 .to_point(&snapshot.buffer_snapshot)
459 .row
460 .saturating_add_signed(*offset),
461 };
462
463 Ok(MultiBufferRow(target).min(snapshot.buffer_snapshot.max_row()))
464 }
465}
466
467#[derive(Debug, Clone, PartialEq, Deserialize)]
468pub(crate) struct CommandRange {
469 start: Position,
470 end: Option<Position>,
471}
472
473impl CommandRange {
474 fn head(&self) -> &Position {
475 self.end.as_ref().unwrap_or(&self.start)
476 }
477
478 pub(crate) fn buffer_range(
479 &self,
480 vim: &Vim,
481 editor: &mut Editor,
482 cx: &mut WindowContext,
483 ) -> Result<Range<MultiBufferRow>> {
484 let start = self.start.buffer_row(vim, editor, cx)?;
485 let end = if let Some(end) = self.end.as_ref() {
486 end.buffer_row(vim, editor, cx)?
487 } else {
488 start
489 };
490 if end < start {
491 anyhow::Ok(end..start)
492 } else {
493 anyhow::Ok(start..end)
494 }
495 }
496
497 pub fn as_count(&self) -> Option<u32> {
498 if let CommandRange {
499 start: Position::Line { row, offset: 0 },
500 end: None,
501 } = &self
502 {
503 Some(*row)
504 } else {
505 None
506 }
507 }
508}
509
510fn generate_commands(_: &AppContext) -> Vec<VimCommand> {
511 vec![
512 VimCommand::new(
513 ("w", "rite"),
514 workspace::Save {
515 save_intent: Some(SaveIntent::Save),
516 },
517 )
518 .bang(workspace::Save {
519 save_intent: Some(SaveIntent::Overwrite),
520 }),
521 VimCommand::new(
522 ("q", "uit"),
523 workspace::CloseActiveItem {
524 save_intent: Some(SaveIntent::Close),
525 },
526 )
527 .bang(workspace::CloseActiveItem {
528 save_intent: Some(SaveIntent::Skip),
529 }),
530 VimCommand::new(
531 ("wq", ""),
532 workspace::CloseActiveItem {
533 save_intent: Some(SaveIntent::Save),
534 },
535 )
536 .bang(workspace::CloseActiveItem {
537 save_intent: Some(SaveIntent::Overwrite),
538 }),
539 VimCommand::new(
540 ("x", "it"),
541 workspace::CloseActiveItem {
542 save_intent: Some(SaveIntent::SaveAll),
543 },
544 )
545 .bang(workspace::CloseActiveItem {
546 save_intent: Some(SaveIntent::Overwrite),
547 }),
548 VimCommand::new(
549 ("ex", "it"),
550 workspace::CloseActiveItem {
551 save_intent: Some(SaveIntent::SaveAll),
552 },
553 )
554 .bang(workspace::CloseActiveItem {
555 save_intent: Some(SaveIntent::Overwrite),
556 }),
557 VimCommand::new(
558 ("up", "date"),
559 workspace::Save {
560 save_intent: Some(SaveIntent::SaveAll),
561 },
562 ),
563 VimCommand::new(
564 ("wa", "ll"),
565 workspace::SaveAll {
566 save_intent: Some(SaveIntent::SaveAll),
567 },
568 )
569 .bang(workspace::SaveAll {
570 save_intent: Some(SaveIntent::Overwrite),
571 }),
572 VimCommand::new(
573 ("qa", "ll"),
574 workspace::CloseAllItemsAndPanes {
575 save_intent: Some(SaveIntent::Close),
576 },
577 )
578 .bang(workspace::CloseAllItemsAndPanes {
579 save_intent: Some(SaveIntent::Skip),
580 }),
581 VimCommand::new(
582 ("quita", "ll"),
583 workspace::CloseAllItemsAndPanes {
584 save_intent: Some(SaveIntent::Close),
585 },
586 )
587 .bang(workspace::CloseAllItemsAndPanes {
588 save_intent: Some(SaveIntent::Skip),
589 }),
590 VimCommand::new(
591 ("xa", "ll"),
592 workspace::CloseAllItemsAndPanes {
593 save_intent: Some(SaveIntent::SaveAll),
594 },
595 )
596 .bang(workspace::CloseAllItemsAndPanes {
597 save_intent: Some(SaveIntent::Overwrite),
598 }),
599 VimCommand::new(
600 ("wqa", "ll"),
601 workspace::CloseAllItemsAndPanes {
602 save_intent: Some(SaveIntent::SaveAll),
603 },
604 )
605 .bang(workspace::CloseAllItemsAndPanes {
606 save_intent: Some(SaveIntent::Overwrite),
607 }),
608 VimCommand::new(("cq", "uit"), zed_actions::Quit),
609 VimCommand::new(("sp", "lit"), workspace::SplitHorizontal),
610 VimCommand::new(("vs", "plit"), workspace::SplitVertical),
611 VimCommand::new(
612 ("bd", "elete"),
613 workspace::CloseActiveItem {
614 save_intent: Some(SaveIntent::Close),
615 },
616 )
617 .bang(workspace::CloseActiveItem {
618 save_intent: Some(SaveIntent::Skip),
619 }),
620 VimCommand::new(("bn", "ext"), workspace::ActivateNextItem).count(),
621 VimCommand::new(("bN", "ext"), workspace::ActivatePrevItem).count(),
622 VimCommand::new(("bp", "revious"), workspace::ActivatePrevItem).count(),
623 VimCommand::new(("bf", "irst"), workspace::ActivateItem(0)),
624 VimCommand::new(("br", "ewind"), workspace::ActivateItem(0)),
625 VimCommand::new(("bl", "ast"), workspace::ActivateLastItem),
626 VimCommand::new(("new", ""), workspace::NewFileSplitHorizontal),
627 VimCommand::new(("vne", "w"), workspace::NewFileSplitVertical),
628 VimCommand::new(("tabe", "dit"), workspace::NewFile),
629 VimCommand::new(("tabnew", ""), workspace::NewFile),
630 VimCommand::new(("tabn", "ext"), workspace::ActivateNextItem).count(),
631 VimCommand::new(("tabp", "revious"), workspace::ActivatePrevItem).count(),
632 VimCommand::new(("tabN", "ext"), workspace::ActivatePrevItem).count(),
633 VimCommand::new(
634 ("tabc", "lose"),
635 workspace::CloseActiveItem {
636 save_intent: Some(SaveIntent::Close),
637 },
638 ),
639 VimCommand::new(
640 ("tabo", "nly"),
641 workspace::CloseInactiveItems {
642 save_intent: Some(SaveIntent::Close),
643 close_pinned: false,
644 },
645 )
646 .bang(workspace::CloseInactiveItems {
647 save_intent: Some(SaveIntent::Skip),
648 close_pinned: false,
649 }),
650 VimCommand::new(
651 ("on", "ly"),
652 workspace::CloseInactiveTabsAndPanes {
653 save_intent: Some(SaveIntent::Close),
654 },
655 )
656 .bang(workspace::CloseInactiveTabsAndPanes {
657 save_intent: Some(SaveIntent::Skip),
658 }),
659 VimCommand::str(("cl", "ist"), "diagnostics::Deploy"),
660 VimCommand::new(("cc", ""), editor::actions::Hover),
661 VimCommand::new(("ll", ""), editor::actions::Hover),
662 VimCommand::new(("cn", "ext"), editor::actions::GoToDiagnostic).range(wrap_count),
663 VimCommand::new(("cp", "revious"), editor::actions::GoToPrevDiagnostic).range(wrap_count),
664 VimCommand::new(("cN", "ext"), editor::actions::GoToPrevDiagnostic).range(wrap_count),
665 VimCommand::new(("lp", "revious"), editor::actions::GoToPrevDiagnostic).range(wrap_count),
666 VimCommand::new(("lN", "ext"), editor::actions::GoToPrevDiagnostic).range(wrap_count),
667 VimCommand::new(("j", "oin"), JoinLines).range(select_range),
668 VimCommand::new(("fo", "ld"), editor::actions::FoldSelectedRanges).range(act_on_range),
669 VimCommand::new(("foldo", "pen"), editor::actions::UnfoldLines)
670 .bang(editor::actions::UnfoldRecursive)
671 .range(act_on_range),
672 VimCommand::new(("foldc", "lose"), editor::actions::Fold)
673 .bang(editor::actions::FoldRecursive)
674 .range(act_on_range),
675 VimCommand::new(("dif", "fupdate"), editor::actions::ToggleHunkDiff).range(act_on_range),
676 VimCommand::new(("rev", "ert"), editor::actions::RevertSelectedHunks).range(act_on_range),
677 VimCommand::new(("d", "elete"), VisualDeleteLine).range(select_range),
678 VimCommand::new(("y", "ank"), gpui::NoAction).range(|_, range| {
679 Some(
680 YankCommand {
681 range: range.clone(),
682 }
683 .boxed_clone(),
684 )
685 }),
686 VimCommand::new(("sor", "t"), SortLinesCaseSensitive).range(select_range),
687 VimCommand::new(("sort i", ""), SortLinesCaseInsensitive).range(select_range),
688 VimCommand::str(("E", "xplore"), "project_panel::ToggleFocus"),
689 VimCommand::str(("H", "explore"), "project_panel::ToggleFocus"),
690 VimCommand::str(("L", "explore"), "project_panel::ToggleFocus"),
691 VimCommand::str(("S", "explore"), "project_panel::ToggleFocus"),
692 VimCommand::str(("Ve", "xplore"), "project_panel::ToggleFocus"),
693 VimCommand::str(("te", "rm"), "terminal_panel::ToggleFocus"),
694 VimCommand::str(("T", "erm"), "terminal_panel::ToggleFocus"),
695 VimCommand::str(("C", "ollab"), "collab_panel::ToggleFocus"),
696 VimCommand::str(("Ch", "at"), "chat_panel::ToggleFocus"),
697 VimCommand::str(("No", "tifications"), "notification_panel::ToggleFocus"),
698 VimCommand::str(("A", "I"), "assistant::ToggleFocus"),
699 VimCommand::new(("noh", "lsearch"), search::buffer_search::Dismiss),
700 VimCommand::new(("$", ""), EndOfDocument),
701 VimCommand::new(("%", ""), EndOfDocument),
702 VimCommand::new(("0", ""), StartOfDocument),
703 VimCommand::new(("e", "dit"), editor::actions::ReloadFile)
704 .bang(editor::actions::ReloadFile),
705 VimCommand::new(("cpp", "link"), editor::actions::CopyPermalinkToLine).range(act_on_range),
706 ]
707}
708
709struct VimCommands(Vec<VimCommand>);
710// safety: we only ever access this from the main thread (as ensured by the cx argument)
711// actions are not Sync so we can't otherwise use a OnceLock.
712unsafe impl Sync for VimCommands {}
713impl Global for VimCommands {}
714
715fn commands(cx: &AppContext) -> &Vec<VimCommand> {
716 static COMMANDS: OnceLock<VimCommands> = OnceLock::new();
717 &COMMANDS
718 .get_or_init(|| VimCommands(generate_commands(cx)))
719 .0
720}
721
722fn act_on_range(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
723 Some(
724 WithRange {
725 restore_selection: true,
726 range: range.clone(),
727 action: WrappedAction(action),
728 }
729 .boxed_clone(),
730 )
731}
732
733fn select_range(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
734 Some(
735 WithRange {
736 restore_selection: false,
737 range: range.clone(),
738 action: WrappedAction(action),
739 }
740 .boxed_clone(),
741 )
742}
743
744fn wrap_count(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
745 range.as_count().map(|count| {
746 WithCount {
747 count,
748 action: WrappedAction(action),
749 }
750 .boxed_clone()
751 })
752}
753
754pub fn command_interceptor(mut input: &str, cx: &AppContext) -> Option<CommandInterceptResult> {
755 // NOTE: We also need to support passing arguments to commands like :w
756 // (ideally with filename autocompletion).
757 while input.starts_with(':') {
758 input = &input[1..];
759 }
760
761 let (range, query) = VimCommand::parse_range(input);
762 let range_prefix = input[0..(input.len() - query.len())].to_string();
763 let query = query.as_str().trim();
764
765 let action = if range.is_some() && query.is_empty() {
766 Some(
767 GoToLine {
768 range: range.clone().unwrap(),
769 }
770 .boxed_clone(),
771 )
772 } else if query.starts_with('/') || query.starts_with('?') {
773 Some(
774 FindCommand {
775 query: query[1..].to_string(),
776 backwards: query.starts_with('?'),
777 }
778 .boxed_clone(),
779 )
780 } else if query.starts_with('s') {
781 let mut substitute = "substitute".chars().peekable();
782 let mut query = query.chars().peekable();
783 while substitute
784 .peek()
785 .is_some_and(|char| Some(char) == query.peek())
786 {
787 substitute.next();
788 query.next();
789 }
790 if let Some(replacement) = Replacement::parse(query) {
791 let range = range.clone().unwrap_or(CommandRange {
792 start: Position::CurrentLine { offset: 0 },
793 end: None,
794 });
795 Some(ReplaceCommand { replacement, range }.boxed_clone())
796 } else {
797 None
798 }
799 } else if query.starts_with('g') || query.starts_with('v') {
800 let mut global = "global".chars().peekable();
801 let mut query = query.chars().peekable();
802 let mut invert = false;
803 if query.peek() == Some(&'v') {
804 invert = true;
805 query.next();
806 }
807 while global.peek().is_some_and(|char| Some(char) == query.peek()) {
808 global.next();
809 query.next();
810 }
811 if !invert && query.peek() == Some(&'!') {
812 invert = true;
813 query.next();
814 }
815 let range = range.clone().unwrap_or(CommandRange {
816 start: Position::Line { row: 0, offset: 0 },
817 end: Some(Position::LastLine { offset: 0 }),
818 });
819 if let Some(action) = OnMatchingLines::parse(query, invert, range, cx) {
820 Some(action.boxed_clone())
821 } else {
822 None
823 }
824 } else {
825 None
826 };
827 if let Some(action) = action {
828 let string = input.to_string();
829 let positions = generate_positions(&string, &(range_prefix + query));
830 return Some(CommandInterceptResult {
831 action,
832 string,
833 positions,
834 });
835 }
836
837 for command in commands(cx).iter() {
838 if let Some(action) = command.parse(query, &range, cx) {
839 let mut string = ":".to_owned() + &range_prefix + command.prefix + command.suffix;
840 if query.ends_with('!') {
841 string.push('!');
842 }
843 let positions = generate_positions(&string, &(range_prefix + query));
844
845 return Some(CommandInterceptResult {
846 action,
847 string,
848 positions,
849 });
850 }
851 }
852 None
853}
854
855fn generate_positions(string: &str, query: &str) -> Vec<usize> {
856 let mut positions = Vec::new();
857 let mut chars = query.chars();
858
859 let Some(mut current) = chars.next() else {
860 return positions;
861 };
862
863 for (i, c) in string.char_indices() {
864 if c == current {
865 positions.push(i);
866 if let Some(c) = chars.next() {
867 current = c;
868 } else {
869 break;
870 }
871 }
872 }
873
874 positions
875}
876
877#[derive(Debug, PartialEq, Deserialize, Clone)]
878pub(crate) struct OnMatchingLines {
879 range: CommandRange,
880 search: String,
881 action: WrappedAction,
882 invert: bool,
883}
884
885impl OnMatchingLines {
886 // convert a vim query into something more usable by zed.
887 // we don't attempt to fully convert between the two regex syntaxes,
888 // but we do flip \( and \) to ( and ) (and vice-versa) in the pattern,
889 // and convert \0..\9 to $0..$9 in the replacement so that common idioms work.
890 pub(crate) fn parse(
891 mut chars: Peekable<Chars>,
892 invert: bool,
893 range: CommandRange,
894 cx: &AppContext,
895 ) -> Option<Self> {
896 let delimiter = chars.next().filter(|c| {
897 !c.is_alphanumeric() && *c != '"' && *c != '|' && *c != '\'' && *c != '!'
898 })?;
899
900 let mut search = String::new();
901 let mut escaped = false;
902
903 while let Some(c) = chars.next() {
904 if escaped {
905 escaped = false;
906 // unescape escaped parens
907 if c != '(' && c != ')' && c != delimiter {
908 search.push('\\')
909 }
910 search.push(c)
911 } else if c == '\\' {
912 escaped = true;
913 } else if c == delimiter {
914 break;
915 } else {
916 // escape unescaped parens
917 if c == '(' || c == ')' {
918 search.push('\\')
919 }
920 search.push(c)
921 }
922 }
923
924 let command: String = chars.collect();
925
926 let action = WrappedAction(command_interceptor(&command, cx)?.action);
927
928 Some(Self {
929 range,
930 search,
931 invert,
932 action,
933 })
934 }
935
936 pub fn run(&self, vim: &mut Vim, cx: &mut ViewContext<Vim>) {
937 let result = vim.update_editor(cx, |vim, editor, cx| {
938 self.range.buffer_range(vim, editor, cx)
939 });
940
941 let range = match result {
942 None => return,
943 Some(e @ Err(_)) => {
944 let Some(workspace) = vim.workspace(cx) else {
945 return;
946 };
947 workspace.update(cx, |workspace, cx| {
948 e.notify_err(workspace, cx);
949 });
950 return;
951 }
952 Some(Ok(result)) => result,
953 };
954
955 let mut action = self.action.boxed_clone();
956 let mut last_pattern = self.search.clone();
957
958 let mut regexes = match Regex::new(&self.search) {
959 Ok(regex) => vec![(regex, !self.invert)],
960 e @ Err(_) => {
961 let Some(workspace) = vim.workspace(cx) else {
962 return;
963 };
964 workspace.update(cx, |workspace, cx| {
965 e.notify_err(workspace, cx);
966 });
967 return;
968 }
969 };
970 while let Some(inner) = action
971 .boxed_clone()
972 .as_any()
973 .downcast_ref::<OnMatchingLines>()
974 {
975 let Some(regex) = Regex::new(&inner.search).ok() else {
976 break;
977 };
978 last_pattern = inner.search.clone();
979 action = inner.action.boxed_clone();
980 regexes.push((regex, !inner.invert))
981 }
982
983 if let Some(pane) = vim.pane(cx) {
984 pane.update(cx, |pane, cx| {
985 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()
986 {
987 search_bar.update(cx, |search_bar, cx| {
988 if search_bar.show(cx) {
989 let _ = search_bar.search(
990 &last_pattern,
991 Some(SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE),
992 cx,
993 );
994 }
995 });
996 }
997 });
998 };
999
1000 vim.update_editor(cx, |_, editor, cx| {
1001 let snapshot = editor.snapshot(cx);
1002 let mut row = range.start.0;
1003
1004 let point_range = Point::new(range.start.0, 0)
1005 ..snapshot
1006 .buffer_snapshot
1007 .clip_point(Point::new(range.end.0 + 1, 0), Bias::Left);
1008 cx.spawn(|editor, mut cx| async move {
1009 let new_selections = cx
1010 .background_executor()
1011 .spawn(async move {
1012 let mut line = String::new();
1013 let mut new_selections = Vec::new();
1014 let chunks = snapshot
1015 .buffer_snapshot
1016 .text_for_range(point_range)
1017 .chain(["\n"]);
1018
1019 for chunk in chunks {
1020 for (newline_ix, text) in chunk.split('\n').enumerate() {
1021 if newline_ix > 0 {
1022 if regexes.iter().all(|(regex, should_match)| {
1023 regex.is_match(&line) == *should_match
1024 }) {
1025 new_selections
1026 .push(Point::new(row, 0).to_display_point(&snapshot))
1027 }
1028 row += 1;
1029 line.clear();
1030 }
1031 line.push_str(text)
1032 }
1033 }
1034
1035 new_selections
1036 })
1037 .await;
1038
1039 if new_selections.is_empty() {
1040 return;
1041 }
1042 editor
1043 .update(&mut cx, |editor, cx| {
1044 editor.start_transaction_at(Instant::now(), cx);
1045 editor.change_selections(None, cx, |s| {
1046 s.replace_cursors_with(|_| new_selections);
1047 });
1048 cx.dispatch_action(action);
1049 cx.defer(move |editor, cx| {
1050 let newest = editor.selections.newest::<Point>(cx).clone();
1051 editor.change_selections(None, cx, |s| {
1052 s.select(vec![newest]);
1053 });
1054 editor.end_transaction_at(Instant::now(), cx);
1055 })
1056 })
1057 .ok();
1058 })
1059 .detach();
1060 });
1061 }
1062}
1063
1064#[cfg(test)]
1065mod test {
1066 use std::path::Path;
1067
1068 use crate::{
1069 state::Mode,
1070 test::{NeovimBackedTestContext, VimTestContext},
1071 };
1072 use editor::Editor;
1073 use gpui::TestAppContext;
1074 use indoc::indoc;
1075 use ui::ViewContext;
1076 use workspace::Workspace;
1077
1078 #[gpui::test]
1079 async fn test_command_basics(cx: &mut TestAppContext) {
1080 let mut cx = NeovimBackedTestContext::new(cx).await;
1081
1082 cx.set_shared_state(indoc! {"
1083 ˇa
1084 b
1085 c"})
1086 .await;
1087
1088 cx.simulate_shared_keystrokes(": j enter").await;
1089
1090 // hack: our cursor positioning after a join command is wrong
1091 cx.simulate_shared_keystrokes("^").await;
1092 cx.shared_state().await.assert_eq(indoc! {
1093 "ˇa b
1094 c"
1095 });
1096 }
1097
1098 #[gpui::test]
1099 async fn test_command_goto(cx: &mut TestAppContext) {
1100 let mut cx = NeovimBackedTestContext::new(cx).await;
1101
1102 cx.set_shared_state(indoc! {"
1103 ˇa
1104 b
1105 c"})
1106 .await;
1107 cx.simulate_shared_keystrokes(": 3 enter").await;
1108 cx.shared_state().await.assert_eq(indoc! {"
1109 a
1110 b
1111 ˇc"});
1112 }
1113
1114 #[gpui::test]
1115 async fn test_command_replace(cx: &mut TestAppContext) {
1116 let mut cx = NeovimBackedTestContext::new(cx).await;
1117
1118 cx.set_shared_state(indoc! {"
1119 ˇa
1120 b
1121 b
1122 c"})
1123 .await;
1124 cx.simulate_shared_keystrokes(": % s / b / d enter").await;
1125 cx.shared_state().await.assert_eq(indoc! {"
1126 a
1127 d
1128 ˇd
1129 c"});
1130 cx.simulate_shared_keystrokes(": % s : . : \\ 0 \\ 0 enter")
1131 .await;
1132 cx.shared_state().await.assert_eq(indoc! {"
1133 aa
1134 dd
1135 dd
1136 ˇcc"});
1137 cx.simulate_shared_keystrokes("k : s / dd / ee enter").await;
1138 cx.shared_state().await.assert_eq(indoc! {"
1139 aa
1140 dd
1141 ˇee
1142 cc"});
1143 }
1144
1145 #[gpui::test]
1146 async fn test_command_search(cx: &mut TestAppContext) {
1147 let mut cx = NeovimBackedTestContext::new(cx).await;
1148
1149 cx.set_shared_state(indoc! {"
1150 ˇa
1151 b
1152 a
1153 c"})
1154 .await;
1155 cx.simulate_shared_keystrokes(": / b enter").await;
1156 cx.shared_state().await.assert_eq(indoc! {"
1157 a
1158 ˇb
1159 a
1160 c"});
1161 cx.simulate_shared_keystrokes(": ? a enter").await;
1162 cx.shared_state().await.assert_eq(indoc! {"
1163 ˇa
1164 b
1165 a
1166 c"});
1167 }
1168
1169 #[gpui::test]
1170 async fn test_command_write(cx: &mut TestAppContext) {
1171 let mut cx = VimTestContext::new(cx, true).await;
1172 let path = Path::new("/root/dir/file.rs");
1173 let fs = cx.workspace(|workspace, cx| workspace.project().read(cx).fs().clone());
1174
1175 cx.simulate_keystrokes("i @ escape");
1176 cx.simulate_keystrokes(": w enter");
1177
1178 assert_eq!(fs.load(path).await.unwrap(), "@\n");
1179
1180 fs.as_fake().insert_file(path, b"oops\n".to_vec()).await;
1181
1182 // conflict!
1183 cx.simulate_keystrokes("i @ escape");
1184 cx.simulate_keystrokes(": w enter");
1185 assert!(cx.has_pending_prompt());
1186 // "Cancel"
1187 cx.simulate_prompt_answer(0);
1188 assert_eq!(fs.load(path).await.unwrap(), "oops\n");
1189 assert!(!cx.has_pending_prompt());
1190 // force overwrite
1191 cx.simulate_keystrokes(": w ! enter");
1192 assert!(!cx.has_pending_prompt());
1193 assert_eq!(fs.load(path).await.unwrap(), "@@\n");
1194 }
1195
1196 #[gpui::test]
1197 async fn test_command_quit(cx: &mut TestAppContext) {
1198 let mut cx = VimTestContext::new(cx, true).await;
1199
1200 cx.simulate_keystrokes(": n e w enter");
1201 cx.workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 2));
1202 cx.simulate_keystrokes(": q enter");
1203 cx.workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 1));
1204 cx.simulate_keystrokes(": n e w enter");
1205 cx.workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 2));
1206 cx.simulate_keystrokes(": q a enter");
1207 cx.workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 0));
1208 }
1209
1210 #[gpui::test]
1211 async fn test_offsets(cx: &mut TestAppContext) {
1212 let mut cx = NeovimBackedTestContext::new(cx).await;
1213
1214 cx.set_shared_state("ˇ1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n")
1215 .await;
1216
1217 cx.simulate_shared_keystrokes(": + enter").await;
1218 cx.shared_state()
1219 .await
1220 .assert_eq("1\nˇ2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n");
1221
1222 cx.simulate_shared_keystrokes(": 1 0 - enter").await;
1223 cx.shared_state()
1224 .await
1225 .assert_eq("1\n2\n3\n4\n5\n6\n7\n8\nˇ9\n10\n11\n");
1226
1227 cx.simulate_shared_keystrokes(": . - 2 enter").await;
1228 cx.shared_state()
1229 .await
1230 .assert_eq("1\n2\n3\n4\n5\n6\nˇ7\n8\n9\n10\n11\n");
1231
1232 cx.simulate_shared_keystrokes(": % enter").await;
1233 cx.shared_state()
1234 .await
1235 .assert_eq("1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\nˇ");
1236 }
1237
1238 #[gpui::test]
1239 async fn test_command_ranges(cx: &mut TestAppContext) {
1240 let mut cx = NeovimBackedTestContext::new(cx).await;
1241
1242 cx.set_shared_state("ˇ1\n2\n3\n4\n4\n3\n2\n1").await;
1243
1244 cx.simulate_shared_keystrokes(": 2 , 4 d enter").await;
1245 cx.shared_state().await.assert_eq("1\nˇ4\n3\n2\n1");
1246
1247 cx.simulate_shared_keystrokes(": 2 , 4 s o r t enter").await;
1248 cx.shared_state().await.assert_eq("1\nˇ2\n3\n4\n1");
1249
1250 cx.simulate_shared_keystrokes(": 2 , 4 j o i n enter").await;
1251 cx.shared_state().await.assert_eq("1\nˇ2 3 4\n1");
1252 }
1253
1254 #[gpui::test]
1255 async fn test_command_visual_replace(cx: &mut TestAppContext) {
1256 let mut cx = NeovimBackedTestContext::new(cx).await;
1257
1258 cx.set_shared_state("ˇ1\n2\n3\n4\n4\n3\n2\n1").await;
1259
1260 cx.simulate_shared_keystrokes("v 2 j : s / . / k enter")
1261 .await;
1262 cx.shared_state().await.assert_eq("k\nk\nˇk\n4\n4\n3\n2\n1");
1263 }
1264
1265 fn assert_active_item(
1266 workspace: &mut Workspace,
1267 expected_path: &str,
1268 expected_text: &str,
1269 cx: &mut ViewContext<Workspace>,
1270 ) {
1271 let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();
1272
1273 let buffer = active_editor
1274 .read(cx)
1275 .buffer()
1276 .read(cx)
1277 .as_singleton()
1278 .unwrap();
1279
1280 let text = buffer.read(cx).text();
1281 let file = buffer.read(cx).file().unwrap();
1282 let file_path = file.as_local().unwrap().abs_path(cx);
1283
1284 assert_eq!(text, expected_text);
1285 assert_eq!(file_path.to_str().unwrap(), expected_path);
1286 }
1287
1288 #[gpui::test]
1289 async fn test_command_gf(cx: &mut TestAppContext) {
1290 let mut cx = VimTestContext::new(cx, true).await;
1291
1292 // Assert base state, that we're in /root/dir/file.rs
1293 cx.workspace(|workspace, cx| {
1294 assert_active_item(workspace, "/root/dir/file.rs", "", cx);
1295 });
1296
1297 // Insert a new file
1298 let fs = cx.workspace(|workspace, cx| workspace.project().read(cx).fs().clone());
1299 fs.as_fake()
1300 .insert_file("/root/dir/file2.rs", "This is file2.rs".as_bytes().to_vec())
1301 .await;
1302 fs.as_fake()
1303 .insert_file("/root/dir/file3.rs", "go to file3".as_bytes().to_vec())
1304 .await;
1305
1306 // Put the path to the second file into the currently open buffer
1307 cx.set_state(indoc! {"go to fiˇle2.rs"}, Mode::Normal);
1308
1309 // Go to file2.rs
1310 cx.simulate_keystrokes("g f");
1311
1312 // We now have two items
1313 cx.workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 2));
1314 cx.workspace(|workspace, cx| {
1315 assert_active_item(workspace, "/root/dir/file2.rs", "This is file2.rs", cx);
1316 });
1317
1318 // Update editor to point to `file2.rs`
1319 cx.editor = cx.workspace(|workspace, cx| workspace.active_item_as::<Editor>(cx).unwrap());
1320
1321 // Put the path to the third file into the currently open buffer,
1322 // but remove its suffix, because we want that lookup to happen automatically.
1323 cx.set_state(indoc! {"go to fiˇle3"}, Mode::Normal);
1324
1325 // Go to file3.rs
1326 cx.simulate_keystrokes("g f");
1327
1328 // We now have three items
1329 cx.workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 3));
1330 cx.workspace(|workspace, cx| {
1331 assert_active_item(workspace, "/root/dir/file3.rs", "go to file3", cx);
1332 });
1333 }
1334
1335 #[gpui::test]
1336 async fn test_command_matching_lines(cx: &mut TestAppContext) {
1337 let mut cx = NeovimBackedTestContext::new(cx).await;
1338
1339 cx.set_shared_state(indoc! {"
1340 ˇa
1341 b
1342 a
1343 b
1344 a
1345 "})
1346 .await;
1347
1348 cx.simulate_shared_keystrokes(":").await;
1349 cx.simulate_shared_keystrokes("g / a / d").await;
1350 cx.simulate_shared_keystrokes("enter").await;
1351
1352 cx.shared_state().await.assert_eq(indoc! {"
1353 b
1354 b
1355 ˇ"});
1356
1357 cx.simulate_shared_keystrokes("u").await;
1358
1359 cx.shared_state().await.assert_eq(indoc! {"
1360 ˇa
1361 b
1362 a
1363 b
1364 a
1365 "});
1366
1367 cx.simulate_shared_keystrokes(":").await;
1368 cx.simulate_shared_keystrokes("v / a / d").await;
1369 cx.simulate_shared_keystrokes("enter").await;
1370
1371 cx.shared_state().await.assert_eq(indoc! {"
1372 a
1373 a
1374 ˇa"});
1375 }
1376}