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