Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-core/src/movement.rs')
| -rw-r--r-- | helix-core/src/movement.rs | 1117 |
1 files changed, 51 insertions, 1066 deletions
diff --git a/helix-core/src/movement.rs b/helix-core/src/movement.rs index 09a99db2..e559f1ea 100644 --- a/helix-core/src/movement.rs +++ b/helix-core/src/movement.rs @@ -1,22 +1,19 @@ -use std::{borrow::Cow, cmp::Reverse, iter}; +use std::iter; use ropey::iter::Chars; +use tree_sitter::{Node, QueryCursor}; use crate::{ - char_idx_at_visual_offset, chars::{categorize_char, char_is_line_ending, CharCategory}, - doc_formatter::TextFormat, + coords_at_pos, graphemes::{ next_grapheme_boundary, nth_next_grapheme_boundary, nth_prev_grapheme_boundary, prev_grapheme_boundary, }, - line_ending::rope_is_line_ending, - position::char_idx_at_visual_block_offset, - syntax, - text_annotations::TextAnnotations, + pos_at_coords, + syntax::LanguageConfiguration, textobject::TextObject, - tree_sitter::Node, - visual_offset_from_block, Range, RopeSlice, Selection, Syntax, + Position, Range, RopeSlice, }; #[derive(Debug, Copy, Clone, PartialEq, Eq)] @@ -37,8 +34,6 @@ pub fn move_horizontally( dir: Direction, count: usize, behaviour: Movement, - _: &TextFormat, - _: &mut TextAnnotations, ) -> Range { let pos = range.cursor(slice); @@ -52,116 +47,38 @@ pub fn move_horizontally( range.put_cursor(slice, new_pos, behaviour == Movement::Extend) } -pub fn move_vertically_visual( - slice: RopeSlice, - range: Range, - dir: Direction, - count: usize, - behaviour: Movement, - text_fmt: &TextFormat, - annotations: &mut TextAnnotations, -) -> Range { - if !text_fmt.soft_wrap { - return move_vertically(slice, range, dir, count, behaviour, text_fmt, annotations); - } - annotations.clear_line_annotations(); - let pos = range.cursor(slice); - - // Compute the current position's 2d coordinates. - let (visual_pos, block_off) = visual_offset_from_block(slice, pos, pos, text_fmt, annotations); - let new_col = range - .old_visual_position - .map_or(visual_pos.col as u32, |(_, col)| col); - - // Compute the new position. - let mut row_off = match dir { - Direction::Forward => count as isize, - Direction::Backward => -(count as isize), - }; - - // Compute visual offset relative to block start to avoid trasversing the block twice - row_off += visual_pos.row as isize; - let (mut new_pos, virtual_rows) = char_idx_at_visual_offset( - slice, - block_off, - row_off, - new_col as usize, - text_fmt, - annotations, - ); - if dir == Direction::Forward { - new_pos += (virtual_rows != 0) as usize; - } - - // Special-case to avoid moving to the end of the last non-empty line. - if behaviour == Movement::Extend && slice.line(slice.char_to_line(new_pos)).len_chars() == 0 { - return range; - } - - let mut new_range = range.put_cursor(slice, new_pos, behaviour == Movement::Extend); - new_range.old_visual_position = Some((0, new_col)); - new_range -} - pub fn move_vertically( slice: RopeSlice, range: Range, dir: Direction, count: usize, behaviour: Movement, - text_fmt: &TextFormat, - annotations: &mut TextAnnotations, ) -> Range { - annotations.clear_line_annotations(); let pos = range.cursor(slice); - let line_idx = slice.char_to_line(pos); - let line_start = slice.line_to_char(line_idx); // Compute the current position's 2d coordinates. - let visual_pos = visual_offset_from_block(slice, line_start, pos, text_fmt, annotations).0; - let (mut new_row, new_col) = range - .old_visual_position - .map_or((visual_pos.row as u32, visual_pos.col as u32), |pos| pos); - new_row = new_row.max(visual_pos.row as u32); - let line_idx = slice.char_to_line(pos); + // TODO: switch this to use `visual_coords_at_pos` rather than + // `coords_at_pos` as this will cause a jerky movement when the visual + // position does not match, like moving from a line with tabs/CJK to + // a line without + let Position { row, col } = coords_at_pos(slice, pos); + let horiz = range.horiz.unwrap_or(col as u32); // Compute the new position. - let mut new_line_idx = match dir { - Direction::Forward => line_idx.saturating_add(count), - Direction::Backward => line_idx.saturating_sub(count), - }; - - let line = if new_line_idx >= slice.len_lines() - 1 { - // there is no line terminator for the last line - // so the logic below is not necessary here - new_line_idx = slice.len_lines() - 1; - slice - } else { - // char_idx_at_visual_block_offset returns a one-past-the-end index - // in case it reaches the end of the slice - // to avoid moving to the nextline in that case the line terminator is removed from the line - let new_line_end = prev_grapheme_boundary(slice, slice.line_to_char(new_line_idx + 1)); - slice.slice(..new_line_end) + let new_row = match dir { + Direction::Forward => (row + count).min(slice.len_lines().saturating_sub(1)), + Direction::Backward => row.saturating_sub(count), }; - - let new_line_start = line.line_to_char(new_line_idx); - - let (new_pos, _) = char_idx_at_visual_block_offset( - line, - new_line_start, - new_row as usize, - new_col as usize, - text_fmt, - annotations, - ); + let new_col = col.max(horiz as usize); + let new_pos = pos_at_coords(slice, Position::new(new_row, new_col), true); // Special-case to avoid moving to the end of the last non-empty line. - if behaviour == Movement::Extend && slice.line(new_line_idx).len_chars() == 0 { + if behaviour == Movement::Extend && slice.line(new_row).len_chars() == 0 { return range; } let mut new_range = range.put_cursor(slice, new_pos, behaviour == Movement::Extend); - new_range.old_visual_position = Some((new_row, new_col)); + new_range.horiz = Some(horiz); new_range } @@ -177,10 +94,6 @@ pub fn move_prev_word_start(slice: RopeSlice, range: Range, count: usize) -> Ran word_move(slice, range, count, WordMotionTarget::PrevWordStart) } -pub fn move_prev_word_end(slice: RopeSlice, range: Range, count: usize) -> Range { - word_move(slice, range, count, WordMotionTarget::PrevWordEnd) -} - pub fn move_next_long_word_start(slice: RopeSlice, range: Range, count: usize) -> Range { word_move(slice, range, count, WordMotionTarget::NextLongWordStart) } @@ -193,24 +106,8 @@ pub fn move_prev_long_word_start(slice: RopeSlice, range: Range, count: usize) - word_move(slice, range, count, WordMotionTarget::PrevLongWordStart) } -pub fn move_prev_long_word_end(slice: RopeSlice, range: Range, count: usize) -> Range { - word_move(slice, range, count, WordMotionTarget::PrevLongWordEnd) -} - -pub fn move_next_sub_word_start(slice: RopeSlice, range: Range, count: usize) -> Range { - word_move(slice, range, count, WordMotionTarget::NextSubWordStart) -} - -pub fn move_next_sub_word_end(slice: RopeSlice, range: Range, count: usize) -> Range { - word_move(slice, range, count, WordMotionTarget::NextSubWordEnd) -} - -pub fn move_prev_sub_word_start(slice: RopeSlice, range: Range, count: usize) -> Range { - word_move(slice, range, count, WordMotionTarget::PrevSubWordStart) -} - -pub fn move_prev_sub_word_end(slice: RopeSlice, range: Range, count: usize) -> Range { - word_move(slice, range, count, WordMotionTarget::PrevSubWordEnd) +pub fn move_prev_word_end(slice: RopeSlice, range: Range, count: usize) -> Range { + word_move(slice, range, count, WordMotionTarget::PrevWordEnd) } fn word_move(slice: RopeSlice, range: Range, count: usize, target: WordMotionTarget) -> Range { @@ -218,10 +115,7 @@ fn word_move(slice: RopeSlice, range: Range, count: usize, target: WordMotionTar target, WordMotionTarget::PrevWordStart | WordMotionTarget::PrevLongWordStart - | WordMotionTarget::PrevSubWordStart | WordMotionTarget::PrevWordEnd - | WordMotionTarget::PrevLongWordEnd - | WordMotionTarget::PrevSubWordEnd ); // Special-case early-out. @@ -250,107 +144,9 @@ fn word_move(slice: RopeSlice, range: Range, count: usize, target: WordMotionTar }; // Do the main work. - let mut range = start_range; - for _ in 0..count { - let next_range = slice.chars_at(range.head).range_to_target(target, range); - if range == next_range { - break; - } - range = next_range; - } - range -} - -pub fn move_prev_paragraph( - slice: RopeSlice, - range: Range, - count: usize, - behavior: Movement, -) -> Range { - let mut line = range.cursor_line(slice); - let first_char = slice.line_to_char(line) == range.cursor(slice); - let prev_line_empty = rope_is_line_ending(slice.line(line.saturating_sub(1))); - let curr_line_empty = rope_is_line_ending(slice.line(line)); - let prev_empty_to_line = prev_line_empty && !curr_line_empty; - - // skip character before paragraph boundary - if prev_empty_to_line && !first_char { - line += 1; - } - let mut lines = slice.lines_at(line); - lines.reverse(); - let mut lines = lines.map(rope_is_line_ending).peekable(); - let mut last_line = line; - for _ in 0..count { - while lines.next_if(|&e| e).is_some() { - line -= 1; - } - while lines.next_if(|&e| !e).is_some() { - line -= 1; - } - if line == last_line { - break; - } - last_line = line; - } - - let head = slice.line_to_char(line); - let anchor = if behavior == Movement::Move { - // exclude first character after paragraph boundary - if prev_empty_to_line && first_char { - range.cursor(slice) - } else { - range.head - } - } else { - range.put_cursor(slice, head, true).anchor - }; - Range::new(anchor, head) -} - -pub fn move_next_paragraph( - slice: RopeSlice, - range: Range, - count: usize, - behavior: Movement, -) -> Range { - let mut line = range.cursor_line(slice); - let last_char = - prev_grapheme_boundary(slice, slice.line_to_char(line + 1)) == range.cursor(slice); - let curr_line_empty = rope_is_line_ending(slice.line(line)); - let next_line_empty = - rope_is_line_ending(slice.line(slice.len_lines().saturating_sub(1).min(line + 1))); - let curr_empty_to_line = curr_line_empty && !next_line_empty; - - // skip character after paragraph boundary - if curr_empty_to_line && last_char { - line += 1; - } - let mut lines = slice.lines_at(line).map(rope_is_line_ending).peekable(); - let mut last_line = line; - for _ in 0..count { - while lines.next_if(|&e| !e).is_some() { - line += 1; - } - while lines.next_if(|&e| e).is_some() { - line += 1; - } - if line == last_line { - break; - } - last_line = line; - } - let head = slice.line_to_char(line); - let anchor = if behavior == Movement::Move { - if curr_empty_to_line && last_char { - range.head - } else { - range.cursor(slice) - } - } else { - range.put_cursor(slice, head, true).anchor - }; - Range::new(anchor, head) + (0..count).fold(start_range, |r, _| { + slice.chars_at(r.head).range_to_target(target, r) + }) } // ---- util ------------ @@ -394,19 +190,12 @@ pub enum WordMotionTarget { NextWordEnd, PrevWordStart, PrevWordEnd, - // A "Long word" (also known as a WORD in Vim/Kakoune) is strictly + // A "Long word" (also known as a WORD in vim/kakoune) is strictly // delimited by whitespace, and can consist of punctuation as well // as alphanumerics. NextLongWordStart, NextLongWordEnd, PrevLongWordStart, - PrevLongWordEnd, - // A sub word is similar to a regular word, except it is also delimited by - // underscores and transitions from lowercase to uppercase. - NextSubWordStart, - NextSubWordEnd, - PrevSubWordStart, - PrevSubWordEnd, } pub trait CharHelpers { @@ -422,10 +211,7 @@ impl CharHelpers for Chars<'_> { target, WordMotionTarget::PrevWordStart | WordMotionTarget::PrevLongWordStart - | WordMotionTarget::PrevSubWordStart | WordMotionTarget::PrevWordEnd - | WordMotionTarget::PrevLongWordEnd - | WordMotionTarget::PrevSubWordEnd ); // Reverse the iterator if needed for the motion direction. @@ -502,25 +288,6 @@ fn is_long_word_boundary(a: char, b: char) -> bool { } } -fn is_sub_word_boundary(a: char, b: char, dir: Direction) -> bool { - match (categorize_char(a), categorize_char(b)) { - (CharCategory::Word, CharCategory::Word) => { - if (a == '_') != (b == '_') { - return true; - } - - // Subword boundaries are directional: in 'fooBar', there is a - // boundary between 'o' and 'B', but not between 'B' and 'a'. - match dir { - Direction::Forward => a.is_lowercase() && b.is_uppercase(), - Direction::Backward => a.is_uppercase() && b.is_lowercase(), - } - } - (a, b) if a != b => true, - _ => false, - } -} - fn reached_target(target: WordMotionTarget, prev_ch: char, next_ch: char) -> bool { match target { WordMotionTarget::NextWordStart | WordMotionTarget::PrevWordEnd => { @@ -531,7 +298,7 @@ fn reached_target(target: WordMotionTarget, prev_ch: char, next_ch: char) -> boo is_word_boundary(prev_ch, next_ch) && (!prev_ch.is_whitespace() || char_is_line_ending(next_ch)) } - WordMotionTarget::NextLongWordStart | WordMotionTarget::PrevLongWordEnd => { + WordMotionTarget::NextLongWordStart => { is_long_word_boundary(prev_ch, next_ch) && (char_is_line_ending(next_ch) || !next_ch.is_whitespace()) } @@ -539,44 +306,24 @@ fn reached_target(target: WordMotionTarget, prev_ch: char, next_ch: char) -> boo is_long_word_boundary(prev_ch, next_ch) && (!prev_ch.is_whitespace() || char_is_line_ending(next_ch)) } - WordMotionTarget::NextSubWordStart => { - is_sub_word_boundary(prev_ch, next_ch, Direction::Forward) - && (char_is_line_ending(next_ch) || !(next_ch.is_whitespace() || next_ch == '_')) - } - WordMotionTarget::PrevSubWordEnd => { - is_sub_word_boundary(prev_ch, next_ch, Direction::Backward) - && (char_is_line_ending(next_ch) || !(next_ch.is_whitespace() || next_ch == '_')) - } - WordMotionTarget::NextSubWordEnd => { - is_sub_word_boundary(prev_ch, next_ch, Direction::Forward) - && (!(prev_ch.is_whitespace() || prev_ch == '_') || char_is_line_ending(next_ch)) - } - WordMotionTarget::PrevSubWordStart => { - is_sub_word_boundary(prev_ch, next_ch, Direction::Backward) - && (!(prev_ch.is_whitespace() || prev_ch == '_') || char_is_line_ending(next_ch)) - } } } -/// Finds the range of the next or previous textobject in the syntax sub-tree of `node`. -/// Returns the range in the forwards direction. -#[allow(clippy::too_many_arguments)] pub fn goto_treesitter_object( slice: RopeSlice, range: Range, object_name: &str, dir: Direction, - slice_tree: &Node, - syntax: &Syntax, - loader: &syntax::Loader, - count: usize, + slice_tree: Node, + lang_config: &LanguageConfiguration, + _count: usize, ) -> Range { - let textobject_query = loader.textobject_query(syntax.root_language()); - let get_range = move |range: Range| -> Option<Range> { + let get_range = move || -> Option<Range> { let byte_pos = slice.char_to_byte(range.cursor(slice)); let cap_name = |t: TextObject| format!("{}.{}", object_name, t); - let nodes = textobject_query?.capture_nodes_any( + let mut cursor = QueryCursor::new(); + let nodes = lang_config.textobject_query()?.capture_nodes_any( &[ &cap_name(TextObject::Movement), &cap_name(TextObject::Around), @@ -584,15 +331,16 @@ pub fn goto_treesitter_object( ], slice_tree, slice, + &mut cursor, )?; let node = match dir { Direction::Forward => nodes .filter(|n| n.start_byte() > byte_pos) - .min_by_key(|n| (n.start_byte(), Reverse(n.end_byte())))?, + .min_by_key(|n| n.start_byte())?, Direction::Backward => nodes - .filter(|n| n.end_byte() < byte_pos) - .max_by_key(|n| (n.end_byte(), Reverse(n.start_byte())))?, + .filter(|n| n.start_byte() < byte_pos) + .max_by_key(|n| n.start_byte())?, }; let len = slice.len_bytes(); @@ -606,99 +354,15 @@ pub fn goto_treesitter_object( let end_char = slice.byte_to_char(end_byte); // head of range should be at beginning - Some(Range::new(start_char, end_char)) + Some(Range::new(end_char, start_char)) }; - let mut last_range = range; - for _ in 0..count { - match get_range(last_range) { - Some(r) if r != last_range => last_range = r, - _ => break, - } - } - last_range -} - -fn find_parent_start<'tree>(node: &Node<'tree>) -> Option<Node<'tree>> { - let start = node.start_byte(); - let mut node = Cow::Borrowed(node); - - while node.start_byte() >= start || !node.is_named() { - node = Cow::Owned(node.parent()?); - } - - Some(node.into_owned()) -} - -pub fn move_parent_node_end( - syntax: &Syntax, - text: RopeSlice, - selection: Selection, - dir: Direction, - movement: Movement, -) -> Selection { - selection.transform(|range| { - let start_from = text.char_to_byte(range.from()) as u32; - let start_to = text.char_to_byte(range.to()) as u32; - - let mut node = match syntax.named_descendant_for_byte_range(start_from, start_to) { - Some(node) => node, - None => { - log::debug!( - "no descendant found for byte range: {} - {}", - start_from, - start_to - ); - return range; - } - }; - - let mut end_head = match dir { - // moving forward, we always want to move one past the end of the - // current node, so use the end byte of the current node, which is an exclusive - // end of the range - Direction::Forward => text.byte_to_char(node.end_byte() as usize), - - // moving backward, we want the cursor to land on the start char of - // the current node, or if it is already at the start of a node, to traverse up to - // the parent - Direction::Backward => { - let end_head = text.byte_to_char(node.start_byte() as usize); - - // if we're already on the beginning, look up to the parent - if end_head == range.cursor(text) { - node = find_parent_start(&node).unwrap_or(node); - text.byte_to_char(node.start_byte() as usize) - } else { - end_head - } - } - }; - - if movement == Movement::Move { - // preserve direction of original range - if range.direction() == Direction::Forward { - Range::new(end_head, end_head + 1) - } else { - Range::new(end_head + 1, end_head) - } - } else { - // if we end up with a forward range, then adjust it to be one past - // where we want - if end_head >= range.anchor { - end_head += 1; - } - - Range::new(range.anchor, end_head) - } - }) + get_range().unwrap_or(range) } #[cfg(test)] mod test { use ropey::Rope; - use crate::{coords_at_pos, pos_at_coords}; - use super::*; const SINGLE_LINE_SAMPLE: &str = "This is a simple alphabetic line"; @@ -725,16 +389,7 @@ mod test { assert_eq!( coords_at_pos( slice, - move_vertically_visual( - slice, - range, - Direction::Forward, - 1, - Movement::Move, - &TextFormat::default(), - &mut TextAnnotations::default(), - ) - .head + move_vertically(slice, range, Direction::Forward, 1, Movement::Move).head ), (1, 3).into() ); @@ -758,15 +413,7 @@ mod test { ]; for ((direction, amount), coordinates) in moves_and_expected_coordinates { - range = move_horizontally( - slice, - range, - direction, - amount, - Movement::Move, - &TextFormat::default(), - &mut TextAnnotations::default(), - ); + range = move_horizontally(slice, range, direction, amount, Movement::Move); assert_eq!(coords_at_pos(slice, range.head), coordinates.into()) } } @@ -792,15 +439,7 @@ mod test { ]; for ((direction, amount), coordinates) in moves_and_expected_coordinates { - range = move_horizontally( - slice, - range, - direction, - amount, - Movement::Move, - &TextFormat::default(), - &mut TextAnnotations::default(), - ); + range = move_horizontally(slice, range, direction, amount, Movement::Move); assert_eq!(coords_at_pos(slice, range.head), coordinates.into()); assert_eq!(range.head, range.anchor); } @@ -822,15 +461,7 @@ mod test { ]; for (direction, amount) in moves { - range = move_horizontally( - slice, - range, - direction, - amount, - Movement::Extend, - &TextFormat::default(), - &mut TextAnnotations::default(), - ); + range = move_horizontally(slice, range, direction, amount, Movement::Extend); assert_eq!(range.anchor, original_anchor); } } @@ -854,15 +485,7 @@ mod test { ]; for ((direction, amount), coordinates) in moves_and_expected_coordinates { - range = move_vertically_visual( - slice, - range, - direction, - amount, - Movement::Move, - &TextFormat::default(), - &mut TextAnnotations::default(), - ); + range = move_vertically(slice, range, direction, amount, Movement::Move); assert_eq!(coords_at_pos(slice, range.head), coordinates.into()); assert_eq!(range.head, range.anchor); } @@ -896,24 +519,8 @@ mod test { for ((axis, direction, amount), coordinates) in moves_and_expected_coordinates { range = match axis { - Axis::H => move_horizontally( - slice, - range, - direction, - amount, - Movement::Move, - &TextFormat::default(), - &mut TextAnnotations::default(), - ), - Axis::V => move_vertically_visual( - slice, - range, - direction, - amount, - Movement::Move, - &TextFormat::default(), - &mut TextAnnotations::default(), - ), + Axis::H => move_horizontally(slice, range, direction, amount, Movement::Move), + Axis::V => move_vertically(slice, range, direction, amount, Movement::Move), }; assert_eq!(coords_at_pos(slice, range.head), coordinates.into()); assert_eq!(range.head, range.anchor); @@ -937,34 +544,18 @@ mod test { let moves_and_expected_coordinates = [ // Places cursor at the fourth kana. ((Axis::H, Direction::Forward, 4), (0, 4)), - // Descent places cursor at the 8th character. - ((Axis::V, Direction::Forward, 1usize), (1, 8)), - // Moving back 2 characters. - ((Axis::H, Direction::Backward, 2usize), (1, 6)), + // Descent places cursor at the 4th character. + ((Axis::V, Direction::Forward, 1usize), (1, 4)), + // Moving back 1 character. + ((Axis::H, Direction::Backward, 1usize), (1, 3)), // Jumping back up 1 line. ((Axis::V, Direction::Backward, 1usize), (0, 3)), ]; for ((axis, direction, amount), coordinates) in moves_and_expected_coordinates { range = match axis { - Axis::H => move_horizontally( - slice, - range, - direction, - amount, - Movement::Move, - &TextFormat::default(), - &mut TextAnnotations::default(), - ), - Axis::V => move_vertically_visual( - slice, - range, - direction, - amount, - Movement::Move, - &TextFormat::default(), - &mut TextAnnotations::default(), - ), + Axis::H => move_horizontally(slice, range, direction, amount, Movement::Move), + Axis::V => move_vertically(slice, range, direction, amount, Movement::Move), }; assert_eq!(coords_at_pos(slice, range.head), coordinates.into()); assert_eq!(range.head, range.anchor); @@ -1076,178 +667,6 @@ mod test { } #[test] - fn test_behaviour_when_moving_to_start_of_next_sub_words() { - let tests = [ - ( - "NextSubwordStart", - vec![ - (1, Range::new(0, 0), Range::new(0, 4)), - (1, Range::new(4, 4), Range::new(4, 11)), - ], - ), - ( - "next_subword_start", - vec![ - (1, Range::new(0, 0), Range::new(0, 5)), - (1, Range::new(4, 4), Range::new(5, 13)), - ], - ), - ( - "Next_Subword_Start", - vec![ - (1, Range::new(0, 0), Range::new(0, 5)), - (1, Range::new(4, 4), Range::new(5, 13)), - ], - ), - ( - "NEXT_SUBWORD_START", - vec![ - (1, Range::new(0, 0), Range::new(0, 5)), - (1, Range::new(4, 4), Range::new(5, 13)), - ], - ), - ( - "next subword start", - vec![ - (1, Range::new(0, 0), Range::new(0, 5)), - (1, Range::new(4, 4), Range::new(5, 13)), - ], - ), - ( - "Next Subword Start", - vec![ - (1, Range::new(0, 0), Range::new(0, 5)), - (1, Range::new(4, 4), Range::new(5, 13)), - ], - ), - ( - "NEXT SUBWORD START", - vec![ - (1, Range::new(0, 0), Range::new(0, 5)), - (1, Range::new(4, 4), Range::new(5, 13)), - ], - ), - ( - "next__subword__start", - vec![ - (1, Range::new(0, 0), Range::new(0, 6)), - (1, Range::new(4, 4), Range::new(4, 6)), - (1, Range::new(5, 5), Range::new(6, 15)), - ], - ), - ( - "Next__Subword__Start", - vec![ - (1, Range::new(0, 0), Range::new(0, 6)), - (1, Range::new(4, 4), Range::new(4, 6)), - (1, Range::new(5, 5), Range::new(6, 15)), - ], - ), - ( - "NEXT__SUBWORD__START", - vec![ - (1, Range::new(0, 0), Range::new(0, 6)), - (1, Range::new(4, 4), Range::new(4, 6)), - (1, Range::new(5, 5), Range::new(6, 15)), - ], - ), - ]; - - for (sample, scenario) in tests { - for (count, begin, expected_end) in scenario.into_iter() { - let range = move_next_sub_word_start(Rope::from(sample).slice(..), begin, count); - assert_eq!(range, expected_end, "Case failed: [{}]", sample); - } - } - } - - #[test] - fn test_behaviour_when_moving_to_end_of_next_sub_words() { - let tests = [ - ( - "NextSubwordEnd", - vec![ - (1, Range::new(0, 0), Range::new(0, 4)), - (1, Range::new(4, 4), Range::new(4, 11)), - ], - ), - ( - "next subword end", - vec![ - (1, Range::new(0, 0), Range::new(0, 4)), - (1, Range::new(4, 4), Range::new(4, 12)), - ], - ), - ( - "Next Subword End", - vec![ - (1, Range::new(0, 0), Range::new(0, 4)), - (1, Range::new(4, 4), Range::new(4, 12)), - ], - ), - ( - "NEXT SUBWORD END", - vec![ - (1, Range::new(0, 0), Range::new(0, 4)), - (1, Range::new(4, 4), Range::new(4, 12)), - ], - ), - ( - "next_subword_end", - vec![ - (1, Range::new(0, 0), Range::new(0, 4)), - (1, Range::new(4, 4), Range::new(4, 12)), - ], - ), - ( - "Next_Subword_End", - vec![ - (1, Range::new(0, 0), Range::new(0, 4)), - (1, Range::new(4, 4), Range::new(4, 12)), - ], - ), - ( - "NEXT_SUBWORD_END", - vec![ - (1, Range::new(0, 0), Range::new(0, 4)), - (1, Range::new(4, 4), Range::new(4, 12)), - ], - ), - ( - "next__subword__end", - vec![ - (1, Range::new(0, 0), Range::new(0, 4)), - (1, Range::new(4, 4), Range::new(4, 13)), - (1, Range::new(5, 5), Range::new(5, 13)), - ], - ), - ( - "Next__Subword__End", - vec![ - (1, Range::new(0, 0), Range::new(0, 4)), - (1, Range::new(4, 4), Range::new(4, 13)), - (1, Range::new(5, 5), Range::new(5, 13)), - ], - ), - ( - "NEXT__SUBWORD__END", - vec![ - (1, Range::new(0, 0), Range::new(0, 4)), - (1, Range::new(4, 4), Range::new(4, 13)), - (1, Range::new(5, 5), Range::new(5, 13)), - ], - ), - ]; - - for (sample, scenario) in tests { - for (count, begin, expected_end) in scenario.into_iter() { - let range = move_next_sub_word_end(Rope::from(sample).slice(..), begin, count); - assert_eq!(range, expected_end, "Case failed: [{}]", sample); - } - } - } - - #[test] fn test_behaviour_when_moving_to_start_of_next_long_words() { let tests = [ ("Basic forward motion stops at the first space", @@ -1417,92 +836,6 @@ mod test { } #[test] - fn test_behaviour_when_moving_to_start_of_previous_sub_words() { - let tests = [ - ( - "PrevSubwordEnd", - vec![ - (1, Range::new(13, 13), Range::new(14, 11)), - (1, Range::new(11, 11), Range::new(11, 4)), - ], - ), - ( - "prev subword end", - vec![ - (1, Range::new(15, 15), Range::new(16, 13)), - (1, Range::new(12, 12), Range::new(13, 5)), - ], - ), - ( - "Prev Subword End", - vec![ - (1, Range::new(15, 15), Range::new(16, 13)), - (1, Range::new(12, 12), Range::new(13, 5)), - ], - ), - ( - "PREV SUBWORD END", - vec![ - (1, Range::new(15, 15), Range::new(16, 13)), - (1, Range::new(12, 12), Range::new(13, 5)), - ], - ), - ( - "prev_subword_end", - vec![ - (1, Range::new(15, 15), Range::new(16, 13)), - (1, Range::new(12, 12), Range::new(13, 5)), - ], - ), - ( - "Prev_Subword_End", - vec![ - (1, Range::new(15, 15), Range::new(16, 13)), - (1, Range::new(12, 12), Range::new(13, 5)), - ], - ), - ( - "PREV_SUBWORD_END", - vec![ - (1, Range::new(15, 15), Range::new(16, 13)), - (1, Range::new(12, 12), Range::new(13, 5)), - ], - ), - ( - "prev__subword__end", - vec![ - (1, Range::new(17, 17), Range::new(18, 15)), - (1, Range::new(13, 13), Range::new(14, 6)), - (1, Range::new(14, 14), Range::new(15, 6)), - ], - ), - ( - "Prev__Subword__End", - vec![ - (1, Range::new(17, 17), Range::new(18, 15)), - (1, Range::new(13, 13), Range::new(14, 6)), - (1, Range::new(14, 14), Range::new(15, 6)), - ], - ), - ( - "PREV__SUBWORD__END", - vec![ - (1, Range::new(17, 17), Range::new(18, 15)), - (1, Range::new(13, 13), Range::new(14, 6)), - (1, Range::new(14, 14), Range::new(15, 6)), - ], - ), - ]; - - for (sample, scenario) in tests { - for (count, begin, expected_end) in scenario.into_iter() { - let range = move_prev_sub_word_start(Rope::from(sample).slice(..), begin, count); - assert_eq!(range, expected_end, "Case failed: [{}]", sample); - } - } - } - - #[test] fn test_behaviour_when_moving_to_start_of_previous_long_words() { let tests = [ ( @@ -1766,92 +1099,6 @@ mod test { } #[test] - fn test_behaviour_when_moving_to_end_of_previous_sub_words() { - let tests = [ - ( - "PrevSubwordEnd", - vec![ - (1, Range::new(13, 13), Range::new(14, 11)), - (1, Range::new(11, 11), Range::new(11, 4)), - ], - ), - ( - "prev subword end", - vec![ - (1, Range::new(15, 15), Range::new(16, 12)), - (1, Range::new(12, 12), Range::new(12, 4)), - ], - ), - ( - "Prev Subword End", - vec![ - (1, Range::new(15, 15), Range::new(16, 12)), - (1, Range::new(12, 12), Range::new(12, 4)), - ], - ), - ( - "PREV SUBWORD END", - vec![ - (1, Range::new(15, 15), Range::new(16, 12)), - (1, Range::new(12, 12), Range::new(12, 4)), - ], - ), - ( - "prev_subword_end", - vec![ - (1, Range::new(15, 15), Range::new(16, 12)), - (1, Range::new(12, 12), Range::new(12, 4)), - ], - ), - ( - "Prev_Subword_End", - vec![ - (1, Range::new(15, 15), Range::new(16, 12)), - (1, Range::new(12, 12), Range::new(12, 4)), - ], - ), - ( - "PREV_SUBWORD_END", - vec![ - (1, Range::new(15, 15), Range::new(16, 12)), - (1, Range::new(12, 12), Range::new(12, 4)), - ], - ), - ( - "prev__subword__end", - vec![ - (1, Range::new(17, 17), Range::new(18, 13)), - (1, Range::new(13, 13), Range::new(13, 4)), - (1, Range::new(14, 14), Range::new(15, 13)), - ], - ), - ( - "Prev__Subword__End", - vec![ - (1, Range::new(17, 17), Range::new(18, 13)), - (1, Range::new(13, 13), Range::new(13, 4)), - (1, Range::new(14, 14), Range::new(15, 13)), - ], - ), - ( - "PREV__SUBWORD__END", - vec![ - (1, Range::new(17, 17), Range::new(18, 13)), - (1, Range::new(13, 13), Range::new(13, 4)), - (1, Range::new(14, 14), Range::new(15, 13)), - ], - ), - ]; - - for (sample, scenario) in tests { - for (count, begin, expected_end) in scenario.into_iter() { - let range = move_prev_sub_word_end(Rope::from(sample).slice(..), begin, count); - assert_eq!(range, expected_end, "Case failed: [{}]", sample); - } - } - } - - #[test] fn test_behaviour_when_moving_to_end_of_next_long_words() { let tests = [ ("Basic forward motion from the start of a word to the end of it", @@ -1932,266 +1179,4 @@ mod test { } } } - - #[test] - fn test_behaviour_when_moving_to_end_of_prev_long_words() { - let tests = [ - ( - "Basic backward motion from the middle of a word", - vec![(1, Range::new(3, 3), Range::new(4, 0))], - ), - ("Starting from after boundary retreats the anchor", - vec![(1, Range::new(0, 9), Range::new(8, 0))], - ), - ( - "Jump to end of a word succeeded by whitespace", - vec![(1, Range::new(10, 10), Range::new(10, 4))], - ), - ( - " Jump to start of line from end of word preceded by whitespace", - vec![(1, Range::new(3, 4), Range::new(4, 0))], - ), - ("Previous anchor is irrelevant for backward motions", - vec![(1, Range::new(12, 5), Range::new(6, 0))]), - ( - " Starting from whitespace moves to first space in sequence", - vec![(1, Range::new(0, 4), Range::new(4, 0))], - ), - ("Identifiers_with_underscores are considered a single word", - vec![(1, Range::new(0, 20), Range::new(20, 0))]), - ( - "Jumping\n \nback through a newline selects whitespace", - vec![(1, Range::new(0, 13), Range::new(12, 8))], - ), - ( - "Jumping to start of word from the end selects the word", - vec![(1, Range::new(6, 7), Range::new(7, 0))], - ), - ( - "alphanumeric.!,and.?=punctuation are treated exactly the same", - vec![(1, Range::new(29, 30), Range::new(30, 0))], - ), - ( - "... ... punctuation and spaces behave as expected", - vec![ - (1, Range::new(0, 10), Range::new(9, 3)), - (1, Range::new(10, 6), Range::new(7, 3)), - ], - ), - (".._.._ punctuation is joined by underscores into a single block", - vec![(1, Range::new(0, 6), Range::new(6, 0))]), - ( - "Newlines\n\nare bridged seamlessly.", - vec![(1, Range::new(0, 10), Range::new(8, 0))], - ), - ( - "Jumping \n\n\n\n\nback from within a newline group selects previous block", - vec![(1, Range::new(0, 13), Range::new(11, 7))], - ), - ( - "Failed motions do not modify the range", - vec![(0, Range::new(3, 0), Range::new(3, 0))], - ), - ( - "Multiple motions at once resolve correctly", - vec![(3, Range::new(19, 19), Range::new(8, 0))], - ), - ( - "Excessive motions are performed partially", - vec![(999, Range::new(40, 40), Range::new(9, 0))], - ), - ( - "", // Edge case of moving backwards in empty string - vec![(1, Range::new(0, 0), Range::new(0, 0))], - ), - ( - "\n\n\n\n\n", // Edge case of moving backwards in all newlines - vec![(1, Range::new(5, 5), Range::new(0, 0))], - ), - (" \n \nJumping back through alternated space blocks and newlines selects the space blocks", - vec![ - (1, Range::new(0, 8), Range::new(7, 4)), - (1, Range::new(7, 4), Range::new(3, 0)), - ]), - ("ヒーリ..クス multibyte characters behave as normal characters, including when interacting with punctuation", - vec![ - (1, Range::new(0, 8), Range::new(7, 0)), - ]), - ]; - - for (sample, scenario) in tests { - for (count, begin, expected_end) in scenario.into_iter() { - let range = move_prev_long_word_end(Rope::from(sample).slice(..), begin, count); - assert_eq!(range, expected_end, "Case failed: [{}]", sample); - } - } - } - - #[test] - fn test_behaviour_when_moving_to_prev_paragraph_single() { - let tests = [ - ("#[|]#", "#[|]#"), - ("#[s|]#tart at\nfirst char\n", "#[|s]#tart at\nfirst char\n"), - ("start at\nlast char#[\n|]#", "#[|start at\nlast char\n]#"), - ( - "goto\nfirst\n\n#[p|]#aragraph", - "#[|goto\nfirst\n\n]#paragraph", - ), - ( - "goto\nfirst\n#[\n|]#paragraph", - "#[|goto\nfirst\n\n]#paragraph", - ), - ( - "goto\nsecond\n\np#[a|]#ragraph", - "goto\nsecond\n\n#[|pa]#ragraph", - ), - ( - "here\n\nhave\nmultiple\nparagraph\n\n\n\n\n#[|]#", - "here\n\n#[|have\nmultiple\nparagraph\n\n\n\n\n]#", - ), - ]; - - for (before, expected) in tests { - let (s, selection) = crate::test::print(before); - let text = Rope::from(s.as_str()); - let selection = - selection.transform(|r| move_prev_paragraph(text.slice(..), r, 1, Movement::Move)); - let actual = crate::test::plain(s.as_ref(), &selection); - assert_eq!(actual, expected, "\nbefore: `{:?}`", before); - } - } - - #[test] - fn test_behaviour_when_moving_to_prev_paragraph_double() { - let tests = [ - ( - "on#[e|]#\n\ntwo\n\nthree\n\n", - "#[|one]#\n\ntwo\n\nthree\n\n", - ), - ( - "one\n\ntwo\n\nth#[r|]#ee\n\n", - "one\n\n#[|two\n\nthr]#ee\n\n", - ), - ]; - - for (before, expected) in tests { - let (s, selection) = crate::test::print(before); - let text = Rope::from(s.as_str()); - let selection = - selection.transform(|r| move_prev_paragraph(text.slice(..), r, 2, Movement::Move)); - let actual = crate::test::plain(s.as_ref(), &selection); - assert_eq!(actual, expected, "\nbefore: `{:?}`", before); - } - } - - #[test] - fn test_behaviour_when_moving_to_prev_paragraph_extend() { - let tests = [ - ( - "one\n\n#[|two\n\n]#three\n\n", - "#[|one\n\ntwo\n\n]#three\n\n", - ), - ( - "#[|one\n\ntwo\n\n]#three\n\n", - "#[|one\n\ntwo\n\n]#three\n\n", - ), - ]; - - for (before, expected) in tests { - let (s, selection) = crate::test::print(before); - let text = Rope::from(s.as_str()); - let selection = selection - .transform(|r| move_prev_paragraph(text.slice(..), r, 1, Movement::Extend)); - let actual = crate::test::plain(s.as_ref(), &selection); - assert_eq!(actual, expected, "\nbefore: `{:?}`", before); - } - } - - #[test] - fn test_behaviour_when_moving_to_next_paragraph_single() { - let tests = [ - ("#[|]#", "#[|]#"), - ("#[s|]#tart at\nfirst char\n", "#[start at\nfirst char\n|]#"), - ("start at\nlast char#[\n|]#", "start at\nlast char#[\n|]#"), - ( - "a\nb\n\n#[g|]#oto\nthird\n\nparagraph", - "a\nb\n\n#[goto\nthird\n\n|]#paragraph", - ), - ( - "a\nb\n#[\n|]#goto\nthird\n\nparagraph", - "a\nb\n\n#[goto\nthird\n\n|]#paragraph", - ), - ( - "a\nb#[\n|]#\n\ngoto\nsecond\n\nparagraph", - "a\nb#[\n\n|]#goto\nsecond\n\nparagraph", - ), - ( - "here\n\nhave\n#[m|]#ultiple\nparagraph\n\n\n\n\n", - "here\n\nhave\n#[multiple\nparagraph\n\n\n\n\n|]#", - ), - ( - "#[t|]#ext\n\n\nafter two blank lines\n\nmore text\n", - "#[text\n\n\n|]#after two blank lines\n\nmore text\n", - ), - ( - "#[text\n\n\n|]#after two blank lines\n\nmore text\n", - "text\n\n\n#[after two blank lines\n\n|]#more text\n", - ), - ]; - - for (before, expected) in tests { - let (s, selection) = crate::test::print(before); - let text = Rope::from(s.as_str()); - let selection = - selection.transform(|r| move_next_paragraph(text.slice(..), r, 1, Movement::Move)); - let actual = crate::test::plain(s.as_ref(), &selection); - assert_eq!(actual, expected, "\nbefore: `{:?}`", before); - } - } - - #[test] - fn test_behaviour_when_moving_to_next_paragraph_double() { - let tests = [ - ( - "one\n\ntwo\n\nth#[r|]#ee\n\n", - "one\n\ntwo\n\nth#[ree\n\n|]#", - ), - ( - "on#[e|]#\n\ntwo\n\nthree\n\n", - "on#[e\n\ntwo\n\n|]#three\n\n", - ), - ]; - - for (before, expected) in tests { - let (s, selection) = crate::test::print(before); - let text = Rope::from(s.as_str()); - let selection = - selection.transform(|r| move_next_paragraph(text.slice(..), r, 2, Movement::Move)); - let actual = crate::test::plain(s.as_ref(), &selection); - assert_eq!(actual, expected, "\nbefore: `{:?}`", before); - } - } - - #[test] - fn test_behaviour_when_moving_to_next_paragraph_extend() { - let tests = [ - ( - "one\n\n#[two\n\n|]#three\n\n", - "one\n\n#[two\n\nthree\n\n|]#", - ), - ( - "one\n\n#[two\n\nthree\n\n|]#", - "one\n\n#[two\n\nthree\n\n|]#", - ), - ]; - - for (before, expected) in tests { - let (s, selection) = crate::test::print(before); - let text = Rope::from(s.as_str()); - let selection = selection - .transform(|r| move_next_paragraph(text.slice(..), r, 1, Movement::Extend)); - let actual = crate::test::plain(s.as_ref(), &selection); - assert_eq!(actual, expected, "\nbefore: `{:?}`", before); - } - } } |