@@ -2,6 +2,7 @@
2//! this module provides the functionality to insert the paired closing character.
2//! this module provides the functionality to insert the paired closing character.
4use crate::{Range, Rope, Selection, Tendril, Transaction};
4use crate::{Range, Rope, Selection, Tendril, Transaction};
5use smallvec::SmallVec;
6use smallvec::SmallVec;
7// Heavily based on https://github.com/codemirror/closebrackets/
8// Heavily based on https://github.com/codemirror/closebrackets/
@@ -15,7 +16,9 @@ pub const PAIRS: &[(char, char)] = &[
15 ('`', '`'),
16 ('`', '`'),
19+// [TODO] build this dynamically in language config. see #992
20+const OPEN_BEFORE: &str = "([{'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}";
18−const CLOSE_BEFORE: &str = ")]}'\":;> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}"; // includes space and newlines
21+const CLOSE_BEFORE: &str = ")]}'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}"; // includes space and newlines
20// insert hook:
23// insert hook:
21// Fn(doc, selection, char) => Option<Transaction>
24// 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
26// to simplify, maybe return Option<Transaction> and just reimplement the default
29// to simplify, maybe return Option<Transaction> and just reimplement the default
28−// TODO: delete implementation where it erases the whole bracket (|) -> |
32+// * delete implementation where it erases the whole bracket (|) -> |
33+// * do not reduce to cursors; use whole selections, and surround with pair
34+// * change to multi character pairs to handle cases like placing the cursor in the
35+// middle of triple quotes, and more exotic pairs like Jinja's {% %}
30#[must_use]
37#[must_use]
31pub fn hook(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> {
38pub fn hook(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> {
39+ debug!("autopairs hook selection: {:#?}", selection);
41+ let cursors = selection.clone().cursors(doc.slice(..));
32 for &(open, close) in PAIRS {
43 for &(open, close) in PAIRS {
33 if open == ch {
44 if open == ch {
34 if open == close {
45 if open == close {
35− return handle_same(doc, selection, open);
46+ return Some(handle_same(doc, &cursors, open, CLOSE_BEFORE, OPEN_BEFORE));
37− return Some(handle_open(doc, selection, open, close, CLOSE_BEFORE));
48+ return Some(handle_open(doc, &cursors, open, close, CLOSE_BEFORE));
41 if close == ch {
52 if close == ch {
42 // && char_at pos == close
53 // && char_at pos == close
43− return Some(handle_close(doc, selection, open, close));
54+ return Some(handle_close(doc, &cursors, open, close));
50−// TODO: special handling for lifetimes in rust: if preceeded by & or < don't auto close '
51−// for example "&'a mut", or "fn<'a>"
53−fn next_char(doc: &Rope, pos: usize) -> Option<char> {
61+fn prev_char(doc: &Rope, pos: usize) -> Option<char> {
54− if pos >= doc.len_chars() {
62+ if pos == 0 {
55 return None;
63 return None;
57− Some(doc.char(pos))
66+ doc.get_char(pos - 1)
59−// TODO: selections should be extended if range, moved if point.
61−// TODO: if not cursor but selection, wrap on both sides of selection (surround)
62fn handle_open(
69fn handle_open(
63 doc: &Rope,
70 doc: &Rope,
64 selection: &Selection,
71 selection: &Selection,
@@ -66,98 +73,362 @@ fn handle_open(
66 close: char,
73 close: char,
67 close_before: &str,
74 close_before: &str,
68) -> Transaction {
75) -> Transaction {
69− let mut ranges = SmallVec::with_capacity(selection.len());
76+ let mut end_ranges = SmallVec::with_capacity(selection.len());
71 let mut offs = 0;
78 let mut offs = 0;
73− let transaction = Transaction::change_by_selection(doc, selection, |range| {
80+ let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
74− let pos = range.head;
81+ let start_head = start_range.head;
75− let next = next_char(doc, pos);
83+ let next = doc.get_char(start_head);
77− let head = pos + offs + open.len_utf8();
84+ let end_head = start_head + offs + open.len_utf8();
78− // if selection, retain anchor, if cursor, move over
85+
86+ let end_anchor = if start_range.is_empty() {
89+ start_range.anchor + offs
79− ranges.push(Range::new(
92+ end_ranges.push(Range::new(end_anchor, end_head));
80− if range.is_empty() {
88 match next {
94 match next {
89 Some(ch) if !close_before.contains(ch) => {
95 Some(ch) if !close_before.contains(ch) => {
90− offs += 1;
96+ offs += open.len_utf8();
91− // TODO: else return (use default handler that inserts open)
92− (pos, pos, Some(Tendril::from_char(open)))
97+ (start_head, start_head, Some(Tendril::from_char(open)))
94 // None | Some(ch) if close_before.contains(ch) => {}
99 // None | Some(ch) if close_before.contains(ch) => {}
96 // insert open & close
101 // insert open & close
97− let mut pair = Tendril::with_capacity(2);
102+ let pair = Tendril::from_iter([open, close]);
98− pair.push_char(open);
103+ offs += open.len_utf8() + close.len_utf8();
99− pair.push_char(close);
104+ (start_head, start_head, Some(pair))
103− (pos, pos, Some(pair))
108− transaction.with_selection(Selection::new(ranges, selection.primary_index()))
109+ let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
110+ debug!("auto pair transaction: {:#?}", t);
111fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) -> Transaction {
114fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) -> Transaction {
112− let mut ranges = SmallVec::with_capacity(selection.len());
115+ let mut end_ranges = SmallVec::with_capacity(selection.len());
114 let mut offs = 0;
117 let mut offs = 0;
116− let transaction = Transaction::change_by_selection(doc, selection, |range| {
119+ let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
117− let pos = range.head;
120+ let start_head = start_range.head;
118− let next = next_char(doc, pos);
121+ let next = doc.get_char(start_head);
122+ let end_head = start_head + offs + close.len_utf8();
120− let head = pos + offs + close.len_utf8();
124+ let end_anchor = if start_range.is_empty() {
121− // if selection, retain anchor, if cursor, move over
125+ end_head
127+ start_range.anchor + offs
122− ranges.push(Range::new(
130+ end_ranges.push(Range::new(end_anchor, end_head));
123− if range.is_empty() {
131 if next == Some(close) {
132 if next == Some(close) {
132− // return transaction that moves past close
133+ // return transaction that moves past close
133− (pos, pos, None) // no-op
134+ (start_head, start_head, None) // no-op
135 offs += close.len_utf8();
136 offs += close.len_utf8();
137+ (start_head, start_head, Some(Tendril::from_char(close)))
141+ transaction.with_selection(Selection::new(end_ranges, selection.primary_index()))
137− // TODO: else return (use default handler that inserts close)
144+/// handle cases where open and close is the same, or in triples ("""docstring""")
138− (pos, pos, Some(Tendril::from_char(close)))
145+fn handle_same(
147+ selection: &Selection,
152+ let mut end_ranges = SmallVec::with_capacity(selection.len());
156+ let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
157+ let start_head = start_range.head;
158+ let end_head = start_head + offs + token.len_utf8();
160+ // if selection, retain anchor, if cursor, move over
161+ let end_anchor = if start_range.is_empty() {
164+ start_range.anchor + offs
167+ end_ranges.push(Range::new(end_anchor, end_head));
169+ let next = doc.get_char(start_head);
170+ let prev = prev_char(doc, start_head);
172+ if next == Some(token) {
173+ // return transaction that moves past close
174+ (start_head, start_head, None) // no-op
176+ let mut pair = Tendril::with_capacity(2 * token.len_utf8() as u32);
177+ pair.push_char(token);
179+ // for equal pairs, don't insert both open and close if either
180+ // side has a non-pair char
181+ if (next.is_none() || close_before.contains(next.unwrap()))
182+ && (prev.is_none() || open_before.contains(prev.unwrap()))
184+ pair.push_char(token);
188+ (start_head, start_head, Some(pair))
142− transaction.with_selection(Selection::new(ranges, selection.primary_index()))
192+ transaction.with_selection(Selection::new(end_ranges, selection.primary_index()))
145−// handle cases where open and close is the same, or in triples ("""docstring""")
195+#[cfg(test)]
146−fn handle_same(_doc: &Rope, _selection: &Selection, _token: char) -> Option<Transaction> {
196+mod test {
147− // if not cursor but selection, wrap
197+ use super::*;
148− // let next = next char
198+ use smallvec::smallvec;
150− // if next == bracket {
200+ fn differing_pairs() -> impl Iterator<Item = &'static (char, char)> {
151− // // if start of syntax node, insert token twice (new pair because node is complete)
201+ PAIRS.iter().filter(|(open, close)| open != close)
152− // // elseif colsedBracketAt
202+ }
153− // // is_triple == allow triple && next 3 is equal
203+
154− // // cursor jump over
204+ fn matching_pairs() -> impl Iterator<Item = &'static (char, char)> {
205+ PAIRS.iter().filter(|(open, close)| open == close)
212+ expected_doc: &Rope,
213+ expected_sel: &Selection,
215+ let trans = hook(&in_doc, &in_sel, ch).unwrap();
216+ let mut actual_doc = in_doc.clone();
217+ assert!(trans.apply(&mut actual_doc));
218+ assert_eq!(expected_doc, &actual_doc);
219+ assert_eq!(expected_sel, trans.selection().unwrap());
222+ fn test_hooks_with_pairs<I, F, R>(
226+ get_expected_doc: F,
227+ actual_sel: &Selection,
229+ I: IntoIterator<Item = &'static (char, char)>,
230+ F: Fn(char, char) -> R,
234+ pairs.into_iter().for_each(|(open, close)| {
239+ &Rope::from(get_expected_doc(*open, *close)),
245+ // [] indicates range
247+ /// [] -> insert ( -> ([])
249+ fn test_insert_blank() {
250+ test_hooks_with_pairs(
252+ &Selection::single(1, 0),
254+ |open, close| format!("{}{}", open, close),
255+ &Selection::single(1, 1),
260+ /// [] -> insert -> ([])
263+ fn test_insert_blank_multi_cursor() {
264+ test_hooks_with_pairs(
265+ &Rope::from("\n\n\n"),
267+ smallvec!(Range::new(1, 0), Range::new(2, 1), Range::new(3, 2),),
273+ "{open}{close}\n{open}{close}\n{open}{close}\n",
279+ smallvec!(Range::point(1), Range::point(4), Range::point(7),),
285+ // [TODO] broken until it works with selections
286+ /// fo[o] -> append ( -> fo[o(])
290+ test_hooks_with_pairs(
292+ &Selection::single(2, 4),
294+ |open, close| format!("foo{}{}", open, close),
295+ &Selection::single(2, 5),
299+ /// ([]) -> insert ) -> ()[]
301+ fn test_insert_close_inside_pair() {
302+ for (open, close) in PAIRS {
303+ let doc = Rope::from(format!("{}{}", open, close));
307+ &Selection::single(2, 1),
310+ &Selection::point(2),
316+ /// ([]) -> insert ) -> ()[]
319+ fn test_insert_close_inside_pair_multi_cursor() {
320+ let sel = Selection::new(
321+ smallvec!(Range::new(2, 1), Range::new(5, 4), Range::new(8, 7),),
325+ let expected_sel = Selection::new(
326+ // smallvec!(Range::new(3, 2), Range::new(6, 5), Range::new(9, 8),),
327+ smallvec!(Range::point(2), Range::point(5), Range::point(8),),
331+ for (open, close) in PAIRS {
332+ let doc = Rope::from(format!(
333+ "{open}{close}\n{open}{close}\n{open}{close}\n",
338+ test_hooks(&doc, &sel, *close, &doc, &expected_sel);
342+ /// ([]) -> insert ( -> (([]))
344+ fn test_insert_open_inside_pair() {
345+ let sel = Selection::single(2, 1);
346+ let expected_sel = Selection::point(2);
348+ for (open, close) in differing_pairs() {
349+ let doc = Rope::from(format!("{}{}", open, close));
350+ let expected_doc = Rope::from(format!(
351+ "{open}{open}{close}{close}",
356+ test_hooks(&doc, &sel, *open, &expected_doc, &expected_sel);
360+ /// ([]) -> insert " -> ("[]")
362+ fn test_insert_nested_open_inside_pair() {
363+ let sel = Selection::single(2, 1);
364+ let expected_sel = Selection::point(2);
366+ for (outer_open, outer_close) in differing_pairs() {
367+ let doc = Rope::from(format!("{}{}", outer_open, outer_close,));
369+ for (inner_open, inner_close) in matching_pairs() {
370+ let expected_doc = Rope::from(format!(
372+ outer_open, inner_open, inner_close, outer_close
375+ test_hooks(&doc, &sel, *inner_open, &expected_doc, &expected_sel);
380+ /// []word -> insert ( -> ([]word
382+ fn test_insert_open_before_non_pair() {
383+ test_hooks_with_pairs(
384+ &Rope::from("word"),
385+ &Selection::single(1, 0),
387+ |open, _| format!("{}word", open),
388+ &Selection::point(1),
392+ // [TODO] broken until it works with selections
393+ /// [wor]d -> insert ( -> ([wor]d
396+ fn test_insert_open_with_selection() {
397+ test_hooks_with_pairs(
398+ &Rope::from("word"),
399+ &Selection::single(0, 4),
401+ |open, _| format!("{}word", open),
402+ &Selection::single(1, 5),
156− //} else if allow_triple && followed by triple {
405+
157− //}
406+ /// we want pairs that are *not* the same char to be inserted after
158− //} else if next != word char && prev != bracket && prev != word char {
407+ /// a non-pair char, for cases like functions, but for pairs that are
159− // // condition checks for cases like I' where you don't want I'' (or I'm)
408+ /// the same char, we want to *not* insert a pair to handle cases like "I'm"
410+ /// word[] -> insert ( -> word([])
160− // insert pair ("")
411+ /// word[] -> insert ' -> word'[]
162− None
413+ fn test_insert_open_after_non_pair() {
414+ let doc = Rope::from("word");
415+ let sel = Selection::single(5, 4);
416+ let expected_sel = Selection::point(5);
418+ test_hooks_with_pairs(
422+ |open, close| format!("word{}{}", open, close),
426+ test_hooks_with_pairs(
430+ |open, _| format!("word{}", open),