|
| 1 | +use std::cmp; |
1 | 2 | use std::fmt::Display; |
2 | 3 |
|
3 | 4 | use ropey::RopeSlice; |
4 | 5 |
|
5 | 6 | use crate::chars::{categorize_char, char_is_whitespace, CharCategory}; |
6 | 7 | use crate::graphemes::{next_grapheme_boundary, prev_grapheme_boundary}; |
7 | | -use crate::line_ending::rope_is_line_ending; |
| 8 | +use crate::indent::indent_level_for_line; |
| 9 | +use crate::line_ending::{get_line_ending, rope_is_line_ending}; |
8 | 10 | use crate::movement::Direction; |
9 | 11 | use crate::syntax; |
10 | 12 | use crate::Range; |
@@ -197,6 +199,92 @@ pub fn textobject_paragraph( |
197 | 199 | Range::new(anchor, head) |
198 | 200 | } |
199 | 201 |
|
| 202 | +pub fn textobject_indentation_level( |
| 203 | + slice: RopeSlice, |
| 204 | + range: Range, |
| 205 | + textobject: TextObject, |
| 206 | + count: usize, |
| 207 | + indent_width: usize, |
| 208 | + tab_width: usize, |
| 209 | +) -> Range { |
| 210 | + let (mut line_start, mut line_end) = range.line_range(slice); |
| 211 | + let mut min_indent: Option<usize> = None; |
| 212 | + |
| 213 | + // Find the innermost indent represented by the current selection range. |
| 214 | + // Range could be only on one line, so we need an inclusive range in the |
| 215 | + // loop definition. |
| 216 | + for i in line_start..=line_end { |
| 217 | + let line = slice.line(i); |
| 218 | + |
| 219 | + // Including empty lines leads to pathological behaviour, where having |
| 220 | + // an empty line in a multi-line selection causes the entire buffer to |
| 221 | + // be selected, which is not intuitively what we want. |
| 222 | + if !rope_is_line_ending(line) { |
| 223 | + let indent_level = indent_level_for_line(line, tab_width, indent_width); |
| 224 | + min_indent = if let Some(prev_min_indent) = min_indent { |
| 225 | + Some(cmp::min(indent_level, prev_min_indent)) |
| 226 | + } else { |
| 227 | + Some(indent_level) |
| 228 | + } |
| 229 | + } |
| 230 | + } |
| 231 | + |
| 232 | + // It can happen that the selection consists of an empty line, so min_indent |
| 233 | + // will be untouched, in which case we can skip the rest of the function |
| 234 | + // and no-op. |
| 235 | + if min_indent.is_none() { |
| 236 | + return range; |
| 237 | + } |
| 238 | + |
| 239 | + let min_indent = min_indent.unwrap() + 1 - count; |
| 240 | + |
| 241 | + // Traverse backwards until there are no more lines indented the same or |
| 242 | + // greater, and extend the start of the range to it. |
| 243 | + if line_start > 0 { |
| 244 | + for line in slice.lines_at(line_start).reversed() { |
| 245 | + let indent_level = indent_level_for_line(line, tab_width, indent_width); |
| 246 | + let empty_line = rope_is_line_ending(line); |
| 247 | + if (min_indent > 0 && indent_level >= min_indent) |
| 248 | + || (min_indent == 0 && !empty_line) |
| 249 | + || (textobject == TextObject::Around && empty_line) |
| 250 | + { |
| 251 | + line_start -= 1; |
| 252 | + } else { |
| 253 | + break; |
| 254 | + } |
| 255 | + } |
| 256 | + } |
| 257 | + |
| 258 | + // Traverse forwards until there are no more lines indented the same or |
| 259 | + // greater, and extend the end of the range to it. |
| 260 | + if line_end < slice.len_lines() { |
| 261 | + for line in slice.lines_at(line_end + 1) { |
| 262 | + let indent_level = indent_level_for_line(line, tab_width, indent_width); |
| 263 | + let empty_line = rope_is_line_ending(line); |
| 264 | + if (min_indent > 0 && indent_level >= min_indent) |
| 265 | + || (min_indent == 0 && !empty_line) |
| 266 | + || (textobject == TextObject::Around && empty_line) |
| 267 | + { |
| 268 | + line_end += 1; |
| 269 | + } else { |
| 270 | + break; |
| 271 | + } |
| 272 | + } |
| 273 | + } |
| 274 | + |
| 275 | + let new_char_start = slice.line_to_char(line_start); |
| 276 | + let new_line_end_slice = slice.line(line_end); |
| 277 | + let mut new_char_end = new_line_end_slice.chars().count() + slice.line_to_char(line_end); |
| 278 | + |
| 279 | + // Unless the end of the new range is to the end of the buffer, we want to |
| 280 | + // trim the final line ending from the selection. |
| 281 | + if let Some(line_ending) = get_line_ending(&new_line_end_slice) { |
| 282 | + new_char_end = new_char_end.saturating_sub(line_ending.len_chars()); |
| 283 | + } |
| 284 | + |
| 285 | + Range::new(new_char_start, new_char_end).with_direction(range.direction()) |
| 286 | +} |
| 287 | + |
200 | 288 | pub fn textobject_pair_surround( |
201 | 289 | syntax: Option<&Syntax>, |
202 | 290 | slice: RopeSlice, |
@@ -497,6 +585,160 @@ mod test { |
497 | 585 | } |
498 | 586 | } |
499 | 587 |
|
| 588 | + #[test] |
| 589 | + fn test_textobject_indentation_level_inside() { |
| 590 | + let tests = [ |
| 591 | + ("#[|]#", "#[|]#", 1), |
| 592 | + ( |
| 593 | + "unindented\n\t#[i|]#ndented once", |
| 594 | + "unindented\n#[\tindented once|]#", |
| 595 | + 1, |
| 596 | + ), |
| 597 | + ( |
| 598 | + "unindented\n\t#[i|]#ndented once\n", |
| 599 | + "unindented\n#[\tindented once|]#\n", |
| 600 | + 1, |
| 601 | + ), |
| 602 | + ( |
| 603 | + "unindented\n\t#[|in]#dented once\n", |
| 604 | + "unindented\n#[|\tindented once]#\n", |
| 605 | + 1, |
| 606 | + ), |
| 607 | + ( |
| 608 | + "#[u|]#nindented\n\tindented once\n", |
| 609 | + "#[unindented\n\tindented once|]#\n", |
| 610 | + 1, |
| 611 | + ), |
| 612 | + ( |
| 613 | + "unindented\n\n\t#[i|]#ndented once and separated\n", |
| 614 | + "unindented\n\n#[\tindented once and separated|]#\n", |
| 615 | + 1, |
| 616 | + ), |
| 617 | + ( |
| 618 | + "#[u|]#nindented\n\n\tindented once and separated\n", |
| 619 | + "#[unindented|]#\n\n\tindented once and separated\n", |
| 620 | + 1, |
| 621 | + ), |
| 622 | + ( |
| 623 | + "unindented\n\nunindented again\n\tindented #[once|]#\nunindented one more time", |
| 624 | + "unindented\n\nunindented again\n#[\tindented once|]#\nunindented one more time", |
| 625 | + 1, |
| 626 | + ), |
| 627 | + ( |
| 628 | + "unindented\n\nunindented #[again\n\tindented|]# once\nunindented one more time\n", |
| 629 | + "unindented\n\n#[unindented again\n\tindented once\nunindented one more time|]#\n", |
| 630 | + 1, |
| 631 | + ), |
| 632 | + ( |
| 633 | + "unindented\n\tindented #[once\n\n\tindented once|]# and separated\n\tindented once again\nunindented one more time\n", |
| 634 | + "unindented\n#[\tindented once\n\n\tindented once and separated\n\tindented once again|]#\nunindented one more time\n", |
| 635 | + 1, |
| 636 | + ), |
| 637 | + ( |
| 638 | + "unindented\n\tindented once\n#[\n|]#\tindented once and separated\n\tindented once again\nunindented one more time\n", |
| 639 | + "unindented\n\tindented once\n#[\n|]#\tindented once and separated\n\tindented once again\nunindented one more time\n", |
| 640 | + 1, |
| 641 | + ), |
| 642 | + ( |
| 643 | + "unindented\n\tindented once\n\t\tindented #[twice|]#\n\tindented once again\nunindented\n", |
| 644 | + "unindented\n#[\tindented once\n\t\tindented twice\n\tindented once again|]#\nunindented\n", |
| 645 | + 2, |
| 646 | + ), |
| 647 | + ( |
| 648 | + "unindented\n\tindented once\n\t\tindented #[twice|]#\n\tindented once again\nunindented\n", |
| 649 | + "#[unindented\n\tindented once\n\t\tindented twice\n\tindented once again\nunindented|]#\n", |
| 650 | + 3, |
| 651 | + ), |
| 652 | + ]; |
| 653 | + |
| 654 | + for (before, expected, count) in tests { |
| 655 | + let (s, selection) = crate::test::print(before); |
| 656 | + let text = Rope::from(s.as_str()); |
| 657 | + let selection = selection.transform(|r| { |
| 658 | + textobject_indentation_level(text.slice(..), r, TextObject::Inside, count, 4, 4) |
| 659 | + }); |
| 660 | + let actual = crate::test::plain(s.as_ref(), &selection); |
| 661 | + assert_eq!(actual, expected, "\nbefore: `{:?}`", before); |
| 662 | + } |
| 663 | + } |
| 664 | + |
| 665 | + #[test] |
| 666 | + fn test_textobject_indentation_level_around() { |
| 667 | + let tests = [ |
| 668 | + ("#[|]#", "#[|]#", 1), |
| 669 | + ( |
| 670 | + "unindented\n\t#[i|]#ndented once", |
| 671 | + "unindented\n#[\tindented once|]#", |
| 672 | + 1, |
| 673 | + ), |
| 674 | + ( |
| 675 | + "unindented\n\t#[i|]#ndented once\n", |
| 676 | + "unindented\n#[\tindented once\n|]#", |
| 677 | + 1, |
| 678 | + ), |
| 679 | + ( |
| 680 | + "unindented\n\t#[|in]#dented once\n", |
| 681 | + "unindented\n#[|\tindented once\n]#", |
| 682 | + 1, |
| 683 | + ), |
| 684 | + ( |
| 685 | + "#[u|]#nindented\n\tindented once\n", |
| 686 | + "#[unindented\n\tindented once\n|]#", |
| 687 | + 1, |
| 688 | + ), |
| 689 | + ( |
| 690 | + "unindented\n\n\t#[i|]#ndented once and separated\n", |
| 691 | + "unindented\n#[\n\tindented once and separated\n|]#", |
| 692 | + 1, |
| 693 | + ), |
| 694 | + ( |
| 695 | + "#[u|]#nindented\n\n\tindented once and separated\n", |
| 696 | + "#[unindented\n\n\tindented once and separated\n|]#", |
| 697 | + 1, |
| 698 | + ), |
| 699 | + ( |
| 700 | + "unindented\n\nunindented again\n\tindented #[once|]#\nunindented one more time", |
| 701 | + "unindented\n\nunindented again\n#[\tindented once|]#\nunindented one more time", |
| 702 | + 1, |
| 703 | + ), |
| 704 | + ( |
| 705 | + "unindented\n\nunindented #[again\n\tindented|]# once\nunindented one more time\n", |
| 706 | + "#[unindented\n\nunindented again\n\tindented once\nunindented one more time\n|]#", |
| 707 | + 1, |
| 708 | + ), |
| 709 | + ( |
| 710 | + "unindented\n\tindented #[once\n\n\tindented once|]# and separated\n\tindented once again\nunindented one more time\n", |
| 711 | + "unindented\n#[\tindented once\n\n\tindented once and separated\n\tindented once again|]#\nunindented one more time\n", |
| 712 | + 1, |
| 713 | + ), |
| 714 | + ( |
| 715 | + "unindented\n\tindented once\n#[\n|]#\tindented once and separated\n\tindented once again\nunindented one more time\n", |
| 716 | + "unindented\n\tindented once\n#[\n|]#\tindented once and separated\n\tindented once again\nunindented one more time\n", |
| 717 | + 1, |
| 718 | + ), |
| 719 | + ( |
| 720 | + "unindented\n\tindented once\n\t\tindented #[twice|]#\n\tindented once again\nunindented\n", |
| 721 | + "unindented\n#[\tindented once\n\t\tindented twice\n\tindented once again|]#\nunindented\n", |
| 722 | + 2, |
| 723 | + ), |
| 724 | + ( |
| 725 | + "unindented\n\tindented once\n\t\tindented #[twice|]#\n\tindented once again\nunindented\n", |
| 726 | + "#[unindented\n\tindented once\n\t\tindented twice\n\tindented once again\nunindented\n|]#", |
| 727 | + 3, |
| 728 | + ), |
| 729 | + ]; |
| 730 | + |
| 731 | + for (before, expected, count) in tests { |
| 732 | + let (s, selection) = crate::test::print(before); |
| 733 | + let text = Rope::from(s.as_str()); |
| 734 | + let selection = selection.transform(|r| { |
| 735 | + textobject_indentation_level(text.slice(..), r, TextObject::Around, count, 4, 4) |
| 736 | + }); |
| 737 | + let actual = crate::test::plain(s.as_ref(), &selection); |
| 738 | + assert_eq!(actual, expected, "\nbefore: `{:?}`", before); |
| 739 | + } |
| 740 | + } |
| 741 | + |
500 | 742 | #[test] |
501 | 743 | fn test_textobject_surround() { |
502 | 744 | // (text, [(cursor position, textobject, final range, surround char, count), ...]) |
|
0 commit comments