mirror of
https://github.com/helix-editor/helix.git
synced 2024-11-25 02:46:17 +04:00
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:
parent
b935fac957
commit
a494f47a5d
@ -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`:
|
||||
|
@ -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,21 +479,25 @@ fn test_insert_blank() {
|
||||
|
||||
#[test]
|
||||
fn test_insert_before_multi_code_point_graphemes() {
|
||||
test_hooks_with_pairs(
|
||||
for (_, close) in differing_pairs() {
|
||||
test_hooks(
|
||||
&Rope::from(format!("hello 👨👩👧👦 goodbye{}", LINE_END)),
|
||||
&Selection::single(13, 6),
|
||||
PAIRS,
|
||||
|open, _| format!("hello {}👨👩👧👦 goodbye{}", open, LINE_END),
|
||||
*close,
|
||||
DEFAULT_PAIRS,
|
||||
&Rope::from(format!("hello {}👨👩👧👦 goodbye{}", close, LINE_END)),
|
||||
&Selection::single(14, 7),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
}],
|
||||
});
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user