mirror of https://github.com/helix-editor/helix
Update surround commands to work with gap indexing.
parent
753f7f381b
commit
b4c59b444c
|
@ -56,7 +56,7 @@ pub fn move_horizontally(
|
||||||
};
|
};
|
||||||
|
|
||||||
// Compute the final new range.
|
// Compute the final new range.
|
||||||
range.put(slice, behaviour == Extend, new_pos)
|
range.put(slice, new_pos, behaviour == Extend)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_vertically(
|
pub fn move_vertically(
|
||||||
|
@ -106,7 +106,7 @@ pub fn move_vertically(
|
||||||
new_pos
|
new_pos
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut new_range = range.put(slice, true, new_head);
|
let mut new_range = range.put(slice, new_head, true);
|
||||||
new_range.horiz = Some(horiz);
|
new_range.horiz = Some(horiz);
|
||||||
new_range
|
new_range
|
||||||
}
|
}
|
||||||
|
@ -427,7 +427,7 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn vertical_moves_in_single_column() {
|
fn vertical_moves_in_single_column() {
|
||||||
let text = Rope::from(MULTILINE_SAMPLE);
|
let text = Rope::from(MULTILINE_SAMPLE);
|
||||||
let slice = dbg!(&text).slice(..);
|
let slice = text.slice(..);
|
||||||
let position = pos_at_coords(slice, (0, 0).into());
|
let position = pos_at_coords(slice, (0, 0).into());
|
||||||
let mut range = Range::point(position);
|
let mut range = Range::point(position);
|
||||||
let moves_and_expected_coordinates = IntoIter::new([
|
let moves_and_expected_coordinates = IntoIter::new([
|
||||||
|
|
|
@ -7,12 +7,11 @@ pub fn find_nth_next(
|
||||||
n: usize,
|
n: usize,
|
||||||
inclusive: bool,
|
inclusive: bool,
|
||||||
) -> Option<usize> {
|
) -> Option<usize> {
|
||||||
if pos >= text.len_chars() {
|
if pos >= text.len_chars() || n == 0 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// start searching right after pos
|
let mut chars = text.chars_at(pos);
|
||||||
let mut chars = text.chars_at(pos + 1);
|
|
||||||
|
|
||||||
for _ in 0..n {
|
for _ in 0..n {
|
||||||
loop {
|
loop {
|
||||||
|
@ -40,14 +39,17 @@ pub fn find_nth_prev(
|
||||||
n: usize,
|
n: usize,
|
||||||
inclusive: bool,
|
inclusive: bool,
|
||||||
) -> Option<usize> {
|
) -> Option<usize> {
|
||||||
// start searching right before pos
|
if pos == 0 || n == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
let mut chars = text.chars_at(pos);
|
let mut chars = text.chars_at(pos);
|
||||||
|
|
||||||
for _ in 0..n {
|
for _ in 0..n {
|
||||||
loop {
|
loop {
|
||||||
let c = chars.prev()?;
|
let c = chars.prev()?;
|
||||||
|
|
||||||
pos = pos.saturating_sub(1);
|
pos -= 1;
|
||||||
|
|
||||||
if c == ch {
|
if c == ch {
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -217,7 +217,7 @@ impl Range {
|
||||||
/// grapheme-aligned.
|
/// grapheme-aligned.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn put(self, text: RopeSlice, extend: bool, char_idx: usize) -> Range {
|
pub fn put(self, text: RopeSlice, char_idx: usize, extend: bool) -> Range {
|
||||||
let anchor = if !extend {
|
let anchor = if !extend {
|
||||||
char_idx
|
char_idx
|
||||||
} else if self.head >= self.anchor && char_idx < self.anchor {
|
} else if self.head >= self.anchor && char_idx < self.anchor {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::graphemes::next_grapheme_boundary;
|
||||||
use crate::{search, Selection};
|
use crate::{search, Selection};
|
||||||
use ropey::RopeSlice;
|
use ropey::RopeSlice;
|
||||||
|
|
||||||
|
@ -40,23 +41,35 @@ pub fn find_nth_pairs_pos(
|
||||||
) -> Option<(usize, usize)> {
|
) -> Option<(usize, usize)> {
|
||||||
let (open, close) = get_pair(ch);
|
let (open, close) = get_pair(ch);
|
||||||
|
|
||||||
let (open_pos, close_pos) = if open == close {
|
if text.len_chars() < 2 || pos >= text.len_chars() {
|
||||||
let prev = search::find_nth_prev(text, open, pos, n, true);
|
return None;
|
||||||
let next = search::find_nth_next(text, close, pos, n, true);
|
}
|
||||||
if text.char(pos) == open {
|
|
||||||
// cursor is *on* a pair
|
if open == close {
|
||||||
next.map(|n| (pos, n)).or_else(|| prev.map(|p| (p, pos)))?
|
if Some(open) == text.get_char(pos) {
|
||||||
|
// Special case: cursor is directly on a matching char.
|
||||||
|
match pos {
|
||||||
|
0 => Some((pos, search::find_nth_next(text, close, pos + 1, n, true)?)),
|
||||||
|
_ if (pos + 1) == text.len_chars() => Some((
|
||||||
|
search::find_nth_prev(text, open, pos, n, true)?,
|
||||||
|
text.len_chars(),
|
||||||
|
)),
|
||||||
|
// We return no match because there's no way to know which
|
||||||
|
// side of the char we should be searching on.
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
(prev?, next?)
|
Some((
|
||||||
|
search::find_nth_prev(text, open, pos, n, true)?,
|
||||||
|
search::find_nth_next(text, close, pos, n, true)?,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
(
|
Some((
|
||||||
find_nth_open_pair(text, open, close, pos, n)?,
|
find_nth_open_pair(text, open, close, pos, n)?,
|
||||||
find_nth_close_pair(text, open, close, pos, n)?,
|
next_grapheme_boundary(text, find_nth_close_pair(text, open, close, pos, n)?),
|
||||||
)
|
))
|
||||||
};
|
}
|
||||||
|
|
||||||
Some((open_pos, close_pos))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_nth_open_pair(
|
fn find_nth_open_pair(
|
||||||
|
@ -173,12 +186,13 @@ mod test {
|
||||||
let slice = doc.slice(..);
|
let slice = doc.slice(..);
|
||||||
|
|
||||||
// cursor on [t]ext
|
// cursor on [t]ext
|
||||||
assert_eq!(find_nth_pairs_pos(slice, '(', 6, 1), Some((5, 10)));
|
assert_eq!(find_nth_pairs_pos(slice, '(', 6, 1), Some((5, 11)));
|
||||||
assert_eq!(find_nth_pairs_pos(slice, ')', 6, 1), Some((5, 10)));
|
assert_eq!(find_nth_pairs_pos(slice, ')', 6, 1), Some((5, 11)));
|
||||||
// cursor on so[m]e
|
// cursor on so[m]e
|
||||||
assert_eq!(find_nth_pairs_pos(slice, '(', 2, 1), None);
|
assert_eq!(find_nth_pairs_pos(slice, '(', 2, 1), None);
|
||||||
// cursor on bracket itself
|
// cursor on bracket itself
|
||||||
assert_eq!(find_nth_pairs_pos(slice, '(', 5, 1), Some((5, 10)));
|
assert_eq!(find_nth_pairs_pos(slice, '(', 5, 1), Some((5, 11)));
|
||||||
|
assert_eq!(find_nth_pairs_pos(slice, '(', 10, 1), Some((5, 11)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -187,9 +201,9 @@ mod test {
|
||||||
let slice = doc.slice(..);
|
let slice = doc.slice(..);
|
||||||
|
|
||||||
// cursor on go[o]d
|
// cursor on go[o]d
|
||||||
assert_eq!(find_nth_pairs_pos(slice, '(', 13, 1), Some((10, 15)));
|
assert_eq!(find_nth_pairs_pos(slice, '(', 13, 1), Some((10, 16)));
|
||||||
assert_eq!(find_nth_pairs_pos(slice, '(', 13, 2), Some((4, 21)));
|
assert_eq!(find_nth_pairs_pos(slice, '(', 13, 2), Some((4, 22)));
|
||||||
assert_eq!(find_nth_pairs_pos(slice, '(', 13, 3), Some((0, 27)));
|
assert_eq!(find_nth_pairs_pos(slice, '(', 13, 3), Some((0, 28)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -198,14 +212,14 @@ mod test {
|
||||||
let slice = doc.slice(..);
|
let slice = doc.slice(..);
|
||||||
|
|
||||||
// cursor on go[o]d
|
// cursor on go[o]d
|
||||||
assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 1), Some((10, 15)));
|
assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 1), Some((10, 16)));
|
||||||
assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 2), Some((4, 21)));
|
assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 2), Some((4, 22)));
|
||||||
assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 3), Some((0, 27)));
|
assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 3), Some((0, 28)));
|
||||||
// cursor on the quotes
|
// cursor on the quotes
|
||||||
assert_eq!(find_nth_pairs_pos(slice, '\'', 10, 1), Some((10, 15)));
|
assert_eq!(find_nth_pairs_pos(slice, '\'', 10, 1), None);
|
||||||
// this is the best we can do since opening and closing pairs are same
|
// this is the best we can do since opening and closing pairs are same
|
||||||
assert_eq!(find_nth_pairs_pos(slice, '\'', 0, 1), Some((0, 4)));
|
assert_eq!(find_nth_pairs_pos(slice, '\'', 0, 1), Some((0, 5)));
|
||||||
assert_eq!(find_nth_pairs_pos(slice, '\'', 27, 1), Some((21, 27)));
|
assert_eq!(find_nth_pairs_pos(slice, '\'', 27, 1), Some((21, 28)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -214,8 +228,8 @@ mod test {
|
||||||
let slice = doc.slice(..);
|
let slice = doc.slice(..);
|
||||||
|
|
||||||
// cursor on go[o]d
|
// cursor on go[o]d
|
||||||
assert_eq!(find_nth_pairs_pos(slice, '(', 15, 1), Some((5, 24)));
|
assert_eq!(find_nth_pairs_pos(slice, '(', 15, 1), Some((5, 25)));
|
||||||
assert_eq!(find_nth_pairs_pos(slice, '(', 15, 2), Some((0, 31)));
|
assert_eq!(find_nth_pairs_pos(slice, '(', 15, 2), Some((0, 32)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -224,9 +238,9 @@ mod test {
|
||||||
let slice = doc.slice(..);
|
let slice = doc.slice(..);
|
||||||
|
|
||||||
// cursor on go[o]d
|
// cursor on go[o]d
|
||||||
assert_eq!(find_nth_pairs_pos(slice, '{', 13, 1), Some((10, 15)));
|
assert_eq!(find_nth_pairs_pos(slice, '{', 13, 1), Some((10, 16)));
|
||||||
assert_eq!(find_nth_pairs_pos(slice, '[', 13, 1), Some((4, 21)));
|
assert_eq!(find_nth_pairs_pos(slice, '[', 13, 1), Some((4, 22)));
|
||||||
assert_eq!(find_nth_pairs_pos(slice, '(', 13, 1), Some((0, 27)));
|
assert_eq!(find_nth_pairs_pos(slice, '(', 13, 1), Some((0, 28)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -243,7 +257,7 @@ mod test {
|
||||||
get_surround_pos(slice, &selection, '(', 1)
|
get_surround_pos(slice, &selection, '(', 1)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.as_slice(),
|
.as_slice(),
|
||||||
&[0, 5, 7, 13, 15, 23]
|
&[0, 6, 7, 14, 15, 24]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,21 +1,16 @@
|
||||||
use ropey::RopeSlice;
|
use ropey::RopeSlice;
|
||||||
|
|
||||||
use crate::chars::{categorize_char, char_is_line_ending, char_is_whitespace, CharCategory};
|
use crate::chars::{categorize_char, CharCategory};
|
||||||
use crate::movement::{self, Direction};
|
use crate::graphemes::{next_grapheme_boundary, prev_grapheme_boundary};
|
||||||
|
use crate::movement::Direction;
|
||||||
use crate::surround;
|
use crate::surround;
|
||||||
use crate::Range;
|
use crate::Range;
|
||||||
|
|
||||||
fn this_word_end_pos(slice: RopeSlice, pos: usize) -> usize {
|
fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction) -> usize {
|
||||||
this_word_bound_pos(slice, pos, Direction::Forward)
|
use CharCategory::{Eol, Whitespace};
|
||||||
}
|
|
||||||
|
|
||||||
fn this_word_start_pos(slice: RopeSlice, pos: usize) -> usize {
|
|
||||||
this_word_bound_pos(slice, pos, Direction::Backward)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn this_word_bound_pos(slice: RopeSlice, mut pos: usize, direction: Direction) -> usize {
|
|
||||||
let iter = match direction {
|
let iter = match direction {
|
||||||
Direction::Forward => slice.chars_at(pos + 1),
|
Direction::Forward => slice.chars_at(pos),
|
||||||
Direction::Backward => {
|
Direction::Backward => {
|
||||||
let mut iter = slice.chars_at(pos);
|
let mut iter = slice.chars_at(pos);
|
||||||
iter.reverse();
|
iter.reverse();
|
||||||
|
@ -23,25 +18,32 @@ fn this_word_bound_pos(slice: RopeSlice, mut pos: usize, direction: Direction) -
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
match categorize_char(slice.char(pos)) {
|
let mut prev_category = match direction {
|
||||||
CharCategory::Eol | CharCategory::Whitespace => pos,
|
Direction::Forward if pos == 0 => Whitespace,
|
||||||
category => {
|
Direction::Forward => categorize_char(slice.char(pos - 1)),
|
||||||
for peek in iter {
|
Direction::Backward if pos == slice.len_chars() => Whitespace,
|
||||||
let curr_category = categorize_char(peek);
|
Direction::Backward => categorize_char(slice.char(pos)),
|
||||||
if curr_category != category
|
};
|
||||||
|| curr_category == CharCategory::Eol
|
|
||||||
|| curr_category == CharCategory::Whitespace
|
for ch in iter {
|
||||||
{
|
match categorize_char(ch) {
|
||||||
|
Eol | Whitespace => return pos,
|
||||||
|
category => {
|
||||||
|
if category != prev_category && pos != 0 && pos != slice.len_chars() {
|
||||||
return pos;
|
return pos;
|
||||||
}
|
} else {
|
||||||
pos = match direction {
|
if direction == Direction::Forward {
|
||||||
Direction::Forward => pos + 1,
|
pos += 1;
|
||||||
Direction::Backward => pos.saturating_sub(1),
|
} else {
|
||||||
|
pos = pos.saturating_sub(1);
|
||||||
|
}
|
||||||
|
prev_category = category;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pos
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pos
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||||
|
@ -55,46 +57,42 @@ pub fn textobject_word(
|
||||||
slice: RopeSlice,
|
slice: RopeSlice,
|
||||||
range: Range,
|
range: Range,
|
||||||
textobject: TextObject,
|
textobject: TextObject,
|
||||||
count: usize,
|
_count: usize,
|
||||||
) -> Range {
|
) -> Range {
|
||||||
let this_word_start = this_word_start_pos(slice, range.head);
|
// For 1-width cursor semantics.
|
||||||
let this_word_end = this_word_end_pos(slice, range.head);
|
let head = if range.head > range.anchor {
|
||||||
|
prev_grapheme_boundary(slice, range.head)
|
||||||
let (anchor, head);
|
} else {
|
||||||
match textobject {
|
range.head
|
||||||
TextObject::Inside => {
|
|
||||||
anchor = this_word_start;
|
|
||||||
head = this_word_end;
|
|
||||||
}
|
|
||||||
TextObject::Around => {
|
|
||||||
if slice
|
|
||||||
.get_char(this_word_end + 1)
|
|
||||||
.map_or(true, char_is_line_ending)
|
|
||||||
{
|
|
||||||
head = this_word_end;
|
|
||||||
if slice
|
|
||||||
.get_char(this_word_start.saturating_sub(1))
|
|
||||||
.map_or(true, char_is_line_ending)
|
|
||||||
{
|
|
||||||
// single word on a line
|
|
||||||
anchor = this_word_start;
|
|
||||||
} else {
|
|
||||||
// last word on a line, select the whitespace before it too
|
|
||||||
anchor = movement::move_prev_word_end(slice, range, count).head;
|
|
||||||
}
|
|
||||||
} else if char_is_whitespace(slice.char(range.head)) {
|
|
||||||
// select whole whitespace and next word
|
|
||||||
head = movement::move_next_word_end(slice, range, count).head;
|
|
||||||
anchor = movement::backwards_skip_while(slice, range.head, |c| c.is_whitespace())
|
|
||||||
.map(|p| p + 1) // p is first *non* whitespace char, so +1 to get whitespace pos
|
|
||||||
.unwrap_or(0);
|
|
||||||
} else {
|
|
||||||
head = movement::move_next_word_start(slice, range, count).head;
|
|
||||||
anchor = this_word_start;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
Range::new(anchor, head)
|
|
||||||
|
let word_start = find_word_boundary(slice, head, Direction::Backward);
|
||||||
|
let word_end = match slice.get_char(head).map(categorize_char) {
|
||||||
|
None | Some(CharCategory::Whitespace | CharCategory::Eol) => head,
|
||||||
|
_ => find_word_boundary(slice, head + 1, Direction::Forward),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Special case.
|
||||||
|
if word_start == word_end {
|
||||||
|
return Range::new(word_start, word_end);
|
||||||
|
}
|
||||||
|
|
||||||
|
match textobject {
|
||||||
|
TextObject::Inside => Range::new(word_start, word_end),
|
||||||
|
TextObject::Around => Range::new(
|
||||||
|
match slice
|
||||||
|
.get_char(word_start.saturating_sub(1))
|
||||||
|
.map(categorize_char)
|
||||||
|
{
|
||||||
|
None | Some(CharCategory::Eol) => word_start,
|
||||||
|
_ => prev_grapheme_boundary(slice, word_start),
|
||||||
|
},
|
||||||
|
match slice.get_char(word_end).map(categorize_char) {
|
||||||
|
None | Some(CharCategory::Eol) => word_end,
|
||||||
|
_ => next_grapheme_boundary(slice, word_end),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn textobject_surround(
|
pub fn textobject_surround(
|
||||||
|
@ -106,7 +104,10 @@ pub fn textobject_surround(
|
||||||
) -> Range {
|
) -> Range {
|
||||||
surround::find_nth_pairs_pos(slice, ch, range.head, count)
|
surround::find_nth_pairs_pos(slice, ch, range.head, count)
|
||||||
.map(|(anchor, head)| match textobject {
|
.map(|(anchor, head)| match textobject {
|
||||||
TextObject::Inside => Range::new(anchor + 1, head.saturating_sub(1)),
|
TextObject::Inside => Range::new(
|
||||||
|
next_grapheme_boundary(slice, anchor),
|
||||||
|
prev_grapheme_boundary(slice, head),
|
||||||
|
),
|
||||||
TextObject::Around => Range::new(anchor, head),
|
TextObject::Around => Range::new(anchor, head),
|
||||||
})
|
})
|
||||||
.unwrap_or(range)
|
.unwrap_or(range)
|
||||||
|
@ -126,70 +127,70 @@ mod test {
|
||||||
let tests = &[
|
let tests = &[
|
||||||
(
|
(
|
||||||
"cursor at beginning of doc",
|
"cursor at beginning of doc",
|
||||||
vec![(0, Inside, (0, 5)), (0, Around, (0, 6))],
|
vec![(0, Inside, (0, 6)), (0, Around, (0, 7))],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"cursor at middle of word",
|
"cursor at middle of word",
|
||||||
vec![
|
vec![
|
||||||
(13, Inside, (10, 15)),
|
(13, Inside, (10, 16)),
|
||||||
(10, Inside, (10, 15)),
|
(10, Inside, (10, 16)),
|
||||||
(15, Inside, (10, 15)),
|
(15, Inside, (10, 16)),
|
||||||
(13, Around, (10, 16)),
|
(13, Around, (9, 17)),
|
||||||
(10, Around, (10, 16)),
|
(10, Around, (9, 17)),
|
||||||
(15, Around, (10, 16)),
|
(15, Around, (9, 17)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"cursor between word whitespace",
|
"cursor between word whitespace",
|
||||||
vec![(6, Inside, (6, 6)), (6, Around, (6, 13))],
|
vec![(6, Inside, (6, 6)), (6, Around, (6, 6))],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"cursor on word before newline\n",
|
"cursor on word before newline\n",
|
||||||
vec![
|
vec![
|
||||||
(22, Inside, (22, 28)),
|
(22, Inside, (22, 29)),
|
||||||
(28, Inside, (22, 28)),
|
(28, Inside, (22, 29)),
|
||||||
(25, Inside, (22, 28)),
|
(25, Inside, (22, 29)),
|
||||||
(22, Around, (21, 28)),
|
(22, Around, (21, 29)),
|
||||||
(28, Around, (21, 28)),
|
(28, Around, (21, 29)),
|
||||||
(25, Around, (21, 28)),
|
(25, Around, (21, 29)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"cursor on newline\nnext line",
|
"cursor on newline\nnext line",
|
||||||
vec![(17, Inside, (17, 17)), (17, Around, (17, 22))],
|
vec![(17, Inside, (17, 17)), (17, Around, (17, 17))],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"cursor on word after newline\nnext line",
|
"cursor on word after newline\nnext line",
|
||||||
vec![
|
vec![
|
||||||
(29, Inside, (29, 32)),
|
(29, Inside, (29, 33)),
|
||||||
(30, Inside, (29, 32)),
|
(30, Inside, (29, 33)),
|
||||||
(32, Inside, (29, 32)),
|
(32, Inside, (29, 33)),
|
||||||
(29, Around, (29, 33)),
|
(29, Around, (29, 34)),
|
||||||
(30, Around, (29, 33)),
|
(30, Around, (29, 34)),
|
||||||
(32, Around, (29, 33)),
|
(32, Around, (29, 34)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"cursor on #$%:;* punctuation",
|
"cursor on #$%:;* punctuation",
|
||||||
vec![
|
vec![
|
||||||
(13, Inside, (10, 15)),
|
(13, Inside, (10, 16)),
|
||||||
(10, Inside, (10, 15)),
|
(10, Inside, (10, 16)),
|
||||||
(15, Inside, (10, 15)),
|
(15, Inside, (10, 16)),
|
||||||
(13, Around, (10, 16)),
|
(13, Around, (9, 17)),
|
||||||
(10, Around, (10, 16)),
|
(10, Around, (9, 17)),
|
||||||
(15, Around, (10, 16)),
|
(15, Around, (9, 17)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"cursor on punc%^#$:;.tuation",
|
"cursor on punc%^#$:;.tuation",
|
||||||
vec![
|
vec![
|
||||||
(14, Inside, (14, 20)),
|
(14, Inside, (14, 21)),
|
||||||
(20, Inside, (14, 20)),
|
(20, Inside, (14, 21)),
|
||||||
(17, Inside, (14, 20)),
|
(17, Inside, (14, 21)),
|
||||||
(14, Around, (14, 20)),
|
(14, Around, (13, 22)),
|
||||||
// FIXME: edge case
|
// FIXME: edge case
|
||||||
// (20, Around, (14, 20)),
|
// (20, Around, (14, 20)),
|
||||||
(17, Around, (14, 20)),
|
(17, Around, (13, 22)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
@ -198,14 +199,14 @@ mod test {
|
||||||
(9, Inside, (9, 9)),
|
(9, Inside, (9, 9)),
|
||||||
(10, Inside, (10, 10)),
|
(10, Inside, (10, 10)),
|
||||||
(11, Inside, (11, 11)),
|
(11, Inside, (11, 11)),
|
||||||
(9, Around, (9, 16)),
|
(9, Around, (9, 9)),
|
||||||
(10, Around, (9, 16)),
|
(10, Around, (10, 10)),
|
||||||
(11, Around, (9, 16)),
|
(11, Around, (11, 11)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"cursor at end of doc",
|
"cursor at end of doc",
|
||||||
vec![(19, Inside, (17, 19)), (19, Around, (16, 19))],
|
vec![(19, Inside, (17, 20)), (19, Around, (16, 20))],
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -234,67 +235,67 @@ mod test {
|
||||||
"simple (single) surround pairs",
|
"simple (single) surround pairs",
|
||||||
vec![
|
vec![
|
||||||
(3, Inside, (3, 3), '(', 1),
|
(3, Inside, (3, 3), '(', 1),
|
||||||
(7, Inside, (8, 13), ')', 1),
|
(7, Inside, (8, 14), ')', 1),
|
||||||
(10, Inside, (8, 13), '(', 1),
|
(10, Inside, (8, 14), '(', 1),
|
||||||
(14, Inside, (8, 13), ')', 1),
|
(14, Inside, (8, 14), ')', 1),
|
||||||
(3, Around, (3, 3), '(', 1),
|
(3, Around, (3, 3), '(', 1),
|
||||||
(7, Around, (7, 14), ')', 1),
|
(7, Around, (7, 15), ')', 1),
|
||||||
(10, Around, (7, 14), '(', 1),
|
(10, Around, (7, 15), '(', 1),
|
||||||
(14, Around, (7, 14), ')', 1),
|
(14, Around, (7, 15), ')', 1),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"samexx 'single' surround pairs",
|
"samexx 'single' surround pairs",
|
||||||
vec![
|
vec![
|
||||||
(3, Inside, (3, 3), '\'', 1),
|
(3, Inside, (3, 3), '\'', 1),
|
||||||
(7, Inside, (8, 13), '\'', 1),
|
(7, Inside, (7, 7), '\'', 1),
|
||||||
(10, Inside, (8, 13), '\'', 1),
|
(10, Inside, (8, 14), '\'', 1),
|
||||||
(14, Inside, (8, 13), '\'', 1),
|
(14, Inside, (14, 14), '\'', 1),
|
||||||
(3, Around, (3, 3), '\'', 1),
|
(3, Around, (3, 3), '\'', 1),
|
||||||
(7, Around, (7, 14), '\'', 1),
|
(7, Around, (7, 7), '\'', 1),
|
||||||
(10, Around, (7, 14), '\'', 1),
|
(10, Around, (7, 15), '\'', 1),
|
||||||
(14, Around, (7, 14), '\'', 1),
|
(14, Around, (14, 14), '\'', 1),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"(nested (surround (pairs)) 3 levels)",
|
"(nested (surround (pairs)) 3 levels)",
|
||||||
vec![
|
vec![
|
||||||
(0, Inside, (1, 34), '(', 1),
|
(0, Inside, (1, 35), '(', 1),
|
||||||
(6, Inside, (1, 34), ')', 1),
|
(6, Inside, (1, 35), ')', 1),
|
||||||
(8, Inside, (9, 24), '(', 1),
|
(8, Inside, (9, 25), '(', 1),
|
||||||
(8, Inside, (9, 34), ')', 2),
|
(8, Inside, (9, 35), ')', 2),
|
||||||
(20, Inside, (9, 24), '(', 2),
|
(20, Inside, (9, 25), '(', 2),
|
||||||
(20, Inside, (1, 34), ')', 3),
|
(20, Inside, (1, 35), ')', 3),
|
||||||
(0, Around, (0, 35), '(', 1),
|
(0, Around, (0, 36), '(', 1),
|
||||||
(6, Around, (0, 35), ')', 1),
|
(6, Around, (0, 36), ')', 1),
|
||||||
(8, Around, (8, 25), '(', 1),
|
(8, Around, (8, 26), '(', 1),
|
||||||
(8, Around, (8, 35), ')', 2),
|
(8, Around, (8, 36), ')', 2),
|
||||||
(20, Around, (8, 25), '(', 2),
|
(20, Around, (8, 26), '(', 2),
|
||||||
(20, Around, (0, 35), ')', 3),
|
(20, Around, (0, 36), ')', 3),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"(mixed {surround [pair] same} line)",
|
"(mixed {surround [pair] same} line)",
|
||||||
vec![
|
vec![
|
||||||
(2, Inside, (1, 33), '(', 1),
|
(2, Inside, (1, 34), '(', 1),
|
||||||
(9, Inside, (8, 27), '{', 1),
|
(9, Inside, (8, 28), '{', 1),
|
||||||
(18, Inside, (18, 21), '[', 1),
|
(18, Inside, (18, 22), '[', 1),
|
||||||
(2, Around, (0, 34), '(', 1),
|
(2, Around, (0, 35), '(', 1),
|
||||||
(9, Around, (7, 28), '{', 1),
|
(9, Around, (7, 29), '{', 1),
|
||||||
(18, Around, (17, 22), '[', 1),
|
(18, Around, (17, 23), '[', 1),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"(stepped (surround) pairs (should) skip)",
|
"(stepped (surround) pairs (should) skip)",
|
||||||
vec![(22, Inside, (1, 38), '(', 1), (22, Around, (0, 39), '(', 1)],
|
vec![(22, Inside, (1, 39), '(', 1), (22, Around, (0, 40), '(', 1)],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"[surround pairs{\non different]\nlines}",
|
"[surround pairs{\non different]\nlines}",
|
||||||
vec![
|
vec![
|
||||||
(7, Inside, (1, 28), '[', 1),
|
(7, Inside, (1, 29), '[', 1),
|
||||||
(15, Inside, (16, 35), '{', 1),
|
(15, Inside, (16, 36), '{', 1),
|
||||||
(7, Around, (0, 29), '[', 1),
|
(7, Around, (0, 30), '[', 1),
|
||||||
(15, Around, (15, 36), '{', 1),
|
(15, Around, (15, 37), '{', 1),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
use helix_core::{
|
use helix_core::{
|
||||||
comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes, indent,
|
comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes, indent,
|
||||||
line_ending::{
|
line_ending::{get_line_ending_of_str, line_end_char_index, str_is_line_ending},
|
||||||
get_line_ending_of_str, line_end_char_index, rope_end_without_line_ending,
|
|
||||||
str_is_line_ending,
|
|
||||||
},
|
|
||||||
match_brackets,
|
match_brackets,
|
||||||
movement::{self, Direction},
|
movement::{self, Direction},
|
||||||
object, pos_at_coords,
|
object, pos_at_coords,
|
||||||
|
@ -392,20 +389,14 @@ fn goto_line_end(cx: &mut Context) {
|
||||||
doc.set_selection(
|
doc.set_selection(
|
||||||
view.id,
|
view.id,
|
||||||
doc.selection(view.id).clone().transform(|range| {
|
doc.selection(view.id).clone().transform(|range| {
|
||||||
let text = doc.text();
|
let text = doc.text().slice(..);
|
||||||
let line = text.char_to_line(range.head);
|
let line = text.char_to_line(range.head);
|
||||||
|
|
||||||
let pos = line_end_char_index(&text.slice(..), line);
|
let pos = line_end_char_index(&text, line);
|
||||||
let pos = graphemes::nth_prev_grapheme_boundary(text.slice(..), pos, 1);
|
let pos = graphemes::nth_prev_grapheme_boundary(text, pos, 1);
|
||||||
let pos = range.head.max(pos).max(text.line_to_char(line));
|
let pos = range.head.max(pos).max(text.line_to_char(line));
|
||||||
|
|
||||||
Range::new(
|
range.put(text, pos, doc.mode == Mode::Select)
|
||||||
match doc.mode {
|
|
||||||
Mode::Normal | Mode::Insert => pos,
|
|
||||||
Mode::Select => range.anchor,
|
|
||||||
},
|
|
||||||
pos,
|
|
||||||
)
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -416,11 +407,11 @@ fn goto_line_end_newline(cx: &mut Context) {
|
||||||
doc.set_selection(
|
doc.set_selection(
|
||||||
view.id,
|
view.id,
|
||||||
doc.selection(view.id).clone().transform(|range| {
|
doc.selection(view.id).clone().transform(|range| {
|
||||||
let text = doc.text();
|
let text = doc.text().slice(..);
|
||||||
let line = text.char_to_line(range.head);
|
let line = text.char_to_line(range.head);
|
||||||
|
|
||||||
let pos = line_end_char_index(&text.slice(..), line);
|
let pos = line_end_char_index(&text, line);
|
||||||
Range::new(pos, pos)
|
range.put(text, pos, doc.mode == Mode::Select)
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -430,18 +421,12 @@ fn goto_line_start(cx: &mut Context) {
|
||||||
doc.set_selection(
|
doc.set_selection(
|
||||||
view.id,
|
view.id,
|
||||||
doc.selection(view.id).clone().transform(|range| {
|
doc.selection(view.id).clone().transform(|range| {
|
||||||
let text = doc.text();
|
let text = doc.text().slice(..);
|
||||||
let line = text.char_to_line(range.head);
|
let line = text.char_to_line(range.head);
|
||||||
|
|
||||||
// adjust to start of the line
|
// adjust to start of the line
|
||||||
let pos = text.line_to_char(line);
|
let pos = text.line_to_char(line);
|
||||||
Range::new(
|
range.put(text, pos, doc.mode == Mode::Select)
|
||||||
match doc.mode {
|
|
||||||
Mode::Normal | Mode::Insert => pos,
|
|
||||||
Mode::Select => range.anchor,
|
|
||||||
},
|
|
||||||
pos,
|
|
||||||
)
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -451,18 +436,12 @@ fn goto_first_nonwhitespace(cx: &mut Context) {
|
||||||
doc.set_selection(
|
doc.set_selection(
|
||||||
view.id,
|
view.id,
|
||||||
doc.selection(view.id).clone().transform(|range| {
|
doc.selection(view.id).clone().transform(|range| {
|
||||||
let text = doc.text();
|
let text = doc.text().slice(..);
|
||||||
let line_idx = text.char_to_line(range.head);
|
let line_idx = text.char_to_line(range.head);
|
||||||
|
|
||||||
if let Some(pos) = find_first_non_whitespace_char(text.line(line_idx)) {
|
if let Some(pos) = find_first_non_whitespace_char(text.line(line_idx)) {
|
||||||
let pos = pos + text.line_to_char(line_idx);
|
let pos = pos + text.line_to_char(line_idx);
|
||||||
Range::new(
|
range.put(text, pos, doc.mode == Mode::Select)
|
||||||
match doc.mode {
|
|
||||||
Mode::Normal | Mode::Insert => pos,
|
|
||||||
Mode::Select => range.anchor,
|
|
||||||
},
|
|
||||||
pos,
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
range
|
range
|
||||||
}
|
}
|
||||||
|
@ -581,9 +560,7 @@ fn goto_file_start(cx: &mut Context) {
|
||||||
fn goto_file_end(cx: &mut Context) {
|
fn goto_file_end(cx: &mut Context) {
|
||||||
push_jump(cx.editor);
|
push_jump(cx.editor);
|
||||||
let (view, doc) = current!(cx.editor);
|
let (view, doc) = current!(cx.editor);
|
||||||
let text = doc.text();
|
doc.set_selection(view.id, Selection::point(doc.text().len_chars()));
|
||||||
let last_line = text.line_to_char(text.len_lines().saturating_sub(2));
|
|
||||||
doc.set_selection(view.id, Selection::point(last_line));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extend_next_word_start(cx: &mut Context) {
|
fn extend_next_word_start(cx: &mut Context) {
|
||||||
|
@ -592,9 +569,10 @@ fn extend_next_word_start(cx: &mut Context) {
|
||||||
doc.set_selection(
|
doc.set_selection(
|
||||||
view.id,
|
view.id,
|
||||||
doc.selection(view.id).clone().transform(|range| {
|
doc.selection(view.id).clone().transform(|range| {
|
||||||
let word = movement::move_next_word_start(doc.text().slice(..), range, count);
|
let text = doc.text().slice(..);
|
||||||
|
let word = movement::move_next_word_start(text, range, count);
|
||||||
let pos = word.head;
|
let pos = word.head;
|
||||||
Range::new(range.anchor, pos)
|
range.put(text, pos, true)
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -605,9 +583,10 @@ fn extend_prev_word_start(cx: &mut Context) {
|
||||||
doc.set_selection(
|
doc.set_selection(
|
||||||
view.id,
|
view.id,
|
||||||
doc.selection(view.id).clone().transform(|range| {
|
doc.selection(view.id).clone().transform(|range| {
|
||||||
let word = movement::move_prev_word_start(doc.text().slice(..), range, count);
|
let text = doc.text().slice(..);
|
||||||
|
let word = movement::move_prev_word_start(text, range, count);
|
||||||
let pos = word.head;
|
let pos = word.head;
|
||||||
Range::new(range.anchor, pos)
|
range.put(text, pos, true)
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -618,9 +597,10 @@ fn extend_next_word_end(cx: &mut Context) {
|
||||||
doc.set_selection(
|
doc.set_selection(
|
||||||
view.id,
|
view.id,
|
||||||
doc.selection(view.id).clone().transform(|range| {
|
doc.selection(view.id).clone().transform(|range| {
|
||||||
let word = movement::move_next_word_end(doc.text().slice(..), range, count);
|
let text = doc.text().slice(..);
|
||||||
|
let word = movement::move_next_word_end(text, range, count);
|
||||||
let pos = word.head;
|
let pos = word.head;
|
||||||
Range::new(range.anchor, pos)
|
range.put(text, pos, true)
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -669,18 +649,9 @@ where
|
||||||
doc.set_selection(
|
doc.set_selection(
|
||||||
view.id,
|
view.id,
|
||||||
doc.selection(view.id).clone().transform(|range| {
|
doc.selection(view.id).clone().transform(|range| {
|
||||||
search_fn(doc.text().slice(..), ch, range.head, count, inclusive).map_or(
|
let text = doc.text().slice(..);
|
||||||
range,
|
search_fn(text, ch, range.head, count, inclusive)
|
||||||
|pos| {
|
.map_or(range, |pos| range.put(text, pos, extend))
|
||||||
if extend {
|
|
||||||
Range::new(range.anchor, pos)
|
|
||||||
} else {
|
|
||||||
// select
|
|
||||||
Range::new(range.head, pos)
|
|
||||||
}
|
|
||||||
// or (pos, pos) to move to found val
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
@ -940,7 +911,7 @@ fn extend_line_down(cx: &mut Context) {
|
||||||
fn select_all(cx: &mut Context) {
|
fn select_all(cx: &mut Context) {
|
||||||
let (view, doc) = current!(cx.editor);
|
let (view, doc) = current!(cx.editor);
|
||||||
|
|
||||||
let end = rope_end_without_line_ending(&doc.text().slice(..));
|
let end = doc.text().len_chars();
|
||||||
doc.set_selection(view.id, Selection::single(0, end))
|
doc.set_selection(view.id, Selection::single(0, end))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -997,12 +968,10 @@ fn search_impl(doc: &mut Document, view: &mut View, contents: &str, regex: &Rege
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let head = end - 1;
|
|
||||||
|
|
||||||
let selection = if extend {
|
let selection = if extend {
|
||||||
selection.clone().push(Range::new(start, head))
|
selection.clone().push(Range::new(start, end))
|
||||||
} else {
|
} else {
|
||||||
Selection::single(start, head)
|
Selection::single(start, end)
|
||||||
};
|
};
|
||||||
|
|
||||||
doc.set_selection(view.id, selection);
|
doc.set_selection(view.id, selection);
|
||||||
|
@ -1143,9 +1112,15 @@ fn collapse_selection(cx: &mut Context) {
|
||||||
|
|
||||||
doc.set_selection(
|
doc.set_selection(
|
||||||
view.id,
|
view.id,
|
||||||
doc.selection(view.id)
|
doc.selection(view.id).clone().transform(|range| {
|
||||||
.clone()
|
let pos = if range.head > range.anchor {
|
||||||
.transform(|range| Range::new(range.head, range.head)),
|
// For 1-width cursor semantics.
|
||||||
|
graphemes::prev_grapheme_boundary(doc.text().slice(..), range.head)
|
||||||
|
} else {
|
||||||
|
range.head
|
||||||
|
};
|
||||||
|
Range::new(pos, pos)
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1184,10 +1159,13 @@ fn append_mode(cx: &mut Context) {
|
||||||
doc.restore_cursor = true;
|
doc.restore_cursor = true;
|
||||||
|
|
||||||
let selection = doc.selection(view.id).clone().transform(|range| {
|
let selection = doc.selection(view.id).clone().transform(|range| {
|
||||||
Range::new(
|
let to = if range.to() == range.from() {
|
||||||
range.from(),
|
// For 1-width cursor semantics.
|
||||||
graphemes::next_grapheme_boundary(doc.text().slice(..), range.to()), // to() + next char
|
graphemes::next_grapheme_boundary(doc.text().slice(..), range.to())
|
||||||
)
|
} else {
|
||||||
|
range.to()
|
||||||
|
};
|
||||||
|
Range::new(range.from(), to)
|
||||||
});
|
});
|
||||||
|
|
||||||
let end = doc.text().len_chars();
|
let end = doc.text().len_chars();
|
||||||
|
|
Loading…
Reference in New Issue