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