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