mirror of
https://github.com/zed-industries/zed.git
synced 2026-08-11 10:04:46 +00:00
vim: Add Helix _ action to trim whitespace from selections (#55253)
Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [X] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Closes #53517 Release Notes: - Added Helix trim whitespace from selections action on `_`. --------- Co-authored-by: Tom Houlé <tom@tomhoule.com>
This commit is contained in:
parent
b2131e9df8
commit
b6ebe0ff35
2 changed files with 134 additions and 0 deletions
|
|
@ -487,6 +487,7 @@
|
|||
"x": "vim::HelixSelectLine",
|
||||
"*": "buffer_search::UseSelectionForFind",
|
||||
"shift-x": "editor::SelectLine",
|
||||
"_": "vim::HelixTrimSelections",
|
||||
"ctrl-c": "editor::ToggleComments",
|
||||
"alt-o": "editor::SelectLargerSyntaxNode",
|
||||
"alt-i": "editor::SelectSmallerSyntaxNode",
|
||||
|
|
|
|||
|
|
@ -63,6 +63,9 @@ actions!(
|
|||
HelixSelectNext,
|
||||
/// Select the previous match for the current search query.
|
||||
HelixSelectPrevious,
|
||||
/// Trim leading and trailing whitespace from each selection.
|
||||
/// Originally-empty selections (cursors) are dropped before trimming.
|
||||
HelixTrimSelections,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
@ -89,6 +92,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
|
|||
Vim::action(editor, cx, Vim::helix_jump_to_word);
|
||||
Vim::action(editor, cx, Vim::helix_select_next);
|
||||
Vim::action(editor, cx, Vim::helix_select_previous);
|
||||
Vim::action(editor, cx, Vim::helix_trim_selections);
|
||||
Vim::action(editor, cx, |vim, _: &PushHelixSurroundAdd, window, cx| {
|
||||
vim.clear_operator(window, cx);
|
||||
vim.push_operator(Operator::HelixSurroundAdd, window, cx);
|
||||
|
|
@ -912,6 +916,94 @@ impl Vim {
|
|||
});
|
||||
}
|
||||
|
||||
fn helix_trim_selections(
|
||||
&mut self,
|
||||
_: &HelixTrimSelections,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
Vim::take_count(cx);
|
||||
Vim::take_forced_motion(cx);
|
||||
self.update_editor(cx, |_, editor, cx| {
|
||||
let display_snapshot = editor.display_snapshot(cx);
|
||||
let buffer = display_snapshot.buffer_snapshot();
|
||||
let selections = editor
|
||||
.selections
|
||||
.all::<MultiBufferOffset>(&display_snapshot);
|
||||
let mut trimmed = Vec::new();
|
||||
|
||||
for selection in &selections {
|
||||
if selection.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let all_whitespace = buffer
|
||||
.text_for_range(selection.start..selection.end)
|
||||
.flat_map(|chunk| chunk.chars())
|
||||
.all(|ch| ch.is_whitespace());
|
||||
if all_whitespace {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut new_start = selection.start;
|
||||
for ch in buffer.chars_at(selection.start) {
|
||||
if !ch.is_whitespace() {
|
||||
break;
|
||||
}
|
||||
new_start += ch.len_utf8();
|
||||
}
|
||||
|
||||
let mut new_end = selection.end;
|
||||
for ch in buffer.reversed_chars_at(selection.end) {
|
||||
if !ch.is_whitespace() {
|
||||
break;
|
||||
}
|
||||
new_end -= ch.len_utf8();
|
||||
}
|
||||
|
||||
let mut new_selection = selection.clone();
|
||||
new_selection.start = new_start;
|
||||
new_selection.end = new_end;
|
||||
trimmed.push(new_selection);
|
||||
}
|
||||
|
||||
if !trimmed.is_empty() {
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
s.select(trimmed);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// All selections were empty or entirely whitespace. Match Helix's
|
||||
// fallback: `collapse_selection` followed by `keep_primary_selection`.
|
||||
// Take the newest selection and collapse it to a one-char cursor at
|
||||
// the head (head - 1 for a forward, non-empty selection; head as-is
|
||||
// otherwise).
|
||||
let newest = editor
|
||||
.selections
|
||||
.newest::<MultiBufferOffset>(&display_snapshot);
|
||||
let head = if newest.reversed {
|
||||
newest.start
|
||||
} else {
|
||||
newest.end
|
||||
};
|
||||
let cursor_offset = if !newest.reversed && !newest.is_empty() {
|
||||
let mut p = head;
|
||||
if let Some(ch) = buffer.reversed_chars_at(head).next() {
|
||||
p -= ch.len_utf8();
|
||||
}
|
||||
p
|
||||
} else {
|
||||
head
|
||||
};
|
||||
let mut collapsed = newest;
|
||||
collapsed.start = cursor_offset;
|
||||
collapsed.end = cursor_offset;
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
s.select(vec![collapsed]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn do_helix_substitute(&mut self, yank: bool, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.update_editor(cx, |vim, editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
|
|
@ -4472,4 +4564,45 @@ mod test {
|
|||
|
||||
cx.assert_state("const after = 2; console.log(afterˇ)", Mode::HelixNormal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_helix_trim_selections(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
|
||||
// Canonical case: a line selection (as produced by `x`) including its
|
||||
// trailing newline. `_` strips the leading indent and trailing newline.
|
||||
cx.set_state("« indented line\nˇ»next line\n", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("_");
|
||||
cx.assert_state(" «indented lineˇ»\nnext line\n", Mode::HelixNormal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_helix_trim_selections_all_whitespace(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
|
||||
// An entirely-whitespace selection is filtered out before trimming.
|
||||
// When every selection filters out, Helix falls back to
|
||||
// `collapse_selection` + `keep_primary_selection`, which collapses the
|
||||
// primary selection to a one-char cursor at head - 1 (the last
|
||||
// whitespace char of the original selection).
|
||||
cx.set_state("aa« ˇ»next\n", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("_");
|
||||
cx.assert_state("aa ˇ next\n", Mode::HelixNormal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_helix_trim_selections_consumes_count(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
|
||||
// A count is meaningless for `_`, but it must not leak into the next
|
||||
// command.
|
||||
cx.set_state("« aa ˇ»\nbbb\nccc\n", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("2 _");
|
||||
cx.assert_state(" «aaˇ» \nbbb\nccc\n", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("x");
|
||||
cx.assert_state("« aa \nˇ»bbb\nccc\n", Mode::HelixNormal);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue