Skip to content

Commit f0f0a38

Browse files
fantacellkubkon
authored andcommitted
helix: Change selection cloning (zed-industries#38090)
Closes zed-industries#33637 Closes zed-industries#37332 and solves part of zed-industries#33580 (comment) 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 <[email protected]>
1 parent 7521e2a commit f0f0a38

File tree

3 files changed

+265
-2
lines changed

3 files changed

+265
-2
lines changed

assets/keymaps/vim.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -498,8 +498,9 @@
498498
"ctrl-c": "editor::ToggleComments",
499499
"d": "vim::HelixDelete",
500500
"c": "vim::Substitute",
501-
"shift-c": ["editor::AddSelectionBelow", { "skip_soft_wrap": true }],
502-
"alt-shift-c": ["editor::AddSelectionAbove", { "skip_soft_wrap": true }]
501+
"shift-c": "vim::HelixDuplicateBelow",
502+
"alt-shift-c": "vim::HelixDuplicateAbove",
503+
",": "vim::HelixKeepNewestSelection"
503504
}
504505
},
505506
{

crates/vim/src/helix.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
mod boundary;
2+
mod duplicate;
23
mod object;
34
mod paste;
45
mod select;
@@ -40,6 +41,13 @@ actions!(
4041
HelixSelectLine,
4142
/// Select all matches of a given pattern within the current selection.
4243
HelixSelectRegex,
44+
/// Removes all but the one selection that was created last.
45+
/// `Newest` can eventually be `Primary`.
46+
HelixKeepNewestSelection,
47+
/// Copies all selections below.
48+
HelixDuplicateBelow,
49+
/// Copies all selections above.
50+
HelixDuplicateAbove,
4351
]
4452
);
4553

@@ -51,6 +59,15 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
5159
Vim::action(editor, cx, Vim::helix_goto_last_modification);
5260
Vim::action(editor, cx, Vim::helix_paste);
5361
Vim::action(editor, cx, Vim::helix_select_regex);
62+
Vim::action(editor, cx, Vim::helix_keep_newest_selection);
63+
Vim::action(editor, cx, |vim, _: &HelixDuplicateBelow, window, cx| {
64+
let times = Vim::take_count(cx);
65+
vim.helix_duplicate_selections_below(times, window, cx);
66+
});
67+
Vim::action(editor, cx, |vim, _: &HelixDuplicateAbove, window, cx| {
68+
let times = Vim::take_count(cx);
69+
vim.helix_duplicate_selections_above(times, window, cx);
70+
});
5471
}
5572

5673
impl Vim {
@@ -575,6 +592,18 @@ impl Vim {
575592
});
576593
});
577594
}
595+
596+
fn helix_keep_newest_selection(
597+
&mut self,
598+
_: &HelixKeepNewestSelection,
599+
window: &mut Window,
600+
cx: &mut Context<Self>,
601+
) {
602+
self.update_editor(cx, |_, editor, cx| {
603+
let newest = editor.selections.newest::<usize>(cx);
604+
editor.change_selections(Default::default(), window, cx, |s| s.select(vec![newest]));
605+
});
606+
}
578607
}
579608

580609
#[cfg(test)]

