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.rs | 421 |
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 } |