4use tree_sitter::{Node, QueryCursor};
6use crate::chars::{categorize_char, char_is_whitespace, CharCategory};
7use crate::graphemes::{next_grapheme_boundary, prev_grapheme_boundary};
8use crate::line_ending::rope_is_line_ending;
9use crate::movement::Direction;
11use crate::syntax::LanguageConfiguration;
14fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction, long: bool) -> usize {
15 use CharCategory::{Eol, Whitespace};
17 let iter = match direction {
18 Direction::Forward => slice.chars_at(pos),
19 Direction::Backward => {
20 let mut iter = slice.chars_at(pos);
26 let mut prev_category = match direction {
27 Direction::Forward if pos == 0 => Whitespace,
28 Direction::Forward => categorize_char(slice.char(pos - 1)),
29 Direction::Backward if pos == slice.len_chars() => Whitespace,
30 Direction::Backward => categorize_char(slice.char(pos)),
34 match categorize_char(ch) {
35 Eol | Whitespace => return pos,
37 if !long && category != prev_category && pos != 0 && pos != slice.len_chars() {
41 Direction::Forward => pos += 1,
42 Direction::Backward => pos = pos.saturating_sub(1),
44 prev_category = category;
53#[derive(Copy, Clone, PartialEq, Eq, Debug)]
57 /// Used for moving between objects.
61impl Display for TextObject {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 f.write_str(match self {
64 Self::Around => "around",
65 Self::Inside => "inside",
66 Self::Movement => "movement",
71// count doesn't do anything yet
72pub fn textobject_word(
75 textobject: TextObject,
79 let pos = range.cursor(slice);
81 let word_start = find_word_boundary(slice, pos, Direction::Backward, long);
82 let word_end = match slice.get_char(pos).map(categorize_char) {
83 None | Some(CharCategory::Whitespace | CharCategory::Eol) => pos,
84 _ => find_word_boundary(slice, pos + 1, Direction::Forward, long),
88 if word_start == word_end {
89 return Range::new(word_start, word_end);
93 TextObject::Inside => Range::new(word_start, word_end),
94 TextObject::Around => {
95 let whitespace_count_right = slice
97 .take_while(|c| char_is_whitespace(*c))
100 if whitespace_count_right > 0 {
101 Range::new(word_start, word_end + whitespace_count_right)
103 let whitespace_count_left = {
104 let mut iter = slice.chars_at(word_start);
106 iter.take_while(|c| char_is_whitespace(*c)).count()
108 Range::new(word_start - whitespace_count_left, word_end)
111 TextObject::Movement => unreachable!(),
115pub fn textobject_paragraph(
118 textobject: TextObject,
121 let mut line = range.cursor_line(slice);
122 let prev_line_empty = rope_is_line_ending(slice.line(line.saturating_sub(1)));
123 let curr_line_empty = rope_is_line_ending(slice.line(line));
124 let next_line_empty = rope_is_line_ending(slice.line(line.saturating_sub(1)));
126 prev_grapheme_boundary(slice, slice.line_to_char(line + 1)) == range.cursor(slice);
127 let prev_empty_to_line = prev_line_empty && !curr_line_empty;
128 let curr_empty_to_line = curr_line_empty && !next_line_empty;
130 // skip character before paragraph boundary
131 let mut line_back = line; // line but backwards
132 if prev_empty_to_line || curr_empty_to_line {
135 // do not include current paragraph on paragraph end (include next)
136 if !(curr_empty_to_line && last_char) {
137 let mut lines = slice.lines_at(line_back);
139 let mut lines = lines.map(rope_is_line_ending).peekable();
140 while lines.next_if(|&e| e).is_some() {
143 while lines.next_if(|&e| !e).is_some() {
148 // skip character after paragraph boundary
149 if curr_empty_to_line && last_char {
152 let mut lines = slice.lines_at(line).map(rope_is_line_ending).peekable();
153 let mut count_done = 0; // count how many non-whitespace paragraphs done
155 let mut done = false;
156 while lines.next_if(|&e| !e).is_some() {
160 while lines.next_if(|&e| e).is_some() {
163 count_done += done as usize;
166 // search one paragraph backwards for last paragraph
167 // makes `map` at the end of the paragraph with trailing newlines useful
168 let last_paragraph = count_done != count && lines.peek().is_none();
170 let mut lines = slice.lines_at(line_back);
172 let mut lines = lines.map(rope_is_line_ending).peekable();
173 while lines.next_if(|&e| e).is_some() {
176 while lines.next_if(|&e| !e).is_some() {
181 // handle last whitespaces part separately depending on textobject
183 TextObject::Around => {}
184 TextObject::Inside => {
185 // remove last whitespace paragraph
186 let mut lines = slice.lines_at(line);
188 let mut lines = lines.map(rope_is_line_ending).peekable();
189 while lines.next_if(|&e| e).is_some() {
193 TextObject::Movement => unreachable!(),
196 let anchor = slice.line_to_char(line_back);
197 let head = slice.line_to_char(line);
198 Range::new(anchor, head)
201pub fn textobject_pair_surround(
204 textobject: TextObject,
208 textobject_pair_surround_impl(slice, range, textobject, Some(ch), count)
211pub fn textobject_pair_surround_closest(
214 textobject: TextObject,
217 textobject_pair_surround_impl(slice, range, textobject, None, count)
220fn textobject_pair_surround_impl(
223 textobject: TextObject,
227 let pair_pos = match ch {
228 Some(ch) => surround::find_nth_pairs_pos(slice, ch, range, count),
229 // Automatically find the closest surround pairs
230 None => surround::find_nth_closest_pairs_pos(slice, range, count),
233 .map(|(anchor, head)| match textobject {
234 TextObject::Inside => Range::new(next_grapheme_boundary(slice, anchor), head),
235 TextObject::Around => Range::new(anchor, next_grapheme_boundary(slice, head)),
236 TextObject::Movement => unreachable!(),
241/// Transform the given range to select text objects based on tree-sitter.
242/// `object_name` is a query capture base name like "function", "class", etc.
243/// `slice_tree` is the tree-sitter node corresponding to given text slice.
244pub fn textobject_treesitter(
247 textobject: TextObject,
250 lang_config: &LanguageConfiguration,
253 let get_range = move || -> Option<Range> {
254 let byte_pos = slice.char_to_byte(range.cursor(slice));
256 let capture_name = format!("{}.{}", object_name, textobject); // eg. function.inner
257 let mut cursor = QueryCursor::new();
258 let node = lang_config
260 .capture_nodes(&capture_name, slice_tree, slice, &mut cursor)?
261 .filter(|node| node.byte_range().contains(&byte_pos))
262 .min_by_key(|node| node.byte_range().len())?;
264 let len = slice.len_bytes();
265 let start_byte = node.start_byte();
266 let end_byte = node.end_byte();
267 if start_byte >= len || end_byte >= len {
271 let start_char = slice.byte_to_char(start_byte);
272 let end_char = slice.byte_to_char(end_byte);
274 Some(Range::new(start_char, end_char))
276 get_range().unwrap_or(range)
281 use super::TextObject::*;
288 fn test_textobject_word() {
289 // (text, [(char position, textobject, final range), ...])
292 "cursor at beginning of doc",
293 vec![(0, Inside, (0, 6)), (0, Around, (0, 7))],
296 "cursor at middle of word",
298 (13, Inside, (10, 16)),
299 (10, Inside, (10, 16)),
300 (15, Inside, (10, 16)),
301 (13, Around, (10, 17)),
302 (10, Around, (10, 17)),
303 (15, Around, (10, 17)),
307 "cursor between word whitespace",
308 vec![(6, Inside, (6, 6)), (6, Around, (6, 6))],
311 "cursor on word before newline\n",
313 (22, Inside, (22, 29)),
314 (28, Inside, (22, 29)),
315 (25, Inside, (22, 29)),
316 (22, Around, (21, 29)),
317 (28, Around, (21, 29)),
318 (25, Around, (21, 29)),
322 "cursor on newline\nnext line",
323 vec![(17, Inside, (17, 17)), (17, Around, (17, 17))],
326 "cursor on word after newline\nnext line",
328 (29, Inside, (29, 33)),
329 (30, Inside, (29, 33)),
330 (32, Inside, (29, 33)),
331 (29, Around, (29, 34)),
332 (30, Around, (29, 34)),
333 (32, Around, (29, 34)),
337 "cursor on #$%:;* punctuation",
339 (13, Inside, (10, 16)),
340 (10, Inside, (10, 16)),
341 (15, Inside, (10, 16)),
342 (13, Around, (10, 17)),
343 (10, Around, (10, 17)),
344 (15, Around, (10, 17)),
348 "cursor on punc%^#$:;.tuation",
350 (14, Inside, (14, 21)),
351 (20, Inside, (14, 21)),
352 (17, Inside, (14, 21)),
353 (14, Around, (14, 21)),
354 (20, Around, (14, 21)),
355 (17, Around, (14, 21)),
359 "cursor in extra whitespace",
362 (10, Inside, (10, 10)),
363 (11, Inside, (11, 11)),
365 (10, Around, (10, 10)),
366 (11, Around, (11, 11)),
370 "cursor on word with extra whitespace",
371 vec![(11, Inside, (10, 14)), (11, Around, (10, 17))],
374 "cursor at end with extra whitespace",
375 vec![(28, Inside, (27, 37)), (28, Around, (24, 37))],
378 "cursor at end of doc",
379 vec![(19, Inside, (17, 20)), (19, Around, (16, 20))],
383 for (sample, scenario) in tests {
384 let doc = Rope::from(*sample);
385 let slice = doc.slice(..);
386 for &case in scenario {
387 let (pos, objtype, expected_range) = case;
388 // cursor is a single width selection
389 let range = Range::new(pos, pos + 1);
390 let result = textobject_word(slice, range, objtype, 1, false);
393 expected_range.into(),
394 "\nCase failed: {:?} - {:?}",
403 fn test_textobject_paragraph_inside_single() {
406 ("firs#[t|]#\n\nparagraph\n\n", "#[first\n|]#\nparagraph\n\n"),
408 "second\n\npa#[r|]#agraph\n\n",
409 "second\n\n#[paragraph\n|]#\n",
411 ("#[f|]#irst char\n\n", "#[first char\n|]#\n"),
412 ("last char\n#[\n|]#", "#[last char\n|]#\n"),
414 "empty to line\n#[\n|]#paragraph boundary\n\n",
415 "empty to line\n\n#[paragraph boundary\n|]#\n",
418 "line to empty\n\n#[p|]#aragraph boundary\n\n",
419 "line to empty\n\n#[paragraph boundary\n|]#\n",
423 for (before, expected) in tests {
424 let (s, selection) = crate::test::print(before);
425 let text = Rope::from(s.as_str());
426 let selection = selection
427 .transform(|r| textobject_paragraph(text.slice(..), r, TextObject::Inside, 1));
428 let actual = crate::test::plain(&s, selection);
429 assert_eq!(actual, expected, "\nbefore: `{:?}`", before);
434 fn test_textobject_paragraph_inside_double() {
437 "last two\n\n#[p|]#aragraph\n\nwithout whitespaces\n\n",
438 "last two\n\n#[paragraph\n\nwithout whitespaces\n|]#\n",
441 "last two\n#[\n|]#paragraph\n\nwithout whitespaces\n\n",
442 "last two\n\n#[paragraph\n\nwithout whitespaces\n|]#\n",
446 for (before, expected) in tests {
447 let (s, selection) = crate::test::print(before);
448 let text = Rope::from(s.as_str());
449 let selection = selection
450 .transform(|r| textobject_paragraph(text.slice(..), r, TextObject::Inside, 2));
451 let actual = crate::test::plain(&s, selection);
452 assert_eq!(actual, expected, "\nbefore: `{:?}`", before);
457 fn test_textobject_paragraph_around_single() {
460 ("firs#[t|]#\n\nparagraph\n\n", "#[first\n\n|]#paragraph\n\n"),
462 "second\n\npa#[r|]#agraph\n\n",
463 "second\n\n#[paragraph\n\n|]#",
465 ("#[f|]#irst char\n\n", "#[first char\n\n|]#"),
466 ("last char\n#[\n|]#", "#[last char\n\n|]#"),
468 "empty to line\n#[\n|]#paragraph boundary\n\n",
469 "empty to line\n\n#[paragraph boundary\n\n|]#",
472 "line to empty\n\n#[p|]#aragraph boundary\n\n",
473 "line to empty\n\n#[paragraph boundary\n\n|]#",
477 for (before, expected) in tests {
478 let (s, selection) = crate::test::print(before);
479 let text = Rope::from(s.as_str());
480 let selection = selection
481 .transform(|r| textobject_paragraph(text.slice(..), r, TextObject::Around, 1));
482 let actual = crate::test::plain(&s, selection);
483 assert_eq!(actual, expected, "\nbefore: `{:?}`", before);
488 fn test_textobject_surround() {
489 // (text, [(cursor position, textobject, final range, surround char, count), ...])
492 "simple (single) surround pairs",
494 (3, Inside, (3, 3), '(', 1),
495 (7, Inside, (8, 14), ')', 1),
496 (10, Inside, (8, 14), '(', 1),
497 (14, Inside, (8, 14), ')', 1),
498 (3, Around, (3, 3), '(', 1),
499 (7, Around, (7, 15), ')', 1),
500 (10, Around, (7, 15), '(', 1),
501 (14, Around, (7, 15), ')', 1),
505 "samexx 'single' surround pairs",
507 (3, Inside, (3, 3), '\'', 1),
508 (7, Inside, (7, 7), '\'', 1),
509 (10, Inside, (8, 14), '\'', 1),
510 (14, Inside, (14, 14), '\'', 1),
511 (3, Around, (3, 3), '\'', 1),
512 (7, Around, (7, 7), '\'', 1),
513 (10, Around, (7, 15), '\'', 1),
514 (14, Around, (14, 14), '\'', 1),
518 "(nested (surround (pairs)) 3 levels)",
520 (0, Inside, (1, 35), '(', 1),
521 (6, Inside, (1, 35), ')', 1),
522 (8, Inside, (9, 25), '(', 1),
523 (8, Inside, (9, 35), ')', 2),
524 (20, Inside, (9, 25), '(', 2),
525 (20, Inside, (1, 35), ')', 3),
526 (0, Around, (0, 36), '(', 1),
527 (6, Around, (0, 36), ')', 1),
528 (8, Around, (8, 26), '(', 1),
529 (8, Around, (8, 36), ')', 2),
530 (20, Around, (8, 26), '(', 2),
531 (20, Around, (0, 36), ')', 3),
535 "(mixed {surround [pair] same} line)",
537 (2, Inside, (1, 34), '(', 1),
538 (9, Inside, (8, 28), '{', 1),
539 (18, Inside, (18, 22), '[', 1),
540 (2, Around, (0, 35), '(', 1),
541 (9, Around, (7, 29), '{', 1),
542 (18, Around, (17, 23), '[', 1),
546 "(stepped (surround) pairs (should) skip)",
547 vec![(22, Inside, (1, 39), '(', 1), (22, Around, (0, 40), '(', 1)],
550 "[surround pairs{\non different]\nlines}",
552 (7, Inside, (1, 29), '[', 1),
553 (15, Inside, (16, 36), '{', 1),
554 (7, Around, (0, 30), '[', 1),
555 (15, Around, (15, 37), '{', 1),
560 for (sample, scenario) in tests {
561 let doc = Rope::from(*sample);
562 let slice = doc.slice(..);
563 for &case in scenario {
564 let (pos, objtype, expected_range, ch, count) = case;
565 let result = textobject_pair_surround(slice, Range::point(pos), objtype, ch, count);
568 expected_range.into(),
569 "\nCase failed: {:?} - {:?}",
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list