From 71999cce43b3750362cdea534c12cbf320d2677b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= <blaz@mxxn.io>
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 00000000..52a45075
--- /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<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 (|) -> |
+
+pub fn hook(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> {
+    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<char> {
+    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 a742ca13..6b991881 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 91edcf81..2e7104cd 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 54fea453..f7e137e0 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<Transaction>;
+
+    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');
     }