Skip to content

Commit 20be133

Browse files
helix: Allow yank without a selection (#35612)
Related #4642 Release Notes: - Helix: without active selection, pressing `y` in helix mode will yank a single character under cursor. --------- Co-authored-by: Conrad Irwin <[email protected]>
1 parent 528d56e commit 20be133

File tree

3 files changed

+100
-1
lines changed

3 files changed

+100
-1
lines changed

assets/keymaps/vim.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,7 @@
390390
"right": "vim::WrappingRight",
391391
"h": "vim::WrappingLeft",
392392
"l": "vim::WrappingRight",
393-
"y": "editor::Copy",
393+
"y": "vim::HelixYank",
394394
"alt-;": "vim::OtherEnd",
395395
"ctrl-r": "vim::Redo",
396396
"f": ["vim::PushFindForward", { "before": false, "multiline": true }],

crates/vim/src/helix.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ actions!(
1515
[
1616
/// Switches to normal mode after the cursor (Helix-style).
1717
HelixNormalAfter,
18+
/// Yanks the current selection or character if no selection.
19+
HelixYank,
1820
/// Inserts at the beginning of the selection.
1921
HelixInsert,
2022
/// Appends at the end of the selection.
@@ -26,6 +28,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
2628
Vim::action(editor, cx, Vim::helix_normal_after);
2729
Vim::action(editor, cx, Vim::helix_insert);
2830
Vim::action(editor, cx, Vim::helix_append);
31+
Vim::action(editor, cx, Vim::helix_yank);
2932
}
3033

3134
impl Vim {
@@ -310,6 +313,47 @@ impl Vim {
310313
}
311314
}
312315

316+
pub fn helix_yank(&mut self, _: &HelixYank, window: &mut Window, cx: &mut Context<Self>) {
317+
self.update_editor(cx, |vim, editor, cx| {
318+
let has_selection = editor
319+
.selections
320+
.all_adjusted(cx)
321+
.iter()
322+
.any(|selection| !selection.is_empty());
323+
324+
if !has_selection {
325+
// If no selection, expand to current character (like 'v' does)
326+
editor.change_selections(Default::default(), window, cx, |s| {
327+
s.move_with(|map, selection| {
328+
let head = selection.head();
329+
let new_head = movement::saturating_right(map, head);
330+
selection.set_tail(head, SelectionGoal::None);
331+
selection.set_head(new_head, SelectionGoal::None);
332+
});
333+
});
334+
vim.yank_selections_content(
335+
editor,
336+
crate::motion::MotionKind::Exclusive,
337+
window,
338+
cx,
339+
);
340+
editor.change_selections(Default::default(), window, cx, |s| {
341+
s.move_with(|_map, selection| {
342+
selection.collapse_to(selection.start, SelectionGoal::None);
343+
});
344+
});
345+
} else {
346+
// Yank the selection(s)
347+
vim.yank_selections_content(
348+
editor,
349+
crate::motion::MotionKind::Exclusive,
350+
window,
351+
cx,
352+
);
353+
}
354+
});
355+
}
356+
313357
fn helix_insert(&mut self, _: &HelixInsert, window: &mut Window, cx: &mut Context<Self>) {
314358
self.start_recording(cx);
315359
self.update_editor(cx, |_, editor, cx| {
@@ -703,4 +747,29 @@ mod test {
703747

704748
cx.assert_state("«xxˇ»", Mode::HelixNormal);
705749
}
750+
751+
#[gpui::test]
752+
async fn test_helix_yank(cx: &mut gpui::TestAppContext) {
753+
let mut cx = VimTestContext::new(cx, true).await;
754+
cx.enable_helix();
755+
756+
// Test yanking current character with no selection
757+
cx.set_state("hello ˇworld", Mode::HelixNormal);
758+
cx.simulate_keystrokes("y");
759+
760+
// Test cursor remains at the same position after yanking single character
761+
cx.assert_state("hello ˇworld", Mode::HelixNormal);
762+
cx.shared_clipboard().assert_eq("w");
763+
764+
// Move cursor and yank another character
765+
cx.simulate_keystrokes("l");
766+
cx.simulate_keystrokes("y");
767+
cx.shared_clipboard().assert_eq("o");
768+
769+
// Test yanking with existing selection
770+
cx.set_state("hello «worlˇ»d", Mode::HelixNormal);
771+
cx.simulate_keystrokes("y");
772+
cx.shared_clipboard().assert_eq("worl");
773+
cx.assert_state("hello «worlˇ»d", Mode::HelixNormal);
774+
}
706775
}

crates/vim/src/test/vim_test_context.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,16 @@ impl VimTestContext {
143143
})
144144
}
145145

146+
pub fn enable_helix(&mut self) {
147+
self.cx.update(|_, cx| {
148+
SettingsStore::update_global(cx, |store, cx| {
149+
store.update_user_settings::<vim_mode_setting::HelixModeSetting>(cx, |s| {
150+
*s = Some(true)
151+
});
152+
});
153+
})
154+
}
155+
146156
pub fn mode(&mut self) -> Mode {
147157
self.update_editor(|editor, _, cx| editor.addon::<VimAddon>().unwrap().entity.read(cx).mode)
148158
}
@@ -210,6 +220,26 @@ impl VimTestContext {
210220
assert_eq!(self.mode(), Mode::Normal, "{}", self.assertion_context());
211221
assert_eq!(self.active_operator(), None, "{}", self.assertion_context());
212222
}
223+
224+
pub fn shared_clipboard(&mut self) -> VimClipboard {
225+
VimClipboard {
226+
editor: self
227+
.read_from_clipboard()
228+
.map(|item| item.text().unwrap().to_string())
229+
.unwrap_or_default(),
230+
}
231+
}
232+
}
233+
234+
pub struct VimClipboard {
235+
editor: String,
236+
}
237+
238+
impl VimClipboard {
239+
#[track_caller]
240+
pub fn assert_eq(&self, expected: &str) {
241+
assert_eq!(self.editor, expected);
242+
}
213243
}
214244

215245
impl Deref for VimTestContext {

0 commit comments

Comments
 (0)