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 | 308 |
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) } |