@@ -2,6 +2,7 @@
22//! this module provides the functionality to insert the paired closing character.
33
44use crate::{Range, Rope, Selection, Tendril, Transaction};
5+use log::debug;
56use smallvec::SmallVec;
67
78// Heavily based on https://github.com/codemirror/closebrackets/
@@ -15,7 +16,9 @@ pub const PAIRS: &[(char, char)] = &[
1516 ('`', '`'),
1617];
1718
18−const CLOSE_BEFORE: &str = ")]}'\":;> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}"; // includes space and newlines
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}";
21+const CLOSE_BEFORE: &str = ")]}'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}"; // includes space and newlines
1922
2023// insert hook:
2124// 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
2528//
2629// to simplify, maybe return Option<Transaction> and just reimplement the default
2730
28−// TODO: delete implementation where it erases the whole bracket (|) -> |
31+// [TODO]
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 {% %}
2936
3037#[must_use]
3138pub fn hook(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> {
39+ debug!("autopairs hook selection: {:#?}", selection);
40+
41+ let cursors = selection.clone().cursors(doc.slice(..));
42+
3243 for &(open, close) in PAIRS {
3344 if open == ch {
3445 if open == close {
35− return handle_same(doc, selection, open);
46+ return Some(handle_same(doc, &cursors, open, CLOSE_BEFORE, OPEN_BEFORE));
3647 } else {
37− return Some(handle_open(doc, selection, open, close, CLOSE_BEFORE));
48+ return Some(handle_open(doc, &cursors, open, close, CLOSE_BEFORE));
3849 }
3950 }
4051
4152 if close == ch {
4253 // && char_at pos == close
43− return Some(handle_close(doc, selection, open, close));
54+ return Some(handle_close(doc, &cursors, open, close));
4455 }
4556 }
4657
4758 None
4859}
4960
50−// TODO: special handling for lifetimes in rust: if preceeded by & or < don't auto close '
51−// for example "&'a mut", or "fn<'a>"
52−
53−fn next_char(doc: &Rope, pos: usize) -> Option<char> {
54− if pos >= doc.len_chars() {
61+fn prev_char(doc: &Rope, pos: usize) -> Option<char> {
62+ if pos == 0 {
5563 return None;
5664 }
57− Some(doc.char(pos))
65+
66+ doc.get_char(pos - 1)
5867}
59−// TODO: selections should be extended if range, moved if point.
6068
61−// TODO: if not cursor but selection, wrap on both sides of selection (surround)
6269fn handle_open(
6370 doc: &Rope,
6471 selection: &Selection,
@@ -66,98 +73,362 @@ fn handle_open(
6673 close: char,
6774 close_before: &str,
6875) -> Transaction {
69− let mut ranges = SmallVec::with_capacity(selection.len());
76+ let mut end_ranges = SmallVec::with_capacity(selection.len());
7077
7178 let mut offs = 0;
7279
73− let transaction = Transaction::change_by_selection(doc, selection, |range| {
74− let pos = range.head;
75− let next = next_char(doc, pos);
80+ let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
81+ let start_head = start_range.head;
7682
77− let head = pos + offs + open.len_utf8();
78− // if selection, retain anchor, if cursor, move over
79− ranges.push(Range::new(
80− if range.is_empty() {
81− head
82− } else {
83− range.anchor + offs
84− },
85− head,
86− ));
83+ let next = doc.get_char(start_head);
84+ let end_head = start_head + offs + open.len_utf8();
85+
86+ let end_anchor = if start_range.is_empty() {
87+ end_head
88+ } else {
89+ start_range.anchor + offs
90+ };
91+
92+ end_ranges.push(Range::new(end_anchor, end_head));
8793
8894 match next {
8995 Some(ch) if !close_before.contains(ch) => {
90− offs += 1;
91− // TODO: else return (use default handler that inserts open)
92− (pos, pos, Some(Tendril::from_char(open)))
96+ offs += open.len_utf8();
97+ (start_head, start_head, Some(Tendril::from_char(open)))
9398 }
9499 // None | Some(ch) if close_before.contains(ch) => {}
95100 _ => {
96101 // insert open & close
97− let mut pair = Tendril::with_capacity(2);
98− pair.push_char(open);
99− pair.push_char(close);
100−
101− offs += 2;
102−
103− (pos, pos, Some(pair))
102+ let pair = Tendril::from_iter([open, close]);
103+ offs += open.len_utf8() + close.len_utf8();
104+ (start_head, start_head, Some(pair))
104105 }
105106 }
106107 });
107108
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);
111+ t
109112}
110113
111114fn 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());
113116
114117 let mut offs = 0;
115118
116− let transaction = Transaction::change_by_selection(doc, selection, |range| {
117− let pos = range.head;
118− let next = next_char(doc, pos);
119+ let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
120+ let start_head = start_range.head;
121+ let next = doc.get_char(start_head);
122+ let end_head = start_head + offs + close.len_utf8();
119123
120− let head = pos + offs + close.len_utf8();
121− // if selection, retain anchor, if cursor, move over
122− ranges.push(Range::new(
123− if range.is_empty() {
124− head
125− } else {
126− range.anchor + offs
127− },
128− head,
129− ));
124+ let end_anchor = if start_range.is_empty() {
125+ end_head
126+ } else {
127+ start_range.anchor + offs
128+ };
129+
130+ end_ranges.push(Range::new(end_anchor, end_head));
130131
131132 if next == Some(close) {
132− // return transaction that moves past close
133− (pos, pos, None) // no-op
133+ // return transaction that moves past close
134+ (start_head, start_head, None) // no-op
134135 } else {
135136 offs += close.len_utf8();
137+ (start_head, start_head, Some(Tendril::from_char(close)))
138+ }
139+ });
140+
141+ transaction.with_selection(Selection::new(end_ranges, selection.primary_index()))
142+}
136143
137− // TODO: else return (use default handler that inserts close)
138− (pos, pos, Some(Tendril::from_char(close)))
144+/// handle cases where open and close is the same, or in triples ("""docstring""")
145+fn handle_same(
146+ doc: &Rope,
147+ selection: &Selection,
148+ token: char,
149+ close_before: &str,
150+ open_before: &str,
151+) -> Transaction {
152+ let mut end_ranges = SmallVec::with_capacity(selection.len());
153+
154+ let mut offs = 0;
155+
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();
159+
160+ // if selection, retain anchor, if cursor, move over
161+ let end_anchor = if start_range.is_empty() {
162+ end_head
163+ } else {
164+ start_range.anchor + offs
165+ };
166+
167+ end_ranges.push(Range::new(end_anchor, end_head));
168+
169+ let next = doc.get_char(start_head);
170+ let prev = prev_char(doc, start_head);
171+
172+ if next == Some(token) {
173+ // return transaction that moves past close
174+ (start_head, start_head, None) // no-op
175+ } else {
176+ let mut pair = Tendril::with_capacity(2 * token.len_utf8() as u32);
177+ pair.push_char(token);
178+
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()))
183+ {
184+ pair.push_char(token);
185+ }
186+
187+ offs += pair.len();
188+ (start_head, start_head, Some(pair))
139189 }
140190 });
141191
142− transaction.with_selection(Selection::new(ranges, selection.primary_index()))
192+ transaction.with_selection(Selection::new(end_ranges, selection.primary_index()))
143193}
144194
145−// handle cases where open and close is the same, or in triples ("""docstring""")
146−fn handle_same(_doc: &Rope, _selection: &Selection, _token: char) -> Option<Transaction> {
147− // if not cursor but selection, wrap
148− // let next = next char
149−
150− // if next == bracket {
151− // // if start of syntax node, insert token twice (new pair because node is complete)
152− // // elseif colsedBracketAt
153− // // is_triple == allow triple && next 3 is equal
154− // // cursor jump over
155− // }
156− //} else if allow_triple && followed by triple {
157− //}
158− //} else if next != word char && prev != bracket && prev != word char {
159− // // condition checks for cases like I' where you don't want I'' (or I'm)
160− // insert pair ("")
161− //}
162− None
195+#[cfg(test)]
196+mod test {
197+ use super::*;
198+ use smallvec::smallvec;
199+
200+ fn differing_pairs() -> impl Iterator<Item = &'static (char, char)> {
201+ PAIRS.iter().filter(|(open, close)| open != close)
202+ }
203+
204+ fn matching_pairs() -> impl Iterator<Item = &'static (char, char)> {
205+ PAIRS.iter().filter(|(open, close)| open == close)
206+ }
207+
208+ fn test_hooks(
209+ in_doc: &Rope,
210+ in_sel: &Selection,
211+ ch: char,
212+ expected_doc: &Rope,
213+ expected_sel: &Selection,
214+ ) {
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());
220+ }
221+
222+ fn test_hooks_with_pairs<I, F, R>(
223+ in_doc: &Rope,
224+ in_sel: &Selection,
225+ pairs: I,
226+ get_expected_doc: F,
227+ actual_sel: &Selection,
228+ ) where
229+ I: IntoIterator<Item = &'static (char, char)>,
230+ F: Fn(char, char) -> R,
231+ R: Into<Rope>,
232+ Rope: From<R>,
233+ {
234+ pairs.into_iter().for_each(|(open, close)| {
235+ test_hooks(
236+ in_doc,
237+ in_sel,
238+ *open,
239+ &Rope::from(get_expected_doc(*open, *close)),
240+ actual_sel,
241+ )
242+ });
243+ }
244+
245+ // [] indicates range
246+
247+ /// [] -> insert ( -> ([])
248+ #[test]
249+ fn test_insert_blank() {
250+ test_hooks_with_pairs(
251+ &Rope::new(),
252+ &Selection::single(1, 0),
253+ PAIRS,
254+ |open, close| format!("{}{}", open, close),
255+ &Selection::single(1, 1),
256+ );
257+ }
258+
259+ /// [] ([])
260+ /// [] -> insert -> ([])
261+ /// [] ([])
262+ #[test]
263+ fn test_insert_blank_multi_cursor() {
264+ test_hooks_with_pairs(
265+ &Rope::from("\n\n\n"),
266+ &Selection::new(
267+ smallvec!(Range::new(1, 0), Range::new(2, 1), Range::new(3, 2),),
268+ 0,
269+ ),
270+ PAIRS,
271+ |open, close| {
272+ format!(
273+ "{open}{close}\n{open}{close}\n{open}{close}\n",
274+ open = open,
275+ close = close
276+ )
277+ },
278+ &Selection::new(
279+ smallvec!(Range::point(1), Range::point(4), Range::point(7),),
280+ 0,
281+ ),
282+ );
283+ }
284+
285+ // [TODO] broken until it works with selections
286+ /// fo[o] -> append ( -> fo[o(])
287+ #[ignore]
288+ #[test]
289+ fn test_append() {
290+ test_hooks_with_pairs(
291+ &Rope::from("foo"),
292+ &Selection::single(2, 4),
293+ PAIRS,
294+ |open, close| format!("foo{}{}", open, close),
295+ &Selection::single(2, 5),
296+ );
297+ }
298+
299+ /// ([]) -> insert ) -> ()[]
300+ #[test]
301+ fn test_insert_close_inside_pair() {
302+ for (open, close) in PAIRS {
303+ let doc = Rope::from(format!("{}{}", open, close));
304+
305+ test_hooks(
306+ &doc,
307+ &Selection::single(2, 1),
308+ *close,
309+ &doc,
310+ &Selection::point(2),
311+ );
312+ }
313+ }
314+
315+ /// ([]) ()[]
316+ /// ([]) -> insert ) -> ()[]
317+ /// ([]) ()[]
318+ #[test]
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),),
322+ 0,
323+ );
324+
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),),
328+ 0,
329+ );
330+
331+ for (open, close) in PAIRS {
332+ let doc = Rope::from(format!(
333+ "{open}{close}\n{open}{close}\n{open}{close}\n",
334+ open = open,
335+ close = close
336+ ));
337+
338+ test_hooks(&doc, &sel, *close, &doc, &expected_sel);
339+ }
340+ }
341+
342+ /// ([]) -> insert ( -> (([]))
343+ #[test]
344+ fn test_insert_open_inside_pair() {
345+ let sel = Selection::single(2, 1);
346+ let expected_sel = Selection::point(2);
347+
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}",
352+ open = open,
353+ close = close
354+ ));
355+
356+ test_hooks(&doc, &sel, *open, &expected_doc, &expected_sel);
357+ }
358+ }
359+
360+ /// ([]) -> insert " -> ("[]")
361+ #[test]
362+ fn test_insert_nested_open_inside_pair() {
363+ let sel = Selection::single(2, 1);
364+ let expected_sel = Selection::point(2);
365+
366+ for (outer_open, outer_close) in differing_pairs() {
367+ let doc = Rope::from(format!("{}{}", outer_open, outer_close,));
368+
369+ for (inner_open, inner_close) in matching_pairs() {
370+ let expected_doc = Rope::from(format!(
371+ "{}{}{}{}",
372+ outer_open, inner_open, inner_close, outer_close
373+ ));
374+
375+ test_hooks(&doc, &sel, *inner_open, &expected_doc, &expected_sel);
376+ }
377+ }
378+ }
379+
380+ /// []word -> insert ( -> ([]word
381+ #[test]
382+ fn test_insert_open_before_non_pair() {
383+ test_hooks_with_pairs(
384+ &Rope::from("word"),
385+ &Selection::single(1, 0),
386+ PAIRS,
387+ |open, _| format!("{}word", open),
388+ &Selection::point(1),
389+ )
390+ }
391+
392+ // [TODO] broken until it works with selections
393+ /// [wor]d -> insert ( -> ([wor]d
394+ #[test]
395+ #[ignore]
396+ fn test_insert_open_with_selection() {
397+ test_hooks_with_pairs(
398+ &Rope::from("word"),
399+ &Selection::single(0, 4),
400+ PAIRS,
401+ |open, _| format!("{}word", open),
402+ &Selection::single(1, 5),
403+ )
404+ }
405+
406+ /// we want pairs that are *not* the same char to be inserted after
407+ /// a non-pair char, for cases like functions, but for pairs that are
408+ /// the same char, we want to *not* insert a pair to handle cases like "I'm"
409+ ///
410+ /// word[] -> insert ( -> word([])
411+ /// word[] -> insert ' -> word'[]
412+ #[test]
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);
417+
418+ test_hooks_with_pairs(
419+ &doc,
420+ &sel,
421+ differing_pairs(),
422+ |open, close| format!("word{}{}", open, close),
423+ &expected_sel,
424+ );
425+
426+ test_hooks_with_pairs(
427+ &doc,
428+ &sel,
429+ matching_pairs(),
430+ |open, _| format!("word{}", open),
431+ &expected_sel,
432+ );
433+ }
163434}