4use tree_sitter::{Node, QueryCursor};
6use crate::chars::{categorize_char, char_is_whitespace, CharCategory};
7use crate::graphemes::next_grapheme_boundary;
8use crate::movement::Direction;
10use crate::syntax::LanguageConfiguration;
13fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction, long: bool) -> usize {
14 use CharCategory::{Eol, Whitespace};
16 let iter = match direction {
17 Direction::Forward => slice.chars_at(pos),
18 Direction::Backward => {
19 let mut iter = slice.chars_at(pos);
25 let mut prev_category = match direction {
26 Direction::Forward if pos == 0 => Whitespace,
27 Direction::Forward => categorize_char(slice.char(pos - 1)),
28 Direction::Backward if pos == slice.len_chars() => Whitespace,
29 Direction::Backward => categorize_char(slice.char(pos)),
33 match categorize_char(ch) {
34 Eol | Whitespace => return pos,
36 if !long && category != prev_category && pos != 0 && pos != slice.len_chars() {
40 Direction::Forward => pos += 1,
41 Direction::Backward => pos = pos.saturating_sub(1),
43 prev_category = category;
52#[derive(Copy, Clone, PartialEq, Eq, Debug)]
58impl Display for TextObject {
59 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60 f.write_str(match self {
61 Self::Around => "around",
62 Self::Inside => "inside",
67// count doesn't do anything yet
68pub fn textobject_word(
71 textobject: TextObject,
75 let pos = range.cursor(slice);
77 let word_start = find_word_boundary(slice, pos, Direction::Backward, long);
78 let word_end = match slice.get_char(pos).map(categorize_char) {
79 None | Some(CharCategory::Whitespace | CharCategory::Eol) => pos,
80 _ => find_word_boundary(slice, pos + 1, Direction::Forward, long),
84 if word_start == word_end {
85 return Range::new(word_start, word_end);
89 TextObject::Inside => Range::new(word_start, word_end),
90 TextObject::Around => {
91 let whitespace_count_right = slice
93 .take_while(|c| char_is_whitespace(*c))
96 if whitespace_count_right > 0 {
97 Range::new(word_start, word_end + whitespace_count_right)
99 let whitespace_count_left = {
100 let mut iter = slice.chars_at(word_start);
102 iter.take_while(|c| char_is_whitespace(*c)).count()
104 Range::new(word_start - whitespace_count_left, word_end)
110pub fn textobject_surround(
113 textobject: TextObject,
117 surround::find_nth_pairs_pos(slice, ch, range, count)
118 .map(|(anchor, head)| match textobject {
119 TextObject::Inside => Range::new(next_grapheme_boundary(slice, anchor), head),
120 TextObject::Around => Range::new(anchor, next_grapheme_boundary(slice, head)),
125/// Transform the given range to select text objects based on tree-sitter.
126/// `object_name` is a query capture base name like "function", "class", etc.
127/// `slice_tree` is the tree-sitter node corresponding to given text slice.
128pub fn textobject_treesitter(
131 textobject: TextObject,
134 lang_config: &LanguageConfiguration,
137 let get_range = move || -> Option<Range> {
138 let byte_pos = slice.char_to_byte(range.cursor(slice));
140 let capture_name = format!("{}.{}", object_name, textobject); // eg. function.inner
141 let mut cursor = QueryCursor::new();
142 let node = lang_config
144 .capture_nodes(&capture_name, slice_tree, slice, &mut cursor)?
145 .filter(|node| node.byte_range().contains(&byte_pos))
146 .min_by_key(|node| node.byte_range().len())?;
148 let len = slice.len_bytes();
149 let start_byte = node.start_byte();
150 let end_byte = node.end_byte();
151 if start_byte >= len || end_byte >= len {
155 let start_char = slice.byte_to_char(start_byte);
156 let end_char = slice.byte_to_char(end_byte);
158 Some(Range::new(start_char, end_char))
160 get_range().unwrap_or(range)
165 use super::TextObject::*;
172 fn test_textobject_word() {
173 // (text, [(char position, textobject, final range), ...])
176 "cursor at beginning of doc",
177 vec![(0, Inside, (0, 6)), (0, Around, (0, 7))],
180 "cursor at middle of word",
182 (13, Inside, (10, 16)),
183 (10, Inside, (10, 16)),
184 (15, Inside, (10, 16)),
185 (13, Around, (10, 17)),
186 (10, Around, (10, 17)),
187 (15, Around, (10, 17)),
191 "cursor between word whitespace",
192 vec![(6, Inside, (6, 6)), (6, Around, (6, 6))],
195 "cursor on word before newline\n",
197 (22, Inside, (22, 29)),
198 (28, Inside, (22, 29)),
199 (25, Inside, (22, 29)),
200 (22, Around, (21, 29)),
201 (28, Around, (21, 29)),
202 (25, Around, (21, 29)),
206 "cursor on newline\nnext line",
207 vec![(17, Inside, (17, 17)), (17, Around, (17, 17))],
210 "cursor on word after newline\nnext line",
212 (29, Inside, (29, 33)),
213 (30, Inside, (29, 33)),
214 (32, Inside, (29, 33)),
215 (29, Around, (29, 34)),
216 (30, Around, (29, 34)),
217 (32, Around, (29, 34)),
221 "cursor on #$%:;* punctuation",
223 (13, Inside, (10, 16)),
224 (10, Inside, (10, 16)),
225 (15, Inside, (10, 16)),
226 (13, Around, (10, 17)),
227 (10, Around, (10, 17)),
228 (15, Around, (10, 17)),
232 "cursor on punc%^#$:;.tuation",
234 (14, Inside, (14, 21)),
235 (20, Inside, (14, 21)),
236 (17, Inside, (14, 21)),
237 (14, Around, (14, 21)),
238 (20, Around, (14, 21)),
239 (17, Around, (14, 21)),
243 "cursor in extra whitespace",
246 (10, Inside, (10, 10)),
247 (11, Inside, (11, 11)),
249 (10, Around, (10, 10)),
250 (11, Around, (11, 11)),
254 "cursor on word with extra whitespace",
255 vec![(11, Inside, (10, 14)), (11, Around, (10, 17))],
258 "cursor at end with extra whitespace",
259 vec![(28, Inside, (27, 37)), (28, Around, (24, 37))],
262 "cursor at end of doc",
263 vec![(19, Inside, (17, 20)), (19, Around, (16, 20))],
267 for (sample, scenario) in tests {
268 let doc = Rope::from(*sample);
269 let slice = doc.slice(..);
270 for &case in scenario {
271 let (pos, objtype, expected_range) = case;
272 // cursor is a single width selection
273 let range = Range::new(pos, pos + 1);
274 let result = textobject_word(slice, range, objtype, 1, false);
277 expected_range.into(),
278 "\nCase failed: {:?} - {:?}",
287 fn test_textobject_surround() {
288 // (text, [(cursor position, textobject, final range, surround char, count), ...])
291 "simple (single) surround pairs",
293 (3, Inside, (3, 3), '(', 1),
294 (7, Inside, (8, 14), ')', 1),
295 (10, Inside, (8, 14), '(', 1),
296 (14, Inside, (8, 14), ')', 1),
297 (3, Around, (3, 3), '(', 1),
298 (7, Around, (7, 15), ')', 1),
299 (10, Around, (7, 15), '(', 1),
300 (14, Around, (7, 15), ')', 1),
304 "samexx 'single' surround pairs",
306 (3, Inside, (3, 3), '\'', 1),
307 (7, Inside, (7, 7), '\'', 1),
308 (10, Inside, (8, 14), '\'', 1),
309 (14, Inside, (14, 14), '\'', 1),
310 (3, Around, (3, 3), '\'', 1),
311 (7, Around, (7, 7), '\'', 1),
312 (10, Around, (7, 15), '\'', 1),
313 (14, Around, (14, 14), '\'', 1),
317 "(nested (surround (pairs)) 3 levels)",
319 (0, Inside, (1, 35), '(', 1),
320 (6, Inside, (1, 35), ')', 1),
321 (8, Inside, (9, 25), '(', 1),
322 (8, Inside, (9, 35), ')', 2),
323 (20, Inside, (9, 25), '(', 2),
324 (20, Inside, (1, 35), ')', 3),
325 (0, Around, (0, 36), '(', 1),
326 (6, Around, (0, 36), ')', 1),
327 (8, Around, (8, 26), '(', 1),
328 (8, Around, (8, 36), ')', 2),
329 (20, Around, (8, 26), '(', 2),
330 (20, Around, (0, 36), ')', 3),
334 "(mixed {surround [pair] same} line)",
336 (2, Inside, (1, 34), '(', 1),
337 (9, Inside, (8, 28), '{', 1),
338 (18, Inside, (18, 22), '[', 1),
339 (2, Around, (0, 35), '(', 1),
340 (9, Around, (7, 29), '{', 1),
341 (18, Around, (17, 23), '[', 1),
345 "(stepped (surround) pairs (should) skip)",
346 vec![(22, Inside, (1, 39), '(', 1), (22, Around, (0, 40), '(', 1)],
349 "[surround pairs{\non different]\nlines}",
351 (7, Inside, (1, 29), '[', 1),
352 (15, Inside, (16, 36), '{', 1),
353 (7, Around, (0, 30), '[', 1),
354 (15, Around, (15, 37), '{', 1),
359 for (sample, scenario) in tests {
360 let doc = Rope::from(*sample);
361 let slice = doc.slice(..);
362 for &case in scenario {
363 let (pos, objtype, expected_range, ch, count) = case;
364 let result = textobject_surround(slice, Range::point(pos), objtype, ch, count);
367 expected_range.into(),
368 "\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