add anchored movement commands

Commands added:
* `move_anchored_line_up`
* `move_anchored_line_down`
* `move_anchored_visual_line_up`
* `move_anchored_visual_line_down`
* `extend_anchored_line_up`
* `extend_anchored_line_down`
* `extend_anchored_visual_line_up`
* `extend_anchored_visual_line_down`

These new commands move cursors vertically. A cursor will move depending
on its position:

* If it is on a newline character of a non-empty line, the cursor will
  stay on newlines (i.e. on a line's last character).

* If it is on a non-newline character of a non-empty line, the cursor
  will try to avoid newline characters. It will move normally, but if
  it would end up on a newline, instead it will be moved one position
  left of it (i.e. the line's second to last character).

* If it is on the newline character of an empty line (that contains
  nothing except the newline character), the cursor will continue to
  move like before: If it stayed on newline before, it will continue to
  do so. Otherwise it will try to avoid them (except on empty lines).
This commit is contained in:
Pantos 2024-04-23 21:33:13 +02:00
parent 2b1435d37b
commit cc659f2cba
2 changed files with 490 additions and 2 deletions

View File

@ -11,7 +11,7 @@
next_grapheme_boundary, nth_next_grapheme_boundary, nth_prev_grapheme_boundary,
prev_grapheme_boundary,
},
line_ending::rope_is_line_ending,
line_ending::{line_end_char_index, rope_is_line_ending},
position::char_idx_at_visual_block_offset,
syntax::LanguageConfiguration,
text_annotations::TextAnnotations,
@ -199,6 +199,190 @@ pub fn move_vertically(
new_range
}
type MoveFn =
fn(RopeSlice, Range, Direction, usize, Movement, &TextFormat, &mut TextAnnotations) -> Range;
#[allow(clippy::too_many_arguments)] // just an internal helper function
fn move_anchored(
move_fn: MoveFn,
slice: RopeSlice,
range: Range,
dir: Direction,
count: usize,
behaviour: Movement,
text_fmt: &TextFormat,
annotations: &mut TextAnnotations,
) -> Range {
/// Store an indicator in a given [`Range`] to be able to remember the previous strategy
/// (stay on newlines or avoid them) after encountering a newline.
fn set_indicator_to_stay_on_newline(range: &mut Range) {
let softwrapped_lines: u32 = range.old_visual_position.unzip().0.unwrap_or(0);
range.old_visual_position = Some((softwrapped_lines, u32::MAX));
}
/// Retrieve the indicator that might previously have been set with
/// [`set_indicator_to_stay_on_newline()`].
fn get_indicator_to_stay_on_newline(range: &Range) -> bool {
match range.old_visual_position {
None => false,
Some((_, u32::MAX)) => true,
Some((_, _)) => false,
}
}
/// Figure out if a certain position/index is in a visual empty line.
///
/// If the given `pos` is a newline character and it is alone in its line or visual line,
/// this function will return `true`, otherwise `false.
fn is_in_visual_empty_line(
slice: RopeSlice,
text_fmt: &TextFormat,
annotations: &TextAnnotations,
pos: usize,
) -> bool {
let line = slice.char_to_line(pos);
// if this line only contains a newline char, it is empty
if rope_is_line_ending(slice.line(line)) {
return true;
}
// if we got here without soft wrap, this line is not empty
if !text_fmt.soft_wrap {
return false;
}
// if pos is not the last character, there have to be other chars in the same visual line
if pos != line_end_char_index(&slice, line) {
return false;
}
// if the previous char (has to exist) is not in the same row, this is an empty visual line
let prev = prev_grapheme_boundary(slice, pos);
let pos_visual_row = visual_offset_from_block(slice, pos, pos, text_fmt, annotations)
.0
.row;
let prev_visual_row = visual_offset_from_block(slice, prev, prev, text_fmt, annotations)
.0
.row;
pos_visual_row != prev_visual_row
}
/// Move to the next newline character, in direction of movement.
fn move_to_next_newline(
slice: RopeSlice,
range: Range,
dir: Direction,
count: usize,
behaviour: Movement,
text_fmt: &TextFormat,
annotations: &mut TextAnnotations,
) -> Range {
// Move to new position.
// Note: We can't use the given `move_fn` here. If we move visually backwards and soft-wrap
// is enabled, we would end up in the same line, and get the same newline character that we
// are actually coming from.
let new_range = move_vertically(slice, range, dir, count, behaviour, text_fmt, annotations);
let new_pos = new_range.cursor(slice);
let new_line = slice.char_to_line(new_pos);
// move to newline char in this line
let newline_pos = line_end_char_index(&slice, new_line);
new_range.put_cursor(slice, newline_pos, behaviour == Movement::Extend)
}
/// Move a range's cursor to the previous grapheme in the same line, if the cursor is on a
/// newline character in a non-empty (visual or non-visual) line.
fn try_to_avoid_newline(
slice: RopeSlice,
range: Range,
behaviour: Movement,
text_fmt: &TextFormat,
annotations: &mut TextAnnotations,
) -> Range {
let pos = range.cursor(slice);
let line = slice.char_to_line(pos);
let end_char_index = line_end_char_index(&slice, line);
let pos_is_in_empty_line = is_in_visual_empty_line(slice, text_fmt, annotations, pos);
let pos_is_at_end_of_line = pos == end_char_index;
if !pos_is_at_end_of_line || pos_is_in_empty_line {
return range;
}
// move away from newline character
let new_pos = prev_grapheme_boundary(slice, end_char_index);
let old_visual_position = range.old_visual_position;
let mut new_range = range.put_cursor(slice, new_pos, behaviour == Movement::Extend);
new_range.old_visual_position = old_visual_position;
new_range
}
let pos = range.cursor(slice);
let line = slice.char_to_line(pos);
let pos_is_at_end_of_line = pos == line_end_char_index(&slice, line);
let pos_is_in_empty_line = is_in_visual_empty_line(slice, text_fmt, annotations, pos);
let new_range = move_fn(slice, range, dir, count, behaviour, text_fmt, annotations);
// Stay on newline characters if the cursor currently is on one. If the current line is empty
// (i.e. it only contains a newline character), only stay on newlines if also done so before.
let stayed_on_newline_before = get_indicator_to_stay_on_newline(&range);
let stay_on_newline =
pos_is_at_end_of_line && (stayed_on_newline_before || !pos_is_in_empty_line);
if stay_on_newline {
let mut updated_range =
move_to_next_newline(slice, range, dir, count, behaviour, text_fmt, annotations);
set_indicator_to_stay_on_newline(&mut updated_range);
updated_range
} else {
try_to_avoid_newline(slice, new_range, behaviour, text_fmt, annotations)
}
}
pub fn move_vertically_anchored(
slice: RopeSlice,
range: Range,
dir: Direction,
count: usize,
behaviour: Movement,
text_fmt: &TextFormat,
annotations: &mut TextAnnotations,
) -> Range {
move_anchored(
move_vertically,
slice,
range,
dir,
count,
behaviour,
text_fmt,
annotations,
)
}
pub fn move_vertically_anchored_visual(
slice: RopeSlice,
range: Range,
dir: Direction,
count: usize,
behaviour: Movement,
text_fmt: &TextFormat,
annotations: &mut TextAnnotations,
) -> Range {
move_anchored(
move_vertically_visual,
slice,
range,
dir,
count,
behaviour,
text_fmt,
annotations,
)
}
pub fn move_next_word_start(slice: RopeSlice, range: Range, count: usize) -> Range {
word_move(slice, range, count, WordMotionTarget::NextWordStart)
}
@ -772,6 +956,229 @@ fn test_vertical_move() {
);
}
#[test]
fn vertical_anchored_move_from_newline_stays_on_newline() {
let text = Rope::from("aaa\na\n\naaaa\n");
let slice = text.slice(..);
let pos = pos_at_coords(slice, (0, 3).into(), true);
let mut range = Range::new(pos, pos);
let vmove = |range, direction, count| {
move_vertically_anchored(
slice,
range,
direction,
count,
Movement::Move,
&TextFormat::default(),
&mut TextAnnotations::default(),
)
};
range = vmove(range, Direction::Forward, 1);
assert_eq!(coords_at_pos(slice, range.head), (1, 1).into());
range = vmove(range, Direction::Forward, 1);
assert_eq!(coords_at_pos(slice, range.head), (2, 0).into());
range = vmove(range, Direction::Forward, 1);
assert_eq!(coords_at_pos(slice, range.head), (3, 4).into());
range = vmove(range, Direction::Forward, 1);
assert_eq!(coords_at_pos(slice, range.head), (4, 0).into());
range = vmove(range, Direction::Forward, 1);
assert_eq!(coords_at_pos(slice, range.head), (4, 0).into());
range = vmove(range, Direction::Backward, 1);
assert_eq!(coords_at_pos(slice, range.head), (3, 4).into());
range = vmove(range, Direction::Backward, 1);
assert_eq!(coords_at_pos(slice, range.head), (2, 0).into());
range = vmove(range, Direction::Backward, 1);
assert_eq!(coords_at_pos(slice, range.head), (1, 1).into());
range = vmove(range, Direction::Backward, 1);
assert_eq!(coords_at_pos(slice, range.head), (0, 3).into());
range = vmove(range, Direction::Backward, 1);
assert_eq!(coords_at_pos(slice, range.head), (0, 3).into());
range = vmove(range, Direction::Forward, 3);
assert_eq!(coords_at_pos(slice, range.head), (3, 4).into());
range = vmove(range, Direction::Backward, 3);
assert_eq!(coords_at_pos(slice, range.head), (0, 3).into());
}
#[test]
fn vertical_anchored_move_from_non_newline_avoids_newline() {
let text = Rope::from("aaa\na\n\naaaa\n");
let slice = text.slice(..);
let pos = pos_at_coords(slice, (0, 2).into(), true);
let mut range = Range::new(pos, pos);
let vmove = |range, direction, count| {
move_vertically_anchored(
slice,
range,
direction,
count,
Movement::Move,
&TextFormat::default(),
&mut TextAnnotations::default(),
)
};
range = vmove(range, Direction::Forward, 1);
assert_eq!(coords_at_pos(slice, range.head), (1, 0).into());
range = vmove(range, Direction::Forward, 1);
assert_eq!(coords_at_pos(slice, range.head), (2, 0).into());
range = vmove(range, Direction::Forward, 1);
assert_eq!(coords_at_pos(slice, range.head), (3, 2).into());
range = vmove(range, Direction::Forward, 1);
assert_eq!(coords_at_pos(slice, range.head), (4, 0).into());
range = vmove(range, Direction::Forward, 1);
assert_eq!(coords_at_pos(slice, range.head), (4, 0).into());
range = vmove(range, Direction::Backward, 1);
assert_eq!(coords_at_pos(slice, range.head), (3, 2).into());
range = vmove(range, Direction::Backward, 1);
assert_eq!(coords_at_pos(slice, range.head), (2, 0).into());
range = vmove(range, Direction::Backward, 1);
assert_eq!(coords_at_pos(slice, range.head), (1, 0).into());
range = vmove(range, Direction::Backward, 1);
assert_eq!(coords_at_pos(slice, range.head), (0, 2).into());
range = vmove(range, Direction::Backward, 1);
assert_eq!(coords_at_pos(slice, range.head), (0, 2).into());
range = vmove(range, Direction::Forward, 3);
assert_eq!(coords_at_pos(slice, range.head), (3, 2).into());
range = vmove(range, Direction::Backward, 3);
assert_eq!(coords_at_pos(slice, range.head), (0, 2).into());
}
#[test]
fn vertical_visual_anchored_move_from_newline_stays_on_newline() {
let text_fmt = TextFormat {
soft_wrap: true,
viewport_width: 6,
..Default::default()
};
let text = Rope::from("a\naaaaaabb\naaaaaab\n\naa\n");
let slice = text.slice(..);
let pos = pos_at_coords(slice, (0, 1).into(), true);
let mut range = Range::new(pos, pos);
let vvmove = |range, direction, count| -> Range {
move_vertically_anchored_visual(
slice,
range,
direction,
count,
Movement::Move,
&text_fmt,
&mut TextAnnotations::default(),
)
};
range = vvmove(range, Direction::Forward, 1);
assert_eq!(coords_at_pos(slice, range.head), (1, 8).into());
range = vvmove(range, Direction::Forward, 1);
assert_eq!(coords_at_pos(slice, range.head), (2, 7).into());
range = vvmove(range, Direction::Forward, 1);
assert_eq!(coords_at_pos(slice, range.head), (3, 0).into());
range = vvmove(range, Direction::Forward, 1);
assert_eq!(coords_at_pos(slice, range.head), (4, 2).into());
range = vvmove(range, Direction::Forward, 1);
assert_eq!(coords_at_pos(slice, range.head), (5, 0).into());
range = vvmove(range, Direction::Forward, 1);
assert_eq!(coords_at_pos(slice, range.head), (5, 0).into());
range = vvmove(range, Direction::Backward, 1);
assert_eq!(coords_at_pos(slice, range.head), (4, 2).into());
range = vvmove(range, Direction::Backward, 1);
assert_eq!(coords_at_pos(slice, range.head), (3, 0).into());
range = vvmove(range, Direction::Backward, 1);
assert_eq!(coords_at_pos(slice, range.head), (2, 7).into());
range = vvmove(range, Direction::Backward, 1);
assert_eq!(coords_at_pos(slice, range.head), (1, 8).into());
range = vvmove(range, Direction::Backward, 1);
assert_eq!(coords_at_pos(slice, range.head), (0, 1).into());
range = vvmove(range, Direction::Backward, 1);
assert_eq!(coords_at_pos(slice, range.head), (0, 1).into());
range = vvmove(range, Direction::Forward, 4);
assert_eq!(coords_at_pos(slice, range.head), (4, 2).into());
range = vvmove(range, Direction::Backward, 3);
assert_eq!(coords_at_pos(slice, range.head), (1, 8).into());
}
#[test]
fn vertical_visual_anchored_move_from_non_newline_avoids_newline() {
let text_fmt = TextFormat {
soft_wrap: true,
viewport_width: 6,
..Default::default()
};
let text = Rope::from("aaaaaabb\naaa\n\naaaaaa\n aaaabb\na");
let slice = text.slice(..);
let pos = pos_at_coords(slice, (0, 3).into(), true);
let mut range = Range::new(pos, pos);
let vvmove = |range, direction, count| -> Range {
move_vertically_anchored_visual(
slice,
range,
direction,
count,
Movement::Move,
&text_fmt,
&mut TextAnnotations::default(),
)
};
range = vvmove(range, Direction::Forward, 1);
// wrapped word, stay in same line
assert_eq!(coords_at_pos(slice, range.head), (0, 7).into());
range = vvmove(range, Direction::Forward, 1);
assert_eq!(coords_at_pos(slice, range.head), (1, 2).into());
range = vvmove(range, Direction::Forward, 1);
assert_eq!(coords_at_pos(slice, range.head), (2, 0).into());
range = vvmove(range, Direction::Forward, 1);
assert_eq!(coords_at_pos(slice, range.head), (3, 3).into());
range = vvmove(range, Direction::Forward, 1);
// wrapped newline, stay in same line
assert_eq!(coords_at_pos(slice, range.head), (3, 6).into());
range = vvmove(range, Direction::Forward, 1);
// line was visually empty, continue avoiding newlines
assert_eq!(coords_at_pos(slice, range.head), (4, 3).into());
range = vvmove(range, Direction::Forward, 1);
assert_eq!(coords_at_pos(slice, range.head), (4, 6).into());
range = vvmove(range, Direction::Forward, 1);
assert_eq!(coords_at_pos(slice, range.head), (5, 0).into());
range = vvmove(range, Direction::Forward, 1);
assert_eq!(coords_at_pos(slice, range.head), (5, 0).into());
range = vvmove(range, Direction::Backward, 1);
assert_eq!(coords_at_pos(slice, range.head), (4, 6).into());
range = vvmove(range, Direction::Backward, 1);
assert_eq!(coords_at_pos(slice, range.head), (4, 3).into());
range = vvmove(range, Direction::Backward, 1);
assert_eq!(coords_at_pos(slice, range.head), (3, 6).into());
range = vvmove(range, Direction::Backward, 1);
assert_eq!(coords_at_pos(slice, range.head), (3, 3).into());
range = vvmove(range, Direction::Backward, 1);
assert_eq!(coords_at_pos(slice, range.head), (2, 0).into());
range = vvmove(range, Direction::Backward, 1);
assert_eq!(coords_at_pos(slice, range.head), (1, 2).into());
range = vvmove(range, Direction::Backward, 1);
assert_eq!(coords_at_pos(slice, range.head), (0, 7).into());
range = vvmove(range, Direction::Backward, 1);
assert_eq!(coords_at_pos(slice, range.head), (0, 3).into());
range = vvmove(range, Direction::Backward, 1);
assert_eq!(coords_at_pos(slice, range.head), (0, 3).into());
range = vvmove(range, Direction::Forward, 4);
assert_eq!(coords_at_pos(slice, range.head), (3, 3).into());
range = vvmove(range, Direction::Backward, 3);
assert_eq!(coords_at_pos(slice, range.head), (0, 7).into());
}
#[test]
fn horizontal_movement_in_same_line() {
let text = Rope::from("a\na\naaaa");

View File

@ -282,16 +282,24 @@ pub fn doc(&self) -> &str {
move_same_line_char_right, "Move right within in the same line only",
move_line_up, "Move up",
move_line_down, "Move down",
move_anchored_line_up, "Move up with newline anchoring behaviour",
move_anchored_line_down, "Move down with newline anchoring behaviour",
move_visual_line_up, "Move up",
move_visual_line_down, "Move down",
move_anchored_visual_line_up, "Move up with newline anchoring behaviour",
move_anchored_visual_line_down, "Move down with newline anchoring behaviour",
extend_char_left, "Extend left",
extend_char_right, "Extend right",
extend_same_line_char_left, "Extend left within the same line only",
extend_same_line_char_right, "Extend right within the same line only",
extend_line_up, "Extend up",
extend_line_down, "Extend down",
extend_anchored_line_up, "Extend up with newline anchoring behaviour",
extend_anchored_line_down, "Extend down with newline anchoring behaviour",
extend_visual_line_up, "Extend up",
extend_visual_line_down, "Extend down",
extend_anchored_visual_line_up, "Extend up with newline anchoring behaviour",
extend_anchored_visual_line_down, "Extend down with newline anchoring behaviour",
copy_selection_on_next_line, "Copy selection on next line",
copy_selection_on_prev_line, "Copy selection on previous line",
move_next_word_start, "Move to start of next word",
@ -701,7 +709,8 @@ fn move_impl(cx: &mut Context, move_fn: MoveFn, dir: Direction, behaviour: Movem
}
use helix_core::movement::{
move_horizontally, move_horizontally_same_line, move_vertically, move_vertically_visual,
move_horizontally, move_horizontally_same_line, move_vertically, move_vertically_anchored,
move_vertically_anchored_visual, move_vertically_visual,
};
fn move_char_left(cx: &mut Context) {
@ -738,6 +747,24 @@ fn move_line_down(cx: &mut Context) {
move_impl(cx, move_vertically, Direction::Forward, Movement::Move)
}
fn move_anchored_line_up(cx: &mut Context) {
move_impl(
cx,
move_vertically_anchored,
Direction::Backward,
Movement::Move,
)
}
fn move_anchored_line_down(cx: &mut Context) {
move_impl(
cx,
move_vertically_anchored,
Direction::Forward,
Movement::Move,
)
}
fn move_visual_line_up(cx: &mut Context) {
move_impl(
cx,
@ -756,6 +783,24 @@ fn move_visual_line_down(cx: &mut Context) {
)
}
fn move_anchored_visual_line_up(cx: &mut Context) {
move_impl(
cx,
move_vertically_anchored_visual,
Direction::Backward,
Movement::Move,
)
}
fn move_anchored_visual_line_down(cx: &mut Context) {
move_impl(
cx,
move_vertically_anchored_visual,
Direction::Forward,
Movement::Move,
)
}
fn extend_char_left(cx: &mut Context) {
move_impl(cx, move_horizontally, Direction::Backward, Movement::Extend)
}
@ -790,6 +835,24 @@ fn extend_line_down(cx: &mut Context) {
move_impl(cx, move_vertically, Direction::Forward, Movement::Extend)
}
fn extend_anchored_line_up(cx: &mut Context) {
move_impl(
cx,
move_vertically_anchored,
Direction::Backward,
Movement::Extend,
)
}
fn extend_anchored_line_down(cx: &mut Context) {
move_impl(
cx,
move_vertically_anchored,
Direction::Forward,
Movement::Extend,
)
}
fn extend_visual_line_up(cx: &mut Context) {
move_impl(
cx,
@ -808,6 +871,24 @@ fn extend_visual_line_down(cx: &mut Context) {
)
}
fn extend_anchored_visual_line_up(cx: &mut Context) {
move_impl(
cx,
move_vertically_anchored_visual,
Direction::Backward,
Movement::Extend,
)
}
fn extend_anchored_visual_line_down(cx: &mut Context) {
move_impl(
cx,
move_vertically_anchored_visual,
Direction::Forward,
Movement::Extend,
)
}
fn goto_line_end_impl(view: &mut View, doc: &mut Document, movement: Movement) {
let text = doc.text().slice(..);