mirror of
https://github.com/helix-editor/helix.git
synced 2024-11-25 19:03:30 +04:00
Merge 9360563997
into f305c7299d
This commit is contained in:
commit
0512b8fb03
@ -1,11 +1,9 @@
|
|||||||
//! When typing the opening character of one of the possible pairs defined below,
|
//! When typing the opening character of one of the possible pairs defined below,
|
||||||
//! this module provides the functionality to insert the paired closing character.
|
//! 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 std::collections::HashMap;
|
||||||
|
|
||||||
use smallvec::SmallVec;
|
|
||||||
|
|
||||||
// Heavily based on https://github.com/codemirror/closebrackets/
|
// Heavily based on https://github.com/codemirror/closebrackets/
|
||||||
pub const DEFAULT_PAIRS: &[(char, char)] = &[
|
pub const DEFAULT_PAIRS: &[(char, char)] = &[
|
||||||
('(', ')'),
|
('(', ')'),
|
||||||
@ -106,37 +104,128 @@ fn default() -> Self {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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]
|
// [TODO]
|
||||||
// * delete implementation where it erases the whole bracket (|) -> |
|
// * delete implementation where it erases the whole bracket (|) -> |
|
||||||
// * change to multi character pairs to handle cases like placing the cursor in the
|
// * 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 {% %}
|
// middle of triple quotes, and more exotic pairs like Jinja's {% %}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn hook(doc: &Rope, selection: &Selection, ch: char, pairs: &AutoPairs) -> Option<Transaction> {
|
pub fn hook_insert(
|
||||||
log::trace!("autopairs hook selection: {:#?}", selection);
|
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 let Some(pair) = pairs.get(ch) {
|
||||||
if pair.same() {
|
if pair.same() {
|
||||||
return Some(handle_same(doc, selection, pair));
|
return handle_insert_same(doc, range, pair);
|
||||||
} else if pair.open == ch {
|
} else if pair.open == ch {
|
||||||
return Some(handle_open(doc, selection, pair));
|
return handle_insert_open(doc, range, pair);
|
||||||
} else if pair.close == ch {
|
} else if pair.close == ch {
|
||||||
// && char_at pos == close
|
// && 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
|
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> {
|
fn prev_char(doc: &Rope, pos: usize) -> Option<char> {
|
||||||
if pos == 0 {
|
if pos == 0 {
|
||||||
return None;
|
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
|
/// 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
|
// When the character under the cursor changes due to complete pair
|
||||||
// insertion, we must look backward a grapheme and then add the length
|
// 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.
|
// 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
|
// 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() {
|
if start_range.head == doc.len_chars() && start_range.anchor == doc.len_chars() {
|
||||||
return Range::new(
|
return Range::new(start_range.anchor + 1, start_range.head + 1);
|
||||||
start_range.anchor + offset + 1,
|
|
||||||
start_range.head + offset + 1,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let doc_slice = doc.slice(..);
|
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
|
// just skip over graphemes
|
||||||
if len_inserted == 0 {
|
if len_inserted == 0 {
|
||||||
let end_anchor = if single_grapheme {
|
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,
|
// even for backward inserts with multiple grapheme selections,
|
||||||
// we want the anchor to stay where it is so that the relative
|
// 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
|
// foo([) wor]d -> insert ) -> foo()[ wor]d
|
||||||
} else {
|
} else {
|
||||||
start_range.anchor + offset
|
start_range.anchor
|
||||||
};
|
};
|
||||||
|
|
||||||
return Range::new(
|
return Range::new(
|
||||||
end_anchor,
|
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
|
// trivial case: only inserted a single-char opener, just move the selection
|
||||||
if len_inserted == 1 {
|
if len_inserted == 1 {
|
||||||
let end_anchor = if single_grapheme || start_range.direction() == Direction::Backward {
|
let end_anchor = if single_grapheme || start_range.direction() == Direction::Backward {
|
||||||
start_range.anchor + offset + 1
|
start_range.anchor + 1
|
||||||
} else {
|
} 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
|
// If the head = 0, then we must be in insert mode with a backward
|
||||||
// cursor, which implies the head will just move
|
// cursor, which implies the head will just move
|
||||||
let end_head = if start_range.head == 0 || start_range.direction() == Direction::Backward {
|
let end_head = if start_range.head == 0 || start_range.direction() == Direction::Backward {
|
||||||
start_range.head + offset + 1
|
start_range.head + 1
|
||||||
} else {
|
} else {
|
||||||
// We must have a forward cursor, which means we must move to the
|
// 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
|
// other end of the grapheme to get to where the new characters
|
||||||
// are inserted, then move the head to where it should be
|
// are inserted, then move the head to where it should be
|
||||||
let prev_bound = graphemes::prev_grapheme_boundary(doc_slice, start_range.head);
|
let prev_bound = graphemes::prev_grapheme_boundary(doc_slice, start_range.head);
|
||||||
log::trace!(
|
log::trace!("prev_bound: {}, len_inserted: {}", prev_bound, len_inserted);
|
||||||
"prev_bound: {}, offset: {}, len_inserted: {}",
|
|
||||||
prev_bound,
|
prev_bound + len_inserted
|
||||||
offset,
|
|
||||||
len_inserted
|
|
||||||
);
|
|
||||||
prev_bound + offset + len_inserted
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let end_anchor = match (start_range.len(), start_range.direction()) {
|
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
|
// if we are appending, the anchor stays where it is; only offset
|
||||||
// for multiple range insertions
|
// for multiple range insertions
|
||||||
} else {
|
} 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
|
// if we're backward, then the head is at the first char
|
||||||
// of the typed char, so we need to add the length of
|
// of the typed char, so we need to add the length of
|
||||||
// the closing char
|
// the closing char
|
||||||
graphemes::prev_grapheme_boundary(doc.slice(..), start_range.anchor)
|
graphemes::prev_grapheme_boundary(doc.slice(..), start_range.anchor) + len_inserted
|
||||||
+ len_inserted
|
|
||||||
+ offset
|
|
||||||
} else {
|
} else {
|
||||||
// when we are inserting in front of a selection, we need to move
|
// when we are inserting in front of a selection, we need to move
|
||||||
// the anchor over by however many characters were inserted overall
|
// the anchor over by however many characters were inserted overall
|
||||||
start_range.anchor + offset + len_inserted
|
start_range.anchor + len_inserted
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -262,12 +342,8 @@ fn get_next_range(doc: &Rope, start_range: &Range, offset: usize, len_inserted:
|
|||||||
Range::new(end_anchor, end_head)
|
Range::new(end_anchor, end_head)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_open(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
|
fn handle_insert_open(doc: &Rope, range: &Range, pair: &Pair) -> Option<(Change, Range)> {
|
||||||
let mut end_ranges = SmallVec::with_capacity(selection.len());
|
let cursor = range.cursor(doc.slice(..));
|
||||||
let mut offs = 0;
|
|
||||||
|
|
||||||
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 next_char = doc.get_char(cursor);
|
||||||
let len_inserted;
|
let len_inserted;
|
||||||
|
|
||||||
@ -275,11 +351,8 @@ fn handle_open(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
|
|||||||
// inserting exactly one or two chars. When arbitrary length pairs are
|
// inserting exactly one or two chars. When arbitrary length pairs are
|
||||||
// added, these will need to be changed.
|
// added, these will need to be changed.
|
||||||
let change = match next_char {
|
let change = match next_char {
|
||||||
Some(_) if !pair.should_close(doc, start_range) => {
|
Some(_) if !pair.should_close(doc, range) => {
|
||||||
len_inserted = 1;
|
return None;
|
||||||
let mut tendril = Tendril::new();
|
|
||||||
tendril.push(pair.open);
|
|
||||||
(cursor, cursor, Some(tendril))
|
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// insert open & close
|
// insert open & close
|
||||||
@ -289,57 +362,36 @@ fn handle_open(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let next_range = get_next_range(doc, start_range, offs, len_inserted);
|
let next_range = get_next_range(doc, range, len_inserted);
|
||||||
end_ranges.push(next_range);
|
let result = (change, next_range);
|
||||||
offs += len_inserted;
|
|
||||||
|
|
||||||
change
|
log::debug!("auto pair change: {:#?}", &result);
|
||||||
});
|
|
||||||
|
|
||||||
let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
|
Some(result)
|
||||||
log::debug!("auto pair transaction: {:#?}", t);
|
|
||||||
t
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_close(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
|
fn handle_insert_close(doc: &Rope, range: &Range, pair: &Pair) -> Option<(Change, Range)> {
|
||||||
let mut end_ranges = SmallVec::with_capacity(selection.len());
|
let cursor = range.cursor(doc.slice(..));
|
||||||
let mut offs = 0;
|
|
||||||
|
|
||||||
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 next_char = doc.get_char(cursor);
|
||||||
let mut len_inserted = 0;
|
|
||||||
|
|
||||||
let change = if next_char == Some(pair.close) {
|
let change = if next_char == Some(pair.close) {
|
||||||
// return transaction that moves past close
|
// return transaction that moves past close
|
||||||
(cursor, cursor, None) // no-op
|
(cursor, cursor, None) // no-op
|
||||||
} else {
|
} else {
|
||||||
len_inserted = 1;
|
return None;
|
||||||
let mut tendril = Tendril::new();
|
|
||||||
tendril.push(pair.close);
|
|
||||||
(cursor, cursor, Some(tendril))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let next_range = get_next_range(doc, start_range, offs, len_inserted);
|
let next_range = get_next_range(doc, range, 0);
|
||||||
end_ranges.push(next_range);
|
let result = (change, next_range);
|
||||||
offs += len_inserted;
|
|
||||||
|
|
||||||
change
|
log::debug!("auto pair change: {:#?}", &result);
|
||||||
});
|
|
||||||
|
|
||||||
let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
|
Some(result)
|
||||||
log::debug!("auto pair transaction: {:#?}", t);
|
|
||||||
t
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// handle cases where open and close is the same, or in triples ("""docstring""")
|
/// handle cases where open and close is the same, or in triples ("""docstring""")
|
||||||
fn handle_same(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
|
fn handle_insert_same(doc: &Rope, range: &Range, pair: &Pair) -> Option<(Change, Range)> {
|
||||||
let mut end_ranges = SmallVec::with_capacity(selection.len());
|
let cursor = range.cursor(doc.slice(..));
|
||||||
|
|
||||||
let mut offs = 0;
|
|
||||||
|
|
||||||
let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
|
|
||||||
let cursor = start_range.cursor(doc.slice(..));
|
|
||||||
let mut len_inserted = 0;
|
let mut len_inserted = 0;
|
||||||
let next_char = doc.get_char(cursor);
|
let next_char = doc.get_char(cursor);
|
||||||
|
|
||||||
@ -347,27 +399,19 @@ fn handle_same(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
|
|||||||
// return transaction that moves past close
|
// return transaction that moves past close
|
||||||
(cursor, cursor, None) // no-op
|
(cursor, cursor, None) // no-op
|
||||||
} else {
|
} else {
|
||||||
let mut pair_str = Tendril::new();
|
if !pair.should_close(doc, range) {
|
||||||
pair_str.push(pair.open);
|
return None;
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
len_inserted += pair_str.chars().count();
|
let pair_str = Tendril::from_iter([pair.open, pair.close]);
|
||||||
|
len_inserted = 2;
|
||||||
(cursor, cursor, Some(pair_str))
|
(cursor, cursor, Some(pair_str))
|
||||||
};
|
};
|
||||||
|
|
||||||
let next_range = get_next_range(doc, start_range, offs, len_inserted);
|
let next_range = get_next_range(doc, range, len_inserted);
|
||||||
end_ranges.push(next_range);
|
let result = (change, next_range);
|
||||||
offs += len_inserted;
|
|
||||||
|
|
||||||
change
|
log::debug!("auto pair change: {:#?}", &result);
|
||||||
});
|
|
||||||
|
|
||||||
let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
|
Some(result)
|
||||||
log::debug!("auto pair transaction: {:#?}", t);
|
|
||||||
t
|
|
||||||
}
|
}
|
||||||
|
@ -513,6 +513,49 @@ pub fn map_pos(&self, mut pos: usize, assoc: Assoc) -> usize {
|
|||||||
pub fn changes_iter(&self) -> ChangeIterator {
|
pub fn changes_iter(&self) -> ChangeIterator {
|
||||||
ChangeIterator::new(self)
|
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
|
/// Transaction represents a single undoable unit of changes. Several changes can be grouped into
|
||||||
@ -606,38 +649,7 @@ pub fn change<I>(doc: &Rope, changes: I) -> Self
|
|||||||
where
|
where
|
||||||
I: Iterator<Item = Change>,
|
I: Iterator<Item = Change>,
|
||||||
{
|
{
|
||||||
let len = doc.len_chars();
|
Self::from(ChangeSet::from_changes(doc, changes))
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate a transaction from a set of potentially overlapping deletions
|
/// Generate a transaction from a set of potentially overlapping deletions
|
||||||
@ -726,9 +738,60 @@ pub fn change_by_selection_ignore_overlapping(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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.
|
/// Generate a transaction with a deletion per selection range.
|
||||||
/// Compared to using `change_by_selection` directly these ranges may overlap.
|
/// 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
|
pub fn delete_by_selection<F>(doc: &Rope, selection: &Selection, f: F) -> Self
|
||||||
where
|
where
|
||||||
F: FnMut(&Range) -> Deletion,
|
F: FnMut(&Range) -> Deletion,
|
||||||
@ -736,6 +799,59 @@ pub fn delete_by_selection<F>(doc: &Rope, selection: &Selection, f: F) -> Self
|
|||||||
Self::delete(doc, selection.iter().map(f))
|
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.
|
/// Insert text at each selection head.
|
||||||
pub fn insert(doc: &Rope, selection: &Selection, text: Tendril) -> Self {
|
pub fn insert(doc: &Rope, selection: &Selection, text: Tendril) -> Self {
|
||||||
Self::change_by_selection(doc, selection, |range| {
|
Self::change_by_selection(doc, selection, |range| {
|
||||||
|
@ -3851,16 +3851,6 @@ fn exclude_cursor(text: RopeSlice, range: Range, cursor: Range) -> Range {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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_core::auto_pairs;
|
||||||
use helix_view::editor::SmartTabConfig;
|
use helix_view::editor::SmartTabConfig;
|
||||||
|
|
||||||
@ -3870,15 +3860,25 @@ pub fn insert_char(cx: &mut Context, c: char) {
|
|||||||
let selection = doc.selection(view.id);
|
let selection = doc.selection(view.id);
|
||||||
let auto_pairs = doc.auto_pairs(cx.editor);
|
let auto_pairs = doc.auto_pairs(cx.editor);
|
||||||
|
|
||||||
let transaction = auto_pairs
|
let insert_char = |range: Range, ch: char| {
|
||||||
.as_ref()
|
let cursor = range.cursor(text.slice(..));
|
||||||
.and_then(|ap| auto_pairs::hook(text, selection, c, ap))
|
let t = Tendril::from_iter([ch]);
|
||||||
.or_else(|| insert(text, selection, c));
|
((cursor, cursor, Some(t)), None)
|
||||||
|
};
|
||||||
|
|
||||||
let (view, doc) = current!(cx.editor);
|
let transaction = Transaction::change_by_and_with_selection(text, selection, |range| {
|
||||||
if let Some(t) = transaction {
|
auto_pairs
|
||||||
doc.apply(&t, view.id);
|
.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 });
|
helix_event::dispatch(PostInsertChar { c, cx });
|
||||||
}
|
}
|
||||||
@ -4049,28 +4049,26 @@ pub fn insert_newline(cx: &mut Context) {
|
|||||||
doc.apply(&transaction, view.id);
|
doc.apply(&transaction, view.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_char_backward(cx: &mut Context) {
|
fn dedent(doc: &Document, range: &Range) -> Option<Deletion> {
|
||||||
let count = cx.count();
|
|
||||||
let (view, doc) = current_ref!(cx.editor);
|
|
||||||
let text = doc.text().slice(..);
|
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 pos = range.cursor(text);
|
let pos = range.cursor(text);
|
||||||
if pos == 0 {
|
|
||||||
return (pos, pos);
|
|
||||||
}
|
|
||||||
let line_start_pos = text.line_to_char(range.cursor_line(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.
|
// consider to delete by indent level if all characters before `pos` are indent units.
|
||||||
let fragment = Cow::from(text.slice(line_start_pos..pos));
|
let fragment = Cow::from(text.slice(line_start_pos..pos));
|
||||||
if !fragment.is_empty() && fragment.chars().all(|ch| ch == ' ' || ch == '\t') {
|
|
||||||
|
if fragment.is_empty() || !fragment.chars().all(|ch| ch == ' ' || ch == '\t') {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
if text.get_char(pos.saturating_sub(1)) == Some('\t') {
|
if text.get_char(pos.saturating_sub(1)) == Some('\t') {
|
||||||
// fast path, delete one char
|
// fast path, delete one char
|
||||||
(graphemes::nth_prev_grapheme_boundary(text, pos, 1), pos)
|
return Some((graphemes::nth_prev_grapheme_boundary(text, pos, 1), pos));
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
let tab_width = doc.tab_width();
|
||||||
|
let indent_width = doc.indent_width();
|
||||||
|
|
||||||
let width: usize = fragment
|
let width: usize = fragment
|
||||||
.chars()
|
.chars()
|
||||||
.map(|ch| {
|
.map(|ch| {
|
||||||
@ -4083,12 +4081,18 @@ pub fn delete_char_backward(cx: &mut Context) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.sum();
|
.sum();
|
||||||
let mut drop = width % indent_width; // round down to nearest unit
|
|
||||||
|
// 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 {
|
if drop == 0 {
|
||||||
drop = indent_width
|
drop = indent_width
|
||||||
}; // if it's already at a unit, consume a whole unit
|
};
|
||||||
|
|
||||||
let mut chars = fragment.chars().rev();
|
let mut chars = fragment.chars().rev();
|
||||||
let mut start = pos;
|
let mut start = pos;
|
||||||
|
|
||||||
for _ in 0..drop {
|
for _ in 0..drop {
|
||||||
// delete up to `drop` spaces
|
// delete up to `drop` spaces
|
||||||
match chars.next() {
|
match chars.next() {
|
||||||
@ -4096,35 +4100,45 @@ pub fn delete_char_backward(cx: &mut Context) {
|
|||||||
_ => break,
|
_ => break,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(start, pos) // delete!
|
|
||||||
|
Some((start, pos)) // delete!
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
match (
|
pub fn delete_char_backward(cx: &mut Context) {
|
||||||
text.get_char(pos.saturating_sub(1)),
|
let count = cx.count();
|
||||||
text.get_char(pos),
|
let (view, doc) = current_ref!(cx.editor);
|
||||||
auto_pairs,
|
let text = doc.text().slice(..);
|
||||||
) {
|
|
||||||
(Some(_x), Some(_y), Some(ap))
|
let transaction = Transaction::delete_by_and_with_selection(
|
||||||
if range.is_single_grapheme(text)
|
doc.text(),
|
||||||
&& ap.get(_x).is_some()
|
doc.selection(view.id),
|
||||||
&& ap.get(_x).unwrap().open == _x
|
|range| {
|
||||||
&& ap.get(_x).unwrap().close == _y =>
|
let pos = range.cursor(text);
|
||||||
// delete both autopaired characters
|
|
||||||
{
|
log::debug!("cursor: {}, len: {}", pos, text.len_chars());
|
||||||
|
|
||||||
|
if pos == 0 {
|
||||||
|
return ((pos, pos), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
(graphemes::nth_prev_grapheme_boundary(text, pos, count), pos),
|
||||||
graphemes::nth_next_grapheme_boundary(text, pos, count),
|
None,
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
_ =>
|
},
|
||||||
// delete 1 char
|
);
|
||||||
{
|
|
||||||
(graphemes::nth_prev_grapheme_boundary(text, pos, count), pos)
|
log::debug!("delete_char_backward transaction: {:?}", transaction);
|
||||||
}
|
|
||||||
}
|
let doc = doc_mut!(cx.editor, &doc.id());
|
||||||
}
|
|
||||||
});
|
|
||||||
let (view, doc) = current!(cx.editor);
|
|
||||||
doc.apply(&transaction, view.id);
|
doc.apply(&transaction, view.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,10 +16,119 @@ fn matching_pairs() -> impl Iterator<Item = &'static (char, char)> {
|
|||||||
async fn insert_basic() -> anyhow::Result<()> {
|
async fn insert_basic() -> anyhow::Result<()> {
|
||||||
for pair in DEFAULT_PAIRS {
|
for pair in DEFAULT_PAIRS {
|
||||||
test((
|
test((
|
||||||
format!("#[{}|]#", LINE_END),
|
"#[\n|]#",
|
||||||
format!("i{}", pair.0),
|
format!("i{}", pair.0),
|
||||||
format!("{}#[|{}]#{}", pair.0, pair.1, LINE_END),
|
format!("{}#[|{}]#", pair.0, pair.1),
|
||||||
LineFeedHandling::AsIs,
|
))
|
||||||
|
.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?;
|
.await?;
|
||||||
}
|
}
|
||||||
@ -567,3 +676,760 @@ async fn append_inside_nested_pair_multi() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
Ok(())
|
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(())
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user