on master help sign in
Configurable auto pairs (#1624)
a494f47Skyler Hawthorne
book/src/configuration.md+43−1
@@ -36,7 +36,6 @@ hidden = false
36| `shell` | Shell to use when running external commands. | Unix: `["sh", "-c"]`<br/>Windows: `["cmd", "/C"]` |
36| `shell` | Shell to use when running external commands. | Unix: `["sh", "-c"]`<br/>Windows: `["cmd", "/C"]` |
37| `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` |
37| `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` |
38| `smart-case` | Enable smart case regex searching (case insensitive unless pattern contains upper case characters) | `true` |
38| `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` |
40| `auto-completion` | Enable automatic pop up of auto-completion. | `true` |
39| `auto-completion` | Enable automatic pop up of auto-completion. | `true` |
41| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. | `400` |
40| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. | `400` |
42| `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
41| `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.
76|`git-exclude` | Enables reading `.git/info/exclude` files. | true
75|`git-exclude` | Enables reading `.git/info/exclude` files. | true
77|`max-depth` | Set with an integer value for maximum depth to recurse. | Defaults to `None`.
76|`max-depth` | Set with an integer value for maximum depth to recurse. | Defaults to `None`.
78
77
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+
79## LSP
121## LSP
80
122
81To display all language server messages in the status line add the following to your `config.toml`:
123To 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 @@
4use crate::{
4use crate::{
5 graphemes, movement::Direction, Range, Rope, RopeGraphemes, Selection, Tendril, Transaction,
5 graphemes, movement::Direction, Range, Rope, RopeGraphemes, Selection, Tendril, Transaction,
6};
6};
7+use std::collections::HashMap;
8+
7use log::debug;
9use log::debug;
8use smallvec::SmallVec;
10use smallvec::SmallVec;
9
11
10// Heavily based on https://github.com/codemirror/closebrackets/
12// Heavily based on https://github.com/codemirror/closebrackets/
11
13
12−pub const PAIRS: &[(char, char)] = &[
14+pub const DEFAULT_PAIRS: &[(char, char)] = &[
13 ('(', ')'),
15 ('(', ')'),
14 ('{', '}'),
16 ('{', '}'),
15 ('[', ']'),
17 ('[', ']'),
@@ -18,9 +20,95 @@ pub const PAIRS: &[(char, char)] = &[
18 ('`', '`'),
20 ('`', '`'),
19];
21];
20
22
21−// [TODO] build this dynamically in language config. see #992
23+/// The type that represents the collection of auto pairs,
22−const OPEN_BEFORE: &str = "([{'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}";
24+/// keyed by the opener.
23−const CLOSE_BEFORE: &str = ")]}'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}"; // includes space and newlines
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+}
24
112
25// insert hook:
113// insert hook:
26// Fn(doc, selection, char) => Option<Transaction>
114// 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
36// middle of triple quotes, and more exotic pairs like Jinja's {% %}
124// middle of triple quotes, and more exotic pairs like Jinja's {% %}
37
125
38#[must_use]
126#[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> {
40 debug!("autopairs hook selection: {:#?}", selection);
128 debug!("autopairs hook selection: {:#?}", selection);
41
129
42− for &(open, close) in PAIRS {
130+ if let Some(pair) = pairs.get(ch) {
43− if open == ch {
131+ if pair.same() {
44− if open == close {
45− return Some(handle_same(doc, selection, open, CLOSE_BEFORE, OPEN_BEFORE));
132+ return Some(handle_same(doc, selection, pair));
46− } else {
133+ } else if pair.open == ch {
47− return Some(handle_open(doc, selection, open, close, CLOSE_BEFORE));
134+ return Some(handle_open(doc, selection, pair));
48− }
49− }
50−
51− if close == ch {
135+ } else if pair.close == ch {
52 // && char_at pos == close
136 // && char_at pos == close
53− return Some(handle_close(doc, selection, open, close));
137+ return Some(handle_close(doc, selection, pair));
54 }
138 }
55 }
139 }
56
140
@@ -196,13 +280,7 @@ fn get_next_range(
196 Range::new(end_anchor, end_head)
280 Range::new(end_anchor, end_head)
197}
281}
198
282
199−fn handle_open(
200− doc: &Rope,
201− selection: &Selection,
283+fn handle_open(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
202− open: char,
203− close: char,
204− close_before: &str,
205−) -> Transaction {
206 let mut end_ranges = SmallVec::with_capacity(selection.len());
284 let mut end_ranges = SmallVec::with_capacity(selection.len());
207 let mut offs = 0;
285 let mut offs = 0;
208
286
@@ -212,22 +290,21 @@ fn handle_open(
212 let len_inserted;
290 let len_inserted;
213
291
214 let change = match next_char {
292 let change = match next_char {
215− Some(ch) if !close_before.contains(ch) => {
293+ Some(_) if !pair.should_close(doc, start_range) => {
216− len_inserted = open.len_utf8();
294+ len_inserted = pair.open.len_utf8();
217 let mut tendril = Tendril::new();
295 let mut tendril = Tendril::new();
218− tendril.push(open);
296+ tendril.push(pair.open);
219 (cursor, cursor, Some(tendril))
297 (cursor, cursor, Some(tendril))
220 }
298 }
221− // None | Some(ch) if close_before.contains(ch) => {}
222 _ => {
299 _ => {
223 // insert open & close
300 // insert open & close
224− let pair = Tendril::from_iter([open, close]);
301+ let pair_str = Tendril::from_iter([pair.open, pair.close]);
225− len_inserted = open.len_utf8() + close.len_utf8();
302+ len_inserted = pair.open.len_utf8() + pair.close.len_utf8();
226− (cursor, cursor, Some(pair))
303+ (cursor, cursor, Some(pair_str))
227 }
304 }
228 };
305 };
229
306
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);
231 end_ranges.push(next_range);
308 end_ranges.push(next_range);
232 offs += len_inserted;
309 offs += len_inserted;
233
310
@@ -239,7 +316,7 @@ fn handle_open(
239 t
316 t
240}
317}
241
318
242−fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) -> Transaction {
319+fn handle_close(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
243 let mut end_ranges = SmallVec::with_capacity(selection.len());
320 let mut end_ranges = SmallVec::with_capacity(selection.len());
244
321
245 let mut offs = 0;
322 let mut offs = 0;
@@ -249,17 +326,17 @@ fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) ->
249 let next_char = doc.get_char(cursor);
326 let next_char = doc.get_char(cursor);
250 let mut len_inserted = 0;
327 let mut len_inserted = 0;
251
328
252− let change = if next_char == Some(close) {
329+ let change = if next_char == Some(pair.close) {
253 // return transaction that moves past close
330 // return transaction that moves past close
254 (cursor, cursor, None) // no-op
331 (cursor, cursor, None) // no-op
255 } else {
332 } else {
256− len_inserted += close.len_utf8();
333+ len_inserted += pair.close.len_utf8();
257 let mut tendril = Tendril::new();
334 let mut tendril = Tendril::new();
258− tendril.push(close);
335+ tendril.push(pair.close);
259 (cursor, cursor, Some(tendril))
336 (cursor, cursor, Some(tendril))
260 };
337 };
261
338
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);
263 end_ranges.push(next_range);
340 end_ranges.push(next_range);
264 offs += len_inserted;
341 offs += len_inserted;
265
342
@@ -272,13 +349,7 @@ fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) ->
272}
349}
273
350
274/// handle cases where open and close is the same, or in triples ("""docstring""")
351/// handle cases where open and close is the same, or in triples ("""docstring""")
275−fn handle_same(
276− doc: &Rope,
277− selection: &Selection,
352+fn handle_same(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
278− token: char,
279− close_before: &str,
280− open_before: &str,
281−) -> Transaction {
282 let mut end_ranges = SmallVec::with_capacity(selection.len());
353 let mut end_ranges = SmallVec::with_capacity(selection.len());
283
354
284 let mut offs = 0;
355 let mut offs = 0;
@@ -286,30 +357,26 @@ fn handle_same(
286 let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
357 let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
287 let cursor = start_range.cursor(doc.slice(..));
358 let cursor = start_range.cursor(doc.slice(..));
288 let mut len_inserted = 0;
359 let mut len_inserted = 0;
289−
290 let next_char = doc.get_char(cursor);
360 let next_char = doc.get_char(cursor);
291− let prev_char = prev_char(doc, cursor);
292
361
293− let change = if next_char == Some(token) {
362+ let change = if next_char == Some(pair.open) {
294 // return transaction that moves past close
363 // return transaction that moves past close
295 (cursor, cursor, None) // no-op
364 (cursor, cursor, None) // no-op
296 } else {
365 } else {
297− let mut pair = Tendril::new();
366+ let mut pair_str = Tendril::new();
298− pair.push(token);
367+ pair_str.push(pair.open);
299
368
300 // for equal pairs, don't insert both open and close if either
369 // for equal pairs, don't insert both open and close if either
301 // side has a non-pair char
370 // side has a non-pair char
302− if (next_char.is_none() || close_before.contains(next_char.unwrap()))
371+ if pair.should_close(doc, start_range) {
303− && (prev_char.is_none() || open_before.contains(prev_char.unwrap()))
372+ pair_str.push(pair.close);
304− {
305− pair.push(token);
306 }
373 }
307
374
308− len_inserted += pair.len();
375+ len_inserted += pair_str.len();
309− (cursor, cursor, Some(pair))
376+ (cursor, cursor, Some(pair_str))
310 };
377 };
311
378
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);
313 end_ranges.push(next_range);
380 end_ranges.push(next_range);
314 offs += len_inserted;
381 offs += len_inserted;
315
382
@@ -329,21 +396,23 @@ mod test {
329 const LINE_END: &str = crate::DEFAULT_LINE_ENDING.as_str();
396 const LINE_END: &str = crate::DEFAULT_LINE_ENDING.as_str();
330
397
331 fn differing_pairs() -> impl Iterator<Item = &'static (char, char)> {
398 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)
333 }
400 }
334
401
335 fn matching_pairs() -> impl Iterator<Item = &'static (char, char)> {
402 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)
337 }
404 }
338
405
339 fn test_hooks(
406 fn test_hooks(
340 in_doc: &Rope,
407 in_doc: &Rope,
341 in_sel: &Selection,
408 in_sel: &Selection,
342 ch: char,
409 ch: char,
410+ pairs: &[(char, char)],
343 expected_doc: &Rope,
411 expected_doc: &Rope,
344 expected_sel: &Selection,
412 expected_sel: &Selection,
345 ) {
413 ) {
414+ let pairs = AutoPairs::new(pairs.iter());
346− let trans = hook(in_doc, in_sel, ch).unwrap();
415+ let trans = hook(in_doc, in_sel, ch, &pairs).unwrap();
347 let mut actual_doc = in_doc.clone();
416 let mut actual_doc = in_doc.clone();
348 assert!(trans.apply(&mut actual_doc));
417 assert!(trans.apply(&mut actual_doc));
349 assert_eq!(expected_doc, &actual_doc);
418 assert_eq!(expected_doc, &actual_doc);
@@ -353,7 +422,8 @@ mod test {
353 fn test_hooks_with_pairs<I, F, R>(
422 fn test_hooks_with_pairs<I, F, R>(
354 in_doc: &Rope,
423 in_doc: &Rope,
355 in_sel: &Selection,
424 in_sel: &Selection,
356− pairs: I,
425+ test_pairs: I,
426+ pairs: &[(char, char)],
357 get_expected_doc: F,
427 get_expected_doc: F,
358 actual_sel: &Selection,
428 actual_sel: &Selection,
359 ) where
429 ) where
@@ -362,11 +432,12 @@ mod test {
362 R: Into<Rope>,
432 R: Into<Rope>,
363 Rope: From<R>,
433 Rope: From<R>,
364 {
434 {
365− pairs.into_iter().for_each(|(open, close)| {
435+ test_pairs.into_iter().for_each(|(open, close)| {
366 test_hooks(
436 test_hooks(
367 in_doc,
437 in_doc,
368 in_sel,
438 in_sel,
369 *open,
439 *open,
440+ pairs,
370 &Rope::from(get_expected_doc(*open, *close)),
441 &Rope::from(get_expected_doc(*open, *close)),
371 actual_sel,
442 actual_sel,
372 )
443 )
@@ -381,7 +452,8 @@ mod test {
381 test_hooks_with_pairs(
452 test_hooks_with_pairs(
382 &Rope::from(LINE_END),
453 &Rope::from(LINE_END),
383 &Selection::single(1, 0),
454 &Selection::single(1, 0),
384− PAIRS,
455+ DEFAULT_PAIRS,
456+ DEFAULT_PAIRS,
385 |open, close| format!("{}{}{}", open, close, LINE_END),
457 |open, close| format!("{}{}{}", open, close, LINE_END),
386 &Selection::single(2, 1),
458 &Selection::single(2, 1),
387 );
459 );
@@ -391,7 +463,8 @@ mod test {
391 test_hooks_with_pairs(
463 test_hooks_with_pairs(
392 &empty_doc,
464 &empty_doc,
393 &Selection::single(empty_doc.len_chars(), LINE_END.len()),
465 &Selection::single(empty_doc.len_chars(), LINE_END.len()),
394− PAIRS,
466+ DEFAULT_PAIRS,
467+ DEFAULT_PAIRS,
395 |open, close| {
468 |open, close| {
396 format!(
469 format!(
397 "{line_end}{open}{close}{line_end}",
470 "{line_end}{open}{close}{line_end}",
@@ -406,13 +479,16 @@ mod test {
406
479
407 #[test]
480 #[test]
408 fn test_insert_before_multi_code_point_graphemes() {
481 fn test_insert_before_multi_code_point_graphemes() {
409− test_hooks_with_pairs(
482+ for (_, close) in differing_pairs() {
483+ test_hooks(
410− &Rope::from(format!("hello 👨‍👩‍👧‍👦 goodbye{}", LINE_END)),
484+ &Rope::from(format!("hello 👨‍👩‍👧‍👦 goodbye{}", LINE_END)),
411− &Selection::single(13, 6),
485+ &Selection::single(13, 6),
412− PAIRS,
486+ *close,
487+ DEFAULT_PAIRS,
413− |open, _| format!("hello {}👨‍👩‍👧‍👦 goodbye{}", open, LINE_END),
488+ &Rope::from(format!("hello {}👨‍👩‍👧‍👦 goodbye{}", close, LINE_END)),
414− &Selection::single(14, 7),
489+ &Selection::single(14, 7),
415− );
490+ );
491+ }
416 }
492 }
417
493
418 #[test]
494 #[test]
@@ -420,7 +496,8 @@ mod test {
420 test_hooks_with_pairs(
496 test_hooks_with_pairs(
421 &Rope::from(LINE_END),
497 &Rope::from(LINE_END),
422 &Selection::single(LINE_END.len(), LINE_END.len()),
498 &Selection::single(LINE_END.len(), LINE_END.len()),
423− PAIRS,
499+ DEFAULT_PAIRS,
500+ DEFAULT_PAIRS,
424 |open, close| format!("{}{}{}", LINE_END, open, close),
501 |open, close| format!("{}{}{}", LINE_END, open, close),
425 &Selection::single(LINE_END.len() + 1, LINE_END.len() + 1),
502 &Selection::single(LINE_END.len() + 1, LINE_END.len() + 1),
426 );
503 );
@@ -428,7 +505,8 @@ mod test {
428 test_hooks_with_pairs(
505 test_hooks_with_pairs(
429 &Rope::from(format!("foo{}", LINE_END)),
506 &Rope::from(format!("foo{}", LINE_END)),
430 &Selection::single(3 + LINE_END.len(), 3 + LINE_END.len()),
507 &Selection::single(3 + LINE_END.len(), 3 + LINE_END.len()),
431− PAIRS,
508+ DEFAULT_PAIRS,
509+ DEFAULT_PAIRS,
432 |open, close| format!("foo{}{}{}", LINE_END, open, close),
510 |open, close| format!("foo{}{}{}", LINE_END, open, close),
433 &Selection::single(LINE_END.len() + 4, LINE_END.len() + 4),
511 &Selection::single(LINE_END.len() + 4, LINE_END.len() + 4),
434 );
512 );
@@ -442,7 +520,8 @@ mod test {
442 &Rope::from(format!("{line_end}{line_end}", line_end = LINE_END)),
520 &Rope::from(format!("{line_end}{line_end}", line_end = LINE_END)),
443 // before inserting the pair, the cursor covers all of both empty lines
521 // before inserting the pair, the cursor covers all of both empty lines
444 &Selection::single(0, LINE_END.len() * 2),
522 &Selection::single(0, LINE_END.len() * 2),
445− PAIRS,
523+ DEFAULT_PAIRS,
524+ DEFAULT_PAIRS,
446 |open, close| {
525 |open, close| {
447 format!(
526 format!(
448 "{line_end}{open}{close}{line_end}",
527 "{line_end}{open}{close}{line_end}",
@@ -467,7 +546,8 @@ mod test {
467 smallvec!(Range::new(1, 0), Range::new(2, 1), Range::new(3, 2),),
546 smallvec!(Range::new(1, 0), Range::new(2, 1), Range::new(3, 2),),
468 0,
547 0,
469 ),
548 ),
470− PAIRS,
549+ DEFAULT_PAIRS,
550+ DEFAULT_PAIRS,
471 |open, close| {
551 |open, close| {
472 format!(
552 format!(
473 "{open}{close}\n{open}{close}\n{open}{close}\n",
553 "{open}{close}\n{open}{close}\n{open}{close}\n",
@@ -489,6 +569,7 @@ mod test {
489 &Rope::from("foo\n"),
569 &Rope::from("foo\n"),
490 &Selection::single(2, 4),
570 &Selection::single(2, 4),
491 differing_pairs(),
571 differing_pairs(),
572+ DEFAULT_PAIRS,
492 |open, close| format!("foo{}{}\n", open, close),
573 |open, close| format!("foo{}{}\n", open, close),
493 &Selection::single(2, 5),
574 &Selection::single(2, 5),
494 );
575 );
@@ -501,6 +582,7 @@ mod test {
501 &Rope::from(format!("foo{}", LINE_END)),
582 &Rope::from(format!("foo{}", LINE_END)),
502 &Selection::single(3, 3 + LINE_END.len()),
583 &Selection::single(3, 3 + LINE_END.len()),
503 differing_pairs(),
584 differing_pairs(),
585+ DEFAULT_PAIRS,
504 |open, close| format!("foo{}{}{}", open, close, LINE_END),
586 |open, close| format!("foo{}{}{}", open, close, LINE_END),
505 &Selection::single(4, 5),
587 &Selection::single(4, 5),
506 );
588 );
@@ -518,6 +600,7 @@ mod test {
518 0,
600 0,
519 ),
601 ),
520 differing_pairs(),
602 differing_pairs(),
603+ DEFAULT_PAIRS,
521 |open, close| {
604 |open, close| {
522 format!(
605 format!(
523 "foo{open}{close}\nfoo{open}{close}\nfoo{open}{close}\n",
606 "foo{open}{close}\nfoo{open}{close}\nfoo{open}{close}\n",
@@ -535,13 +618,14 @@ mod test {
535 /// ([)] -> insert ) -> ()[]
618 /// ([)] -> insert ) -> ()[]
536 #[test]
619 #[test]
537 fn test_insert_close_inside_pair() {
620 fn test_insert_close_inside_pair() {
538− for (open, close) in PAIRS {
621+ for (open, close) in DEFAULT_PAIRS {
539 let doc = Rope::from(format!("{}{}{}", open, close, LINE_END));
622 let doc = Rope::from(format!("{}{}{}", open, close, LINE_END));
540
623
541 test_hooks(
624 test_hooks(
542 &doc,
625 &doc,
543 &Selection::single(2, 1),
626 &Selection::single(2, 1),
544 *close,
627 *close,
628+ DEFAULT_PAIRS,
545 &doc,
629 &doc,
546 &Selection::single(2 + LINE_END.len(), 2),
630 &Selection::single(2 + LINE_END.len(), 2),
547 );
631 );
@@ -551,13 +635,14 @@ mod test {
551 /// [(]) -> append ) -> [()]
635 /// [(]) -> append ) -> [()]
552 #[test]
636 #[test]
553 fn test_append_close_inside_pair() {
637 fn test_append_close_inside_pair() {
554− for (open, close) in PAIRS {
638+ for (open, close) in DEFAULT_PAIRS {
555 let doc = Rope::from(format!("{}{}{}", open, close, LINE_END));
639 let doc = Rope::from(format!("{}{}{}", open, close, LINE_END));
556
640
557 test_hooks(
641 test_hooks(
558 &doc,
642 &doc,
559 &Selection::single(0, 2),
643 &Selection::single(0, 2),
560 *close,
644 *close,
645+ DEFAULT_PAIRS,
561 &doc,
646 &doc,
562 &Selection::single(0, 2 + LINE_END.len()),
647 &Selection::single(0, 2 + LINE_END.len()),
563 );
648 );
@@ -579,14 +664,14 @@ mod test {
579 0,
664 0,
580 );
665 );
581
666
582− for (open, close) in PAIRS {
667+ for (open, close) in DEFAULT_PAIRS {
583 let doc = Rope::from(format!(
668 let doc = Rope::from(format!(
584 "{open}{close}\n{open}{close}\n{open}{close}\n",
669 "{open}{close}\n{open}{close}\n{open}{close}\n",
585 open = open,
670 open = open,
586 close = close
671 close = close
587 ));
672 ));
588
673
589− test_hooks(&doc, &sel, *close, &doc, &expected_sel);
674+ test_hooks(&doc, &sel, *close, DEFAULT_PAIRS, &doc, &expected_sel);
590 }
675 }
591 }
676 }
592
677
@@ -605,14 +690,14 @@ mod test {
605 0,
690 0,
606 );
691 );
607
692
608− for (open, close) in PAIRS {
693+ for (open, close) in DEFAULT_PAIRS {
609 let doc = Rope::from(format!(
694 let doc = Rope::from(format!(
610 "{open}{close}\n{open}{close}\n{open}{close}\n",
695 "{open}{close}\n{open}{close}\n{open}{close}\n",
611 open = open,
696 open = open,
612 close = close
697 close = close
613 ));
698 ));
614
699
615− test_hooks(&doc, &sel, *close, &doc, &expected_sel);
700+ test_hooks(&doc, &sel, *close, DEFAULT_PAIRS, &doc, &expected_sel);
616 }
701 }
617 }
702 }
618
703
@@ -630,7 +715,14 @@ mod test {
630 close = close
715 close = close
631 ));
716 ));
632
717
718+ test_hooks(
719+ &doc,
720+ &sel,
721+ *open,
722+ DEFAULT_PAIRS,
633− test_hooks(&doc, &sel, *open, &expected_doc, &expected_sel);
723+ &expected_doc,
724+ &expected_sel,
725+ );
634 }
726 }
635 }
727 }
636
728
@@ -648,7 +740,14 @@ mod test {
648 close = close
740 close = close
649 ));
741 ));
650
742
743+ test_hooks(
744+ &doc,
745+ &sel,
746+ *open,
747+ DEFAULT_PAIRS,
651− test_hooks(&doc, &sel, *open, &expected_doc, &expected_sel);
748+ &expected_doc,
749+ &expected_sel,
750+ );
652 }
751 }
653 }
752 }
654
753
@@ -667,7 +766,14 @@ mod test {
667 outer_open, inner_open, inner_close, outer_close
766 outer_open, inner_open, inner_close, outer_close
668 ));
767 ));
669
768
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+ );
671 }
777 }
672 }
778 }
673 }
779 }
@@ -687,7 +793,14 @@ mod test {
687 outer_open, inner_open, inner_close, outer_close
793 outer_open, inner_open, inner_close, outer_close
688 ));
794 ));
689
795
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+ );
691 }
804 }
692 }
805 }
693 }
806 }
@@ -698,7 +811,8 @@ mod test {
698 test_hooks_with_pairs(
811 test_hooks_with_pairs(
699 &Rope::from("word"),
812 &Rope::from("word"),
700 &Selection::single(1, 0),
813 &Selection::single(1, 0),
701− PAIRS,
814+ DEFAULT_PAIRS,
815+ DEFAULT_PAIRS,
702 |open, _| format!("{}word", open),
816 |open, _| format!("{}word", open),
703 &Selection::single(2, 1),
817 &Selection::single(2, 1),
704 )
818 )
@@ -710,7 +824,8 @@ mod test {
710 test_hooks_with_pairs(
824 test_hooks_with_pairs(
711 &Rope::from("word"),
825 &Rope::from("word"),
712 &Selection::single(3, 0),
826 &Selection::single(3, 0),
713− PAIRS,
827+ DEFAULT_PAIRS,
828+ DEFAULT_PAIRS,
714 |open, _| format!("{}word", open),
829 |open, _| format!("{}word", open),
715 &Selection::single(4, 1),
830 &Selection::single(4, 1),
716 )
831 )
@@ -722,10 +837,17 @@ mod test {
722 let sel = Selection::single(0, 4);
837 let sel = Selection::single(0, 4);
723 let expected_sel = Selection::single(0, 5);
838 let expected_sel = Selection::single(0, 5);
724
839
725− for (_, close) in PAIRS {
840+ for (_, close) in DEFAULT_PAIRS {
726 let doc = Rope::from("word");
841 let doc = Rope::from("word");
727 let expected_doc = Rope::from(format!("wor{}d", close));
842 let expected_doc = Rope::from(format!("wor{}d", close));
843+ test_hooks(
844+ &doc,
845+ &sel,
846+ *close,
847+ DEFAULT_PAIRS,
728− test_hooks(&doc, &sel, *close, &expected_doc, &expected_sel);
848+ &expected_doc,
849+ &expected_sel,
850+ );
729 }
851 }
730 }
852 }
731
853
@@ -736,6 +858,7 @@ mod test {
736 &Rope::from("foo word"),
858 &Rope::from("foo word"),
737 &Selection::single(7, 3),
859 &Selection::single(7, 3),
738 differing_pairs(),
860 differing_pairs(),
861+ DEFAULT_PAIRS,
739 |open, close| format!("foo{}{} word", open, close),
862 |open, close| format!("foo{}{} word", open, close),
740 &Selection::single(9, 4),
863 &Selection::single(9, 4),
741 )
864 )
@@ -749,6 +872,7 @@ mod test {
749 &Rope::from(format!("foo{}{} word{}", open, close, LINE_END)),
872 &Rope::from(format!("foo{}{} word{}", open, close, LINE_END)),
750 &Selection::single(9, 4),
873 &Selection::single(9, 4),
751 *close,
874 *close,
875+ DEFAULT_PAIRS,
752 &Rope::from(format!("foo{}{} word{}", open, close, LINE_END)),
876 &Rope::from(format!("foo{}{} word{}", open, close, LINE_END)),
753 &Selection::single(9, 5),
877 &Selection::single(9, 5),
754 )
878 )
@@ -771,6 +895,7 @@ mod test {
771 &doc,
895 &doc,
772 &sel,
896 &sel,
773 differing_pairs(),
897 differing_pairs(),
898+ DEFAULT_PAIRS,
774 |open, close| format!("word{}{}{}", open, close, LINE_END),
899 |open, close| format!("word{}{}{}", open, close, LINE_END),
775 &expected_sel,
900 &expected_sel,
776 );
901 );
@@ -779,8 +904,34 @@ mod test {
779 &doc,
904 &doc,
780 &sel,
905 &sel,
781 matching_pairs(),
906 matching_pairs(),
907+ DEFAULT_PAIRS,
782 |open, _| format!("word{}{}", open, LINE_END),
908 |open, _| format!("word{}{}", open, LINE_END),
783 &expected_sel,
909 &expected_sel,
784 );
910 );
785 }
911 }
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+ }
786}
937}
helix-core/src/indent.rs+1
@@ -442,6 +442,7 @@ where
442 indent_query: OnceCell::new(),
442 indent_query: OnceCell::new(),
443 textobject_query: OnceCell::new(),
443 textobject_query: OnceCell::new(),
444 debugger: None,
444 debugger: None,
445+ auto_pairs: None,
445 }],
446 }],
446 });
447 });
447
448
helix-core/src/syntax.rs+66
@@ -1,4 +1,5 @@
1use crate::{
1use crate::{
2+ auto_pairs::AutoPairs,
2 chars::char_is_line_ending,
3 chars::char_is_line_ending,
3 diagnostic::Severity,
4 diagnostic::Severity,
4 regex::Regex,
5 regex::Regex,
@@ -17,6 +18,7 @@ use std::{
17 collections::{HashMap, HashSet, VecDeque},
18 collections::{HashMap, HashSet, VecDeque},
18 fmt,
19 fmt,
19 path::Path,
20 path::Path,
21+ str::FromStr,
20 sync::Arc,
22 sync::Arc,
21};
23};
22
24
@@ -41,6 +43,13 @@ where
41 .transpose()
43 .transpose()
42}
44}
43
45
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+
44#[derive(Debug, Serialize, Deserialize)]
53#[derive(Debug, Serialize, Deserialize)]
45#[serde(deny_unknown_fields)]
54#[serde(deny_unknown_fields)]
46pub struct Configuration {
55pub struct Configuration {
@@ -89,6 +98,13 @@ pub struct LanguageConfiguration {
89 pub(crate) textobject_query: OnceCell<Option<TextObjectQuery>>,
98 pub(crate) textobject_query: OnceCell<Option<TextObjectQuery>>,
90 #[serde(skip_serializing_if = "Option::is_none")]
99 #[serde(skip_serializing_if = "Option::is_none")]
91 pub debugger: Option<DebugAdapterConfig>,
100 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>,
92}
108}
93
109
94#[derive(Debug, Serialize, Deserialize)]
110#[derive(Debug, Serialize, Deserialize)]
@@ -162,6 +178,56 @@ pub struct IndentationConfiguration {
162 pub unit: String,
178 pub unit: String,
163}
179}
164
180
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+
165#[derive(Debug, Serialize, Deserialize)]
231#[derive(Debug, Serialize, Deserialize)]
166#[serde(rename_all = "kebab-case")]
232#[serde(rename_all = "kebab-case")]
167pub struct IndentQuery {
233pub struct IndentQuery {
helix-term/src/commands.rs+22−16
@@ -4045,22 +4045,19 @@ pub mod insert {
4045 use helix_core::auto_pairs;
4045 use helix_core::auto_pairs;
4046
4046
4047 pub fn insert_char(cx: &mut Context, c: char) {
4047 pub fn insert_char(cx: &mut Context, c: char) {
4048− let (view, doc) = current!(cx.editor);
4048+ let (view, doc) = current_ref!(cx.editor);
4049−
4050− let hooks: &[Hook] = match cx.editor.config.auto_pairs {
4051− true => &[auto_pairs::hook, insert],
4052− false => &[insert],
4053− };
4054−
4055 let text = doc.text();
4049 let text = doc.text();
4056 let selection = doc.selection(view.id);
4050 let selection = doc.selection(view.id);
4051+ let auto_pairs = doc.auto_pairs(cx.editor);
4057
4052
4058− // run through insert hooks, stopping on the first one that returns Some(t)
4053+ let transaction = auto_pairs
4059− for hook in hooks {
4054+ .as_ref()
4060− if let Some(transaction) = hook(text, selection, c) {
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 {
4061− doc.apply(&transaction, view.id);
4060+ doc.apply(&t, view.id);
4062− break;
4063− }
4064 }
4061 }
4065
4062
4066 // TODO: need a post insert hook too for certain triggers (autocomplete, signature help, etc)
4063 // TODO: need a post insert hook too for certain triggers (autocomplete, signature help, etc)
@@ -4087,7 +4084,7 @@ pub mod insert {
4087 }
4084 }
4088
4085
4089 pub fn insert_newline(cx: &mut Context) {
4086 pub fn insert_newline(cx: &mut Context) {
4090− let (view, doc) = current!(cx.editor);
4087+ let (view, doc) = current_ref!(cx.editor);
4091 let text = doc.text().slice(..);
4088 let text = doc.text().slice(..);
4092
4089
4093 let contents = doc.text();
4090 let contents = doc.text();
@@ -4122,8 +4119,16 @@ pub mod insert {
4122
4119
4123 let indent = doc.indent_unit().repeat(indent_level);
4120 let indent = doc.indent_unit().repeat(indent_level);
4124 let mut text = String::new();
4121 let mut text = String::new();
4122+ // If we are between pairs (such as brackets), we want to
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
4123+ // insert an additional line which is indented one level
4126− let new_head_pos = if helix_core::auto_pairs::PAIRS.contains(&(prev, curr)) {
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 {
4127 let inner_indent = doc.indent_unit().repeat(indent_level + 1);
4132 let inner_indent = doc.indent_unit().repeat(indent_level + 1);
4128 text.reserve_exact(2 + indent.len() + inner_indent.len());
4133 text.reserve_exact(2 + indent.len() + inner_indent.len());
4129 text.push_str(doc.line_ending.as_str());
4134 text.push_str(doc.line_ending.as_str());
@@ -4150,6 +4155,7 @@ pub mod insert {
4150
4155
4151 transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index()));
4156 transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index()));
4152
4157
4158+ let (view, doc) = current!(cx.editor);
4153 doc.apply(&transaction, view.id);
4159 doc.apply(&transaction, view.id);
4154 }
4160 }
4155
4161
helix-view/src/document.rs+25−2
@@ -1,4 +1,5 @@
1use anyhow::{anyhow, bail, Context, Error};
1use anyhow::{anyhow, bail, Context, Error};
2+use helix_core::auto_pairs::AutoPairs;
2use serde::de::{self, Deserialize, Deserializer};
3use serde::de::{self, Deserialize, Deserializer};
3use serde::Serialize;
4use serde::Serialize;
4use std::cell::Cell;
5use std::cell::Cell;
@@ -20,7 +21,7 @@ use helix_core::{
20};
21};
21use helix_lsp::util::LspFormatting;
22use helix_lsp::util::LspFormatting;
22
23
23−use crate::{DocumentId, ViewId};
24+use crate::{DocumentId, Editor, ViewId};
24
25
25/// 8kB of buffer space for encoding and decoding `Rope`s.
26/// 8kB of buffer space for encoding and decoding `Rope`s.
26const BUF_SIZE: usize = 8192;
27const BUF_SIZE: usize = 8192;
@@ -98,7 +99,7 @@ pub struct Document {
98 pub line_ending: LineEnding,
99 pub line_ending: LineEnding,
99
100
100 syntax: Option<Syntax>,
101 syntax: Option<Syntax>,
101− // /// Corresponding language scope name. Usually `source.<lang>`.
102+ /// Corresponding language scope name. Usually `source.<lang>`.
102 pub(crate) language: Option<Arc<LanguageConfiguration>>,
103 pub(crate) language: Option<Arc<LanguageConfiguration>>,
103
104
104 /// Pending changes since last history commit.
105 /// Pending changes since last history commit.
@@ -946,6 +947,28 @@ impl Document {
946 self.diagnostics
947 self.diagnostics
947 .sort_unstable_by_key(|diagnostic| diagnostic.range);
948 .sort_unstable_by_key(|diagnostic| diagnostic.range);
948 }
949 }
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+ }
949}
972}
950
973
951impl Default for Document {
974impl Default for Document {
helix-view/src/editor.rs+15−4
@@ -13,6 +13,7 @@ use futures_util::future;
13use futures_util::stream::select_all::SelectAll;
13use futures_util::stream::select_all::SelectAll;
14use tokio_stream::wrappers::UnboundedReceiverStream;
14use tokio_stream::wrappers::UnboundedReceiverStream;
15
15
16+use log::debug;
16use std::{
17use std::{
17 borrow::Cow,
18 borrow::Cow,
18 collections::{BTreeMap, HashMap},
19 collections::{BTreeMap, HashMap},
@@ -29,7 +30,10 @@ use anyhow::{bail, Error};
29
30
30pub use helix_core::diagnostic::Severity;
31pub use helix_core::diagnostic::Severity;
31pub use helix_core::register::Registers;
32pub use helix_core::register::Registers;
32−use helix_core::syntax;
33+use helix_core::{
34+ auto_pairs::AutoPairs,
35+ syntax::{self, AutoPairConfig},
36+};
33use helix_core::{Position, Selection};
37use helix_core::{Position, Selection};
34use helix_dap as dap;
38use helix_dap as dap;
35
39
@@ -98,8 +102,10 @@ pub struct Config {
98 pub line_number: LineNumber,
102 pub line_number: LineNumber,
99 /// Middle click paste support. Defaults to true.
103 /// Middle click paste support. Defaults to true.
100 pub middle_click_paste: bool,
104 pub middle_click_paste: bool,
101− /// Automatic insertion of pairs to parentheses, brackets, etc. Defaults to true.
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.
102− pub auto_pairs: bool,
108+ pub auto_pairs: AutoPairConfig,
103 /// Automatic auto-completion, automatically pop up without user trigger. Defaults to true.
109 /// Automatic auto-completion, automatically pop up without user trigger. Defaults to true.
104 pub auto_completion: bool,
110 pub auto_completion: bool,
105 /// Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. Defaults to 400ms.
111 /// 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 {
217 },
223 },
218 line_number: LineNumber::Absolute,
224 line_number: LineNumber::Absolute,
219 middle_click_paste: true,
225 middle_click_paste: true,
220− auto_pairs: true,
226+ auto_pairs: AutoPairConfig::default(),
221 auto_completion: true,
227 auto_completion: true,
222 idle_timeout: Duration::from_millis(400),
228 idle_timeout: Duration::from_millis(400),
223 completion_trigger_len: 2,
229 completion_trigger_len: 2,
@@ -289,6 +295,7 @@ pub struct Editor {
289 pub autoinfo: Option<Info>,
295 pub autoinfo: Option<Info>,
290
296
291 pub config: Config,
297 pub config: Config,
298+ pub auto_pairs: Option<AutoPairs>,
292
299
293 pub idle_timer: Pin<Box<Sleep>>,
300 pub idle_timer: Pin<Box<Sleep>>,
294 pub last_motion: Option<Motion>,
301 pub last_motion: Option<Motion>,
@@ -312,6 +319,9 @@ impl Editor {
312 config: Config,
319 config: Config,
313 ) -> Self {
320 ) -> Self {
314 let language_servers = helix_lsp::Registry::new();
321 let language_servers = helix_lsp::Registry::new();
322+ let auto_pairs = (&config.auto_pairs).into();
323+
324+ debug!("Editor config: {config:#?}");
315
325
316 // HAXX: offset the render area height by 1 to account for prompt/commandline
326 // HAXX: offset the render area height by 1 to account for prompt/commandline
317 area.height -= 1;
327 area.height -= 1;
@@ -337,6 +347,7 @@ impl Editor {
337 idle_timer: Box::pin(sleep(config.idle_timeout)),
347 idle_timer: Box::pin(sleep(config.idle_timeout)),
338 last_motion: None,
348 last_motion: None,
339 config,
349 config,
350+ auto_pairs,
340 exit_code: 0,
351 exit_code: 0,
341 }
352 }
342 }
353 }
languages.toml+7
@@ -9,6 +9,13 @@ comment-token = "//"
9language-server = { command = "rust-analyzer" }
9language-server = { command = "rust-analyzer" }
10indent = { tab-width = 4, unit = " " }
10indent = { tab-width = 4, unit = " " }
11
11
12+[language.auto-pairs]
13+'(' = ')'
14+'{' = '}'
15+'[' = ']'
16+'"' = '"'
17+'`' = '`'
18+
12[language.debugger]
19[language.debugger]
13name = "lldb-vscode"
20name = "lldb-vscode"
14transport = "stdio"
21transport = "stdio"