crates/vim/src/helix/duplicate.rs

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
use std::ops::Range;
2+
3+
use editor::{DisplayPoint, display_map::DisplaySnapshot};
4+
use gpui::Context;
5+
use text::Bias;
6+
use ui::Window;
7+
8+
use crate::Vim;
9+
10+
impl Vim {
11+
/// Creates a duplicate of every selection below it in the first place that has both its start
12+
/// and end
13+
pub(super) fn helix_duplicate_selections_below(
14+
&mut self,
15+
times: Option<usize>,
16+
window: &mut Window,
17+
cx: &mut Context<Self>,
18+
) {
19+
self.duplicate_selections(
20+
times,
21+
window,
22+
cx,
23+
|prev_point| *prev_point.row_mut() += 1,
24+
|prev_range, map| prev_range.end.row() >= map.max_point().row(),
25+
false,
26+
);
27+
}
28+
29+
/// Creates a duplicate of every selection above it in the first place that has both its start
30+
/// and end
31+
pub(super) fn helix_duplicate_selections_above(
32+
&mut self,
33+
times: Option<usize>,
34+
window: &mut Window,
35+
cx: &mut Context<Self>,
36+
) {
37+
self.duplicate_selections(
38+
times,
39+
window,
40+
cx,
41+
|prev_point| *prev_point.row_mut() = prev_point.row().0.saturating_sub(1),
42+
|prev_range, _| prev_range.start.row() == DisplayPoint::zero().row(),
43+
true,
44+
);
45+
}
46+
47+
fn duplicate_selections(
48+
&mut self,
49+
times: Option<usize>,
50+
window: &mut Window,
51+
cx: &mut Context<Self>,
52+
advance_search: impl Fn(&mut DisplayPoint),
53+
end_search: impl Fn(&Range<DisplayPoint>, &DisplaySnapshot) -> bool,
54+
above: bool,
55+
) {
56+
let times = times.unwrap_or(1);
57+
self.update_editor(cx, |_, editor, cx| {
58+
let mut selections = Vec::new();
59+
let (map, mut original_selections) = editor.selections.all_display(cx);
60+
// The order matters, because it is recorded when the selections are added.
61+
if above {
62+
original_selections.reverse();
63+
}
64+
65+
for origin in original_selections {
66+
let origin = origin.tail()..origin.head();
67+
selections.push(display_point_range_to_offset_range(&origin, &map));
68+
let mut last_origin = origin;
69+
for _ in 1..=times {
70+
if let Some(duplicate) = find_next_valid_duplicate_space(
71+
last_origin.clone(),
72+
&map,
73+
&advance_search,
74+
&end_search,
75+
) {
76+
selections.push(display_point_range_to_offset_range(&duplicate, &map));
77+
last_origin = duplicate;
78+
} else {
79+
break;
80+
}
81+
}
82+
}
83+
84+
editor.change_selections(Default::default(), window, cx, |s| {
85+
s.select_ranges(selections);
86+
});
87+
});
88+
}
89+
}
90+
91+
fn find_next_valid_duplicate_space(
92+
mut origin: Range<DisplayPoint>,
93+
map: &DisplaySnapshot,
94+
advance_search: &impl Fn(&mut DisplayPoint),
95+
end_search: &impl Fn(&Range<DisplayPoint>, &DisplaySnapshot) -> bool,
96+
) -> Option<Range<DisplayPoint>> {
97+
while !end_search(&origin, map) {
98+
advance_search(&mut origin.start);
99+
advance_search(&mut origin.end);
100+
101+
if map.clip_point(origin.start, Bias::Left) == origin.start
102+
&& map.clip_point(origin.end, Bias::Right) == origin.end
103+
{
104+
return Some(origin);
105+
}
106+
}
107+
None
108+
}
109+
110+
fn display_point_range_to_offset_range(
111+
range: &Range<DisplayPoint>,
112+
map: &DisplaySnapshot,
113+
) -> Range<usize> {
114+
range.start.to_offset(map, Bias::Left)..range.end.to_offset(map, Bias::Right)
115+
}
116+
117+
#[cfg(test)]
118+
mod tests {
119+
use db::indoc;
120+
121+
use crate::{state::Mode, test::VimTestContext};
122+
123+
#[gpui::test]
124+
async fn test_selection_duplication(cx: &mut gpui::TestAppContext) {
125+
let mut cx = VimTestContext::new(cx, true).await;
126+
cx.enable_helix();
127+
128+
cx.set_state(
129+
indoc! {"
130+
The quick brown
131+
fox «jumpsˇ»
132+
over the
133+
lazy dog."},
134+
Mode::HelixNormal,
135+
);
136+
137+
cx.simulate_keystrokes("C");
138+
139+
cx.assert_state(
140+
indoc! {"
141+
The quick brown
142+
fox «jumpsˇ»
143+
over the
144+
lazy« dog.ˇ»"},
145+
Mode::HelixNormal,
146+
);
147+
148+
cx.simulate_keystrokes("C");
149+
150+
cx.assert_state(
151+
indoc! {"
152+
The quick brown
153+
fox «jumpsˇ»
154+
over the
155+
lazy« dog.ˇ»"},
156+
Mode::HelixNormal,
157+
);
158+
159+
cx.simulate_keystrokes("alt-C");
160+
161+
cx.assert_state(
162+
indoc! {"
163+
The «quickˇ» brown
164+
fox «jumpsˇ»
165+
over the
166+
lazy« dog.ˇ»"},
167+
Mode::HelixNormal,
168+
);
169+
170+
cx.simulate_keystrokes(",");
171+
172+
cx.assert_state(
173+
indoc! {"
174+
The «quickˇ» brown
175+
fox jumps
176+
over the
177+
lazy dog."},
178+
Mode::HelixNormal,
179+
);
180+
}
181+
182+
#[gpui::test]
183+
async fn test_selection_duplication_backwards(cx: &mut gpui::TestAppContext) {
184+
let mut cx = VimTestContext::new(cx, true).await;
185+
cx.enable_helix();
186+
187+
cx.set_state(
188+
indoc! {"
189+
The quick brown
190+
«ˇfox» jumps
191+
over the
192+
lazy dog."},
193+
Mode::HelixNormal,
194+
);
195+
196+
cx.simulate_keystrokes("C C alt-C");
197+
198+
cx.assert_state(
199+
indoc! {"
200+
«ˇThe» quick brown
201+
«ˇfox» jumps
202+
«ˇove»r the
203+
«ˇlaz»y dog."},
204+
Mode::HelixNormal,
205+
);
206+
}
207+
208+
#[gpui::test]
209+
async fn test_selection_duplication_count(cx: &mut gpui::TestAppContext) {
210+
let mut cx = VimTestContext::new(cx, true).await;
211+
cx.enable_helix();
212+
213+
cx.set_state(
214+
indoc! {"
215+
The «qˇ»uick brown
216+
fox jumps
217+
over the
218+
lazy dog."},
219+
Mode::HelixNormal,
220+
);
221+
222+
cx.simulate_keystrokes("9 C");
223+
224+
cx.assert_state(
225+
indoc! {"
226+
The «qˇ»uick brown
227+
fox «jˇ»umps
228+
over« ˇ»the
229+
lazy« ˇ»dog."},
230+
Mode::HelixNormal,
231+
);
232+
}
233+
}

0 commit comments

Comments
 (0)