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
This commit is contained in:
Skyler Hawthorne 2022-02-25 03:36:54 -05:00 committed by GitHub
parent b935fac957
commit a494f47a5d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 417 additions and 110 deletions

View File

@ -36,7 +36,6 @@ ### `[editor]` Section
| `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 @@ ### `[editor.file-picker]` Section
|`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`:

View File

@ -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 @@
('`', '`'),
];
// [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 @@
// 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 @@ fn test_hooks(
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 @@ fn test_hooks_with_pairs<I, F, R>(
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 @@ fn test_insert_blank() {
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 @@ fn test_insert_blank() {
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 @@ fn test_insert_blank() {
#[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 @@ fn test_insert_at_end_of_document() {
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 @@ fn test_insert_at_end_of_document() {
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 @@ fn test_append_blank() {
&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 @@ fn test_insert_blank_multi_cursor() {
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 @@ fn test_append() {
&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 @@ fn test_append_single_cursor() {
&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 @@ fn test_append_multi() {
0,
),
differing_pairs(),
DEFAULT_PAIRS,
|open, close| {
format!(
"foo{open}{close}\nfoo{open}{close}\nfoo{open}{close}\n",
@ -535,13 +618,14 @@ fn test_append_multi() {
/// ([)] -> 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 @@ fn test_insert_close_inside_pair() {
/// [(]) -> 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 @@ fn test_insert_close_inside_pair_multi_cursor() {
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 @@ fn test_append_close_inside_pair_multi_cursor() {
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 @@ fn test_insert_open_inside_pair() {
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 @@ fn test_append_open_inside_pair() {
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 @@ fn test_insert_nested_open_inside_pair() {
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 @@ fn test_append_nested_open_inside_pair() {
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 @@ fn test_insert_open_before_non_pair() {
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 @@ fn test_insert_open_with_selection() {
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 @@ fn test_append_close_inside_non_pair_with_selection() {
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 @@ fn test_insert_open_trailing_word_with_selection() {
&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 @@ fn test_insert_close_inside_pair_trailing_word_with_selection() {
&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 @@ fn test_insert_open_after_non_pair() {
&doc,
&sel,
differing_pairs(),
DEFAULT_PAIRS,
|open, close| format!("word{}{}{}", open, close, LINE_END),
&expected_sel,
);
@ -779,8 +904,34 @@ fn test_insert_open_after_non_pair() {
&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),
)
}
}

View File

@ -442,6 +442,7 @@ pub fn change<I>(document: &Document, changes: I) -> Self
indent_query: OnceCell::new(),
textobject_query: OnceCell::new(),
debugger: None,
auto_pairs: None,
}],
});

View File

@ -1,4 +1,5 @@
use crate::{
auto_pairs::AutoPairs,
chars::char_is_line_ending,
diagnostic::Severity,
regex::Regex,
@ -17,6 +18,7 @@
collections::{HashMap, HashSet, VecDeque},
fmt,
path::Path,
str::FromStr,
sync::Arc,
};
@ -41,6 +43,13 @@ fn deserialize_lsp_config<'de, D>(deserializer: D) -> Result<Option<serde_json::
.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 {

View File

@ -4045,22 +4045,19 @@ fn insert(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> {
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 fn insert_tab(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 contents = doc.text();
@ -4122,8 +4119,16 @@ pub fn insert_newline(cx: &mut Context) {
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 fn insert_newline(cx: &mut Context) {
transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index()));
let (view, doc) = current!(cx.editor);
doc.apply(&transaction, view.id);
}

View File

@ -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_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 @@ pub fn set_diagnostics(&mut self, diagnostics: Vec<Diagnostic>) {
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 {

View File

@ -13,6 +13,7 @@
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 @@
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 @@ fn default() -> Self {
},
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 @@ pub fn new(
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 @@ pub fn new(
idle_timer: Box::pin(sleep(config.idle_timeout)),
last_motion: None,
config,
auto_pairs,
exit_code: 0,
}
}

View File

@ -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"