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.rs308
1 files changed, 176 insertions, 132 deletions
diff --git a/helix-core/src/auto_pairs.rs b/helix-core/src/auto_pairs.rs
index 85329040..5811186c 100644
--- a/helix-core/src/auto_pairs.rs
+++ b/helix-core/src/auto_pairs.rs
@@ -1,11 +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 crate::{graphemes, movement::Direction, Change, Deletion, Range, Rope, Tendril};
use std::collections::HashMap;
-use smallvec::SmallVec;
-
// Heavily based on https://github.com/codemirror/closebrackets/
pub const DEFAULT_PAIRS: &[(char, char)] = &[
('(', ')'),
@@ -106,37 +104,128 @@ impl Default for AutoPairs {
}
}
-// insert hook:
-// Fn(doc, selection, char) => Option<Transaction>
-// problem is, we want to do this per range, so we can call default handler for some ranges
-// so maybe ret Vec<Option<Change>>
-// but we also need to be able to return transactions...
-//
-// 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 {% %}
#[must_use]
-pub fn hook(doc: &Rope, selection: &Selection, ch: char, pairs: &AutoPairs) -> Option<Transaction> {
- log::trace!("autopairs hook selection: {:#?}", selection);
+pub fn hook_insert(
+ doc: &Rope,
+ range: &Range,
+ ch: char,
+ pairs: &AutoPairs,
+) -> Option<(Change, Range)> {
+ log::trace!("autopairs hook range: {:#?}", range);
if let Some(pair) = pairs.get(ch) {
if pair.same() {
- return Some(handle_same(doc, selection, pair));
+ return handle_insert_same(doc, range, pair);
} else if pair.open == ch {
- return Some(handle_open(doc, selection, pair));
+ return handle_insert_open(doc, range, pair);
} else if pair.close == ch {
// && char_at pos == close
- return Some(handle_close(doc, selection, pair));
+ return handle_insert_close(doc, range, pair);
}
+ } else if ch.is_whitespace() {
+ return handle_insert_whitespace(doc, range, ch, pairs);
}
None
}
+#[must_use]
+pub fn hook_delete(doc: &Rope, range: &Range, pairs: &AutoPairs) -> Option<(Deletion, Range)> {
+ log::trace!("autopairs delete hook range: {:#?}", range);
+
+ let text = doc.slice(..);
+ let cursor = range.cursor(text);
+
+ let cur = doc.get_char(cursor)?;
+ let prev = prev_char(doc, cursor)?;
+
+ // check for whitespace surrounding a pair
+ if doc.len_chars() >= 4 && prev.is_whitespace() && cur.is_whitespace() {
+ let second_prev = doc.get_char(graphemes::nth_prev_grapheme_boundary(text, cursor, 2))?;
+ let second_next = doc.get_char(graphemes::next_grapheme_boundary(text, cursor))?;
+ log::debug!("second_prev: {}, second_next: {}", second_prev, second_next);
+
+ if let Some(pair) = pairs.get(second_prev) {
+ if pair.open == second_prev && pair.close == second_next {
+ return handle_delete(doc, range);
+ }
+ }
+ }
+
+ let pair = pairs.get(cur)?;
+
+ if pair.open != prev || pair.close != cur {
+ return None;
+ }
+
+ handle_delete(doc, range)
+}
+
+pub fn handle_delete(doc: &Rope, range: &Range) -> Option<(Deletion, Range)> {
+ let text = doc.slice(..);
+ let cursor = range.cursor(text);
+
+ let end_next = graphemes::next_grapheme_boundary(text, cursor);
+ let end_prev = graphemes::prev_grapheme_boundary(text, cursor);
+
+ let delete = (end_prev, end_next);
+ let size_delete = end_next - end_prev;
+ let next_head = graphemes::next_grapheme_boundary(text, range.head) - size_delete;
+
+ // if the range is a single grapheme cursor, we do not want to shrink the
+ // range, just move it, so we only subtract the size of the closing pair char
+ let next_anchor = match (range.direction(), range.is_single_grapheme(text)) {
+ // single grapheme forward needs to move, but only the width of the
+ // character under the cursor, which is the closer
+ (Direction::Forward, true) => range.anchor - (end_next - cursor),
+ (Direction::Backward, true) => range.anchor - (cursor - end_prev),
+
+ (Direction::Forward, false) => range.anchor,
+ (Direction::Backward, false) => range.anchor - size_delete,
+ };
+
+ let next_range = Range::new(next_anchor, next_head);
+
+ log::trace!(
+ "auto pair delete: {:?}, range: {:?}, next_range: {:?}, text len: {}",
+ delete,
+ range,
+ next_range,
+ text.len_chars()
+ );
+
+ Some((delete, next_range))
+}
+
+fn handle_insert_whitespace(
+ doc: &Rope,
+ range: &Range,
+ ch: char,
+ pairs: &AutoPairs,
+) -> Option<(Change, Range)> {
+ let text = doc.slice(..);
+ let cursor = range.cursor(text);
+ let cur = doc.get_char(cursor)?;
+ let prev = prev_char(doc, cursor)?;
+ let pair = pairs.get(cur)?;
+
+ if pair.open != prev || pair.close != cur {
+ return None;
+ }
+
+ let whitespace_pair = Pair {
+ open: ch,
+ close: ch,
+ };
+
+ handle_insert_same(doc, range, &whitespace_pair)
+}
+
fn prev_char(doc: &Rope, pos: usize) -> Option<char> {
if pos == 0 {
return None;
@@ -146,7 +235,7 @@ fn prev_char(doc: &Rope, pos: usize) -> Option<char> {
}
/// 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 {
+fn get_next_range(doc: &Rope, start_range: &Range, 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.
@@ -165,10 +254,7 @@ fn get_next_range(doc: &Rope, start_range: &Range, offset: usize, len_inserted:
// 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,
- );
+ return Range::new(start_range.anchor + 1, start_range.head + 1);
}
let doc_slice = doc.slice(..);
@@ -177,7 +263,7 @@ fn get_next_range(doc: &Rope, start_range: &Range, offset: usize, len_inserted:
// just skip over graphemes
if len_inserted == 0 {
let end_anchor = if single_grapheme {
- graphemes::next_grapheme_boundary(doc_slice, start_range.anchor) + offset
+ graphemes::next_grapheme_boundary(doc_slice, start_range.anchor)
// even for backward inserts with multiple grapheme selections,
// we want the anchor to stay where it is so that the relative
@@ -185,42 +271,38 @@ fn get_next_range(doc: &Rope, start_range: &Range, offset: usize, len_inserted:
//
// foo([) wor]d -> insert ) -> foo()[ wor]d
} else {
- start_range.anchor + offset
+ start_range.anchor
};
return Range::new(
end_anchor,
- graphemes::next_grapheme_boundary(doc_slice, start_range.head) + offset,
+ graphemes::next_grapheme_boundary(doc_slice, start_range.head),
);
}
// 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
+ start_range.anchor + 1
} else {
- start_range.anchor + offset
+ start_range.anchor
};
- return Range::new(end_anchor, start_range.head + offset + 1);
+ return Range::new(end_anchor, start_range.head + 1);
}
// 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
+ start_range.head + 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
+ log::trace!("prev_bound: {}, len_inserted: {}", prev_bound, len_inserted);
+
+ prev_bound + len_inserted
};
let end_anchor = match (start_range.len(), start_range.direction()) {
@@ -239,7 +321,7 @@ fn get_next_range(doc: &Rope, start_range: &Range, offset: usize, len_inserted:
// if we are appending, the anchor stays where it is; only offset
// for multiple range insertions
} else {
- start_range.anchor + offset
+ start_range.anchor
}
}
@@ -248,13 +330,11 @@ fn get_next_range(doc: &Rope, start_range: &Range, offset: usize, len_inserted:
// 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
+ graphemes::prev_grapheme_boundary(doc.slice(..), start_range.anchor) + len_inserted
} 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
+ start_range.anchor + len_inserted
}
}
};
@@ -262,112 +342,76 @@ fn get_next_range(doc: &Rope, start_range: &Range, offset: usize, len_inserted:
Range::new(end_anchor, end_head)
}
-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;
-
- // 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))
- }
- _ => {
- // insert open & close
- let pair_str = Tendril::from_iter([pair.open, pair.close]);
- len_inserted = 2;
- (cursor, cursor, Some(pair_str))
- }
- };
+fn handle_insert_open(doc: &Rope, range: &Range, pair: &Pair) -> Option<(Change, Range)> {
+ let cursor = range.cursor(doc.slice(..));
+ let next_char = doc.get_char(cursor);
+ let len_inserted;
+
+ // 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, range) => {
+ return None;
+ }
+ _ => {
+ // 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;
+ let next_range = get_next_range(doc, range, len_inserted);
+ let result = (change, next_range);
- change
- });
+ log::debug!("auto pair change: {:#?}", &result);
- let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
- log::debug!("auto pair transaction: {:#?}", t);
- t
+ Some(result)
}
-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;
+fn handle_insert_close(doc: &Rope, range: &Range, pair: &Pair) -> Option<(Change, Range)> {
+ let cursor = range.cursor(doc.slice(..));
+ let next_char = doc.get_char(cursor);
- 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 change = if next_char == Some(pair.close) {
+ // return transaction that moves past close
+ (cursor, cursor, None) // no-op
+ } else {
+ return None;
+ };
- let next_range = get_next_range(doc, start_range, offs, len_inserted);
- end_ranges.push(next_range);
- offs += len_inserted;
+ let next_range = get_next_range(doc, range, 0);
+ let result = (change, next_range);
- change
- });
+ log::debug!("auto pair change: {:#?}", &result);
- let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
- log::debug!("auto pair transaction: {:#?}", t);
- t
+ Some(result)
}
/// 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());
-
- 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 change = if next_char == Some(pair.open) {
- // return transaction that moves past close
- (cursor, cursor, 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);
- }
+fn handle_insert_same(doc: &Rope, range: &Range, pair: &Pair) -> Option<(Change, Range)> {
+ let cursor = range.cursor(doc.slice(..));
+ let mut len_inserted = 0;
+ let next_char = doc.get_char(cursor);
+
+ let change = if next_char == Some(pair.open) {
+ // return transaction that moves past close
+ (cursor, cursor, None) // no-op
+ } else {
+ if !pair.should_close(doc, range) {
+ return None;
+ }
- len_inserted += pair_str.chars().count();
- (cursor, cursor, Some(pair_str))
- };
+ 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;
+ let next_range = get_next_range(doc, range, len_inserted);
+ let result = (change, next_range);
- change
- });
+ log::debug!("auto pair change: {:#?}", &result);
- let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
- log::debug!("auto pair transaction: {:#?}", t);
- t
+ Some(result)
}