1use anyhow::{Result, anyhow};
2use collections::{HashMap, HashSet};
3use command_palette_hooks::{CommandInterceptItem, CommandInterceptResult};
4use editor::{
5 Bias, Editor, EditorSettings, SelectionEffects, ToPoint,
6 actions::{SortLinesCaseInsensitive, SortLinesCaseSensitive},
7 display_map::ToDisplayPoint,
8};
9use futures::AsyncWriteExt as _;
10use gpui::{
11 Action, App, AppContext as _, Context, Global, Keystroke, Task, WeakEntity, Window, actions,
12};
13use itertools::Itertools;
14use language::Point;
15use multi_buffer::MultiBufferRow;
16use project::ProjectPath;
17use regex::Regex;
18use schemars::JsonSchema;
19use search::{BufferSearchBar, SearchOptions};
20use serde::Deserialize;
21use settings::{Settings, SettingsStore};
22use std::{
23 iter::Peekable,
24 ops::{Deref, Range},
25 path::{Path, PathBuf},
26 process::Stdio,
27 str::Chars,
28 sync::OnceLock,
29 time::Instant,
30};
31use task::{HideStrategy, RevealStrategy, SpawnInTerminal, TaskId};
32use ui::ActiveTheme;
33use util::{
34 ResultExt,
35 paths::PathStyle,
36 rel_path::{RelPath, RelPathBuf},
37};
38use workspace::{Item, SaveIntent, Workspace, notifications::NotifyResultExt};
39use workspace::{SplitDirection, notifications::DetachAndPromptErr};
40use zed_actions::{OpenDocs, RevealTarget};
41
42use crate::{
43 ToggleMarksView, ToggleRegistersView, Vim,
44 motion::{EndOfDocument, Motion, MotionKind, StartOfDocument},
45 normal::{
46 JoinLines,
47 search::{FindCommand, ReplaceCommand, Replacement},
48 },
49 object::Object,
50 state::{Mark, Mode},
51 visual::VisualDeleteLine,
52};
53
54/// Goes to the specified line number in the editor.
55#[derive(Clone, Debug, PartialEq, Action)]
56#[action(namespace = vim, no_json, no_register)]
57pub struct GoToLine {
58 range: CommandRange,
59}
60
61/// Yanks (copies) text based on the specified range.
62#[derive(Clone, Debug, PartialEq, Action)]
63#[action(namespace = vim, no_json, no_register)]
64pub struct YankCommand {
65 range: CommandRange,
66}
67
68/// Executes a command with the specified range.
69#[derive(Clone, Debug, PartialEq, Action)]
70#[action(namespace = vim, no_json, no_register)]
71pub struct WithRange {
72 restore_selection: bool,
73 range: CommandRange,
74 action: WrappedAction,
75}
76
77/// Executes a command with the specified count.
78#[derive(Clone, Debug, PartialEq, Action)]
79#[action(namespace = vim, no_json, no_register)]
80pub struct WithCount {
81 count: u32,
82 action: WrappedAction,
83}
84
85#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
86pub enum VimOption {
87 Wrap(bool),
88 Number(bool),
89 RelativeNumber(bool),
90 IgnoreCase(bool),
91}
92
93impl VimOption {
94 fn possible_commands(query: &str) -> Vec<CommandInterceptItem> {
95 let mut prefix_of_options = Vec::new();
96 let mut options = query.split(" ").collect::<Vec<_>>();
97 let prefix = options.pop().unwrap_or_default();
98 for option in options {
99 if let Some(opt) = Self::from(option) {
100 prefix_of_options.push(opt)
101 } else {
102 return vec![];
103 }
104 }
105
106 Self::possibilities(prefix)
107 .map(|possible| {
108 let mut options = prefix_of_options.clone();
109 options.push(possible);
110
111 CommandInterceptItem {
112 string: format!(
113 ":set {}",
114 options.iter().map(|opt| opt.to_string()).join(" ")
115 ),
116 action: VimSet { options }.boxed_clone(),
117 positions: vec![],
118 }
119 })
120 .collect()
121 }
122
123 fn possibilities(query: &str) -> impl Iterator<Item = Self> + '_ {
124 [
125 (None, VimOption::Wrap(true)),
126 (None, VimOption::Wrap(false)),
127 (None, VimOption::Number(true)),
128 (None, VimOption::Number(false)),
129 (None, VimOption::RelativeNumber(true)),
130 (None, VimOption::RelativeNumber(false)),
131 (Some("rnu"), VimOption::RelativeNumber(true)),
132 (Some("nornu"), VimOption::RelativeNumber(false)),
133 (None, VimOption::IgnoreCase(true)),
134 (None, VimOption::IgnoreCase(false)),
135 (Some("ic"), VimOption::IgnoreCase(true)),
136 (Some("noic"), VimOption::IgnoreCase(false)),
137 ]
138 .into_iter()
139 .filter(move |(prefix, option)| prefix.unwrap_or(option.to_string()).starts_with(query))
140 .map(|(_, option)| option)
141 }
142
143 fn from(option: &str) -> Option<Self> {
144 match option {
145 "wrap" => Some(Self::Wrap(true)),
146 "nowrap" => Some(Self::Wrap(false)),
147
148 "number" => Some(Self::Number(true)),
149 "nu" => Some(Self::Number(true)),
150 "nonumber" => Some(Self::Number(false)),
151 "nonu" => Some(Self::Number(false)),
152
153 "relativenumber" => Some(Self::RelativeNumber(true)),
154 "rnu" => Some(Self::RelativeNumber(true)),
155 "norelativenumber" => Some(Self::RelativeNumber(false)),
156 "nornu" => Some(Self::RelativeNumber(false)),
157
158 "ignorecase" => Some(Self::IgnoreCase(true)),
159 "ic" => Some(Self::IgnoreCase(true)),
160 "noignorecase" => Some(Self::IgnoreCase(false)),
161 "noic" => Some(Self::IgnoreCase(false)),
162
163 _ => None,
164 }
165 }
166
167 fn to_string(&self) -> &'static str {
168 match self {
169 VimOption::Wrap(true) => "wrap",
170 VimOption::Wrap(false) => "nowrap",
171 VimOption::Number(true) => "number",
172 VimOption::Number(false) => "nonumber",
173 VimOption::RelativeNumber(true) => "relativenumber",
174 VimOption::RelativeNumber(false) => "norelativenumber",
175 VimOption::IgnoreCase(true) => "ignorecase",
176 VimOption::IgnoreCase(false) => "noignorecase",
177 }
178 }
179}
180
181/// Sets vim options and configuration values.
182#[derive(Clone, PartialEq, Action)]
183#[action(namespace = vim, no_json, no_register)]
184pub struct VimSet {
185 options: Vec<VimOption>,
186}
187
188/// Saves the current file with optional save intent.
189#[derive(Clone, PartialEq, Action)]
190#[action(namespace = vim, no_json, no_register)]
191struct VimSave {
192 pub save_intent: Option<SaveIntent>,
193 pub filename: String,
194}
195
196/// Deletes the specified marks from the editor.
197#[derive(Clone, PartialEq, Action)]
198#[action(namespace = vim, no_json, no_register)]
199struct VimSplit {
200 pub vertical: bool,
201 pub filename: String,
202}
203
204#[derive(Clone, PartialEq, Action)]
205#[action(namespace = vim, no_json, no_register)]
206enum DeleteMarks {
207 Marks(String),
208 AllLocal,
209}
210
211actions!(
212 vim,
213 [
214 /// Executes a command in visual mode.
215 VisualCommand,
216 /// Executes a command with a count prefix.
217 CountCommand,
218 /// Executes a shell command.
219 ShellCommand,
220 /// Indicates that an argument is required for the command.
221 ArgumentRequired
222 ]
223);
224
225/// Opens the specified file for editing.
226#[derive(Clone, PartialEq, Action)]
227#[action(namespace = vim, no_json, no_register)]
228struct VimEdit {
229 pub filename: String,
230}
231
232#[derive(Clone, PartialEq, Action)]
233#[action(namespace = vim, no_json, no_register)]
234struct VimNorm {
235 pub range: Option<CommandRange>,
236 pub command: String,
237}
238
239#[derive(Debug)]
240struct WrappedAction(Box<dyn Action>);
241
242impl PartialEq for WrappedAction {
243 fn eq(&self, other: &Self) -> bool {
244 self.0.partial_eq(&*other.0)
245 }
246}
247
248impl Clone for WrappedAction {
249 fn clone(&self) -> Self {
250 Self(self.0.boxed_clone())
251 }
252}
253
254impl Deref for WrappedAction {
255 type Target = dyn Action;
256 fn deref(&self) -> &dyn Action {
257 &*self.0
258 }
259}
260
261pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
262 // Vim::action(editor, cx, |vim, action: &StartOfLine, window, cx| {
263 Vim::action(editor, cx, |vim, action: &VimSet, _, cx| {
264 for option in action.options.iter() {
265 vim.update_editor(cx, |_, editor, cx| match option {
266 VimOption::Wrap(true) => {
267 editor
268 .set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
269 }
270 VimOption::Wrap(false) => {
271 editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);
272 }
273 VimOption::Number(enabled) => {
274 editor.set_show_line_numbers(*enabled, cx);
275 }
276 VimOption::RelativeNumber(enabled) => {
277 editor.set_relative_line_number(Some(*enabled), cx);
278 }
279 VimOption::IgnoreCase(enabled) => {
280 let mut settings = EditorSettings::get_global(cx).clone();
281 settings.search.case_sensitive = !*enabled;
282 SettingsStore::update(cx, |store, _| {
283 store.override_global(settings);
284 });
285 }
286 });
287 }
288 });
289 Vim::action(editor, cx, |vim, _: &VisualCommand, window, cx| {
290 let Some(workspace) = vim.workspace(window) else {
291 return;
292 };
293 workspace.update(cx, |workspace, cx| {
294 command_palette::CommandPalette::toggle(workspace, "'<,'>", window, cx);
295 })
296 });
297
298 Vim::action(editor, cx, |vim, _: &ShellCommand, window, cx| {
299 let Some(workspace) = vim.workspace(window) else {
300 return;
301 };
302 workspace.update(cx, |workspace, cx| {
303 command_palette::CommandPalette::toggle(workspace, "'<,'>!", window, cx);
304 })
305 });
306
307 Vim::action(editor, cx, |_, _: &ArgumentRequired, window, cx| {
308 let _ = window.prompt(
309 gpui::PromptLevel::Critical,
310 "Argument required",
311 None,
312 &["Cancel"],
313 cx,
314 );
315 });
316
317 Vim::action(editor, cx, |vim, _: &ShellCommand, window, cx| {
318 let Some(workspace) = vim.workspace(window) else {
319 return;
320 };
321 workspace.update(cx, |workspace, cx| {
322 command_palette::CommandPalette::toggle(workspace, "'<,'>!", window, cx);
323 })
324 });
325
326 Vim::action(editor, cx, |vim, action: &VimSave, window, cx| {
327 vim.update_editor(cx, |_, editor, cx| {
328 let Some(project) = editor.project().cloned() else {
329 return;
330 };
331 let Some(worktree) = project.read(cx).visible_worktrees(cx).next() else {
332 return;
333 };
334 let path_style = worktree.read(cx).path_style();
335 let Ok(project_path) =
336 RelPath::new(Path::new(&action.filename), path_style).map(|path| ProjectPath {
337 worktree_id: worktree.read(cx).id(),
338 path: path.into_arc(),
339 })
340 else {
341 // TODO implement save_as with absolute path
342 Task::ready(Err::<(), _>(anyhow!(
343 "Cannot save buffer with absolute path"
344 )))
345 .detach_and_prompt_err(
346 "Failed to save",
347 window,
348 cx,
349 |_, _, _| None,
350 );
351 return;
352 };
353
354 if project.read(cx).entry_for_path(&project_path, cx).is_some()
355 && action.save_intent != Some(SaveIntent::Overwrite)
356 {
357 let answer = window.prompt(
358 gpui::PromptLevel::Critical,
359 &format!(
360 "{} already exists. Do you want to replace it?",
361 project_path.path.display(path_style)
362 ),
363 Some(
364 "A file or folder with the same name already exists. \
365 Replacing it will overwrite its current contents.",
366 ),
367 &["Replace", "Cancel"],
368 cx,
369 );
370 cx.spawn_in(window, async move |editor, cx| {
371 if answer.await.ok() != Some(0) {
372 return;
373 }
374
375 let _ = editor.update_in(cx, |editor, window, cx| {
376 editor
377 .save_as(project, project_path, window, cx)
378 .detach_and_prompt_err("Failed to :w", window, cx, |_, _, _| None);
379 });
380 })
381 .detach();
382 } else {
383 editor
384 .save_as(project, project_path, window, cx)
385 .detach_and_prompt_err("Failed to :w", window, cx, |_, _, _| None);
386 }
387 });
388 });
389
390 Vim::action(editor, cx, |vim, action: &VimSplit, window, cx| {
391 let Some(workspace) = vim.workspace(window) else {
392 return;
393 };
394
395 workspace.update(cx, |workspace, cx| {
396 let project = workspace.project().clone();
397 let Some(worktree) = project.read(cx).visible_worktrees(cx).next() else {
398 return;
399 };
400 let path_style = worktree.read(cx).path_style();
401 let Some(path) = RelPath::new(Path::new(&action.filename), path_style).log_err() else {
402 return;
403 };
404 let project_path = ProjectPath {
405 worktree_id: worktree.read(cx).id(),
406 path: path.into_arc(),
407 };
408
409 let direction = if action.vertical {
410 SplitDirection::vertical(cx)
411 } else {
412 SplitDirection::horizontal(cx)
413 };
414
415 workspace
416 .split_path_preview(project_path, false, Some(direction), window, cx)
417 .detach_and_log_err(cx);
418 })
419 });
420
421 Vim::action(editor, cx, |vim, action: &DeleteMarks, window, cx| {
422 fn err(s: String, window: &mut Window, cx: &mut Context<Editor>) {
423 let _ = window.prompt(
424 gpui::PromptLevel::Critical,
425 &format!("Invalid argument: {}", s),
426 None,
427 &["Cancel"],
428 cx,
429 );
430 }
431 vim.update_editor(cx, |vim, editor, cx| match action {
432 DeleteMarks::Marks(s) => {
433 if s.starts_with('-') || s.ends_with('-') || s.contains(['\'', '`']) {
434 err(s.clone(), window, cx);
435 return;
436 }
437
438 let to_delete = if s.len() < 3 {
439 Some(s.clone())
440 } else {
441 s.chars()
442 .tuple_windows::<(_, _, _)>()
443 .map(|(a, b, c)| {
444 if b == '-' {
445 if match a {
446 'a'..='z' => a <= c && c <= 'z',
447 'A'..='Z' => a <= c && c <= 'Z',
448 '0'..='9' => a <= c && c <= '9',
449 _ => false,
450 } {
451 Some((a..=c).collect_vec())
452 } else {
453 None
454 }
455 } else if a == '-' {
456 if c == '-' { None } else { Some(vec![c]) }
457 } else if c == '-' {
458 if a == '-' { None } else { Some(vec![a]) }
459 } else {
460 Some(vec![a, b, c])
461 }
462 })
463 .fold_options(HashSet::<char>::default(), |mut set, chars| {
464 set.extend(chars.iter().copied());
465 set
466 })
467 .map(|set| set.iter().collect::<String>())
468 };
469
470 let Some(to_delete) = to_delete else {
471 err(s.clone(), window, cx);
472 return;
473 };
474
475 for c in to_delete.chars().filter(|c| !c.is_whitespace()) {
476 vim.delete_mark(c.to_string(), editor, window, cx);
477 }
478 }
479 DeleteMarks::AllLocal => {
480 for s in 'a'..='z' {
481 vim.delete_mark(s.to_string(), editor, window, cx);
482 }
483 }
484 });
485 });
486
487 Vim::action(editor, cx, |vim, action: &VimEdit, window, cx| {
488 vim.update_editor(cx, |vim, editor, cx| {
489 let Some(workspace) = vim.workspace(window) else {
490 return;
491 };
492 let Some(project) = editor.project().cloned() else {
493 return;
494 };
495 let Some(worktree) = project.read(cx).visible_worktrees(cx).next() else {
496 return;
497 };
498 let path_style = worktree.read(cx).path_style();
499 let Some(path) = RelPath::new(Path::new(&action.filename), path_style).log_err() else {
500 return;
501 };
502 let project_path = ProjectPath {
503 worktree_id: worktree.read(cx).id(),
504 path: path.into_arc(),
505 };
506
507 let _ = workspace.update(cx, |workspace, cx| {
508 workspace
509 .open_path(project_path, None, true, window, cx)
510 .detach_and_log_err(cx);
511 });
512 });
513 });
514
515 Vim::action(editor, cx, |vim, action: &VimNorm, window, cx| {
516 let keystrokes = action
517 .command
518 .chars()
519 .map(|c| Keystroke::parse(&c.to_string()).unwrap())
520 .collect();
521 vim.switch_mode(Mode::Normal, true, window, cx);
522 let initial_selections =
523 vim.update_editor(cx, |_, editor, _| editor.selections.disjoint_anchors_arc());
524 if let Some(range) = &action.range {
525 let result = vim.update_editor(cx, |vim, editor, cx| {
526 let range = range.buffer_range(vim, editor, window, cx)?;
527 editor.change_selections(
528 SelectionEffects::no_scroll().nav_history(false),
529 window,
530 cx,
531 |s| {
532 s.select_ranges(
533 (range.start.0..=range.end.0)
534 .map(|line| Point::new(line, 0)..Point::new(line, 0)),
535 );
536 },
537 );
538 anyhow::Ok(())
539 });
540 if let Some(Err(err)) = result {
541 log::error!("Error selecting range: {}", err);
542 return;
543 }
544 };
545
546 let Some(workspace) = vim.workspace(window) else {
547 return;
548 };
549 let task = workspace.update(cx, |workspace, cx| {
550 workspace.send_keystrokes_impl(keystrokes, window, cx)
551 });
552 let had_range = action.range.is_some();
553
554 cx.spawn_in(window, async move |vim, cx| {
555 task.await;
556 vim.update_in(cx, |vim, window, cx| {
557 vim.update_editor(cx, |_, editor, cx| {
558 if had_range {
559 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
560 s.select_anchor_ranges([s.newest_anchor().range()]);
561 })
562 }
563 });
564 if matches!(vim.mode, Mode::Insert | Mode::Replace) {
565 vim.normal_before(&Default::default(), window, cx);
566 } else {
567 vim.switch_mode(Mode::Normal, true, window, cx);
568 }
569 vim.update_editor(cx, |_, editor, cx| {
570 if let Some(first_sel) = initial_selections
571 && let Some(tx_id) = editor
572 .buffer()
573 .update(cx, |multi, cx| multi.last_transaction_id(cx))
574 {
575 let last_sel = editor.selections.disjoint_anchors_arc();
576 editor.modify_transaction_selection_history(tx_id, |old| {
577 old.0 = first_sel;
578 old.1 = Some(last_sel);
579 });
580 }
581 });
582 })
583 .ok();
584 })
585 .detach();
586 });
587
588 Vim::action(editor, cx, |vim, _: &CountCommand, window, cx| {
589 let Some(workspace) = vim.workspace(window) else {
590 return;
591 };
592 let count = Vim::take_count(cx).unwrap_or(1);
593 Vim::take_forced_motion(cx);
594 let n = if count > 1 {
595 format!(".,.+{}", count.saturating_sub(1))
596 } else {
597 ".".to_string()
598 };
599 workspace.update(cx, |workspace, cx| {
600 command_palette::CommandPalette::toggle(workspace, &n, window, cx);
601 })
602 });
603
604 Vim::action(editor, cx, |vim, action: &GoToLine, window, cx| {
605 vim.switch_mode(Mode::Normal, false, window, cx);
606 let result = vim.update_editor(cx, |vim, editor, cx| {
607 let snapshot = editor.snapshot(window, cx);
608 let buffer_row = action.range.head().buffer_row(vim, editor, window, cx)?;
609 let current = editor
610 .selections
611 .newest::<Point>(&editor.display_snapshot(cx));
612 let target = snapshot
613 .buffer_snapshot()
614 .clip_point(Point::new(buffer_row.0, current.head().column), Bias::Left);
615 editor.change_selections(Default::default(), window, cx, |s| {
616 s.select_ranges([target..target]);
617 });
618
619 anyhow::Ok(())
620 });
621 if let Some(e @ Err(_)) = result {
622 let Some(workspace) = vim.workspace(window) else {
623 return;
624 };
625 workspace.update(cx, |workspace, cx| {
626 e.notify_err(workspace, cx);
627 });
628 }
629 });
630
631 Vim::action(editor, cx, |vim, action: &YankCommand, window, cx| {
632 vim.update_editor(cx, |vim, editor, cx| {
633 let snapshot = editor.snapshot(window, cx);
634 if let Ok(range) = action.range.buffer_range(vim, editor, window, cx) {
635 let end = if range.end < snapshot.buffer_snapshot().max_row() {
636 Point::new(range.end.0 + 1, 0)
637 } else {
638 snapshot.buffer_snapshot().max_point()
639 };
640 vim.copy_ranges(
641 editor,
642 MotionKind::Linewise,
643 true,
644 vec![Point::new(range.start.0, 0)..end],
645 window,
646 cx,
647 )
648 }
649 });
650 });
651
652 Vim::action(editor, cx, |_, action: &WithCount, window, cx| {
653 for _ in 0..action.count {
654 window.dispatch_action(action.action.boxed_clone(), cx)
655 }
656 });
657
658 Vim::action(editor, cx, |vim, action: &WithRange, window, cx| {
659 let result = vim.update_editor(cx, |vim, editor, cx| {
660 action.range.buffer_range(vim, editor, window, cx)
661 });
662
663 let range = match result {
664 None => return,
665 Some(e @ Err(_)) => {
666 let Some(workspace) = vim.workspace(window) else {
667 return;
668 };
669 workspace.update(cx, |workspace, cx| {
670 e.notify_err(workspace, cx);
671 });
672 return;
673 }
674 Some(Ok(result)) => result,
675 };
676
677 let previous_selections = vim
678 .update_editor(cx, |_, editor, cx| {
679 let selections = action.restore_selection.then(|| {
680 editor
681 .selections
682 .disjoint_anchor_ranges()
683 .collect::<Vec<_>>()
684 });
685 let snapshot = editor.buffer().read(cx).snapshot(cx);
686 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
687 let end = Point::new(range.end.0, snapshot.line_len(range.end));
688 s.select_ranges([end..Point::new(range.start.0, 0)]);
689 });
690 selections
691 })
692 .flatten();
693 window.dispatch_action(action.action.boxed_clone(), cx);
694 cx.defer_in(window, move |vim, window, cx| {
695 vim.update_editor(cx, |_, editor, cx| {
696 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
697 if let Some(previous_selections) = previous_selections {
698 s.select_ranges(previous_selections);
699 } else {
700 s.select_ranges([
701 Point::new(range.start.0, 0)..Point::new(range.start.0, 0)
702 ]);
703 }
704 })
705 });
706 });
707 });
708
709 Vim::action(editor, cx, |vim, action: &OnMatchingLines, window, cx| {
710 action.run(vim, window, cx)
711 });
712
713 Vim::action(editor, cx, |vim, action: &ShellExec, window, cx| {
714 action.run(vim, window, cx)
715 })
716}
717
718#[derive(Default)]
719struct VimCommand {
720 prefix: &'static str,
721 suffix: &'static str,
722 action: Option<Box<dyn Action>>,
723 action_name: Option<&'static str>,
724 bang_action: Option<Box<dyn Action>>,
725 args: Option<
726 Box<dyn Fn(Box<dyn Action>, String) -> Option<Box<dyn Action>> + Send + Sync + 'static>,
727 >,
728 /// Optional range Range to use if no range is specified.
729 default_range: Option<CommandRange>,
730 range: Option<
731 Box<
732 dyn Fn(Box<dyn Action>, &CommandRange) -> Option<Box<dyn Action>>
733 + Send
734 + Sync
735 + 'static,
736 >,
737 >,
738 has_count: bool,
739 has_filename: bool,
740}
741
742struct ParsedQuery {
743 args: String,
744 has_bang: bool,
745 has_space: bool,
746}
747
748impl VimCommand {
749 fn new(pattern: (&'static str, &'static str), action: impl Action) -> Self {
750 Self {
751 prefix: pattern.0,
752 suffix: pattern.1,
753 action: Some(action.boxed_clone()),
754 ..Default::default()
755 }
756 }
757
758 // from_str is used for actions in other crates.
759 fn str(pattern: (&'static str, &'static str), action_name: &'static str) -> Self {
760 Self {
761 prefix: pattern.0,
762 suffix: pattern.1,
763 action_name: Some(action_name),
764 ..Default::default()
765 }
766 }
767
768 fn bang(mut self, bang_action: impl Action) -> Self {
769 self.bang_action = Some(bang_action.boxed_clone());
770 self
771 }
772
773 fn args(
774 mut self,
775 f: impl Fn(Box<dyn Action>, String) -> Option<Box<dyn Action>> + Send + Sync + 'static,
776 ) -> Self {
777 self.args = Some(Box::new(f));
778 self
779 }
780
781 fn filename(
782 mut self,
783 f: impl Fn(Box<dyn Action>, String) -> Option<Box<dyn Action>> + Send + Sync + 'static,
784 ) -> Self {
785 self.args = Some(Box::new(f));
786 self.has_filename = true;
787 self
788 }
789
790 fn range(
791 mut self,
792 f: impl Fn(Box<dyn Action>, &CommandRange) -> Option<Box<dyn Action>> + Send + Sync + 'static,
793 ) -> Self {
794 self.range = Some(Box::new(f));
795 self
796 }
797
798 fn default_range(mut self, range: CommandRange) -> Self {
799 self.default_range = Some(range);
800 self
801 }
802
803 fn count(mut self) -> Self {
804 self.has_count = true;
805 self
806 }
807
808 fn generate_filename_completions(
809 parsed_query: &ParsedQuery,
810 workspace: WeakEntity<Workspace>,
811 cx: &mut App,
812 ) -> Task<Vec<String>> {
813 let ParsedQuery {
814 args,
815 has_bang: _,
816 has_space: _,
817 } = parsed_query;
818 let Some(workspace) = workspace.upgrade() else {
819 return Task::ready(Vec::new());
820 };
821
822 let (task, args_path) = workspace.update(cx, |workspace, cx| {
823 let prefix = workspace
824 .project()
825 .read(cx)
826 .visible_worktrees(cx)
827 .map(|worktree| worktree.read(cx).abs_path().to_path_buf())
828 .next()
829 .or_else(std::env::home_dir)
830 .unwrap_or_else(|| PathBuf::from(""));
831
832 let rel_path = match RelPath::new(Path::new(&args), PathStyle::local()) {
833 Ok(path) => path.to_rel_path_buf(),
834 Err(_) => {
835 return (Task::ready(Ok(Vec::new())), RelPathBuf::new());
836 }
837 };
838
839 let rel_path = if args.ends_with(PathStyle::local().separator()) {
840 rel_path
841 } else {
842 rel_path
843 .parent()
844 .map(|rel_path| rel_path.to_rel_path_buf())
845 .unwrap_or(RelPathBuf::new())
846 };
847
848 let task = workspace.project().update(cx, |project, cx| {
849 let path = prefix
850 .join(rel_path.as_std_path())
851 .to_string_lossy()
852 .to_string();
853 project.list_directory(path, cx)
854 });
855
856 (task, rel_path)
857 });
858
859 cx.background_spawn(async move {
860 let directories = task.await.unwrap_or_default();
861 directories
862 .iter()
863 .map(|dir| {
864 let path = RelPath::new(dir.path.as_path(), PathStyle::local())
865 .map(|cow| cow.into_owned())
866 .unwrap_or(RelPathBuf::new());
867 let mut path_string = args_path
868 .join(&path)
869 .display(PathStyle::local())
870 .to_string();
871 if dir.is_dir {
872 path_string.push_str(PathStyle::local().separator());
873 }
874 path_string
875 })
876 .collect()
877 })
878 }
879
880 fn get_parsed_query(&self, query: String) -> Option<ParsedQuery> {
881 let rest = query
882 .strip_prefix(self.prefix)?
883 .to_string()
884 .chars()
885 .zip_longest(self.suffix.to_string().chars())
886 .skip_while(|e| e.clone().both().map(|(s, q)| s == q).unwrap_or(false))
887 .filter_map(|e| e.left())
888 .collect::<String>();
889 let has_bang = rest.starts_with('!');
890 let has_space = rest.starts_with("! ") || rest.starts_with(' ');
891 let args = if has_bang {
892 rest.strip_prefix('!')?.trim().to_string()
893 } else if rest.is_empty() {
894 "".into()
895 } else {
896 rest.strip_prefix(' ')?.trim().to_string()
897 };
898 Some(ParsedQuery {
899 args,
900 has_bang,
901 has_space,
902 })
903 }
904
905 fn parse(
906 &self,
907 query: &str,
908 range: &Option<CommandRange>,
909 cx: &App,
910 ) -> Option<Box<dyn Action>> {
911 let ParsedQuery {
912 args,
913 has_bang,
914 has_space: _,
915 } = self.get_parsed_query(query.to_string())?;
916 let action = if has_bang && self.bang_action.is_some() {
917 self.bang_action.as_ref().unwrap().boxed_clone()
918 } else if let Some(action) = self.action.as_ref() {
919 action.boxed_clone()
920 } else if let Some(action_name) = self.action_name {
921 cx.build_action(action_name, None).log_err()?
922 } else {
923 return None;
924 };
925
926 let action = if args.is_empty() {
927 action
928 } else {
929 // if command does not accept args and we have args then we should do no action
930 self.args.as_ref()?(action, args)?
931 };
932
933 let range = range.as_ref().or(self.default_range.as_ref());
934 if let Some(range) = range {
935 self.range.as_ref().and_then(|f| f(action, range))
936 } else {
937 Some(action)
938 }
939 }
940
941 // TODO: ranges with search queries
942 fn parse_range(query: &str) -> (Option<CommandRange>, String) {
943 let mut chars = query.chars().peekable();
944
945 match chars.peek() {
946 Some('%') => {
947 chars.next();
948 return (
949 Some(CommandRange {
950 start: Position::Line { row: 1, offset: 0 },
951 end: Some(Position::LastLine { offset: 0 }),
952 }),
953 chars.collect(),
954 );
955 }
956 Some('*') => {
957 chars.next();
958 return (
959 Some(CommandRange {
960 start: Position::Mark {
961 name: '<',
962 offset: 0,
963 },
964 end: Some(Position::Mark {
965 name: '>',
966 offset: 0,
967 }),
968 }),
969 chars.collect(),
970 );
971 }
972 _ => {}
973 }
974
975 let start = Self::parse_position(&mut chars);
976
977 match chars.peek() {
978 Some(',' | ';') => {
979 chars.next();
980 (
981 Some(CommandRange {
982 start: start.unwrap_or(Position::CurrentLine { offset: 0 }),
983 end: Self::parse_position(&mut chars),
984 }),
985 chars.collect(),
986 )
987 }
988 _ => (
989 start.map(|start| CommandRange { start, end: None }),
990 chars.collect(),
991 ),
992 }
993 }
994
995 fn parse_position(chars: &mut Peekable<Chars>) -> Option<Position> {
996 match chars.peek()? {
997 '0'..='9' => {
998 let row = Self::parse_u32(chars);
999 Some(Position::Line {
1000 row,
1001 offset: Self::parse_offset(chars),
1002 })
1003 }
1004 '\'' => {
1005 chars.next();
1006 let name = chars.next()?;
1007 Some(Position::Mark {
1008 name,
1009 offset: Self::parse_offset(chars),
1010 })
1011 }
1012 '.' => {
1013 chars.next();
1014 Some(Position::CurrentLine {
1015 offset: Self::parse_offset(chars),
1016 })
1017 }
1018 '+' | '-' => Some(Position::CurrentLine {
1019 offset: Self::parse_offset(chars),
1020 }),
1021 '$' => {
1022 chars.next();
1023 Some(Position::LastLine {
1024 offset: Self::parse_offset(chars),
1025 })
1026 }
1027 _ => None,
1028 }
1029 }
1030
1031 fn parse_offset(chars: &mut Peekable<Chars>) -> i32 {
1032 let mut res: i32 = 0;
1033 while matches!(chars.peek(), Some('+' | '-')) {
1034 let sign = if chars.next().unwrap() == '+' { 1 } else { -1 };
1035 let amount = if matches!(chars.peek(), Some('0'..='9')) {
1036 (Self::parse_u32(chars) as i32).saturating_mul(sign)
1037 } else {
1038 sign
1039 };
1040 res = res.saturating_add(amount)
1041 }
1042 res
1043 }
1044
1045 fn parse_u32(chars: &mut Peekable<Chars>) -> u32 {
1046 let mut res: u32 = 0;
1047 while matches!(chars.peek(), Some('0'..='9')) {
1048 res = res
1049 .saturating_mul(10)
1050 .saturating_add(chars.next().unwrap() as u32 - '0' as u32);
1051 }
1052 res
1053 }
1054}
1055
1056#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq)]
1057enum Position {
1058 Line { row: u32, offset: i32 },
1059 Mark { name: char, offset: i32 },
1060 LastLine { offset: i32 },
1061 CurrentLine { offset: i32 },
1062}
1063
1064impl Position {
1065 fn buffer_row(
1066 &self,
1067 vim: &Vim,
1068 editor: &mut Editor,
1069 window: &mut Window,
1070 cx: &mut App,
1071 ) -> Result<MultiBufferRow> {
1072 let snapshot = editor.snapshot(window, cx);
1073 let target = match self {
1074 Position::Line { row, offset } => {
1075 if let Some(anchor) = editor.active_excerpt(cx).and_then(|(_, buffer, _)| {
1076 editor.buffer().read(cx).buffer_point_to_anchor(
1077 &buffer,
1078 Point::new(row.saturating_sub(1), 0),
1079 cx,
1080 )
1081 }) {
1082 anchor
1083 .to_point(&snapshot.buffer_snapshot())
1084 .row
1085 .saturating_add_signed(*offset)
1086 } else {
1087 row.saturating_add_signed(offset.saturating_sub(1))
1088 }
1089 }
1090 Position::Mark { name, offset } => {
1091 let Some(Mark::Local(anchors)) =
1092 vim.get_mark(&name.to_string(), editor, window, cx)
1093 else {
1094 anyhow::bail!("mark {name} not set");
1095 };
1096 let Some(mark) = anchors.last() else {
1097 anyhow::bail!("mark {name} contains empty anchors");
1098 };
1099 mark.to_point(&snapshot.buffer_snapshot())
1100 .row
1101 .saturating_add_signed(*offset)
1102 }
1103 Position::LastLine { offset } => snapshot
1104 .buffer_snapshot()
1105 .max_row()
1106 .0
1107 .saturating_add_signed(*offset),
1108 Position::CurrentLine { offset } => editor
1109 .selections
1110 .newest_anchor()
1111 .head()
1112 .to_point(&snapshot.buffer_snapshot())
1113 .row
1114 .saturating_add_signed(*offset),
1115 };
1116
1117 Ok(MultiBufferRow(target).min(snapshot.buffer_snapshot().max_row()))
1118 }
1119}
1120
1121#[derive(Clone, Debug, PartialEq)]
1122pub(crate) struct CommandRange {
1123 start: Position,
1124 end: Option<Position>,
1125}
1126
1127impl CommandRange {
1128 fn head(&self) -> &Position {
1129 self.end.as_ref().unwrap_or(&self.start)
1130 }
1131
1132 /// Convert the `CommandRange` into a `Range<MultiBufferRow>`.
1133 pub(crate) fn buffer_range(
1134 &self,
1135 vim: &Vim,
1136 editor: &mut Editor,
1137 window: &mut Window,
1138 cx: &mut App,
1139 ) -> Result<Range<MultiBufferRow>> {
1140 let start = self.start.buffer_row(vim, editor, window, cx)?;
1141 let end = if let Some(end) = self.end.as_ref() {
1142 end.buffer_row(vim, editor, window, cx)?
1143 } else {
1144 start
1145 };
1146 if end < start {
1147 anyhow::Ok(end..start)
1148 } else {
1149 anyhow::Ok(start..end)
1150 }
1151 }
1152
1153 pub fn as_count(&self) -> Option<u32> {
1154 if let CommandRange {
1155 start: Position::Line { row, offset: 0 },
1156 end: None,
1157 } = &self
1158 {
1159 Some(*row)
1160 } else {
1161 None
1162 }
1163 }
1164
1165 /// The `CommandRange` representing the entire buffer.
1166 fn buffer() -> Self {
1167 Self {
1168 start: Position::Line { row: 1, offset: 0 },
1169 end: Some(Position::LastLine { offset: 0 }),
1170 }
1171 }
1172}
1173
1174fn generate_commands(_: &App) -> Vec<VimCommand> {
1175 vec![
1176 VimCommand::new(
1177 ("w", "rite"),
1178 workspace::Save {
1179 save_intent: Some(SaveIntent::Save),
1180 },
1181 )
1182 .bang(workspace::Save {
1183 save_intent: Some(SaveIntent::Overwrite),
1184 })
1185 .filename(|action, filename| {
1186 Some(
1187 VimSave {
1188 save_intent: action
1189 .as_any()
1190 .downcast_ref::<workspace::Save>()
1191 .and_then(|action| action.save_intent),
1192 filename,
1193 }
1194 .boxed_clone(),
1195 )
1196 }),
1197 VimCommand::new(("e", "dit"), editor::actions::ReloadFile)
1198 .bang(editor::actions::ReloadFile)
1199 .filename(|_, filename| Some(VimEdit { filename }.boxed_clone())),
1200 VimCommand::new(("sp", "lit"), workspace::SplitHorizontal).filename(|_, filename| {
1201 Some(
1202 VimSplit {
1203 vertical: false,
1204 filename,
1205 }
1206 .boxed_clone(),
1207 )
1208 }),
1209 VimCommand::new(("vs", "plit"), workspace::SplitVertical).filename(|_, filename| {
1210 Some(
1211 VimSplit {
1212 vertical: true,
1213 filename,
1214 }
1215 .boxed_clone(),
1216 )
1217 }),
1218 VimCommand::new(("tabe", "dit"), workspace::NewFile)
1219 .filename(|_action, filename| Some(VimEdit { filename }.boxed_clone())),
1220 VimCommand::new(("tabnew", ""), workspace::NewFile)
1221 .filename(|_action, filename| Some(VimEdit { filename }.boxed_clone())),
1222 VimCommand::new(
1223 ("q", "uit"),
1224 workspace::CloseActiveItem {
1225 save_intent: Some(SaveIntent::Close),
1226 close_pinned: false,
1227 },
1228 )
1229 .bang(workspace::CloseActiveItem {
1230 save_intent: Some(SaveIntent::Skip),
1231 close_pinned: true,
1232 }),
1233 VimCommand::new(
1234 ("wq", ""),
1235 workspace::CloseActiveItem {
1236 save_intent: Some(SaveIntent::Save),
1237 close_pinned: false,
1238 },
1239 )
1240 .bang(workspace::CloseActiveItem {
1241 save_intent: Some(SaveIntent::Overwrite),
1242 close_pinned: true,
1243 }),
1244 VimCommand::new(
1245 ("x", "it"),
1246 workspace::CloseActiveItem {
1247 save_intent: Some(SaveIntent::SaveAll),
1248 close_pinned: false,
1249 },
1250 )
1251 .bang(workspace::CloseActiveItem {
1252 save_intent: Some(SaveIntent::Overwrite),
1253 close_pinned: true,
1254 }),
1255 VimCommand::new(
1256 ("exi", "t"),
1257 workspace::CloseActiveItem {
1258 save_intent: Some(SaveIntent::SaveAll),
1259 close_pinned: false,
1260 },
1261 )
1262 .bang(workspace::CloseActiveItem {
1263 save_intent: Some(SaveIntent::Overwrite),
1264 close_pinned: true,
1265 }),
1266 VimCommand::new(
1267 ("up", "date"),
1268 workspace::Save {
1269 save_intent: Some(SaveIntent::SaveAll),
1270 },
1271 ),
1272 VimCommand::new(
1273 ("wa", "ll"),
1274 workspace::SaveAll {
1275 save_intent: Some(SaveIntent::SaveAll),
1276 },
1277 )
1278 .bang(workspace::SaveAll {
1279 save_intent: Some(SaveIntent::Overwrite),
1280 }),
1281 VimCommand::new(
1282 ("qa", "ll"),
1283 workspace::CloseAllItemsAndPanes {
1284 save_intent: Some(SaveIntent::Close),
1285 },
1286 )
1287 .bang(workspace::CloseAllItemsAndPanes {
1288 save_intent: Some(SaveIntent::Skip),
1289 }),
1290 VimCommand::new(
1291 ("quita", "ll"),
1292 workspace::CloseAllItemsAndPanes {
1293 save_intent: Some(SaveIntent::Close),
1294 },
1295 )
1296 .bang(workspace::CloseAllItemsAndPanes {
1297 save_intent: Some(SaveIntent::Skip),
1298 }),
1299 VimCommand::new(
1300 ("xa", "ll"),
1301 workspace::CloseAllItemsAndPanes {
1302 save_intent: Some(SaveIntent::SaveAll),
1303 },
1304 )
1305 .bang(workspace::CloseAllItemsAndPanes {
1306 save_intent: Some(SaveIntent::Overwrite),
1307 }),
1308 VimCommand::new(
1309 ("wqa", "ll"),
1310 workspace::CloseAllItemsAndPanes {
1311 save_intent: Some(SaveIntent::SaveAll),
1312 },
1313 )
1314 .bang(workspace::CloseAllItemsAndPanes {
1315 save_intent: Some(SaveIntent::Overwrite),
1316 }),
1317 VimCommand::new(("cq", "uit"), zed_actions::Quit),
1318 VimCommand::new(
1319 ("bd", "elete"),
1320 workspace::CloseActiveItem {
1321 save_intent: Some(SaveIntent::Close),
1322 close_pinned: false,
1323 },
1324 )
1325 .bang(workspace::CloseActiveItem {
1326 save_intent: Some(SaveIntent::Skip),
1327 close_pinned: true,
1328 }),
1329 VimCommand::new(
1330 ("norm", "al"),
1331 VimNorm {
1332 command: "".into(),
1333 range: None,
1334 },
1335 )
1336 .args(|_, args| {
1337 Some(
1338 VimNorm {
1339 command: args,
1340 range: None,
1341 }
1342 .boxed_clone(),
1343 )
1344 })
1345 .range(|action, range| {
1346 let mut action: VimNorm = action.as_any().downcast_ref::<VimNorm>().unwrap().clone();
1347 action.range.replace(range.clone());
1348 Some(Box::new(action))
1349 }),
1350 VimCommand::new(("bn", "ext"), workspace::ActivateNextItem).count(),
1351 VimCommand::new(("bN", "ext"), workspace::ActivatePreviousItem).count(),
1352 VimCommand::new(("bp", "revious"), workspace::ActivatePreviousItem).count(),
1353 VimCommand::new(("bf", "irst"), workspace::ActivateItem(0)),
1354 VimCommand::new(("br", "ewind"), workspace::ActivateItem(0)),
1355 VimCommand::new(("bl", "ast"), workspace::ActivateLastItem),
1356 VimCommand::str(("buffers", ""), "tab_switcher::ToggleAll"),
1357 VimCommand::str(("ls", ""), "tab_switcher::ToggleAll"),
1358 VimCommand::new(("new", ""), workspace::NewFileSplitHorizontal),
1359 VimCommand::new(("vne", "w"), workspace::NewFileSplitVertical),
1360 VimCommand::new(("tabn", "ext"), workspace::ActivateNextItem).count(),
1361 VimCommand::new(("tabp", "revious"), workspace::ActivatePreviousItem).count(),
1362 VimCommand::new(("tabN", "ext"), workspace::ActivatePreviousItem).count(),
1363 VimCommand::new(
1364 ("tabc", "lose"),
1365 workspace::CloseActiveItem {
1366 save_intent: Some(SaveIntent::Close),
1367 close_pinned: false,
1368 },
1369 ),
1370 VimCommand::new(
1371 ("tabo", "nly"),
1372 workspace::CloseOtherItems {
1373 save_intent: Some(SaveIntent::Close),
1374 close_pinned: false,
1375 },
1376 )
1377 .bang(workspace::CloseOtherItems {
1378 save_intent: Some(SaveIntent::Skip),
1379 close_pinned: false,
1380 }),
1381 VimCommand::new(
1382 ("on", "ly"),
1383 workspace::CloseInactiveTabsAndPanes {
1384 save_intent: Some(SaveIntent::Close),
1385 },
1386 )
1387 .bang(workspace::CloseInactiveTabsAndPanes {
1388 save_intent: Some(SaveIntent::Skip),
1389 }),
1390 VimCommand::str(("cl", "ist"), "diagnostics::Deploy"),
1391 VimCommand::new(("cc", ""), editor::actions::Hover),
1392 VimCommand::new(("ll", ""), editor::actions::Hover),
1393 VimCommand::new(("cn", "ext"), editor::actions::GoToDiagnostic::default())
1394 .range(wrap_count),
1395 VimCommand::new(
1396 ("cp", "revious"),
1397 editor::actions::GoToPreviousDiagnostic::default(),
1398 )
1399 .range(wrap_count),
1400 VimCommand::new(
1401 ("cN", "ext"),
1402 editor::actions::GoToPreviousDiagnostic::default(),
1403 )
1404 .range(wrap_count),
1405 VimCommand::new(
1406 ("lp", "revious"),
1407 editor::actions::GoToPreviousDiagnostic::default(),
1408 )
1409 .range(wrap_count),
1410 VimCommand::new(
1411 ("lN", "ext"),
1412 editor::actions::GoToPreviousDiagnostic::default(),
1413 )
1414 .range(wrap_count),
1415 VimCommand::new(("j", "oin"), JoinLines).range(select_range),
1416 VimCommand::new(("fo", "ld"), editor::actions::FoldSelectedRanges).range(act_on_range),
1417 VimCommand::new(("foldo", "pen"), editor::actions::UnfoldLines)
1418 .bang(editor::actions::UnfoldRecursive)
1419 .range(act_on_range),
1420 VimCommand::new(("foldc", "lose"), editor::actions::Fold)
1421 .bang(editor::actions::FoldRecursive)
1422 .range(act_on_range),
1423 VimCommand::new(("dif", "fupdate"), editor::actions::ToggleSelectedDiffHunks)
1424 .range(act_on_range),
1425 VimCommand::str(("rev", "ert"), "git::Restore").range(act_on_range),
1426 VimCommand::new(("d", "elete"), VisualDeleteLine).range(select_range),
1427 VimCommand::new(("y", "ank"), gpui::NoAction).range(|_, range| {
1428 Some(
1429 YankCommand {
1430 range: range.clone(),
1431 }
1432 .boxed_clone(),
1433 )
1434 }),
1435 VimCommand::new(("reg", "isters"), ToggleRegistersView).bang(ToggleRegistersView),
1436 VimCommand::new(("di", "splay"), ToggleRegistersView).bang(ToggleRegistersView),
1437 VimCommand::new(("marks", ""), ToggleMarksView).bang(ToggleMarksView),
1438 VimCommand::new(("delm", "arks"), ArgumentRequired)
1439 .bang(DeleteMarks::AllLocal)
1440 .args(|_, args| Some(DeleteMarks::Marks(args).boxed_clone())),
1441 VimCommand::new(("sor", "t"), SortLinesCaseSensitive)
1442 .range(select_range)
1443 .default_range(CommandRange::buffer()),
1444 VimCommand::new(("sort i", ""), SortLinesCaseInsensitive)
1445 .range(select_range)
1446 .default_range(CommandRange::buffer()),
1447 VimCommand::str(("E", "xplore"), "project_panel::ToggleFocus"),
1448 VimCommand::str(("H", "explore"), "project_panel::ToggleFocus"),
1449 VimCommand::str(("L", "explore"), "project_panel::ToggleFocus"),
1450 VimCommand::str(("S", "explore"), "project_panel::ToggleFocus"),
1451 VimCommand::str(("Ve", "xplore"), "project_panel::ToggleFocus"),
1452 VimCommand::str(("te", "rm"), "terminal_panel::Toggle"),
1453 VimCommand::str(("T", "erm"), "terminal_panel::Toggle"),
1454 VimCommand::str(("C", "ollab"), "collab_panel::ToggleFocus"),
1455 VimCommand::str(("No", "tifications"), "notification_panel::ToggleFocus"),
1456 VimCommand::str(("A", "I"), "agent::ToggleFocus"),
1457 VimCommand::str(("G", "it"), "git_panel::ToggleFocus"),
1458 VimCommand::str(("D", "ebug"), "debug_panel::ToggleFocus"),
1459 VimCommand::new(("noh", "lsearch"), search::buffer_search::Dismiss),
1460 VimCommand::new(("$", ""), EndOfDocument),
1461 VimCommand::new(("%", ""), EndOfDocument),
1462 VimCommand::new(("0", ""), StartOfDocument),
1463 VimCommand::new(("ex", ""), editor::actions::ReloadFile).bang(editor::actions::ReloadFile),
1464 VimCommand::new(("cpp", "link"), editor::actions::CopyPermalinkToLine).range(act_on_range),
1465 VimCommand::str(("opt", "ions"), "zed::OpenDefaultSettings"),
1466 VimCommand::str(("map", ""), "vim::OpenDefaultKeymap"),
1467 VimCommand::new(("h", "elp"), OpenDocs),
1468 ]
1469}
1470
1471struct VimCommands(Vec<VimCommand>);
1472// safety: we only ever access this from the main thread (as ensured by the cx argument)
1473// actions are not Sync so we can't otherwise use a OnceLock.
1474unsafe impl Sync for VimCommands {}
1475impl Global for VimCommands {}
1476
1477fn commands(cx: &App) -> &Vec<VimCommand> {
1478 static COMMANDS: OnceLock<VimCommands> = OnceLock::new();
1479 &COMMANDS
1480 .get_or_init(|| VimCommands(generate_commands(cx)))
1481 .0
1482}
1483
1484fn act_on_range(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
1485 Some(
1486 WithRange {
1487 restore_selection: true,
1488 range: range.clone(),
1489 action: WrappedAction(action),
1490 }
1491 .boxed_clone(),
1492 )
1493}
1494
1495fn select_range(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
1496 Some(
1497 WithRange {
1498 restore_selection: false,
1499 range: range.clone(),
1500 action: WrappedAction(action),
1501 }
1502 .boxed_clone(),
1503 )
1504}
1505
1506fn wrap_count(action: Box<dyn Action>, range: &CommandRange) -> Option<Box<dyn Action>> {
1507 range.as_count().map(|count| {
1508 WithCount {
1509 count,
1510 action: WrappedAction(action),
1511 }
1512 .boxed_clone()
1513 })
1514}
1515
1516pub fn command_interceptor(
1517 mut input: &str,
1518 workspace: WeakEntity<Workspace>,
1519 cx: &mut App,
1520) -> Task<CommandInterceptResult> {
1521 while input.starts_with(':') {
1522 input = &input[1..];
1523 }
1524
1525 let (range, query) = VimCommand::parse_range(input);
1526 let range_prefix = input[0..(input.len() - query.len())].to_string();
1527 let has_trailing_space = query.ends_with(" ");
1528 let mut query = query.as_str().trim();
1529
1530 let on_matching_lines = (query.starts_with('g') || query.starts_with('v'))
1531 .then(|| {
1532 let (pattern, range, search, invert) = OnMatchingLines::parse(query, &range)?;
1533 let start_idx = query.len() - pattern.len();
1534 query = query[start_idx..].trim();
1535 Some((range, search, invert))
1536 })
1537 .flatten();
1538
1539 let mut action = if range.is_some() && query.is_empty() {
1540 Some(
1541 GoToLine {
1542 range: range.clone().unwrap(),
1543 }
1544 .boxed_clone(),
1545 )
1546 } else if query.starts_with('/') || query.starts_with('?') {
1547 Some(
1548 FindCommand {
1549 query: query[1..].to_string(),
1550 backwards: query.starts_with('?'),
1551 }
1552 .boxed_clone(),
1553 )
1554 } else if query.starts_with("se ") || query.starts_with("set ") {
1555 let (prefix, option) = query.split_once(' ').unwrap();
1556 let mut commands = VimOption::possible_commands(option);
1557 if !commands.is_empty() {
1558 let query = prefix.to_string() + " " + option;
1559 for command in &mut commands {
1560 command.positions = generate_positions(&command.string, &query);
1561 }
1562 }
1563 return Task::ready(CommandInterceptResult {
1564 results: commands,
1565 exclusive: false,
1566 });
1567 } else if query.starts_with('s') {
1568 let mut substitute = "substitute".chars().peekable();
1569 let mut query = query.chars().peekable();
1570 while substitute
1571 .peek()
1572 .is_some_and(|char| Some(char) == query.peek())
1573 {
1574 substitute.next();
1575 query.next();
1576 }
1577 if let Some(replacement) = Replacement::parse(query) {
1578 let range = range.clone().unwrap_or(CommandRange {
1579 start: Position::CurrentLine { offset: 0 },
1580 end: None,
1581 });
1582 Some(ReplaceCommand { replacement, range }.boxed_clone())
1583 } else {
1584 None
1585 }
1586 } else if query.contains('!') {
1587 ShellExec::parse(query, range.clone())
1588 } else if on_matching_lines.is_some() {
1589 commands(cx)
1590 .iter()
1591 .find_map(|command| command.parse(query, &range, cx))
1592 } else {
1593 None
1594 };
1595
1596 if let Some((range, search, invert)) = on_matching_lines
1597 && let Some(ref inner) = action
1598 {
1599 action = Some(Box::new(OnMatchingLines {
1600 range,
1601 search,
1602 action: WrappedAction(inner.boxed_clone()),
1603 invert,
1604 }));
1605 };
1606
1607 if let Some(action) = action {
1608 let string = input.to_string();
1609 let positions = generate_positions(&string, &(range_prefix + query));
1610 return Task::ready(CommandInterceptResult {
1611 results: vec![CommandInterceptItem {
1612 action,
1613 string,
1614 positions,
1615 }],
1616 exclusive: false,
1617 });
1618 }
1619
1620 let Some((mut results, filenames)) =
1621 commands(cx).iter().enumerate().find_map(|(idx, command)| {
1622 let action = command.parse(query, &range, cx)?;
1623 let parsed_query = command.get_parsed_query(query.into())?;
1624 let display_string = ":".to_owned()
1625 + &range_prefix
1626 + command.prefix
1627 + command.suffix
1628 + if parsed_query.has_bang { "!" } else { "" };
1629 let space = if parsed_query.has_space { " " } else { "" };
1630
1631 let string = format!("{}{}{}", &display_string, &space, &parsed_query.args);
1632 let positions = generate_positions(&string, &(range_prefix.clone() + query));
1633
1634 let results = vec![CommandInterceptItem {
1635 action,
1636 string,
1637 positions,
1638 }];
1639
1640 let no_args_positions =
1641 generate_positions(&display_string, &(range_prefix.clone() + query));
1642
1643 // The following are valid autocomplete scenarios:
1644 // :w!filename.txt
1645 // :w filename.txt
1646 // :w[space]
1647 if !command.has_filename
1648 || (!has_trailing_space && !parsed_query.has_bang && parsed_query.args.is_empty())
1649 {
1650 return Some((results, None));
1651 }
1652
1653 Some((
1654 results,
1655 Some((idx, parsed_query, display_string, no_args_positions)),
1656 ))
1657 })
1658 else {
1659 return Task::ready(CommandInterceptResult::default());
1660 };
1661
1662 if let Some((cmd_idx, parsed_query, display_string, no_args_positions)) = filenames {
1663 let filenames = VimCommand::generate_filename_completions(&parsed_query, workspace, cx);
1664 cx.spawn(async move |cx| {
1665 let filenames = filenames.await;
1666 const MAX_RESULTS: usize = 100;
1667 let executor = cx.background_executor().clone();
1668 let mut candidates = Vec::with_capacity(filenames.len());
1669
1670 for (idx, filename) in filenames.iter().enumerate() {
1671 candidates.push(fuzzy::StringMatchCandidate::new(idx, &filename));
1672 }
1673 let filenames = fuzzy::match_strings(
1674 &candidates,
1675 &parsed_query.args,
1676 false,
1677 true,
1678 MAX_RESULTS,
1679 &Default::default(),
1680 executor,
1681 )
1682 .await;
1683
1684 for fuzzy::StringMatch {
1685 candidate_id: _,
1686 score: _,
1687 positions,
1688 string,
1689 } in filenames
1690 {
1691 let offset = display_string.len() + 1;
1692 let mut positions: Vec<_> = positions.iter().map(|&pos| pos + offset).collect();
1693 positions.splice(0..0, no_args_positions.clone());
1694 let string = format!("{display_string} {string}");
1695 let action = match cx
1696 .update(|cx| commands(cx).get(cmd_idx)?.parse(&string[1..], &range, cx))
1697 {
1698 Ok(Some(action)) => action,
1699 _ => continue,
1700 };
1701 results.push(CommandInterceptItem {
1702 action,
1703 string,
1704 positions,
1705 });
1706 }
1707 CommandInterceptResult {
1708 results,
1709 exclusive: true,
1710 }
1711 })
1712 } else {
1713 Task::ready(CommandInterceptResult {
1714 results,
1715 exclusive: false,
1716 })
1717 }
1718}
1719
1720fn generate_positions(string: &str, query: &str) -> Vec<usize> {
1721 let mut positions = Vec::new();
1722 let mut chars = query.chars();
1723
1724 let Some(mut current) = chars.next() else {
1725 return positions;
1726 };
1727
1728 for (i, c) in string.char_indices() {
1729 if c == current {
1730 positions.push(i);
1731 if let Some(c) = chars.next() {
1732 current = c;
1733 } else {
1734 break;
1735 }
1736 }
1737 }
1738
1739 positions
1740}
1741
1742/// Applies a command to all lines matching a pattern.
1743#[derive(Debug, PartialEq, Clone, Action)]
1744#[action(namespace = vim, no_json, no_register)]
1745pub(crate) struct OnMatchingLines {
1746 range: CommandRange,
1747 search: String,
1748 action: WrappedAction,
1749 invert: bool,
1750}
1751
1752impl OnMatchingLines {
1753 // convert a vim query into something more usable by zed.
1754 // we don't attempt to fully convert between the two regex syntaxes,
1755 // but we do flip \( and \) to ( and ) (and vice-versa) in the pattern,
1756 // and convert \0..\9 to $0..$9 in the replacement so that common idioms work.
1757 pub(crate) fn parse(
1758 query: &str,
1759 range: &Option<CommandRange>,
1760 ) -> Option<(String, CommandRange, String, bool)> {
1761 let mut global = "global".chars().peekable();
1762 let mut query_chars = query.chars().peekable();
1763 let mut invert = false;
1764 if query_chars.peek() == Some(&'v') {
1765 invert = true;
1766 query_chars.next();
1767 }
1768 while global
1769 .peek()
1770 .is_some_and(|char| Some(char) == query_chars.peek())
1771 {
1772 global.next();
1773 query_chars.next();
1774 }
1775 if !invert && query_chars.peek() == Some(&'!') {
1776 invert = true;
1777 query_chars.next();
1778 }
1779 let range = range.clone().unwrap_or(CommandRange {
1780 start: Position::Line { row: 0, offset: 0 },
1781 end: Some(Position::LastLine { offset: 0 }),
1782 });
1783
1784 let delimiter = query_chars.next().filter(|c| {
1785 !c.is_alphanumeric() && *c != '"' && *c != '|' && *c != '\'' && *c != '!'
1786 })?;
1787
1788 let mut search = String::new();
1789 let mut escaped = false;
1790
1791 for c in query_chars.by_ref() {
1792 if escaped {
1793 escaped = false;
1794 // unescape escaped parens
1795 if c != '(' && c != ')' && c != delimiter {
1796 search.push('\\')
1797 }
1798 search.push(c)
1799 } else if c == '\\' {
1800 escaped = true;
1801 } else if c == delimiter {
1802 break;
1803 } else {
1804 // escape unescaped parens
1805 if c == '(' || c == ')' {
1806 search.push('\\')
1807 }
1808 search.push(c)
1809 }
1810 }
1811
1812 Some((query_chars.collect::<String>(), range, search, invert))
1813 }
1814
1815 pub fn run(&self, vim: &mut Vim, window: &mut Window, cx: &mut Context<Vim>) {
1816 let result = vim.update_editor(cx, |vim, editor, cx| {
1817 self.range.buffer_range(vim, editor, window, cx)
1818 });
1819
1820 let range = match result {
1821 None => return,
1822 Some(e @ Err(_)) => {
1823 let Some(workspace) = vim.workspace(window) else {
1824 return;
1825 };
1826 workspace.update(cx, |workspace, cx| {
1827 e.notify_err(workspace, cx);
1828 });
1829 return;
1830 }
1831 Some(Ok(result)) => result,
1832 };
1833
1834 let mut action = self.action.boxed_clone();
1835 let mut last_pattern = self.search.clone();
1836
1837 let mut regexes = match Regex::new(&self.search) {
1838 Ok(regex) => vec![(regex, !self.invert)],
1839 e @ Err(_) => {
1840 let Some(workspace) = vim.workspace(window) else {
1841 return;
1842 };
1843 workspace.update(cx, |workspace, cx| {
1844 e.notify_err(workspace, cx);
1845 });
1846 return;
1847 }
1848 };
1849 while let Some(inner) = action
1850 .boxed_clone()
1851 .as_any()
1852 .downcast_ref::<OnMatchingLines>()
1853 {
1854 let Some(regex) = Regex::new(&inner.search).ok() else {
1855 break;
1856 };
1857 last_pattern = inner.search.clone();
1858 action = inner.action.boxed_clone();
1859 regexes.push((regex, !inner.invert))
1860 }
1861
1862 if let Some(pane) = vim.pane(window, cx) {
1863 pane.update(cx, |pane, cx| {
1864 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()
1865 {
1866 search_bar.update(cx, |search_bar, cx| {
1867 if search_bar.show(window, cx) {
1868 let _ = search_bar.search(
1869 &last_pattern,
1870 Some(SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE),
1871 false,
1872 window,
1873 cx,
1874 );
1875 }
1876 });
1877 }
1878 });
1879 };
1880
1881 vim.update_editor(cx, |_, editor, cx| {
1882 let snapshot = editor.snapshot(window, cx);
1883 let mut row = range.start.0;
1884
1885 let point_range = Point::new(range.start.0, 0)
1886 ..snapshot
1887 .buffer_snapshot()
1888 .clip_point(Point::new(range.end.0 + 1, 0), Bias::Left);
1889 cx.spawn_in(window, async move |editor, cx| {
1890 let new_selections = cx
1891 .background_spawn(async move {
1892 let mut line = String::new();
1893 let mut new_selections = Vec::new();
1894 let chunks = snapshot
1895 .buffer_snapshot()
1896 .text_for_range(point_range)
1897 .chain(["\n"]);
1898
1899 for chunk in chunks {
1900 for (newline_ix, text) in chunk.split('\n').enumerate() {
1901 if newline_ix > 0 {
1902 if regexes.iter().all(|(regex, should_match)| {
1903 regex.is_match(&line) == *should_match
1904 }) {
1905 new_selections
1906 .push(Point::new(row, 0).to_display_point(&snapshot))
1907 }
1908 row += 1;
1909 line.clear();
1910 }
1911 line.push_str(text)
1912 }
1913 }
1914
1915 new_selections
1916 })
1917 .await;
1918
1919 if new_selections.is_empty() {
1920 return;
1921 }
1922 editor
1923 .update_in(cx, |editor, window, cx| {
1924 editor.start_transaction_at(Instant::now(), window, cx);
1925 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1926 s.replace_cursors_with(|_| new_selections);
1927 });
1928 window.dispatch_action(action, cx);
1929 cx.defer_in(window, move |editor, window, cx| {
1930 let newest = editor
1931 .selections
1932 .newest::<Point>(&editor.display_snapshot(cx));
1933 editor.change_selections(
1934 SelectionEffects::no_scroll(),
1935 window,
1936 cx,
1937 |s| {
1938 s.select(vec![newest]);
1939 },
1940 );
1941 editor.end_transaction_at(Instant::now(), cx);
1942 })
1943 })
1944 .ok();
1945 })
1946 .detach();
1947 });
1948 }
1949}
1950
1951/// Executes a shell command and returns the output.
1952#[derive(Clone, Debug, PartialEq, Action)]
1953#[action(namespace = vim, no_json, no_register)]
1954pub struct ShellExec {
1955 command: String,
1956 range: Option<CommandRange>,
1957 is_read: bool,
1958}
1959
1960impl Vim {
1961 pub fn cancel_running_command(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1962 if self.running_command.take().is_some() {
1963 self.update_editor(cx, |_, editor, cx| {
1964 editor.transact(window, cx, |editor, _window, _cx| {
1965 editor.clear_row_highlights::<ShellExec>();
1966 })
1967 });
1968 }
1969 }
1970
1971 fn prepare_shell_command(
1972 &mut self,
1973 command: &str,
1974 _: &mut Window,
1975 cx: &mut Context<Self>,
1976 ) -> String {
1977 let mut ret = String::new();
1978 // N.B. non-standard escaping rules:
1979 // * !echo % => "echo README.md"
1980 // * !echo \% => "echo %"
1981 // * !echo \\% => echo \%
1982 // * !echo \\\% => echo \\%
1983 for c in command.chars() {
1984 if c != '%' && c != '!' {
1985 ret.push(c);
1986 continue;
1987 } else if ret.chars().last() == Some('\\') {
1988 ret.pop();
1989 ret.push(c);
1990 continue;
1991 }
1992 match c {
1993 '%' => {
1994 self.update_editor(cx, |_, editor, cx| {
1995 if let Some((_, buffer, _)) = editor.active_excerpt(cx)
1996 && let Some(file) = buffer.read(cx).file()
1997 && let Some(local) = file.as_local()
1998 {
1999 ret.push_str(&local.path().display(local.path_style(cx)));
2000 }
2001 });
2002 }
2003 '!' => {
2004 if let Some(command) = &self.last_command {
2005 ret.push_str(command)
2006 }
2007 }
2008 _ => {}
2009 }
2010 }
2011 self.last_command = Some(ret.clone());
2012 ret
2013 }
2014
2015 pub fn shell_command_motion(
2016 &mut self,
2017 motion: Motion,
2018 times: Option<usize>,
2019 forced_motion: bool,
2020 window: &mut Window,
2021 cx: &mut Context<Vim>,
2022 ) {
2023 self.stop_recording(cx);
2024 let Some(workspace) = self.workspace(window) else {
2025 return;
2026 };
2027 let command = self.update_editor(cx, |_, editor, cx| {
2028 let snapshot = editor.snapshot(window, cx);
2029 let start = editor
2030 .selections
2031 .newest_display(&editor.display_snapshot(cx));
2032 let text_layout_details = editor.text_layout_details(window);
2033 let (mut range, _) = motion
2034 .range(
2035 &snapshot,
2036 start.clone(),
2037 times,
2038 &text_layout_details,
2039 forced_motion,
2040 )
2041 .unwrap_or((start.range(), MotionKind::Exclusive));
2042 if range.start != start.start {
2043 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2044 s.select_ranges([
2045 range.start.to_point(&snapshot)..range.start.to_point(&snapshot)
2046 ]);
2047 })
2048 }
2049 if range.end.row() > range.start.row() && range.end.column() != 0 {
2050 *range.end.row_mut() -= 1
2051 }
2052 if range.end.row() == range.start.row() {
2053 ".!".to_string()
2054 } else {
2055 format!(".,.+{}!", (range.end.row() - range.start.row()).0)
2056 }
2057 });
2058 if let Some(command) = command {
2059 workspace.update(cx, |workspace, cx| {
2060 command_palette::CommandPalette::toggle(workspace, &command, window, cx);
2061 });
2062 }
2063 }
2064
2065 pub fn shell_command_object(
2066 &mut self,
2067 object: Object,
2068 around: bool,
2069 window: &mut Window,
2070 cx: &mut Context<Vim>,
2071 ) {
2072 self.stop_recording(cx);
2073 let Some(workspace) = self.workspace(window) else {
2074 return;
2075 };
2076 let command = self.update_editor(cx, |_, editor, cx| {
2077 let snapshot = editor.snapshot(window, cx);
2078 let start = editor
2079 .selections
2080 .newest_display(&editor.display_snapshot(cx));
2081 let range = object
2082 .range(&snapshot, start.clone(), around, None)
2083 .unwrap_or(start.range());
2084 if range.start != start.start {
2085 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2086 s.select_ranges([
2087 range.start.to_point(&snapshot)..range.start.to_point(&snapshot)
2088 ]);
2089 })
2090 }
2091 if range.end.row() == range.start.row() {
2092 ".!".to_string()
2093 } else {
2094 format!(".,.+{}!", (range.end.row() - range.start.row()).0)
2095 }
2096 });
2097 if let Some(command) = command {
2098 workspace.update(cx, |workspace, cx| {
2099 command_palette::CommandPalette::toggle(workspace, &command, window, cx);
2100 });
2101 }
2102 }
2103}
2104
2105impl ShellExec {
2106 pub fn parse(query: &str, range: Option<CommandRange>) -> Option<Box<dyn Action>> {
2107 let (before, after) = query.split_once('!')?;
2108 let before = before.trim();
2109
2110 if !"read".starts_with(before) {
2111 return None;
2112 }
2113
2114 Some(
2115 ShellExec {
2116 command: after.trim().to_string(),
2117 range,
2118 is_read: !before.is_empty(),
2119 }
2120 .boxed_clone(),
2121 )
2122 }
2123
2124 pub fn run(&self, vim: &mut Vim, window: &mut Window, cx: &mut Context<Vim>) {
2125 let Some(workspace) = vim.workspace(window) else {
2126 return;
2127 };
2128
2129 let project = workspace.read(cx).project().clone();
2130 let command = vim.prepare_shell_command(&self.command, window, cx);
2131
2132 if self.range.is_none() && !self.is_read {
2133 workspace.update(cx, |workspace, cx| {
2134 let project = workspace.project().read(cx);
2135 let cwd = project.first_project_directory(cx);
2136 let shell = project.terminal_settings(&cwd, cx).shell.clone();
2137
2138 let spawn_in_terminal = SpawnInTerminal {
2139 id: TaskId("vim".to_string()),
2140 full_label: command.clone(),
2141 label: command.clone(),
2142 command: Some(command.clone()),
2143 args: Vec::new(),
2144 command_label: command.clone(),
2145 cwd,
2146 env: HashMap::default(),
2147 use_new_terminal: true,
2148 allow_concurrent_runs: true,
2149 reveal: RevealStrategy::NoFocus,
2150 reveal_target: RevealTarget::Dock,
2151 hide: HideStrategy::Never,
2152 shell,
2153 show_summary: false,
2154 show_command: false,
2155 show_rerun: false,
2156 };
2157
2158 let task_status = workspace.spawn_in_terminal(spawn_in_terminal, window, cx);
2159 cx.background_spawn(async move {
2160 match task_status.await {
2161 Some(Ok(status)) => {
2162 if status.success() {
2163 log::debug!("Vim shell exec succeeded");
2164 } else {
2165 log::debug!("Vim shell exec failed, code: {:?}", status.code());
2166 }
2167 }
2168 Some(Err(e)) => log::error!("Vim shell exec failed: {e}"),
2169 None => log::debug!("Vim shell exec got cancelled"),
2170 }
2171 })
2172 .detach();
2173 });
2174 return;
2175 };
2176
2177 let mut input_snapshot = None;
2178 let mut input_range = None;
2179 let mut needs_newline_prefix = false;
2180 vim.update_editor(cx, |vim, editor, cx| {
2181 let snapshot = editor.buffer().read(cx).snapshot(cx);
2182 let range = if let Some(range) = self.range.clone() {
2183 let Some(range) = range.buffer_range(vim, editor, window, cx).log_err() else {
2184 return;
2185 };
2186 Point::new(range.start.0, 0)
2187 ..snapshot.clip_point(Point::new(range.end.0 + 1, 0), Bias::Right)
2188 } else {
2189 let mut end = editor
2190 .selections
2191 .newest::<Point>(&editor.display_snapshot(cx))
2192 .range()
2193 .end;
2194 end = snapshot.clip_point(Point::new(end.row + 1, 0), Bias::Right);
2195 needs_newline_prefix = end == snapshot.max_point();
2196 end..end
2197 };
2198 if self.is_read {
2199 input_range =
2200 Some(snapshot.anchor_after(range.end)..snapshot.anchor_after(range.end));
2201 } else {
2202 input_range =
2203 Some(snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end));
2204 }
2205 editor.highlight_rows::<ShellExec>(
2206 input_range.clone().unwrap(),
2207 cx.theme().status().unreachable_background,
2208 Default::default(),
2209 cx,
2210 );
2211
2212 if !self.is_read {
2213 input_snapshot = Some(snapshot)
2214 }
2215 });
2216
2217 let Some(range) = input_range else { return };
2218
2219 let process_task = project.update(cx, |project, cx| project.exec_in_shell(command, cx));
2220
2221 let is_read = self.is_read;
2222
2223 let task = cx.spawn_in(window, async move |vim, cx| {
2224 let Some(mut process) = process_task.await.log_err() else {
2225 return;
2226 };
2227 process.stdout(Stdio::piped());
2228 process.stderr(Stdio::piped());
2229
2230 if input_snapshot.is_some() {
2231 process.stdin(Stdio::piped());
2232 } else {
2233 process.stdin(Stdio::null());
2234 };
2235
2236 let Some(mut running) = process.spawn().log_err() else {
2237 vim.update_in(cx, |vim, window, cx| {
2238 vim.cancel_running_command(window, cx);
2239 })
2240 .log_err();
2241 return;
2242 };
2243
2244 if let Some(mut stdin) = running.stdin.take()
2245 && let Some(snapshot) = input_snapshot
2246 {
2247 let range = range.clone();
2248 cx.background_spawn(async move {
2249 for chunk in snapshot.text_for_range(range) {
2250 if stdin.write_all(chunk.as_bytes()).await.log_err().is_none() {
2251 return;
2252 }
2253 }
2254 stdin.flush().await.log_err();
2255 })
2256 .detach();
2257 };
2258
2259 let output = cx.background_spawn(running.output()).await;
2260
2261 let Some(output) = output.log_err() else {
2262 vim.update_in(cx, |vim, window, cx| {
2263 vim.cancel_running_command(window, cx);
2264 })
2265 .log_err();
2266 return;
2267 };
2268 let mut text = String::new();
2269 if needs_newline_prefix {
2270 text.push('\n');
2271 }
2272 text.push_str(&String::from_utf8_lossy(&output.stdout));
2273 text.push_str(&String::from_utf8_lossy(&output.stderr));
2274 if !text.is_empty() && text.chars().last() != Some('\n') {
2275 text.push('\n');
2276 }
2277
2278 vim.update_in(cx, |vim, window, cx| {
2279 vim.update_editor(cx, |_, editor, cx| {
2280 editor.transact(window, cx, |editor, window, cx| {
2281 editor.edit([(range.clone(), text)], cx);
2282 let snapshot = editor.buffer().read(cx).snapshot(cx);
2283 editor.change_selections(Default::default(), window, cx, |s| {
2284 let point = if is_read {
2285 let point = range.end.to_point(&snapshot);
2286 Point::new(point.row.saturating_sub(1), 0)
2287 } else {
2288 let point = range.start.to_point(&snapshot);
2289 Point::new(point.row, 0)
2290 };
2291 s.select_ranges([point..point]);
2292 })
2293 })
2294 });
2295 vim.cancel_running_command(window, cx);
2296 })
2297 .log_err();
2298 });
2299 vim.running_command.replace(task);
2300 }
2301}
2302
2303#[cfg(test)]
2304mod test {
2305 use std::path::Path;
2306
2307 use crate::{
2308 VimAddon,
2309 state::Mode,
2310 test::{NeovimBackedTestContext, VimTestContext},
2311 };
2312 use editor::{Editor, EditorSettings};
2313 use gpui::{Context, TestAppContext};
2314 use indoc::indoc;
2315 use settings::Settings;
2316 use util::path;
2317 use workspace::Workspace;
2318
2319 #[gpui::test]
2320 async fn test_command_basics(cx: &mut TestAppContext) {
2321 let mut cx = NeovimBackedTestContext::new(cx).await;
2322
2323 cx.set_shared_state(indoc! {"
2324 ˇa
2325 b
2326 c"})
2327 .await;
2328
2329 cx.simulate_shared_keystrokes(": j enter").await;
2330
2331 // hack: our cursor positioning after a join command is wrong
2332 cx.simulate_shared_keystrokes("^").await;
2333 cx.shared_state().await.assert_eq(indoc! {
2334 "ˇa b
2335 c"
2336 });
2337 }
2338
2339 #[gpui::test]
2340 async fn test_command_goto(cx: &mut TestAppContext) {
2341 let mut cx = NeovimBackedTestContext::new(cx).await;
2342
2343 cx.set_shared_state(indoc! {"
2344 ˇa
2345 b
2346 c"})
2347 .await;
2348 cx.simulate_shared_keystrokes(": 3 enter").await;
2349 cx.shared_state().await.assert_eq(indoc! {"
2350 a
2351 b
2352 ˇc"});
2353 }
2354
2355 #[gpui::test]
2356 async fn test_command_replace(cx: &mut TestAppContext) {
2357 let mut cx = NeovimBackedTestContext::new(cx).await;
2358
2359 cx.set_shared_state(indoc! {"
2360 ˇa
2361 b
2362 b
2363 c"})
2364 .await;
2365 cx.simulate_shared_keystrokes(": % s / b / d enter").await;
2366 cx.shared_state().await.assert_eq(indoc! {"
2367 a
2368 d
2369 ˇd
2370 c"});
2371 cx.simulate_shared_keystrokes(": % s : . : \\ 0 \\ 0 enter")
2372 .await;
2373 cx.shared_state().await.assert_eq(indoc! {"
2374 aa
2375 dd
2376 dd
2377 ˇcc"});
2378 cx.simulate_shared_keystrokes("k : s / d d / e e enter")
2379 .await;
2380 cx.shared_state().await.assert_eq(indoc! {"
2381 aa
2382 dd
2383 ˇee
2384 cc"});
2385 }
2386
2387 #[gpui::test]
2388 async fn test_command_search(cx: &mut TestAppContext) {
2389 let mut cx = NeovimBackedTestContext::new(cx).await;
2390
2391 cx.set_shared_state(indoc! {"
2392 ˇa
2393 b
2394 a
2395 c"})
2396 .await;
2397 cx.simulate_shared_keystrokes(": / b enter").await;
2398 cx.shared_state().await.assert_eq(indoc! {"
2399 a
2400 ˇb
2401 a
2402 c"});
2403 cx.simulate_shared_keystrokes(": ? a enter").await;
2404 cx.shared_state().await.assert_eq(indoc! {"
2405 ˇa
2406 b
2407 a
2408 c"});
2409 }
2410
2411 #[gpui::test]
2412 async fn test_command_write(cx: &mut TestAppContext) {
2413 let mut cx = VimTestContext::new(cx, true).await;
2414 let path = Path::new(path!("/root/dir/file.rs"));
2415 let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
2416
2417 cx.simulate_keystrokes("i @ escape");
2418 cx.simulate_keystrokes(": w enter");
2419
2420 assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "@\n");
2421
2422 fs.as_fake().insert_file(path, b"oops\n".to_vec()).await;
2423
2424 // conflict!
2425 cx.simulate_keystrokes("i @ escape");
2426 cx.simulate_keystrokes(": w enter");
2427 cx.simulate_prompt_answer("Cancel");
2428
2429 assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "oops\n");
2430 assert!(!cx.has_pending_prompt());
2431 cx.simulate_keystrokes(": w !");
2432 cx.simulate_keystrokes("enter");
2433 assert!(!cx.has_pending_prompt());
2434 assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "@@\n");
2435 }
2436
2437 #[gpui::test]
2438 async fn test_command_quit(cx: &mut TestAppContext) {
2439 let mut cx = VimTestContext::new(cx, true).await;
2440
2441 cx.simulate_keystrokes(": n e w enter");
2442 cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
2443 cx.simulate_keystrokes(": q enter");
2444 cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 1));
2445 cx.simulate_keystrokes(": n e w enter");
2446 cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
2447 cx.simulate_keystrokes(": q a enter");
2448 cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 0));
2449 }
2450
2451 #[gpui::test]
2452 async fn test_offsets(cx: &mut TestAppContext) {
2453 let mut cx = NeovimBackedTestContext::new(cx).await;
2454
2455 cx.set_shared_state("ˇ1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n")
2456 .await;
2457
2458 cx.simulate_shared_keystrokes(": + enter").await;
2459 cx.shared_state()
2460 .await
2461 .assert_eq("1\nˇ2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n");
2462
2463 cx.simulate_shared_keystrokes(": 1 0 - enter").await;
2464 cx.shared_state()
2465 .await
2466 .assert_eq("1\n2\n3\n4\n5\n6\n7\n8\nˇ9\n10\n11\n");
2467
2468 cx.simulate_shared_keystrokes(": . - 2 enter").await;
2469 cx.shared_state()
2470 .await
2471 .assert_eq("1\n2\n3\n4\n5\n6\nˇ7\n8\n9\n10\n11\n");
2472
2473 cx.simulate_shared_keystrokes(": % enter").await;
2474 cx.shared_state()
2475 .await
2476 .assert_eq("1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\nˇ");
2477 }
2478
2479 #[gpui::test]
2480 async fn test_command_ranges(cx: &mut TestAppContext) {
2481 let mut cx = NeovimBackedTestContext::new(cx).await;
2482
2483 cx.set_shared_state("ˇ1\n2\n3\n4\n4\n3\n2\n1").await;
2484
2485 cx.simulate_shared_keystrokes(": 2 , 4 d enter").await;
2486 cx.shared_state().await.assert_eq("1\nˇ4\n3\n2\n1");
2487
2488 cx.simulate_shared_keystrokes(": 2 , 4 s o r t enter").await;
2489 cx.shared_state().await.assert_eq("1\nˇ2\n3\n4\n1");
2490
2491 cx.simulate_shared_keystrokes(": 2 , 4 j o i n enter").await;
2492 cx.shared_state().await.assert_eq("1\nˇ2 3 4\n1");
2493 }
2494
2495 #[gpui::test]
2496 async fn test_command_visual_replace(cx: &mut TestAppContext) {
2497 let mut cx = NeovimBackedTestContext::new(cx).await;
2498
2499 cx.set_shared_state("ˇ1\n2\n3\n4\n4\n3\n2\n1").await;
2500
2501 cx.simulate_shared_keystrokes("v 2 j : s / . / k enter")
2502 .await;
2503 cx.shared_state().await.assert_eq("k\nk\nˇk\n4\n4\n3\n2\n1");
2504 }
2505
2506 #[track_caller]
2507 fn assert_active_item(
2508 workspace: &mut Workspace,
2509 expected_path: &str,
2510 expected_text: &str,
2511 cx: &mut Context<Workspace>,
2512 ) {
2513 let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();
2514
2515 let buffer = active_editor
2516 .read(cx)
2517 .buffer()
2518 .read(cx)
2519 .as_singleton()
2520 .unwrap();
2521
2522 let text = buffer.read(cx).text();
2523 let file = buffer.read(cx).file().unwrap();
2524 let file_path = file.as_local().unwrap().abs_path(cx);
2525
2526 assert_eq!(text, expected_text);
2527 assert_eq!(file_path, Path::new(expected_path));
2528 }
2529
2530 #[gpui::test]
2531 async fn test_command_gf(cx: &mut TestAppContext) {
2532 let mut cx = VimTestContext::new(cx, true).await;
2533
2534 // Assert base state, that we're in /root/dir/file.rs
2535 cx.workspace(|workspace, _, cx| {
2536 assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
2537 });
2538
2539 // Insert a new file
2540 let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
2541 fs.as_fake()
2542 .insert_file(
2543 path!("/root/dir/file2.rs"),
2544 "This is file2.rs".as_bytes().to_vec(),
2545 )
2546 .await;
2547 fs.as_fake()
2548 .insert_file(
2549 path!("/root/dir/file3.rs"),
2550 "go to file3".as_bytes().to_vec(),
2551 )
2552 .await;
2553
2554 // Put the path to the second file into the currently open buffer
2555 cx.set_state(indoc! {"go to fiˇle2.rs"}, Mode::Normal);
2556
2557 // Go to file2.rs
2558 cx.simulate_keystrokes("g f");
2559
2560 // We now have two items
2561 cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
2562 cx.workspace(|workspace, _, cx| {
2563 assert_active_item(
2564 workspace,
2565 path!("/root/dir/file2.rs"),
2566 "This is file2.rs",
2567 cx,
2568 );
2569 });
2570
2571 // Update editor to point to `file2.rs`
2572 cx.editor =
2573 cx.workspace(|workspace, _, cx| workspace.active_item_as::<Editor>(cx).unwrap());
2574
2575 // Put the path to the third file into the currently open buffer,
2576 // but remove its suffix, because we want that lookup to happen automatically.
2577 cx.set_state(indoc! {"go to fiˇle3"}, Mode::Normal);
2578
2579 // Go to file3.rs
2580 cx.simulate_keystrokes("g f");
2581
2582 // We now have three items
2583 cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 3));
2584 cx.workspace(|workspace, _, cx| {
2585 assert_active_item(workspace, path!("/root/dir/file3.rs"), "go to file3", cx);
2586 });
2587 }
2588
2589 #[gpui::test]
2590 async fn test_command_write_filename(cx: &mut TestAppContext) {
2591 let mut cx = VimTestContext::new(cx, true).await;
2592
2593 cx.workspace(|workspace, _, cx| {
2594 assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
2595 });
2596
2597 cx.simulate_keystrokes(": w space other.rs");
2598 cx.simulate_keystrokes("enter");
2599
2600 cx.workspace(|workspace, _, cx| {
2601 assert_active_item(workspace, path!("/root/other.rs"), "", cx);
2602 });
2603
2604 cx.simulate_keystrokes(": w space dir/file.rs");
2605 cx.simulate_keystrokes("enter");
2606
2607 cx.simulate_prompt_answer("Replace");
2608 cx.run_until_parked();
2609
2610 cx.workspace(|workspace, _, cx| {
2611 assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx);
2612 });
2613
2614 cx.simulate_keystrokes(": w ! space other.rs");
2615 cx.simulate_keystrokes("enter");
2616
2617 cx.workspace(|workspace, _, cx| {
2618 assert_active_item(workspace, path!("/root/other.rs"), "", cx);
2619 });
2620 }
2621
2622 #[gpui::test]
2623 async fn test_command_matching_lines(cx: &mut TestAppContext) {
2624 let mut cx = NeovimBackedTestContext::new(cx).await;
2625
2626 cx.set_shared_state(indoc! {"
2627 ˇa
2628 b
2629 a
2630 b
2631 a
2632 "})
2633 .await;
2634
2635 cx.simulate_shared_keystrokes(":").await;
2636 cx.simulate_shared_keystrokes("g / a / d").await;
2637 cx.simulate_shared_keystrokes("enter").await;
2638
2639 cx.shared_state().await.assert_eq(indoc! {"
2640 b
2641 b
2642 ˇ"});
2643
2644 cx.simulate_shared_keystrokes("u").await;
2645
2646 cx.shared_state().await.assert_eq(indoc! {"
2647 ˇa
2648 b
2649 a
2650 b
2651 a
2652 "});
2653
2654 cx.simulate_shared_keystrokes(":").await;
2655 cx.simulate_shared_keystrokes("v / a / d").await;
2656 cx.simulate_shared_keystrokes("enter").await;
2657
2658 cx.shared_state().await.assert_eq(indoc! {"
2659 a
2660 a
2661 ˇa"});
2662 }
2663
2664 #[gpui::test]
2665 async fn test_del_marks(cx: &mut TestAppContext) {
2666 let mut cx = NeovimBackedTestContext::new(cx).await;
2667
2668 cx.set_shared_state(indoc! {"
2669 ˇa
2670 b
2671 a
2672 b
2673 a
2674 "})
2675 .await;
2676
2677 cx.simulate_shared_keystrokes("m a").await;
2678
2679 let mark = cx.update_editor(|editor, window, cx| {
2680 let vim = editor.addon::<VimAddon>().unwrap().entity.clone();
2681 vim.update(cx, |vim, cx| vim.get_mark("a", editor, window, cx))
2682 });
2683 assert!(mark.is_some());
2684
2685 cx.simulate_shared_keystrokes(": d e l m space a").await;
2686 cx.simulate_shared_keystrokes("enter").await;
2687
2688 let mark = cx.update_editor(|editor, window, cx| {
2689 let vim = editor.addon::<VimAddon>().unwrap().entity.clone();
2690 vim.update(cx, |vim, cx| vim.get_mark("a", editor, window, cx))
2691 });
2692 assert!(mark.is_none())
2693 }
2694
2695 #[gpui::test]
2696 async fn test_normal_command(cx: &mut TestAppContext) {
2697 let mut cx = NeovimBackedTestContext::new(cx).await;
2698
2699 cx.set_shared_state(indoc! {"
2700 The quick
2701 brown« fox
2702 jumpsˇ» over
2703 the lazy dog
2704 "})
2705 .await;
2706
2707 cx.simulate_shared_keystrokes(": n o r m space w C w o r d")
2708 .await;
2709 cx.simulate_shared_keystrokes("enter").await;
2710
2711 cx.shared_state().await.assert_eq(indoc! {"
2712 The quick
2713 brown word
2714 jumps worˇd
2715 the lazy dog
2716 "});
2717
2718 cx.simulate_shared_keystrokes(": n o r m space _ w c i w t e s t")
2719 .await;
2720 cx.simulate_shared_keystrokes("enter").await;
2721
2722 cx.shared_state().await.assert_eq(indoc! {"
2723 The quick
2724 brown word
2725 jumps tesˇt
2726 the lazy dog
2727 "});
2728
2729 cx.simulate_shared_keystrokes("_ l v l : n o r m space s l a")
2730 .await;
2731 cx.simulate_shared_keystrokes("enter").await;
2732
2733 cx.shared_state().await.assert_eq(indoc! {"
2734 The quick
2735 brown word
2736 lˇaumps test
2737 the lazy dog
2738 "});
2739
2740 cx.set_shared_state(indoc! {"
2741 ˇThe quick
2742 brown fox
2743 jumps over
2744 the lazy dog
2745 "})
2746 .await;
2747
2748 cx.simulate_shared_keystrokes("c i w M y escape").await;
2749
2750 cx.shared_state().await.assert_eq(indoc! {"
2751 Mˇy quick
2752 brown fox
2753 jumps over
2754 the lazy dog
2755 "});
2756
2757 cx.simulate_shared_keystrokes(": n o r m space u").await;
2758 cx.simulate_shared_keystrokes("enter").await;
2759
2760 cx.shared_state().await.assert_eq(indoc! {"
2761 ˇThe quick
2762 brown fox
2763 jumps over
2764 the lazy dog
2765 "});
2766 // Once ctrl-v to input character literals is added there should be a test for redo
2767 }
2768
2769 #[gpui::test]
2770 async fn test_command_tabnew(cx: &mut TestAppContext) {
2771 let mut cx = VimTestContext::new(cx, true).await;
2772
2773 // Create a new file to ensure that, when the filename is used with
2774 // `:tabnew`, it opens the existing file in a new tab.
2775 let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
2776 fs.as_fake()
2777 .insert_file(path!("/root/dir/file_2.rs"), "file_2".as_bytes().to_vec())
2778 .await;
2779
2780 cx.simulate_keystrokes(": tabnew");
2781 cx.simulate_keystrokes("enter");
2782 cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
2783
2784 // Assert that the new tab is empty and not associated with any file, as
2785 // no file path was provided to the `:tabnew` command.
2786 cx.workspace(|workspace, _window, cx| {
2787 let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();
2788 let buffer = active_editor
2789 .read(cx)
2790 .buffer()
2791 .read(cx)
2792 .as_singleton()
2793 .unwrap();
2794
2795 assert!(&buffer.read(cx).file().is_none());
2796 });
2797
2798 // Leverage the filename as an argument to the `:tabnew` command,
2799 // ensuring that the file, instead of an empty buffer, is opened in a
2800 // new tab.
2801 cx.simulate_keystrokes(": tabnew space dir/file_2.rs");
2802 cx.simulate_keystrokes("enter");
2803
2804 cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 3));
2805 cx.workspace(|workspace, _, cx| {
2806 assert_active_item(workspace, path!("/root/dir/file_2.rs"), "file_2", cx);
2807 });
2808
2809 // If the `filename` argument provided to the `:tabnew` command is for a
2810 // file that doesn't yet exist, it should still associate the buffer
2811 // with that file path, so that when the buffer contents are saved, the
2812 // file is created.
2813 cx.simulate_keystrokes(": tabnew space dir/file_3.rs");
2814 cx.simulate_keystrokes("enter");
2815
2816 cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 4));
2817 cx.workspace(|workspace, _, cx| {
2818 assert_active_item(workspace, path!("/root/dir/file_3.rs"), "", cx);
2819 });
2820 }
2821
2822 #[gpui::test]
2823 async fn test_command_tabedit(cx: &mut TestAppContext) {
2824 let mut cx = VimTestContext::new(cx, true).await;
2825
2826 // Create a new file to ensure that, when the filename is used with
2827 // `:tabedit`, it opens the existing file in a new tab.
2828 let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
2829 fs.as_fake()
2830 .insert_file(path!("/root/dir/file_2.rs"), "file_2".as_bytes().to_vec())
2831 .await;
2832
2833 cx.simulate_keystrokes(": tabedit");
2834 cx.simulate_keystrokes("enter");
2835 cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
2836
2837 // Assert that the new tab is empty and not associated with any file, as
2838 // no file path was provided to the `:tabedit` command.
2839 cx.workspace(|workspace, _window, cx| {
2840 let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();
2841 let buffer = active_editor
2842 .read(cx)
2843 .buffer()
2844 .read(cx)
2845 .as_singleton()
2846 .unwrap();
2847
2848 assert!(&buffer.read(cx).file().is_none());
2849 });
2850
2851 // Leverage the filename as an argument to the `:tabedit` command,
2852 // ensuring that the file, instead of an empty buffer, is opened in a
2853 // new tab.
2854 cx.simulate_keystrokes(": tabedit space dir/file_2.rs");
2855 cx.simulate_keystrokes("enter");
2856
2857 cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 3));
2858 cx.workspace(|workspace, _, cx| {
2859 assert_active_item(workspace, path!("/root/dir/file_2.rs"), "file_2", cx);
2860 });
2861
2862 // If the `filename` argument provided to the `:tabedit` command is for a
2863 // file that doesn't yet exist, it should still associate the buffer
2864 // with that file path, so that when the buffer contents are saved, the
2865 // file is created.
2866 cx.simulate_keystrokes(": tabedit space dir/file_3.rs");
2867 cx.simulate_keystrokes("enter");
2868
2869 cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 4));
2870 cx.workspace(|workspace, _, cx| {
2871 assert_active_item(workspace, path!("/root/dir/file_3.rs"), "", cx);
2872 });
2873 }
2874
2875 #[gpui::test]
2876 async fn test_ignorecase_command(cx: &mut TestAppContext) {
2877 let mut cx = VimTestContext::new(cx, true).await;
2878 cx.read(|cx| {
2879 assert_eq!(
2880 EditorSettings::get_global(cx).search.case_sensitive,
2881 false,
2882 "The `case_sensitive` setting should be `false` by default."
2883 );
2884 });
2885 cx.simulate_keystrokes(": set space noignorecase");
2886 cx.simulate_keystrokes("enter");
2887 cx.read(|cx| {
2888 assert_eq!(
2889 EditorSettings::get_global(cx).search.case_sensitive,
2890 true,
2891 "The `case_sensitive` setting should have been enabled with `:set noignorecase`."
2892 );
2893 });
2894 cx.simulate_keystrokes(": set space ignorecase");
2895 cx.simulate_keystrokes("enter");
2896 cx.read(|cx| {
2897 assert_eq!(
2898 EditorSettings::get_global(cx).search.case_sensitive,
2899 false,
2900 "The `case_sensitive` setting should have been disabled with `:set ignorecase`."
2901 );
2902 });
2903 cx.simulate_keystrokes(": set space noic");
2904 cx.simulate_keystrokes("enter");
2905 cx.read(|cx| {
2906 assert_eq!(
2907 EditorSettings::get_global(cx).search.case_sensitive,
2908 true,
2909 "The `case_sensitive` setting should have been enabled with `:set noic`."
2910 );
2911 });
2912 cx.simulate_keystrokes(": set space ic");
2913 cx.simulate_keystrokes("enter");
2914 cx.read(|cx| {
2915 assert_eq!(
2916 EditorSettings::get_global(cx).search.case_sensitive,
2917 false,
2918 "The `case_sensitive` setting should have been disabled with `:set ic`."
2919 );
2920 });
2921 }
2922
2923 #[gpui::test]
2924 async fn test_sort_commands(cx: &mut TestAppContext) {
2925 let mut cx = VimTestContext::new(cx, true).await;
2926
2927 cx.set_state(
2928 indoc! {"
2929 «hornet
2930 quirrel
2931 elderbug
2932 cornifer
2933 idaˇ»
2934 "},
2935 Mode::Visual,
2936 );
2937
2938 cx.simulate_keystrokes(": sort");
2939 cx.simulate_keystrokes("enter");
2940
2941 cx.assert_state(
2942 indoc! {"
2943 ˇcornifer
2944 elderbug
2945 hornet
2946 ida
2947 quirrel
2948 "},
2949 Mode::Normal,
2950 );
2951
2952 // Assert that, by default, `:sort` takes case into consideration.
2953 cx.set_state(
2954 indoc! {"
2955 «hornet
2956 quirrel
2957 Elderbug
2958 cornifer
2959 idaˇ»
2960 "},
2961 Mode::Visual,
2962 );
2963
2964 cx.simulate_keystrokes(": sort");
2965 cx.simulate_keystrokes("enter");
2966
2967 cx.assert_state(
2968 indoc! {"
2969 ˇElderbug
2970 cornifer
2971 hornet
2972 ida
2973 quirrel
2974 "},
2975 Mode::Normal,
2976 );
2977
2978 // Assert that, if the `i` option is passed, `:sort` ignores case.
2979 cx.set_state(
2980 indoc! {"
2981 «hornet
2982 quirrel
2983 Elderbug
2984 cornifer
2985 idaˇ»
2986 "},
2987 Mode::Visual,
2988 );
2989
2990 cx.simulate_keystrokes(": sort space i");
2991 cx.simulate_keystrokes("enter");
2992
2993 cx.assert_state(
2994 indoc! {"
2995 ˇcornifer
2996 Elderbug
2997 hornet
2998 ida
2999 quirrel
3000 "},
3001 Mode::Normal,
3002 );
3003
3004 // When no range is provided, sorts the whole buffer.
3005 cx.set_state(
3006 indoc! {"
3007 ˇhornet
3008 quirrel
3009 elderbug
3010 cornifer
3011 ida
3012 "},
3013 Mode::Normal,
3014 );
3015
3016 cx.simulate_keystrokes(": sort");
3017 cx.simulate_keystrokes("enter");
3018
3019 cx.assert_state(
3020 indoc! {"
3021 ˇcornifer
3022 elderbug
3023 hornet
3024 ida
3025 quirrel
3026 "},
3027 Mode::Normal,
3028 );
3029 }
3030}