Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-core/src/textobject.rs')
-rw-r--r--helix-core/src/textobject.rs262
1 files changed, 22 insertions, 240 deletions
diff --git a/helix-core/src/textobject.rs b/helix-core/src/textobject.rs
index 008228f4..975ed115 100644
--- a/helix-core/src/textobject.rs
+++ b/helix-core/src/textobject.rs
@@ -1,16 +1,16 @@
use std::fmt::Display;
use ropey::RopeSlice;
+use tree_sitter::{Node, QueryCursor};
use crate::chars::{categorize_char, char_is_whitespace, CharCategory};
-use crate::graphemes::{next_grapheme_boundary, prev_grapheme_boundary};
-use crate::line_ending::rope_is_line_ending;
+use crate::graphemes::next_grapheme_boundary;
use crate::movement::Direction;
-use crate::syntax;
+use crate::surround;
+use crate::syntax::LanguageConfiguration;
use crate::Range;
-use crate::{surround, Syntax};
-fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction, long: bool) -> usize {
+fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction) -> usize {
use CharCategory::{Eol, Whitespace};
let iter = match direction {
@@ -33,7 +33,7 @@ fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction, lo
match categorize_char(ch) {
Eol | Whitespace => return pos,
category => {
- if !long && category != prev_category && pos != 0 && pos != slice.len_chars() {
+ if category != prev_category && pos != 0 && pos != slice.len_chars() {
return pos;
} else {
match direction {
@@ -53,8 +53,6 @@ fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction, lo
pub enum TextObject {
Around,
Inside,
- /// Used for moving between objects.
- Movement,
}
impl Display for TextObject {
@@ -62,7 +60,6 @@ impl Display for TextObject {
f.write_str(match self {
Self::Around => "around",
Self::Inside => "inside",
- Self::Movement => "movement",
})
}
}
@@ -73,14 +70,13 @@ pub fn textobject_word(
range: Range,
textobject: TextObject,
_count: usize,
- long: bool,
) -> Range {
let pos = range.cursor(slice);
- let word_start = find_word_boundary(slice, pos, Direction::Backward, long);
+ let word_start = find_word_boundary(slice, pos, Direction::Backward);
let word_end = match slice.get_char(pos).map(categorize_char) {
None | Some(CharCategory::Whitespace | CharCategory::Eol) => pos,
- _ => find_word_boundary(slice, pos + 1, Direction::Forward, long),
+ _ => find_word_boundary(slice, pos + 1, Direction::Forward),
};
// Special case.
@@ -107,146 +103,20 @@ pub fn textobject_word(
Range::new(word_start - whitespace_count_left, word_end)
}
}
- TextObject::Movement => unreachable!(),
}
}
-pub fn textobject_paragraph(
- slice: RopeSlice,
- range: Range,
- textobject: TextObject,
- count: usize,
-) -> Range {
- let mut line = range.cursor_line(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 next_line_empty = rope_is_line_ending(slice.line(line.saturating_sub(1)));
- let last_char =
- prev_grapheme_boundary(slice, slice.line_to_char(line + 1)) == range.cursor(slice);
- let prev_empty_to_line = prev_line_empty && !curr_line_empty;
- let curr_empty_to_line = curr_line_empty && !next_line_empty;
-
- // skip character before paragraph boundary
- let mut line_back = line; // line but backwards
- if prev_empty_to_line || curr_empty_to_line {
- line_back += 1;
- }
- // do not include current paragraph on paragraph end (include next)
- if !(curr_empty_to_line && last_char) {
- let mut lines = slice.lines_at(line_back);
- lines.reverse();
- let mut lines = lines.map(rope_is_line_ending).peekable();
- while lines.next_if(|&e| e).is_some() {
- line_back -= 1;
- }
- while lines.next_if(|&e| !e).is_some() {
- line_back -= 1;
- }
- }
-
- // 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 count_done = 0; // count how many non-whitespace paragraphs done
- for _ in 0..count {
- let mut done = false;
- while lines.next_if(|&e| !e).is_some() {
- line += 1;
- done = true;
- }
- while lines.next_if(|&e| e).is_some() {
- line += 1;
- }
- count_done += done as usize;
- }
-
- // search one paragraph backwards for last paragraph
- // makes `map` at the end of the paragraph with trailing newlines useful
- let last_paragraph = count_done != count && lines.peek().is_none();
- if last_paragraph {
- let mut lines = slice.lines_at(line_back);
- lines.reverse();
- let mut lines = lines.map(rope_is_line_ending).peekable();
- while lines.next_if(|&e| e).is_some() {
- line_back -= 1;
- }
- while lines.next_if(|&e| !e).is_some() {
- line_back -= 1;
- }
- }
-
- // handle last whitespaces part separately depending on textobject
- match textobject {
- TextObject::Around => {}
- TextObject::Inside => {
- // remove last whitespace paragraph
- let mut lines = slice.lines_at(line);
- lines.reverse();
- let mut lines = lines.map(rope_is_line_ending).peekable();
- while lines.next_if(|&e| e).is_some() {
- line -= 1;
- }
- }
- TextObject::Movement => unreachable!(),
- }
-
- let anchor = slice.line_to_char(line_back);
- let head = slice.line_to_char(line);
- Range::new(anchor, head)
-}
-
-pub fn textobject_pair_surround(
- syntax: Option<&Syntax>,
+pub fn textobject_surround(
slice: RopeSlice,
range: Range,
textobject: TextObject,
ch: char,
count: usize,
) -> Range {
- textobject_pair_surround_impl(syntax, slice, range, textobject, Some(ch), count)
-}
-
-pub fn textobject_pair_surround_closest(
- syntax: Option<&Syntax>,
- slice: RopeSlice,
- range: Range,
- textobject: TextObject,
- count: usize,
-) -> Range {
- textobject_pair_surround_impl(syntax, slice, range, textobject, None, count)
-}
-
-fn textobject_pair_surround_impl(
- syntax: Option<&Syntax>,
- slice: RopeSlice,
- range: Range,
- textobject: TextObject,
- ch: Option<char>,
- count: usize,
-) -> Range {
- let pair_pos = match ch {
- Some(ch) => surround::find_nth_pairs_pos(slice, ch, range, count),
- None => surround::find_nth_closest_pairs_pos(syntax, slice, range, count),
- };
- pair_pos
+ surround::find_nth_pairs_pos(slice, ch, range.head, count)
.map(|(anchor, head)| match textobject {
- TextObject::Inside => {
- if anchor < head {
- Range::new(next_grapheme_boundary(slice, anchor), head)
- } else {
- Range::new(anchor, next_grapheme_boundary(slice, head))
- }
- }
- TextObject::Around => {
- if anchor < head {
- Range::new(anchor, next_grapheme_boundary(slice, head))
- } else {
- Range::new(next_grapheme_boundary(slice, anchor), head)
- }
- }
- TextObject::Movement => unreachable!(),
+ TextObject::Inside => Range::new(next_grapheme_boundary(slice, anchor), head),
+ TextObject::Around => Range::new(anchor, next_grapheme_boundary(slice, head)),
})
.unwrap_or(range)
}
@@ -259,18 +129,18 @@ pub fn textobject_treesitter(
range: Range,
textobject: TextObject,
object_name: &str,
- syntax: &Syntax,
- loader: &syntax::Loader,
+ slice_tree: Node,
+ lang_config: &LanguageConfiguration,
_count: usize,
) -> Range {
- let root = syntax.tree().root_node();
- let textobject_query = loader.textobject_query(syntax.root_language());
let get_range = move || -> Option<Range> {
let byte_pos = slice.char_to_byte(range.cursor(slice));
let capture_name = format!("{}.{}", object_name, textobject); // eg. function.inner
- let node = textobject_query?
- .capture_nodes(&capture_name, &root, slice)?
+ let mut cursor = QueryCursor::new();
+ let node = lang_config
+ .textobject_query()?
+ .capture_nodes(&capture_name, slice_tree, slice, &mut cursor)?
.filter(|node| node.byte_range().contains(&byte_pos))
.min_by_key(|node| node.byte_range().len())?;
@@ -299,7 +169,7 @@ mod test {
#[test]
fn test_textobject_word() {
- // (text, [(char position, textobject, final range), ...])
+ // (text, [(cursor position, textobject, final range), ...])
let tests = &[
(
"cursor at beginning of doc",
@@ -398,9 +268,7 @@ mod test {
let slice = doc.slice(..);
for &case in scenario {
let (pos, objtype, expected_range) = case;
- // cursor is a single width selection
- let range = Range::new(pos, pos + 1);
- let result = textobject_word(slice, range, objtype, 1, false);
+ let result = textobject_word(slice, Range::point(pos), objtype, 1);
assert_eq!(
result,
expected_range.into(),
@@ -413,93 +281,8 @@ mod test {
}
#[test]
- fn test_textobject_paragraph_inside_single() {
- let tests = [
- ("#[|]#", "#[|]#"),
- ("firs#[t|]#\n\nparagraph\n\n", "#[first\n|]#\nparagraph\n\n"),
- (
- "second\n\npa#[r|]#agraph\n\n",
- "second\n\n#[paragraph\n|]#\n",
- ),
- ("#[f|]#irst char\n\n", "#[first char\n|]#\n"),
- ("last char\n#[\n|]#", "#[last char\n|]#\n"),
- (
- "empty to line\n#[\n|]#paragraph boundary\n\n",
- "empty to line\n\n#[paragraph boundary\n|]#\n",
- ),
- (
- "line to empty\n\n#[p|]#aragraph boundary\n\n",
- "line to empty\n\n#[paragraph boundary\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| textobject_paragraph(text.slice(..), r, TextObject::Inside, 1));
- let actual = crate::test::plain(s.as_ref(), &selection);
- assert_eq!(actual, expected, "\nbefore: `{:?}`", before);
- }
- }
-
- #[test]
- fn test_textobject_paragraph_inside_double() {
- let tests = [
- (
- "last two\n\n#[p|]#aragraph\n\nwithout whitespaces\n\n",
- "last two\n\n#[paragraph\n\nwithout whitespaces\n|]#\n",
- ),
- (
- "last two\n#[\n|]#paragraph\n\nwithout whitespaces\n\n",
- "last two\n\n#[paragraph\n\nwithout whitespaces\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| textobject_paragraph(text.slice(..), r, TextObject::Inside, 2));
- let actual = crate::test::plain(s.as_ref(), &selection);
- assert_eq!(actual, expected, "\nbefore: `{:?}`", before);
- }
- }
-
- #[test]
- fn test_textobject_paragraph_around_single() {
- let tests = [
- ("#[|]#", "#[|]#"),
- ("firs#[t|]#\n\nparagraph\n\n", "#[first\n\n|]#paragraph\n\n"),
- (
- "second\n\npa#[r|]#agraph\n\n",
- "second\n\n#[paragraph\n\n|]#",
- ),
- ("#[f|]#irst char\n\n", "#[first char\n\n|]#"),
- ("last char\n#[\n|]#", "#[last char\n\n|]#"),
- (
- "empty to line\n#[\n|]#paragraph boundary\n\n",
- "empty to line\n\n#[paragraph boundary\n\n|]#",
- ),
- (
- "line to empty\n\n#[p|]#aragraph boundary\n\n",
- "line to empty\n\n#[paragraph boundary\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| textobject_paragraph(text.slice(..), r, TextObject::Around, 1));
- let actual = crate::test::plain(s.as_ref(), &selection);
- assert_eq!(actual, expected, "\nbefore: `{:?}`", before);
- }
- }
-
- #[test]
fn test_textobject_surround() {
- // (text, [(cursor position, textobject, final range, surround char, count), ...])
+ // (text, [(cursor position, textobject, final range, count), ...])
let tests = &[
(
"simple (single) surround pairs",
@@ -575,8 +358,7 @@ mod test {
let slice = doc.slice(..);
for &case in scenario {
let (pos, objtype, expected_range, ch, count) = case;
- let result =
- textobject_pair_surround(None, slice, Range::point(pos), objtype, ch, count);
+ let result = textobject_surround(slice, Range::point(pos), objtype, ch, count);
assert_eq!(
result,
expected_range.into(),