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