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 => {
236 Range::new(next_grapheme_boundary(slice, anchor), head)
238 Range::new(anchor, next_grapheme_boundary(slice, head))
241 TextObject::Around => {
243 Range::new(anchor, next_grapheme_boundary(slice, head))
245 Range::new(next_grapheme_boundary(slice, anchor), head)
248 TextObject::Movement => unreachable!(),
253/// Transform the given range to select text objects based on tree-sitter.
254/// `object_name` is a query capture base name like "function", "class", etc.
255/// `slice_tree` is the tree-sitter node corresponding to given text slice.
256pub fn textobject_treesitter(
259 textobject: TextObject,
262 lang_config: &LanguageConfiguration,
265 let get_range = move || -> Option<Range> {
266 let byte_pos = slice.char_to_byte(range.cursor(slice));
268 let capture_name = format!("{}.{}", object_name, textobject); // eg. function.inner
269 let mut cursor = QueryCursor::new();
270 let node = lang_config
272 .capture_nodes(&capture_name, slice_tree, slice, &mut cursor)?
273 .filter(|node| node.byte_range().contains(&byte_pos))
274 .min_by_key(|node| node.byte_range().len())?;
276 let len = slice.len_bytes();
277 let start_byte = node.start_byte();
278 let end_byte = node.end_byte();
279 if start_byte >= len || end_byte >= len {
283 let start_char = slice.byte_to_char(start_byte);
284 let end_char = slice.byte_to_char(end_byte);
286 Some(Range::new(start_char, end_char))
288 get_range().unwrap_or(range)
293 use super::TextObject::*;
300 fn test_textobject_word() {
301 // (text, [(char position, textobject, final range), ...])
304 "cursor at beginning of doc",
305 vec![(0, Inside, (0, 6)), (0, Around, (0, 7))],
308 "cursor at middle of word",
310 (13, Inside, (10, 16)),
311 (10, Inside, (10, 16)),
312 (15, Inside, (10, 16)),
313 (13, Around, (10, 17)),
314 (10, Around, (10, 17)),
315 (15, Around, (10, 17)),
319 "cursor between word whitespace",
320 vec![(6, Inside, (6, 6)), (6, Around, (6, 6))],
323 "cursor on word before newline\n",
325 (22, Inside, (22, 29)),
326 (28, Inside, (22, 29)),
327 (25, Inside, (22, 29)),
328 (22, Around, (21, 29)),
329 (28, Around, (21, 29)),
330 (25, Around, (21, 29)),
334 "cursor on newline\nnext line",
335 vec![(17, Inside, (17, 17)), (17, Around, (17, 17))],
338 "cursor on word after newline\nnext line",
340 (29, Inside, (29, 33)),
341 (30, Inside, (29, 33)),
342 (32, Inside, (29, 33)),
343 (29, Around, (29, 34)),
344 (30, Around, (29, 34)),
345 (32, Around, (29, 34)),
349 "cursor on #$%:;* punctuation",
351 (13, Inside, (10, 16)),
352 (10, Inside, (10, 16)),
353 (15, Inside, (10, 16)),
354 (13, Around, (10, 17)),
355 (10, Around, (10, 17)),
356 (15, Around, (10, 17)),
360 "cursor on punc%^#$:;.tuation",
362 (14, Inside, (14, 21)),
363 (20, Inside, (14, 21)),
364 (17, Inside, (14, 21)),
365 (14, Around, (14, 21)),
366 (20, Around, (14, 21)),
367 (17, Around, (14, 21)),
371 "cursor in extra whitespace",
374 (10, Inside, (10, 10)),
375 (11, Inside, (11, 11)),
377 (10, Around, (10, 10)),
378 (11, Around, (11, 11)),
382 "cursor on word with extra whitespace",
383 vec![(11, Inside, (10, 14)), (11, Around, (10, 17))],
386 "cursor at end with extra whitespace",
387 vec![(28, Inside, (27, 37)), (28, Around, (24, 37))],
390 "cursor at end of doc",
391 vec![(19, Inside, (17, 20)), (19, Around, (16, 20))],
395 for (sample, scenario) in tests {
396 let doc = Rope::from(*sample);
397 let slice = doc.slice(..);
398 for &case in scenario {
399 let (pos, objtype, expected_range) = case;
400 // cursor is a single width selection
401 let range = Range::new(pos, pos + 1);
402 let result = textobject_word(slice, range, objtype, 1, false);
405 expected_range.into(),
406 "\nCase failed: {:?} - {:?}",
415 fn test_textobject_paragraph_inside_single() {
418 ("firs#[t|]#\n\nparagraph\n\n", "#[first\n|]#\nparagraph\n\n"),
420 "second\n\npa#[r|]#agraph\n\n",
421 "second\n\n#[paragraph\n|]#\n",
423 ("#[f|]#irst char\n\n", "#[first char\n|]#\n"),
424 ("last char\n#[\n|]#", "#[last char\n|]#\n"),
426 "empty to line\n#[\n|]#paragraph boundary\n\n",
427 "empty to line\n\n#[paragraph boundary\n|]#\n",
430 "line to empty\n\n#[p|]#aragraph boundary\n\n",
431 "line to empty\n\n#[paragraph boundary\n|]#\n",
435 for (before, expected) in tests {
436 let (s, selection) = crate::test::print(before);
437 let text = Rope::from(s.as_str());
438 let selection = selection
439 .transform(|r| textobject_paragraph(text.slice(..), r, TextObject::Inside, 1));
440 let actual = crate::test::plain(&s, selection);
441 assert_eq!(actual, expected, "\nbefore: `{:?}`", before);
446 fn test_textobject_paragraph_inside_double() {
449 "last two\n\n#[p|]#aragraph\n\nwithout whitespaces\n\n",
450 "last two\n\n#[paragraph\n\nwithout whitespaces\n|]#\n",
453 "last two\n#[\n|]#paragraph\n\nwithout whitespaces\n\n",
454 "last two\n\n#[paragraph\n\nwithout whitespaces\n|]#\n",
458 for (before, expected) in tests {
459 let (s, selection) = crate::test::print(before);
460 let text = Rope::from(s.as_str());
461 let selection = selection
462 .transform(|r| textobject_paragraph(text.slice(..), r, TextObject::Inside, 2));
463 let actual = crate::test::plain(&s, selection);
464 assert_eq!(actual, expected, "\nbefore: `{:?}`", before);
469 fn test_textobject_paragraph_around_single() {
472 ("firs#[t|]#\n\nparagraph\n\n", "#[first\n\n|]#paragraph\n\n"),
474 "second\n\npa#[r|]#agraph\n\n",
475 "second\n\n#[paragraph\n\n|]#",
477 ("#[f|]#irst char\n\n", "#[first char\n\n|]#"),
478 ("last char\n#[\n|]#", "#[last char\n\n|]#"),
480 "empty to line\n#[\n|]#paragraph boundary\n\n",
481 "empty to line\n\n#[paragraph boundary\n\n|]#",
484 "line to empty\n\n#[p|]#aragraph boundary\n\n",
485 "line to empty\n\n#[paragraph boundary\n\n|]#",
489 for (before, expected) in tests {
490 let (s, selection) = crate::test::print(before);
491 let text = Rope::from(s.as_str());
492 let selection = selection
493 .transform(|r| textobject_paragraph(text.slice(..), r, TextObject::Around, 1));
494 let actual = crate::test::plain(&s, selection);
495 assert_eq!(actual, expected, "\nbefore: `{:?}`", before);
500 fn test_textobject_surround() {
501 // (text, [(cursor position, textobject, final range, surround char, count), ...])
504 "simple (single) surround pairs",
506 (3, Inside, (3, 3), '(', 1),
507 (7, Inside, (8, 14), ')', 1),
508 (10, Inside, (8, 14), '(', 1),
509 (14, Inside, (8, 14), ')', 1),
510 (3, Around, (3, 3), '(', 1),
511 (7, Around, (7, 15), ')', 1),
512 (10, Around, (7, 15), '(', 1),
513 (14, Around, (7, 15), ')', 1),
517 "samexx 'single' surround pairs",
519 (3, Inside, (3, 3), '\'', 1),
520 (7, Inside, (7, 7), '\'', 1),
521 (10, Inside, (8, 14), '\'', 1),
522 (14, Inside, (14, 14), '\'', 1),
523 (3, Around, (3, 3), '\'', 1),
524 (7, Around, (7, 7), '\'', 1),
525 (10, Around, (7, 15), '\'', 1),
526 (14, Around, (14, 14), '\'', 1),
530 "(nested (surround (pairs)) 3 levels)",
532 (0, Inside, (1, 35), '(', 1),
533 (6, Inside, (1, 35), ')', 1),
534 (8, Inside, (9, 25), '(', 1),
535 (8, Inside, (9, 35), ')', 2),
536 (20, Inside, (9, 25), '(', 2),
537 (20, Inside, (1, 35), ')', 3),
538 (0, Around, (0, 36), '(', 1),
539 (6, Around, (0, 36), ')', 1),
540 (8, Around, (8, 26), '(', 1),
541 (8, Around, (8, 36), ')', 2),
542 (20, Around, (8, 26), '(', 2),
543 (20, Around, (0, 36), ')', 3),
547 "(mixed {surround [pair] same} line)",
549 (2, Inside, (1, 34), '(', 1),
550 (9, Inside, (8, 28), '{', 1),
551 (18, Inside, (18, 22), '[', 1),
552 (2, Around, (0, 35), '(', 1),
553 (9, Around, (7, 29), '{', 1),
554 (18, Around, (17, 23), '[', 1),
558 "(stepped (surround) pairs (should) skip)",
559 vec![(22, Inside, (1, 39), '(', 1), (22, Around, (0, 40), '(', 1)],
562 "[surround pairs{\non different]\nlines}",
564 (7, Inside, (1, 29), '[', 1),
565 (15, Inside, (16, 36), '{', 1),
566 (7, Around, (0, 30), '[', 1),
567 (15, Around, (15, 37), '{', 1),
572 for (sample, scenario) in tests {
573 let doc = Rope::from(*sample);
574 let slice = doc.slice(..);
575 for &case in scenario {
576 let (pos, objtype, expected_range, ch, count) = case;
577 let result = textobject_pair_surround(slice, Range::point(pos), objtype, ch, count);
580 expected_range.into(),
581 "\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