Compare commits

...

14 Commits

Author SHA1 Message Date
Skyler Hawthorne
fdd3f8b88f dead10ck 2025-09-15 12:10:29 -04:00
Skyler Hawthorne
09de695156 fix as-is tests 2025-09-15 12:07:34 -04:00
Skyler Hawthorne
99775897ac account for overlapping deletes 2025-09-15 12:07:34 -04:00
Skyler Hawthorne
631c3e3644 insert double whitespace inside pair 2025-09-15 12:07:34 -04:00
Skyler Hawthorne
bb0198ee73 Delete pairs with multi-char-range selections
This completes auto pair deletions. Currently, auto pairs only get
deleted when the range is a single grapheme wide, since otherwise,
the selection would get computed incorrectly through the normal change
mapping process. Now auto pairs get deleted even with larger ranges, and
the resulting selection is correct.
2025-09-15 12:07:34 -04:00
Skyler Hawthorne
8dfb0b5b85 add delete_by_and_with_selection 2025-09-15 12:07:34 -04:00
Skyler Hawthorne
15834cfe81 Change auto pair hook to operate on single changes
Change the auto pair hook to operate on single ranges to allow
transactions that mix auto pair changes with other operations, such as
inserting or deleting a single char, and denendting.
2025-09-15 12:07:34 -04:00
Skyler Hawthorne
0578063eed backfill auto pair delete tests 2025-09-15 12:07:34 -04:00
Skyler Hawthorne
d26feaf1a3 Add Transaction::change_by_and_with_selection
Adds `Transaction::change_by_and_with_selection` which centralizes
logic for producing change sets with a potentially new selection that
is applied incrementally, rather than all at once at the end with
`with_selection`. It also centralizes the offset tracking logic so that
the caller can construct a new selection with ranges as if they were
operating on the text as-is.
2025-09-15 12:07:34 -04:00
Skyler Hawthorne
699391dd19 feat(command): expand_selection_around
Introduces a new command `expand_selection_around` that expands the
selection to the parent node, like `expand_selection`, except it splits
on the selection you start with and continues expansion around this
initial selection.
2025-09-15 12:00:53 -04:00
Skyler Hawthorne
d445661815 Generalize View::object_selections into map
This allows using multiple distinct state histories. By default, all
history is also cleared any time a view's selection is set, unless
explicitly told to save the state. This way, we can have control over
when selection history is saved. They are also cleared on any text
edit, since an edit could invalidate the previous selection, potentially
causing a panic.

Additionally, the object selections have been moved into `Document`
so that it is easier to manipulate them when changes to the document
happen. They have been put into a wrapper struct named `ViewData`, where
the intention is that any further fields that we want to add in the
future that must be associated with a view, but are more convenient to
store in a document, can be added here, instead of further polluting the
core `Document` type.
2025-09-15 12:00:53 -04:00
Skyler Hawthorne
a938646274 Add Selection::{overlaps,without} and fix contains
Adds two helper functions to `Selection`:

* `overlaps`: tests whether two `Selection`s contain any ranges which
  overlap with each other
* `without`: Computes a new `Selection` that is the set difference
  of two `Selection`s, i.e. everything in the first `Selection`
  with everything that overlaps in the second `Selection` removed,
  potentially leaving holes in the original ranges.

It also fixes a bug with `Selection::contains`: it assumes that if the
second `Selection` has a greater number of ranges than the first, then
the first cannot contain the second; but this is false, since one range
from the first could contain multiple smaller ranges in the second.
2025-09-15 12:00:53 -04:00
Skyler Hawthorne
d49c8efa6e shrink_selection without history selects first contained child
This changes the behavior of `shrink_selection` to iterate through child
nodes until it finds one that is contained within the selection, with
at least one of the ends of the selection being exclusively inside the
starting selection (though not necessarily both ends). This produces
more intuitive behavior for selecting the "first logical thing" inside
the selection.
2025-09-15 12:00:53 -04:00
Skyler Hawthorne
9816696784 backfill expand/shrink tests 2025-09-15 12:00:53 -04:00
10 changed files with 2147 additions and 351 deletions

View File

