aboutsummaryrefslogtreecommitdiff
path: root/helix-core
diff options
context:
space:
mode:
Diffstat (limited to 'helix-core')
-rw-r--r--helix-core/src/auto_pairs.rs421
-rw-r--r--helix-core/src/transaction.rs2
2 files changed, 347 insertions, 76 deletions
diff --git a/helix-core/src/auto_pairs.rs b/helix-core/src/auto_pairs.rs
index cc966852..c037afef 100644
--- a/helix-core/src/auto_pairs.rs
+++ b/helix-core/src/auto_pairs.rs
@@ -2,6 +2,7 @@
//! this module provides the functionality to insert the paired closing character.
use crate::{Range, Rope, Selection, Tendril, Transaction};
+use log::debug;
use smallvec::SmallVec;
// Heavily based on https://github.com/codemirror/closebrackets/
@@ -15,7 +16,9 @@ pub const PAIRS: &[(char, char)] = &[
('`', '`'),
];
-const CLOSE_BEFORE: &str = ")]}'\":;> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}"; // includes space and newlines
+// [TODO] build this dynamically in language config. see #992
+const OPEN_BEFORE: &str = "([{'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}";
+const CLOSE_BEFORE: &str = ")]}'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}"; // includes space and newlines
// insert hook:
// Fn(doc, selection, char) => Option<Transaction>
@@ -25,40 +28,44 @@ const CLOSE_BEFORE: &str = ")]}'\":;> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{202
//
// to simplify, maybe return Option<Transaction> and just reimplement the default
-// TODO: delete implementation where it erases the whole bracket (|) -> |
+// [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 {% %}
#[must_use]
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 handle_same(doc, selection, open);
+ return Some(handle_same(doc, &cursors, open, CLOSE_BEFORE, OPEN_BEFORE));
} else {
- return Some(handle_open(doc, selection, open, close, CLOSE_BEFORE));
+ return Some(handle_open(doc, &cursors, open, close, CLOSE_BEFORE));
}
}
if close == ch {
// && char_at pos == close
- return Some(handle_close(doc, selection, open, close));
+ return Some(handle_close(doc, &cursors, open, close));
}
}
None
}
-// TODO: special handling for lifetimes in rust: if preceeded by & or < don't auto close '
-// for example "&'a mut", or "fn<'a>"
-
-fn next_char(doc: &Rope, pos: usize) -> Option<char> {
- if pos >= doc.len_chars() {
+fn prev_char(doc: &Rope, pos: usize) -> Option<char> {
+ if pos == 0 {
return None;
}
- Some(doc.char(pos))
+
+ doc.get_char(pos - 1)
}
-// TODO: selections should be extended if range, moved if point.
-// TODO: if not cursor but selection, wrap on both sides of selection (surround)
fn handle_open(
doc: &Rope,
selection: &Selection,
@@ -66,98 +73,362 @@ fn handle_open(
close: char,
close_before: &str,
) -> Transaction {
- let mut ranges = SmallVec::with_capacity(selection.len());
+ let mut end_ranges = SmallVec::with_capacity(selection.len());
let mut offs = 0;
- let transaction = Transaction::change_by_selection(doc, selection, |range| {
- let pos = range.head;
- let next = next_char(doc, pos);
+ let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
+ let start_head = start_range.head;
- let head = pos + offs + open.len_utf8();
- // if selection, retain anchor, if cursor, move over
- ranges.push(Range::new(
- if range.is_empty() {
- head
- } else {
- range.anchor + offs
- },
- 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));
match next {
Some(ch) if !close_before.contains(ch) => {
- offs += 1;
- // TODO: else return (use default handler that inserts open)
- (pos, pos, Some(Tendril::from_char(open)))
+ offs += open.len_utf8();
+ (start_head, start_head, Some(Tendril::from_char(open)))
}
// None | Some(ch) if close_before.contains(ch) => {}
_ => {
// insert open & close
- let mut pair = Tendril::with_capacity(2);
- pair.push_char(open);
- pair.push_char(close);
-
- offs += 2;
-
- (pos, pos, Some(pair))
+ let pair = Tendril::from_iter([open, close]);
+ offs += open.len_utf8() + close.len_utf8();
+ (start_head, start_head, Some(pair))
}
}
});
- transaction.with_selection(Selection::new(ranges, selection.primary_index()))
+ let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
+ debug!("auto pair transaction: {:#?}", t);
+ t
}
fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) -> Transaction {
- let mut ranges = SmallVec::with_capacity(selection.len());
+ let mut end_ranges = SmallVec::with_capacity(selection.len());
let mut offs = 0;
- let transaction = Transaction::change_by_selection(doc, selection, |range| {
- let pos = range.head;
- let next = next_char(doc, pos);
+ 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 head = pos + offs + close.len_utf8();
- // if selection, retain anchor, if cursor, move over
- ranges.push(Range::new(
- if range.is_empty() {
- head
- } else {
- range.anchor + offs
- },
- head,
- ));
+ let end_anchor = if start_range.is_empty() {
+ end_head
+ } else {
+ start_range.anchor + offs
+ };
+
+ end_ranges.push(Range::new(end_anchor, end_head));
if next == Some(close) {
- // return transaction that moves past close
- (pos, pos, None) // no-op
+ // 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)))
+ }
+ });
+
+ transaction.with_selection(Selection::new(end_ranges, selection.primary_index()))
+}
- // TODO: else return (use default handler that inserts close)
- (pos, pos, Some(Tendril::from_char(close)))
+/// handle cases where open and close is the same, or in triples ("""docstring""")
+fn handle_same(
+ doc: &Rope,
+ selection: &Selection,
+ token: char,
+ close_before: &str,
+ open_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 end_head = start_head + offs + token.len_utf8();
+
+ // if selection, retain anchor, if cursor, move over
+ 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 next = doc.get_char(start_head);
+ let prev = prev_char(doc, start_head);
+
+ if next == Some(token) {
+ // return transaction that moves past close
+ (start_head, start_head, 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()))
+ {
+ pair.push_char(token);
+ }
+
+ offs += pair.len();
+ (start_head, start_head, Some(pair))
}
});
- transaction.with_selection(Selection::new(ranges, selection.primary_index()))
+ transaction.with_selection(Selection::new(end_ranges, selection.primary_index()))
}
-// handle cases where open and close is the same, or in triples ("""docstring""")
-fn handle_same(_doc: &Rope, _selection: &Selection, _token: char) -> Option<Transaction> {
- // if not cursor but selection, wrap
- // let next = next char
-
- // if next == bracket {
- // // if start of syntax node, insert token twice (new pair because node is complete)
- // // elseif colsedBracketAt
- // // is_triple == allow triple && next 3 is equal
- // // cursor jump over
- // }
- //} else if allow_triple && followed by triple {
- //}
- //} else if next != word char && prev != bracket && prev != word char {
- // // condition checks for cases like I' where you don't want I'' (or I'm)
- // insert pair ("")
- //}
- None
+#[cfg(test)]
+mod test {
+ use super::*;
+ use smallvec::smallvec;
+
+ fn differing_pairs() -> impl Iterator<Item = &'static (char, char)> {
+ PAIRS.iter().filter(|(open, close)| open != close)
+ }
+
+ fn matching_pairs() -> impl Iterator<Item = &'static (char, char)> {
+ PAIRS.iter().filter(|(open, close)| open == close)
+ }
+
+ fn test_hooks(
+ in_doc: &Rope,
+ in_sel: &Selection,
+ ch: char,
+ expected_doc: &Rope,
+ expected_sel: &Selection,
+ ) {
+ let trans = hook(&in_doc, &in_sel, ch).unwrap();
+ let mut actual_doc = in_doc.clone();
+ assert!(trans.apply(&mut actual_doc));
+ assert_eq!(expected_doc, &actual_doc);
+ assert_eq!(expected_sel, trans.selection().unwrap());
+ }
+
+ fn test_hooks_with_pairs<I, F, R>(
+ in_doc: &Rope,
+ in_sel: &Selection,
+ pairs: I,
+ get_expected_doc: F,
+ actual_sel: &Selection,
+ ) where
+ I: IntoIterator<Item = &'static (char, char)>,
+ F: Fn(char, char) -> R,
+ R: Into<Rope>,
+ Rope: From<R>,
+ {
+ pairs.into_iter().for_each(|(open, close)| {
+ test_hooks(
+ in_doc,
+ in_sel,
+ *open,
+ &Rope::from(get_expected_doc(*open, *close)),
+ actual_sel,
+ )
+ });
+ }
+
+ // [] indicates range
+
+ /// [] -> insert ( -> ([])
+ #[test]
+ fn test_insert_blank() {
+ test_hooks_with_pairs(
+ &Rope::new(),
+ &Selection::single(1, 0),
+ PAIRS,
+ |open, close| format!("{}{}", open, close),
+ &Selection::single(1, 1),
+ );
+ }
+
+ /// [] ([])
+ /// [] -> insert -> ([])
+ /// [] ([])
+ #[test]
+ fn test_insert_blank_multi_cursor() {
+ test_hooks_with_pairs(
+ &Rope::from("\n\n\n"),
+ &Selection::new(
+ smallvec!(Range::new(1, 0), Range::new(2, 1), Range::new(3, 2),),
+ 0,
+ ),
+ PAIRS,
+ |open, close| {
+ format!(
+ "{open}{close}\n{open}{close}\n{open}{close}\n",
+ open = open,
+ close = close
+ )
+ },
+ &Selection::new(
+ smallvec!(Range::point(1), Range::point(4), Range::point(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"),
+ &Selection::single(2, 4),
+ PAIRS,
+ |open, close| format!("foo{}{}", open, close),
+ &Selection::single(2, 5),
+ );
+ }
+
+ /// ([]) -> insert ) -> ()[]
+ #[test]
+ fn test_insert_close_inside_pair() {
+ for (open, close) in PAIRS {
+ let doc = Rope::from(format!("{}{}", open, close));
+
+ test_hooks(
+ &doc,
+ &Selection::single(2, 1),
+ *close,
+ &doc,
+ &Selection::point(2),
+ );
+ }
+ }
+
+ /// ([]) ()[]
+ /// ([]) -> insert ) -> ()[]
+ /// ([]) ()[]
+ #[test]
+ fn test_insert_close_inside_pair_multi_cursor() {
+ let sel = Selection::new(
+ smallvec!(Range::new(2, 1), Range::new(5, 4), Range::new(8, 7),),
+ 0,
+ );
+
+ 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),),
+ 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);
+ }
+ }
+
+ /// ([]) -> insert ( -> (([]))
+ #[test]
+ fn test_insert_open_inside_pair() {
+ let sel = Selection::single(2, 1);
+ let expected_sel = Selection::point(2);
+
+ for (open, close) in differing_pairs() {
+ let doc = Rope::from(format!("{}{}", open, close));
+ let expected_doc = Rope::from(format!(
+ "{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);
+
+ 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);
+ }
+ }
+ }
+
+ /// []word -> insert ( -> ([]word
+ #[test]
+ fn test_insert_open_before_non_pair() {
+ test_hooks_with_pairs(
+ &Rope::from("word"),
+ &Selection::single(1, 0),
+ PAIRS,
+ |open, _| format!("{}word", open),
+ &Selection::point(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),
+ PAIRS,
+ |open, _| format!("{}word", open),
+ &Selection::single(1, 5),
+ )
+ }
+
+ /// we want pairs that are *not* the same char to be inserted after
+ /// a non-pair char, for cases like functions, but for pairs that are
+ /// the same char, we want to *not* insert a pair to handle cases like "I'm"
+ ///
+ /// word[] -> insert ( -> word([])
+ /// word[] -> insert ' -> word'[]
+ #[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);
+
+ test_hooks_with_pairs(
+ &doc,
+ &sel,
+ differing_pairs(),
+ |open, close| format!("word{}{}", open, close),
+ &expected_sel,
+ );
+
+ test_hooks_with_pairs(
+ &doc,
+ &sel,
+ matching_pairs(),
+ |open, _| format!("word{}", open),
+ &expected_sel,
+ );
+ }
}
diff --git a/helix-core/src/transaction.rs b/helix-core/src/transaction.rs
index b62f4a9b..d8d389f3 100644
--- a/helix-core/src/transaction.rs
+++ b/helix-core/src/transaction.rs
@@ -409,7 +409,7 @@ impl ChangeSet {
/// Transaction represents a single undoable unit of changes. Several changes can be grouped into
/// a single transaction.
-#[derive(Debug, Default, Clone)]
+#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Transaction {
changes: ChangeSet,
selection: Option<Selection>,