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
pull/1635/head^2
Skyler Hawthorne 2 years ago committed by GitHub
parent b935fac957
commit a494f47a5d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -36,7 +36,6 @@ hidden = false
| `shell` | Shell to use when running external commands. | Unix: `["sh", "-c"]`<br/>Windows: `["cmd", "/C"]` | | `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` | | `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` | | `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` | | `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` | | `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` | | `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 |`git-exclude` | Enables reading `.git/info/exclude` files. | true
|`max-depth` | Set with an integer value for maximum depth to recurse. | Defaults to `None`. |`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 ## LSP
To display all language server messages in the status line add the following to your `config.toml`: To display all language server messages in the status line add the following to your `config.toml`:

@ -4,12 +4,14 @@
use crate::{ use crate::{
graphemes, movement::Direction, Range, Rope, RopeGraphemes, Selection, Tendril, Transaction, graphemes, movement::Direction, Range, Rope, RopeGraphemes, Selection, Tendril, Transaction,
}; };
use std::collections::HashMap;
use log::debug; use log::debug;
use smallvec::SmallVec; use smallvec::SmallVec;
// Heavily based on https://github.com/codemirror/closebrackets/ // 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 /// The type that represents the collection of auto pairs,
const OPEN_BEFORE: &str = "([{'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}"; /// keyed by the opener.
const CLOSE_BEFORE: &str = ")]}'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}"; // includes space and newlines #[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: // insert hook:
// Fn(doc, selection, char) => Option<Transaction> // 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 {% %} // middle of triple quotes, and more exotic pairs like Jinja's {% %}
#[must_use] #[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); debug!("autopairs hook selection: {:#?}", selection);
for &(open, close) in PAIRS { if let Some(pair) = pairs.get(ch) {
if open == ch { if pair.same() {
if open == close { return Some(handle_same(doc, selection, pair));
return Some(handle_same(doc, selection, open, CLOSE_BEFORE, OPEN_BEFORE)); } else if pair.open == ch {
} else { return Some(handle_open(doc, selection, pair));
return Some(handle_open(doc, selection, open, close, CLOSE_BEFORE)); } else if pair.close == ch {
}
}
if close == ch {
// && char_at pos == close // && 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) Range::new(end_anchor, end_head)
} }
fn handle_open( fn handle_open(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
doc: &Rope,
selection: &Selection,
open: char,
close: char,
close_before: &str,
) -> Transaction {
let mut end_ranges = SmallVec::with_capacity(selection.len()); let mut end_ranges = SmallVec::with_capacity(selection.len());
let mut offs = 0; let mut offs = 0;
@ -212,22 +290,21 @@ fn handle_open(
let len_inserted; let len_inserted;
let change = match next_char { let change = match next_char {
Some(ch) if !close_before.contains(ch) => { Some(_) if !pair.should_close(doc, start_range) => {
len_inserted = open.len_utf8(); len_inserted = pair.open.len_utf8();
let mut tendril = Tendril::new(); let mut tendril = Tendril::new();
tendril.push(open); tendril.push(pair.open);
(cursor, cursor, Some(tendril)) (cursor, cursor, Some(tendril))
} }
// None | Some(ch) if close_before.contains(ch) => {}
_ => { _ => {
// insert open & close // insert open & close
let pair = Tendril::from_iter([open, close]); let pair_str = Tendril::from_iter([pair.open, pair.close]);
len_inserted = open.len_utf8() + close.len_utf8(); len_inserted = pair.open.len_utf8() + pair.close.len_utf8();
(cursor, cursor, Some(pair)) (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); end_ranges.push(next_range);
offs += len_inserted; offs += len_inserted;
@ -239,7 +316,7 @@ fn handle_open(
t 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 end_ranges = SmallVec::with_capacity(selection.len());
let mut offs = 0; 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 next_char = doc.get_char(cursor);
let mut len_inserted = 0; 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 // return transaction that moves past close
(cursor, cursor, None) // no-op (cursor, cursor, None) // no-op
} else { } else {
len_inserted += close.len_utf8(); len_inserted += pair.close.len_utf8();
let mut tendril = Tendril::new(); let mut tendril = Tendril::new();
tendril.push(close); tendril.push(pair.close);
(cursor, cursor, Some(tendril)) (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); end_ranges.push(next_range);
offs += len_inserted; 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""") /// handle cases where open and close is the same, or in triples ("""docstring""")
fn handle_same( fn handle_same(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
doc: &Rope,
selection: &Selection,
token: char,
close_before: &str,
open_before: &str,
) -> Transaction {
let mut end_ranges = SmallVec::with_capacity(selection.len()); let mut end_ranges = SmallVec::with_capacity(selection.len());
let mut offs = 0; let mut offs = 0;
@ -286,30 +357,26 @@ fn handle_same(
let transaction = Transaction::change_by_selection(doc, selection, |start_range| { let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
let cursor = start_range.cursor(doc.slice(..)); let cursor = start_range.cursor(doc.slice(..));
let mut len_inserted = 0; let mut len_inserted = 0;
let next_char = doc.get_char(cursor); 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 // return transaction that moves past close
(cursor, cursor, None) // no-op (cursor, cursor, None) // no-op
} else { } else {
let mut pair = Tendril::new(); let mut pair_str = Tendril::new();
pair.push(token); pair_str.push(pair.open);
// for equal pairs, don't insert both open and close if either // for equal pairs, don't insert both open and close if either
// side has a non-pair char // side has a non-pair char
if (next_char.is_none() || close_before.contains(next_char.unwrap())) if pair.should_close(doc, start_range) {
&& (prev_char.is_none() || open_before.contains(prev_char.unwrap())) pair_str.push(pair.close);
{
pair.push(token);
} }
len_inserted += pair.len(); len_inserted += pair_str.len();
(cursor, cursor, Some(pair)) (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); end_ranges.push(next_range);
offs += len_inserted; offs += len_inserted;
@ -329,21 +396,23 @@ mod test {
const LINE_END: &str = crate::DEFAULT_LINE_ENDING.as_str(); const LINE_END: &str = crate::DEFAULT_LINE_ENDING.as_str();
fn differing_pairs() -> impl Iterator<Item = &'static (char, char)> { 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)> { 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( fn test_hooks(
in_doc: &Rope, in_doc: &Rope,
in_sel: &Selection, in_sel: &Selection,
ch: char, ch: char,
pairs: &[(char, char)],
expected_doc: &Rope, expected_doc: &Rope,
expected_sel: &Selection, 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(); let mut actual_doc = in_doc.clone();
assert!(trans.apply(&mut actual_doc)); assert!(trans.apply(&mut actual_doc));
assert_eq!(expected_doc, &actual_doc); assert_eq!(expected_doc, &actual_doc);
@ -353,7 +422,8 @@ mod test {
fn test_hooks_with_pairs<I, F, R>( fn test_hooks_with_pairs<I, F, R>(
in_doc: &Rope, in_doc: &Rope,
in_sel: &Selection, in_sel: &Selection,
pairs: I, test_pairs: I,
pairs: &[(char, char)],
get_expected_doc: F, get_expected_doc: F,
actual_sel: &Selection, actual_sel: &Selection,
) where ) where
@ -362,11 +432,12 @@ mod test {
R: Into<Rope>, R: Into<Rope>,
Rope: From<R>, Rope: From<R>,
{ {
pairs.into_iter().for_each(|(open, close)| { test_pairs.into_iter().for_each(|(open, close)| {
test_hooks( test_hooks(
in_doc, in_doc,
in_sel, in_sel,
*open, *open,
pairs,
&Rope::from(get_expected_doc(*open, *close)), &Rope::from(get_expected_doc(*open, *close)),
actual_sel, actual_sel,
) )
@ -381,7 +452,8 @@ mod test {
test_hooks_with_pairs( test_hooks_with_pairs(
&Rope::from(LINE_END), &Rope::from(LINE_END),
&Selection::single(1, 0), &Selection::single(1, 0),
PAIRS, DEFAULT_PAIRS,
DEFAULT_PAIRS,
|open, close| format!("{}{}{}", open, close, LINE_END), |open, close| format!("{}{}{}", open, close, LINE_END),
&Selection::single(2, 1), &Selection::single(2, 1),
); );
@ -391,7 +463,8 @@ mod test {
test_hooks_with_pairs( test_hooks_with_pairs(
&empty_doc, &empty_doc,
&Selection::single(empty_doc.len_chars(), LINE_END.len()), &Selection::single(empty_doc.len_chars(), LINE_END.len()),
PAIRS, DEFAULT_PAIRS,
DEFAULT_PAIRS,
|open, close| { |open, close| {
format!( format!(
"{line_end}{open}{close}{line_end}", "{line_end}{open}{close}{line_end}",
@ -406,13 +479,16 @@ mod test {
#[test] #[test]
fn test_insert_before_multi_code_point_graphemes() { fn test_insert_before_multi_code_point_graphemes() {
test_hooks_with_pairs( for (_, close) in differing_pairs() {
&Rope::from(format!("hello 👨‍👩‍👧‍👦 goodbye{}", LINE_END)), test_hooks(
&Selection::single(13, 6), &Rope::from(format!("hello 👨‍👩‍👧‍👦 goodbye{}", LINE_END)),
PAIRS, &Selection::single(13, 6),
|open, _| format!("hello {}👨‍👩‍👧‍👦 goodbye{}", open, LINE_END), *close,
&Selection::single(14, 7), DEFAULT_PAIRS,
); &Rope::from(format!("hello {}👨‍👩‍👧‍👦 goodbye{}", close, LINE_END)),
&Selection::single(14, 7),
);
}
} }
#[test] #[test]
@ -420,7 +496,8 @@ mod test {
test_hooks_with_pairs( test_hooks_with_pairs(
&Rope::from(LINE_END), &Rope::from(LINE_END),
&Selection::single(LINE_END.len(), LINE_END.len()), &Selection::single(LINE_END.len(), LINE_END.len()),
PAIRS, DEFAULT_PAIRS,
DEFAULT_PAIRS,
|open, close| format!("{}{}{}", LINE_END, open, close), |open, close| format!("{}{}{}", LINE_END, open, close),
&Selection::single(LINE_END.len() + 1, LINE_END.len() + 1), &Selection::single(LINE_END.len() + 1, LINE_END.len() + 1),
); );
@ -428,7 +505,8 @@ mod test {
test_hooks_with_pairs( test_hooks_with_pairs(
&Rope::from(format!("foo{}", LINE_END)), &Rope::from(format!("foo{}", LINE_END)),
&Selection::single(3 + LINE_END.len(), 3 + LINE_END.len()), &Selection::single(3 + LINE_END.len(), 3 + LINE_END.len()),
PAIRS, DEFAULT_PAIRS,
DEFAULT_PAIRS,
|open, close| format!("foo{}{}{}", LINE_END, open, close), |open, close| format!("foo{}{}{}", LINE_END, open, close),
&Selection::single(LINE_END.len() + 4, LINE_END.len() + 4), &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)), &Rope::from(format!("{line_end}{line_end}", line_end = LINE_END)),
// before inserting the pair, the cursor covers all of both empty lines // before inserting the pair, the cursor covers all of both empty lines
&Selection::single(0, LINE_END.len() * 2), &Selection::single(0, LINE_END.len() * 2),
PAIRS, DEFAULT_PAIRS,
DEFAULT_PAIRS,
|open, close| { |open, close| {
format!( format!(
"{line_end}{open}{close}{line_end}", "{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),), smallvec!(Range::new(1, 0), Range::new(2, 1), Range::new(3, 2),),
0, 0,
), ),
PAIRS, DEFAULT_PAIRS,
DEFAULT_PAIRS,
|open, close| { |open, close| {
format!( format!(
"{open}{close}\n{open}{close}\n{open}{close}\n", "{open}{close}\n{open}{close}\n{open}{close}\n",
@ -489,6 +569,7 @@ mod test {
&Rope::from("foo\n"), &Rope::from("foo\n"),
&Selection::single(2, 4), &Selection::single(2, 4),
differing_pairs(), differing_pairs(),
DEFAULT_PAIRS,
|open, close| format!("foo{}{}\n", open, close), |open, close| format!("foo{}{}\n", open, close),
&Selection::single(2, 5), &Selection::single(2, 5),
); );
@ -501,6 +582,7 @@ mod test {
&Rope::from(format!("foo{}", LINE_END)), &Rope::from(format!("foo{}", LINE_END)),
&Selection::single(3, 3 + LINE_END.len()), &Selection::single(3, 3 + LINE_END.len()),
differing_pairs(), differing_pairs(),
DEFAULT_PAIRS,
|open, close| format!("foo{}{}{}", open, close, LINE_END), |open, close| format!("foo{}{}{}", open, close, LINE_END),
&Selection::single(4, 5), &Selection::single(4, 5),
); );
@ -518,6 +600,7 @@ mod test {
0, 0,
), ),
differing_pairs(), differing_pairs(),
DEFAULT_PAIRS,
|open, close| { |open, close| {
format!( format!(
"foo{open}{close}\nfoo{open}{close}\nfoo{open}{close}\n", "foo{open}{close}\nfoo{open}{close}\nfoo{open}{close}\n",
@ -535,13 +618,14 @@ mod test {
/// ([)] -> insert ) -> ()[] /// ([)] -> insert ) -> ()[]
#[test] #[test]
fn test_insert_close_inside_pair() { 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)); let doc = Rope::from(format!("{}{}{}", open, close, LINE_END));
test_hooks( test_hooks(
&doc, &doc,
&Selection::single(2, 1), &Selection::single(2, 1),
*close, *close,
DEFAULT_PAIRS,
&doc, &doc,
&Selection::single(2 + LINE_END.len(), 2), &Selection::single(2 + LINE_END.len(), 2),
); );
@ -551,13 +635,14 @@ mod test {
/// [(]) -> append ) -> [()] /// [(]) -> append ) -> [()]
#[test] #[test]
fn test_append_close_inside_pair() { 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)); let doc = Rope::from(format!("{}{}{}", open, close, LINE_END));
test_hooks( test_hooks(
&doc, &doc,
&Selection::single(0, 2), &Selection::single(0, 2),
*close, *close,
DEFAULT_PAIRS,
&doc, &doc,
&Selection::single(0, 2 + LINE_END.len()), &Selection::single(0, 2 + LINE_END.len()),
); );
@ -579,14 +664,14 @@ mod test {
0, 0,
); );
for (open, close) in PAIRS { for (open, close) in DEFAULT_PAIRS {
let doc = Rope::from(format!( let doc = Rope::from(format!(
"{open}{close}\n{open}{close}\n{open}{close}\n", "{open}{close}\n{open}{close}\n{open}{close}\n",
open = open, open = open,
close = close 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, 0,
); );
for (open, close) in PAIRS { for (open, close) in DEFAULT_PAIRS {
let doc = Rope::from(format!( let doc = Rope::from(format!(
"{open}{close}\n{open}{close}\n{open}{close}\n", "{open}{close}\n{open}{close}\n{open}{close}\n",
open = open, open = open,
close = close 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 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 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 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 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( test_hooks_with_pairs(
&Rope::from("word"), &Rope::from("word"),
&Selection::single(1, 0), &Selection::single(1, 0),
PAIRS, DEFAULT_PAIRS,
DEFAULT_PAIRS,
|open, _| format!("{}word", open), |open, _| format!("{}word", open),
&Selection::single(2, 1), &Selection::single(2, 1),
) )
@ -710,7 +824,8 @@ mod test {
test_hooks_with_pairs( test_hooks_with_pairs(
&Rope::from("word"), &Rope::from("word"),
&Selection::single(3, 0), &Selection::single(3, 0),
PAIRS, DEFAULT_PAIRS,
DEFAULT_PAIRS,
|open, _| format!("{}word", open), |open, _| format!("{}word", open),
&Selection::single(4, 1), &Selection::single(4, 1),
) )
@ -722,10 +837,17 @@ mod test {
let sel = Selection::single(0, 4); let sel = Selection::single(0, 4);
let expected_sel = Selection::single(0, 5); let expected_sel = Selection::single(0, 5);
for (_, close) in PAIRS { for (_, close) in DEFAULT_PAIRS {
let doc = Rope::from("word"); let doc = Rope::from("word");
let expected_doc = Rope::from(format!("wor{}d", close)); 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"), &Rope::from("foo word"),
&Selection::single(7, 3), &Selection::single(7, 3),
differing_pairs(), differing_pairs(),
DEFAULT_PAIRS,
|open, close| format!("foo{}{} word", open, close), |open, close| format!("foo{}{} word", open, close),
&Selection::single(9, 4), &Selection::single(9, 4),
) )
@ -749,6 +872,7 @@ mod test {
&Rope::from(format!("foo{}{} word{}", open, close, LINE_END)), &Rope::from(format!("foo{}{} word{}", open, close, LINE_END)),
&Selection::single(9, 4), &Selection::single(9, 4),
*close, *close,
DEFAULT_PAIRS,
&Rope::from(format!("foo{}{} word{}", open, close, LINE_END)), &Rope::from(format!("foo{}{} word{}", open, close, LINE_END)),
&Selection::single(9, 5), &Selection::single(9, 5),
) )
@ -771,6 +895,7 @@ mod test {
&doc, &doc,
&sel, &sel,
differing_pairs(), differing_pairs(),
DEFAULT_PAIRS,
|open, close| format!("word{}{}{}", open, close, LINE_END), |open, close| format!("word{}{}{}", open, close, LINE_END),
&expected_sel, &expected_sel,
); );
@ -779,8 +904,34 @@ mod test {
&doc, &doc,
&sel, &sel,
matching_pairs(), matching_pairs(),
DEFAULT_PAIRS,
|open, _| format!("word{}{}", open, LINE_END), |open, _| format!("word{}{}", open, LINE_END),
&expected_sel, &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),
)
}
} }

@ -442,6 +442,7 @@ where
indent_query: OnceCell::new(), indent_query: OnceCell::new(),
textobject_query: OnceCell::new(), textobject_query: OnceCell::new(),
debugger: None, debugger: None,
auto_pairs: None,
}], }],
}); });

@ -1,4 +1,5 @@
use crate::{ use crate::{
auto_pairs::AutoPairs,
chars::char_is_line_ending, chars::char_is_line_ending,
diagnostic::Severity, diagnostic::Severity,
regex::Regex, regex::Regex,
@ -17,6 +18,7 @@ use std::{
collections::{HashMap, HashSet, VecDeque}, collections::{HashMap, HashSet, VecDeque},
fmt, fmt,
path::Path, path::Path,
str::FromStr,
sync::Arc, sync::Arc,
}; };
@ -41,6 +43,13 @@ where
.transpose() .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)] #[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct Configuration { pub struct Configuration {
@ -89,6 +98,13 @@ pub struct LanguageConfiguration {
pub(crate) textobject_query: OnceCell<Option<TextObjectQuery>>, pub(crate) textobject_query: OnceCell<Option<TextObjectQuery>>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub debugger: Option<DebugAdapterConfig>, 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)] #[derive(Debug, Serialize, Deserialize)]
@ -162,6 +178,56 @@ pub struct IndentationConfiguration {
pub unit: String, 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)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct IndentQuery { pub struct IndentQuery {

@ -4045,22 +4045,19 @@ pub mod insert {
use helix_core::auto_pairs; use helix_core::auto_pairs;
pub fn insert_char(cx: &mut Context, c: char) { pub fn insert_char(cx: &mut Context, c: char) {
let (view, doc) = current!(cx.editor); let (view, doc) = current_ref!(cx.editor);
let hooks: &[Hook] = match cx.editor.config.auto_pairs {
true => &[auto_pairs::hook, insert],
false => &[insert],
};
let text = doc.text(); let text = doc.text();
let selection = doc.selection(view.id); 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) let transaction = auto_pairs
for hook in hooks { .as_ref()
if let Some(transaction) = hook(text, selection, c) { .and_then(|ap| auto_pairs::hook(text, selection, c, ap))
doc.apply(&transaction, view.id); .or_else(|| insert(text, selection, c));
break;
} 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) // 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) { 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 text = doc.text().slice(..);
let contents = doc.text(); let contents = doc.text();
@ -4122,8 +4119,16 @@ pub mod insert {
let indent = doc.indent_unit().repeat(indent_level); let indent = doc.indent_unit().repeat(indent_level);
let mut text = String::new(); 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 // If we are between pairs (such as brackets), we want to
let new_head_pos = if helix_core::auto_pairs::PAIRS.contains(&(prev, curr)) { // 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); let inner_indent = doc.indent_unit().repeat(indent_level + 1);
text.reserve_exact(2 + indent.len() + inner_indent.len()); text.reserve_exact(2 + indent.len() + inner_indent.len());
text.push_str(doc.line_ending.as_str()); 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())); transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index()));
let (view, doc) = current!(cx.editor);
doc.apply(&transaction, view.id); doc.apply(&transaction, view.id);
} }

@ -1,4 +1,5 @@
use anyhow::{anyhow, bail, Context, Error}; use anyhow::{anyhow, bail, Context, Error};
use helix_core::auto_pairs::AutoPairs;
use serde::de::{self, Deserialize, Deserializer}; use serde::de::{self, Deserialize, Deserializer};
use serde::Serialize; use serde::Serialize;
use std::cell::Cell; use std::cell::Cell;
@ -20,7 +21,7 @@ use helix_core::{
}; };
use helix_lsp::util::LspFormatting; use helix_lsp::util::LspFormatting;
use crate::{DocumentId, ViewId}; use crate::{DocumentId, Editor, ViewId};
/// 8kB of buffer space for encoding and decoding `Rope`s. /// 8kB of buffer space for encoding and decoding `Rope`s.
const BUF_SIZE: usize = 8192; const BUF_SIZE: usize = 8192;
@ -98,7 +99,7 @@ pub struct Document {
pub line_ending: LineEnding, pub line_ending: LineEnding,
syntax: Option<Syntax>, syntax: Option<Syntax>,
// /// Corresponding language scope name. Usually `source.<lang>`. /// Corresponding language scope name. Usually `source.<lang>`.
pub(crate) language: Option<Arc<LanguageConfiguration>>, pub(crate) language: Option<Arc<LanguageConfiguration>>,
/// Pending changes since last history commit. /// Pending changes since last history commit.
@ -946,6 +947,28 @@ impl Document {
self.diagnostics self.diagnostics
.sort_unstable_by_key(|diagnostic| diagnostic.range); .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 { impl Default for Document {

@ -13,6 +13,7 @@ use futures_util::future;
use futures_util::stream::select_all::SelectAll; use futures_util::stream::select_all::SelectAll;
use tokio_stream::wrappers::UnboundedReceiverStream; use tokio_stream::wrappers::UnboundedReceiverStream;
use log::debug;
use std::{ use std::{
borrow::Cow, borrow::Cow,
collections::{BTreeMap, HashMap}, collections::{BTreeMap, HashMap},
@ -29,7 +30,10 @@ use anyhow::{bail, Error};
pub use helix_core::diagnostic::Severity; pub use helix_core::diagnostic::Severity;
pub use helix_core::register::Registers; 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_core::{Position, Selection};
use helix_dap as dap; use helix_dap as dap;
@ -98,8 +102,10 @@ pub struct Config {
pub line_number: LineNumber, pub line_number: LineNumber,
/// Middle click paste support. Defaults to true. /// Middle click paste support. Defaults to true.
pub middle_click_paste: bool, pub middle_click_paste: bool,
/// Automatic insertion of pairs to parentheses, brackets, etc. Defaults to true. /// Automatic insertion of pairs to parentheses, brackets,
pub auto_pairs: bool, /// 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. /// Automatic auto-completion, automatically pop up without user trigger. Defaults to true.
pub auto_completion: bool, 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. /// 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, line_number: LineNumber::Absolute,
middle_click_paste: true, middle_click_paste: true,
auto_pairs: true, auto_pairs: AutoPairConfig::default(),
auto_completion: true, auto_completion: true,
idle_timeout: Duration::from_millis(400), idle_timeout: Duration::from_millis(400),
completion_trigger_len: 2, completion_trigger_len: 2,
@ -289,6 +295,7 @@ pub struct Editor {
pub autoinfo: Option<Info>, pub autoinfo: Option<Info>,
pub config: Config, pub config: Config,
pub auto_pairs: Option<AutoPairs>,
pub idle_timer: Pin<Box<Sleep>>, pub idle_timer: Pin<Box<Sleep>>,
pub last_motion: Option<Motion>, pub last_motion: Option<Motion>,
@ -312,6 +319,9 @@ impl Editor {
config: Config, config: Config,
) -> Self { ) -> Self {
let language_servers = helix_lsp::Registry::new(); 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 // HAXX: offset the render area height by 1 to account for prompt/commandline
area.height -= 1; area.height -= 1;
@ -337,6 +347,7 @@ impl Editor {
idle_timer: Box::pin(sleep(config.idle_timeout)), idle_timer: Box::pin(sleep(config.idle_timeout)),
last_motion: None, last_motion: None,
config, config,
auto_pairs,
exit_code: 0, exit_code: 0,
} }
} }

@ -9,6 +9,13 @@ comment-token = "//"
language-server = { command = "rust-analyzer" } language-server = { command = "rust-analyzer" }
indent = { tab-width = 4, unit = " " } indent = { tab-width = 4, unit = " " }
[language.auto-pairs]
'(' = ')'
'{' = '}'
'[' = ']'
'"' = '"'
'`' = '`'
[language.debugger] [language.debugger]
name = "lldb-vscode" name = "lldb-vscode"
transport = "stdio" transport = "stdio"

Loading…
Cancel
Save