summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSkyler Hawthorne2022-02-25 08:36:54 +0000
committerGitHub2022-02-25 08:36:54 +0000
commita494f47a5df543a3ab8d6530a5acbc2a5bd04d44 (patch)
tree084ef1b24c593d58d6616b37e073073d93009ff3
parentb935fac9576cf333e22b82e40da8c4d73c8e547d (diff)
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
-rw-r--r--book/src/configuration.md44
-rw-r--r--helix-core/src/auto_pairs.rs325
-rw-r--r--helix-core/src/indent.rs1
-rw-r--r--helix-core/src/syntax.rs66
-rw-r--r--helix-term/src/commands.rs38
-rw-r--r--helix-view/src/document.rs27
-rw-r--r--helix-view/src/editor.rs19
-rw-r--r--languages.toml7
8 files changed, 417 insertions, 110 deletions
diff --git a/book/src/configuration.md b/book/src/configuration.md
index 8048f548..8f6e8bbb 100644
--- a/book/src/configuration.md
+++ b/book/src/configuration.md
@@ -36,7 +36,6 @@ hidden = false
| `shell` | Shell to use when running external commands. | Unix: `["sh", "-c"]`<br/>Windows: `["cmd", "/C"]` |
| `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` |
| `smart-case` | Enable smart case regex searching (case insensitive unless pattern contains upper case characters) | `true` |
-| `auto-pairs` | Enable automatic insertion of pairs to parenthese, brackets, etc. | `true` |
| `auto-completion` | Enable automatic pop up of auto-completion. | `true` |
| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. | `400` |
| `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.
|`git-exclude` | Enables reading `.git/info/exclude` files. | true
|`max-depth` | Set with an integer value for maximum depth to recurse. | Defaults to `None`.
+### `[editor.auto-pairs]` Section
+
+Enable automatic insertion of pairs to parentheses, brackets, etc. Can be
+a simple boolean value, or a specific mapping of pairs of single characters.
+
+| Key | Description |
+| --- | ----------- |
+| `false` | Completely disable auto pairing, regardless of language-specific settings
+| `true` | Use the default pairs: <code>(){}[]''""``</code>
+| Mapping of pairs | e.g. `{ "(" = ")", "{" = "}", ... }`
+
+Example
+
+```toml
+[editor.auto-pairs]
+'(' = ')'
+'{' = '}'
+'[' = ']'
+'"' = '"'
+'`' = '`'
+'<' = '>'
+```
+
+Additionally, this setting can be used in a language config. Unless
+the editor setting is `false`, this will override the editor config in
+documents with this language.
+
+Example `languages.toml` that adds <> and removes ''
+
+```toml
+[[language]]
+name = "rust"
+
+[language.auto-pairs]
+'(' = ')'
+'{' = '}'
+'[' = ']'
+'"' = '"'
+'`' = '`'
+'<' = '>'
+```
+
+
## LSP
To display all language server messages in the status line add the following to your `config.toml`:
diff --git a/helix-core/src/auto_pairs.rs b/helix-core/src/auto_pairs.rs
index f4359a34..bcd47356 100644
--- a/helix-core/src/auto_pairs.rs
+++ b/helix-core/src/auto_pairs.rs
@@ -4,12 +4,14 @@
use crate::{
graphemes, movement::Direction, Range, Rope, RopeGraphemes, Selection, Tendril, Transaction,
};
+use std::collections::HashMap;
+
use log::debug;
use smallvec::SmallVec;
// Heavily based on https://github.com/codemirror/closebrackets/
-pub const PAIRS: &[(char, char)] = &[
+pub const DEFAULT_PAIRS: &[(char, char)] = &[
('(', ')'),
('{', '}'),
('[', ']'),
@@ -18,9 +20,95 @@ pub const PAIRS: &[(char, char)] = &[
('`', '`'),
];
-// [TODO] build this dynamically in language config. see #992
-const OPEN_BEFORE: &str = "([{'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}";
-const CLOSE_BEFORE: &str = ")]}'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}"; // includes space and newlines
+/// The type that represents the collection of auto pairs,
+/// keyed by the opener.
+#[derive(Debug, Clone)]
+pub struct AutoPairs(HashMap<char, Pair>);
+
+/// Represents the config for a particular pairing.
+#[derive(Debug, Clone, Copy)]
+pub struct Pair {
+ pub open: char,
+ pub close: char,
+}
+
+impl Pair {
+ /// true if open == close
+ pub fn same(&self) -> bool {
+ self.open == self.close
+ }
+
+ /// true if all of the pair's conditions hold for the given document and range
+ pub fn should_close(&self, doc: &Rope, range: &Range) -> bool {
+ let mut should_close = Self::next_is_not_alpha(doc, range);
+
+ if self.same() {
+ should_close &= Self::prev_is_not_alpha(doc, range);
+ }
+
+ should_close
+ }
+
+ pub fn next_is_not_alpha(doc: &Rope, range: &Range) -> bool {
+ let cursor = range.cursor(doc.slice(..));
+ let next_char = doc.get_char(cursor);
+ next_char.map(|c| !c.is_alphanumeric()).unwrap_or(true)
+ }
+
+ pub fn prev_is_not_alpha(doc: &Rope, range: &Range) -> bool {
+ let cursor = range.cursor(doc.slice(..));
+ let prev_char = prev_char(doc, cursor);
+ prev_char.map(|c| !c.is_alphanumeric()).unwrap_or(true)
+ }
+}
+
+impl From<&(char, char)> for Pair {
+ fn from(&(open, close): &(char, char)) -> Self {
+ Self { open, close }
+ }
+}
+
+impl From<(&char, &char)> for Pair {
+ fn from((open, close): (&char, &char)) -> Self {
+ Self {
+ open: *open,
+ close: *close,
+ }
+ }
+}
+
+impl AutoPairs {
+ /// Make a new AutoPairs set with the given pairs and default conditions.
+ pub fn new<'a, V: 'a, A>(pairs: V) -> Self
+ where
+ V: IntoIterator<Item = A>,
+ A: Into<Pair>,
+ {
+ let mut auto_pairs = HashMap::new();
+
+ for pair in pairs.into_iter() {
+ let auto_pair = pair.into();
+
+ auto_pairs.insert(auto_pair.open, auto_pair);
+
+ if auto_pair.open != auto_pair.close {
+ auto_pairs.insert(auto_pair.close, auto_pair);
+ }
+ }
+
+ Self(auto_pairs)
+ }
+
+ pub fn get(&self, ch: char) -> Option<&Pair> {
+ self.0.get(&ch)
+ }
+}
+
+impl Default for AutoPairs {
+ fn default() -> Self {
+ AutoPairs::new(DEFAULT_PAIRS.iter())
+ }
+}
// insert hook:
// 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
// middle of triple quotes, and more exotic pairs like Jinja's {% %}
#[must_use]
-pub fn hook(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> {
+pub fn hook(doc: &Rope, selection: &Selection, ch: char, pairs: &AutoPairs) -> Option<Transaction> {
debug!("autopairs hook selection: {:#?}", selection);
- for &(open, close) in PAIRS {
- if open == ch {
- if open == close {
- return Some(handle_same(doc, selection, open, CLOSE_BEFORE, OPEN_BEFORE));
- } else {
- return Some(handle_open(doc, selection, open, close, CLOSE_BEFORE));
- }
- }
-
- if close == ch {
+ if let Some(pair) = pairs.get(ch) {
+ if pair.same() {
+ return Some(handle_same(doc, selection, pair));
+ } else if pair.open == ch {
+ return Some(handle_open(doc, selection, pair));
+ } else if pair.close == ch {
// && char_at pos == close
- return Some(handle_close(doc, selection, open, close));
+ return Some(handle_close(doc, selection, pair));
}
}
@@ -196,13 +280,7 @@ fn get_next_range(
Range::new(end_anchor, end_head)
}
-fn handle_open(
- doc: &Rope,
- selection: &Selection,
- open: char,
- close: char,
- close_before: &str,
-) -> Transaction {
+fn handle_open(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
let mut end_ranges = SmallVec::with_capacity(selection.len());
let mut offs = 0;
@@ -212,22 +290,21 @@ fn handle_open(
let len_inserted;
let change = match next_char {
- Some(ch) if !close_before.contains(ch) => {
- len_inserted = open.len_utf8();
+ Some(_) if !pair.should_close(doc, start_range) => {
+ len_inserted = pair.open.len_utf8();
let mut tendril = Tendril::new();
- tendril.push(open);
+ tendril.push(pair.open);
(cursor, cursor, Some(tendril))
}
- // None | Some(ch) if close_before.contains(ch) => {}
_ => {
// insert open & close
- let pair = Tendril::from_iter([open, close]);
- len_inserted = open.len_utf8() + close.len_utf8();
- (cursor, cursor, Some(pair))
+ let pair_str = Tendril::from_iter([pair.open, pair.close]);
+ len_inserted = pair.open.len_utf8() + pair.close.len_utf8();
+ (cursor, cursor, Some(pair_str))
}
};
- let next_range = get_next_range(doc, start_range, offs, open, len_inserted);
+ let next_range = get_next_range(doc, start_range, offs, pair.open, len_inserted);
end_ranges.push(next_range);
offs += len_inserted;
@@ -239,7 +316,7 @@ fn handle_open(
t
}
-fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) -> Transaction {
+fn handle_close(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
let mut end_ranges = SmallVec::with_capacity(selection.len());
let mut offs = 0;
@@ -249,17 +326,17 @@ fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) ->
let next_char = doc.get_char(cursor);
let mut len_inserted = 0;
- let change = if next_char == Some(close) {
+ let change = if next_char == Some(pair.close) {
// return transaction that moves past close
(cursor, cursor, None) // no-op
} else {
- len_inserted += close.len_utf8();
+ len_inserted += pair.close.len_utf8();
let mut tendril = Tendril::new();
- tendril.push(close);
+ tendril.push(pair.close);
(cursor, cursor, Some(tendril))
};
- let next_range = get_next_range(doc, start_range, offs, close, len_inserted);
+ let next_range = get_next_range(doc, start_range, offs, pair.close, len_inserted);
end_ranges.push(next_range);
offs += len_inserted;
@@ -272,13 +349,7 @@ fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) ->
}
/// handle cases where open and close is the same, or in triples ("""docstring""")
-fn handle_same(
- doc: &Rope,
- selection: &Selection,
- token: char,
- close_before: &str,
- open_before: &str,
-) -> Transaction {
+fn handle_same(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
let mut end_ranges = SmallVec::with_capacity(selection.len());
let mut offs = 0;
@@ -286,30 +357,26 @@ fn handle_same(
let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
let cursor = start_range.cursor(doc.slice(..));
let mut len_inserted = 0;
-
let next_char = doc.get_char(cursor);
- let prev_char = prev_char(doc, cursor);
- let change = if next_char == Some(token) {
+ let change = if next_char == Some(pair.open) {
// return transaction that moves past close
(cursor, cursor, None) // no-op
} else {
- let mut pair = Tendril::new();
- pair.push(token);
+ let mut pair_str = Tendril::new();
+ pair_str.push(pair.open);
// for equal pairs, don't insert both open and close if either
// side has a non-pair char
- if (next_char.is_none() || close_before.contains(next_char.unwrap()))
- && (prev_char.is_none() || open_before.contains(prev_char.unwrap()))
- {
- pair.push(token);
+ if pair.should_close(doc, start_range) {
+ pair_str.push(pair.close);
}
- len_inserted += pair.len();
- (cursor, cursor, Some(pair))
+ len_inserted += pair_str.len();
+ (cursor, cursor, Some(pair_str))
};
- let next_range = get_next_range(doc, start_range, offs, token, len_inserted);
+ let next_range = get_next_range(doc, start_range, offs, pair.open, len_inserted);
end_ranges.push(next_range);
offs += len_inserted;
@@ -329,21 +396,23 @@ mod test {
const LINE_END: &str = crate::DEFAULT_LINE_ENDING.as_str();
fn differing_pairs() -> impl Iterator<Item = &'static (char, char)> {
- PAIRS.iter().filter(|(open, close)| open != close)
+ DEFAULT_PAIRS.iter().filter(|(open, close)| open != close)
}
fn matching_pairs() -> impl Iterator<Item = &'static (char, char)> {
- PAIRS.iter().filter(|(open, close)| open == close)
+ DEFAULT_PAIRS.iter().filter(|(open, close)| open == close)
}
fn test_hooks(
in_doc: &Rope,
in_sel: &Selection,
ch: char,
+ pairs: &[(char, char)],
expected_doc: &Rope,
expected_sel: &Selection,
) {
- let trans = hook(in_doc, in_sel, ch).unwrap();
+ let pairs = AutoPairs::new(pairs.iter());
+ let trans = hook(in_doc, in_sel, ch, &pairs).unwrap();
let mut actual_doc = in_doc.clone();
assert!(trans.apply(&mut actual_doc));
assert_eq!(expected_doc, &actual_doc);
@@ -353,7 +422,8 @@ mod test {
fn test_hooks_with_pairs<I, F, R>(
in_doc: &Rope,
in_sel: &Selection,
- pairs: I,
+ test_pairs: I,
+ pairs: &[(char, char)],
get_expected_doc: F,
actual_sel: &Selection,
) where
@@ -362,11 +432,12 @@ mod test {
R: Into<Rope>,
Rope: From<R>,
{
- pairs.into_iter().for_each(|(open, close)| {
+ test_pairs.into_iter().for_each(|(open, close)| {
test_hooks(
in_doc,
in_sel,
*open,
+ pairs,
&Rope::from(get_expected_doc(*open, *close)),
actual_sel,
)
@@ -381,7 +452,8 @@ mod test {
test_hooks_with_pairs(
&Rope::from(LINE_END),
&Selection::single(1, 0),
- PAIRS,
+ DEFAULT_PAIRS,
+ DEFAULT_PAIRS,
|open, close| format!("{}{}{}", open, close, LINE_END),
&Selection::single(2, 1),
);
@@ -391,7 +463,8 @@ mod test {
test_hooks_with_pairs(
&empty_doc,
&Selection::single(empty_doc.len_chars(), LINE_END.len()),
- PAIRS,
+ DEFAULT_PAIRS,
+ DEFAULT_PAIRS,
|open, close| {
format!(
"{line_end}{open}{close}{line_end}",
@@ -406,13 +479,16 @@ mod test {
#[test]
fn test_insert_before_multi_code_point_graphemes() {
- test_hooks_with_pairs(
- &Rope::from(format!("hello ๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ goodbye{}", LINE_END)),
- &Selection::single(13, 6),
- PAIRS,
- |open, _| format!("hello {}๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ goodbye{}", open, LINE_END),
- &Selection::single(14, 7),
- );
+ for (_, close) in differing_pairs() {
+ test_hooks(
+ &Rope::from(format!("hello ๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ goodbye{}", LINE_END)),
+ &Selection::single(13, 6),
+ *close,
+ DEFAULT_PAIRS,
+ &Rope::from(format!("hello {}๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ goodbye{}", close, LINE_END)),
+ &Selection::single(14, 7),
+ );
+ }
}
#[test]
@@ -420,7 +496,8 @@ mod test {
test_hooks_with_pairs(
&Rope::from(LINE_END),
&Selection::single(LINE_END.len(), LINE_END.len()),
- PAIRS,
+ DEFAULT_PAIRS,
+ DEFAULT_PAIRS,
|open, close| format!("{}{}{}", LINE_END, open, close),
&Selection::single(LINE_END.len() + 1, LINE_END.len() + 1),
);
@@ -428,7 +505,8 @@ mod test {
test_hooks_with_pairs(
&Rope::from(format!("foo{}", LINE_END)),
&Selection::single(3 + LINE_END.len(), 3 + LINE_END.len()),
- PAIRS,
+ DEFAULT_PAIRS,
+ DEFAULT_PAIRS,
|open, close| format!("foo{}{}{}", LINE_END, open, close),
&Selection::single(LINE_END.len() + 4, LINE_END.len() + 4),
);
@@ -442,7 +520,8 @@ mod test {
&Rope::from(format!("{line_end}{line_end}", line_end = LINE_END)),
// before inserting the pair, the cursor covers all of both empty lines
&Selection::single(0, LINE_END.len() * 2),
- PAIRS,
+ DEFAULT_PAIRS,
+ DEFAULT_PAIRS,
|open, close| {
format!(
"{line_end}{open}{close}{line_end}",
@@ -467,7 +546,8 @@ mod test {
smallvec!(Range::new(1, 0), Range::new(2, 1), Range::new(3, 2),),
0,
),
- PAIRS,
+ DEFAULT_PAIRS,
+ DEFAULT_PAIRS,
|open, close| {
format!(
"{open}{close}\n{open}{close}\n{open}{close}\n",
@@ -489,6 +569,7 @@ mod test {
&Rope::from("foo\n"),
&Selection::single(2, 4),
differing_pairs(),
+ DEFAULT_PAIRS,
|open, close| format!("foo{}{}\n", open, close),
&Selection::single(2, 5),
);
@@ -501,6 +582,7 @@ mod test {
&Rope::from(format!("foo{}", LINE_END)),
&Selection::single(3, 3 + LINE_END.len()),
differing_pairs(),
+ DEFAULT_PAIRS,
|open, close| format!("foo{}{}{}", open, close, LINE_END),
&Selection::single(4, 5),
);
@@ -518,6 +600,7 @@ mod test {
0,
),
differing_pairs(),
+ DEFAULT_PAIRS,
|open, close| {
format!(
"foo{open}{close}\nfoo{open}{close}\nfoo{open}{close}\n",
@@ -535,13 +618,14 @@ mod test {
/// ([)] -> insert ) -> ()[]
#[test]
fn test_insert_close_inside_pair() {
- for (open, close) in PAIRS {
+ for (open, close) in DEFAULT_PAIRS {
let doc = Rope::from(format!("{}{}{}", open, close, LINE_END));
test_hooks(
&doc,
&Selection::single(2, 1),
*close,
+ DEFAULT_PAIRS,
&doc,
&Selection::single(2 + LINE_END.len(), 2),
);
@@ -551,13 +635,14 @@ mod test {
/// [(]) -> append ) -> [()]
#[test]
fn test_append_close_inside_pair() {
- for (open, close) in PAIRS {
+ for (open, close) in DEFAULT_PAIRS {
let doc = Rope::from(format!("{}{}{}", open, close, LINE_END));
test_hooks(
&doc,
&Selection::single(0, 2),
*close,
+ DEFAULT_PAIRS,
&doc,
&Selection::single(0, 2 + LINE_END.len()),
);
@@ -579,14 +664,14 @@ mod test {
0,
);
- for (open, close) in PAIRS {
+ for (open, close) in DEFAULT_PAIRS {
let doc = Rope::from(format!(
"{open}{close}\n{open}{close}\n{open}{close}\n",
open = open,
close = close
));
- test_hooks(&doc, &sel, *close, &doc, &expected_sel);
+ test_hooks(&doc, &sel, *close, DEFAULT_PAIRS, &doc, &expected_sel);
}
}
@@ -605,14 +690,14 @@ mod test {
0,
);
- for (open, close) in PAIRS {
+ for (open, close) in DEFAULT_PAIRS {
let doc = Rope::from(format!(
"{open}{close}\n{open}{close}\n{open}{close}\n",
open = open,
close = close
));
- test_hooks(&doc, &sel, *close, &doc, &expected_sel);
+ test_hooks(&doc, &sel, *close, DEFAULT_PAIRS, &doc, &expected_sel);
}
}
@@ -630,7 +715,14 @@ mod test {
close = close
));
- test_hooks(&doc, &sel, *open, &expected_doc, &expected_sel);
+ test_hooks(
+ &doc,
+ &sel,
+ *open,
+ DEFAULT_PAIRS,
+ &expected_doc,
+ &expected_sel,
+ );
}
}
@@ -648,7 +740,14 @@ mod test {
close = close
));
- test_hooks(&doc, &sel, *open, &expected_doc, &expected_sel);
+ test_hooks(
+ &doc,
+ &sel,
+ *open,
+ DEFAULT_PAIRS,
+ &expected_doc,
+ &expected_sel,
+ );
}
}
@@ -667,7 +766,14 @@ mod test {
outer_open, inner_open, inner_close, outer_close
));
- test_hooks(&doc, &sel, *inner_open, &expected_doc, &expected_sel);
+ test_hooks(
+ &doc,
+ &sel,
+ *inner_open,
+ DEFAULT_PAIRS,
+ &expected_doc,
+ &expected_sel,
+ );
}
}
}
@@ -687,7 +793,14 @@ mod test {
outer_open, inner_open, inner_close, outer_close
));
- test_hooks(&doc, &sel, *inner_open, &expected_doc, &expected_sel);
+ test_hooks(
+ &doc,
+ &sel,
+ *inner_open,
+ DEFAULT_PAIRS,
+ &expected_doc,
+ &expected_sel,
+ );
}
}
}
@@ -698,7 +811,8 @@ mod test {
test_hooks_with_pairs(
&Rope::from("word"),
&Selection::single(1, 0),
- PAIRS,
+ DEFAULT_PAIRS,
+ DEFAULT_PAIRS,
|open, _| format!("{}word", open),
&Selection::single(2, 1),
)
@@ -710,7 +824,8 @@ mod test {
test_hooks_with_pairs(
&Rope::from("word"),
&Selection::single(3, 0),
- PAIRS,
+ DEFAULT_PAIRS,
+ DEFAULT_PAIRS,
|open, _| format!("{}word", open),
&Selection::single(4, 1),
)
@@ -722,10 +837,17 @@ mod test {
let sel = Selection::single(0, 4);
let expected_sel = Selection::single(0, 5);
- for (_, close) in PAIRS {
+ for (_, close) in DEFAULT_PAIRS {
let doc = Rope::from("word");
let expected_doc = Rope::from(format!("wor{}d", close));
- test_hooks(&doc, &sel, *close, &expected_doc, &expected_sel);
+ test_hooks(
+ &doc,
+ &sel,
+ *close,
+ DEFAULT_PAIRS,
+ &expected_doc,
+ &expected_sel,
+ );
}
}
@@ -736,6 +858,7 @@ mod test {
&Rope::from("foo word"),
&Selection::single(7, 3),
differing_pairs(),
+ DEFAULT_PAIRS,
|open, close| format!("foo{}{} word", open, close),
&Selection::single(9, 4),
)
@@ -749,6 +872,7 @@ mod test {
&Rope::from(format!("foo{}{} word{}", open, close, LINE_END)),
&Selection::single(9, 4),
*close,
+ DEFAULT_PAIRS,
&Rope::from(format!("foo{}{} word{}", open, close, LINE_END)),
&Selection::single(9, 5),
)
@@ -771,6 +895,7 @@ mod test {
&doc,
&sel,
differing_pairs(),
+ DEFAULT_PAIRS,
|open, close| format!("word{}{}{}", open, close, LINE_END),
&expected_sel,
);
@@ -779,8 +904,34 @@ mod test {
&doc,
&sel,
matching_pairs(),
+ DEFAULT_PAIRS,
|open, _| format!("word{}{}", open, LINE_END),
&expected_sel,
);
}
+
+ #[test]
+ fn test_configured_pairs() {
+ let test_pairs = &[('`', ':'), ('+', '-')];
+
+ test_hooks_with_pairs(
+ &Rope::from(LINE_END),
+ &Selection::single(1, 0),
+ test_pairs,
+ test_pairs,
+ |open, close| format!("{}{}{}", open, close, LINE_END),
+ &Selection::single(2, 1),
+ );
+
+ let doc = Rope::from(format!("foo`: word{}", LINE_END));
+
+ test_hooks(
+ &doc,
+ &Selection::single(9, 4),
+ ':',
+ test_pairs,
+ &doc,
+ &Selection::single(9, 5),
+ )
+ }
}
diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs
index 5d20edc1..9a329d95 100644
--- a/helix-core/src/indent.rs
+++ b/helix-core/src/indent.rs
@@ -442,6 +442,7 @@ where
indent_query: OnceCell::new(),
textobject_query: OnceCell::new(),
debugger: None,
+ auto_pairs: None,
}],
});
diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs
index ccf91100..ca06e2dd 100644
--- a/helix-core/src/syntax.rs
+++ b/helix-core/src/syntax.rs
@@ -1,4 +1,5 @@
use crate::{
+ auto_pairs::AutoPairs,
chars::char_is_line_ending,
diagnostic::Severity,
regex::Regex,
@@ -17,6 +18,7 @@ use std::{
collections::{HashMap, HashSet, VecDeque},
fmt,
path::Path,
+ str::FromStr,
sync::Arc,
};
@@ -41,6 +43,13 @@ where
.transpose()
}
+pub fn deserialize_auto_pairs<'de, D>(deserializer: D) -> Result<Option<AutoPairs>, D::Error>
+where
+ D: serde::Deserializer<'de>,
+{
+ Ok(Option::<AutoPairConfig>::deserialize(deserializer)?.and_then(AutoPairConfig::into))
+}
+
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Configuration {
@@ -89,6 +98,13 @@ pub struct LanguageConfiguration {
pub(crate) textobject_query: OnceCell<Option<TextObjectQuery>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub debugger: Option<DebugAdapterConfig>,
+
+ /// Automatic insertion of pairs to parentheses, brackets,
+ /// etc. Defaults to true. Optionally, this can be a list of 2-tuples
+ /// to specify a list of characters to pair. This overrides the
+ /// global setting.
+ #[serde(default, skip_serializing, deserialize_with = "deserialize_auto_pairs")]
+ pub auto_pairs: Option<AutoPairs>,
}
#[derive(Debug, Serialize, Deserialize)]
@@ -162,6 +178,56 @@ pub struct IndentationConfiguration {
pub unit: String,
}
+/// Configuration for auto pairs
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case", deny_unknown_fields, untagged)]
+pub enum AutoPairConfig {
+ /// Enables or disables auto pairing. False means disabled. True means to use the default pairs.
+ Enable(bool),
+
+ /// The mappings of pairs.
+ Pairs(HashMap<char, char>),
+}
+
+impl Default for AutoPairConfig {
+ fn default() -> Self {
+ AutoPairConfig::Enable(true)
+ }
+}
+
+impl From<&AutoPairConfig> for Option<AutoPairs> {
+ fn from(auto_pair_config: &AutoPairConfig) -> Self {
+ match auto_pair_config {
+ AutoPairConfig::Enable(false) => None,
+ AutoPairConfig::Enable(true) => Some(AutoPairs::default()),
+ AutoPairConfig::Pairs(pairs) => Some(AutoPairs::new(pairs.iter())),
+ }
+ }
+}
+
+impl From<AutoPairConfig> for Option<AutoPairs> {
+ fn from(auto_pairs_config: AutoPairConfig) -> Self {
+ (&auto_pairs_config).into()
+ }
+}
+
+impl FromStr for AutoPairConfig {
+ type Err = std::str::ParseBoolError;
+
+ // only do bool parsing for runtime setting
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let enable: bool = s.parse()?;
+
+ let enable = if enable {
+ AutoPairConfig::Enable(true)
+ } else {
+ AutoPairConfig::Enable(false)
+ };
+
+ Ok(enable)
+ }
+}
+
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct IndentQuery {
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index 1272cc8a..c4f25e88 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -4045,22 +4045,19 @@ pub mod insert {
use helix_core::auto_pairs;
pub fn insert_char(cx: &mut Context, c: char) {
- let (view, doc) = current!(cx.editor);
-
- let hooks: &[Hook] = match cx.editor.config.auto_pairs {
- true => &[auto_pairs::hook, insert],
- false => &[insert],
- };
-
+ let (view, doc) = current_ref!(cx.editor);
let text = doc.text();
let selection = doc.selection(view.id);
+ let auto_pairs = doc.auto_pairs(cx.editor);
- // run through insert hooks, stopping on the first one that returns Some(t)
- for hook in hooks {
- if let Some(transaction) = hook(text, selection, c) {
- doc.apply(&transaction, view.id);
- break;
- }
+ let transaction = auto_pairs
+ .as_ref()
+ .and_then(|ap| auto_pairs::hook(text, selection, c, ap))
+ .or_else(|| insert(text, selection, c));
+
+ let (view, doc) = current!(cx.editor);
+ if let Some(t) = transaction {
+ doc.apply(&t, view.id);
}
// TODO: need a post insert hook too for certain triggers (autocomplete, signature help, etc)
@@ -4087,7 +4084,7 @@ pub mod insert {
}
pub fn insert_newline(cx: &mut Context) {
- let (view, doc) = current!(cx.editor);
+ let (view, doc) = current_ref!(cx.editor);
let text = doc.text().slice(..);
let contents = doc.text();
@@ -4122,8 +4119,16 @@ pub mod insert {
let indent = doc.indent_unit().repeat(indent_level);
let mut text = String::new();
- // 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
- let new_head_pos = if helix_core::auto_pairs::PAIRS.contains(&(prev, curr)) {
+ // 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
+ let on_auto_pair = doc
+ .auto_pairs(cx.editor)
+ .and_then(|pairs| pairs.get(prev))
+ .and_then(|pair| if pair.close == curr { Some(pair) } else { None })
+ .is_some();
+
+ let new_head_pos = if on_auto_pair {
let inner_indent = doc.indent_unit().repeat(indent_level + 1);
text.reserve_exact(2 + indent.len() + inner_indent.len());
text.push_str(doc.line_ending.as_str());
@@ -4150,6 +4155,7 @@ pub mod insert {
transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index()));
+ let (view, doc) = current!(cx.editor);
doc.apply(&transaction, view.id);
}
diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs
index f13338ba..671ceb75 100644
--- a/helix-view/src/document.rs
+++ b/helix-view/src/document.rs
@@ -1,4 +1,5 @@
use anyhow::{anyhow, bail, Context, Error};
+use helix_core::auto_pairs::AutoPairs;
use serde::de::{self, Deserialize, Deserializer};
use serde::Serialize;
use std::cell::Cell;
@@ -20,7 +21,7 @@ use helix_core::{
};
use helix_lsp::util::LspFormatting;
-use crate::{DocumentId, ViewId};
+use crate::{DocumentId, Editor, ViewId};
/// 8kB of buffer space for encoding and decoding `Rope`s.
const BUF_SIZE: usize = 8192;
@@ -98,7 +99,7 @@ pub struct Document {
pub line_ending: LineEnding,
syntax: Option<Syntax>,
- // /// Corresponding language scope name. Usually `source.<lang>`.
+ /// Corresponding language scope name. Usually `source.<lang>`.
pub(crate) language: Option<Arc<LanguageConfiguration>>,
/// Pending changes since last history commit.
@@ -946,6 +947,28 @@ impl Document {
self.diagnostics
.sort_unstable_by_key(|diagnostic| diagnostic.range);
}
+
+ /// Get the document's auto pairs. If the document has a recognized
+ /// language config with auto pairs configured, returns that;
+ /// otherwise, falls back to the global auto pairs config. If the global
+ /// config is false, then ignore language settings.
+ pub fn auto_pairs<'a>(&'a self, editor: &'a Editor) -> Option<&'a AutoPairs> {
+ let global_config = (editor.auto_pairs).as_ref();
+
+ // NOTE: If the user specifies the global auto pairs config as false, then
+ // we want to disable it globally regardless of language settings
+ #[allow(clippy::question_mark)]
+ {
+ if global_config.is_none() {
+ return None;
+ }
+ }
+
+ match &self.language {
+ Some(lang) => lang.as_ref().auto_pairs.as_ref().or(global_config),
+ None => global_config,
+ }
+ }
}
impl Default for Document {
diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
index d44dc1c6..85d9be67 100644
--- a/helix-view/src/editor.rs
+++ b/helix-view/src/editor.rs
@@ -13,6 +13,7 @@ use futures_util::future;
use futures_util::stream::select_all::SelectAll;
use tokio_stream::wrappers::UnboundedReceiverStream;
+use log::debug;
use std::{
borrow::Cow,
collections::{BTreeMap, HashMap},
@@ -29,7 +30,10 @@ use anyhow::{bail, Error};
pub use helix_core::diagnostic::Severity;
pub use helix_core::register::Registers;
-use helix_core::syntax;
+use helix_core::{
+ auto_pairs::AutoPairs,
+ syntax::{self, AutoPairConfig},
+};
use helix_core::{Position, Selection};
use helix_dap as dap;
@@ -98,8 +102,10 @@ pub struct Config {
pub line_number: LineNumber,
/// Middle click paste support. Defaults to true.
pub middle_click_paste: bool,
- /// Automatic insertion of pairs to parentheses, brackets, etc. Defaults to true.
- pub auto_pairs: bool,
+ /// Automatic insertion of pairs to parentheses, brackets,
+ /// etc. Optionally, this can be a list of 2-tuples to specify a
+ /// global list of characters to pair. Defaults to true.
+ pub auto_pairs: AutoPairConfig,
/// Automatic auto-completion, automatically pop up without user trigger. Defaults to true.
pub auto_completion: bool,
/// 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 {
},
line_number: LineNumber::Absolute,
middle_click_paste: true,
- auto_pairs: true,
+ auto_pairs: AutoPairConfig::default(),
auto_completion: true,
idle_timeout: Duration::from_millis(400),
completion_trigger_len: 2,
@@ -289,6 +295,7 @@ pub struct Editor {
pub autoinfo: Option<Info>,
pub config: Config,
+ pub auto_pairs: Option<AutoPairs>,
pub idle_timer: Pin<Box<Sleep>>,
pub last_motion: Option<Motion>,
@@ -312,6 +319,9 @@ impl Editor {
config: Config,
) -> Self {
let language_servers = helix_lsp::Registry::new();
+ let auto_pairs = (&config.auto_pairs).into();
+
+ debug!("Editor config: {config:#?}");
// HAXX: offset the render area height by 1 to account for prompt/commandline
area.height -= 1;
@@ -337,6 +347,7 @@ impl Editor {
idle_timer: Box::pin(sleep(config.idle_timeout)),
last_motion: None,
config,
+ auto_pairs,
exit_code: 0,
}
}
diff --git a/languages.toml b/languages.toml
index 9876bcf1..33906e4b 100644
--- a/languages.toml
+++ b/languages.toml
@@ -9,6 +9,13 @@ comment-token = "//"
language-server = { command = "rust-analyzer" }
indent = { tab-width = 4, unit = " " }
+[language.auto-pairs]
+'(' = ')'
+'{' = '}'
+'[' = ']'
+'"' = '"'
+'`' = '`'
+
[language.debugger]
name = "lldb-vscode"
transport = "stdio"