diff --git a/helix-core/src/movement.rs b/helix-core/src/movement.rs index e446d8cc4..0c2c7048a 100644 --- a/helix-core/src/movement.rs +++ b/helix-core/src/movement.rs @@ -52,6 +52,40 @@ 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, + text_fmt: &TextFormat, + annotations: &mut TextAnnotations, +) -> Range { + let pos = range.cursor(slice); + let line = slice.char_to_line(pos); + + let new_range = move_horizontally(slice, range, dir, count, behaviour, text_fmt, annotations); + let new_pos = new_range.cursor(slice); + let new_line = slice.char_to_line(new_pos); + + match new_line.cmp(&line) { + std::cmp::Ordering::Equal => { + // we'll end up in same line - move there + new_range + } + std::cmp::Ordering::Less => { + // we'll end up in a line before - move to the beginning of the line + let line_beginning = slice.line_to_char(line); + range.put_cursor(slice, line_beginning, behaviour == Movement::Extend) + } + std::cmp::Ordering::Greater => { + // we'll end up in a line after - move to the end of the line + let line_end = line_end_char_index(&slice, line); + range.put_cursor(slice, line_end, behaviour == Movement::Extend) + } + } +} + pub fn move_vertically_visual( slice: RopeSlice, range: Range, @@ -738,6 +772,37 @@ mod test { ); } + #[test] + fn horizontal_movement_in_same_line() { + let text = Rope::from("a\na\naaaa"); + let slice = text.slice(..); + let pos = pos_at_coords(slice, (1, 0).into(), true); + let mut range = Range::new(pos, pos); + + let hmove = |range, direction, count| -> Range { + move_horizontally_same_line( + slice, + range, + direction, + count, + Movement::Move, + &TextFormat::default(), + &mut TextAnnotations::default(), + ) + }; + + range = hmove(range, Direction::Backward, 1); + assert_eq!(coords_at_pos(slice, range.head), (1, 0).into()); + range = hmove(range, Direction::Forward, 2); + assert_eq!(coords_at_pos(slice, range.head), (1, 1).into()); + range = hmove(range, Direction::Forward, 2); + assert_eq!(coords_at_pos(slice, range.head), (1, 1).into()); + range = hmove(range, Direction::Backward, 1); + assert_eq!(coords_at_pos(slice, range.head), (1, 0).into()); + range = hmove(range, Direction::Backward, 2); + assert_eq!(coords_at_pos(slice, range.head), (1, 0).into()); + } + #[test] fn horizontal_moves_through_single_line_text() { let text = Rope::from(SINGLE_LINE_SAMPLE); diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 61855d356..850247fb8 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -26,7 +26,7 @@ use helix_core::{ indent::{self, IndentStyle}, line_ending::{get_line_ending_of_str, line_end_char_index}, match_brackets, - movement::{self, move_vertically_visual, Direction}, + movement::{self, Direction, Movement}, object, pos_at_coords, regex::{self, Regex}, search::{self, CharMatcher}, @@ -52,7 +52,6 @@ use helix_view::{ use anyhow::{anyhow, bail, ensure, Context as _}; use insert::*; -use movement::Movement; use crate::{ args, @@ -279,12 +278,16 @@ impl MappableCommand { no_op, "Do nothing", move_char_left, "Move left", move_char_right, "Move right", + move_same_line_char_left, "Move left within in the same line only", + move_same_line_char_right, "Move right within in the same line only", move_line_up, "Move up", move_line_down, "Move down", move_visual_line_up, "Move up", move_visual_line_down, "Move down", 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_visual_line_up, "Extend up", @@ -371,6 +374,7 @@ impl MappableCommand { ensure_selections_forward, "Ensure all selections face forward", insert_mode, "Insert before selection", append_mode, "Append after selection", + append_mode_same_line, "Append after selection within the same line only", command_mode, "Enter command mode", file_picker, "Open file picker", file_picker_in_current_buffer_directory, "Open file picker at current buffer's directory", @@ -696,7 +700,9 @@ fn move_impl(cx: &mut Context, move_fn: MoveFn, dir: Direction, behaviour: Movem 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_visual, +}; fn move_char_left(cx: &mut Context) { move_impl(cx, move_horizontally, Direction::Backward, Movement::Move) @@ -706,6 +712,24 @@ fn move_char_right(cx: &mut Context) { move_impl(cx, move_horizontally, Direction::Forward, Movement::Move) } +fn move_same_line_char_left(cx: &mut Context) { + move_impl( + cx, + move_horizontally_same_line, + Direction::Backward, + Movement::Move, + ) +} + +fn move_same_line_char_right(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) } @@ -740,6 +764,24 @@ fn extend_char_right(cx: &mut Context) { move_impl(cx, move_horizontally, Direction::Forward, Movement::Extend) } +fn extend_same_line_char_left(cx: &mut Context) { + move_impl( + cx, + move_horizontally_same_line, + Direction::Backward, + Movement::Extend, + ) +} + +fn extend_same_line_char_right(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) } @@ -2912,6 +2954,30 @@ fn append_mode(cx: &mut Context) { doc.set_selection(view.id, selection); } +fn append_mode_same_line(cx: &mut Context) { + enter_insert_mode(cx); + let (view, doc) = current!(cx.editor); + doc.restore_cursor = true; + let text = doc.text().slice(..); + + let selection = doc.selection(view.id).clone().transform(|range| { + let pos = range.cursor(text); + let line = text.char_to_line(pos); + let end_char_index = line_end_char_index(&text, line); + let pos_is_at_end_of_line = pos == end_char_index; + + if pos_is_at_end_of_line { + range + } else { + Range::new( + range.from(), + graphemes::next_grapheme_boundary(doc.text().slice(..), range.to()), + ) + } + }); + doc.set_selection(view.id, selection); +} + fn file_picker(cx: &mut Context) { let root = find_workspace().0; if !root.exists() {