Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Vim like movement commands #4204

Closed
102 changes: 101 additions & 1 deletion helix-core/src/movement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::{
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},
pos_at_visual_coords,
syntax::LanguageConfiguration,
textobject::TextObject,
Expand Down Expand Up @@ -48,6 +48,28 @@ pub fn move_horizontally(
range.put_cursor(slice, new_pos, behaviour == Movement::Extend)
}

pub fn move_horizontally_same_line(
slice: RopeSlice,
range: Range,
dir: Direction,
count: usize,
behaviour: Movement,
_: usize,
) -> Range {
let pos = range.cursor(slice);
let new_pos = match dir {
Direction::Forward => nth_next_grapheme_boundary(slice, pos, count),
Direction::Backward => nth_prev_grapheme_boundary(slice, pos, count),
};

// only allow moves within the same line.
if slice.char_to_line(pos) == slice.char_to_line(new_pos) {
range.put_cursor(slice, new_pos, behaviour == Movement::Extend)
} else {
range
}
}

pub fn move_vertically(
slice: RopeSlice,
range: Range,
Expand Down Expand Up @@ -80,6 +102,84 @@ pub fn move_vertically(
new_range
}

/// Implements anchored vertical movement.
///
/// Anchored movement behaves differently depending on where the cursor is placed with
/// regards to newlines. If the cursor is currently on a newline, then moving up and
/// down will place you again on the newline of that line.
///
/// If you are not on the newline, then moving up and down will move you to the same
/// column except if that column is further to the left from where you are. In that case
/// it places you on the rightmost column that is not a newline.
pub fn move_vertically_anchored(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have some tests for both of these functions?

slice: RopeSlice,
range: Range,
dir: Direction,
count: usize,
behaviour: Movement,
tab_width: usize,
) -> Range {
let pos = range.cursor(slice);
let line = slice.char_to_line(pos);

// Compute the current position's 2d coordinates.
let Position { row, col } = visual_coords_at_pos(slice, pos, tab_width);
let horiz = range.horiz.unwrap_or(col as u32);

// if we are resting on a newline character and we did not just move
// across an empty line we are in newline movement mode. To disambiugate these
// cases we take advantage that anchored movement sets the horizontal column
// to the largest possible integer.
let newline_move_mode = pos == line_end_char_index(&slice, line)
&& (horiz == u32::MAX || !rope_is_line_ending(slice.line(line)));

// Compute the new position.
let new_row = match dir {
Direction::Forward => (row + count).min(slice.len_lines().saturating_sub(1)),
Direction::Backward => row.saturating_sub(count),
};

// if we are already on a newline character, we want to navigate to the
// newline character on the new line we moved to.
let (new_pos, new_horiz) = if newline_move_mode {
let new_pos = line_end_char_index(
&slice,
slice.char_to_line(pos_at_visual_coords(
slice,
Position::new(new_row, col),
tab_width,
)),
);

// when in newline_move_mode we set the horizontal column to the largest
// possible integer to be able to disambiugate movements across empty lines.
(new_pos, u32::MAX)

// otherwise move up and down like normal but ensure to never place the
// cursor on the newline character.
} else {
let new_col = col.max(horiz as usize);
let mut new_pos = pos_at_visual_coords(slice, Position::new(new_row, new_col), tab_width);

// Special-case to avoid moving to the end of the last non-empty line.
if behaviour == Movement::Extend && slice.line(new_row).len_chars() == 0 {
return range;
}

// Move away from the newline character.
let end_index = line_end_char_index(&slice, new_row);
if new_pos == end_index && !rope_is_line_ending(slice.line(new_row)) {
new_pos = prev_grapheme_boundary(slice, end_index);
}

(new_pos, horiz)
};

let mut new_range = range.put_cursor(slice, new_pos, behaviour == Movement::Extend);
new_range.horiz = Some(new_horiz);
new_range
}

pub fn move_next_word_start(slice: RopeSlice, range: Range, count: usize) -> Range {
word_move(slice, range, count, WordMotionTarget::NextWordStart)
}
Expand Down
84 changes: 83 additions & 1 deletion helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,13 +197,21 @@ impl MappableCommand {
static_commands!(
no_op, "Do nothing",
move_char_left, "Move left",
move_char_left_same_line, "Move left within same line only",
move_char_right, "Move right",
move_char_right_same_line, "Move right within same line only",
move_line_up, "Move up",
move_line_up_anchored, "Move up with newline anchoring behavior",
move_line_down, "Move down",
move_line_down_anchored, "Move down with newline anchoring behavior",
extend_char_left, "Extend left",
extend_char_left_same_line, "Extend left within same line only",
extend_char_right, "Extend right",
extend_char_right_same_line, "Extend left within same line only",
extend_line_up, "Extend up",
extend_line_up_anchored, "Extend up with newline anchoring behavior",
extend_line_down, "Extend down",
extend_line_down_anchored, "Extend down with newline anchoring behavior",
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",
Expand Down Expand Up @@ -534,40 +542,114 @@ where
doc.set_selection(view.id, selection);
}

use helix_core::movement::{move_horizontally, move_vertically};
use helix_core::movement::{
move_horizontally, move_horizontally_same_line, move_vertically, move_vertically_anchored,
};

fn move_char_left(cx: &mut Context) {
move_impl(cx, move_horizontally, Direction::Backward, Movement::Move)
}

fn move_char_left_same_line(cx: &mut Context) {
move_impl(
cx,
move_horizontally_same_line,
Direction::Backward,
Movement::Move,
)
}

fn move_char_right(cx: &mut Context) {
move_impl(cx, move_horizontally, Direction::Forward, Movement::Move)
}

fn move_char_right_same_line(cx: &mut Context) {
move_impl(
cx,
move_horizontally_same_line,
Direction::Forward,
Movement::Move,
)
}

fn move_line_up(cx: &mut Context) {
move_impl(cx, move_vertically, Direction::Backward, Movement::Move)
}

fn move_line_up_anchored(cx: &mut Context) {
move_impl(
cx,
move_vertically_anchored,
Direction::Backward,
Movement::Move,
)
}

fn move_line_down(cx: &mut Context) {
move_impl(cx, move_vertically, Direction::Forward, Movement::Move)
}

fn move_line_down_anchored(cx: &mut Context) {
move_impl(
cx,
move_vertically_anchored,
Direction::Forward,
Movement::Move,
)
}

fn extend_char_left(cx: &mut Context) {
move_impl(cx, move_horizontally, Direction::Backward, Movement::Extend)
}

fn extend_char_left_same_line(cx: &mut Context) {
move_impl(
cx,
move_horizontally_same_line,
Direction::Backward,
Movement::Extend,
)
}

fn extend_char_right(cx: &mut Context) {
move_impl(cx, move_horizontally, Direction::Forward, Movement::Extend)
}

fn extend_char_right_same_line(cx: &mut Context) {
move_impl(
cx,
move_horizontally_same_line,
Direction::Forward,
Movement::Extend,
)
}

fn extend_line_up(cx: &mut Context) {
move_impl(cx, move_vertically, Direction::Backward, Movement::Extend)
}

fn extend_line_up_anchored(cx: &mut Context) {
move_impl(
cx,
move_vertically_anchored,
Direction::Backward,
Movement::Extend,
)
}

fn extend_line_down(cx: &mut Context) {
move_impl(cx, move_vertically, Direction::Forward, Movement::Extend)
}

fn extend_line_down_anchored(cx: &mut Context) {
move_impl(
cx,
move_vertically_anchored,
Direction::Forward,
Movement::Extend,
)
}

fn goto_line_end_impl(view: &mut View, doc: &mut Document, movement: Movement) {
let text = doc.text().slice(..);

Expand Down