on master help sign in
Make horizontal selection movement work properly.
28d2d68Nathan Vegdahl
helix-core/src/movement.rs+68−30
@@ -5,8 +5,11 @@ use ropey::iter::Chars;
5use crate::{
5use crate::{
6 chars::{categorize_char, char_is_line_ending, CharCategory},
6 chars::{categorize_char, char_is_line_ending, CharCategory},
7 coords_at_pos,
7 coords_at_pos,
8+ graphemes::{
8− graphemes::{nth_next_grapheme_boundary, nth_prev_grapheme_boundary},
9+ next_grapheme_boundary, nth_next_grapheme_boundary, nth_prev_grapheme_boundary,
10+ prev_grapheme_boundary,
11+ },
9− line_ending::{get_line_ending, line_end_char_index},
12+ line_ending::get_line_ending,
10 pos_at_coords, Position, Range, RopeSlice,
13 pos_at_coords, Position, Range, RopeSlice,
11};
14};
12
15
@@ -29,25 +32,60 @@ pub fn move_horizontally(
29 count: usize,
32 count: usize,
30 behaviour: Movement,
33 behaviour: Movement,
31) -> Range {
34) -> Range {
32− let pos = range.head;
33− let line = slice.char_to_line(pos);
34− // TODO: we can optimize clamping by passing in RopeSlice limited to current line. that way
35− // we stop calculating past start/end of line.
36− let pos = match dir {
35+ match (behaviour, dir) {
37− Direction::Backward => {
36+ (Movement::Move, Direction::Backward) => {
38− let start = slice.line_to_char(line);
37+ let count = if range.anchor < range.head {
38+ count + 1
39+ } else {
40+ count
41+ };
39− nth_prev_grapheme_boundary(slice, pos, count).max(start)
42+ let pos = nth_prev_grapheme_boundary(slice, range.head, count);
43+ Range::new(pos, pos)
40 }
44 }
41− Direction::Forward => {
45+ (Movement::Move, Direction::Forward) => {
42− let end_char_idx = line_end_char_index(&slice, line);
46+ let count = if range.anchor < range.head {
47+ count - 1
48+ } else {
49+ count
50+ };
43− nth_next_grapheme_boundary(slice, pos, count).min(end_char_idx)
51+ let pos = nth_next_grapheme_boundary(slice, range.head, count);
52+ Range::new(pos, pos)
44 }
53 }
45− };
54+ (Movement::Extend, Direction::Backward) => {
46− let anchor = match behaviour {
55+ // Ensure a valid initial selection state.
56+ let range = range.min_width_1(slice);
57+
58+ // Do the main movement.
59+ let mut head = nth_prev_grapheme_boundary(slice, range.head, count);
60+ let mut anchor = range.anchor;
61+
62+ // If the head and anchor crossed over each other, we need to
63+ // fiddle around to make it behave like a 1-wide cursor.
64+ if head <= anchor && range.head > range.anchor {
65+ anchor = next_grapheme_boundary(slice, anchor);
66+ head = prev_grapheme_boundary(slice, head);
67+ }
68+
69+ Range::new(anchor, head)
70+ }
47− Movement::Extend => range.anchor,
71+ (Movement::Extend, Direction::Forward) => {
48− Movement::Move => pos,
72+ // Ensure a valid initial selection state.
73+ let range = range.min_width_1(slice);
74+
75+ // Do the main movement.
76+ let mut head = nth_next_grapheme_boundary(slice, range.head, count);
77+ let mut anchor = range.anchor;
78+
79+ // If the head and anchor crossed over each other, we need to
80+ // fiddle around to make it behave like a 1-wide cursor.
81+ if head >= anchor && range.head < range.anchor {
82+ anchor = prev_grapheme_boundary(slice, anchor);
83+ head = next_grapheme_boundary(slice, head);
49− };
84+ }
85+
50− Range::new(anchor, pos)
86+ Range::new(anchor, head)
87+ }
88+ }
51}
89}
52
90
53pub fn move_vertically(
91pub fn move_vertically(
@@ -323,7 +361,7 @@ mod test {
323 }
361 }
324
362
325 #[test]
363 #[test]
326− fn horizontal_moves_through_single_line_in_single_line_text() {
364+ fn horizontal_moves_through_single_line_text() {
327 let text = Rope::from(SINGLE_LINE_SAMPLE);
365 let text = Rope::from(SINGLE_LINE_SAMPLE);
328 let slice = text.slice(..);
366 let slice = text.slice(..);
329 let position = pos_at_coords(slice, (0, 0).into());
367 let position = pos_at_coords(slice, (0, 0).into());
@@ -346,7 +384,7 @@ mod test {
346 }
384 }
347
385
348 #[test]
386 #[test]
349− fn horizontal_moves_through_single_line_in_multiline_text() {
387+ fn horizontal_moves_through_multiline_text() {
350 let text = Rope::from(MULTILINE_SAMPLE);
388 let text = Rope::from(MULTILINE_SAMPLE);
351 let slice = text.slice(..);
389 let slice = text.slice(..);
352 let position = pos_at_coords(slice, (0, 0).into());
390 let position = pos_at_coords(slice, (0, 0).into());
@@ -354,15 +392,15 @@ mod test {
354 let mut range = Range::point(position);
392 let mut range = Range::point(position);
355
393
356 let moves_and_expected_coordinates = IntoIter::new([
394 let moves_and_expected_coordinates = IntoIter::new([
357− ((Direction::Forward, 1usize), (0, 1)), // M|ultiline\n
395+ ((Direction::Forward, 11usize), (1, 1)), // Multiline\nt|ext sample\n...
358− ((Direction::Forward, 2usize), (0, 3)), // Mul|tiline\n
396+ ((Direction::Backward, 1usize), (1, 0)), // Multiline\n|text sample\n...
359− ((Direction::Backward, 6usize), (0, 0)), // |Multiline\n
397+ ((Direction::Backward, 5usize), (0, 5)), // Multi|line\ntext sample\n...
360− ((Direction::Backward, 999usize), (0, 0)), // |Multiline\n
398+ ((Direction::Backward, 999usize), (0, 0)), // |Multiline\ntext sample\n...
361− ((Direction::Forward, 3usize), (0, 3)), // Mul|tiline\n
399+ ((Direction::Forward, 3usize), (0, 3)), // Mul|tiline\ntext sample\n...
362− ((Direction::Forward, 0usize), (0, 3)), // Mul|tiline\n
400+ ((Direction::Forward, 0usize), (0, 3)), // Mul|tiline\ntext sample\n...
363− ((Direction::Backward, 0usize), (0, 3)), // Mul|tiline\n
401+ ((Direction::Backward, 0usize), (0, 3)), // Mul|tiline\ntext sample\n...
364− ((Direction::Forward, 999usize), (0, 9)), // Multiline|\n
402+ ((Direction::Forward, 999usize), (5, 0)), // ...and whitespaced\n|
365− ((Direction::Forward, 999usize), (0, 9)), // Multiline|\n
403+ ((Direction::Forward, 999usize), (5, 0)), // ...and whitespaced\n|
366 ]);
404 ]);
367
405
368 for ((direction, amount), coordinates) in moves_and_expected_coordinates {
406 for ((direction, amount), coordinates) in moves_and_expected_coordinates {
helix-term/src/commands.rs+19−1
@@ -2193,7 +2193,25 @@ fn goto_mode(cx: &mut Context) {
2193}
2193}
2194
2194
2195fn select_mode(cx: &mut Context) {
2195fn select_mode(cx: &mut Context) {
2196+ let (view, doc) = current!(cx.editor);
2197+
2198+ // Make sure all selections are at least 1-wide.
2199+ // (With the exception of being in an empty document, of course.)
2200+ doc.set_selection(
2201+ view.id,
2202+ doc.selection(view.id).clone().transform(|range| {
2203+ if range.is_empty() && range.head == doc.text().len_chars() {
2204+ Range::new(
2205+ graphemes::prev_grapheme_boundary(doc.text().slice(..), range.anchor),
2206+ range.head,
2207+ )
2208+ } else {
2209+ range.min_width_1(doc.text().slice(..))
2210+ }
2211+ }),
2212+ );
2213+
2196− doc_mut!(cx.editor).mode = Mode::Select;
2214+ doc.mode = Mode::Select;
2197}
2215}
2198
2216
2199fn exit_select_mode(cx: &mut Context) {
2217fn exit_select_mode(cx: &mut Context) {