aboutsummaryrefslogtreecommitdiff
path: root/helix-core/src/movement.rs
diff options
context:
space:
mode:
Diffstat (limited to 'helix-core/src/movement.rs')
-rw-r--r--helix-core/src/movement.rs181
1 files changed, 123 insertions, 58 deletions
diff --git a/helix-core/src/movement.rs b/helix-core/src/movement.rs
index f9e5deb4..bc56f9a4 100644
--- a/helix-core/src/movement.rs
+++ b/helix-core/src/movement.rs
@@ -5,8 +5,11 @@ use ropey::iter::Chars;
use crate::{
chars::{categorize_char, char_is_line_ending, CharCategory},
coords_at_pos,
- graphemes::{nth_next_grapheme_boundary, nth_prev_grapheme_boundary},
- line_ending::{get_line_ending, line_end_char_index},
+ graphemes::{
+ next_grapheme_boundary, nth_next_grapheme_boundary, nth_prev_grapheme_boundary,
+ prev_grapheme_boundary, RopeGraphemes,
+ },
+ line_ending::line_without_line_ending,
pos_at_coords, Position, Range, RopeSlice,
};
@@ -29,25 +32,60 @@ pub fn move_horizontally(
count: usize,
behaviour: Movement,
) -> Range {
- let pos = range.head;
- let line = slice.char_to_line(pos);
- // TODO: we can optimize clamping by passing in RopeSlice limited to current line. that way
- // we stop calculating past start/end of line.
- let pos = match dir {
- Direction::Backward => {
- let start = slice.line_to_char(line);
- nth_prev_grapheme_boundary(slice, pos, count).max(start)
+ match (behaviour, dir) {
+ (Movement::Move, Direction::Backward) => {
+ let count = if range.anchor < range.head {
+ count + 1
+ } else {
+ count
+ };
+ let pos = nth_prev_grapheme_boundary(slice, range.head, count);
+ Range::new(pos, pos)
}
- Direction::Forward => {
- let end_char_idx = line_end_char_index(&slice, line);
- nth_next_grapheme_boundary(slice, pos, count).min(end_char_idx)
+ (Movement::Move, Direction::Forward) => {
+ let count = if range.anchor < range.head {
+ count - 1
+ } else {
+ count
+ };
+ let pos = nth_next_grapheme_boundary(slice, range.head, count);
+ Range::new(pos, pos)
}
- };
- let anchor = match behaviour {
- Movement::Extend => range.anchor,
- Movement::Move => pos,
- };
- Range::new(anchor, pos)
+ (Movement::Extend, Direction::Backward) => {
+ // Ensure a valid initial selection state.
+ let range = range.min_width_1(slice);
+
+ // Do the main movement.
+ let mut head = nth_prev_grapheme_boundary(slice, range.head, count);
+ let mut anchor = range.anchor;
+
+ // If the head and anchor crossed over each other, we need to
+ // fiddle around to make it behave like a 1-wide cursor.
+ if head <= anchor && range.head > range.anchor {
+ anchor = next_grapheme_boundary(slice, anchor);
+ head = prev_grapheme_boundary(slice, head);
+ }
+
+ Range::new(anchor, head)
+ }
+ (Movement::Extend, Direction::Forward) => {
+ // Ensure a valid initial selection state.
+ let range = range.min_width_1(slice);
+
+ // Do the main movement.
+ let mut head = nth_next_grapheme_boundary(slice, range.head, count);
+ let mut anchor = range.anchor;
+
+ // If the head and anchor crossed over each other, we need to
+ // fiddle around to make it behave like a 1-wide cursor.
+ if head >= anchor && range.head < range.anchor {
+ anchor = prev_grapheme_boundary(slice, anchor);
+ head = next_grapheme_boundary(slice, head);
+ }
+
+ Range::new(anchor, head)
+ }
+ }
}
pub fn move_vertically(
@@ -57,36 +95,61 @@ pub fn move_vertically(
count: usize,
behaviour: Movement,
) -> Range {
- let Position { row, col } = coords_at_pos(slice, range.head);
+ // Shift back one grapheme if needed, to account for
+ // the cursor being visually 1-width.
+ let pos = if range.head > range.anchor {
+ prev_grapheme_boundary(slice, range.head)
+ } else {
+ range.head
+ };
+ // Compute the current position's 2d coordinates.
+ let Position { row, col } = coords_at_pos(slice, pos);
let horiz = range.horiz.unwrap_or(col as u32);
- let new_line = match dir {
- Direction::Backward => row.saturating_sub(count),
- Direction::Forward => std::cmp::min(
- row.saturating_add(count),
- slice.len_lines().saturating_sub(2),
- ),
- };
+ // Compute the new position.
+ let new_pos = {
+ let new_row = if dir == Direction::Backward {
+ row.saturating_sub(count)
+ } else {
+ (row + count).min(slice.len_lines().saturating_sub(1))
+ };
+ let max_col = RopeGraphemes::new(line_without_line_ending(&slice, new_row)).count();
+ let new_col = col.max(horiz as usize).min(max_col);
- // Length of the line sans line-ending.
- let new_line_len = {
- let line = slice.line(new_line);
- line.len_chars() - get_line_ending(&line).map(|le| le.len_chars()).unwrap_or(0)
+ pos_at_coords(slice, Position::new(new_row, new_col))
};
- let new_col = std::cmp::min(horiz as usize, new_line_len);
-
- let pos = pos_at_coords(slice, Position::new(new_line, new_col));
+ // Compute the new range according to the type of movement.
+ match behaviour {
+ Movement::Move => Range {
+ anchor: new_pos,
+ head: new_pos,
+ horiz: Some(horiz),
+ },
+
+ Movement::Extend => {
+ let new_head = if new_pos >= range.anchor {
+ next_grapheme_boundary(slice, new_pos)
+ } else {
+ new_pos
+ };
- let anchor = match behaviour {
- Movement::Extend => range.anchor,
- Movement::Move => pos,
- };
+ let new_anchor = if range.anchor <= range.head && range.anchor > new_head {
+ next_grapheme_boundary(slice, range.anchor)
+ } else if range.anchor > range.head && range.anchor < new_head {
+ prev_grapheme_boundary(slice, range.anchor)
+ } else {
+ range.anchor
+ };
- let mut range = Range::new(anchor, pos);
- range.horiz = Some(horiz);
- range
+ Range {
+ anchor: new_anchor,
+ head: new_head,
+ horiz: Some(horiz),
+ }
+ }
+ }
}
pub fn move_next_word_start(slice: RopeSlice, range: Range, count: usize) -> Range {
@@ -330,7 +393,7 @@ mod test {
}
#[test]
- fn horizontal_moves_through_single_line_in_single_line_text() {
+ fn horizontal_moves_through_single_line_text() {
let text = Rope::from(SINGLE_LINE_SAMPLE);
let slice = text.slice(..);
let position = pos_at_coords(slice, (0, 0).into());
@@ -353,7 +416,7 @@ mod test {
}
#[test]
- fn horizontal_moves_through_single_line_in_multiline_text() {
+ fn horizontal_moves_through_multiline_text() {
let text = Rope::from(MULTILINE_SAMPLE);
let slice = text.slice(..);
let position = pos_at_coords(slice, (0, 0).into());
@@ -361,15 +424,15 @@ mod test {
let mut range = Range::point(position);
let moves_and_expected_coordinates = IntoIter::new([
- ((Direction::Forward, 1usize), (0, 1)), // M|ultiline\n
- ((Direction::Forward, 2usize), (0, 3)), // Mul|tiline\n
- ((Direction::Backward, 6usize), (0, 0)), // |Multiline\n
- ((Direction::Backward, 999usize), (0, 0)), // |Multiline\n
- ((Direction::Forward, 3usize), (0, 3)), // Mul|tiline\n
- ((Direction::Forward, 0usize), (0, 3)), // Mul|tiline\n
- ((Direction::Backward, 0usize), (0, 3)), // Mul|tiline\n
- ((Direction::Forward, 999usize), (0, 9)), // Multiline|\n
- ((Direction::Forward, 999usize), (0, 9)), // Multiline|\n
+ ((Direction::Forward, 11usize), (1, 1)), // Multiline\nt|ext sample\n...
+ ((Direction::Backward, 1usize), (1, 0)), // Multiline\n|text sample\n...
+ ((Direction::Backward, 5usize), (0, 5)), // Multi|line\ntext sample\n...
+ ((Direction::Backward, 999usize), (0, 0)), // |Multiline\ntext sample\n...
+ ((Direction::Forward, 3usize), (0, 3)), // Mul|tiline\ntext sample\n...
+ ((Direction::Forward, 0usize), (0, 3)), // Mul|tiline\ntext sample\n...
+ ((Direction::Backward, 0usize), (0, 3)), // Mul|tiline\ntext sample\n...
+ ((Direction::Forward, 999usize), (5, 0)), // ...and whitespaced\n|
+ ((Direction::Forward, 999usize), (5, 0)), // ...and whitespaced\n|
]);
for ((direction, amount), coordinates) in moves_and_expected_coordinates {
@@ -409,12 +472,13 @@ mod test {
let moves_and_expected_coordinates = IntoIter::new([
((Direction::Forward, 1usize), (1, 0)),
((Direction::Forward, 2usize), (3, 0)),
+ ((Direction::Forward, 1usize), (4, 0)),
((Direction::Backward, 999usize), (0, 0)),
- ((Direction::Forward, 3usize), (3, 0)),
- ((Direction::Forward, 0usize), (3, 0)),
- ((Direction::Backward, 0usize), (3, 0)),
- ((Direction::Forward, 5), (4, 0)),
- ((Direction::Forward, 999usize), (4, 0)),
+ ((Direction::Forward, 4usize), (4, 0)),
+ ((Direction::Forward, 0usize), (4, 0)),
+ ((Direction::Backward, 0usize), (4, 0)),
+ ((Direction::Forward, 5), (5, 0)),
+ ((Direction::Forward, 999usize), (5, 0)),
]);
for ((direction, amount), coordinates) in moves_and_expected_coordinates {
@@ -446,7 +510,8 @@ mod test {
((Axis::V, Direction::Forward, 1usize), (3, 8)),
// Behaviour is preserved even through long jumps
((Axis::V, Direction::Backward, 999usize), (0, 8)),
- ((Axis::V, Direction::Forward, 999usize), (4, 8)),
+ ((Axis::V, Direction::Forward, 4usize), (4, 8)),
+ ((Axis::V, Direction::Forward, 999usize), (5, 0)),
]);
for ((axis, direction, amount), coordinates) in moves_and_expected_coordinates {