Add :delm[arks] {marks} command to delete vim marks (#31140)

Release Notes:

- Implements `:delm[arks] {marks}` specified
[here](https://vimhelp.org/motion.txt.html#%3Adelmarks)
- Adds `ArgumentRequired` action for vim commands that require arguments

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
This commit is contained in:
AidanV 2025-06-02 12:18:28 -07:00 committed by GitHub
parent 864767ad35
commit 9d5fb3c3f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 258 additions and 4 deletions

View file

@ -1,5 +1,5 @@
use anyhow::Result;
use collections::HashMap;
use collections::{HashMap, HashSet};
use command_palette_hooks::CommandInterceptResult;
use editor::{
Bias, Editor, ToPoint,
@ -166,12 +166,21 @@ struct VimSave {
pub filename: String,
}
#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
enum DeleteMarks {
Marks(String),
AllLocal,
}
actions!(
vim,
[VisualCommand, CountCommand, ShellCommand, ArgumentRequired]
);
#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
struct VimEdit {
pub filename: String,
}
actions!(vim, [VisualCommand, CountCommand, ShellCommand]);
impl_internal_actions!(
vim,
[
@ -183,6 +192,7 @@ impl_internal_actions!(
ShellExec,
VimSet,
VimSave,
DeleteMarks,
VimEdit,
]
);
@ -245,6 +255,25 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
})
});
Vim::action(editor, cx, |_, _: &ArgumentRequired, window, cx| {
let _ = window.prompt(
gpui::PromptLevel::Critical,
"Argument required",
None,
&["Cancel"],
cx,
);
});
Vim::action(editor, cx, |vim, _: &ShellCommand, window, cx| {
let Some(workspace) = vim.workspace(window) else {
return;
};
workspace.update(cx, |workspace, cx| {
command_palette::CommandPalette::toggle(workspace, "'<,'>!", window, cx);
})
});
Vim::action(editor, cx, |vim, action: &VimSave, window, cx| {
vim.update_editor(window, cx, |_, editor, window, cx| {
let Some(project) = editor.project.clone() else {
@ -286,6 +315,72 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
});
});
Vim::action(editor, cx, |vim, action: &DeleteMarks, window, cx| {
fn err(s: String, window: &mut Window, cx: &mut Context<Editor>) {
let _ = window.prompt(
gpui::PromptLevel::Critical,
&format!("Invalid argument: {}", s),
None,
&["Cancel"],
cx,
);
}
vim.update_editor(window, cx, |vim, editor, window, cx| match action {
DeleteMarks::Marks(s) => {
if s.starts_with('-') || s.ends_with('-') || s.contains(['\'', '`']) {
err(s.clone(), window, cx);
return;
}
let to_delete = if s.len() < 3 {
Some(s.clone())
} else {
s.chars()
.tuple_windows::<(_, _, _)>()
.map(|(a, b, c)| {
if b == '-' {
if match a {
'a'..='z' => a <= c && c <= 'z',
'A'..='Z' => a <= c && c <= 'Z',
'0'..='9' => a <= c && c <= '9',
_ => false,
} {
Some((a..=c).collect_vec())
} else {
None
}
} else if a == '-' {
if c == '-' { None } else { Some(vec![c]) }
} else if c == '-' {
if a == '-' { None } else { Some(vec![a]) }
} else {
Some(vec![a, b, c])
}
})
.fold_options(HashSet::<char>::default(), |mut set, chars| {
set.extend(chars.iter().copied());
set
})
.map(|set| set.iter().collect::<String>())
};
let Some(to_delete) = to_delete else {
err(s.clone(), window, cx);
return;
};
for c in to_delete.chars().filter(|c| !c.is_whitespace()) {
vim.delete_mark(c.to_string(), editor, window, cx);
}
}
DeleteMarks::AllLocal => {
for s in 'a'..='z' {
vim.delete_mark(s.to_string(), editor, window, cx);
}
}
});
});
Vim::action(editor, cx, |vim, action: &VimEdit, window, cx| {
vim.update_editor(window, cx, |vim, editor, window, cx| {
let Some(workspace) = vim.workspace(window) else {
@ -982,6 +1077,9 @@ fn generate_commands(_: &App) -> Vec<VimCommand> {
}),
VimCommand::new(("reg", "isters"), ToggleRegistersView).bang(ToggleRegistersView),
VimCommand::new(("marks", ""), ToggleMarksView).bang(ToggleMarksView),
VimCommand::new(("delm", "arks"), ArgumentRequired)
.bang(DeleteMarks::AllLocal)
.args(|_, args| Some(DeleteMarks::Marks(args).boxed_clone())),
VimCommand::new(("sor", "t"), SortLinesCaseSensitive).range(select_range),
VimCommand::new(("sort i", ""), SortLinesCaseInsensitive).range(select_range),
VimCommand::str(("E", "xplore"), "project_panel::ToggleFocus"),
@ -1732,6 +1830,7 @@ mod test {
use std::path::Path;
use crate::{
VimAddon,
state::Mode,
test::{NeovimBackedTestContext, VimTestContext},
};
@ -2084,4 +2183,35 @@ mod test {
a
ˇa"});
}
#[gpui::test]
async fn test_del_marks(cx: &mut TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.set_shared_state(indoc! {"
ˇa
b
a
b
a
"})
.await;
cx.simulate_shared_keystrokes("m a").await;
let mark = cx.update_editor(|editor, window, cx| {
let vim = editor.addon::<VimAddon>().unwrap().entity.clone();
vim.update(cx, |vim, cx| vim.get_mark("a", editor, window, cx))
});
assert!(mark.is_some());
cx.simulate_shared_keystrokes(": d e l m space a").await;
cx.simulate_shared_keystrokes("enter").await;
let mark = cx.update_editor(|editor, window, cx| {
let vim = editor.addon::<VimAddon>().unwrap().entity.clone();
vim.update(cx, |vim, cx| vim.get_mark("a", editor, window, cx))
});
assert!(mark.is_none())
}
}