helix: Allow yank without a selection (#35612)
Related https://github.com/zed-industries/zed/issues/4642 Release Notes: - Helix: without active selection, pressing `y` in helix mode will yank a single character under cursor. --------- Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
This commit is contained in:
parent
528d56e807
commit
20be133713
3 changed files with 100 additions and 1 deletions
|
@ -390,7 +390,7 @@
|
||||||
"right": "vim::WrappingRight",
|
"right": "vim::WrappingRight",
|
||||||
"h": "vim::WrappingLeft",
|
"h": "vim::WrappingLeft",
|
||||||
"l": "vim::WrappingRight",
|
"l": "vim::WrappingRight",
|
||||||
"y": "editor::Copy",
|
"y": "vim::HelixYank",
|
||||||
"alt-;": "vim::OtherEnd",
|
"alt-;": "vim::OtherEnd",
|
||||||
"ctrl-r": "vim::Redo",
|
"ctrl-r": "vim::Redo",
|
||||||
"f": ["vim::PushFindForward", { "before": false, "multiline": true }],
|
"f": ["vim::PushFindForward", { "before": false, "multiline": true }],
|
||||||
|
|
|
@ -15,6 +15,8 @@ actions!(
|
||||||
[
|
[
|
||||||
/// Switches to normal mode after the cursor (Helix-style).
|
/// Switches to normal mode after the cursor (Helix-style).
|
||||||
HelixNormalAfter,
|
HelixNormalAfter,
|
||||||
|
/// Yanks the current selection or character if no selection.
|
||||||
|
HelixYank,
|
||||||
/// Inserts at the beginning of the selection.
|
/// Inserts at the beginning of the selection.
|
||||||
HelixInsert,
|
HelixInsert,
|
||||||
/// Appends at the end of the selection.
|
/// Appends at the end of the selection.
|
||||||
|
@ -26,6 +28,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
|
||||||
Vim::action(editor, cx, Vim::helix_normal_after);
|
Vim::action(editor, cx, Vim::helix_normal_after);
|
||||||
Vim::action(editor, cx, Vim::helix_insert);
|
Vim::action(editor, cx, Vim::helix_insert);
|
||||||
Vim::action(editor, cx, Vim::helix_append);
|
Vim::action(editor, cx, Vim::helix_append);
|
||||||
|
Vim::action(editor, cx, Vim::helix_yank);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Vim {
|
impl Vim {
|
||||||
|
@ -310,6 +313,47 @@ impl Vim {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn helix_yank(&mut self, _: &HelixYank, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.update_editor(cx, |vim, editor, cx| {
|
||||||
|
let has_selection = editor
|
||||||
|
.selections
|
||||||
|
.all_adjusted(cx)
|
||||||
|
.iter()
|
||||||
|
.any(|selection| !selection.is_empty());
|
||||||
|
|
||||||
|
if !has_selection {
|
||||||
|
// If no selection, expand to current character (like 'v' does)
|
||||||
|
editor.change_selections(Default::default(), window, cx, |s| {
|
||||||
|
s.move_with(|map, selection| {
|
||||||
|
let head = selection.head();
|
||||||
|
let new_head = movement::saturating_right(map, head);
|
||||||
|
selection.set_tail(head, SelectionGoal::None);
|
||||||
|
selection.set_head(new_head, SelectionGoal::None);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
vim.yank_selections_content(
|
||||||
|
editor,
|
||||||
|
crate::motion::MotionKind::Exclusive,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
editor.change_selections(Default::default(), window, cx, |s| {
|
||||||
|
s.move_with(|_map, selection| {
|
||||||
|
selection.collapse_to(selection.start, SelectionGoal::None);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Yank the selection(s)
|
||||||
|
vim.yank_selections_content(
|
||||||
|
editor,
|
||||||
|
crate::motion::MotionKind::Exclusive,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn helix_insert(&mut self, _: &HelixInsert, window: &mut Window, cx: &mut Context<Self>) {
|
fn helix_insert(&mut self, _: &HelixInsert, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.start_recording(cx);
|
self.start_recording(cx);
|
||||||
self.update_editor(cx, |_, editor, cx| {
|
self.update_editor(cx, |_, editor, cx| {
|
||||||
|
@ -703,4 +747,29 @@ mod test {
|
||||||
|
|
||||||
cx.assert_state("«xxˇ»", Mode::HelixNormal);
|
cx.assert_state("«xxˇ»", Mode::HelixNormal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_helix_yank(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = VimTestContext::new(cx, true).await;
|
||||||
|
cx.enable_helix();
|
||||||
|
|
||||||
|
// Test yanking current character with no selection
|
||||||
|
cx.set_state("hello ˇworld", Mode::HelixNormal);
|
||||||
|
cx.simulate_keystrokes("y");
|
||||||
|
|
||||||
|
// Test cursor remains at the same position after yanking single character
|
||||||
|
cx.assert_state("hello ˇworld", Mode::HelixNormal);
|
||||||
|
cx.shared_clipboard().assert_eq("w");
|
||||||
|
|
||||||
|
// Move cursor and yank another character
|
||||||
|
cx.simulate_keystrokes("l");
|
||||||
|
cx.simulate_keystrokes("y");
|
||||||
|
cx.shared_clipboard().assert_eq("o");
|
||||||
|
|
||||||
|
// Test yanking with existing selection
|
||||||
|
cx.set_state("hello «worlˇ»d", Mode::HelixNormal);
|
||||||
|
cx.simulate_keystrokes("y");
|
||||||
|
cx.shared_clipboard().assert_eq("worl");
|
||||||
|
cx.assert_state("hello «worlˇ»d", Mode::HelixNormal);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -143,6 +143,16 @@ impl VimTestContext {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn enable_helix(&mut self) {
|
||||||
|
self.cx.update(|_, cx| {
|
||||||
|
SettingsStore::update_global(cx, |store, cx| {
|
||||||
|
store.update_user_settings::<vim_mode_setting::HelixModeSetting>(cx, |s| {
|
||||||
|
*s = Some(true)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn mode(&mut self) -> Mode {
|
pub fn mode(&mut self) -> Mode {
|
||||||
self.update_editor(|editor, _, cx| editor.addon::<VimAddon>().unwrap().entity.read(cx).mode)
|
self.update_editor(|editor, _, cx| editor.addon::<VimAddon>().unwrap().entity.read(cx).mode)
|
||||||
}
|
}
|
||||||
|
@ -210,6 +220,26 @@ impl VimTestContext {
|
||||||
assert_eq!(self.mode(), Mode::Normal, "{}", self.assertion_context());
|
assert_eq!(self.mode(), Mode::Normal, "{}", self.assertion_context());
|
||||||
assert_eq!(self.active_operator(), None, "{}", self.assertion_context());
|
assert_eq!(self.active_operator(), None, "{}", self.assertion_context());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn shared_clipboard(&mut self) -> VimClipboard {
|
||||||
|
VimClipboard {
|
||||||
|
editor: self
|
||||||
|
.read_from_clipboard()
|
||||||
|
.map(|item| item.text().unwrap().to_string())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct VimClipboard {
|
||||||
|
editor: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VimClipboard {
|
||||||
|
#[track_caller]
|
||||||
|
pub fn assert_eq(&self, expected: &str) {
|
||||||
|
assert_eq!(self.editor, expected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for VimTestContext {
|
impl Deref for VimTestContext {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue