From 71999cce43b3750362cdea534c12cbf320d2677b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Mon, 22 Mar 2021 12:18:48 +0900 Subject: [PATCH] Implement auto-pairs behavior for open and close. --- helix-core/src/auto_pairs.rs | 117 +++++++++++++++++++++++++++++++++++ helix-core/src/lib.rs | 1 + helix-core/src/selection.rs | 4 ++ helix-term/src/commands.rs | 15 +++++ 4 files changed, 137 insertions(+) create mode 100644 helix-core/src/auto_pairs.rs diff --git a/helix-core/src/auto_pairs.rs b/helix-core/src/auto_pairs.rs new file mode 100644 index 000000000..52a45075f --- /dev/null +++ b/helix-core/src/auto_pairs.rs @@ -0,0 +1,117 @@ +use crate::{Range, Rope, Selection, Tendril, Transaction}; +use smallvec::SmallVec; + +const PAIRS: &[(char, char)] = &[ + ('(', ')'), + ('{', '}'), + ('[', ']'), + ('\'', '\''), + ('"', '"'), + ('`', '`'), +]; + +const CLOSE_BEFORE: &str = ")]}'\":;> \n"; // includes space and newline + +// insert hook: +// Fn(doc, selection, char) => Option +// problem is, we want to do this per range, so we can call default handler for some ranges +// so maybe ret Vec> +// but we also need to be able to return transactions... +// +// to simplify, maybe return Option and just reimplement the default + +// TODO: delete implementation where it erases the whole bracket (|) -> | + +pub fn hook(doc: &Rope, selection: &Selection, ch: char) -> Option { + for &(open, close) in PAIRS { + if open == ch { + let t = if open == close { + return None; + // handle_same() + } else { + handle_open(doc, selection, open, close, CLOSE_BEFORE) + }; + return Some(t); + } + + if close == ch { + // && char_at pos == close + return Some(handle_close(doc, selection, open, close)); + } + } + + None +} + +// TODO: special handling for lifetimes in rust: if preceeded by & or < don't auto close ' +// for example "&'a mut", or "fn<'a>" + +fn next_char(doc: &Rope, pos: usize) -> Option { + if pos >= doc.len_chars() { + return None; + } + Some(doc.char(pos)) +} + +// 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()); + + let mut transaction = Transaction::change_by_selection(doc, selection, |range| { + let pos = range.head; + let next = next_char(doc, pos); + + ranges.push(Range::new(range.anchor, pos + 1)); // pos + open + + match next { + Some(ch) if !close_before.contains(ch) => { + // 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 mut pair = Tendril::with_capacity(2); + pair.push_char(open); + pair.push_char(close); + + (pos, pos, Some(pair)) + } + } + }); + + transaction.with_selection(Selection::new(ranges, selection.primary_index())) +} + +fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) -> Transaction { + let mut ranges = SmallVec::with_capacity(selection.len()); + + let mut transaction = Transaction::change_by_selection(doc, selection, |range| { + let pos = range.head; + let next = next_char(doc, pos); + + ranges.push(Range::new(range.anchor, pos + 1)); // pos + close + + if next == Some(close) { + // return transaction that moves past close + (pos, pos, None) // no-op + } else { + // TODO: else return (use default handler that inserts close) + (pos, pos, Some(Tendril::from_char(close))) + } + }); + + 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() { + // if not cursor but selection, wrap + // let next = next char +} diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index a742ca132..6b9918818 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -1,4 +1,5 @@ #![allow(unused)] +pub mod auto_pairs; pub mod comment; pub mod diagnostic; pub mod graphemes; diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index 91edcf815..2e7104cd7 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -180,6 +180,10 @@ impl Selection { &self.ranges } + pub fn primary_index(&self) -> usize { + self.primary_index + } + #[must_use] /// Constructs a selection holding a single range. pub fn single(anchor: usize, head: usize) -> Self { diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 54fea453e..f7e137e0d 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1009,9 +1009,23 @@ pub fn goto_reference(cx: &mut Context) { // NOTE: Transactions in this module get appended to history when we switch back to normal mode. pub mod insert { use super::*; + pub type Hook = fn(&Rope, &Selection, char) -> Option; + + use helix_core::auto_pairs; + const HOOKS: &[Hook] = &[auto_pairs::hook]; + // TODO: insert means add text just before cursor, on exit we should be on the last letter. pub fn insert_char(cx: &mut Context, c: char) { let doc = cx.doc(); + + // run through insert hooks, stopping on the first one that returns Some(t) + for hook in HOOKS { + if let Some(transaction) = hook(doc.text(), doc.selection(), c) { + doc.apply(&transaction); + return; + } + } + let c = Tendril::from_char(c); let transaction = Transaction::insert(doc.text(), doc.selection(), c); @@ -1019,6 +1033,7 @@ pub mod insert { } pub fn insert_tab(cx: &mut Context) { + // TODO: tab should insert either \t or indent width spaces insert_char(cx, '\t'); }