aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--helix-core/src/auto_pairs.rs335
-rw-r--r--helix-core/src/selection.rs20
2 files changed, 275 insertions, 80 deletions
diff --git a/helix-core/src/auto_pairs.rs b/helix-core/src/auto_pairs.rs
index c037afef..1b3de6ea 100644
--- a/helix-core/src/auto_pairs.rs
+++ b/helix-core/src/auto_pairs.rs
@@ -1,7 +1,7 @@
//! 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::{Range, Rope, Selection, Tendril, Transaction};
+use crate::{movement::Direction, Range, Rope, Selection, Tendril, Transaction};
use log::debug;
use smallvec::SmallVec;
@@ -30,7 +30,6 @@ const CLOSE_BEFORE: &str = ")]}'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{20
// [TODO]
// * delete implementation where it erases the whole bracket (|) -> |
-// * do not reduce to cursors; use whole selections, and surround with pair
// * 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 {% %}
@@ -38,20 +37,18 @@ const CLOSE_BEFORE: &str = ")]}'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{20
pub fn hook(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> {
debug!("autopairs hook selection: {:#?}", selection);
- let cursors = selection.clone().cursors(doc.slice(..));
-
for &(open, close) in PAIRS {
if open == ch {
if open == close {
- return Some(handle_same(doc, &cursors, open, CLOSE_BEFORE, OPEN_BEFORE));
+ return Some(handle_same(doc, selection, open, CLOSE_BEFORE, OPEN_BEFORE));
} else {
- return Some(handle_open(doc, &cursors, open, close, CLOSE_BEFORE));
+ return Some(handle_open(doc, selection, open, close, CLOSE_BEFORE));
}
}
if close == ch {
// && char_at pos == close
- return Some(handle_close(doc, &cursors, open, close));
+ return Some(handle_close(doc, selection, open, close));
}
}
@@ -66,6 +63,36 @@ fn prev_char(doc: &Rope, pos: usize) -> Option<char> {
doc.get_char(pos - 1)
}
+/// calculate what the resulting range should be for an auto pair insertion
+fn get_next_range(
+ start_range: &Range,
+ offset: usize,
+ typed_char: char,
+ len_inserted: usize,
+) -> Range {
+ let end_head = start_range.head + offset + typed_char.len_utf8();
+
+ let end_anchor = match (start_range.len(), start_range.direction()) {
+ // if we have a zero width cursor, it shifts to the same number
+ (0, _) => end_head,
+
+ // if we are inserting for a regular one-width cursor, the anchor
+ // moves with the head
+ (1, Direction::Forward) => end_head - 1,
+ (1, Direction::Backward) => end_head + 1,
+
+ // if we are appending, the anchor stays where it is; only offset
+ // for multiple range insertions
+ (_, Direction::Forward) => start_range.anchor + offset,
+
+ // when we are inserting in front of a selection, we need to move
+ // the anchor over by however many characters were inserted overall
+ (_, Direction::Backward) => start_range.anchor + offset + len_inserted,
+ };
+
+ Range::new(end_anchor, end_head)
+}
+
fn handle_open(
doc: &Rope,
selection: &Selection,
@@ -74,36 +101,32 @@ fn handle_open(
close_before: &str,
) -> Transaction {
let mut end_ranges = SmallVec::with_capacity(selection.len());
-
let mut offs = 0;
let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
- let start_head = start_range.head;
-
- let next = doc.get_char(start_head);
- let end_head = start_head + offs + open.len_utf8();
-
- let end_anchor = if start_range.is_empty() {
- end_head
- } else {
- start_range.anchor + offs
- };
-
- end_ranges.push(Range::new(end_anchor, end_head));
+ let cursor = start_range.cursor(doc.slice(..));
+ let next_char = doc.get_char(cursor);
+ let len_inserted;
- match next {
+ let change = match next_char {
Some(ch) if !close_before.contains(ch) => {
- offs += open.len_utf8();
- (start_head, start_head, Some(Tendril::from_char(open)))
+ len_inserted = open.len_utf8();
+ (cursor, cursor, Some(Tendril::from_char(open)))
}
// None | Some(ch) if close_before.contains(ch) => {}
_ => {
// insert open & close
let pair = Tendril::from_iter([open, close]);
- offs += open.len_utf8() + close.len_utf8();
- (start_head, start_head, Some(pair))
+ len_inserted = open.len_utf8() + close.len_utf8();
+ (cursor, cursor, Some(pair))
}
- }
+ };
+
+ let next_range = get_next_range(start_range, offs, open, len_inserted);
+ end_ranges.push(next_range);
+ offs += len_inserted;
+
+ change
});
let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
@@ -117,28 +140,28 @@ fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) ->
let mut offs = 0;
let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
- let start_head = start_range.head;
- let next = doc.get_char(start_head);
- let end_head = start_head + offs + close.len_utf8();
+ let cursor = start_range.cursor(doc.slice(..));
+ let next_char = doc.get_char(cursor);
+ let mut len_inserted = 0;
- let end_anchor = if start_range.is_empty() {
- end_head
+ let change = if next_char == Some(close) {
+ // return transaction that moves past close
+ (cursor, cursor, None) // no-op
} else {
- start_range.anchor + offs
+ len_inserted += close.len_utf8();
+ (cursor, cursor, Some(Tendril::from_char(close)))
};
- end_ranges.push(Range::new(end_anchor, end_head));
+ let next_range = get_next_range(start_range, offs, close, len_inserted);
+ end_ranges.push(next_range);
+ offs += len_inserted;
- if next == Some(close) {
- // return transaction that moves past close
- (start_head, start_head, None) // no-op
- } else {
- offs += close.len_utf8();
- (start_head, start_head, Some(Tendril::from_char(close)))
- }
+ change
});
- transaction.with_selection(Selection::new(end_ranges, selection.primary_index()))
+ let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
+ debug!("auto pair transaction: {:#?}", t);
+ t
}
/// handle cases where open and close is the same, or in triples ("""docstring""")
@@ -154,42 +177,41 @@ fn handle_same(
let mut offs = 0;
let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
- let start_head = start_range.head;
- let end_head = start_head + offs + token.len_utf8();
+ let cursor = start_range.cursor(doc.slice(..));
+ let mut len_inserted = 0;
- // if selection, retain anchor, if cursor, move over
- let end_anchor = if start_range.is_empty() {
- end_head
- } else {
- start_range.anchor + offs
- };
+ let next_char = doc.get_char(cursor);
+ let prev_char = prev_char(doc, cursor);
- end_ranges.push(Range::new(end_anchor, end_head));
-
- let next = doc.get_char(start_head);
- let prev = prev_char(doc, start_head);
-
- if next == Some(token) {
+ let change = if next_char == Some(token) {
// return transaction that moves past close
- (start_head, start_head, None) // no-op
+ (cursor, cursor, None) // no-op
} else {
let mut pair = Tendril::with_capacity(2 * token.len_utf8() as u32);
pair.push_char(token);
// for equal pairs, don't insert both open and close if either
// side has a non-pair char
- if (next.is_none() || close_before.contains(next.unwrap()))
- && (prev.is_none() || open_before.contains(prev.unwrap()))
+ if (next_char.is_none() || close_before.contains(next_char.unwrap()))
+ && (prev_char.is_none() || open_before.contains(prev_char.unwrap()))
{
pair.push_char(token);
}
- offs += pair.len();
- (start_head, start_head, Some(pair))
- }
+ len_inserted += pair.len();
+ (cursor, cursor, Some(pair))
+ };
+
+ let next_range = get_next_range(start_range, offs, token, len_inserted);
+ end_ranges.push(next_range);
+ offs += len_inserted;
+
+ change
});
- transaction.with_selection(Selection::new(end_ranges, selection.primary_index()))
+ let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
+ debug!("auto pair transaction: {:#?}", t);
+ t
}
#[cfg(test)]
@@ -252,7 +274,20 @@ mod test {
&Selection::single(1, 0),
PAIRS,
|open, close| format!("{}{}", open, close),
- &Selection::single(1, 1),
+ &Selection::single(2, 1),
+ );
+ }
+
+ /// [] -> append ( -> ([])
+ #[test]
+ fn test_append_blank() {
+ test_hooks_with_pairs(
+ // this is what happens when you have a totally blank document and then append
+ &Rope::from("\n\n"),
+ &Selection::single(0, 2),
+ PAIRS,
+ |open, close| format!("\n{}{}\n", open, close),
+ &Selection::single(0, 3),
);
}
@@ -276,26 +311,50 @@ mod test {
)
},
&Selection::new(
- smallvec!(Range::point(1), Range::point(4), Range::point(7),),
+ smallvec!(Range::new(2, 1), Range::new(5, 4), Range::new(8, 7),),
0,
),
);
}
- // [TODO] broken until it works with selections
/// fo[o] -> append ( -> fo[o(])
- #[ignore]
#[test]
fn test_append() {
test_hooks_with_pairs(
- &Rope::from("foo"),
+ &Rope::from("foo\n"),
&Selection::single(2, 4),
- PAIRS,
- |open, close| format!("foo{}{}", open, close),
+ differing_pairs(),
+ |open, close| format!("foo{}{}\n", open, close),
&Selection::single(2, 5),
);
}
+ /// fo[o] fo[o(])
+ /// fo[o] -> append ( -> fo[o(])
+ /// fo[o] fo[o(])
+ #[test]
+ fn test_append_multi() {
+ test_hooks_with_pairs(
+ &Rope::from("foo\nfoo\nfoo\n"),
+ &Selection::new(
+ smallvec!(Range::new(2, 4), Range::new(6, 8), Range::new(10, 12)),
+ 0,
+ ),
+ differing_pairs(),
+ |open, close| {
+ format!(
+ "foo{open}{close}\nfoo{open}{close}\nfoo{open}{close}\n",
+ open = open,
+ close = close
+ )
+ },
+ &Selection::new(
+ smallvec!(Range::new(2, 5), Range::new(8, 11), Range::new(14, 17)),
+ 0,
+ ),
+ );
+ }
+
/// ([]) -> insert ) -> ()[]
#[test]
fn test_insert_close_inside_pair() {
@@ -307,7 +366,23 @@ mod test {
&Selection::single(2, 1),
*close,
&doc,
- &Selection::point(2),
+ &Selection::single(3, 2),
+ );
+ }
+ }
+
+ /// [(]) -> append ) -> [()]
+ #[test]
+ fn test_append_close_inside_pair() {
+ for (open, close) in PAIRS {
+ let doc = Rope::from(format!("{}{}\n", open, close));
+
+ test_hooks(
+ &doc,
+ &Selection::single(0, 2),
+ *close,
+ &doc,
+ &Selection::single(0, 3),
);
}
}
@@ -323,8 +398,33 @@ mod test {
);
let expected_sel = Selection::new(
- // smallvec!(Range::new(3, 2), Range::new(6, 5), Range::new(9, 8),),
- smallvec!(Range::point(2), Range::point(5), Range::point(8),),
+ smallvec!(Range::new(3, 2), Range::new(6, 5), Range::new(9, 8),),
+ 0,
+ );
+
+ for (open, close) in PAIRS {
+ let doc = Rope::from(format!(
+ "{open}{close}\n{open}{close}\n{open}{close}\n",
+ open = open,
+ close = close
+ ));
+
+ test_hooks(&doc, &sel, *close, &doc, &expected_sel);
+ }
+ }
+
+ /// [(]) [()]
+ /// [(]) -> append ) -> [()]
+ /// [(]) [()]
+ #[test]
+ fn test_append_close_inside_pair_multi_cursor() {
+ let sel = Selection::new(
+ smallvec!(Range::new(0, 2), Range::new(3, 5), Range::new(6, 8),),
+ 0,
+ );
+
+ let expected_sel = Selection::new(
+ smallvec!(Range::new(0, 3), Range::new(3, 6), Range::new(6, 9),),
0,
);
@@ -343,7 +443,7 @@ mod test {
#[test]
fn test_insert_open_inside_pair() {
let sel = Selection::single(2, 1);
- let expected_sel = Selection::point(2);
+ let expected_sel = Selection::single(3, 2);
for (open, close) in differing_pairs() {
let doc = Rope::from(format!("{}{}", open, close));
@@ -357,11 +457,49 @@ mod test {
}
}
+ /// [word(]) -> append ( -> [word((]))
+ #[test]
+ fn test_append_open_inside_pair() {
+ let sel = Selection::single(0, 6);
+ let expected_sel = Selection::single(0, 7);
+
+ for (open, close) in differing_pairs() {
+ let doc = Rope::from(format!("word{}{}", open, close));
+ let expected_doc = Rope::from(format!(
+ "word{open}{open}{close}{close}",
+ open = open,
+ close = close
+ ));
+
+ test_hooks(&doc, &sel, *open, &expected_doc, &expected_sel);
+ }
+ }
+
/// ([]) -> insert " -> ("[]")
#[test]
fn test_insert_nested_open_inside_pair() {
let sel = Selection::single(2, 1);
- let expected_sel = Selection::point(2);
+ let expected_sel = Selection::single(3, 2);
+
+ for (outer_open, outer_close) in differing_pairs() {
+ let doc = Rope::from(format!("{}{}", outer_open, outer_close,));
+
+ for (inner_open, inner_close) in matching_pairs() {
+ let expected_doc = Rope::from(format!(
+ "{}{}{}{}",
+ outer_open, inner_open, inner_close, outer_close
+ ));
+
+ test_hooks(&doc, &sel, *inner_open, &expected_doc, &expected_sel);
+ }
+ }
+ }
+
+ /// [(]) -> append " -> [("]")
+ #[test]
+ fn test_append_nested_open_inside_pair() {
+ let sel = Selection::single(0, 2);
+ let expected_sel = Selection::single(0, 3);
for (outer_open, outer_close) in differing_pairs() {
let doc = Rope::from(format!("{}{}", outer_open, outer_close,));
@@ -385,21 +523,44 @@ mod test {
&Selection::single(1, 0),
PAIRS,
|open, _| format!("{}word", open),
- &Selection::point(1),
+ &Selection::single(2, 1),
)
}
- // [TODO] broken until it works with selections
/// [wor]d -> insert ( -> ([wor]d
#[test]
- #[ignore]
fn test_insert_open_with_selection() {
test_hooks_with_pairs(
&Rope::from("word"),
- &Selection::single(0, 4),
+ &Selection::single(3, 0),
PAIRS,
|open, _| format!("{}word", open),
- &Selection::single(1, 5),
+ &Selection::single(4, 1),
+ )
+ }
+
+ /// [wor]d -> append ) -> [wor)]d
+ #[test]
+ fn test_append_close_inside_non_pair_with_selection() {
+ let sel = Selection::single(0, 4);
+ let expected_sel = Selection::single(0, 5);
+
+ for (_, close) in PAIRS {
+ let doc = Rope::from("word");
+ let expected_doc = Rope::from(format!("wor{}d", close));
+ test_hooks(&doc, &sel, *close, &expected_doc, &expected_sel);
+ }
+ }
+
+ /// foo[ wor]d -> insert ( -> foo([) wor]d
+ #[test]
+ fn test_insert_open_trailing_word_with_selection() {
+ test_hooks_with_pairs(
+ &Rope::from("foo word"),
+ &Selection::single(7, 3),
+ differing_pairs(),
+ |open, close| format!("foo{}{} word", open, close),
+ &Selection::single(9, 4),
)
}
@@ -413,7 +574,7 @@ mod test {
fn test_insert_open_after_non_pair() {
let doc = Rope::from("word");
let sel = Selection::single(5, 4);
- let expected_sel = Selection::point(5);
+ let expected_sel = Selection::single(6, 5);
test_hooks_with_pairs(
&doc,
@@ -431,4 +592,18 @@ mod test {
&expected_sel,
);
}
+
+ /// appending with only a cursor should stay a cursor
+ ///
+ /// [] -> append to end "foo -> "foo[]"
+ #[test]
+ fn test_append_single_cursor() {
+ test_hooks_with_pairs(
+ &Rope::from("\n"),
+ &Selection::single(0, 1),
+ PAIRS,
+ |open, close| format!("{}{}\n", open, close),
+ &Selection::single(1, 2),
+ );
+ }
}
diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs
index 116a1c7c..884c98ac 100644
--- a/helix-core/src/selection.rs
+++ b/helix-core/src/selection.rs
@@ -7,6 +7,7 @@ use crate::{
ensure_grapheme_boundary_next, ensure_grapheme_boundary_prev, next_grapheme_boundary,
prev_grapheme_boundary,
},
+ movement::Direction,
Assoc, ChangeSet, RopeSlice,
};
use smallvec::{smallvec, SmallVec};
@@ -82,6 +83,13 @@ impl Range {
std::cmp::max(self.anchor, self.head)
}
+ /// Total length of the range.
+ #[inline]
+ #[must_use]
+ pub fn len(&self) -> usize {
+ self.to() - self.from()
+ }
+
/// The (inclusive) range of lines that the range overlaps.
#[inline]
#[must_use]
@@ -102,6 +110,18 @@ impl Range {
self.anchor == self.head
}
+ /// `Direction::Backward` when head < anchor.
+ /// `Direction::Backward` otherwise.
+ #[inline]
+ #[must_use]
+ pub fn direction(&self) -> Direction {
+ if self.head < self.anchor {
+ Direction::Backward
+ } else {
+ Direction::Forward
+ }
+ }
+
/// Check two ranges for overlap.
#[must_use]
pub fn overlaps(&self, other: &Self) -> bool {