on 376c19e help sign in
helix/helix-core/src/textobject.rs
588 lines 20 kB history raw
1use std::fmt::Display;
3use ropey::RopeSlice;
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;
10use crate::surround;
11use crate::syntax::LanguageConfiguration;
12use crate::Range;
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);
21 iter.reverse();
22 iter
23 }
24 };
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)),
31 };
33 for ch in iter {
34 match categorize_char(ch) {
35 Eol | Whitespace => return pos,
36 category => {
37 if !long && category != prev_category && pos != 0 && pos != slice.len_chars() {
38 return pos;
39 } else {
40 match direction {
41 Direction::Forward => pos += 1,
42 Direction::Backward => pos = pos.saturating_sub(1),
43 }
44 prev_category = category;
45 }
46 }
47 }
48 }
50 pos
53#[derive(Copy, Clone, PartialEq, Eq, Debug)]
54pub enum TextObject {
55 Around,
56 Inside,
57 /// Used for moving between objects.
58 Movement,
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",
67 })
68 }
71// count doesn't do anything yet
72pub fn textobject_word(
73 slice: RopeSlice,
74 range: Range,
75 textobject: TextObject,
76 _count: usize,
77 long: bool,
78) -> Range {
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),
85 };
87 // Special case.
88 if word_start == word_end {
89 return Range::new(word_start, word_end);
90 }
92 match textobject {
93 TextObject::Inside => Range::new(word_start, word_end),
94 TextObject::Around => {
95 let whitespace_count_right = slice
96 .chars_at(word_end)
97 .take_while(|c| char_is_whitespace(*c))
98 .count();
100 if whitespace_count_right > 0 {
101 Range::new(word_start, word_end + whitespace_count_right)
102 } else {
103 let whitespace_count_left = {
104 let mut iter = slice.chars_at(word_start);
105 iter.reverse();
106 iter.take_while(|c| char_is_whitespace(*c)).count()
107 };
108 Range::new(word_start - whitespace_count_left, word_end)
109 }
110 }
111 TextObject::Movement => unreachable!(),
112 }
115pub fn textobject_paragraph(
116 slice: RopeSlice,
117 range: Range,
118 textobject: TextObject,
119 count: usize,
120) -> Range {
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)));
125 let last_char =
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 {
133 line_back += 1;
134 }
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);
138 lines.reverse();
139 let mut lines = lines.map(rope_is_line_ending).peekable();
140 while lines.next_if(|&e| e).is_some() {
141 line_back -= 1;
142 }
143 while lines.next_if(|&e| !e).is_some() {
144 line_back -= 1;
145 }
146 }
148 // skip character after paragraph boundary
149 if curr_empty_to_line && last_char {
150 line += 1;
151 }
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
154 for _ in 0..count {
155 let mut done = false;
156 while lines.next_if(|&e| !e).is_some() {
157 line += 1;
158 done = true;
159 }
160 while lines.next_if(|&e| e).is_some() {
161 line += 1;
162 }
163 count_done += done as usize;
164 }
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();
169 if last_paragraph {
170 let mut lines = slice.lines_at(line_back);
171 lines.reverse();
172 let mut lines = lines.map(rope_is_line_ending).peekable();
173 while lines.next_if(|&e| e).is_some() {
174 line_back -= 1;
175 }
176 while lines.next_if(|&e| !e).is_some() {
177 line_back -= 1;
178 }
179 }
181 // handle last whitespaces part separately depending on textobject
182 match textobject {
183 TextObject::Around => {}
184 TextObject::Inside => {
185 // remove last whitespace paragraph
186 let mut lines = slice.lines_at(line);
187 lines.reverse();
188 let mut lines = lines.map(rope_is_line_ending).peekable();
189 while lines.next_if(|&e| e).is_some() {
190 line -= 1;
191 }
192 }
193 TextObject::Movement => unreachable!(),
194 }
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(
202 slice: RopeSlice,
203 range: Range,
204 textobject: TextObject,
205 ch: char,
206 count: usize,
207) -> Range {
208 textobject_pair_surround_impl(slice, range, textobject, Some(ch), count)
211pub fn textobject_pair_surround_closest(
212 slice: RopeSlice,
213 range: Range,
214 textobject: TextObject,
215 count: usize,
216) -> Range {
217 textobject_pair_surround_impl(slice, range, textobject, None, count)
220fn textobject_pair_surround_impl(
221 slice: RopeSlice,
222 range: Range,
223 textobject: TextObject,
224 ch: Option<char>,
225 count: usize,
226) -> Range {
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),
231 };
232 pair_pos
233 .map(|(anchor, head)| match textobject {
234 TextObject::Inside => {
235 if anchor < head {
236 Range::new(next_grapheme_boundary(slice, anchor), head)
237 } else {
238 Range::new(anchor, next_grapheme_boundary(slice, head))
239 }
240 }
241 TextObject::Around => {
242 if anchor < head {
243 Range::new(anchor, next_grapheme_boundary(slice, head))
244 } else {
245 Range::new(next_grapheme_boundary(slice, anchor), head)
246 }
247 }
248 TextObject::Movement => unreachable!(),
249 })
250 .unwrap_or(range)
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(
257 slice: RopeSlice,
258 range: Range,
259 textobject: TextObject,
260 object_name: &str,
261 slice_tree: Node,
262 lang_config: &LanguageConfiguration,
263 _count: usize,
264) -> Range {
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
271 .textobject_query()?
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 {
280 return None;
281 }
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))
287 };
288 get_range().unwrap_or(range)
291#[cfg(test)]
292mod test {
293 use super::TextObject::*;
294 use super::*;
296 use crate::Range;
297 use ropey::Rope;
299 #[test]
300 fn test_textobject_word() {
301 // (text, [(char position, textobject, final range), ...])
302 let tests = &[
303 (
304 "cursor at beginning of doc",
305 vec![(0, Inside, (0, 6)), (0, Around, (0, 7))],
306 ),
307 (
308 "cursor at middle of word",
309 vec![
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)),
316 ],
317 ),
318 (
319 "cursor between word whitespace",
320 vec![(6, Inside, (6, 6)), (6, Around, (6, 6))],
321 ),
322 (
323 "cursor on word before newline\n",
324 vec![
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)),
331 ],
332 ),
333 (
334 "cursor on newline\nnext line",
335 vec![(17, Inside, (17, 17)), (17, Around, (17, 17))],
336 ),
337 (
338 "cursor on word after newline\nnext line",
339 vec![
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)),
346 ],
347 ),
348 (
349 "cursor on #$%:;* punctuation",
350 vec![
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)),
357 ],
358 ),
359 (
360 "cursor on punc%^#$:;.tuation",
361 vec![
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)),
368 ],
369 ),
370 (
371 "cursor in extra whitespace",
372 vec![
373 (9, Inside, (9, 9)),
374 (10, Inside, (10, 10)),
375 (11, Inside, (11, 11)),
376 (9, Around, (9, 9)),
377 (10, Around, (10, 10)),
378 (11, Around, (11, 11)),
379 ],
380 ),
381 (
382 "cursor on word with extra whitespace",
383 vec![(11, Inside, (10, 14)), (11, Around, (10, 17))],
384 ),
385 (
386 "cursor at end with extra whitespace",
387 vec![(28, Inside, (27, 37)), (28, Around, (24, 37))],
388 ),
389 (
390 "cursor at end of doc",
391 vec![(19, Inside, (17, 20)), (19, Around, (16, 20))],
392 ),
393 ];
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);
403 assert_eq!(
404 result,
405 expected_range.into(),
406 "\nCase failed: {:?} - {:?}",
407 sample,
408 case
409 );
410 }
411 }
412 }
414 #[test]
415 fn test_textobject_paragraph_inside_single() {
416 let tests = [
417 ("#[|]#", "#[|]#"),
418 ("firs#[t|]#\n\nparagraph\n\n", "#[first\n|]#\nparagraph\n\n"),
419 (
420 "second\n\npa#[r|]#agraph\n\n",
421 "second\n\n#[paragraph\n|]#\n",
422 ),
423 ("#[f|]#irst char\n\n", "#[first char\n|]#\n"),
424 ("last char\n#[\n|]#", "#[last char\n|]#\n"),
425 (
426 "empty to line\n#[\n|]#paragraph boundary\n\n",
427 "empty to line\n\n#[paragraph boundary\n|]#\n",
428 ),
429 (
430 "line to empty\n\n#[p|]#aragraph boundary\n\n",
431 "line to empty\n\n#[paragraph boundary\n|]#\n",
432 ),
433 ];
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);
442 }
443 }
445 #[test]
446 fn test_textobject_paragraph_inside_double() {
447 let tests = [
448 (
449 "last two\n\n#[p|]#aragraph\n\nwithout whitespaces\n\n",
450 "last two\n\n#[paragraph\n\nwithout whitespaces\n|]#\n",
451 ),
452 (
453 "last two\n#[\n|]#paragraph\n\nwithout whitespaces\n\n",
454 "last two\n\n#[paragraph\n\nwithout whitespaces\n|]#\n",
455 ),
456 ];
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);
465 }
466 }
468 #[test]
469 fn test_textobject_paragraph_around_single() {
470 let tests = [
471 ("#[|]#", "#[|]#"),
472 ("firs#[t|]#\n\nparagraph\n\n", "#[first\n\n|]#paragraph\n\n"),
473 (
474 "second\n\npa#[r|]#agraph\n\n",
475 "second\n\n#[paragraph\n\n|]#",
476 ),
477 ("#[f|]#irst char\n\n", "#[first char\n\n|]#"),
478 ("last char\n#[\n|]#", "#[last char\n\n|]#"),
479 (
480 "empty to line\n#[\n|]#paragraph boundary\n\n",
481 "empty to line\n\n#[paragraph boundary\n\n|]#",
482 ),
483 (
484 "line to empty\n\n#[p|]#aragraph boundary\n\n",
485 "line to empty\n\n#[paragraph boundary\n\n|]#",
486 ),
487 ];
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);
496 }
497 }
499 #[test]
500 fn test_textobject_surround() {
501 // (text, [(cursor position, textobject, final range, surround char, count), ...])
502 let tests = &[
503 (
504 "simple (single) surround pairs",
505 vec![
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),
514 ],
515 ),
516 (
517 "samexx 'single' surround pairs",
518 vec![
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),
527 ],
528 ),
529 (
530 "(nested (surround (pairs)) 3 levels)",
531 vec![
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),
544 ],
545 ),
546 (
547 "(mixed {surround [pair] same} line)",
548 vec![
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),
555 ],
556 ),
557 (
558 "(stepped (surround) pairs (should) skip)",
559 vec![(22, Inside, (1, 39), '(', 1), (22, Around, (0, 40), '(', 1)],
560 ),
561 (
562 "[surround pairs{\non different]\nlines}",
563 vec![
564 (7, Inside, (1, 29), '[', 1),
565 (15, Inside, (16, 36), '{', 1),
566 (7, Around, (0, 30), '[', 1),
567 (15, Around, (15, 37), '{', 1),
568 ],
569 ),
570 ];
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);
578 assert_eq!(
579 result,
580 expected_range.into(),
581 "\nCase failed: {:?} - {:?}",
582 sample,
583 case
584 );
585 }
586 }
587 }