@@ -1,11 +1,9 @@
//! When typing the opening character of one of the possible pairs defined below,
//! this module provides the functionality to insert the paired closing character.
use crate::{graphemes, movement::Direction, Range, Rope, Selection, Tendril, Transaction};
use crate::{graphemes, movement::Direction, Change, Deletion, Range, Rope, Tendril};
use std::collections::HashMap;
use smallvec::SmallVec;
// Heavily based on https://github.com/codemirror/closebrackets/
pub const DEFAULT_PAIRS: &[(char, char)] = &[
('(', ')'),
@@ -106,37 +104,128 @@ impl Default for AutoPairs {
}
}
// insert hook:
// Fn(doc, selection, char) => Option<Transaction>
// problem is, we want to do this per range, so we can call default handler for some ranges
// so maybe ret Vec<Option<Change>>
// but we also need to be able to return transactions...
//
// to simplify, maybe return Option<Transaction> and just reimplement the default
// [TODO]
// * delete implementation where it erases the whole bracket (|) -> |
// * change to multi character pairs to handle cases like placing the cursor in the
// middle of triple quotes, and more exotic pairs like Jinja's {% %}
#[must_use]
pub fn hook(doc: &Rope, selection: &Selection, ch: char, pairs: &AutoPairs) -> Option<Transaction> {
log::trace!("autopairs hook selection: {:#?}", selection);
pub fn hook_insert(
doc: &Rope,
range: &Range,
ch: char,
pairs: &AutoPairs,
) -> Option<(Change, Range)> {
log::trace!("autopairs hook range: {:#?}", range);
if let Some(pair) = pairs.get(ch) {
if pair.same() {
return Some(handle_same(doc, selection, pair));
return handle_insert_same(doc, range, pair);
} else if pair.open == ch {
return Some(handle_open(doc, selection, pair));
return handle_insert_open(doc, range, pair);
} else if pair.close == ch {
// && char_at pos == close
return Some(handle_close(doc, selection, pair));
return handle_insert_close(doc, range, pair);
}
} else if ch.is_whitespace() {
return handle_insert_whitespace(doc, range, ch, pairs);
}
None
}
#[must_use]
pub fn hook_delete(doc: &Rope, range: &Range, pairs: &AutoPairs) -> Option<(Deletion, Range)> {
log::trace!("autopairs delete hook range: {:#?}", range);
let text = doc.slice(..);
let cursor = range.cursor(text);
let cur = doc.get_char(cursor)?;
let prev = prev_char(doc, cursor)?;
// check for whitespace surrounding a pair
if doc.len_chars() >= 4 && prev.is_whitespace() && cur.is_whitespace() {
let second_prev = doc.get_char(graphemes::nth_prev_grapheme_boundary(text, cursor, 2))?;
let second_next = doc.get_char(graphemes::next_grapheme_boundary(text, cursor))?;
log::debug!("second_prev: {}, second_next: {}", second_prev, second_next);
if let Some(pair) = pairs.get(second_prev) {
if pair.open == second_prev && pair.close == second_next {
return handle_delete(doc, range);
}
}
}
let pair = pairs.get(cur)?;
if pair.open != prev || pair.close != cur {
return None;
}
handle_delete(doc, range)
}
pub fn handle_delete(doc: &Rope, range: &Range) -> Option<(Deletion, Range)> {
let text = doc.slice(..);
let cursor = range.cursor(text);
let end_next = graphemes::next_grapheme_boundary(text, cursor);
let end_prev = graphemes::prev_grapheme_boundary(text, cursor);
let delete = (end_prev, end_next);
let size_delete = end_next - end_prev;
let next_head = graphemes::next_grapheme_boundary(text, range.head) - size_delete;
// if the range is a single grapheme cursor, we do not want to shrink the
// range, just move it, so we only subtract the size of the closing pair char
let next_anchor = match (range.direction(), range.is_single_grapheme(text)) {
// single grapheme forward needs to move, but only the width of the
// character under the cursor, which is the closer
(Direction::Forward, true) => range.anchor - (end_next - cursor),
(Direction::Backward, true) => range.anchor - (cursor - end_prev),
(Direction::Forward, false) => range.anchor,
(Direction::Backward, false) => range.anchor - size_delete,
};
let next_range = Range::new(next_anchor, next_head);
log::trace!(
"auto pair delete: {:?}, range: {:?}, next_range: {:?}, text len: {}",
delete,
range,
next_range,
text.len_chars()
);
Some((delete, next_range))
}
fn handle_insert_whitespace(
doc: &Rope,
range: &Range,
ch: char,
pairs: &AutoPairs,
) -> Option<(Change, Range)> {
let text = doc.slice(..);
let cursor = range.cursor(text);
let cur = doc.get_char(cursor)?;
let prev = prev_char(doc, cursor)?;
let pair = pairs.get(cur)?;
if pair.open != prev || pair.close != cur {
return None;
}
let whitespace_pair = Pair {
open: ch,
close: ch,
};
handle_insert_same(doc, range, &whitespace_pair)
}
fn prev_char(doc: &Rope, pos: usize) -> Option<char> {
if pos == 0 {
return None;
@@ -146,7 +235,7 @@ fn prev_char(doc: &Rope, pos: usize) -> Option<char> {
}
/// calculate what the resulting range should be for an auto pair insertion
fn get_next_range(doc: &Rope, start_range: &Range, offset: usize, len_inserted: usize) -> Range {
fn get_next_range(doc: &Rope, start_range: &Range, len_inserted: usize) -> Range {
// When the character under the cursor changes due to complete pair
// insertion, we must look backward a grapheme and then add the length
// of the insertion to put the resulting cursor in the right place, e.g.
@@ -165,10 +254,7 @@ fn get_next_range(doc: &Rope, start_range: &Range, offset: usize, len_inserted:
// inserting at the very end of the document after the last newline
if start_range.head == doc.len_chars() && start_range.anchor == doc.len_chars() {
return Range::new(
start_range.anchor + offset + 1,
start_range.head + offset + 1,
);
return Range::new(start_range.anchor + 1, start_range.head + 1);
}
let doc_slice = doc.slice(..);
@@ -177,7 +263,7 @@ fn get_next_range(doc: &Rope, start_range: &Range, offset: usize, len_inserted:
// just skip over graphemes
if len_inserted == 0 {
let end_anchor = if single_grapheme {
graphemes::next_grapheme_boundary(doc_slice, start_range.anchor) + offset
graphemes::next_grapheme_boundary(doc_slice, start_range.anchor)
// even for backward inserts with multiple grapheme selections,
// we want the anchor to stay where it is so that the relative
@@ -185,42 +271,38 @@ fn get_next_range(doc: &Rope, start_range: &Range, offset: usize, len_inserted:
//
// foo([) wor]d -> insert ) -> foo()[ wor]d
} else {
start_range.anchor + offset
start_range.anchor
};
return Range::new(
end_anchor,
graphemes::next_grapheme_boundary(doc_slice, start_range.head) + offset,
graphemes::next_grapheme_boundary(doc_slice, start_range.head),
);
}
// trivial case: only inserted a single-char opener, just move the selection
if len_inserted == 1 {
let end_anchor = if single_grapheme || start_range.direction() == Direction::Backward {
start_range.anchor + offset + 1
start_range.anchor + 1
} else {
start_range.anchor + offset
start_range.anchor
};
return Range::new(end_anchor, start_range.head + offset + 1);
return Range::new(end_anchor, start_range.head + 1);
}
// If the head = 0, then we must be in insert mode with a backward
// cursor, which implies the head will just move
let end_head = if start_range.head == 0 || start_range.direction() == Direction::Backward {
start_range.head + offset + 1
start_range.head + 1
} else {
// We must have a forward cursor, which means we must move to the
// other end of the grapheme to get to where the new characters
// are inserted, then move the head to where it should be
let prev_bound = graphemes::prev_grapheme_boundary(doc_slice, start_range.head);
log::trace!(
"prev_bound: {}, offset: {}, len_inserted: {}",
prev_bound,
offset,
len_inserted
);
prev_bound + offset + len_inserted
log::trace!("prev_bound: {}, len_inserted: {}", prev_bound, len_inserted);
prev_bound + len_inserted
};
let end_anchor = match (start_range.len(), start_range.direction()) {
@@ -239,7 +321,7 @@ fn get_next_range(doc: &Rope, start_range: &Range, offset: usize, len_inserted:
// if we are appending, the anchor stays where it is; only offset
// for multiple range insertions
} else {
start_range.anchor + offset
start_range.anchor
}
}
@@ -248,13 +330,11 @@ fn get_next_range(doc: &Rope, start_range: &Range, offset: usize, len_inserted:
// if we're backward, then the head is at the first char
// of the typed char, so we need to add the length of
// the closing char
graphemes::prev_grapheme_boundary(doc.slice(..), start_range.anchor)
+ len_inserted
+ offset
graphemes::prev_grapheme_boundary(doc.slice(..), start_range.anchor) + len_inserted
} else {
// when we are inserting in front of a selection, we need to move
// the anchor over by however many characters were inserted overall
start_range.anchor + offset + len_inserted
start_range.anchor + len_inserted
}
}
};
@@ -262,112 +342,76 @@ fn get_next_range(doc: &Rope, start_range: &Range, offset: usize, len_inserted:
Range::new(end_anchor, end_head)
}
fn handle_open(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
let mut end_ranges = SmallVec::with_capacity(selection.len());
let mut offs = 0;
fn handle_insert_open(doc: &Rope, range: &Range, pair: &Pair) -> Option<(Change, Range)> {
let cursor = range.cursor(doc.slice(..));
let next_char = doc.get_char(cursor);
let len_inserted;
let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
let cursor = start_range.cursor(doc.slice(..));
let next_char = doc.get_char(cursor);
let len_inserted;
// Since auto pairs are currently limited to single chars, we're either
// inserting exactly one or two chars. When arbitrary length pairs are
// added, these will need to be changed.
let change = match next_char {
Some(_) if !pair.should_close(doc, range) => {
return None;
}
_ => {
// insert open & close
let pair_str = Tendril::from_iter([pair.open, pair.close]);
len_inserted = 2;
(cursor, cursor, Some(pair_str))
}
};
// Since auto pairs are currently limited to single chars, we're either
// inserting exactly one or two chars. When arbitrary length pairs are
// added, these will need to be changed.
let change = match next_char {
Some(_) if !pair.should_close(doc, start_range) => {
len_inserted = 1;
let mut tendril = Tendril::new();
tendril.push(pair.open);
(cursor, cursor, Some(tendril))
}
_ => {
// insert open & close
let pair_str = Tendril::from_iter([pair.open, pair.close]);
len_inserted = 2;
(cursor, cursor, Some(pair_str))
}
};
let next_range = get_next_range(doc, range, len_inserted);
let result = (change, next_range);
let next_range = get_next_range(doc, start_range, offs, len_inserted);
end_ranges.push(next_range);
offs += len_inserted;
log::debug!("auto pair change: {:#?}", &result);
change
});
let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
log::debug!("auto pair transaction: {:#?}", t);
t
Some(result)
}
fn handle_close(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
let mut end_ranges = SmallVec::with_capacity(selection.len());
let mut offs = 0;
fn handle_insert_close(doc: &Rope, range: &Range, pair: &Pair) -> Option<(Change, Range)> {
let cursor = range.cursor(doc.slice(..));
let next_char = doc.get_char(cursor);
let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
let cursor = start_range.cursor(doc.slice(..));
let next_char = doc.get_char(cursor);
let mut len_inserted = 0;
let change = if next_char == Some(pair.close) {
// return transaction that moves past close
(cursor, cursor, None) // no-op
} else {
return None;
};
let change = if next_char == Some(pair.close) {
// return transaction that moves past close
(cursor, cursor, None) // no-op
} else {
len_inserted = 1;
let mut tendril = Tendril::new();
tendril.push(pair.close);
(cursor, cursor, Some(tendril))
};
let next_range = get_next_range(doc, range, 0);
let result = (change, next_range);
let next_range = get_next_range(doc, start_range, offs, len_inserted);
end_ranges.push(next_range);
offs += len_inserted;
log::debug!("auto pair change: {:#?}", &result);
change
});
let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
log::debug!("auto pair transaction: {:#?}", t);
t
Some(result)
}
/// handle cases where open and close is the same, or in triples ("""docstring""")
fn handle_same(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
let mut end_ranges = SmallVec::with_capacity(selection.len());
fn handle_insert_same(doc: &Rope, range: &Range, pair: &Pair) -> Option<(Change, Range)> {
let cursor = range.cursor(doc.slice(..));
let mut len_inserted = 0;
let next_char = doc.get_char(cursor);
let mut offs = 0;
let change = if next_char == Some(pair.open) {
// return transaction that moves past close
(cursor, cursor, None) // no-op
} else {
if !pair.should_close(doc, range) {
return None;
}
let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
let cursor = start_range.cursor(doc.slice(..));
let mut len_inserted = 0;
let next_char = doc.get_char(cursor);
let pair_str = Tendril::from_iter([pair.open, pair.close]);
len_inserted = 2;
(cursor, cursor, Some(pair_str))
};
let change = if next_char == Some(pair.open) {
// return transaction that moves past close
(cursor, cursor, None) // no-op
} else {
let mut pair_str = Tendril::new();
pair_str.push(pair.open);
let next_range = get_next_range(doc, range, len_inserted);
let result = (change, next_range);
// for equal pairs, don't insert both open and close if either
// side has a non-pair char
if pair.should_close(doc, start_range) {
pair_str.push(pair.close);
}
log::debug!("auto pair change: {:#?}", &result);
len_inserted += pair_str.chars().count();
(cursor, cursor, Some(pair_str))
};
let next_range = get_next_range(doc, start_range, offs, len_inserted);
end_ranges.push(next_range);
offs += len_inserted;
change
});
let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
log::debug!("auto pair transaction: {:#?}", t);
t
Some(result)
}

View File

@@ -25,38 +25,59 @@ pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: Selection)
}
pub fn shrink_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
select_node_impl(
syntax,
text,
selection,
|cursor| {
cursor.goto_first_child();
},
None,
)
selection.transform(move |range| {
let (from, to) = range.into_byte_range(text);
let mut cursor = syntax.walk();
cursor.reset_to_byte_range(from, to);
if let Some(node) = cursor
.into_iter()
.find(|node| node.is_named() && node.is_contained_within(from..to))
{
return Range::from_node(node, text, range.direction());
}
range
})
}
pub fn select_next_sibling(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
select_node_impl(
syntax,
text,
selection,
|cursor| {
while !cursor.goto_next_sibling() {
if !cursor.goto_parent() {
break;
}
selection.transform(move |range| {
let (from, to) = range.into_byte_range(text);
let mut cursor = syntax.walk();
cursor.reset_to_byte_range(from, to);
while !cursor.goto_next_sibling() {
if !cursor.goto_parent() {
return range;
}
},
Some(Direction::Forward),
)
}
Range::from_node(cursor.node(), text, Direction::Forward)
})
}
pub fn select_prev_sibling(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
selection.transform(move |range| {
let (from, to) = range.into_byte_range(text);
let mut cursor = syntax.walk();
cursor.reset_to_byte_range(from, to);
while !cursor.goto_previous_sibling() {
if !cursor.goto_parent() {
return range;
}
}
Range::from_node(cursor.node(), text, Direction::Backward)
})
}
pub fn select_all_siblings(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
let mut cursor = syntax.walk();
selection.transform_iter(move |range| {
let (from, to) = range.into_byte_range(text);
cursor.reset_to_byte_range(from as u32, to as u32);
cursor.reset_to_byte_range(from, to);
if !cursor.goto_parent_with(|parent| parent.child_count() > 1) {
return vec![range].into_iter();
@@ -70,7 +91,7 @@ pub fn select_all_children(syntax: &Syntax, text: RopeSlice, selection: Selectio
let mut cursor = syntax.walk();
selection.transform_iter(move |range| {
let (from, to) = range.into_byte_range(text);
cursor.reset_to_byte_range(from as u32, to as u32);
cursor.reset_to_byte_range(from, to);
select_children(&mut cursor, text, range).into_iter()
})
}
@@ -88,47 +109,3 @@ fn select_children(cursor: &mut TreeCursor, text: RopeSlice, range: Range) -> Ve
vec![range]
}
}
pub fn select_prev_sibling(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
select_node_impl(
syntax,
text,
selection,
|cursor| {
while !cursor.goto_previous_sibling() {
if !cursor.goto_parent() {
break;
}
}
},
Some(Direction::Backward),
)
}
fn select_node_impl<F>(
syntax: &Syntax,
text: RopeSlice,
selection: Selection,
motion: F,
direction: Option<Direction>,
) -> Selection
where
F: Fn(&mut TreeCursor),
{
let cursor = &mut syntax.walk();
selection.transform(|range| {
let from = text.char_to_byte(range.from()) as u32;
let to = text.char_to_byte(range.to()) as u32;
cursor.reset_to_byte_range(from, to);
motion(cursor);
let node = cursor.node();
let from = text.byte_to_char(node.start_byte() as usize);
let to = text.byte_to_char(node.end_byte() as usize);
Range::new(from, to).with_direction(direction.unwrap_or_else(|| range.direction()))
})
}

View File

@@ -387,8 +387,11 @@ impl Range {
/// Converts this char range into an in order byte range, discarding
/// direction.
pub fn into_byte_range(&self, text: RopeSlice) -> (usize, usize) {
(text.char_to_byte(self.from()), text.char_to_byte(self.to()))
pub fn into_byte_range(&self, text: RopeSlice) -> (u32, u32) {
(
text.char_to_byte(self.from()) as u32,
text.char_to_byte(self.to()) as u32,
)
}
}
@@ -700,22 +703,161 @@ impl Selection {
pub fn contains(&self, other: &Selection) -> bool {
is_subset::<true>(self.range_bounds(), other.range_bounds())
}
/// returns true if self has at least one range that overlaps with at least one range from other
pub fn overlaps(&self, other: &Selection) -> bool {
let (mut iter_self, mut iter_other) = (self.iter(), other.iter());
let (mut ele_self, mut ele_other) = (iter_self.next(), iter_other.next());
loop {
match (ele_self, ele_other) {
(Some(ra), Some(rb)) => {
if ra.overlaps(rb) {
return true;
} else if ra.from() > rb.from() {
ele_self = iter_self.next();
} else {
ele_other = iter_other.next();
}
}
_ => return false,
}
}
}
/// Returns the given selection with the overlapping portions of `other`
/// cut out. If one range from this selection is equal to one from `other`,
/// this range is removed. If this results in an entirely empty selection,
/// `None` is returned.
pub fn without(self, other: &Selection) -> Option<Self> {
if other.contains(&self) {
return None;
}
let mut primary_index = self.primary_index;
let mut ranges = smallvec!();
let (mut iter_self, mut iter_other) = (self.into_iter(), other.iter());
let (mut ele_self, mut ele_other) = (iter_self.next(), iter_other.next());
let mut cur_index = 0;
loop {
match (ele_self, ele_other) {
(Some(ra), Some(rb)) => {
if !ra.overlaps(rb) {
// there's no overlap and it's on the left of rb
if ra.to() <= rb.from() {
ranges.push(ra);
ele_self = iter_self.next();
cur_index += 1;
// otherwise it must be on the right, so move rb forward
} else {
ele_other = iter_other.next();
}
continue;
}
// otherwise there is overlap, so truncate or split
if rb.contains_range(&ra) {
ele_self = iter_self.next();
cur_index += 1;
continue;
}
// [ ra ]
// [ rb ]
if ra.from() <= rb.from() && ra.to() <= rb.to() && ra.to() >= rb.from() {
let new = if ra.direction() == Direction::Backward {
Range::new(rb.from(), ra.head)
} else {
Range::new(ra.anchor, rb.from())
};
ranges.push(new);
ele_self = iter_self.next();
cur_index += 1;
// [ ra ]
// [ rb ]
} else if ra.from() >= rb.from() && ra.to() >= rb.to() && ra.from() <= rb.to() {
let new = if ra.direction() == Direction::Backward {
Range::new(ra.anchor, rb.to() + 1)
} else {
Range::new(rb.to(), ra.head)
};
// don't settle on the new range yet because the next
// rb could chop off the other end of ra
ele_self = Some(new);
ele_other = iter_other.next();
// [ ra ]
// [ rb ]
} else if ra.from() < rb.from() && ra.to() > rb.to() {
// we must split the range into two
let left = if ra.direction() == Direction::Backward {
Range::new(rb.from(), ra.head)
} else {
Range::new(ra.anchor, rb.from())
};
let right = if ra.direction() == Direction::Backward {
Range::new(ra.anchor, rb.to())
} else {
Range::new(rb.to(), ra.head)
};
// We do NOT push right onto the results right away.
// We must put it back into the iterator and check it
// again in case a further range splits it again.
ranges.push(left);
ele_other = iter_other.next();
// offset the primary index whenever we split
if cur_index < primary_index {
primary_index += 1;
}
cur_index += 1;
ele_self = Some(right);
}
}
// the rest just get included as is
(Some(range), None) => {
ranges.push(range);
ele_self = iter_self.next();
cur_index += 1;
}
// exhausted `self`, nothing left to do
(None, _) => {
break;
}
}
}
if ranges.is_empty() {
return None;
}
Some(Selection::new(ranges, primary_index))
}
}
impl<'a> IntoIterator for &'a Selection {
type Item = &'a Range;
type IntoIter = std::slice::Iter<'a, Range>;
fn into_iter(self) -> std::slice::Iter<'a, Range> {
fn into_iter(self) -> Self::IntoIter {
self.ranges().iter()
}
}
impl IntoIterator for Selection {
type Item = Range;
type IntoIter = smallvec::IntoIter<[Range; 1]>;
type IntoIter = smallvec::IntoIter<[Self::Item; 1]>;
fn into_iter(self) -> smallvec::IntoIter<[Range; 1]> {
fn into_iter(self) -> Self::IntoIter {
self.ranges.into_iter()
}
}
@@ -882,6 +1024,7 @@ pub fn split_on_matches(text: RopeSlice, selection: &Selection, regex: &rope::Re
#[cfg(test)]
mod test {
use super::*;
use crate::test;
use crate::Rope;
#[test]
@@ -972,7 +1115,7 @@ mod test {
}
#[test]
fn test_overlaps() {
fn test_range_overlaps() {
fn overlaps(a: (usize, usize), b: (usize, usize)) -> bool {
Range::new(a.0, a.1).overlaps(&Range::new(b.0, b.1))
}
@@ -1022,6 +1165,160 @@ mod test {
assert!(overlaps((1, 1), (1, 1)));
}
#[test]
fn test_selection_overlaps() {
fn overlaps(a: &[(usize, usize)], b: &[(usize, usize)]) -> bool {
let a = Selection::new(
a.iter()
.map(|(anchor, head)| Range::new(*anchor, *head))
.collect(),
0,
);
let b = Selection::new(
b.iter()
.map(|(anchor, head)| Range::new(*anchor, *head))
.collect(),
0,
);
a.overlaps(&b)
}
// Two non-zero-width ranges, no overlap.
assert!(!overlaps(&[(0, 3)], &[(3, 6)]));
assert!(!overlaps(&[(0, 3)], &[(6, 3)]));
assert!(!overlaps(&[(3, 0)], &[(3, 6)]));
assert!(!overlaps(&[(3, 0)], &[(6, 3)]));
assert!(!overlaps(&[(3, 6)], &[(0, 3)]));
assert!(!overlaps(&[(3, 6)], &[(3, 0)]));
assert!(!overlaps(&[(6, 3)], &[(0, 3)]));
assert!(!overlaps(&[(6, 3)], &[(3, 0)]));
// more ranges in one or the other, no overlap
assert!(!overlaps(&[(6, 3), (9, 6)], &[(3, 0)]));
assert!(!overlaps(&[(6, 3), (6, 9)], &[(3, 0)]));
assert!(!overlaps(&[(3, 6), (9, 6)], &[(3, 0)]));
assert!(!overlaps(&[(3, 6), (6, 9)], &[(3, 0)]));
assert!(!overlaps(&[(6, 3), (9, 6)], &[(0, 3)]));
assert!(!overlaps(&[(6, 3), (6, 9)], &[(0, 3)]));
assert!(!overlaps(&[(3, 6), (9, 6)], &[(0, 3)]));
assert!(!overlaps(&[(3, 6), (6, 9)], &[(0, 3)]));
assert!(!overlaps(&[(6, 3)], &[(3, 0), (9, 6)]));
assert!(!overlaps(&[(6, 3)], &[(3, 0), (6, 9)]));
assert!(!overlaps(&[(3, 6)], &[(3, 0), (9, 6)]));
assert!(!overlaps(&[(3, 6)], &[(3, 0), (6, 9)]));
assert!(!overlaps(&[(6, 3)], &[(0, 3), (9, 6)]));
assert!(!overlaps(&[(6, 3)], &[(0, 3), (6, 9)]));
assert!(!overlaps(&[(3, 6)], &[(0, 3), (9, 6)]));
assert!(!overlaps(&[(3, 6)], &[(0, 3), (6, 9)]));
// Two non-zero-width ranges, overlap.
assert!(overlaps(&[(0, 4)], &[(3, 6)]));
assert!(overlaps(&[(0, 4)], &[(6, 3)]));
assert!(overlaps(&[(4, 0)], &[(3, 6)]));
assert!(overlaps(&[(4, 0)], &[(6, 3)]));
assert!(overlaps(&[(3, 6)], &[(0, 4)]));
assert!(overlaps(&[(3, 6)], &[(4, 0)]));
assert!(overlaps(&[(6, 3)], &[(0, 4)]));
assert!(overlaps(&[(6, 3)], &[(4, 0)]));
// Two non-zero-width ranges, overlap, extra from one or the other
assert!(overlaps(&[(0, 4), (7, 10)], &[(3, 6)]));
assert!(overlaps(&[(0, 4), (7, 10)], &[(6, 3)]));
assert!(overlaps(&[(4, 0), (7, 10)], &[(3, 6)]));
assert!(overlaps(&[(4, 0), (7, 10)], &[(6, 3)]));
assert!(overlaps(&[(3, 6), (7, 10)], &[(0, 4)]));
assert!(overlaps(&[(3, 6), (7, 10)], &[(4, 0)]));
assert!(overlaps(&[(6, 3), (7, 10)], &[(0, 4)]));
assert!(overlaps(&[(6, 3), (7, 10)], &[(4, 0)]));
assert!(overlaps(&[(0, 4), (10, 7)], &[(3, 6)]));
assert!(overlaps(&[(0, 4), (10, 7)], &[(6, 3)]));
assert!(overlaps(&[(4, 0), (10, 7)], &[(3, 6)]));
assert!(overlaps(&[(4, 0), (10, 7)], &[(6, 3)]));
assert!(overlaps(&[(3, 6), (10, 7)], &[(0, 4)]));
assert!(overlaps(&[(3, 6), (10, 7)], &[(4, 0)]));
assert!(overlaps(&[(6, 3), (10, 7)], &[(0, 4)]));
assert!(overlaps(&[(6, 3), (10, 7)], &[(4, 0)]));
assert!(overlaps(&[(0, 4)], &[(3, 6), (7, 10)]));
assert!(overlaps(&[(0, 4)], &[(6, 3), (7, 10)]));
assert!(overlaps(&[(4, 0)], &[(3, 6), (7, 10)]));
assert!(overlaps(&[(4, 0)], &[(6, 3), (7, 10)]));
assert!(overlaps(&[(3, 6)], &[(0, 4), (7, 10)]));
assert!(overlaps(&[(3, 6)], &[(4, 0), (7, 10)]));
assert!(overlaps(&[(6, 3)], &[(0, 4), (7, 10)]));
assert!(overlaps(&[(6, 3)], &[(4, 0), (7, 10)]));
assert!(overlaps(&[(0, 4)], &[(3, 6), (10, 7)]));
assert!(overlaps(&[(0, 4)], &[(6, 3), (10, 7)]));
assert!(overlaps(&[(4, 0)], &[(3, 6), (10, 7)]));
assert!(overlaps(&[(4, 0)], &[(6, 3), (10, 7)]));
assert!(overlaps(&[(3, 6)], &[(0, 4), (10, 7)]));
assert!(overlaps(&[(3, 6)], &[(4, 0), (10, 7)]));
assert!(overlaps(&[(6, 3)], &[(0, 4), (10, 7)]));
assert!(overlaps(&[(6, 3)], &[(4, 0), (10, 7)]));
// Zero-width and non-zero-width range, no overlap.
assert!(!overlaps(&[(0, 3)], &[(3, 3)]));
assert!(!overlaps(&[(3, 0)], &[(3, 3)]));
assert!(!overlaps(&[(3, 3)], &[(0, 3)]));
assert!(!overlaps(&[(3, 3)], &[(3, 0)]));
assert!(!overlaps(&[(0, 3), (7, 10)], &[(3, 3)]));
assert!(!overlaps(&[(3, 0), (7, 10)], &[(3, 3)]));
assert!(!overlaps(&[(3, 3), (7, 10)], &[(0, 3)]));
assert!(!overlaps(&[(3, 3), (7, 10)], &[(3, 0)]));
assert!(!overlaps(&[(0, 3)], &[(3, 3), (7, 10)]));
assert!(!overlaps(&[(3, 0)], &[(3, 3), (7, 10)]));
assert!(!overlaps(&[(3, 3)], &[(0, 3), (7, 10)]));
assert!(!overlaps(&[(3, 3)], &[(3, 0), (7, 10)]));
// Zero-width and non-zero-width range, overlap.
assert!(overlaps(&[(1, 4)], &[(1, 1)]));
assert!(overlaps(&[(4, 1)], &[(1, 1)]));
assert!(overlaps(&[(1, 1)], &[(1, 4)]));
assert!(overlaps(&[(1, 1)], &[(4, 1)]));
assert!(overlaps(&[(1, 4)], &[(3, 3)]));
assert!(overlaps(&[(4, 1)], &[(3, 3)]));
assert!(overlaps(&[(3, 3)], &[(1, 4)]));
assert!(overlaps(&[(3, 3)], &[(4, 1)]));
assert!(overlaps(&[(1, 4), (7, 10)], &[(1, 1)]));
assert!(overlaps(&[(4, 1), (7, 10)], &[(1, 1)]));
assert!(overlaps(&[(1, 1), (7, 10)], &[(1, 4)]));
assert!(overlaps(&[(1, 1), (7, 10)], &[(4, 1)]));
assert!(overlaps(&[(1, 4), (7, 10)], &[(3, 3)]));
assert!(overlaps(&[(4, 1), (7, 10)], &[(3, 3)]));
assert!(overlaps(&[(3, 3), (7, 10)], &[(1, 4)]));
assert!(overlaps(&[(3, 3), (7, 10)], &[(4, 1)]));
assert!(overlaps(&[(1, 4)], &[(1, 1), (7, 10)]));
assert!(overlaps(&[(4, 1)], &[(1, 1), (7, 10)]));
assert!(overlaps(&[(1, 1)], &[(1, 4), (7, 10)]));
assert!(overlaps(&[(1, 1)], &[(4, 1), (7, 10)]));
assert!(overlaps(&[(1, 4)], &[(3, 3), (7, 10)]));
assert!(overlaps(&[(4, 1)], &[(3, 3), (7, 10)]));
assert!(overlaps(&[(3, 3)], &[(1, 4), (7, 10)]));
assert!(overlaps(&[(3, 3)], &[(4, 1), (7, 10)]));
// Two zero-width ranges, no overlap.
assert!(!overlaps(&[(0, 0)], &[(1, 1)]));
assert!(!overlaps(&[(1, 1)], &[(0, 0)]));
assert!(!overlaps(&[(0, 0), (2, 2)], &[(1, 1)]));
assert!(!overlaps(&[(0, 0), (2, 2)], &[(1, 1)]));
assert!(!overlaps(&[(1, 1)], &[(0, 0), (2, 2)]));
assert!(!overlaps(&[(1, 1)], &[(0, 0), (2, 2)]));
// Two zero-width ranges, overlap.
assert!(overlaps(&[(1, 1)], &[(1, 1)]));
assert!(overlaps(&[(1, 1), (2, 2)], &[(1, 1)]));
assert!(overlaps(&[(1, 1)], &[(1, 1), (2, 2)]));
}
#[test]
fn test_grapheme_aligned() {
let r = Rope::from_str("\r\nHi\r\n");
@@ -1378,9 +1675,15 @@ mod test {
// multiple matches
assert!(contains(vec!((1, 1), (2, 2)), vec!((1, 1), (2, 2))));
// smaller set can't contain bigger
// extra items out of range
assert!(!contains(vec!((1, 1)), vec!((1, 1), (2, 2))));
// one big range can contain multiple smaller ranges
assert!(contains(
vec!((1, 10)),
vec!((1, 1), (2, 2), (3, 3), (3, 5), (7, 10))
));
assert!(contains(
vec!((1, 1), (2, 4), (5, 6), (7, 9), (10, 13)),
vec!((3, 4), (7, 9))
@@ -1393,4 +1696,143 @@ mod test {
vec!((1, 2), (3, 4), (7, 9))
));
}
#[test]
fn test_selection_without() {
let without = |one, two, expected: Option<_>| {
println!("one: {:?}", one);
println!("two: {:?}", two);
println!("expected: {:?}", expected);
let (one_text, one_sel) = test::print(one);
let (two_text, two_sel) = test::print(two);
assert_eq!(one_text, two_text); // sanity
let actual_sel = one_sel.without(&two_sel);
let expected_sel = expected.map(|exp| {
let (expected_text, expected_sel) = test::print(exp);
assert_eq!(two_text, expected_text);
expected_sel
});
let actual = actual_sel
.as_ref()
.map(|sel| test::plain(two_text.to_string(), sel));
println!("actual: {:?}\n\n", actual);
assert_eq!(
expected_sel,
actual_sel,
"expected: {:?}, got: {:?}",
expected_sel
.as_ref()
.map(|sel| test::plain(two_text.to_string(), sel)),
actual,
);
};
without(
"#[foo bar baz|]#",
"foo #[bar|]# baz",
Some("#[foo |]#bar#( baz|)#"),
);
without("#[foo bar baz|]#", "#[foo bar baz|]#", None);
without("#[foo bar|]# baz", "#[foo bar baz|]#", None);
without("foo #[bar|]# baz", "#[foo bar baz|]#", None);
// direction is preserved
without(
"#[|foo bar baz]#",
"foo #[|bar]# baz",
Some("#[|foo ]#bar#(| baz)#"),
);
// preference for direction is given to the left
without(
"#[|foo bar baz]#",
"foo #[bar|]# baz",
Some("#[|foo ]#bar#(| baz)#"),
);
// disjoint ranges on the right are ignored
without(
"#[foo bar|]# baz",
"foo bar #[baz|]#",
Some("#[foo bar|]# baz"),
);
without(
"#[foo bar|]# baz",
"foo bar#[ baz|]#",
Some("#[foo bar|]# baz"),
);
without(
"#(foo|)# #[bar|]# baz",
"foo#[ b|]#ar ba#(z|)#",
Some("#(foo|)# b#[ar|]# baz"),
);
// ranges contained by those one on the right are removed
without(
"#[foo bar|]# #(b|)#az",
"foo bar#[ b|]#a#(z|)#",
Some("#[foo bar|]# baz"),
);
without(
"#[foo|]# bar #(baz|)#",
"foo bar#[ b|]#a#(z|)#",
Some("#[foo|]# bar b#(a|)#z"),
);
without(
"#[foo bar|]# #(b|)#az",
"foo bar #[b|]#a#(z|)#",
Some("#[foo bar|]# baz"),
);
without(
"#[foo bar|]# #(b|)#a#(z|)#",
"foo bar #[b|]#a#(z|)#",
Some("#[foo bar|]# baz"),
);
without(
"#[foo bar|]# #(b|)#a#(z|)#",
"foo bar #[b|]#a#(z|)#",
Some("#[foo bar|]# baz"),
);
// more than one range intersected by a single range on the right
without(
"#[foo bar|]# #(baz|)#",
"foo b#[ar ba|]#z",
Some("#[foo b|]#ar ba#(z|)#"),
);
// partial overlap
without(
"#[foo bar|]# baz",
"foo #[bar baz|]#",
Some("#[foo |]#bar baz"),
);
without(
"#[foo bar|]# baz",
"foo#[ bar baz|]#",
Some("#[foo|]# bar baz"),
);
without(
"#[foo bar|]# baz",
"foo ba#[r baz|]#",
Some("#[foo ba|]#r baz"),
);
without(
"foo ba#[r baz|]#",
"#[foo bar|]# baz",
Some("foo bar#[ baz|]#"),
);
// primary selection is moved - preference given to the left of a split
without(
"#(|foo)# #(|bar baz)# #[|quux]#",
"f#(o|)#o ba#[r b|]#az q#(uu|)#x",
Some("#(|f)#o#(|o)# #(|ba)#r b#(|az)# #[|q]#uu#(|x)#"),
);
}
}

View File

@@ -523,6 +523,49 @@ impl ChangeSet {
pub fn changes_iter(&self) -> ChangeIterator {
ChangeIterator::new(self)
}
pub fn from_change(doc: &Rope, change: Change) -> Self {
Self::from_changes(doc, std::iter::once(change))
}
/// Generate a ChangeSet from a set of changes.
pub fn from_changes<I>(doc: &Rope, changes: I) -> Self
where
I: Iterator<Item = Change>,
{
let len = doc.len_chars();
let (lower, upper) = changes.size_hint();
let size = upper.unwrap_or(lower);
let mut changeset = ChangeSet::with_capacity(2 * size + 1); // rough estimate
let mut last = 0;
for (from, to, tendril) in changes {
// Verify ranges are ordered and not overlapping
debug_assert!(last <= from);
// Verify ranges are correct
debug_assert!(
from <= to,
"Edit end must end before it starts (should {from} <= {to})"
);
// Retain from last "to" to current "from"
changeset.retain(from - last);
let span = to - from;
match tendril {
Some(text) => {
changeset.insert(text);
changeset.delete(span);
}
None => changeset.delete(span),
}
last = to;
}
changeset.retain(len - last);
changeset
}
}
/// Transaction represents a single undoable unit of changes. Several changes can be grouped into
@@ -616,38 +659,7 @@ impl Transaction {
where
I: Iterator<Item = Change>,
{
let len = doc.len_chars();
let (lower, upper) = changes.size_hint();
let size = upper.unwrap_or(lower);
let mut changeset = ChangeSet::with_capacity(2 * size + 1); // rough estimate
let mut last = 0;
for (from, to, tendril) in changes {
// Verify ranges are ordered and not overlapping
debug_assert!(last <= from);
// Verify ranges are correct
debug_assert!(
from <= to,
"Edit end must end before it starts (should {from} <= {to})"
);
// Retain from last "to" to current "from"
changeset.retain(from - last);
let span = to - from;
match tendril {
Some(text) => {
changeset.insert(text);
changeset.delete(span);
}
None => changeset.delete(span),
}
last = to;
}
changeset.retain(len - last);
Self::from(changeset)
Self::from(ChangeSet::from_changes(doc, changes))
}
/// Generate a transaction from a set of potentially overlapping deletions
@@ -736,9 +748,60 @@ impl Transaction {
)
}
/// Generate a transaction with a change per selection range, which
/// generates a new selection as well. Each range is operated upon by
/// the given function and can optionally produce a new range. If none
/// is returned by the function, that range is mapped through the change
/// as usual.
pub fn change_by_and_with_selection<F>(doc: &Rope, selection: &Selection, mut f: F) -> Self
where
F: FnMut(&Range) -> (Change, Option<Range>),
{
let mut end_ranges = SmallVec::with_capacity(selection.len());
let mut offset = 0;
let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
let ((from, to, replacement), end_range) = f(start_range);
let mut change_size = to as isize - from as isize;
if let Some(ref text) = replacement {
change_size = text.chars().count() as isize - change_size;
} else {
change_size = -change_size;
}
let new_range = if let Some(end_range) = end_range {
end_range
} else {
let changeset = ChangeSet::from_change(doc, (from, to, replacement.clone()));
start_range.map(&changeset)
};
let offset_range = Range::new(
(new_range.anchor as isize + offset) as usize,
(new_range.head as isize + offset) as usize,
);
end_ranges.push(offset_range);
offset += change_size;
log::trace!(
"from: {}, to: {}, replacement: {:?}, offset: {}",
from,
to,
replacement,
offset
);
(from, to, replacement)
});
transaction.with_selection(Selection::new(end_ranges, selection.primary_index()))
}
/// Generate a transaction with a deletion per selection range.
/// Compared to using `change_by_selection` directly these ranges may overlap.
/// In that case they are merged
/// In that case they are merged.
pub fn delete_by_selection<F>(doc: &Rope, selection: &Selection, f: F) -> Self
where
F: FnMut(&Range) -> Deletion,
@@ -746,6 +809,59 @@ impl Transaction {
Self::delete(doc, selection.iter().map(f))
}
/// Generate a transaction with a delete per selection range, which
/// generates a new selection as well. Each range is operated upon by
/// the given function and can optionally produce a new range. If none
/// is returned by the function, that range is mapped through the change
/// as usual.
///
/// Compared to using `change_by_and_with_selection` directly these ranges
/// may overlap. In that case they are merged.
pub fn delete_by_and_with_selection<F>(doc: &Rope, selection: &Selection, mut f: F) -> Self
where
F: FnMut(&Range) -> (Deletion, Option<Range>),
{
let mut end_ranges = SmallVec::with_capacity(selection.len());
let mut offset = 0;
let mut last = 0;
let transaction = Transaction::delete_by_selection(doc, selection, |start_range| {
let ((from, to), end_range) = f(start_range);
// must account for possibly overlapping deletes
let change_size = if last > from { to - last } else { to - from };
let new_range = if let Some(end_range) = end_range {
end_range
} else {
let changeset = ChangeSet::from_change(doc, (from, to, None));
start_range.map(&changeset)
};
let offset_range = Range::new(
new_range.anchor.saturating_sub(offset),
new_range.head.saturating_sub(offset),
);
log::trace!(
"delete from: {}, to: {}, offset: {}, new_range: {:?}, offset_range: {:?}",
from,
to,
offset,
new_range,
offset_range
);
end_ranges.push(offset_range);
offset += change_size;
last = to;
(from, to)
});
transaction.with_selection(Selection::new(end_ranges, selection.primary_index()))
}
/// Insert text at each selection head.
pub fn insert(doc: &Rope, selection: &Selection, text: Tendril) -> Self {
Self::change_by_selection(doc, selection, |range| {

View File

@@ -530,6 +530,7 @@ impl MappableCommand {
select_prev_sibling, "Select previous sibling the in syntax tree",
select_all_siblings, "Select all siblings of the current node",
select_all_children, "Select all children of the current node",
expand_selection_around, "Expand selection to parent syntax node, but exclude the selection you started with",
jump_forward, "Jump forward on jumplist",
jump_backward, "Jump backward on jumplist",
save_selection, "Save current selection to jumplist",
@@ -3942,6 +3943,7 @@ fn goto_first_diag(cx: &mut Context) {
Some(diag) => Selection::single(diag.range.start, diag.range.end),
None => return,
};
doc.set_selection(view.id, selection);
view.diagnostics_handler
.immediately_show_diagnostic(doc, view.id);
@@ -3953,6 +3955,7 @@ fn goto_last_diag(cx: &mut Context) {
Some(diag) => Selection::single(diag.range.start, diag.range.end),
None => return,
};
doc.set_selection(view.id, selection);
view.diagnostics_handler
.immediately_show_diagnostic(doc, view.id);
@@ -4009,6 +4012,7 @@ fn goto_prev_diag(cx: &mut Context) {
view.diagnostics_handler
.immediately_show_diagnostic(doc, view.id);
};
cx.editor.apply_motion(motion)
}
@@ -4127,16 +4131,6 @@ pub mod insert {
}
}
// The default insert hook: simply insert the character
#[allow(clippy::unnecessary_wraps)] // need to use Option<> because of the Hook signature
fn insert(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> {
let cursors = selection.clone().cursors(doc.slice(..));
let mut t = Tendril::new();
t.push(ch);
let transaction = Transaction::insert(doc, &cursors, t);
Some(transaction)
}
use helix_core::auto_pairs;
use helix_view::editor::SmartTabConfig;
@@ -4146,15 +4140,25 @@ pub mod insert {
let selection = doc.selection(view.id);
let auto_pairs = doc.auto_pairs(cx.editor);
let transaction = auto_pairs
.as_ref()
.and_then(|ap| auto_pairs::hook(text, selection, c, ap))
.or_else(|| insert(text, selection, c));
let insert_char = |range: Range, ch: char| {
let cursor = range.cursor(text.slice(..));
let t = Tendril::from_iter([ch]);
((cursor, cursor, Some(t)), None)
};
let (view, doc) = current!(cx.editor);
if let Some(t) = transaction {
doc.apply(&t, view.id);
}
let transaction = Transaction::change_by_and_with_selection(text, selection, |range| {
auto_pairs
.as_ref()
.and_then(|ap| {
auto_pairs::hook_insert(text, range, c, ap)
.map(|(change, range)| (change, Some(range)))
.or_else(|| Some(insert_char(*range, c)))
})
.unwrap_or_else(|| insert_char(*range, c))
});
let doc = doc_mut!(cx.editor, &doc.id());
doc.apply(&transaction, view.id);
helix_event::dispatch(PostInsertChar { c, cx });
}
@@ -4398,82 +4402,96 @@ pub mod insert {
doc.apply(&transaction, view.id);
}
fn dedent(doc: &Document, range: &Range) -> Option<Deletion> {
let text = doc.text().slice(..);
let pos = range.cursor(text);
let line_start_pos = text.line_to_char(range.cursor_line(text));
// consider to delete by indent level if all characters before `pos` are indent units.
let fragment = Cow::from(text.slice(line_start_pos..pos));
if fragment.is_empty() || !fragment.chars().all(|ch| ch == ' ' || ch == '\t') {
return None;
}
if text.get_char(pos.saturating_sub(1)) == Some('\t') {
// fast path, delete one char
return Some((graphemes::nth_prev_grapheme_boundary(text, pos, 1), pos));
}
let tab_width = doc.tab_width();
let indent_width = doc.indent_width();
let width: usize = fragment
.chars()
.map(|ch| {
if ch == '\t' {
tab_width
} else {
// it can be none if it still meet control characters other than '\t'
// here just set the width to 1 (or some value better?).
ch.width().unwrap_or(1)
}
})
.sum();
// round down to nearest unit
let mut drop = width % indent_width;
// if it's already at a unit, consume a whole unit
if drop == 0 {
drop = indent_width
};
let mut chars = fragment.chars().rev();
let mut start = pos;
for _ in 0..drop {
// delete up to `drop` spaces
match chars.next() {
Some(' ') => start -= 1,
_ => break,
}
}
Some((start, pos)) // delete!
}
pub fn delete_char_backward(cx: &mut Context) {
let count = cx.count();
let (view, doc) = current_ref!(cx.editor);
let text = doc.text().slice(..);
let tab_width = doc.tab_width();
let indent_width = doc.indent_width();
let auto_pairs = doc.auto_pairs(cx.editor);
let transaction =
Transaction::delete_by_selection(doc.text(), doc.selection(view.id), |range| {
let transaction = Transaction::delete_by_and_with_selection(
doc.text(),
doc.selection(view.id),
|range| {
let pos = range.cursor(text);
log::debug!("cursor: {}, len: {}", pos, text.len_chars());
if pos == 0 {
return (pos, pos);
return ((pos, pos), None);
}
let line_start_pos = text.line_to_char(range.cursor_line(text));
// consider to delete by indent level if all characters before `pos` are indent units.
let fragment = Cow::from(text.slice(line_start_pos..pos));
if !fragment.is_empty() && fragment.chars().all(|ch| ch == ' ' || ch == '\t') {
if text.get_char(pos.saturating_sub(1)) == Some('\t') {
// fast path, delete one char
(graphemes::nth_prev_grapheme_boundary(text, pos, 1), pos)
} else {
let width: usize = fragment
.chars()
.map(|ch| {
if ch == '\t' {
tab_width
} else {
// it can be none if it still meet control characters other than '\t'
// here just set the width to 1 (or some value better?).
ch.width().unwrap_or(1)
}
})
.sum();
let mut drop = width % indent_width; // round down to nearest unit
if drop == 0 {
drop = indent_width
}; // if it's already at a unit, consume a whole unit
let mut chars = fragment.chars().rev();
let mut start = pos;
for _ in 0..drop {
// delete up to `drop` spaces
match chars.next() {
Some(' ') => start -= 1,
_ => break,
}
}
(start, pos) // delete!
}
} else {
match (
text.get_char(pos.saturating_sub(1)),
text.get_char(pos),
auto_pairs,
) {
(Some(_x), Some(_y), Some(ap))
if range.is_single_grapheme(text)
&& ap.get(_x).is_some()
&& ap.get(_x).unwrap().open == _x
&& ap.get(_x).unwrap().close == _y =>
// delete both autopaired characters
{
(
graphemes::nth_prev_grapheme_boundary(text, pos, count),
graphemes::nth_next_grapheme_boundary(text, pos, count),
)
}
_ =>
// delete 1 char
{
(graphemes::nth_prev_grapheme_boundary(text, pos, count), pos)
}
}
}
});
let (view, doc) = current!(cx.editor);
dedent(doc, range)
.map(|dedent| (dedent, None))
.or_else(|| {
auto_pairs::hook_delete(doc.text(), range, doc.auto_pairs(cx.editor)?)
.map(|(delete, new_range)| (delete, Some(new_range)))
})
.unwrap_or_else(|| {
(
(graphemes::nth_prev_grapheme_boundary(text, pos, count), pos),
None,
)
})
},
);
log::debug!("delete_char_backward transaction: {:?}", transaction);
let doc = doc_mut!(cx.editor, &doc.id());
doc.apply(&transaction, view.id);
}
@@ -5444,6 +5462,10 @@ fn reverse_selection_contents(cx: &mut Context) {
// tree sitter node selection
const EXPAND_KEY: &str = "expand";
const EXPAND_AROUND_BASE_KEY: &str = "expand_around_base";
const PARENTS_KEY: &str = "parents";
fn expand_selection(cx: &mut Context) {
let motion = |editor: &mut Editor| {
let (view, doc) = current!(editor);
@@ -5451,42 +5473,154 @@ fn expand_selection(cx: &mut Context) {
if let Some(syntax) = doc.syntax() {
let text = doc.text().slice(..);
let current_selection = doc.selection(view.id);
let current_selection = doc.selection(view.id).clone();
let selection = object::expand_selection(syntax, text, current_selection.clone());
// check if selection is different from the last one
if *current_selection != selection {
// save current selection so it can be restored using shrink_selection
view.object_selections.push(current_selection.clone());
if current_selection != selection {
let prev_selections = doc
.view_data_mut(view.id)
.object_selections
.entry(EXPAND_KEY)
.or_default();
doc.set_selection(view.id, selection);
// save current selection so it can be restored using shrink_selection
prev_selections.push(current_selection);
doc.set_selection_clear(view.id, selection, false);
}
}
};
cx.editor.apply_motion(motion);
}
fn shrink_selection(cx: &mut Context) {
let motion = |editor: &mut Editor| {
let (view, doc) = current!(editor);
let current_selection = doc.selection(view.id);
let current_selection = doc.selection(view.id).clone();
let prev_expansions = doc
.view_data_mut(view.id)
.object_selections
.entry(EXPAND_KEY)
.or_default();
// try to restore previous selection
if let Some(prev_selection) = view.object_selections.pop() {
if current_selection.contains(&prev_selection) {
doc.set_selection(view.id, prev_selection);
return;
} else {
// clear existing selection as they can't be shrunk to anyway
view.object_selections.clear();
if let Some(prev_selection) = prev_expansions.pop() {
// allow shrinking the selection only if current selection contains the previous object selection
doc.set_selection_clear(view.id, prev_selection, false);
// Do a corresponding pop of the parents from `expand_selection_around`
doc.view_data_mut(view.id)
.object_selections
.entry(PARENTS_KEY)
.and_modify(|parents| {
parents.pop();
});
// need to do this again because borrowing
let prev_expansions = doc
.view_data_mut(view.id)
.object_selections
.entry(EXPAND_KEY)
.or_default();
// if we've emptied out the previous expansions, then clear out the
// base history as well so it doesn't get used again erroneously
if prev_expansions.is_empty() {
doc.view_data_mut(view.id)
.object_selections
.entry(EXPAND_AROUND_BASE_KEY)
.and_modify(|base| {
base.clear();
});
}
return;
}
// if not previous selection, shrink to first child
if let Some(syntax) = doc.syntax() {
let text = doc.text().slice(..);
let selection = object::shrink_selection(syntax, text, current_selection.clone());
doc.set_selection(view.id, selection);
let selection = object::shrink_selection(syntax, text, current_selection);
doc.set_selection_clear(view.id, selection, false);
}
};
cx.editor.apply_motion(motion);
}
fn expand_selection_around(cx: &mut Context) {
let motion = |editor: &mut Editor| {
let (view, doc) = current!(editor);
if doc.syntax().is_some() {
// [NOTE] we do this pop and push dance because if we don't take
// ownership of the objects, then we require multiple
// mutable references to the view's object selections
let mut parents_selection = doc
.view_data_mut(view.id)
.object_selections
.entry(PARENTS_KEY)
.or_default()
.pop();
let mut base_selection = doc
.view_data_mut(view.id)
.object_selections
.entry(EXPAND_AROUND_BASE_KEY)
.or_default()
.pop();
let current_selection = doc.selection(view.id).clone();
if parents_selection.is_none() || base_selection.is_none() {
parents_selection = Some(current_selection.clone());
base_selection = Some(current_selection.clone());
}
let text = doc.text().slice(..);
let syntax = doc.syntax().unwrap();
let outside_selection =
object::expand_selection(syntax, text, parents_selection.clone().unwrap());
let target_selection = match outside_selection
.clone()
.without(&base_selection.clone().unwrap())
{
Some(sel) => sel,
None => outside_selection.clone(),
};
// check if selection is different from the last one
if target_selection != current_selection {
// save current selection so it can be restored using shrink_selection
doc.view_data_mut(view.id)
.object_selections
.entry(EXPAND_KEY)
.or_default()
.push(current_selection);
doc.set_selection_clear(view.id, target_selection, false);
}
let parents = doc
.view_data_mut(view.id)
.object_selections
.entry(PARENTS_KEY)
.or_default();
parents.push(parents_selection.unwrap());
parents.push(outside_selection);
doc.view_data_mut(view.id)
.object_selections
.entry(EXPAND_AROUND_BASE_KEY)
.or_default()
.push(base_selection.unwrap());
}
};
cx.editor.apply_motion(motion);
}
@@ -5504,6 +5638,7 @@ where
doc.set_selection(view.id, selection);
}
};
cx.editor.apply_motion(motion);
}
@@ -5605,8 +5740,6 @@ fn match_brackets(cx: &mut Context) {
doc.set_selection(view.id, selection);
}
//
fn jump_forward(cx: &mut Context) {
let count = cx.count();
let config = cx.editor.config();

View File

@@ -87,6 +87,7 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
";" => collapse_selection,
"A-;" => flip_selections,
"A-o" | "A-up" => expand_selection,
"A-O" => expand_selection_around,
"A-i" | "A-down" => shrink_selection,
"A-I" | "A-S-down" => select_all_children,
"A-p" | "A-left" => select_prev_sibling,

View File

@@ -16,10 +16,119 @@ fn matching_pairs() -> impl Iterator<Item = &'static (char, char)> {
async fn insert_basic() -> anyhow::Result<()> {
for pair in DEFAULT_PAIRS {
test((
format!("#[{}|]#", LINE_END),
"#[\n|]#",
format!("i{}", pair.0),
format!("{}#[|{}]#{}", pair.0, pair.1, LINE_END),
LineFeedHandling::AsIs,
format!("{}#[|{}]#", pair.0, pair.1),
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn insert_whitespace() -> anyhow::Result<()> {
for pair in DEFAULT_PAIRS {
test((
format!("{}#[|{}]#", pair.0, pair.1),
"i ",
format!("{} #[| ]#{}", pair.0, pair.1),
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn insert_whitespace_multi() -> anyhow::Result<()> {
for pair in differing_pairs() {
test((
format!(
indoc! {"\
{open}#[|{close}]#
{open}#(|{open})#{close}{close}
{open}{open}#(|{close}{close})#
foo#(|\n)#
"},
open = pair.0,
close = pair.1,
),
"i ",
format!(
indoc! {"\
{open} #[| ]#{close}
{open} #(|{open})#{close}{close}
{open}{open} #(| {close}{close})#
foo #(|\n)#
"},
open = pair.0,
close = pair.1,
),
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn append_whitespace_multi() -> anyhow::Result<()> {
for pair in differing_pairs() {
test((
format!(
indoc! {"\
#[|{open}]#{close}
#(|{open})#{open}{close}{close}
#(|{open}{open})#{close}{close}
#(|foo)#
"},
open = pair.0,
close = pair.1,
),
"a ",
format!(
indoc! {"\
#[{open} |]#{close}
#({open} {open}|)#{close}{close}
#({open}{open} |)#{close}{close}
#(foo \n|)#
"},
open = pair.0,
close = pair.1,
),
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn insert_whitespace_no_pair() -> anyhow::Result<()> {
for pair in DEFAULT_PAIRS {
// sanity check - do not insert extra whitespace unless immediately
// surrounded by a pair
test((
format!("{} #[|{}]#", pair.0, pair.1),
"i ",
format!("{} #[|{}]#", pair.0, pair.1),
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn insert_whitespace_no_matching_pair() -> anyhow::Result<()> {
for pair in differing_pairs() {
// sanity check - verify whitespace does not insert unless both pairs
// are matches, i.e. no two different openers
test((
format!("{}#[|{}]#", pair.0, pair.0),
"i ",
format!("{} #[|{}]#", pair.0, pair.0),
))
.await?;
}
@@ -567,3 +676,760 @@ async fn append_inside_nested_pair_multi() -> anyhow::Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn delete_basic() -> anyhow::Result<()> {
for pair in DEFAULT_PAIRS {
test((
format!("{}#[|{}]#{}", pair.0, pair.1, LINE_END),
"i<backspace>",
format!("#[|{}]#", LINE_END),
LineFeedHandling::AsIs,
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn delete_multi() -> anyhow::Result<()> {
for pair in DEFAULT_PAIRS {
test((
format!(
indoc! {"\
{open}#[|{close}]#
{open}#(|{close})#
{open}#(|{close})#
"},
open = pair.0,
close = pair.1,
),
"i<backspace>",
indoc! {"\
#[|\n]#
#(|\n)#
#(|\n)#
"},
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn delete_whitespace() -> anyhow::Result<()> {
for pair in DEFAULT_PAIRS {
test((
format!("{} #[| ]#{}", pair.0, pair.1),
"i<backspace>",
format!("{}#[|{}]#", pair.0, pair.1),
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn delete_whitespace_after_word() -> anyhow::Result<()> {
for pair in DEFAULT_PAIRS {
test((
format!("foo{} #[| ]#{}", pair.0, pair.1),
"i<backspace>",
format!("foo{}#[|{}]#", pair.0, pair.1),
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn delete_whitespace_multi() -> anyhow::Result<()> {
for pair in DEFAULT_PAIRS {
test((
format!(
indoc! {"\
{open} #[| ]#{close}
{open} #(|{open})#{close}{close}
{open}{open} #(| {close}{close})#
foo #(|\n)#
"},
open = pair.0,
close = pair.1,
),
"i<backspace>",
format!(
indoc! {"\
{open}#[|{close}]#
{open}#(|{open})#{close}{close}
{open}{open}#(|{close}{close})#
foo#(|\n)#
"},
open = pair.0,
close = pair.1,
),
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn delete_append_whitespace_multi() -> anyhow::Result<()> {
for pair in DEFAULT_PAIRS {
test((
format!(
indoc! {"\
#[{open} |]# {close}
#({open} |)#{open}{close}{close}
#({open}{open} |)# {close}{close}
#(foo |)#
"},
open = pair.0,
close = pair.1,
),
"a<backspace>",
format!(
indoc! {"\
#[{open}{close}|]#
#({open}{open}|)#{close}{close}
#({open}{open}{close}|)#{close}
#(foo\n|)#
"},
open = pair.0,
close = pair.1,
),
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn delete_whitespace_no_pair() -> anyhow::Result<()> {
for pair in DEFAULT_PAIRS {
test((
format!("{} #[|{}]#", pair.0, pair.1),
"i<backspace>",
format!("{} #[|{}]#", pair.0, pair.1),
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn delete_whitespace_no_matching_pair() -> anyhow::Result<()> {
for pair in differing_pairs() {
test((
format!("{} #[|{}]#", pair.0, pair.0),
"i<backspace>",
format!("{}#[|{}]#", pair.0, pair.0),
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn delete_configured_multi_byte_chars() -> anyhow::Result<()> {
// NOTE: these are multi-byte Unicode characters
let pairs = hashmap!('„' => '“', '' => '', '「' => '」');
let config = Config {
editor: helix_view::editor::Config {
auto_pairs: AutoPairConfig::Pairs(pairs.clone()),
..Default::default()
},
..Default::default()
};
for (open, close) in pairs.iter() {
test_with_config(
AppBuilder::new().with_config(config.clone()),
(
format!("{}#[|{}]#{}", open, close, LINE_END),
"i<backspace>",
format!("#[|{}]#", LINE_END),
LineFeedHandling::AsIs,
),
)
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn delete_after_word() -> anyhow::Result<()> {
for pair in DEFAULT_PAIRS {
test((
&format!("foo{}#[|{}]#", pair.0, pair.1),
"i<backspace>",
"foo#[|\n]#",
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn insert_then_delete() -> anyhow::Result<()> {
for pair in differing_pairs() {
test((
"#[\n|]#\n",
format!("ofoo{}<backspace>", pair.0),
"\nfoo#[\n|]#\n",
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn insert_then_delete_whitespace() -> anyhow::Result<()> {
for pair in differing_pairs() {
test((
"foo#[\n|]#",
format!("i{}<space><backspace><backspace>", pair.0),
"foo#[|\n]#",
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn insert_then_delete_multi() -> anyhow::Result<()> {
for pair in differing_pairs() {
test((
indoc! {"\
through a day#[\n|]#
in and out of weeks#(\n|)#
over a year#(\n|)#
"},
format!("i{}<space><backspace><backspace>", pair.0),
indoc! {"\
through a day#[|\n]#
in and out of weeks#(|\n)#
over a year#(|\n)#
"},
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn append_then_delete() -> anyhow::Result<()> {
for pair in differing_pairs() {
test((
"fo#[o|]#",
format!("a{}<space><backspace><backspace>", pair.0),
"fo#[o\n|]#",
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn append_then_delete_multi() -> anyhow::Result<()> {
for pair in differing_pairs() {
test((
indoc! {"\
#[through a day|]#
#(in and out of weeks|)#
#(over a year|)#
"},
format!("a{}<space><backspace><backspace>", pair.0),
indoc! {"\
#[through a day\n|]#
#(in and out of weeks\n|)#
#(over a year\n|)#
"},
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn delete_before_word() -> anyhow::Result<()> {
for pair in DEFAULT_PAIRS {
// sanity check unclosed pair delete
test((
format!("{}#[|f]#oo{}", pair.0, LINE_END),
"i<backspace>",
format!("#[|f]#oo{}", LINE_END),
))
.await?;
// deleting the closing pair should NOT delete the whole pair
test((
format!("{}{}#[|f]#oo{}", pair.0, pair.1, LINE_END),
"i<backspace>",
format!("{}#[|f]#oo{}", pair.0, LINE_END),
))
.await?;
// deleting whole pair before word
test((
format!("{}#[|{}]#foo{}", pair.0, pair.1, LINE_END),
"i<backspace>",
format!("#[|f]#oo{}", LINE_END),
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn delete_before_word_selection() -> anyhow::Result<()> {
for pair in DEFAULT_PAIRS {
// sanity check unclosed pair delete
test((
format!("{}#[|foo]#{}", pair.0, LINE_END),
"i<backspace>",
format!("#[|foo]#{}", LINE_END),
))
.await?;
// deleting the closing pair should NOT delete the whole pair
test((
format!("{}{}#[|foo]#{}", pair.0, pair.1, LINE_END),
"i<backspace>",
format!("{}#[|foo]#{}", pair.0, LINE_END),
))
.await?;
// deleting whole pair before word
test((
format!("{}#[|{}foo]#{}", pair.0, pair.1, LINE_END),
"i<backspace>",
format!("#[|foo]#{}", LINE_END),
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn delete_before_word_selection_trailing_word() -> anyhow::Result<()> {
for pair in DEFAULT_PAIRS {
test((
format!("foo{}#[|{} wor]#{}", pair.0, pair.1, LINE_END),
"i<backspace>",
format!("foo#[| wor]#{}", LINE_END),
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn delete_before_eol() -> anyhow::Result<()> {
for pair in DEFAULT_PAIRS {
test((
format!(
"{eol}{open}#[|{close}]#{eol}",
eol = LINE_END,
open = pair.0,
close = pair.1
),
"i<backspace>",
format!("{0}#[|{0}]#", LINE_END),
LineFeedHandling::AsIs,
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn delete_auto_pairs_disabled() -> anyhow::Result<()> {
for pair in DEFAULT_PAIRS {
test_with_config(
AppBuilder::new().with_config(Config {
editor: helix_view::editor::Config {
auto_pairs: AutoPairConfig::Enable(false),
..Default::default()
},
..Default::default()
}),
(
format!("{}#[|{}]#{}", pair.0, pair.1, LINE_END),
"i<backspace>",
format!("#[|{}]#{}", pair.1, LINE_END),
),
)
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn delete_before_multi_code_point_graphemes() -> anyhow::Result<()> {
for pair in DEFAULT_PAIRS {
test((
format!("hello {}#[|👨‍👩‍👧‍👦]# goodbye{}", pair.1, LINE_END),
"i<backspace>",
format!("hello #[|👨‍👩‍👧‍👦]# goodbye{}", LINE_END),
))
.await?;
test((
format!(
"hello {}{}#[|👨‍👩‍👧‍👦]# goodbye{}",
pair.0, pair.1, LINE_END
),
"i<backspace>",
format!("hello {}#[|👨‍👩‍👧‍👦]# goodbye{}", pair.0, LINE_END),
))
.await?;
test((
format!(
"hello {}#[|{}]#👨‍👩‍👧‍👦 goodbye{}",
pair.0, pair.1, LINE_END
),
"i<backspace>",
format!("hello #[|👨‍👩‍👧‍👦]# goodbye{}", LINE_END),
))
.await?;
test((
format!(
"hello {}#[|{}👨‍👩‍👧‍👦]# goodbye{}",
pair.0, pair.1, LINE_END
),
"i<backspace>",
format!("hello #[|👨‍👩‍👧‍👦]# goodbye{}", LINE_END),
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn delete_at_end_of_document() -> anyhow::Result<()> {
for pair in DEFAULT_PAIRS {
test(TestCase {
in_text: format!("{}{}{}", LINE_END, pair.0, pair.1),
in_selection: Selection::single(LINE_END.len() + 1, LINE_END.len() + 2),
in_keys: String::from("i<backspace>"),
out_text: String::from(LINE_END),
out_selection: Selection::single(LINE_END.len(), LINE_END.len()),
line_feed_handling: LineFeedHandling::AsIs,
})
.await?;
test(TestCase {
in_text: format!("foo{}{}{}", LINE_END, pair.0, pair.1),
in_selection: Selection::single(LINE_END.len() + 4, LINE_END.len() + 5),
in_keys: String::from("i<backspace>"),
out_text: format!("foo{}", LINE_END),
out_selection: Selection::single(3 + LINE_END.len(), 3 + LINE_END.len()),
line_feed_handling: LineFeedHandling::AsIs,
})
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn delete_nested_open_inside_pair() -> anyhow::Result<()> {
for pair in differing_pairs() {
test((
format!(
"{open}{open}#[|{close}]#{close}{eol}",
open = pair.0,
close = pair.1,
eol = LINE_END
),
"i<backspace>",
format!(
"{open}#[|{close}]#{eol}",
open = pair.0,
close = pair.1,
eol = LINE_END
),
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn delete_nested_open_inside_pair_multi() -> anyhow::Result<()> {
for outer_pair in DEFAULT_PAIRS {
for inner_pair in DEFAULT_PAIRS {
if inner_pair.0 == outer_pair.0 {
continue;
}
test((
format!(
"{outer_open}{inner_open}#[|{inner_close}]#{outer_close}{eol}{outer_open}{inner_open}#(|{inner_close})#{outer_close}{eol}{outer_open}{inner_open}#(|{inner_close})#{outer_close}{eol}",
outer_open = outer_pair.0,
outer_close = outer_pair.1,
inner_open = inner_pair.0,
inner_close = inner_pair.1,
eol = LINE_END
),
"i<backspace>",
format!(
"{outer_open}#[|{outer_close}]#{eol}{outer_open}#(|{outer_close})#{eol}{outer_open}#(|{outer_close})#{eol}",
outer_open = outer_pair.0,
outer_close = outer_pair.1,
eol = LINE_END
),
))
.await?;
}
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn delete_append_basic() -> anyhow::Result<()> {
for pair in DEFAULT_PAIRS {
test((
format!(
"#[{eol}{open}|]#{close}{eol}",
open = pair.0,
close = pair.1,
eol = LINE_END
),
"a<backspace>",
format!("#[{eol}{eol}|]#", eol = LINE_END),
LineFeedHandling::AsIs,
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn delete_append_multi_range() -> anyhow::Result<()> {
for pair in DEFAULT_PAIRS {
test((
format!(
"#[ {open}|]#{close}{eol}#( {open}|)#{close}{eol}#( {open}|)#{close}{eol}",
open = pair.0,
close = pair.1,
eol = LINE_END
),
"a<backspace>",
format!("#[ {eol}|]##( {eol}|)##( {eol}|)#", eol = LINE_END),
LineFeedHandling::AsIs,
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn delete_append_end_of_word() -> anyhow::Result<()> {
for pair in DEFAULT_PAIRS {
test((
format!(
"fo#[o{open}|]#{close}{eol}",
open = pair.0,
close = pair.1,
eol = LINE_END
),
"a<backspace>",
format!("fo#[o{}|]#", LINE_END),
LineFeedHandling::AsIs,
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn delete_mixed_dedent() -> anyhow::Result<()> {
for pair in DEFAULT_PAIRS {
test((
format!(
indoc! {"\
bar = {}#[|{}]#
#(|\n)#
foo#(|\n)#
"},
pair.0, pair.1,
),
"i<backspace>",
indoc! {"\
bar = #[|\n]#
#(|\n)#
fo#(|\n)#
"},
))
.await?;
test((
format!(
indoc! {"\
bar = {}#[|{}woop]#
#(|word)#
fo#(|o)#
"},
pair.0, pair.1,
),
"i<backspace>",
indoc! {"\
bar = #[|woop]#
#(|word)#
f#(|o)#
"},
))
.await?;
// delete from the right with append
test((
format!(
indoc! {"\
bar = #[|woop{}]#{}
#(| )#word
#(|fo)#o
"},
pair.0, pair.1,
),
"a<backspace>",
indoc! {"\
bar = #[woop\n|]#
#(w|)#ord
#(fo|)#
"},
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn delete_append_end_of_word_multi() -> anyhow::Result<()> {
for pair in DEFAULT_PAIRS {
test((
format!(
"fo#[o{open}|]#{close}{eol}fo#(o{open}|)#{close}{eol}fo#(o{open}|)#{close}{eol}",
open = pair.0,
close = pair.1,
eol = LINE_END
),
"a<backspace>",
format!("fo#[o{eol}|]#fo#(o{eol}|)#fo#(o{eol}|)#", eol = LINE_END),
LineFeedHandling::AsIs,
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn delete_append_inside_nested_pair() -> anyhow::Result<()> {
for pair in DEFAULT_PAIRS {
test((
format!(
"f#[oo{open}{open}|]#{close}{close}{eol}",
open = pair.0,
close = pair.1,
eol = LINE_END
),
"a<backspace>",
format!(
"f#[oo{open}{close}|]#{eol}",
open = pair.0,
close = pair.1,
eol = LINE_END
),
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn delete_append_middle_of_word() -> anyhow::Result<()> {
for pair in DEFAULT_PAIRS {
test((
format!(
"f#[oo{open}{open}|]#{close}{close}{eol}",
open = pair.0,
close = pair.1,
eol = LINE_END
),
"a<backspace>",
format!(
"f#[oo{open}{close}|]#{eol}",
open = pair.0,
close = pair.1,
eol = LINE_END
),
))
.await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn delete_append_inside_nested_pair_multi() -> anyhow::Result<()> {
for outer_pair in DEFAULT_PAIRS {
for inner_pair in DEFAULT_PAIRS {
if inner_pair.0 == outer_pair.0 {
continue;
}
test((
format!(
"f#[oo{outer_open}{inner_open}|]#{inner_close}{outer_close}{eol}f#(oo{outer_open}{inner_open}|)#{inner_close}{outer_close}{eol}f#(oo{outer_open}{inner_open}|)#{inner_close}{outer_close}{eol}",
outer_open = outer_pair.0,
outer_close = outer_pair.1,
inner_open = inner_pair.0,
inner_close = inner_pair.1,
eol = LINE_END
),
"a<backspace>",
format!(
"f#[oo{outer_open}{outer_close}|]#{eol}f#(oo{outer_open}{outer_close}|)#{eol}f#(oo{outer_open}{outer_close}|)#{eol}",
outer_open = outer_pair.0,
outer_close = outer_pair.1,
eol = LINE_END
),
))
.await?;
}
}
Ok(())
}

View File

@@ -948,3 +948,198 @@ async fn match_bracket() -> anyhow::Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn expand_shrink_selection() -> anyhow::Result<()> {
let tests = vec![
// single range
(
indoc! {r##"
Some(#[thing|]#)
"##},
"<A-o><A-o>",
indoc! {r##"
#[Some(thing)|]#
"##},
),
// multi range
(
indoc! {r##"
Some(#[thing|]#)
Some(#(other_thing|)#)
"##},
"<A-o>",
indoc! {r##"
Some#[(thing)|]#
Some#((other_thing)|)#
"##},
),
// multi range collision merges
(
indoc! {r##"
(
Some(#[thing|]#),
Some(#(other_thing|)#),
)
"##},
"<A-o><A-o><A-o>",
indoc! {r##"
#[(
Some(thing),
Some(other_thing),
)|]#
"##},
),
// multi range collision merges, then shrinks back to original
(
indoc! {r##"
(
Some(#[thing|]#),
Some(#(other_thing|)#),
)
"##},
"<A-o><A-o><A-o><A-i>",
indoc! {r##"
(
#[Some(thing)|]#,
#(Some(other_thing)|)#,
)
"##},
),
(
indoc! {r##"
(
Some(#[thing|]#),
Some(#(other_thing|)#),
)
"##},
"<A-o><A-o><A-o><A-i><A-i>",
indoc! {r##"
(
Some#[(thing)|]#,
Some#((other_thing)|)#,
)
"##},
),
(
indoc! {r##"
(
Some(#[thing|]#),
Some(#(other_thing|)#),
)
"##},
"<A-o><A-o><A-o><A-i><A-i><A-i>",
indoc! {r##"
(
Some(#[thing|]#),
Some(#(other_thing|)#),
)
"##},
),
// shrink with no expansion history defaults to first child
(
indoc! {r##"
#[(
Some(thing),
Some(other_thing),
)|]#
"##},
"<A-i>",
indoc! {r##"
(
#[Some(thing)|]#,
Some(other_thing),
)
"##},
),
// any movement cancels selection history and falls back to first child
(
indoc! {r##"
(
Some(#[thing|]#),
Some(#(other_thing|)#),
)
"##},
"<A-o><A-o><A-o>jkvkkk<A-i>",
indoc! {r##"
(
#[|Some(thing)]#,
Some(other_thing),
)
"##},
),
];
for test in tests {
test_with_config(AppBuilder::new().with_file("foo.rs", None), test).await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn expand_selection_around() -> anyhow::Result<()> {
let tests = vec![
// single cursor stays single cursor, first goes to end of current
// node, then parent
(
indoc! {r##"
Some(#[thing|]#)
"##},
"<A-O><A-O>",
indoc! {r##"
#[Some(|]#thing#()|)#
"##},
),
// shrinking restores previous selection
(
indoc! {r##"
Some(#[thing|]#)
"##},
"<A-O><A-O><A-i><A-i>",
indoc! {r##"
Some(#[thing|]#)
"##},
),
// multi range collision merges expand as normal, except with the
// original selection removed from the result
(
indoc! {r##"
(
Some(#[thing|]#),
Some(#(other_thing|)#),
)
"##},
"<A-O><A-O><A-O>",
indoc! {r##"
#[(
Some(|]#thing#(),
Some(|)#other_thing#(),
)|)#
"##},
),
(
indoc! {r##"
(
Some(#[thing|]#),
Some(#(other_thing|)#),
)
"##},
"<A-O><A-O><A-O><A-i><A-i><A-i>",
indoc! {r##"
(
Some(#[thing|]#),
Some(#(other_thing|)#),
)
"##},
),
];
for test in tests {
test_with_config(AppBuilder::new().with_file("foo.rs", None), test).await?;
}
Ok(())
}

View File

@@ -1320,15 +1320,27 @@ impl Document {
Ok(())
}
/// Select text within the [`Document`].
pub fn set_selection(&mut self, view_id: ViewId, selection: Selection) {
/// Select text within the [`Document`], optionally clearing the previous selection state.
pub fn set_selection_clear(&mut self, view_id: ViewId, selection: Selection, clear_prev: bool) {
// TODO: use a transaction?
self.selections
.insert(view_id, selection.ensure_invariants(self.text().slice(..)));
helix_event::dispatch(SelectionDidChange {
doc: self,
view: view_id,
})
});
if clear_prev {
self.view_data
.entry(view_id)
.and_modify(|view_data| view_data.object_selections.clear());
}
}
/// Select text within the [`Document`].
pub fn set_selection(&mut self, view_id: ViewId, selection: Selection) {
self.set_selection_clear(view_id, selection, true);
}
/// Find the origin selection of the text in a document, i.e. where
@@ -1520,6 +1532,12 @@ impl Document {
apply_inlay_hint_changes(padding_after_inlay_hints);
}
// clear out all associated view object selections, as they are no
// longer valid
self.view_data
.values_mut()
.for_each(|view_data| view_data.object_selections.clear());
helix_event::dispatch(DocumentDidChange {
doc: self,
view: view_id,
@@ -1962,13 +1980,13 @@ impl Document {
&self.selections
}
fn view_data(&self, view_id: ViewId) -> &ViewData {
pub fn view_data(&self, view_id: ViewId) -> &ViewData {
self.view_data
.get(&view_id)
.expect("This should only be called after ensure_view_init")
}
fn view_data_mut(&mut self, view_id: ViewId) -> &mut ViewData {
pub fn view_data_mut(&mut self, view_id: ViewId) -> &mut ViewData {
self.view_data.entry(view_id).or_default()
}
@@ -2286,9 +2304,13 @@ impl Document {
}
}
/// Stores data needed for views that are tied to this specific Document.
#[derive(Debug, Default)]
pub struct ViewData {
view_position: ViewPosition,
/// used to store previous selections of tree-sitter objects
pub object_selections: HashMap<&'static str, Vec<Selection>>,
}
#[derive(Clone, Debug)]
@@ -2339,6 +2361,7 @@ mod test {
Arc::new(ArcSwap::from_pointee(syntax::Loader::default())),
);
let view = ViewId::default();
doc.ensure_view_init(view);
doc.set_selection(view, Selection::single(0, 0));
let transaction =
@@ -2377,7 +2400,9 @@ mod test {
Arc::new(ArcSwap::new(Arc::new(Config::default()))),
Arc::new(ArcSwap::from_pointee(syntax::Loader::default())),
);
let view = ViewId::default();
doc.ensure_view_init(view);
doc.set_selection(view, Selection::single(5, 5));
// insert

View File

@@ -137,8 +137,6 @@ pub struct View {
// uses two docs because we want to be able to swap between the
// two last modified docs which we need to manually keep track of
pub last_modified_docs: [Option<DocumentId>; 2],
/// used to store previous selections of tree-sitter objects
pub object_selections: Vec<Selection>,
/// all gutter-related configuration settings, used primarily for gutter rendering
pub gutters: GutterConfig,
/// A mapping between documents and the last history revision the view was updated at.
@@ -175,7 +173,6 @@ impl View {
jumps: JumpList::new((doc, Selection::point(0))), // TODO: use actual sel
docs_access_history: Vec::new(),
last_modified_docs: [None, None],
object_selections: Vec::new(),
gutters,
doc_revisions: HashMap::new(),
diagnostics_handler: DiagnosticsHandler::new(),