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