helix: Change selection cloning (#38090)
Closes #33637 Closes #37332 and solves part of https://github.com/zed-industries/zed/discussions/33580#discussioncomment-14195506 This improves the "C" and "alt-C" actions to work like helix. It also adds "," which removes all but the newest cursors. In helix the one that's left would be the primary selection, but I don't think that has an equivalent yet, so this simulates what would be the primary selection if it was never cycled with "(" ")". Release Notes: - Improved multicursor creation and deletion in helix mode --------- Co-authored-by: Jakub Konka <kubkon@jakubkonka.com>
This commit is contained in:
@@ -498,8 +498,9 @@
|
||||
"ctrl-c": "editor::ToggleComments",
|
||||
"d": "vim::HelixDelete",
|
||||
"c": "vim::Substitute",
|
||||
"shift-c": ["editor::AddSelectionBelow", { "skip_soft_wrap": true }],
|
||||
"alt-shift-c": ["editor::AddSelectionAbove", { "skip_soft_wrap": true }]
|
||||
"shift-c": "vim::HelixDuplicateBelow",
|
||||
"alt-shift-c": "vim::HelixDuplicateAbove",
|
||||
",": "vim::HelixKeepNewestSelection"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
mod boundary;
|
||||
mod duplicate;
|
||||
mod object;
|
||||
mod paste;
|
||||
mod select;
|
||||
@@ -40,6 +41,13 @@ actions!(
|
||||
HelixSelectLine,
|
||||
/// Select all matches of a given pattern within the current selection.
|
||||
HelixSelectRegex,
|
||||
/// Removes all but the one selection that was created last.
|
||||
/// `Newest` can eventually be `Primary`.
|
||||
HelixKeepNewestSelection,
|
||||
/// Copies all selections below.
|
||||
HelixDuplicateBelow,
|
||||
/// Copies all selections above.
|
||||
HelixDuplicateAbove,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -51,6 +59,15 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
|
||||
Vim::action(editor, cx, Vim::helix_goto_last_modification);
|
||||
Vim::action(editor, cx, Vim::helix_paste);
|
||||
Vim::action(editor, cx, Vim::helix_select_regex);
|
||||
Vim::action(editor, cx, Vim::helix_keep_newest_selection);
|
||||
Vim::action(editor, cx, |vim, _: &HelixDuplicateBelow, window, cx| {
|
||||
let times = Vim::take_count(cx);
|
||||
vim.helix_duplicate_selections_below(times, window, cx);
|
||||
});
|
||||
Vim::action(editor, cx, |vim, _: &HelixDuplicateAbove, window, cx| {
|
||||
let times = Vim::take_count(cx);
|
||||
vim.helix_duplicate_selections_above(times, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
impl Vim {
|
||||
@@ -575,6 +592,18 @@ impl Vim {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn helix_keep_newest_selection(
|
||||
&mut self,
|
||||
_: &HelixKeepNewestSelection,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.update_editor(cx, |_, editor, cx| {
|
||||
let newest = editor.selections.newest::<usize>(cx);
|
||||
editor.change_selections(Default::default(), window, cx, |s| s.select(vec![newest]));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use editor::{DisplayPoint, display_map::DisplaySnapshot};
|
||||
use gpui::Context;
|
||||
use text::Bias;
|
||||
use ui::Window;
|
||||
|
||||
use crate::Vim;
|
||||
|
||||
impl Vim {
|
||||
/// Creates a duplicate of every selection below it in the first place that has both its start
|
||||
/// and end
|
||||
pub(super) fn helix_duplicate_selections_below(
|
||||
&mut self,
|
||||
times: Option<usize>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.duplicate_selections(
|
||||
times,
|
||||
window,
|
||||
cx,
|
||||
|prev_point| *prev_point.row_mut() += 1,
|
||||
|prev_range, map| prev_range.end.row() >= map.max_point().row(),
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
/// Creates a duplicate of every selection above it in the first place that has both its start
|
||||
/// and end
|
||||
pub(super) fn helix_duplicate_selections_above(
|
||||
&mut self,
|
||||
times: Option<usize>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.duplicate_selections(
|
||||
times,
|
||||
window,
|
||||
cx,
|
||||
|prev_point| *prev_point.row_mut() = prev_point.row().0.saturating_sub(1),
|
||||
|prev_range, _| prev_range.start.row() == DisplayPoint::zero().row(),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
fn duplicate_selections(
|
||||
&mut self,
|
||||
times: Option<usize>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
advance_search: impl Fn(&mut DisplayPoint),
|
||||
end_search: impl Fn(&Range<DisplayPoint>, &DisplaySnapshot) -> bool,
|
||||
above: bool,
|
||||
) {
|
||||
let times = times.unwrap_or(1);
|
||||
self.update_editor(cx, |_, editor, cx| {
|
||||
let mut selections = Vec::new();
|
||||
let (map, mut original_selections) = editor.selections.all_display(cx);
|
||||
// The order matters, because it is recorded when the selections are added.
|
||||
if above {
|
||||
original_selections.reverse();
|
||||
}
|
||||
|
||||
for origin in original_selections {
|
||||
let origin = origin.tail()..origin.head();
|
||||
selections.push(display_point_range_to_offset_range(&origin, &map));
|
||||
let mut last_origin = origin;
|
||||
for _ in 1..=times {
|
||||
if let Some(duplicate) = find_next_valid_duplicate_space(
|
||||
last_origin.clone(),
|
||||
&map,
|
||||
&advance_search,
|
||||
&end_search,
|
||||
) {
|
||||
selections.push(display_point_range_to_offset_range(&duplicate, &map));
|
||||
last_origin = duplicate;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
s.select_ranges(selections);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn find_next_valid_duplicate_space(
|
||||
mut origin: Range<DisplayPoint>,
|
||||
map: &DisplaySnapshot,
|
||||
advance_search: &impl Fn(&mut DisplayPoint),
|
||||
end_search: &impl Fn(&Range<DisplayPoint>, &DisplaySnapshot) -> bool,
|
||||
) -> Option<Range<DisplayPoint>> {
|
||||
while !end_search(&origin, map) {
|
||||
advance_search(&mut origin.start);
|
||||
advance_search(&mut origin.end);
|
||||
|
||||
if map.clip_point(origin.start, Bias::Left) == origin.start
|
||||
&& map.clip_point(origin.end, Bias::Right) == origin.end
|
||||
{
|
||||
return Some(origin);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn display_point_range_to_offset_range(
|
||||
range: &Range<DisplayPoint>,
|
||||
map: &DisplaySnapshot,
|
||||
) -> Range<usize> {
|
||||
range.start.to_offset(map, Bias::Left)..range.end.to_offset(map, Bias::Right)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use db::indoc;
|
||||
|
||||
use crate::{state::Mode, test::VimTestContext};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_selection_duplication(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox «jumpsˇ»
|
||||
over the
|
||||
lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
cx.simulate_keystrokes("C");
|
||||
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox «jumpsˇ»
|
||||
over the
|
||||
lazy« dog.ˇ»"},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
cx.simulate_keystrokes("C");
|
||||
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox «jumpsˇ»
|
||||
over the
|
||||
lazy« dog.ˇ»"},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
cx.simulate_keystrokes("alt-C");
|
||||
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
The «quickˇ» brown
|
||||
fox «jumpsˇ»
|
||||
over the
|
||||
lazy« dog.ˇ»"},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
cx.simulate_keystrokes(",");
|
||||
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
The «quickˇ» brown
|
||||
fox jumps
|
||||
over the
|
||||
lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_selection_duplication_backwards(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
«ˇfox» jumps
|
||||
over the
|
||||
lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
cx.simulate_keystrokes("C C alt-C");
|
||||
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
«ˇThe» quick brown
|
||||
«ˇfox» jumps
|
||||
«ˇove»r the
|
||||
«ˇlaz»y dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_selection_duplication_count(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
The «qˇ»uick brown
|
||||
fox jumps
|
||||
over the
|
||||
lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
cx.simulate_keystrokes("9 C");
|
||||
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
The «qˇ»uick brown
|
||||
fox «jˇ»umps
|
||||
over« ˇ»the
|
||||
lazy« ˇ»dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user