mirror of
https://github.com/helix-editor/helix.git
synced 2024-11-22 09:26:19 +04:00
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:
parent
e2a6e33b98
commit
8350ee9a0e
@ -277,6 +277,8 @@ #### Unimpaired
|
||||
| `[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` |
|
||||
|
||||
|
@ -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 @@ fn test_behaviour_when_moving_to_end_of_next_long_words() {
|
||||
#[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 @@ fn test_behaviour_when_moving_to_prev_paragraph_extend() {
|
||||
#[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@"),
|
||||
(
|
||||
|
@ -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();
|
||||
|
@ -4,7 +4,8 @@
|
||||
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 @@ fn test_textobject_word() {
|
||||
}
|
||||
}
|
||||
|
||||
#[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), ...])
|
||||
|
@ -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() {
|
||||
|
Loading…
Reference in New Issue
Block a user