aboutsummaryrefslogtreecommitdiff
path: root/helix-core/src/textobject.rs
diff options
context:
space:
mode:
authorIvan Tham2022-02-21 14:58:54 +0000
committerBlaž Hrastnik2022-04-02 15:46:53 +0000
commit8350ee9a0ef38ff4e78bfa5dc30e874d6d2510ef (patch)
tree01c1486aaa420a8d98046619cde79d53b92cdb19 /helix-core/src/textobject.rs
parente2a6e33b98ac6dd1d42be659706c7e0d248e8f5d (diff)
Add paragraph textobject
Change parameter/argument key from p to a since paragraph only have p but parameter are also called arguments sometimes and a is not used.
Diffstat (limited to 'helix-core/src/textobject.rs')
-rw-r--r--helix-core/src/textobject.rs147
1 files changed, 146 insertions, 1 deletions
diff --git a/helix-core/src/textobject.rs b/helix-core/src/textobject.rs
index 5a55a6f1..fb6b7142 100644
--- a/helix-core/src/textobject.rs
+++ b/helix-core/src/textobject.rs
@@ -4,7 +4,8 @@ use ropey::RopeSlice;
use tree_sitter::{Node, QueryCursor};
use crate::chars::{categorize_char, char_is_whitespace, CharCategory};
-use crate::graphemes::next_grapheme_boundary;
+use crate::graphemes::{next_grapheme_boundary, prev_grapheme_boundary};
+use crate::line_ending::rope_is_line_ending;
use crate::movement::Direction;
use crate::surround;
use crate::syntax::LanguageConfiguration;
@@ -111,6 +112,71 @@ pub fn textobject_word(
}
}
+pub fn textobject_para(
+ 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;
+ }
+ let mut lines = slice.lines_at(line_back);
+ // do not include current paragraph on paragraph end (include next)
+ if !(curr_empty_to_line && last_char) {
+ 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();
+ for _ in 0..count - 1 {
+ while lines.next_if(|&e| !e).is_some() {
+ line += 1;
+ }
+ while lines.next_if(|&e| e).is_some() {
+ line += 1;
+ }
+ }
+ while lines.next_if(|&e| !e).is_some() {
+ line += 1;
+ }
+ // handle last whitespaces part separately depending on textobject
+ match textobject {
+ TextObject::Around => {
+ while lines.next_if(|&e| e).is_some() {
+ line += 1;
+ }
+ }
+ TextObject::Inside => {}
+ 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_surround(
slice: RopeSlice,
range: Range,
@@ -289,6 +355,85 @@ 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_para(text.slice(..), r, TextObject::Inside, 1));
+ let actual = crate::test::plain(&s, 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_para(text.slice(..), r, TextObject::Inside, 2));
+ let actual = crate::test::plain(&s, 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_para(text.slice(..), r, TextObject::Around, 1));
+ let actual = crate::test::plain(&s, selection);
+ assert_eq!(actual, expected, "\nbefore: `{before:?}`");
+ }
+ }
+
+ #[test]
fn test_textobject_surround() {
// (text, [(cursor position, textobject, final range, surround char, count), ...])
let tests = &[