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.
This commit is contained in:
Ivan Tham 2022-02-21 22:58:54 +08:00 committed by Blaž Hrastnik
parent e2a6e33b98
commit 8350ee9a0e
5 changed files with 162 additions and 14 deletions

View file

@ -277,6 +277,8 @@ Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaire
| `[a` | Go to previous argument/parameter (**TS**) | `goto_prev_parameter` |
| `]o` | Go to next comment (**TS**) | `goto_next_comment` |
| `[o` | Go to previous comment (**TS**) | `goto_prev_comment` |
| `]p` | Go to next paragraph | `goto_next_paragraph` |
| `[p` | Go to previous paragraph | `goto_prev_paragraph` |
| `[space` | Add newline above | `add_newline_above` |
| `]space` | Add newline below | `add_newline_below` |

View file

@ -153,12 +153,12 @@ fn word_move(slice: RopeSlice, range: Range, count: usize, target: WordMotionTar
pub fn move_prev_para(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 last_line_empty = rope_is_line_ending(slice.line(line.saturating_sub(1)));
let line_to_empty = last_line_empty && !curr_line_empty;
let prev_empty_to_line = prev_line_empty && !curr_line_empty;
// iterate current line if first character after paragraph boundary
if line_to_empty && !first_char {
// skip character before paragraph boundary
if prev_empty_to_line && !first_char {
line += 1;
}
let mut lines = slice.lines_at(line);
@ -176,7 +176,7 @@ pub fn move_prev_para(slice: RopeSlice, range: Range, count: usize, behavior: Mo
let head = slice.line_to_char(line);
let anchor = if behavior == Movement::Move {
// exclude first character after paragraph boundary
if line_to_empty && first_char {
if prev_empty_to_line && first_char {
range.cursor(slice)
} else {
range.head
@ -193,13 +193,12 @@ pub fn move_next_para(slice: RopeSlice, range: Range, count: usize, behavior: Mo
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(line.saturating_sub(1)));
let empty_to_line = curr_line_empty && !next_line_empty;
let curr_empty_to_line = curr_line_empty && !next_line_empty;
// iterate current line if first character after paragraph boundary
if empty_to_line && last_char {
// 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 {
while lines.next_if(|&e| !e).is_some() {
@ -211,7 +210,7 @@ pub fn move_next_para(slice: RopeSlice, range: Range, count: usize, behavior: Mo
}
let head = slice.line_to_char(line);
let anchor = if behavior == Movement::Move {
if empty_to_line && last_char {
if curr_empty_to_line && last_char {
range.head
} else {
range.cursor(slice)
@ -1256,7 +1255,7 @@ mod test {
#[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"),
@ -1315,7 +1314,7 @@ mod test {
#[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@"),
(

View file

@ -97,8 +97,9 @@ pub fn plain(s: &str, selection: Selection) -> String {
.enumerate()
.flat_map(|(i, range)| {
[
(range.anchor, '^'),
// sort like this before reversed so anchor < head later
(range.head, if i == primary { '@' } else { '|' }),
(range.anchor, '^'),
]
})
.collect();

View file

@ -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,
@ -288,6 +354,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), ...])

View file

@ -3991,6 +3991,7 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
'f' => textobject_treesitter("function", range),
'a' => textobject_treesitter("parameter", range),
'o' => textobject_treesitter("comment", range),
'p' => textobject::textobject_para(text, range, objtype, count),
'm' => {
let ch = text.char(range.cursor(text));
if !ch.is_ascii_alphanumeric() {