on master help sign in
Implement auto-pairs behavior for open and close.
71999ccBlaž Hrastnik
helix-core/src/auto_pairs.rsnew file+117
@@ -0,0 +1,117 @@
1+use crate::{Range, Rope, Selection, Tendril, Transaction};
2+use smallvec::SmallVec;
3+
4+const PAIRS: &[(char, char)] = &[
5+ ('(', ')'),
6+ ('{', '}'),
7+ ('[', ']'),
8+ ('\'', '\''),
9+ ('"', '"'),
10+ ('`', '`'),
11+];
12+
13+const CLOSE_BEFORE: &str = ")]}'\":;> \n"; // includes space and newline
14+
15+// insert hook:
16+// Fn(doc, selection, char) => Option<Transaction>
17+// problem is, we want to do this per range, so we can call default handler for some ranges
18+// so maybe ret Vec<Option<Change>>
19+// but we also need to be able to return transactions...
20+//
21+// to simplify, maybe return Option<Transaction> and just reimplement the default
22+
23+// TODO: delete implementation where it erases the whole bracket (|) -> |
24+
25+pub fn hook(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> {
26+ for &(open, close) in PAIRS {
27+ if open == ch {
28+ let t = if open == close {
29+ return None;
30+ // handle_same()
31+ } else {
32+ handle_open(doc, selection, open, close, CLOSE_BEFORE)
33+ };
34+ return Some(t);
35+ }
36+
37+ if close == ch {
38+ // && char_at pos == close
39+ return Some(handle_close(doc, selection, open, close));
40+ }
41+ }
42+
43+ None
44+}
45+
46+// TODO: special handling for lifetimes in rust: if preceeded by & or < don't auto close '
47+// for example "&'a mut", or "fn<'a>"
48+
49+fn next_char(doc: &Rope, pos: usize) -> Option<char> {
50+ if pos >= doc.len_chars() {
51+ return None;
52+ }
53+ Some(doc.char(pos))
54+}
55+
56+// TODO: if not cursor but selection, wrap on both sides of selection (surround)
57+fn handle_open(
58+ doc: &Rope,
59+ selection: &Selection,
60+ open: char,
61+ close: char,
62+ close_before: &str,
63+) -> Transaction {
64+ let mut ranges = SmallVec::with_capacity(selection.len());
65+
66+ let mut transaction = Transaction::change_by_selection(doc, selection, |range| {
67+ let pos = range.head;
68+ let next = next_char(doc, pos);
69+
70+ ranges.push(Range::new(range.anchor, pos + 1)); // pos + open
71+
72+ match next {
73+ Some(ch) if !close_before.contains(ch) => {
74+ // TODO: else return (use default handler that inserts open)
75+ (pos, pos, Some(Tendril::from_char(open)))
76+ }
77+ // None | Some(ch) if close_before.contains(ch) => {}
78+ _ => {
79+ // insert open & close
80+ let mut pair = Tendril::with_capacity(2);
81+ pair.push_char(open);
82+ pair.push_char(close);
83+
84+ (pos, pos, Some(pair))
85+ }
86+ }
87+ });
88+
89+ transaction.with_selection(Selection::new(ranges, selection.primary_index()))
90+}
91+
92+fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) -> Transaction {
93+ let mut ranges = SmallVec::with_capacity(selection.len());
94+
95+ let mut transaction = Transaction::change_by_selection(doc, selection, |range| {
96+ let pos = range.head;
97+ let next = next_char(doc, pos);
98+
99+ ranges.push(Range::new(range.anchor, pos + 1)); // pos + close
100+
101+ if next == Some(close) {
102+ // return transaction that moves past close
103+ (pos, pos, None) // no-op
104+ } else {
105+ // TODO: else return (use default handler that inserts close)
106+ (pos, pos, Some(Tendril::from_char(close)))
107+ }
108+ });
109+
110+ transaction.with_selection(Selection::new(ranges, selection.primary_index()))
111+}
112+
113+// handle cases where open and close is the same, or in triples ("""docstring""")
114+fn handle_same() {
115+ // if not cursor but selection, wrap
116+ // let next = next char
117+}
helix-core/src/lib.rs+1
@@ -1,4 +1,5 @@
1#![allow(unused)]
1#![allow(unused)]
2+pub mod auto_pairs;
2pub mod comment;
3pub mod comment;
3pub mod diagnostic;
4pub mod diagnostic;
4pub mod graphemes;
5pub mod graphemes;
helix-core/src/selection.rs+4
@@ -180,6 +180,10 @@ impl Selection {
180 &self.ranges
180 &self.ranges
181 }
181 }
182
182
183+ pub fn primary_index(&self) -> usize {
184+ self.primary_index
185+ }
186+
183 #[must_use]
187 #[must_use]
184 /// Constructs a selection holding a single range.
188 /// Constructs a selection holding a single range.
185 pub fn single(anchor: usize, head: usize) -> Self {
189 pub fn single(anchor: usize, head: usize) -> Self {
helix-term/src/commands.rs+15
@@ -1009,9 +1009,23 @@ pub fn goto_reference(cx: &mut Context) {
1009// NOTE: Transactions in this module get appended to history when we switch back to normal mode.
1009// NOTE: Transactions in this module get appended to history when we switch back to normal mode.
1010pub mod insert {
1010pub mod insert {
1011 use super::*;
1011 use super::*;
1012+ pub type Hook = fn(&Rope, &Selection, char) -> Option<Transaction>;
1013+
1014+ use helix_core::auto_pairs;
1015+ const HOOKS: &[Hook] = &[auto_pairs::hook];
1016+
1012 // TODO: insert means add text just before cursor, on exit we should be on the last letter.
1017 // TODO: insert means add text just before cursor, on exit we should be on the last letter.
1013 pub fn insert_char(cx: &mut Context, c: char) {
1018 pub fn insert_char(cx: &mut Context, c: char) {
1014 let doc = cx.doc();
1019 let doc = cx.doc();
1020+
1021+ // run through insert hooks, stopping on the first one that returns Some(t)
1022+ for hook in HOOKS {
1023+ if let Some(transaction) = hook(doc.text(), doc.selection(), c) {
1024+ doc.apply(&transaction);
1025+ return;
1026+ }
1027+ }
1028+
1015 let c = Tendril::from_char(c);
1029 let c = Tendril::from_char(c);
1016 let transaction = Transaction::insert(doc.text(), doc.selection(), c);
1030 let transaction = Transaction::insert(doc.text(), doc.selection(), c);
1017
1031
@@ -1019,6 +1033,7 @@ pub mod insert {
1019 }
1033 }
1020
1034
1021 pub fn insert_tab(cx: &mut Context) {
1035 pub fn insert_tab(cx: &mut Context) {
1036+ // TODO: tab should insert either \t or indent width spaces
1022 insert_char(cx, '\t');
1037 insert_char(cx, '\t');
1023 }
1038 }
1024
1039