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