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