on master help sign in
Add auto pairs for same-char pairs (#1219)
94535faSkyler Hawthorne
helix-core/src/auto_pairs.rs+346−75
@@ -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.
3
3
4use crate::{Range, Rope, Selection, Tendril, Transaction};
4use crate::{Range, Rope, Selection, Tendril, Transaction};
5+use log::debug;
5use smallvec::SmallVec;
6use smallvec::SmallVec;
6
7
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 ('`', '`'),
16];
17];
17
18
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
19
22
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
25//
28//
26// to simplify, maybe return Option<Transaction> and just reimplement the default
29// to simplify, maybe return Option<Transaction> and just reimplement the default
27
30
31+// [TODO]
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 {% %}
29
36
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);
40+
41+ let cursors = selection.clone().cursors(doc.slice(..));
42+
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));
36 } else {
47 } else {
37− return Some(handle_open(doc, selection, open, close, CLOSE_BEFORE));
48+ return Some(handle_open(doc, &cursors, open, close, CLOSE_BEFORE));
38 }
49 }
39 }
50 }
40
51
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));
44 }
55 }
45 }
56 }
46
57
47 None
58 None
48}
59}
49
60
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> {
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;
56 }
64 }
65+
57− Some(doc.char(pos))
66+ doc.get_char(pos - 1)
58}
67}
59−// TODO: selections should be extended if range, moved if point.
60
68
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());
70
77
71 let mut offs = 0;
78 let mut offs = 0;
72
79
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);
76
82
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() {
87+ end_head
88+ } else {
89+ start_range.anchor + offs
90+ };
91+
79− ranges.push(Range::new(
92+ end_ranges.push(Range::new(end_anchor, end_head));
80− if range.is_empty() {
81− head
82− } else {
83− range.anchor + offs
84− },
85− head,
86− ));
87
93
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)))
93 }
98 }
94 // None | Some(ch) if close_before.contains(ch) => {}
99 // None | Some(ch) if close_before.contains(ch) => {}
95 _ => {
100 _ => {
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))
100−
101− offs += 2;
102−
103− (pos, pos, Some(pair))
104 }
105 }
105 }
106 }
106 });
107 });
107
108
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
109}
112}
110
113
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());
113
116
114 let mut offs = 0;
117 let mut offs = 0;
115
118
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();
119
123
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
126+ } else {
127+ start_range.anchor + offs
128+ };
129+
122− ranges.push(Range::new(
130+ end_ranges.push(Range::new(end_anchor, end_head));
123− if range.is_empty() {
124− head
125− } else {
126− range.anchor + offs
127− },
128− head,
129− ));
130
131
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
134 } else {
135 } else {
135 offs += close.len_utf8();
136 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+}
136
143
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(
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))
139 }
189 }
140 });
190 });
141
191
142− transaction.with_selection(Selection::new(ranges, selection.primary_index()))
192+ transaction.with_selection(Selection::new(end_ranges, selection.primary_index()))
143}
193}
144
194
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;
149−
199+
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)
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+ )
155− // }
404+ }
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"
409+ ///
410+ /// word[] -> insert ( -> word([])
160− // insert pair ("")
411+ /// word[] -> insert ' -> word'[]
161− //}
412+ #[test]
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);
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+ }
163}
434}
helix-core/src/transaction.rs+1−1
@@ -409,7 +409,7 @@ impl ChangeSet {
409
409
410/// Transaction represents a single undoable unit of changes. Several changes can be grouped into
410/// Transaction represents a single undoable unit of changes. Several changes can be grouped into
411/// a single transaction.
411/// a single transaction.
412−#[derive(Debug, Default, Clone)]
412+#[derive(Debug, Default, Clone, PartialEq, Eq)]
413pub struct Transaction {
413pub struct Transaction {
414 changes: ChangeSet,
414 changes: ChangeSet,
415 selection: Option<Selection>,
415 selection: Option<Selection>,
helix-term/src/commands.rs+4−3
@@ -4199,8 +4199,9 @@ pub mod insert {
4199 // The default insert hook: simply insert the character
4199 // The default insert hook: simply insert the character
4200 #[allow(clippy::unnecessary_wraps)] // need to use Option<> because of the Hook signature
4200 #[allow(clippy::unnecessary_wraps)] // need to use Option<> because of the Hook signature
4201 fn insert(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> {
4201 fn insert(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> {
4202+ let cursors = selection.clone().cursors(doc.slice(..));
4202 let t = Tendril::from_char(ch);
4203 let t = Tendril::from_char(ch);
4203− let transaction = Transaction::insert(doc, selection, t);
4204+ let transaction = Transaction::insert(doc, &cursors, t);
4204 Some(transaction)
4205 Some(transaction)
4205 }
4206 }
4206
4207
@@ -4215,11 +4216,11 @@ pub mod insert {
4215 };
4216 };
4216
4217
4217 let text = doc.text();
4218 let text = doc.text();
4218− let selection = doc.selection(view.id).clone().cursors(text.slice(..));
4219+ let selection = doc.selection(view.id);
4219
4220
4220 // run through insert hooks, stopping on the first one that returns Some(t)
4221 // run through insert hooks, stopping on the first one that returns Some(t)
4221 for hook in hooks {
4222 for hook in hooks {
4222− if let Some(transaction) = hook(text, &selection, c) {
4223+ if let Some(transaction) = hook(text, selection, c) {
4223 doc.apply(&transaction, view.id);
4224 doc.apply(&transaction, view.id);
4224 break;
4225 break;
4225 }
4226 }