mirror of
https://github.com/helix-editor/helix.git
synced 2025-10-06 08:23:27 +02:00
Compare commits
14 Commits
filesentry
...
dead10ck
Author | SHA1 | Date | |
---|---|---|---|
|
fdd3f8b88f | ||
|
09de695156 | ||
|
99775897ac | ||
|
631c3e3644 | ||
|
bb0198ee73 | ||
|
8dfb0b5b85 | ||
|
15834cfe81 | ||
|
0578063eed | ||
|
d26feaf1a3 | ||
|
699391dd19 | ||
|
d445661815 | ||
|
a938646274 | ||
|
d49c8efa6e | ||
|
9816696784 |
@@ -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)
|
||||
}
|
||||
|
@@ -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()))
|
||||
})
|
||||
}
|
||||
|
@@ -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)#"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -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| {
|
||||
|
@@ -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();
|
||||
|
@@ -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,
|
||||
|
@@ -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(())
|
||||
}
|
||||
|
@@ -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(())
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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(),
|
||||
|
Reference in New Issue
Block a user