on master help sign in
Configurable auto pairs (#1624)

* impl auto pairs config Implements configuration for which pairs of tokens get auto completed. In order to help with this, the logic for when *not* to auto complete has been generalized from a specific hardcoded list of characters to simply testing if the next/prev char is alphanumeric. It is possible to configure a global list of pairs as well as at the language level. The language config will take precedence over the global config. * rename AutoPair -> Pair * clean up insert_char command * remove Rc * remove some explicit cloning with another impl * fix lint * review comments * global auto-pairs = false takes precedence over language settings * make clippy happy * print out editor config on startup * move auto pairs accessor into Document * rearrange auto pair doc comment * use pattern in Froms

commita494f47a5df543a3ab8d6530a5acbc2a5bd04d44 authorSkyler Hawthorne <skyler@dead10ck.com> dateFri 25 Feb 2022, 03:36 (5 years ago) parentb935fac
8 files changed+417−110
book/src/configuration.md+43−1 helix-core/src/auto_pairs.rs+238−87 helix-core/src/indent.rs+1 helix-core/src/syntax.rs+66 helix-term/src/commands.rs+22−16 helix-view/src/document.rs+25−2 helix-view/src/editor.rs+15−4 languages.toml+7
book/src/configuration.md+43−1
@@ -36,7 +36,6 @@ hidden = false
3636| `shell` | Shell to use when running external commands. | Unix: `["sh", "-c"]`<br/>Windows: `["cmd", "/C"]` |
3737| `line-number` | Line number display: `absolute` simply shows each line's number, while `relative` shows the distance from the current line. When unfocused or in insert mode, `relative` will still show absolute line numbers. | `absolute` |
3838| `smart-case` | Enable smart case regex searching (case insensitive unless pattern contains upper case characters) | `true` |
39−| `auto-pairs` | Enable automatic insertion of pairs to parenthese, brackets, etc. | `true` |
4039| `auto-completion` | Enable automatic pop up of auto-completion. | `true` |
4140| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. | `400` |
4241| `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
@@ -76,6 +75,49 @@ available, which is not defined by default.
7675|`git-exclude` | Enables reading `.git/info/exclude` files. | true
7776|`max-depth` | Set with an integer value for maximum depth to recurse. | Defaults to `None`.
7877
78+### `[editor.auto-pairs]` Section
79+
80+Enable automatic insertion of pairs to parentheses, brackets, etc. Can be
81+a simple boolean value, or a specific mapping of pairs of single characters.
82+
83+| Key | Description |
84+| --- | ----------- |
85+| `false` | Completely disable auto pairing, regardless of language-specific settings
86+| `true` | Use the default pairs: <code>(){}[]''""``</code>
87+| Mapping of pairs | e.g. `{ "(" = ")", "{" = "}", ... }`
88+
89+Example
90+
91+```toml
92+[editor.auto-pairs]
93+'(' = ')'
94+'{' = '}'
95+'[' = ']'
96+'"' = '"'
97+'`' = '`'
98+'<' = '>'
99+```
100+
101+Additionally, this setting can be used in a language config. Unless
102+the editor setting is `false`, this will override the editor config in
103+documents with this language.
104+
105+Example `languages.toml` that adds <> and removes ''
106+
107+```toml
108+[[language]]
109+name = "rust"
110+
111+[language.auto-pairs]
112+'(' = ')'
113+'{' = '}'
114+'[' = ']'
115+'"' = '"'
116+'`' = '`'
117+'<' = '>'
118+```
119+
120+
79121## LSP
80122
81123To display all language server messages in the status line add the following to your `config.toml`:
helix-core/src/auto_pairs.rs+238−87
@@ -4,12 +4,14 @@
44use crate::{
55 graphemes, movement::Direction, Range, Rope, RopeGraphemes, Selection, Tendril, Transaction,
66};
7+use std::collections::HashMap;
8+
79use log::debug;
810use smallvec::SmallVec;
911
1012// Heavily based on https://github.com/codemirror/closebrackets/
1113
12−pub const PAIRS: &[(char, char)] = &[
14+pub const DEFAULT_PAIRS: &[(char, char)] = &[
1315 ('(', ')'),
1416 ('{', '}'),
1517 ('[', ']'),
@@ -18,9 +20,95 @@ pub const PAIRS: &[(char, char)] = &[
1820 ('`', '`'),
1921];
2022
21−// [TODO] build this dynamically in language config. see #992
22−const OPEN_BEFORE: &str = "([{'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}";
23−const CLOSE_BEFORE: &str = ")]}'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}"; // includes space and newlines
23+/// The type that represents the collection of auto pairs,
24+/// keyed by the opener.
25+#[derive(Debug, Clone)]
26+pub struct AutoPairs(HashMap<char, Pair>);
27+
28+/// Represents the config for a particular pairing.
29+#[derive(Debug, Clone, Copy)]
30+pub struct Pair {
31+ pub open: char,
32+ pub close: char,
33+}
34+
35+impl Pair {
36+ /// true if open == close
37+ pub fn same(&self) -> bool {
38+ self.open == self.close
39+ }
40+
41+ /// true if all of the pair's conditions hold for the given document and range
42+ pub fn should_close(&self, doc: &Rope, range: &Range) -> bool {
43+ let mut should_close = Self::next_is_not_alpha(doc, range);
44+
45+ if self.same() {
46+ should_close &= Self::prev_is_not_alpha(doc, range);
47+ }
48+
49+ should_close
50+ }
51+
52+ pub fn next_is_not_alpha(doc: &Rope, range: &Range) -> bool {
53+ let cursor = range.cursor(doc.slice(..));
54+ let next_char = doc.get_char(cursor);
55+ next_char.map(|c| !c.is_alphanumeric()).unwrap_or(true)
56+ }
57+
58+ pub fn prev_is_not_alpha(doc: &Rope, range: &Range) -> bool {
59+ let cursor = range.cursor(doc.slice(..));
60+ let prev_char = prev_char(doc, cursor);
61+ prev_char.map(|c| !c.is_alphanumeric()).unwrap_or(true)
62+ }
63+}
64+
65+impl From<&(char, char)> for Pair {
66+ fn from(&(open, close): &(char, char)) -> Self {
67+ Self { open, close }
68+ }
69+}
70+
71+impl From<(&char, &char)> for Pair {
72+ fn from((open, close): (&char, &char)) -> Self {
73+ Self {
74+ open: *open,
75+ close: *close,
76+ }
77+ }
78+}
79+
80+impl AutoPairs {
81+ /// Make a new AutoPairs set with the given pairs and default conditions.
82+ pub fn new<'a, V: 'a, A>(pairs: V) -> Self
83+ where
84+ V: IntoIterator<Item = A>,
85+ A: Into<Pair>,
86+ {
87+ let mut auto_pairs = HashMap::new();
88+
89+ for pair in pairs.into_iter() {
90+ let auto_pair = pair.into();
91+
92+ auto_pairs.insert(auto_pair.open, auto_pair);
93+
94+ if auto_pair.open != auto_pair.close {
95+ auto_pairs.insert(auto_pair.close, auto_pair);
96+ }
97+ }
98+
99+ Self(auto_pairs)
100+ }
101+
102+ pub fn get(&self, ch: char) -> Option<&Pair> {
103+ self.0.get(&ch)
104+ }
105+}
106+
107+impl Default for AutoPairs {
108+ fn default() -> Self {
109+ AutoPairs::new(DEFAULT_PAIRS.iter())
110+ }
111+}
24112
25113// insert hook:
26114// Fn(doc, selection, char) => Option<Transaction>
@@ -36,21 +124,17 @@ const CLOSE_BEFORE: &str = ")]}'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{20
36124// middle of triple quotes, and more exotic pairs like Jinja's {% %}
37125
38126#[must_use]
39−pub fn hook(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> {
127+pub fn hook(doc: &Rope, selection: &Selection, ch: char, pairs: &AutoPairs) -> Option<Transaction> {
40128 debug!("autopairs hook selection: {:#?}", selection);
41129
42− for &(open, close) in PAIRS {
43− if open == ch {
44− if open == close {
45− return Some(handle_same(doc, selection, open, CLOSE_BEFORE, OPEN_BEFORE));
46− } else {
47− return Some(handle_open(doc, selection, open, close, CLOSE_BEFORE));
48− }
49− }
50−
51− if close == ch {
130+ if let Some(pair) = pairs.get(ch) {
131+ if pair.same() {
132+ return Some(handle_same(doc, selection, pair));
133+ } else if pair.open == ch {
134+ return Some(handle_open(doc, selection, pair));
135+ } else if pair.close == ch {
52136 // && char_at pos == close
53− return Some(handle_close(doc, selection, open, close));
137+ return Some(handle_close(doc, selection, pair));
54138 }
55139 }
56140
@@ -196,13 +280,7 @@ fn get_next_range(
196280 Range::new(end_anchor, end_head)
197281}
198282
199−fn handle_open(
200− doc: &Rope,
201− selection: &Selection,
202− open: char,
203− close: char,
204− close_before: &str,
205−) -> Transaction {
283+fn handle_open(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
206284 let mut end_ranges = SmallVec::with_capacity(selection.len());
207285 let mut offs = 0;
208286
@@ -212,22 +290,21 @@ fn handle_open(
212290 let len_inserted;
213291
214292 let change = match next_char {
215− Some(ch) if !close_before.contains(ch) => {
216− len_inserted = open.len_utf8();
293+ Some(_) if !pair.should_close(doc, start_range) => {
294+ len_inserted = pair.open.len_utf8();
217295 let mut tendril = Tendril::new();
218− tendril.push(open);
296+ tendril.push(pair.open);
219297 (cursor, cursor, Some(tendril))
220298 }
221− // None | Some(ch) if close_before.contains(ch) => {}
222299 _ => {
223300 // insert open & close
224− let pair = Tendril::from_iter([open, close]);
225− len_inserted = open.len_utf8() + close.len_utf8();
226− (cursor, cursor, Some(pair))
301+ let pair_str = Tendril::from_iter([pair.open, pair.close]);
302+ len_inserted = pair.open.len_utf8() + pair.close.len_utf8();
303+ (cursor, cursor, Some(pair_str))
227304 }
228305 };
229306
230− let next_range = get_next_range(doc, start_range, offs, open, len_inserted);
307+ let next_range = get_next_range(doc, start_range, offs, pair.open, len_inserted);
231308 end_ranges.push(next_range);
232309 offs += len_inserted;
233310
@@ -239,7 +316,7 @@ fn handle_open(
239316 t
240317}
241318
242−fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) -> Transaction {
319+fn handle_close(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
243320 let mut end_ranges = SmallVec::with_capacity(selection.len());
244321
245322 let mut offs = 0;
@@ -249,17 +326,17 @@ fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) ->
249326 let next_char = doc.get_char(cursor);
250327 let mut len_inserted = 0;
251328
252− let change = if next_char == Some(close) {
329+ let change = if next_char == Some(pair.close) {
253330 // return transaction that moves past close
254331 (cursor, cursor, None) // no-op
255332 } else {
256− len_inserted += close.len_utf8();
333+ len_inserted += pair.close.len_utf8();
257334 let mut tendril = Tendril::new();
258− tendril.push(close);
335+ tendril.push(pair.close);
259336 (cursor, cursor, Some(tendril))
260337 };
261338
262− let next_range = get_next_range(doc, start_range, offs, close, len_inserted);
339+ let next_range = get_next_range(doc, start_range, offs, pair.close, len_inserted);
263340 end_ranges.push(next_range);
264341 offs += len_inserted;
265342
@@ -272,13 +349,7 @@ fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) ->
272349}
273350
274351/// handle cases where open and close is the same, or in triples ("""docstring""")
275−fn handle_same(
276− doc: &Rope,
277− selection: &Selection,
278− token: char,
279− close_before: &str,
280− open_before: &str,
281−) -> Transaction {
352+fn handle_same(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
282353 let mut end_ranges = SmallVec::with_capacity(selection.len());
283354
284355 let mut offs = 0;
@@ -286,30 +357,26 @@ fn handle_same(
286357 let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
287358 let cursor = start_range.cursor(doc.slice(..));
288359 let mut len_inserted = 0;
289−
290360 let next_char = doc.get_char(cursor);
291− let prev_char = prev_char(doc, cursor);
292361
293− let change = if next_char == Some(token) {
362+ let change = if next_char == Some(pair.open) {
294363 // return transaction that moves past close
295364 (cursor, cursor, None) // no-op
296365 } else {
297− let mut pair = Tendril::new();
298− pair.push(token);
366+ let mut pair_str = Tendril::new();
367+ pair_str.push(pair.open);
299368
300369 // for equal pairs, don't insert both open and close if either
301370 // side has a non-pair char
302− if (next_char.is_none() || close_before.contains(next_char.unwrap()))
303− && (prev_char.is_none() || open_before.contains(prev_char.unwrap()))
304− {
305− pair.push(token);
371+ if pair.should_close(doc, start_range) {
372+ pair_str.push(pair.close);
306373 }
307374
308− len_inserted += pair.len();
309− (cursor, cursor, Some(pair))
375+ len_inserted += pair_str.len();
376+ (cursor, cursor, Some(pair_str))
310377 };
311378
312− let next_range = get_next_range(doc, start_range, offs, token, len_inserted);
379+ let next_range = get_next_range(doc, start_range, offs, pair.open, len_inserted);
313380 end_ranges.push(next_range);
314381 offs += len_inserted;
315382
@@ -329,21 +396,23 @@ mod test {
329396 const LINE_END: &str = crate::DEFAULT_LINE_ENDING.as_str();
330397
331398 fn differing_pairs() -> impl Iterator<Item = &'static (char, char)> {
332− PAIRS.iter().filter(|(open, close)| open != close)
399+ DEFAULT_PAIRS.iter().filter(|(open, close)| open != close)
333400 }
334401
335402 fn matching_pairs() -> impl Iterator<Item = &'static (char, char)> {
336− PAIRS.iter().filter(|(open, close)| open == close)
403+ DEFAULT_PAIRS.iter().filter(|(open, close)| open == close)
337404 }
338405
339406 fn test_hooks(
340407 in_doc: &Rope,
341408 in_sel: &Selection,
342409 ch: char,
410+ pairs: &[(char, char)],
343411 expected_doc: &Rope,
344412 expected_sel: &Selection,
345413 ) {
346− let trans = hook(in_doc, in_sel, ch).unwrap();
414+ let pairs = AutoPairs::new(pairs.iter());
415+ let trans = hook(in_doc, in_sel, ch, &pairs).unwrap();
347416 let mut actual_doc = in_doc.clone();
348417 assert!(trans.apply(&mut actual_doc));
349418 assert_eq!(expected_doc, &actual_doc);
@@ -353,7 +422,8 @@ mod test {
353422 fn test_hooks_with_pairs<I, F, R>(
354423 in_doc: &Rope,
355424 in_sel: &Selection,
356− pairs: I,
425+ test_pairs: I,
426+ pairs: &[(char, char)],
357427 get_expected_doc: F,
358428 actual_sel: &Selection,
359429 ) where
@@ -362,11 +432,12 @@ mod test {
362432 R: Into<Rope>,
363433 Rope: From<R>,
364434 {
365− pairs.into_iter().for_each(|(open, close)| {
435+ test_pairs.into_iter().for_each(|(open, close)| {
366436 test_hooks(
367437 in_doc,
368438 in_sel,
369439 *open,
440+ pairs,
370441 &Rope::from(get_expected_doc(*open, *close)),
371442 actual_sel,
372443 )
@@ -381,7 +452,8 @@ mod test {
381452 test_hooks_with_pairs(
382453 &Rope::from(LINE_END),
383454 &Selection::single(1, 0),
384− PAIRS,
455+ DEFAULT_PAIRS,
456+ DEFAULT_PAIRS,
385457 |open, close| format!("{}{}{}", open, close, LINE_END),
386458 &Selection::single(2, 1),
387459 );
@@ -391,7 +463,8 @@ mod test {
391463 test_hooks_with_pairs(
392464 &empty_doc,
393465 &Selection::single(empty_doc.len_chars(), LINE_END.len()),
394− PAIRS,
466+ DEFAULT_PAIRS,
467+ DEFAULT_PAIRS,
395468 |open, close| {
396469 format!(
397470 "{line_end}{open}{close}{line_end}",
@@ -406,13 +479,16 @@ mod test {
406479
407480 #[test]
408481 fn test_insert_before_multi_code_point_graphemes() {
409− test_hooks_with_pairs(
410− &Rope::from(format!("hello 👨‍👩‍👧‍👦 goodbye{}", LINE_END)),
411− &Selection::single(13, 6),
412− PAIRS,
413− |open, _| format!("hello {}👨‍👩‍👧‍👦 goodbye{}", open, LINE_END),
414− &Selection::single(14, 7),
415− );
482+ for (_, close) in differing_pairs() {
483+ test_hooks(
484+ &Rope::from(format!("hello 👨‍👩‍👧‍👦 goodbye{}", LINE_END)),
485+ &Selection::single(13, 6),
486+ *close,
487+ DEFAULT_PAIRS,
488+ &Rope::from(format!("hello {}👨‍👩‍👧‍👦 goodbye{}", close, LINE_END)),
489+ &Selection::single(14, 7),
490+ );
491+ }
416492 }
417493
418494 #[test]
@@ -420,7 +496,8 @@ mod test {
420496 test_hooks_with_pairs(
421497 &Rope::from(LINE_END),
422498 &Selection::single(LINE_END.len(), LINE_END.len()),
423− PAIRS,
499+ DEFAULT_PAIRS,
500+ DEFAULT_PAIRS,
424501 |open, close| format!("{}{}{}", LINE_END, open, close),
425502 &Selection::single(LINE_END.len() + 1, LINE_END.len() + 1),
426503 );
@@ -428,7 +505,8 @@ mod test {
428505 test_hooks_with_pairs(
429506 &Rope::from(format!("foo{}", LINE_END)),
430507 &Selection::single(3 + LINE_END.len(), 3 + LINE_END.len()),
431− PAIRS,
508+ DEFAULT_PAIRS,
509+ DEFAULT_PAIRS,
432510 |open, close| format!("foo{}{}{}", LINE_END, open, close),
433511 &Selection::single(LINE_END.len() + 4, LINE_END.len() + 4),
434512 );
@@ -442,7 +520,8 @@ mod test {
442520 &Rope::from(format!("{line_end}{line_end}", line_end = LINE_END)),
443521 // before inserting the pair, the cursor covers all of both empty lines
444522 &Selection::single(0, LINE_END.len() * 2),
445− PAIRS,
523+ DEFAULT_PAIRS,
524+ DEFAULT_PAIRS,
446525 |open, close| {
447526 format!(
448527 "{line_end}{open}{close}{line_end}",
@@ -467,7 +546,8 @@ mod test {
467546 smallvec!(Range::new(1, 0), Range::new(2, 1), Range::new(3, 2),),
468547 0,
469548 ),
470− PAIRS,
549+ DEFAULT_PAIRS,
550+ DEFAULT_PAIRS,
471551 |open, close| {
472552 format!(
473553 "{open}{close}\n{open}{close}\n{open}{close}\n",
@@ -489,6 +569,7 @@ mod test {
489569 &Rope::from("foo\n"),
490570 &Selection::single(2, 4),
491571 differing_pairs(),
572+ DEFAULT_PAIRS,
492573 |open, close| format!("foo{}{}\n", open, close),
493574 &Selection::single(2, 5),
494575 );
@@ -501,6 +582,7 @@ mod test {
501582 &Rope::from(format!("foo{}", LINE_END)),
502583 &Selection::single(3, 3 + LINE_END.len()),
503584 differing_pairs(),
585+ DEFAULT_PAIRS,
504586 |open, close| format!("foo{}{}{}", open, close, LINE_END),
505587 &Selection::single(4, 5),
506588 );
@@ -518,6 +600,7 @@ mod test {
518600 0,
519601 ),
520602 differing_pairs(),
603+ DEFAULT_PAIRS,
521604 |open, close| {
522605 format!(
523606 "foo{open}{close}\nfoo{open}{close}\nfoo{open}{close}\n",
@@ -535,13 +618,14 @@ mod test {
535618 /// ([)] -> insert ) -> ()[]
536619 #[test]
537620 fn test_insert_close_inside_pair() {
538− for (open, close) in PAIRS {
621+ for (open, close) in DEFAULT_PAIRS {
539622 let doc = Rope::from(format!("{}{}{}", open, close, LINE_END));
540623
541624 test_hooks(
542625 &doc,
543626 &Selection::single(2, 1),
544627 *close,
628+ DEFAULT_PAIRS,
545629 &doc,
546630 &Selection::single(2 + LINE_END.len(), 2),
547631 );
@@ -551,13 +635,14 @@ mod test {
551635 /// [(]) -> append ) -> [()]
552636 #[test]
553637 fn test_append_close_inside_pair() {
554− for (open, close) in PAIRS {
638+ for (open, close) in DEFAULT_PAIRS {
555639 let doc = Rope::from(format!("{}{}{}", open, close, LINE_END));
556640
557641 test_hooks(
558642 &doc,
559643 &Selection::single(0, 2),
560644 *close,
645+ DEFAULT_PAIRS,
561646 &doc,
562647 &Selection::single(0, 2 + LINE_END.len()),
563648 );
@@ -579,14 +664,14 @@ mod test {
579664 0,
580665 );
581666
582− for (open, close) in PAIRS {
667+ for (open, close) in DEFAULT_PAIRS {
583668 let doc = Rope::from(format!(
584669 "{open}{close}\n{open}{close}\n{open}{close}\n",
585670 open = open,
586671 close = close
587672 ));
588673
589− test_hooks(&doc, &sel, *close, &doc, &expected_sel);
674+ test_hooks(&doc, &sel, *close, DEFAULT_PAIRS, &doc, &expected_sel);
590675 }
591676 }
592677
@@ -605,14 +690,14 @@ mod test {
605690 0,
606691 );
607692
608− for (open, close) in PAIRS {
693+ for (open, close) in DEFAULT_PAIRS {
609694 let doc = Rope::from(format!(
610695 "{open}{close}\n{open}{close}\n{open}{close}\n",
611696 open = open,
612697 close = close
613698 ));
614699
615− test_hooks(&doc, &sel, *close, &doc, &expected_sel);
700+ test_hooks(&doc, &sel, *close, DEFAULT_PAIRS, &doc, &expected_sel);
616701 }
617702 }
618703
@@ -630,7 +715,14 @@ mod test {
630715 close = close
631716 ));
632717
633− test_hooks(&doc, &sel, *open, &expected_doc, &expected_sel);
718+ test_hooks(
719+ &doc,
720+ &sel,
721+ *open,
722+ DEFAULT_PAIRS,
723+ &expected_doc,
724+ &expected_sel,
725+ );
634726 }
635727 }
636728
@@ -648,7 +740,14 @@ mod test {
648740 close = close
649741 ));
650742
651− test_hooks(&doc, &sel, *open, &expected_doc, &expected_sel);
743+ test_hooks(
744+ &doc,
745+ &sel,
746+ *open,
747+ DEFAULT_PAIRS,
748+ &expected_doc,
749+ &expected_sel,
750+ );
652751 }
653752 }
654753
@@ -667,7 +766,14 @@ mod test {
667766 outer_open, inner_open, inner_close, outer_close
668767 ));
669768
670− test_hooks(&doc, &sel, *inner_open, &expected_doc, &expected_sel);
769+ test_hooks(
770+ &doc,
771+ &sel,
772+ *inner_open,
773+ DEFAULT_PAIRS,
774+ &expected_doc,
775+ &expected_sel,
776+ );
671777 }
672778 }
673779 }
@@ -687,7 +793,14 @@ mod test {
687793 outer_open, inner_open, inner_close, outer_close
688794 ));
689795
690− test_hooks(&doc, &sel, *inner_open, &expected_doc, &expected_sel);
796+ test_hooks(
797+ &doc,
798+ &sel,
799+ *inner_open,
800+ DEFAULT_PAIRS,
801+ &expected_doc,
802+ &expected_sel,
803+ );
691804 }
692805 }
693806 }
@@ -698,7 +811,8 @@ mod test {
698811 test_hooks_with_pairs(
699812 &Rope::from("word"),
700813 &Selection::single(1, 0),
701− PAIRS,
814+ DEFAULT_PAIRS,
815+ DEFAULT_PAIRS,
702816 |open, _| format!("{}word", open),
703817 &Selection::single(2, 1),
704818 )
@@ -710,7 +824,8 @@ mod test {
710824 test_hooks_with_pairs(
711825 &Rope::from("word"),
712826 &Selection::single(3, 0),
713− PAIRS,
827+ DEFAULT_PAIRS,
828+ DEFAULT_PAIRS,
714829 |open, _| format!("{}word", open),
715830 &Selection::single(4, 1),
716831 )
@@ -722,10 +837,17 @@ mod test {
722837 let sel = Selection::single(0, 4);
723838 let expected_sel = Selection::single(0, 5);
724839
725− for (_, close) in PAIRS {
840+ for (_, close) in DEFAULT_PAIRS {
726841 let doc = Rope::from("word");
727842 let expected_doc = Rope::from(format!("wor{}d", close));
728− test_hooks(&doc, &sel, *close, &expected_doc, &expected_sel);
843+ test_hooks(
844+ &doc,
845+ &sel,
846+ *close,
847+ DEFAULT_PAIRS,
848+ &expected_doc,
849+ &expected_sel,
850+ );
729851 }
730852 }
731853
@@ -736,6 +858,7 @@ mod test {
736858 &Rope::from("foo word"),
737859 &Selection::single(7, 3),
738860 differing_pairs(),
861+ DEFAULT_PAIRS,
739862 |open, close| format!("foo{}{} word", open, close),
740863 &Selection::single(9, 4),
741864 )
@@ -749,6 +872,7 @@ mod test {
749872 &Rope::from(format!("foo{}{} word{}", open, close, LINE_END)),
750873 &Selection::single(9, 4),
751874 *close,
875+ DEFAULT_PAIRS,
752876 &Rope::from(format!("foo{}{} word{}", open, close, LINE_END)),
753877 &Selection::single(9, 5),
754878 )
@@ -771,6 +895,7 @@ mod test {
771895 &doc,
772896 &sel,
773897 differing_pairs(),
898+ DEFAULT_PAIRS,
774899 |open, close| format!("word{}{}{}", open, close, LINE_END),
775900 &expected_sel,
776901 );
@@ -779,8 +904,34 @@ mod test {
779904 &doc,
780905 &sel,
781906 matching_pairs(),
907+ DEFAULT_PAIRS,
782908 |open, _| format!("word{}{}", open, LINE_END),
783909 &expected_sel,
784910 );
785911 }
912+
913+ #[test]
914+ fn test_configured_pairs() {
915+ let test_pairs = &[('`', ':'), ('+', '-')];
916+
917+ test_hooks_with_pairs(
918+ &Rope::from(LINE_END),
919+ &Selection::single(1, 0),
920+ test_pairs,
921+ test_pairs,
922+ |open, close| format!("{}{}{}", open, close, LINE_END),
923+ &Selection::single(2, 1),
924+ );
925+
926+ let doc = Rope::from(format!("foo`: word{}", LINE_END));
927+
928+ test_hooks(
929+ &doc,
930+ &Selection::single(9, 4),
931+ ':',
932+ test_pairs,
933+ &doc,
934+ &Selection::single(9, 5),
935+ )
936+ }
786937}
helix-core/src/indent.rs+1
@@ -442,6 +442,7 @@ where
442442 indent_query: OnceCell::new(),
443443 textobject_query: OnceCell::new(),
444444 debugger: None,
445+ auto_pairs: None,
445446 }],
446447 });
447448
helix-core/src/syntax.rs+66
@@ -1,4 +1,5 @@
11use crate::{
2+ auto_pairs::AutoPairs,
23 chars::char_is_line_ending,
34 diagnostic::Severity,
45 regex::Regex,
@@ -17,6 +18,7 @@ use std::{
1718 collections::{HashMap, HashSet, VecDeque},
1819 fmt,
1920 path::Path,
21+ str::FromStr,
2022 sync::Arc,
2123};
2224
@@ -41,6 +43,13 @@ where
4143 .transpose()
4244}
4345
46+pub fn deserialize_auto_pairs<'de, D>(deserializer: D) -> Result<Option<AutoPairs>, D::Error>
47+where
48+ D: serde::Deserializer<'de>,
49+{
50+ Ok(Option::<AutoPairConfig>::deserialize(deserializer)?.and_then(AutoPairConfig::into))
51+}
52+
4453#[derive(Debug, Serialize, Deserialize)]
4554#[serde(deny_unknown_fields)]
4655pub struct Configuration {
@@ -89,6 +98,13 @@ pub struct LanguageConfiguration {
8998 pub(crate) textobject_query: OnceCell<Option<TextObjectQuery>>,
9099 #[serde(skip_serializing_if = "Option::is_none")]
91100 pub debugger: Option<DebugAdapterConfig>,
101+
102+ /// Automatic insertion of pairs to parentheses, brackets,
103+ /// etc. Defaults to true. Optionally, this can be a list of 2-tuples
104+ /// to specify a list of characters to pair. This overrides the
105+ /// global setting.
106+ #[serde(default, skip_serializing, deserialize_with = "deserialize_auto_pairs")]
107+ pub auto_pairs: Option<AutoPairs>,
92108}
93109
94110#[derive(Debug, Serialize, Deserialize)]
@@ -162,6 +178,56 @@ pub struct IndentationConfiguration {
162178 pub unit: String,
163179}
164180
181+/// Configuration for auto pairs
182+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
183+#[serde(rename_all = "kebab-case", deny_unknown_fields, untagged)]
184+pub enum AutoPairConfig {
185+ /// Enables or disables auto pairing. False means disabled. True means to use the default pairs.
186+ Enable(bool),
187+
188+ /// The mappings of pairs.
189+ Pairs(HashMap<char, char>),
190+}
191+
192+impl Default for AutoPairConfig {
193+ fn default() -> Self {
194+ AutoPairConfig::Enable(true)
195+ }
196+}
197+
198+impl From<&AutoPairConfig> for Option<AutoPairs> {
199+ fn from(auto_pair_config: &AutoPairConfig) -> Self {
200+ match auto_pair_config {
201+ AutoPairConfig::Enable(false) => None,
202+ AutoPairConfig::Enable(true) => Some(AutoPairs::default()),
203+ AutoPairConfig::Pairs(pairs) => Some(AutoPairs::new(pairs.iter())),
204+ }
205+ }
206+}
207+
208+impl From<AutoPairConfig> for Option<AutoPairs> {
209+ fn from(auto_pairs_config: AutoPairConfig) -> Self {
210+ (&auto_pairs_config).into()
211+ }
212+}
213+
214+impl FromStr for AutoPairConfig {
215+ type Err = std::str::ParseBoolError;
216+
217+ // only do bool parsing for runtime setting
218+ fn from_str(s: &str) -> Result<Self, Self::Err> {
219+ let enable: bool = s.parse()?;
220+
221+ let enable = if enable {
222+ AutoPairConfig::Enable(true)
223+ } else {
224+ AutoPairConfig::Enable(false)
225+ };
226+
227+ Ok(enable)
228+ }
229+}
230+
165231#[derive(Debug, Serialize, Deserialize)]
166232#[serde(rename_all = "kebab-case")]
167233pub struct IndentQuery {
helix-term/src/commands.rs+22−16
@@ -4045,22 +4045,19 @@ pub mod insert {
40454045 use helix_core::auto_pairs;
40464046
40474047 pub fn insert_char(cx: &mut Context, c: char) {
4048− let (view, doc) = current!(cx.editor);
4049−
4050− let hooks: &[Hook] = match cx.editor.config.auto_pairs {
4051− true => &[auto_pairs::hook, insert],
4052− false => &[insert],
4053− };
4054−
4048+ let (view, doc) = current_ref!(cx.editor);
40554049 let text = doc.text();
40564050 let selection = doc.selection(view.id);
4051+ let auto_pairs = doc.auto_pairs(cx.editor);
40574052
4058− // run through insert hooks, stopping on the first one that returns Some(t)
4059− for hook in hooks {
4060− if let Some(transaction) = hook(text, selection, c) {
4061− doc.apply(&transaction, view.id);
4062− break;
4063− }
4053+ let transaction = auto_pairs
4054+ .as_ref()
4055+ .and_then(|ap| auto_pairs::hook(text, selection, c, ap))
4056+ .or_else(|| insert(text, selection, c));
4057+
4058+ let (view, doc) = current!(cx.editor);
4059+ if let Some(t) = transaction {
4060+ doc.apply(&t, view.id);
40644061 }
40654062
40664063 // TODO: need a post insert hook too for certain triggers (autocomplete, signature help, etc)
@@ -4087,7 +4084,7 @@ pub mod insert {
40874084 }
40884085
40894086 pub fn insert_newline(cx: &mut Context) {
4090− let (view, doc) = current!(cx.editor);
4087+ let (view, doc) = current_ref!(cx.editor);
40914088 let text = doc.text().slice(..);
40924089
40934090 let contents = doc.text();
@@ -4122,8 +4119,16 @@ pub mod insert {
41224119
41234120 let indent = doc.indent_unit().repeat(indent_level);
41244121 let mut text = String::new();
4125− // If we are between pairs (such as brackets), we want to insert an additional line which is indented one level more and place the cursor there
4126− let new_head_pos = if helix_core::auto_pairs::PAIRS.contains(&(prev, curr)) {
4122+ // If we are between pairs (such as brackets), we want to
4123+ // insert an additional line which is indented one level
4124+ // more and place the cursor there
4125+ let on_auto_pair = doc
4126+ .auto_pairs(cx.editor)
4127+ .and_then(|pairs| pairs.get(prev))
4128+ .and_then(|pair| if pair.close == curr { Some(pair) } else { None })
4129+ .is_some();
4130+
4131+ let new_head_pos = if on_auto_pair {
41274132 let inner_indent = doc.indent_unit().repeat(indent_level + 1);
41284133 text.reserve_exact(2 + indent.len() + inner_indent.len());
41294134 text.push_str(doc.line_ending.as_str());
@@ -4150,6 +4155,7 @@ pub mod insert {
41504155
41514156 transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index()));
41524157
4158+ let (view, doc) = current!(cx.editor);
41534159 doc.apply(&transaction, view.id);
41544160 }
41554161
helix-view/src/document.rs+25−2
@@ -1,4 +1,5 @@
11use anyhow::{anyhow, bail, Context, Error};
2+use helix_core::auto_pairs::AutoPairs;
23use serde::de::{self, Deserialize, Deserializer};
34use serde::Serialize;
45use std::cell::Cell;
@@ -20,7 +21,7 @@ use helix_core::{
2021};
2122use helix_lsp::util::LspFormatting;
2223
23−use crate::{DocumentId, ViewId};
24+use crate::{DocumentId, Editor, ViewId};
2425
2526/// 8kB of buffer space for encoding and decoding `Rope`s.
2627const BUF_SIZE: usize = 8192;
@@ -98,7 +99,7 @@ pub struct Document {
9899 pub line_ending: LineEnding,
99100
100101 syntax: Option<Syntax>,
101− // /// Corresponding language scope name. Usually `source.<lang>`.
102+ /// Corresponding language scope name. Usually `source.<lang>`.
102103 pub(crate) language: Option<Arc<LanguageConfiguration>>,
103104
104105 /// Pending changes since last history commit.
@@ -946,6 +947,28 @@ impl Document {
946947 self.diagnostics
947948 .sort_unstable_by_key(|diagnostic| diagnostic.range);
948949 }
950+
951+ /// Get the document's auto pairs. If the document has a recognized
952+ /// language config with auto pairs configured, returns that;
953+ /// otherwise, falls back to the global auto pairs config. If the global
954+ /// config is false, then ignore language settings.
955+ pub fn auto_pairs<'a>(&'a self, editor: &'a Editor) -> Option<&'a AutoPairs> {
956+ let global_config = (editor.auto_pairs).as_ref();
957+
958+ // NOTE: If the user specifies the global auto pairs config as false, then
959+ // we want to disable it globally regardless of language settings
960+ #[allow(clippy::question_mark)]
961+ {
962+ if global_config.is_none() {
963+ return None;
964+ }
965+ }
966+
967+ match &self.language {
968+ Some(lang) => lang.as_ref().auto_pairs.as_ref().or(global_config),
969+ None => global_config,
970+ }
971+ }
949972}
950973
951974impl Default for Document {
helix-view/src/editor.rs+15−4
@@ -13,6 +13,7 @@ use futures_util::future;
1313use futures_util::stream::select_all::SelectAll;
1414use tokio_stream::wrappers::UnboundedReceiverStream;
1515
16+use log::debug;
1617use std::{
1718 borrow::Cow,
1819 collections::{BTreeMap, HashMap},
@@ -29,7 +30,10 @@ use anyhow::{bail, Error};
2930
3031pub use helix_core::diagnostic::Severity;
3132pub use helix_core::register::Registers;
32−use helix_core::syntax;
33+use helix_core::{
34+ auto_pairs::AutoPairs,
35+ syntax::{self, AutoPairConfig},
36+};
3337use helix_core::{Position, Selection};
3438use helix_dap as dap;
3539
@@ -98,8 +102,10 @@ pub struct Config {
98102 pub line_number: LineNumber,
99103 /// Middle click paste support. Defaults to true.
100104 pub middle_click_paste: bool,
101− /// Automatic insertion of pairs to parentheses, brackets, etc. Defaults to true.
102− pub auto_pairs: bool,
105+ /// Automatic insertion of pairs to parentheses, brackets,
106+ /// etc. Optionally, this can be a list of 2-tuples to specify a
107+ /// global list of characters to pair. Defaults to true.
108+ pub auto_pairs: AutoPairConfig,
103109 /// Automatic auto-completion, automatically pop up without user trigger. Defaults to true.
104110 pub auto_completion: bool,
105111 /// Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. Defaults to 400ms.
@@ -217,7 +223,7 @@ impl Default for Config {
217223 },
218224 line_number: LineNumber::Absolute,
219225 middle_click_paste: true,
220− auto_pairs: true,
226+ auto_pairs: AutoPairConfig::default(),
221227 auto_completion: true,
222228 idle_timeout: Duration::from_millis(400),
223229 completion_trigger_len: 2,
@@ -289,6 +295,7 @@ pub struct Editor {
289295 pub autoinfo: Option<Info>,
290296
291297 pub config: Config,
298+ pub auto_pairs: Option<AutoPairs>,
292299
293300 pub idle_timer: Pin<Box<Sleep>>,
294301 pub last_motion: Option<Motion>,
@@ -312,6 +319,9 @@ impl Editor {
312319 config: Config,
313320 ) -> Self {
314321 let language_servers = helix_lsp::Registry::new();
322+ let auto_pairs = (&config.auto_pairs).into();
323+
324+ debug!("Editor config: {config:#?}");
315325
316326 // HAXX: offset the render area height by 1 to account for prompt/commandline
317327 area.height -= 1;
@@ -337,6 +347,7 @@ impl Editor {
337347 idle_timer: Box::pin(sleep(config.idle_timeout)),
338348 last_motion: None,
339349 config,
350+ auto_pairs,
340351 exit_code: 0,
341352 }
342353 }
languages.toml+7
@@ -9,6 +9,13 @@ comment-token = "//"
99language-server = { command = "rust-analyzer" }
1010indent = { tab-width = 4, unit = " " }
1111
12+[language.auto-pairs]
13+'(' = ')'
14+'{' = '}'
15+'[' = ']'
16+'"' = '"'
17+'`' = '`'
18+
1219[language.debugger]
1320name = "lldb-vscode"
1421transport = "stdio"