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 | 451 |
1 files changed, 22 insertions, 429 deletions
diff --git a/helix-core/src/movement.rs b/helix-core/src/movement.rs index 09a99db2..54eb02fd 100644 --- a/helix-core/src/movement.rs +++ b/helix-core/src/movement.rs @@ -1,6 +1,7 @@ -use std::{borrow::Cow, cmp::Reverse, iter}; +use std::{cmp::Reverse, iter}; use ropey::iter::Chars; +use tree_sitter::{Node, QueryCursor}; use crate::{ char_idx_at_visual_offset, @@ -12,10 +13,9 @@ use crate::{ }, line_ending::rope_is_line_ending, position::char_idx_at_visual_block_offset, - syntax, + syntax::LanguageConfiguration, text_annotations::TextAnnotations, textobject::TextObject, - tree_sitter::Node, visual_offset_from_block, Range, RopeSlice, Selection, Syntax, }; @@ -79,19 +79,19 @@ pub fn move_vertically_visual( Direction::Backward => -(count as isize), }; + // TODO how to handle inline annotations that span an entire visual line (very unlikely). + // 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( + let new_pos = 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; - } + ) + .0; // 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 { @@ -197,31 +197,13 @@ pub fn move_prev_long_word_end(slice: RopeSlice, range: Range, count: usize) -> 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) -} - fn word_move(slice: RopeSlice, range: Range, count: usize, target: WordMotionTarget) -> Range { let is_prev = matches!( target, WordMotionTarget::PrevWordStart | WordMotionTarget::PrevLongWordStart - | WordMotionTarget::PrevSubWordStart | WordMotionTarget::PrevWordEnd | WordMotionTarget::PrevLongWordEnd - | WordMotionTarget::PrevSubWordEnd ); // Special-case early-out. @@ -401,12 +383,6 @@ pub enum WordMotionTarget { 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 +398,8 @@ 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 +476,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 => { @@ -539,44 +494,26 @@ 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, + 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 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,6 +521,7 @@ pub fn goto_treesitter_object( ], slice_tree, slice, + &mut cursor, )?; let node = match dir { @@ -618,15 +556,14 @@ pub fn goto_treesitter_object( last_range } -fn find_parent_start<'tree>(node: &Node<'tree>) -> Option<Node<'tree>> { +fn find_parent_start(mut node: Node) -> Option<Node> { let start = node.start_byte(); - let mut node = Cow::Borrowed(node); while node.start_byte() >= start || !node.is_named() { - node = Cow::Owned(node.parent()?); + node = node.parent()?; } - Some(node.into_owned()) + Some(node) } pub fn move_parent_node_end( @@ -637,8 +574,8 @@ pub fn move_parent_node_end( 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 start_from = text.char_to_byte(range.from()); + let start_to = text.char_to_byte(range.to()); let mut node = match syntax.named_descendant_for_byte_range(start_from, start_to) { Some(node) => node, @@ -656,18 +593,18 @@ pub fn move_parent_node_end( // 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), + Direction::Forward => text.byte_to_char(node.end_byte()), // 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); + let end_head = text.byte_to_char(node.start_byte()); // 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) + node = find_parent_start(node).unwrap_or(node); + text.byte_to_char(node.start_byte()) } else { end_head } @@ -1076,178 +1013,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 +1182,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 +1445,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", |