Skip to content

Commit e3d09e2

Browse files
author
Adir Shemesh
committed
Add vim-like jump to textobject
1 parent ee3171c commit e3d09e2

File tree

2 files changed

+197
-9
lines changed

2 files changed

+197
-9
lines changed

helix-core/src/surround.rs

Lines changed: 188 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::fmt::Display;
22

3+
use crate::line_ending::line_end_char_index;
34
use crate::{movement::Direction, search, Range, Selection};
45
use ropey::RopeSlice;
56

@@ -146,10 +147,8 @@ pub fn find_nth_pairs_pos(
146147
// we should be searching on.
147148
return Err(Error::CursorOnAmbiguousPair);
148149
}
149-
(
150-
search::find_nth_prev(text, open, pos, n),
151-
search::find_nth_next(text, close, pos, n),
152-
)
150+
// Pairs with the same open and close char are only detected in the current line
151+
find_nth_surrounding_char_pair_in_line(&text, n, open, pos)
153152
} else {
154153
(
155154
find_nth_open_pair(text, open, close, pos, n),
@@ -160,6 +159,80 @@ pub fn find_nth_pairs_pos(
160159
Option::zip(open, close).ok_or(Error::PairNotFound)
161160
}
162161

162+
/// Find the position of surround pairs of `ch` - either ones that surround the cursor, or ones
163+
/// that appear later in the text. pairs with the same open and close char are only detected in
164+
/// the current line to avoid ambiguity (e.g. closing a pair of quotes in one line and opening
165+
/// another pair of quotes in the next line).
166+
pub fn find_nth_textobject_pairs_pos(
167+
text: RopeSlice,
168+
ch: char,
169+
range: Range,
170+
n: usize,
171+
) -> Result<(usize, usize)> {
172+
match find_nth_pairs_pos(text, ch, range, n) {
173+
Ok(pair) => Ok(pair),
174+
Err(Error::PairNotFound) if n == 1 => {
175+
// No surrounding pair found, we try to find the next pair of `ch` in the text.
176+
// n > 1 makes no sense in this context. (see test_one_surround_two_next)
177+
let (open, close) = get_pair(ch);
178+
let pos = range.cursor(text);
179+
180+
let (open, close) = if open == close {
181+
// The cursor is not surrounded by a pair of `ch` in the current line. Search
182+
// for the nth next pair of `ch` in the current line
183+
find_nth_next_char_pair_in_line(&text, n, close, pos)
184+
} else {
185+
// The cursor is not surrounded by the pair we are looking for. Search for the
186+
// nth next pair in the rest of the text
187+
let nth_next_open = search::find_nth_next(text, open, pos, n);
188+
(
189+
nth_next_open,
190+
nth_next_open.and_then(|next_open_pos| {
191+
find_nth_close_pair(text, open, close, next_open_pos, n)
192+
}),
193+
)
194+
};
195+
196+
Option::zip(open, close).ok_or(Error::PairNotFound)
197+
}
198+
Err(e) => Err(e),
199+
}
200+
}
201+
202+
/// Find the position of surround pairs of `ch` in that is after `pos` but still in the same line
203+
fn find_nth_next_char_pair_in_line(
204+
text: &RopeSlice,
205+
n: usize,
206+
ch: char,
207+
pos: usize,
208+
) -> (Option<usize>, Option<usize>) {
209+
let line_idx = text.char_to_line(pos);
210+
let line_end = line_end_char_index(text, line_idx);
211+
let lookup_slice = text.slice(pos..line_end);
212+
(
213+
search::find_nth_next(lookup_slice, ch, 0, n).map(|p| p + pos),
214+
search::find_nth_next(lookup_slice, ch, 0, n + 1).map(|p| p + pos),
215+
)
216+
}
217+
218+
/// Find the position of the nth surround pair of `ch` that surrounds the cursor in the current line
219+
fn find_nth_surrounding_char_pair_in_line(
220+
text: &RopeSlice,
221+
n: usize,
222+
ch: char,
223+
pos: usize,
224+
) -> (Option<usize>, Option<usize>) {
225+
let line_idx = text.char_to_line(pos);
226+
let line_start = text.line_to_char(line_idx);
227+
let line_end = line_end_char_index(text, line_idx);
228+
let inline_pos = pos - line_start;
229+
let lookup_slice = text.slice(line_start..line_end);
230+
(
231+
search::find_nth_prev(lookup_slice, ch, inline_pos, n).map(|p| p + line_start),
232+
search::find_nth_next(lookup_slice, ch, inline_pos, n).map(|p| p + line_start),
233+
)
234+
}
235+
163236
fn find_nth_open_pair(
164237
text: RopeSlice,
165238
open: char,
@@ -382,6 +455,117 @@ mod test {
382455
)
383456
}
384457

458+
#[test]
459+
fn test_find_only_same_line_quote() {
460+
#[rustfmt::skip]
461+
let (doc, selection, expectations) =
462+
// We want to find 'value2', not '\nkey2='
463+
rope_with_selections_and_expectations(
464+
"key='value'\nkey2='value2'",
465+
" \n^ _ _",
466+
);
467+
468+
assert_eq!(
469+
find_nth_textobject_pairs_pos(doc.slice(..), '\'', selection.primary(), 1)
470+
.expect("find should succeed"),
471+
(expectations[0], expectations[1])
472+
)
473+
}
474+
475+
#[test]
476+
fn test_find_multiline_parentheses_block() {
477+
#[rustfmt::skip]
478+
let (doc, selection, expectations) =
479+
rope_with_selections_and_expectations(
480+
"some (parentheses \n content) here",
481+
" _ \n^ _ ",
482+
);
483+
484+
assert_eq!(
485+
find_nth_textobject_pairs_pos(doc.slice(..), '(', selection.primary(), 1)
486+
.expect("find should succeed"),
487+
(expectations[0], expectations[1])
488+
)
489+
}
490+
491+
#[test]
492+
fn test_find_pair_after_cursor() {
493+
#[rustfmt::skip]
494+
let (doc, selection, expectations) =
495+
rope_with_selections_and_expectations(
496+
"some (parentheses content) here",
497+
"^ _ _ ",
498+
);
499+
500+
assert_eq!(
501+
find_nth_textobject_pairs_pos(doc.slice(..), '(', selection.primary(), 1)
502+
.expect("find should succeed"),
503+
(expectations[0], expectations[1])
504+
)
505+
}
506+
507+
#[test]
508+
fn test_find_quotes_after_cursor() {
509+
#[rustfmt::skip]
510+
let (doc, selection, expectations) =
511+
rope_with_selections_and_expectations(
512+
"some 'quoted text'",
513+
"^ _ _",
514+
);
515+
516+
assert_eq!(
517+
find_nth_textobject_pairs_pos(doc.slice(..), '\'', selection.primary(), 1)
518+
.expect("find should succeed"),
519+
(expectations[0], expectations[1])
520+
)
521+
}
522+
523+
#[test]
524+
fn test_do_not_find_next_line_quote() {
525+
#[rustfmt::skip]
526+
let (doc, selection, _) =
527+
rope_with_selections_and_expectations(
528+
"this line has no quotes\n'this line does'",
529+
"^ \n ",
530+
);
531+
532+
assert_eq!(
533+
find_nth_textobject_pairs_pos(doc.slice(..), '\'', selection.primary(), 1),
534+
Err(Error::PairNotFound)
535+
)
536+
}
537+
538+
#[test]
539+
fn test_find_next_line_parentheses() {
540+
#[rustfmt::skip]
541+
let (doc, selection, expectations) =
542+
rope_with_selections_and_expectations(
543+
"this line has no parentheses\n(this line does)",
544+
"^ \n_ _",
545+
);
546+
547+
assert_eq!(
548+
find_nth_textobject_pairs_pos(doc.slice(..), '(', selection.primary(), 1)
549+
.expect("find should succeed"),
550+
(expectations[0], expectations[1])
551+
)
552+
}
553+
554+
#[test]
555+
fn test_one_surround_two_next() {
556+
#[rustfmt::skip]
557+
let (doc, selection, _) =
558+
rope_with_selections_and_expectations(
559+
"(hello) ((world))",
560+
" ^ ",
561+
);
562+
563+
assert_eq!(
564+
find_nth_textobject_pairs_pos(doc.slice(..), '(', selection.primary(), 2),
565+
Err(Error::PairNotFound)
566+
)
567+
}
568+
385569
// Create a Rope and a matching Selection using a specification language.
386570
// ^ is a single-point selection.
387571
// _ is an expected index. These are returned as a Vec<usize> for use in assertions.

helix-core/src/textobject.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ fn textobject_pair_surround_impl(
225225
count: usize,
226226
) -> Range {
227227
let pair_pos = match ch {
228-
Some(ch) => surround::find_nth_pairs_pos(slice, ch, range, count),
228+
Some(ch) => surround::find_nth_textobject_pairs_pos(slice, ch, range, count),
229229
// Automatically find the closest surround pairs
230230
None => surround::find_nth_closest_pairs_pos(slice, range, count),
231231
};
@@ -503,11 +503,13 @@ mod test {
503503
(
504504
"simple (single) surround pairs",
505505
vec![
506-
(3, Inside, (3, 3), '(', 1),
506+
(20, Inside, (20, 20), '(', 1),
507+
(3, Inside, (8, 14), '(', 1),
507508
(7, Inside, (8, 14), ')', 1),
508509
(10, Inside, (8, 14), '(', 1),
509510
(14, Inside, (8, 14), ')', 1),
510-
(3, Around, (3, 3), '(', 1),
511+
(20, Around, (20, 20), '(', 1),
512+
(3, Around, (7, 15), '(', 1),
511513
(7, Around, (7, 15), ')', 1),
512514
(10, Around, (7, 15), '(', 1),
513515
(14, Around, (7, 15), ')', 1),
@@ -516,11 +518,13 @@ mod test {
516518
(
517519
"samexx 'single' surround pairs",
518520
vec![
519-
(3, Inside, (3, 3), '\'', 1),
521+
(20, Inside, (20, 20), '(', 1),
522+
(3, Inside, (8, 14), '\'', 1),
520523
(7, Inside, (7, 7), '\'', 1),
521524
(10, Inside, (8, 14), '\'', 1),
522525
(14, Inside, (14, 14), '\'', 1),
523-
(3, Around, (3, 3), '\'', 1),
526+
(20, Around, (20, 20), '(', 1),
527+
(3, Around, (7, 15), '\'', 1),
524528
(7, Around, (7, 7), '\'', 1),
525529
(10, Around, (7, 15), '\'', 1),
526530
(14, Around, (14, 14), '\'', 1),

0 commit comments

Comments
 (0)