Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-core/src/auto_pairs.rs')
-rw-r--r--helix-core/src/auto_pairs.rs421
1 files changed, 104 insertions, 317 deletions
diff --git a/helix-core/src/auto_pairs.rs b/helix-core/src/auto_pairs.rs
index 85329040..9b901e9b 100644
--- a/helix-core/src/auto_pairs.rs
+++ b/helix-core/src/auto_pairs.rs
@@ -1,13 +1,9 @@
-//! When typing the opening character of one of the possible pairs defined below,
-//! this module provides the functionality to insert the paired closing character.
-
-use crate::{graphemes, movement::Direction, Range, Rope, Selection, Tendril, Transaction};
-use std::collections::HashMap;
-
+use crate::{Range, Rope, Selection, Tendril, Transaction};
use smallvec::SmallVec;
// Heavily based on https://github.com/codemirror/closebrackets/
-pub const DEFAULT_PAIRS: &[(char, char)] = &[
+
+pub const PAIRS: &[(char, char)] = &[
('(', ')'),
('{', '}'),
('[', ']'),
@@ -16,95 +12,7 @@ pub const DEFAULT_PAIRS: &[(char, char)] = &[
('`', '`'),
];
-/// The type that represents the collection of auto pairs,
-/// keyed by both opener and closer.
-#[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>(pairs: V) -> Self
- where
- V: IntoIterator<Item = A> + '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())
- }
-}
+const CLOSE_BEFORE: &str = ")]}'\":;> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}"; // includes space and newlines
// insert hook:
// Fn(doc, selection, char) => Option<Transaction>
@@ -114,260 +22,139 @@ impl Default for AutoPairs {
//
// to simplify, maybe return Option<Transaction> and just reimplement the default
-// [TODO]
-// * delete implementation where it erases the whole bracket (|) -> |
-// * change to multi character pairs to handle cases like placing the cursor in the
-// middle of triple quotes, and more exotic pairs like Jinja's {% %}
+// TODO: delete implementation where it erases the whole bracket (|) -> |
#[must_use]
-pub fn hook(doc: &Rope, selection: &Selection, ch: char, pairs: &AutoPairs) -> Option<Transaction> {
- log::trace!("autopairs hook selection: {:#?}", selection);
+pub fn hook(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> {
+ for &(open, close) in PAIRS {
+ if open == ch {
+ if open == close {
+ return handle_same(doc, selection, open);
+ } else {
+ return Some(handle_open(doc, selection, open, close, CLOSE_BEFORE));
+ }
+ }
- 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 {
+ if close == ch {
// && char_at pos == close
- return Some(handle_close(doc, selection, pair));
+ return Some(handle_close(doc, selection, open, close));
}
}
None
}
-fn prev_char(doc: &Rope, pos: usize) -> Option<char> {
- if pos == 0 {
- return None;
- }
-
- doc.get_char(pos - 1)
-}
-
-/// calculate what the resulting range should be for an auto pair insertion
-fn get_next_range(doc: &Rope, start_range: &Range, offset: usize, len_inserted: usize) -> Range {
- // When the character under the cursor changes due to complete pair
- // insertion, we must look backward a grapheme and then add the length
- // of the insertion to put the resulting cursor in the right place, e.g.
- //
- // foo[\r\n] - anchor: 3, head: 5
- // foo([)]\r\n - anchor: 4, head: 5
- //
- // foo[\r\n] - anchor: 3, head: 5
- // foo'[\r\n] - anchor: 4, head: 6
- //
- // foo([)]\r\n - anchor: 4, head: 5
- // foo()[\r\n] - anchor: 5, head: 7
- //
- // [foo]\r\n - anchor: 0, head: 3
- // [foo(])\r\n - anchor: 0, head: 5
-
- // inserting at the very end of the document after the last newline
- if start_range.head == doc.len_chars() && start_range.anchor == doc.len_chars() {
- return Range::new(
- start_range.anchor + offset + 1,
- start_range.head + offset + 1,
- );
- }
-
- let doc_slice = doc.slice(..);
- let single_grapheme = start_range.is_single_grapheme(doc_slice);
-
- // just skip over graphemes
- if len_inserted == 0 {
- let end_anchor = if single_grapheme {
- graphemes::next_grapheme_boundary(doc_slice, start_range.anchor) + offset
-
- // even for backward inserts with multiple grapheme selections,
- // we want the anchor to stay where it is so that the relative
- // selection does not change, e.g.:
- //
- // foo([) wor]d -> insert ) -> foo()[ wor]d
- } else {
- start_range.anchor + offset
- };
-
- return Range::new(
- end_anchor,
- graphemes::next_grapheme_boundary(doc_slice, start_range.head) + offset,
- );
- }
-
- // trivial case: only inserted a single-char opener, just move the selection
- if len_inserted == 1 {
- let end_anchor = if single_grapheme || start_range.direction() == Direction::Backward {
- start_range.anchor + offset + 1
- } else {
- start_range.anchor + offset
- };
+// TODO: special handling for lifetimes in rust: if preceeded by & or < don't auto close '
+// for example "&'a mut", or "fn<'a>"
- return Range::new(end_anchor, start_range.head + offset + 1);
+fn next_char(doc: &Rope, pos: usize) -> Option<char> {
+ if pos >= doc.len_chars() {
+ return None;
}
-
- // If the head = 0, then we must be in insert mode with a backward
- // cursor, which implies the head will just move
- let end_head = if start_range.head == 0 || start_range.direction() == Direction::Backward {
- start_range.head + offset + 1
- } else {
- // We must have a forward cursor, which means we must move to the
- // other end of the grapheme to get to where the new characters
- // are inserted, then move the head to where it should be
- let prev_bound = graphemes::prev_grapheme_boundary(doc_slice, start_range.head);
- log::trace!(
- "prev_bound: {}, offset: {}, len_inserted: {}",
- prev_bound,
- offset,
- len_inserted
- );
- prev_bound + offset + len_inserted
- };
-
- let end_anchor = match (start_range.len(), start_range.direction()) {
- // if we have a zero width cursor, it shifts to the same number
- (0, _) => end_head,
-
- // If we are inserting for a regular one-width cursor, the anchor
- // moves with the head. This is the fast path for ASCII.
- (1, Direction::Forward) => end_head - 1,
- (1, Direction::Backward) => end_head + 1,
-
- (_, Direction::Forward) => {
- if single_grapheme {
- graphemes::prev_grapheme_boundary(doc.slice(..), start_range.head) + 1
-
- // if we are appending, the anchor stays where it is; only offset
- // for multiple range insertions
- } else {
- start_range.anchor + offset
- }
- }
-
- (_, Direction::Backward) => {
- if single_grapheme {
- // if we're backward, then the head is at the first char
- // of the typed char, so we need to add the length of
- // the closing char
- graphemes::prev_grapheme_boundary(doc.slice(..), start_range.anchor)
- + len_inserted
- + offset
- } else {
- // when we are inserting in front of a selection, we need to move
- // the anchor over by however many characters were inserted overall
- start_range.anchor + offset + len_inserted
- }
- }
- };
-
- Range::new(end_anchor, end_head)
+ Some(doc.char(pos))
}
+// TODO: selections should be extended if range, moved if point.
+
+// TODO: if not cursor but selection, wrap on both sides of selection (surround)
+fn handle_open(
+ doc: &Rope,
+ selection: &Selection,
+ open: char,
+ close: char,
+ close_before: &str,
+) -> Transaction {
+ let mut ranges = SmallVec::with_capacity(selection.len());
-fn handle_open(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
- let mut end_ranges = SmallVec::with_capacity(selection.len());
let mut offs = 0;
- let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
- let cursor = start_range.cursor(doc.slice(..));
- let next_char = doc.get_char(cursor);
- let len_inserted;
+ let transaction = Transaction::change_by_selection(doc, selection, |range| {
+ let pos = range.head;
+ let next = next_char(doc, pos);
- // Since auto pairs are currently limited to single chars, we're either
- // inserting exactly one or two chars. When arbitrary length pairs are
- // added, these will need to be changed.
- let change = match next_char {
- Some(_) if !pair.should_close(doc, start_range) => {
- len_inserted = 1;
- let mut tendril = Tendril::new();
- tendril.push(pair.open);
- (cursor, cursor, Some(tendril))
+ let head = pos + offs + open.len_utf8();
+ // if selection, retain anchor, if cursor, move over
+ ranges.push(Range::new(
+ if range.is_empty() {
+ head
+ } else {
+ range.anchor + offs
+ },
+ head,
+ ));
+
+ match next {
+ Some(ch) if !close_before.contains(ch) => {
+ offs += 1;
+ // TODO: else return (use default handler that inserts open)
+ (pos, pos, Some(Tendril::from_char(open)))
}
+ // None | Some(ch) if close_before.contains(ch) => {}
_ => {
// insert open & close
- let pair_str = Tendril::from_iter([pair.open, pair.close]);
- len_inserted = 2;
- (cursor, cursor, Some(pair_str))
- }
- };
-
- let next_range = get_next_range(doc, start_range, offs, len_inserted);
- end_ranges.push(next_range);
- offs += len_inserted;
-
- change
- });
-
- let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
- log::debug!("auto pair transaction: {:#?}", t);
- t
-}
+ let mut pair = Tendril::with_capacity(2);
+ pair.push_char(open);
+ pair.push_char(close);
-fn handle_close(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
- let mut end_ranges = SmallVec::with_capacity(selection.len());
- let mut offs = 0;
-
- let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
- let cursor = start_range.cursor(doc.slice(..));
- let next_char = doc.get_char(cursor);
- let mut len_inserted = 0;
-
- let change = if next_char == Some(pair.close) {
- // return transaction that moves past close
- (cursor, cursor, None) // no-op
- } else {
- len_inserted = 1;
- let mut tendril = Tendril::new();
- tendril.push(pair.close);
- (cursor, cursor, Some(tendril))
- };
-
- let next_range = get_next_range(doc, start_range, offs, len_inserted);
- end_ranges.push(next_range);
- offs += len_inserted;
+ offs += 2;
- change
+ (pos, pos, Some(pair))
+ }
+ }
});
- let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
- log::debug!("auto pair transaction: {:#?}", t);
- t
+ transaction.with_selection(Selection::new(ranges, selection.primary_index()))
}
-/// handle cases where open and close is the same, or in triples ("""docstring""")
-fn handle_same(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
- let mut end_ranges = SmallVec::with_capacity(selection.len());
+fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) -> Transaction {
+ let mut ranges = SmallVec::with_capacity(selection.len());
let mut offs = 0;
- 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 transaction = Transaction::change_by_selection(doc, selection, |range| {
+ let pos = range.head;
+ let next = next_char(doc, pos);
+
+ let head = pos + offs + close.len_utf8();
+ // if selection, retain anchor, if cursor, move over
+ ranges.push(Range::new(
+ if range.is_empty() {
+ head
+ } else {
+ range.anchor + offs
+ },
+ head,
+ ));
- let change = if next_char == Some(pair.open) {
+ if next == Some(close) {
// return transaction that moves past close
- (cursor, cursor, None) // no-op
+ (pos, pos, None) // no-op
} else {
- 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 pair.should_close(doc, start_range) {
- pair_str.push(pair.close);
- }
-
- len_inserted += pair_str.chars().count();
- (cursor, cursor, Some(pair_str))
- };
+ offs += close.len_utf8();
- let next_range = get_next_range(doc, start_range, offs, len_inserted);
- end_ranges.push(next_range);
- offs += len_inserted;
-
- change
+ // TODO: else return (use default handler that inserts close)
+ (pos, pos, Some(Tendril::from_char(close)))
+ }
});
- let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
- log::debug!("auto pair transaction: {:#?}", t);
- t
+ transaction.with_selection(Selection::new(ranges, selection.primary_index()))
+}
+
+// handle cases where open and close is the same, or in triples ("""docstring""")
+fn handle_same(_doc: &Rope, _selection: &Selection, _token: char) -> Option<Transaction> {
+ // if not cursor but selection, wrap
+ // let next = next char
+
+ // if next == bracket {
+ // // if start of syntax node, insert token twice (new pair because node is complete)
+ // // elseif colsedBracketAt
+ // // is_triple == allow triple && next 3 is equal
+ // // cursor jump over
+ // }
+ //} else if allow_triple && followed by triple {
+ //}
+ //} else if next != word char && prev != bracket && prev != word char {
+ // // condition checks for cases like I' where you don't want I'' (or I'm)
+ // insert pair ("")
+ //}
+ None
}