@codemirror/autocomplete 0.19.15 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,21 @@
1
+ ## 0.20.0 (2022-04-20)
2
+
3
+ ### Breaking changes
4
+
5
+ `CompletionResult.span` has been renamed to `validFor`, and may now hold a function as well as a regular expression.
6
+
7
+ ### Bug fixes
8
+
9
+ Remove code that dropped any options beyond the 300th one when matching and sorting option lists.
10
+
11
+ Completion will now apply to all cursors when there are multiple cursors.
12
+
13
+ ### New features
14
+
15
+ `CompletionResult.update` can now be used to implement quick autocompletion updates in a synchronous way.
16
+
17
+ The @codemirror/closebrackets package was merged into this one.
18
+
1
19
  ## 0.19.15 (2022-03-23)
2
20
 
3
21
  ### New features
package/dist/index.cjs CHANGED
@@ -4,9 +4,7 @@ Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var state = require('@codemirror/state');
6
6
  var view = require('@codemirror/view');
7
- var tooltip = require('@codemirror/tooltip');
8
7
  var language = require('@codemirror/language');
9
- var text = require('@codemirror/text');
10
8
 
11
9
  /**
12
10
  An instance of this is passed to completion source functions.
@@ -102,10 +100,10 @@ completes them.
102
100
  */
103
101
  function completeFromList(list) {
104
102
  let options = list.map(o => typeof o == "string" ? { label: o } : o);
105
- let [span, match] = options.every(o => /^\w+$/.test(o.label)) ? [/\w*$/, /\w+$/] : prefixMatch(options);
103
+ let [validFor, match] = options.every(o => /^\w+$/.test(o.label)) ? [/\w*$/, /\w+$/] : prefixMatch(options);
106
104
  return (context) => {
107
105
  let token = context.matchBefore(match);
108
- return token || context.explicit ? { from: token ? token.from : context.pos, options, span } : null;
106
+ return token || context.explicit ? { from: token ? token.from : context.pos, options, validFor } : null;
109
107
  };
110
108
  }
111
109
  /**
@@ -156,12 +154,24 @@ picking a completion.
156
154
  */
157
155
  const pickedCompletion = state.Annotation.define();
158
156
  function applyCompletion(view, option) {
159
- let apply = option.completion.apply || option.completion.label;
157
+ const apply = option.completion.apply || option.completion.label;
160
158
  let result = option.source;
161
159
  if (typeof apply == "string") {
162
- view.dispatch({
163
- changes: { from: result.from, to: result.to, insert: apply },
164
- selection: { anchor: result.from + apply.length },
160
+ view.dispatch(view.state.changeByRange(range => {
161
+ if (range == view.state.selection.main)
162
+ return {
163
+ changes: { from: result.from, to: result.to, insert: apply },
164
+ range: state.EditorSelection.cursor(result.from + apply.length)
165
+ };
166
+ let len = result.to - result.from;
167
+ if (!range.empty ||
168
+ len && view.state.sliceDoc(range.from - len, range.from) != view.state.sliceDoc(result.from, result.to))
169
+ return { range };
170
+ return {
171
+ changes: { from: range.from - len, to: range.from, insert: apply },
172
+ range: state.EditorSelection.cursor(range.from - len + apply.length)
173
+ };
174
+ }), {
165
175
  userEvent: "input.complete",
166
176
  annotations: pickedCompletion.of(option.completion)
167
177
  });
@@ -194,10 +204,10 @@ class FuzzyMatcher {
194
204
  this.precise = [];
195
205
  this.byWord = [];
196
206
  for (let p = 0; p < pattern.length;) {
197
- let char = text.codePointAt(pattern, p), size = text.codePointSize(char);
207
+ let char = state.codePointAt(pattern, p), size = state.codePointSize(char);
198
208
  this.chars.push(char);
199
209
  let part = pattern.slice(p, p + size), upper = part.toUpperCase();
200
- this.folded.push(text.codePointAt(upper == part ? part.toLowerCase() : upper, 0));
210
+ this.folded.push(state.codePointAt(upper == part ? part.toLowerCase() : upper, 0));
201
211
  p += size;
202
212
  }
203
213
  this.astral = pattern.length != this.chars.length;
@@ -218,9 +228,9 @@ class FuzzyMatcher {
218
228
  // For single-character queries, only match when they occur right
219
229
  // at the start
220
230
  if (chars.length == 1) {
221
- let first = text.codePointAt(word, 0);
222
- return first == chars[0] ? [0, 0, text.codePointSize(first)]
223
- : first == folded[0] ? [-200 /* CaseFold */, 0, text.codePointSize(first)] : null;
231
+ let first = state.codePointAt(word, 0);
232
+ return first == chars[0] ? [0, 0, state.codePointSize(first)]
233
+ : first == folded[0] ? [-200 /* CaseFold */, 0, state.codePointSize(first)] : null;
224
234
  }
225
235
  let direct = word.indexOf(this.pattern);
226
236
  if (direct == 0)
@@ -228,10 +238,10 @@ class FuzzyMatcher {
228
238
  let len = chars.length, anyTo = 0;
229
239
  if (direct < 0) {
230
240
  for (let i = 0, e = Math.min(word.length, 200); i < e && anyTo < len;) {
231
- let next = text.codePointAt(word, i);
241
+ let next = state.codePointAt(word, i);
232
242
  if (next == chars[anyTo] || next == folded[anyTo])
233
243
  any[anyTo++] = i;
234
- i += text.codePointSize(next);
244
+ i += state.codePointSize(next);
235
245
  }
236
246
  // No match, exit immediately
237
247
  if (anyTo < len)
@@ -249,7 +259,7 @@ class FuzzyMatcher {
249
259
  let hasLower = /[a-z]/.test(word), wordAdjacent = true;
250
260
  // Go over the option's text, scanning for the various kinds of matches
251
261
  for (let i = 0, e = Math.min(word.length, 200), prevType = 0 /* NonWord */; i < e && byWordTo < len;) {
252
- let next = text.codePointAt(word, i);
262
+ let next = state.codePointAt(word, i);
253
263
  if (direct < 0) {
254
264
  if (preciseTo < len && next == chars[preciseTo])
255
265
  precise[preciseTo++] = i;
@@ -267,7 +277,7 @@ class FuzzyMatcher {
267
277
  }
268
278
  let ch, type = next < 0xff
269
279
  ? (next >= 48 && next <= 57 || next >= 97 && next <= 122 ? 2 /* Lower */ : next >= 65 && next <= 90 ? 1 /* Upper */ : 0 /* NonWord */)
270
- : ((ch = text.fromCodePoint(next)) != ch.toLowerCase() ? 1 /* Upper */ : ch != ch.toUpperCase() ? 2 /* Lower */ : 0 /* NonWord */);
280
+ : ((ch = state.fromCodePoint(next)) != ch.toLowerCase() ? 1 /* Upper */ : ch != ch.toUpperCase() ? 2 /* Lower */ : 0 /* NonWord */);
271
281
  if (!i || type == 1 /* Upper */ && hasLower || prevType == 0 /* NonWord */ && type != 0 /* NonWord */) {
272
282
  if (chars[byWordTo] == next || (folded[byWordTo] == next && (byWordFolded = true)))
273
283
  byWord[byWordTo++] = i;
@@ -275,7 +285,7 @@ class FuzzyMatcher {
275
285
  wordAdjacent = false;
276
286
  }
277
287
  prevType = type;
278
- i += text.codePointSize(next);
288
+ i += state.codePointSize(next);
279
289
  }
280
290
  if (byWordTo == len && byWord[0] == 0 && wordAdjacent)
281
291
  return this.result(-100 /* ByWord */ + (byWordFolded ? -200 /* CaseFold */ : 0), byWord, word);
@@ -293,7 +303,7 @@ class FuzzyMatcher {
293
303
  result(score, positions, word) {
294
304
  let result = [score - word.length], i = 1;
295
305
  for (let pos of positions) {
296
- let to = pos + (this.astral ? text.codePointSize(text.codePointAt(word, pos)) : 1);
306
+ let to = pos + (this.astral ? state.codePointSize(state.codePointAt(word, pos)) : 1);
297
307
  if (i > 1 && result[i - 1] == pos)
298
308
  result[i - 1] = to;
299
309
  else {
@@ -552,7 +562,6 @@ function scrollIntoView(container, element) {
552
562
  container.scrollTop += self.bottom - parent.bottom;
553
563
  }
554
564
 
555
- const MaxOptions = 300;
556
565
  // Used to pick a preferred option when two options with the same
557
566
  // label occur in the result.
558
567
  function score(option) {
@@ -579,10 +588,9 @@ function sortOptions(active, state) {
579
588
  }
580
589
  let result = [], prev = null;
581
590
  for (let opt of options.sort(cmpOption)) {
582
- if (result.length == MaxOptions)
583
- break;
584
591
  if (!prev || prev.label != opt.completion.label || prev.detail != opt.completion.detail ||
585
- prev.type != opt.completion.type || prev.apply != opt.completion.apply)
592
+ (prev.type != null && opt.completion.type != null && prev.type != opt.completion.type) ||
593
+ prev.apply != opt.completion.apply)
586
594
  result.push(opt);
587
595
  else if (score(opt.completion) > score(prev))
588
596
  result[result.length - 1] = opt;
@@ -732,24 +740,27 @@ class ActiveSource {
732
740
  }
733
741
  }
734
742
  class ActiveResult extends ActiveSource {
735
- constructor(source, explicitPos, result, from, to, span) {
743
+ constructor(source, explicitPos, result, from, to) {
736
744
  super(source, 2 /* Result */, explicitPos);
737
745
  this.result = result;
738
746
  this.from = from;
739
747
  this.to = to;
740
- this.span = span;
741
748
  }
742
749
  hasResult() { return true; }
743
750
  handleUserEvent(tr, type, conf) {
751
+ var _a;
744
752
  let from = tr.changes.mapPos(this.from), to = tr.changes.mapPos(this.to, 1);
745
753
  let pos = cur(tr.state);
746
754
  if ((this.explicitPos < 0 ? pos <= from : pos < this.from) ||
747
755
  pos > to ||
748
756
  type == "delete" && cur(tr.startState) == this.from)
749
757
  return new ActiveSource(this.source, type == "input" && conf.activateOnTyping ? 1 /* Pending */ : 0 /* Inactive */);
750
- let explicitPos = this.explicitPos < 0 ? -1 : tr.changes.mapPos(this.explicitPos);
751
- if (this.span && (from == to || this.span.test(tr.state.sliceDoc(from, to))))
752
- return new ActiveResult(this.source, explicitPos, this.result, from, to, this.span);
758
+ let explicitPos = this.explicitPos < 0 ? -1 : tr.changes.mapPos(this.explicitPos), updated;
759
+ if (checkValid(this.result.validFor, tr.state, from, to))
760
+ return new ActiveResult(this.source, explicitPos, this.result, from, to);
761
+ if (this.result.update &&
762
+ (updated = this.result.update(this.result, from, to, new CompletionContext(tr.state, pos, explicitPos >= 0))))
763
+ return new ActiveResult(this.source, explicitPos, updated, updated.from, (_a = updated.to) !== null && _a !== void 0 ? _a : cur(tr.state));
753
764
  return new ActiveSource(this.source, 1 /* Pending */, explicitPos);
754
765
  }
755
766
  handleChange(tr) {
@@ -757,9 +768,15 @@ class ActiveResult extends ActiveSource {
757
768
  }
758
769
  map(mapping) {
759
770
  return mapping.empty ? this :
760
- new ActiveResult(this.source, this.explicitPos < 0 ? -1 : mapping.mapPos(this.explicitPos), this.result, mapping.mapPos(this.from), mapping.mapPos(this.to, 1), this.span);
771
+ new ActiveResult(this.source, this.explicitPos < 0 ? -1 : mapping.mapPos(this.explicitPos), this.result, mapping.mapPos(this.from), mapping.mapPos(this.to, 1));
761
772
  }
762
773
  }
774
+ function checkValid(validFor, state, from, to) {
775
+ if (!validFor)
776
+ return false;
777
+ let text = state.sliceDoc(from, to);
778
+ return typeof validFor == "function" ? validFor(text, from, to, state) : ensureAnchor(validFor, true).test(text);
779
+ }
763
780
  const startCompletionEffect = state.StateEffect.define();
764
781
  const closeCompletionEffect = state.StateEffect.define();
765
782
  const setActiveEffect = state.StateEffect.define({
@@ -770,7 +787,7 @@ const completionState = state.StateField.define({
770
787
  create() { return CompletionState.start(); },
771
788
  update(value, tr) { return value.update(tr); },
772
789
  provide: f => [
773
- tooltip.showTooltip.from(f, val => val.tooltip),
790
+ view.showTooltip.from(f, val => val.tooltip),
774
791
  view.EditorView.contentAttributes.from(f, state => state.attrs)
775
792
  ]
776
793
  });
@@ -781,20 +798,20 @@ Returns a command that moves the completion selection forward or
781
798
  backward by the given amount.
782
799
  */
783
800
  function moveCompletionSelection(forward, by = "option") {
784
- return (view) => {
785
- let cState = view.state.field(completionState, false);
801
+ return (view$1) => {
802
+ let cState = view$1.state.field(completionState, false);
786
803
  if (!cState || !cState.open || Date.now() - cState.open.timestamp < CompletionInteractMargin)
787
804
  return false;
788
- let step = 1, tooltip$1;
789
- if (by == "page" && (tooltip$1 = tooltip.getTooltip(view, cState.open.tooltip)))
790
- step = Math.max(2, Math.floor(tooltip$1.dom.offsetHeight /
791
- tooltip$1.dom.querySelector("li").offsetHeight) - 1);
805
+ let step = 1, tooltip;
806
+ if (by == "page" && (tooltip = view.getTooltip(view$1, cState.open.tooltip)))
807
+ step = Math.max(2, Math.floor(tooltip.dom.offsetHeight /
808
+ tooltip.dom.querySelector("li").offsetHeight) - 1);
792
809
  let selected = cState.open.selected + step * (forward ? 1 : -1), { length } = cState.open.options;
793
810
  if (selected < 0)
794
811
  selected = by == "page" ? 0 : length - 1;
795
812
  else if (selected >= length)
796
813
  selected = by == "page" ? length - 1 : 0;
797
- view.dispatch({ effects: setSelectedEffect.of(selected) });
814
+ view$1.dispatch({ effects: setSelectedEffect.of(selected) });
798
815
  return true;
799
816
  };
800
817
  }
@@ -933,7 +950,7 @@ const completionPlugin = view.ViewPlugin.fromClass(class {
933
950
  continue;
934
951
  this.running.splice(i--, 1);
935
952
  if (query.done) {
936
- let active = new ActiveResult(query.active.source, query.active.explicitPos, query.done, query.done.from, (_a = query.done.to) !== null && _a !== void 0 ? _a : cur(query.updates.length ? query.updates[0].startState : this.view.state), query.done.span && query.done.filter !== false ? ensureAnchor(query.done.span, true) : null);
953
+ let active = new ActiveResult(query.active.source, query.active.explicitPos, query.done, query.done.from, (_a = query.done.to) !== null && _a !== void 0 ? _a : cur(query.updates.length ? query.updates[0].startState : this.view.state));
937
954
  // Replay the transactions that happened since the start of
938
955
  // the request and see if that preserves the result
939
956
  for (let tr of query.updates)
@@ -1206,8 +1223,9 @@ function fieldSelection(ranges, field) {
1206
1223
  return state.EditorSelection.create(ranges.filter(r => r.field == field).map(r => state.EditorSelection.range(r.from, r.to)));
1207
1224
  }
1208
1225
  /**
1209
- Convert a snippet template to a function that can apply it.
1210
- Snippets are written using syntax like this:
1226
+ Convert a snippet template to a function that can
1227
+ [apply](https://codemirror.net/6/docs/ref/#autocomplete.Completion.apply) it. Snippets are written
1228
+ using syntax like this:
1211
1229
 
1212
1230
  "for (let ${index} = 0; ${index} < ${end}; ${index}++) {\n\t${}\n}"
1213
1231
 
@@ -1390,8 +1408,238 @@ const completeAnyWord = context => {
1390
1408
  return null;
1391
1409
  let from = token ? token.from : context.pos;
1392
1410
  let options = collectWords(context.state.doc, wordCache(wordChars), re, 50000 /* Range */, from);
1393
- return { from, options, span: mapRE(re, s => "^" + s) };
1411
+ return { from, options, validFor: mapRE(re, s => "^" + s) };
1412
+ };
1413
+
1414
+ const defaults = {
1415
+ brackets: ["(", "[", "{", "'", '"'],
1416
+ before: ")]}:;>"
1417
+ };
1418
+ const closeBracketEffect = state.StateEffect.define({
1419
+ map(value, mapping) {
1420
+ let mapped = mapping.mapPos(value, -1, state.MapMode.TrackAfter);
1421
+ return mapped == null ? undefined : mapped;
1422
+ }
1423
+ });
1424
+ const skipBracketEffect = state.StateEffect.define({
1425
+ map(value, mapping) { return mapping.mapPos(value); }
1426
+ });
1427
+ const closedBracket = new class extends state.RangeValue {
1394
1428
  };
1429
+ closedBracket.startSide = 1;
1430
+ closedBracket.endSide = -1;
1431
+ const bracketState = state.StateField.define({
1432
+ create() { return state.RangeSet.empty; },
1433
+ update(value, tr) {
1434
+ if (tr.selection) {
1435
+ let lineStart = tr.state.doc.lineAt(tr.selection.main.head).from;
1436
+ let prevLineStart = tr.startState.doc.lineAt(tr.startState.selection.main.head).from;
1437
+ if (lineStart != tr.changes.mapPos(prevLineStart, -1))
1438
+ value = state.RangeSet.empty;
1439
+ }
1440
+ value = value.map(tr.changes);
1441
+ for (let effect of tr.effects) {
1442
+ if (effect.is(closeBracketEffect))
1443
+ value = value.update({ add: [closedBracket.range(effect.value, effect.value + 1)] });
1444
+ else if (effect.is(skipBracketEffect))
1445
+ value = value.update({ filter: from => from != effect.value });
1446
+ }
1447
+ return value;
1448
+ }
1449
+ });
1450
+ /**
1451
+ Extension to enable bracket-closing behavior. When a closeable
1452
+ bracket is typed, its closing bracket is immediately inserted
1453
+ after the cursor. When closing a bracket directly in front of a
1454
+ closing bracket inserted by the extension, the cursor moves over
1455
+ that bracket.
1456
+ */
1457
+ function closeBrackets() {
1458
+ return [inputHandler, bracketState];
1459
+ }
1460
+ const definedClosing = "()[]{}<>";
1461
+ function closing(ch) {
1462
+ for (let i = 0; i < definedClosing.length; i += 2)
1463
+ if (definedClosing.charCodeAt(i) == ch)
1464
+ return definedClosing.charAt(i + 1);
1465
+ return state.fromCodePoint(ch < 128 ? ch : ch + 1);
1466
+ }
1467
+ function config(state, pos) {
1468
+ return state.languageDataAt("closeBrackets", pos)[0] || defaults;
1469
+ }
1470
+ const android = typeof navigator == "object" && /Android\b/.test(navigator.userAgent);
1471
+ const inputHandler = view.EditorView.inputHandler.of((view, from, to, insert) => {
1472
+ if ((android ? view.composing : view.compositionStarted) || view.state.readOnly)
1473
+ return false;
1474
+ let sel = view.state.selection.main;
1475
+ if (insert.length > 2 || insert.length == 2 && state.codePointSize(state.codePointAt(insert, 0)) == 1 ||
1476
+ from != sel.from || to != sel.to)
1477
+ return false;
1478
+ let tr = insertBracket(view.state, insert);
1479
+ if (!tr)
1480
+ return false;
1481
+ view.dispatch(tr);
1482
+ return true;
1483
+ });
1484
+ /**
1485
+ Command that implements deleting a pair of matching brackets when
1486
+ the cursor is between them.
1487
+ */
1488
+ const deleteBracketPair = ({ state: state$1, dispatch }) => {
1489
+ if (state$1.readOnly)
1490
+ return false;
1491
+ let conf = config(state$1, state$1.selection.main.head);
1492
+ let tokens = conf.brackets || defaults.brackets;
1493
+ let dont = null, changes = state$1.changeByRange(range => {
1494
+ if (range.empty) {
1495
+ let before = prevChar(state$1.doc, range.head);
1496
+ for (let token of tokens) {
1497
+ if (token == before && nextChar(state$1.doc, range.head) == closing(state.codePointAt(token, 0)))
1498
+ return { changes: { from: range.head - token.length, to: range.head + token.length },
1499
+ range: state.EditorSelection.cursor(range.head - token.length),
1500
+ userEvent: "delete.backward" };
1501
+ }
1502
+ }
1503
+ return { range: dont = range };
1504
+ });
1505
+ if (!dont)
1506
+ dispatch(state$1.update(changes, { scrollIntoView: true }));
1507
+ return !dont;
1508
+ };
1509
+ /**
1510
+ Close-brackets related key bindings. Binds Backspace to
1511
+ [`deleteBracketPair`](https://codemirror.net/6/docs/ref/#autocomplete.deleteBracketPair).
1512
+ */
1513
+ const closeBracketsKeymap = [
1514
+ { key: "Backspace", run: deleteBracketPair }
1515
+ ];
1516
+ /**
1517
+ Implements the extension's behavior on text insertion. If the
1518
+ given string counts as a bracket in the language around the
1519
+ selection, and replacing the selection with it requires custom
1520
+ behavior (inserting a closing version or skipping past a
1521
+ previously-closed bracket), this function returns a transaction
1522
+ representing that custom behavior. (You only need this if you want
1523
+ to programmatically insert brackets—the
1524
+ [`closeBrackets`](https://codemirror.net/6/docs/ref/#autocomplete.closeBrackets) extension will
1525
+ take care of running this for user input.)
1526
+ */
1527
+ function insertBracket(state$1, bracket) {
1528
+ let conf = config(state$1, state$1.selection.main.head);
1529
+ let tokens = conf.brackets || defaults.brackets;
1530
+ for (let tok of tokens) {
1531
+ let closed = closing(state.codePointAt(tok, 0));
1532
+ if (bracket == tok)
1533
+ return closed == tok ? handleSame(state$1, tok, tokens.indexOf(tok + tok + tok) > -1)
1534
+ : handleOpen(state$1, tok, closed, conf.before || defaults.before);
1535
+ if (bracket == closed && closedBracketAt(state$1, state$1.selection.main.from))
1536
+ return handleClose(state$1, tok, closed);
1537
+ }
1538
+ return null;
1539
+ }
1540
+ function closedBracketAt(state, pos) {
1541
+ let found = false;
1542
+ state.field(bracketState).between(0, state.doc.length, from => {
1543
+ if (from == pos)
1544
+ found = true;
1545
+ });
1546
+ return found;
1547
+ }
1548
+ function nextChar(doc, pos) {
1549
+ let next = doc.sliceString(pos, pos + 2);
1550
+ return next.slice(0, state.codePointSize(state.codePointAt(next, 0)));
1551
+ }
1552
+ function prevChar(doc, pos) {
1553
+ let prev = doc.sliceString(pos - 2, pos);
1554
+ return state.codePointSize(state.codePointAt(prev, 0)) == prev.length ? prev : prev.slice(1);
1555
+ }
1556
+ function handleOpen(state$1, open, close, closeBefore) {
1557
+ let dont = null, changes = state$1.changeByRange(range => {
1558
+ if (!range.empty)
1559
+ return { changes: [{ insert: open, from: range.from }, { insert: close, from: range.to }],
1560
+ effects: closeBracketEffect.of(range.to + open.length),
1561
+ range: state.EditorSelection.range(range.anchor + open.length, range.head + open.length) };
1562
+ let next = nextChar(state$1.doc, range.head);
1563
+ if (!next || /\s/.test(next) || closeBefore.indexOf(next) > -1)
1564
+ return { changes: { insert: open + close, from: range.head },
1565
+ effects: closeBracketEffect.of(range.head + open.length),
1566
+ range: state.EditorSelection.cursor(range.head + open.length) };
1567
+ return { range: dont = range };
1568
+ });
1569
+ return dont ? null : state$1.update(changes, {
1570
+ scrollIntoView: true,
1571
+ userEvent: "input.type"
1572
+ });
1573
+ }
1574
+ function handleClose(state$1, _open, close) {
1575
+ let dont = null, moved = state$1.selection.ranges.map(range => {
1576
+ if (range.empty && nextChar(state$1.doc, range.head) == close)
1577
+ return state.EditorSelection.cursor(range.head + close.length);
1578
+ return dont = range;
1579
+ });
1580
+ return dont ? null : state$1.update({
1581
+ selection: state.EditorSelection.create(moved, state$1.selection.mainIndex),
1582
+ scrollIntoView: true,
1583
+ effects: state$1.selection.ranges.map(({ from }) => skipBracketEffect.of(from))
1584
+ });
1585
+ }
1586
+ // Handles cases where the open and close token are the same, and
1587
+ // possibly triple quotes (as in `"""abc"""`-style quoting).
1588
+ function handleSame(state$1, token, allowTriple) {
1589
+ let dont = null, changes = state$1.changeByRange(range => {
1590
+ if (!range.empty)
1591
+ return { changes: [{ insert: token, from: range.from }, { insert: token, from: range.to }],
1592
+ effects: closeBracketEffect.of(range.to + token.length),
1593
+ range: state.EditorSelection.range(range.anchor + token.length, range.head + token.length) };
1594
+ let pos = range.head, next = nextChar(state$1.doc, pos);
1595
+ if (next == token) {
1596
+ if (nodeStart(state$1, pos)) {
1597
+ return { changes: { insert: token + token, from: pos },
1598
+ effects: closeBracketEffect.of(pos + token.length),
1599
+ range: state.EditorSelection.cursor(pos + token.length) };
1600
+ }
1601
+ else if (closedBracketAt(state$1, pos)) {
1602
+ let isTriple = allowTriple && state$1.sliceDoc(pos, pos + token.length * 3) == token + token + token;
1603
+ return { range: state.EditorSelection.cursor(pos + token.length * (isTriple ? 3 : 1)),
1604
+ effects: skipBracketEffect.of(pos) };
1605
+ }
1606
+ }
1607
+ else if (allowTriple && state$1.sliceDoc(pos - 2 * token.length, pos) == token + token &&
1608
+ nodeStart(state$1, pos - 2 * token.length)) {
1609
+ return { changes: { insert: token + token + token + token, from: pos },
1610
+ effects: closeBracketEffect.of(pos + token.length),
1611
+ range: state.EditorSelection.cursor(pos + token.length) };
1612
+ }
1613
+ else if (state$1.charCategorizer(pos)(next) != state.CharCategory.Word) {
1614
+ let prev = state$1.sliceDoc(pos - 1, pos);
1615
+ if (prev != token && state$1.charCategorizer(pos)(prev) != state.CharCategory.Word && !probablyInString(state$1, pos, token))
1616
+ return { changes: { insert: token + token, from: pos },
1617
+ effects: closeBracketEffect.of(pos + token.length),
1618
+ range: state.EditorSelection.cursor(pos + token.length) };
1619
+ }
1620
+ return { range: dont = range };
1621
+ });
1622
+ return dont ? null : state$1.update(changes, {
1623
+ scrollIntoView: true,
1624
+ userEvent: "input.type"
1625
+ });
1626
+ }
1627
+ function nodeStart(state, pos) {
1628
+ let tree = language.syntaxTree(state).resolveInner(pos + 1);
1629
+ return tree.parent && tree.from == pos;
1630
+ }
1631
+ function probablyInString(state, pos, quoteToken) {
1632
+ let node = language.syntaxTree(state).resolveInner(pos, -1);
1633
+ for (let i = 0; i < 5; i++) {
1634
+ if (state.sliceDoc(node.from, node.from + quoteToken.length) == quoteToken)
1635
+ return true;
1636
+ let parent = node.to == pos && node.parent;
1637
+ if (!parent)
1638
+ break;
1639
+ node = parent;
1640
+ }
1641
+ return false;
1642
+ }
1395
1643
 
1396
1644
  /**
1397
1645
  Returns an extension that enables autocompletion.
@@ -1480,14 +1728,18 @@ exports.CompletionContext = CompletionContext;
1480
1728
  exports.acceptCompletion = acceptCompletion;
1481
1729
  exports.autocompletion = autocompletion;
1482
1730
  exports.clearSnippet = clearSnippet;
1731
+ exports.closeBrackets = closeBrackets;
1732
+ exports.closeBracketsKeymap = closeBracketsKeymap;
1483
1733
  exports.closeCompletion = closeCompletion;
1484
1734
  exports.completeAnyWord = completeAnyWord;
1485
1735
  exports.completeFromList = completeFromList;
1486
1736
  exports.completionKeymap = completionKeymap;
1487
1737
  exports.completionStatus = completionStatus;
1488
1738
  exports.currentCompletions = currentCompletions;
1739
+ exports.deleteBracketPair = deleteBracketPair;
1489
1740
  exports.ifIn = ifIn;
1490
1741
  exports.ifNotIn = ifNotIn;
1742
+ exports.insertBracket = insertBracket;
1491
1743
  exports.moveCompletionSelection = moveCompletionSelection;
1492
1744
  exports.nextSnippetField = nextSnippetField;
1493
1745
  exports.pickedCompletion = pickedCompletion;
package/dist/index.d.ts CHANGED
@@ -52,8 +52,7 @@ interface CompletionConfig {
52
52
  completion, and should produce a DOM node to show. `position`
53
53
  determines where in the DOM the result appears, relative to
54
54
  other added widgets and the standard content. The default icons
55
- have position 20, the label position 50, and the detail position
56
- 70.
55
+ have position 20, the label position 50, and the detail position 70.
57
56
  */
58
57
  addToOptions?: {
59
58
  render: (completion: Completion, state: EditorState) => Node | null;
@@ -226,23 +225,32 @@ interface CompletionResult {
226
225
  */
227
226
  options: readonly Completion[];
228
227
  /**
229
- When given, further input that causes the part of the document
230
- between ([mapped](https://codemirror.net/6/docs/ref/#state.ChangeDesc.mapPos)) `from` and `to` to
231
- match this regular expression will not query the completion
232
- source again, but continue with this list of options. This can
233
- help a lot with responsiveness, since it allows the completion
234
- list to be updated synchronously.
228
+ When given, further typing or deletion that causes the part of
229
+ the document between ([mapped](https://codemirror.net/6/docs/ref/#state.ChangeDesc.mapPos)) `from`
230
+ and `to` to match this regular expression or predicate function
231
+ will not query the completion source again, but continue with
232
+ this list of options. This can help a lot with responsiveness,
233
+ since it allows the completion list to be updated synchronously.
235
234
  */
236
- span?: RegExp;
235
+ validFor?: RegExp | ((text: string, from: number, to: number, state: EditorState) => boolean);
237
236
  /**
238
237
  By default, the library filters and scores completions. Set
239
238
  `filter` to `false` to disable this, and cause your completions
240
239
  to all be included, in the order they were given. When there are
241
240
  other sources, unfiltered completions appear at the top of the
242
- list of completions. `span` must not be given when `filter` is
243
- `false`, because it only works when filtering.
241
+ list of completions. `validFor` must not be given when `filter`
242
+ is `false`, because it only works when filtering.
244
243
  */
245
244
  filter?: boolean;
245
+ /**
246
+ Synchronously update the completion result after typing or
247
+ deletion. If given, this should not do any expensive work, since
248
+ it will be called during editor state updates. The function
249
+ should make sure (similar to
250
+ [`validFor`](https://codemirror.net/6/docs/ref/#autocomplete.CompletionResult.validFor)) that the
251
+ completion still applies in the new state.
252
+ */
253
+ update?: (current: CompletionResult, from: number, to: number, context: CompletionContext) => CompletionResult | null;
246
254
  }
247
255
  /**
248
256
  This annotation is added to transactions that are produced by
@@ -251,8 +259,9 @@ picking a completion.
251
259
  declare const pickedCompletion: _codemirror_state.AnnotationType<Completion>;
252
260
 
253
261
  /**
254
- Convert a snippet template to a function that can apply it.
255
- Snippets are written using syntax like this:
262
+ Convert a snippet template to a function that can
263
+ [apply](https://codemirror.net/6/docs/ref/#autocomplete.Completion.apply) it. Snippets are written
264
+ using syntax like this:
256
265
 
257
266
  "for (let ${index} = 0; ${index} < ${end}; ${index}++) {\n\t${}\n}"
258
267
 
@@ -331,6 +340,56 @@ return those as completions.
331
340
  */
332
341
  declare const completeAnyWord: CompletionSource;
333
342
 
343
+ /**
344
+ Configures bracket closing behavior for a syntax (via
345
+ [language data](https://codemirror.net/6/docs/ref/#state.EditorState.languageDataAt)) using the `"closeBrackets"`
346
+ identifier.
347
+ */
348
+ interface CloseBracketConfig {
349
+ /**
350
+ The opening brackets to close. Defaults to `["(", "[", "{", "'",
351
+ '"']`. Brackets may be single characters or a triple of quotes
352
+ (as in `"''''"`).
353
+ */
354
+ brackets?: string[];
355
+ /**
356
+ Characters in front of which newly opened brackets are
357
+ automatically closed. Closing always happens in front of
358
+ whitespace. Defaults to `")]}:;>"`.
359
+ */
360
+ before?: string;
361
+ }
362
+ /**
363
+ Extension to enable bracket-closing behavior. When a closeable
364
+ bracket is typed, its closing bracket is immediately inserted
365
+ after the cursor. When closing a bracket directly in front of a
366
+ closing bracket inserted by the extension, the cursor moves over
367
+ that bracket.
368
+ */
369
+ declare function closeBrackets(): Extension;
370
+ /**
371
+ Command that implements deleting a pair of matching brackets when
372
+ the cursor is between them.
373
+ */
374
+ declare const deleteBracketPair: StateCommand;
375
+ /**
376
+ Close-brackets related key bindings. Binds Backspace to
377
+ [`deleteBracketPair`](https://codemirror.net/6/docs/ref/#autocomplete.deleteBracketPair).
378
+ */
379
+ declare const closeBracketsKeymap: readonly KeyBinding[];
380
+ /**
381
+ Implements the extension's behavior on text insertion. If the
382
+ given string counts as a bracket in the language around the
383
+ selection, and replacing the selection with it requires custom
384
+ behavior (inserting a closing version or skipping past a
385
+ previously-closed bracket), this function returns a transaction
386
+ representing that custom behavior. (You only need this if you want
387
+ to programmatically insert brackets—the
388
+ [`closeBrackets`](https://codemirror.net/6/docs/ref/#autocomplete.closeBrackets) extension will
389
+ take care of running this for user input.)
390
+ */
391
+ declare function insertBracket(state: EditorState, bracket: string): Transaction | null;
392
+
334
393
  /**
335
394
  Returns an extension that enables autocompletion.
336
395
  */
@@ -373,4 +432,4 @@ the currently selected completion.
373
432
  */
374
433
  declare function setSelectedCompletion(index: number): StateEffect<unknown>;
375
434
 
376
- export { Completion, CompletionContext, CompletionResult, CompletionSource, acceptCompletion, autocompletion, clearSnippet, closeCompletion, completeAnyWord, completeFromList, completionKeymap, completionStatus, currentCompletions, ifIn, ifNotIn, moveCompletionSelection, nextSnippetField, pickedCompletion, prevSnippetField, selectedCompletion, selectedCompletionIndex, setSelectedCompletion, snippet, snippetCompletion, snippetKeymap, startCompletion };
435
+ export { CloseBracketConfig, Completion, CompletionContext, CompletionResult, CompletionSource, acceptCompletion, autocompletion, clearSnippet, closeBrackets, closeBracketsKeymap, closeCompletion, completeAnyWord, completeFromList, completionKeymap, completionStatus, currentCompletions, deleteBracketPair, ifIn, ifNotIn, insertBracket, moveCompletionSelection, nextSnippetField, pickedCompletion, prevSnippetField, selectedCompletion, selectedCompletionIndex, setSelectedCompletion, snippet, snippetCompletion, snippetKeymap, startCompletion };
package/dist/index.js CHANGED
@@ -1,8 +1,6 @@
1
- import { Annotation, Facet, combineConfig, StateEffect, StateField, Prec, EditorSelection, Text, MapMode } from '@codemirror/state';
2
- import { logException, Direction, EditorView, ViewPlugin, Decoration, WidgetType, keymap } from '@codemirror/view';
3
- import { showTooltip, getTooltip } from '@codemirror/tooltip';
1
+ import { Annotation, EditorSelection, codePointAt, codePointSize, fromCodePoint, Facet, combineConfig, StateEffect, StateField, Prec, Text, MapMode, RangeValue, RangeSet, CharCategory } from '@codemirror/state';
2
+ import { logException, Direction, showTooltip, EditorView, ViewPlugin, getTooltip, Decoration, WidgetType, keymap } from '@codemirror/view';
4
3
  import { syntaxTree, indentUnit } from '@codemirror/language';
5
- import { codePointAt, codePointSize, fromCodePoint } from '@codemirror/text';
6
4
 
7
5
  /**
8
6
  An instance of this is passed to completion source functions.
@@ -98,10 +96,10 @@ completes them.
98
96
  */
99
97
  function completeFromList(list) {
100
98
  let options = list.map(o => typeof o == "string" ? { label: o } : o);
101
- let [span, match] = options.every(o => /^\w+$/.test(o.label)) ? [/\w*$/, /\w+$/] : prefixMatch(options);
99
+ let [validFor, match] = options.every(o => /^\w+$/.test(o.label)) ? [/\w*$/, /\w+$/] : prefixMatch(options);
102
100
  return (context) => {
103
101
  let token = context.matchBefore(match);
104
- return token || context.explicit ? { from: token ? token.from : context.pos, options, span } : null;
102
+ return token || context.explicit ? { from: token ? token.from : context.pos, options, validFor } : null;
105
103
  };
106
104
  }
107
105
  /**
@@ -152,12 +150,24 @@ picking a completion.
152
150
  */
153
151
  const pickedCompletion = /*@__PURE__*/Annotation.define();
154
152
  function applyCompletion(view, option) {
155
- let apply = option.completion.apply || option.completion.label;
153
+ const apply = option.completion.apply || option.completion.label;
156
154
  let result = option.source;
157
155
  if (typeof apply == "string") {
158
- view.dispatch({
159
- changes: { from: result.from, to: result.to, insert: apply },
160
- selection: { anchor: result.from + apply.length },
156
+ view.dispatch(view.state.changeByRange(range => {
157
+ if (range == view.state.selection.main)
158
+ return {
159
+ changes: { from: result.from, to: result.to, insert: apply },
160
+ range: EditorSelection.cursor(result.from + apply.length)
161
+ };
162
+ let len = result.to - result.from;
163
+ if (!range.empty ||
164
+ len && view.state.sliceDoc(range.from - len, range.from) != view.state.sliceDoc(result.from, result.to))
165
+ return { range };
166
+ return {
167
+ changes: { from: range.from - len, to: range.from, insert: apply },
168
+ range: EditorSelection.cursor(range.from - len + apply.length)
169
+ };
170
+ }), {
161
171
  userEvent: "input.complete",
162
172
  annotations: pickedCompletion.of(option.completion)
163
173
  });
@@ -548,7 +558,6 @@ function scrollIntoView(container, element) {
548
558
  container.scrollTop += self.bottom - parent.bottom;
549
559
  }
550
560
 
551
- const MaxOptions = 300;
552
561
  // Used to pick a preferred option when two options with the same
553
562
  // label occur in the result.
554
563
  function score(option) {
@@ -575,10 +584,9 @@ function sortOptions(active, state) {
575
584
  }
576
585
  let result = [], prev = null;
577
586
  for (let opt of options.sort(cmpOption)) {
578
- if (result.length == MaxOptions)
579
- break;
580
587
  if (!prev || prev.label != opt.completion.label || prev.detail != opt.completion.detail ||
581
- prev.type != opt.completion.type || prev.apply != opt.completion.apply)
588
+ (prev.type != null && opt.completion.type != null && prev.type != opt.completion.type) ||
589
+ prev.apply != opt.completion.apply)
582
590
  result.push(opt);
583
591
  else if (score(opt.completion) > score(prev))
584
592
  result[result.length - 1] = opt;
@@ -728,24 +736,27 @@ class ActiveSource {
728
736
  }
729
737
  }
730
738
  class ActiveResult extends ActiveSource {
731
- constructor(source, explicitPos, result, from, to, span) {
739
+ constructor(source, explicitPos, result, from, to) {
732
740
  super(source, 2 /* Result */, explicitPos);
733
741
  this.result = result;
734
742
  this.from = from;
735
743
  this.to = to;
736
- this.span = span;
737
744
  }
738
745
  hasResult() { return true; }
739
746
  handleUserEvent(tr, type, conf) {
747
+ var _a;
740
748
  let from = tr.changes.mapPos(this.from), to = tr.changes.mapPos(this.to, 1);
741
749
  let pos = cur(tr.state);
742
750
  if ((this.explicitPos < 0 ? pos <= from : pos < this.from) ||
743
751
  pos > to ||
744
752
  type == "delete" && cur(tr.startState) == this.from)
745
753
  return new ActiveSource(this.source, type == "input" && conf.activateOnTyping ? 1 /* Pending */ : 0 /* Inactive */);
746
- let explicitPos = this.explicitPos < 0 ? -1 : tr.changes.mapPos(this.explicitPos);
747
- if (this.span && (from == to || this.span.test(tr.state.sliceDoc(from, to))))
748
- return new ActiveResult(this.source, explicitPos, this.result, from, to, this.span);
754
+ let explicitPos = this.explicitPos < 0 ? -1 : tr.changes.mapPos(this.explicitPos), updated;
755
+ if (checkValid(this.result.validFor, tr.state, from, to))
756
+ return new ActiveResult(this.source, explicitPos, this.result, from, to);
757
+ if (this.result.update &&
758
+ (updated = this.result.update(this.result, from, to, new CompletionContext(tr.state, pos, explicitPos >= 0))))
759
+ return new ActiveResult(this.source, explicitPos, updated, updated.from, (_a = updated.to) !== null && _a !== void 0 ? _a : cur(tr.state));
749
760
  return new ActiveSource(this.source, 1 /* Pending */, explicitPos);
750
761
  }
751
762
  handleChange(tr) {
@@ -753,9 +764,15 @@ class ActiveResult extends ActiveSource {
753
764
  }
754
765
  map(mapping) {
755
766
  return mapping.empty ? this :
756
- new ActiveResult(this.source, this.explicitPos < 0 ? -1 : mapping.mapPos(this.explicitPos), this.result, mapping.mapPos(this.from), mapping.mapPos(this.to, 1), this.span);
767
+ new ActiveResult(this.source, this.explicitPos < 0 ? -1 : mapping.mapPos(this.explicitPos), this.result, mapping.mapPos(this.from), mapping.mapPos(this.to, 1));
757
768
  }
758
769
  }
770
+ function checkValid(validFor, state, from, to) {
771
+ if (!validFor)
772
+ return false;
773
+ let text = state.sliceDoc(from, to);
774
+ return typeof validFor == "function" ? validFor(text, from, to, state) : ensureAnchor(validFor, true).test(text);
775
+ }
759
776
  const startCompletionEffect = /*@__PURE__*/StateEffect.define();
760
777
  const closeCompletionEffect = /*@__PURE__*/StateEffect.define();
761
778
  const setActiveEffect = /*@__PURE__*/StateEffect.define({
@@ -929,7 +946,7 @@ const completionPlugin = /*@__PURE__*/ViewPlugin.fromClass(class {
929
946
  continue;
930
947
  this.running.splice(i--, 1);
931
948
  if (query.done) {
932
- let active = new ActiveResult(query.active.source, query.active.explicitPos, query.done, query.done.from, (_a = query.done.to) !== null && _a !== void 0 ? _a : cur(query.updates.length ? query.updates[0].startState : this.view.state), query.done.span && query.done.filter !== false ? ensureAnchor(query.done.span, true) : null);
949
+ let active = new ActiveResult(query.active.source, query.active.explicitPos, query.done, query.done.from, (_a = query.done.to) !== null && _a !== void 0 ? _a : cur(query.updates.length ? query.updates[0].startState : this.view.state));
933
950
  // Replay the transactions that happened since the start of
934
951
  // the request and see if that preserves the result
935
952
  for (let tr of query.updates)
@@ -1202,8 +1219,9 @@ function fieldSelection(ranges, field) {
1202
1219
  return EditorSelection.create(ranges.filter(r => r.field == field).map(r => EditorSelection.range(r.from, r.to)));
1203
1220
  }
1204
1221
  /**
1205
- Convert a snippet template to a function that can apply it.
1206
- Snippets are written using syntax like this:
1222
+ Convert a snippet template to a function that can
1223
+ [apply](https://codemirror.net/6/docs/ref/#autocomplete.Completion.apply) it. Snippets are written
1224
+ using syntax like this:
1207
1225
 
1208
1226
  "for (let ${index} = 0; ${index} < ${end}; ${index}++) {\n\t${}\n}"
1209
1227
 
@@ -1386,8 +1404,238 @@ const completeAnyWord = context => {
1386
1404
  return null;
1387
1405
  let from = token ? token.from : context.pos;
1388
1406
  let options = collectWords(context.state.doc, wordCache(wordChars), re, 50000 /* Range */, from);
1389
- return { from, options, span: mapRE(re, s => "^" + s) };
1407
+ return { from, options, validFor: mapRE(re, s => "^" + s) };
1408
+ };
1409
+
1410
+ const defaults = {
1411
+ brackets: ["(", "[", "{", "'", '"'],
1412
+ before: ")]}:;>"
1413
+ };
1414
+ const closeBracketEffect = /*@__PURE__*/StateEffect.define({
1415
+ map(value, mapping) {
1416
+ let mapped = mapping.mapPos(value, -1, MapMode.TrackAfter);
1417
+ return mapped == null ? undefined : mapped;
1418
+ }
1419
+ });
1420
+ const skipBracketEffect = /*@__PURE__*/StateEffect.define({
1421
+ map(value, mapping) { return mapping.mapPos(value); }
1422
+ });
1423
+ const closedBracket = /*@__PURE__*/new class extends RangeValue {
1390
1424
  };
1425
+ closedBracket.startSide = 1;
1426
+ closedBracket.endSide = -1;
1427
+ const bracketState = /*@__PURE__*/StateField.define({
1428
+ create() { return RangeSet.empty; },
1429
+ update(value, tr) {
1430
+ if (tr.selection) {
1431
+ let lineStart = tr.state.doc.lineAt(tr.selection.main.head).from;
1432
+ let prevLineStart = tr.startState.doc.lineAt(tr.startState.selection.main.head).from;
1433
+ if (lineStart != tr.changes.mapPos(prevLineStart, -1))
1434
+ value = RangeSet.empty;
1435
+ }
1436
+ value = value.map(tr.changes);
1437
+ for (let effect of tr.effects) {
1438
+ if (effect.is(closeBracketEffect))
1439
+ value = value.update({ add: [closedBracket.range(effect.value, effect.value + 1)] });
1440
+ else if (effect.is(skipBracketEffect))
1441
+ value = value.update({ filter: from => from != effect.value });
1442
+ }
1443
+ return value;
1444
+ }
1445
+ });
1446
+ /**
1447
+ Extension to enable bracket-closing behavior. When a closeable
1448
+ bracket is typed, its closing bracket is immediately inserted
1449
+ after the cursor. When closing a bracket directly in front of a
1450
+ closing bracket inserted by the extension, the cursor moves over
1451
+ that bracket.
1452
+ */
1453
+ function closeBrackets() {
1454
+ return [inputHandler, bracketState];
1455
+ }
1456
+ const definedClosing = "()[]{}<>";
1457
+ function closing(ch) {
1458
+ for (let i = 0; i < definedClosing.length; i += 2)
1459
+ if (definedClosing.charCodeAt(i) == ch)
1460
+ return definedClosing.charAt(i + 1);
1461
+ return fromCodePoint(ch < 128 ? ch : ch + 1);
1462
+ }
1463
+ function config(state, pos) {
1464
+ return state.languageDataAt("closeBrackets", pos)[0] || defaults;
1465
+ }
1466
+ const android = typeof navigator == "object" && /*@__PURE__*//Android\b/.test(navigator.userAgent);
1467
+ const inputHandler = /*@__PURE__*/EditorView.inputHandler.of((view, from, to, insert) => {
1468
+ if ((android ? view.composing : view.compositionStarted) || view.state.readOnly)
1469
+ return false;
1470
+ let sel = view.state.selection.main;
1471
+ if (insert.length > 2 || insert.length == 2 && codePointSize(codePointAt(insert, 0)) == 1 ||
1472
+ from != sel.from || to != sel.to)
1473
+ return false;
1474
+ let tr = insertBracket(view.state, insert);
1475
+ if (!tr)
1476
+ return false;
1477
+ view.dispatch(tr);
1478
+ return true;
1479
+ });
1480
+ /**
1481
+ Command that implements deleting a pair of matching brackets when
1482
+ the cursor is between them.
1483
+ */
1484
+ const deleteBracketPair = ({ state, dispatch }) => {
1485
+ if (state.readOnly)
1486
+ return false;
1487
+ let conf = config(state, state.selection.main.head);
1488
+ let tokens = conf.brackets || defaults.brackets;
1489
+ let dont = null, changes = state.changeByRange(range => {
1490
+ if (range.empty) {
1491
+ let before = prevChar(state.doc, range.head);
1492
+ for (let token of tokens) {
1493
+ if (token == before && nextChar(state.doc, range.head) == closing(codePointAt(token, 0)))
1494
+ return { changes: { from: range.head - token.length, to: range.head + token.length },
1495
+ range: EditorSelection.cursor(range.head - token.length),
1496
+ userEvent: "delete.backward" };
1497
+ }
1498
+ }
1499
+ return { range: dont = range };
1500
+ });
1501
+ if (!dont)
1502
+ dispatch(state.update(changes, { scrollIntoView: true }));
1503
+ return !dont;
1504
+ };
1505
+ /**
1506
+ Close-brackets related key bindings. Binds Backspace to
1507
+ [`deleteBracketPair`](https://codemirror.net/6/docs/ref/#autocomplete.deleteBracketPair).
1508
+ */
1509
+ const closeBracketsKeymap = [
1510
+ { key: "Backspace", run: deleteBracketPair }
1511
+ ];
1512
+ /**
1513
+ Implements the extension's behavior on text insertion. If the
1514
+ given string counts as a bracket in the language around the
1515
+ selection, and replacing the selection with it requires custom
1516
+ behavior (inserting a closing version or skipping past a
1517
+ previously-closed bracket), this function returns a transaction
1518
+ representing that custom behavior. (You only need this if you want
1519
+ to programmatically insert brackets—the
1520
+ [`closeBrackets`](https://codemirror.net/6/docs/ref/#autocomplete.closeBrackets) extension will
1521
+ take care of running this for user input.)
1522
+ */
1523
+ function insertBracket(state, bracket) {
1524
+ let conf = config(state, state.selection.main.head);
1525
+ let tokens = conf.brackets || defaults.brackets;
1526
+ for (let tok of tokens) {
1527
+ let closed = closing(codePointAt(tok, 0));
1528
+ if (bracket == tok)
1529
+ return closed == tok ? handleSame(state, tok, tokens.indexOf(tok + tok + tok) > -1)
1530
+ : handleOpen(state, tok, closed, conf.before || defaults.before);
1531
+ if (bracket == closed && closedBracketAt(state, state.selection.main.from))
1532
+ return handleClose(state, tok, closed);
1533
+ }
1534
+ return null;
1535
+ }
1536
+ function closedBracketAt(state, pos) {
1537
+ let found = false;
1538
+ state.field(bracketState).between(0, state.doc.length, from => {
1539
+ if (from == pos)
1540
+ found = true;
1541
+ });
1542
+ return found;
1543
+ }
1544
+ function nextChar(doc, pos) {
1545
+ let next = doc.sliceString(pos, pos + 2);
1546
+ return next.slice(0, codePointSize(codePointAt(next, 0)));
1547
+ }
1548
+ function prevChar(doc, pos) {
1549
+ let prev = doc.sliceString(pos - 2, pos);
1550
+ return codePointSize(codePointAt(prev, 0)) == prev.length ? prev : prev.slice(1);
1551
+ }
1552
+ function handleOpen(state, open, close, closeBefore) {
1553
+ let dont = null, changes = state.changeByRange(range => {
1554
+ if (!range.empty)
1555
+ return { changes: [{ insert: open, from: range.from }, { insert: close, from: range.to }],
1556
+ effects: closeBracketEffect.of(range.to + open.length),
1557
+ range: EditorSelection.range(range.anchor + open.length, range.head + open.length) };
1558
+ let next = nextChar(state.doc, range.head);
1559
+ if (!next || /\s/.test(next) || closeBefore.indexOf(next) > -1)
1560
+ return { changes: { insert: open + close, from: range.head },
1561
+ effects: closeBracketEffect.of(range.head + open.length),
1562
+ range: EditorSelection.cursor(range.head + open.length) };
1563
+ return { range: dont = range };
1564
+ });
1565
+ return dont ? null : state.update(changes, {
1566
+ scrollIntoView: true,
1567
+ userEvent: "input.type"
1568
+ });
1569
+ }
1570
+ function handleClose(state, _open, close) {
1571
+ let dont = null, moved = state.selection.ranges.map(range => {
1572
+ if (range.empty && nextChar(state.doc, range.head) == close)
1573
+ return EditorSelection.cursor(range.head + close.length);
1574
+ return dont = range;
1575
+ });
1576
+ return dont ? null : state.update({
1577
+ selection: EditorSelection.create(moved, state.selection.mainIndex),
1578
+ scrollIntoView: true,
1579
+ effects: state.selection.ranges.map(({ from }) => skipBracketEffect.of(from))
1580
+ });
1581
+ }
1582
+ // Handles cases where the open and close token are the same, and
1583
+ // possibly triple quotes (as in `"""abc"""`-style quoting).
1584
+ function handleSame(state, token, allowTriple) {
1585
+ let dont = null, changes = state.changeByRange(range => {
1586
+ if (!range.empty)
1587
+ return { changes: [{ insert: token, from: range.from }, { insert: token, from: range.to }],
1588
+ effects: closeBracketEffect.of(range.to + token.length),
1589
+ range: EditorSelection.range(range.anchor + token.length, range.head + token.length) };
1590
+ let pos = range.head, next = nextChar(state.doc, pos);
1591
+ if (next == token) {
1592
+ if (nodeStart(state, pos)) {
1593
+ return { changes: { insert: token + token, from: pos },
1594
+ effects: closeBracketEffect.of(pos + token.length),
1595
+ range: EditorSelection.cursor(pos + token.length) };
1596
+ }
1597
+ else if (closedBracketAt(state, pos)) {
1598
+ let isTriple = allowTriple && state.sliceDoc(pos, pos + token.length * 3) == token + token + token;
1599
+ return { range: EditorSelection.cursor(pos + token.length * (isTriple ? 3 : 1)),
1600
+ effects: skipBracketEffect.of(pos) };
1601
+ }
1602
+ }
1603
+ else if (allowTriple && state.sliceDoc(pos - 2 * token.length, pos) == token + token &&
1604
+ nodeStart(state, pos - 2 * token.length)) {
1605
+ return { changes: { insert: token + token + token + token, from: pos },
1606
+ effects: closeBracketEffect.of(pos + token.length),
1607
+ range: EditorSelection.cursor(pos + token.length) };
1608
+ }
1609
+ else if (state.charCategorizer(pos)(next) != CharCategory.Word) {
1610
+ let prev = state.sliceDoc(pos - 1, pos);
1611
+ if (prev != token && state.charCategorizer(pos)(prev) != CharCategory.Word && !probablyInString(state, pos, token))
1612
+ return { changes: { insert: token + token, from: pos },
1613
+ effects: closeBracketEffect.of(pos + token.length),
1614
+ range: EditorSelection.cursor(pos + token.length) };
1615
+ }
1616
+ return { range: dont = range };
1617
+ });
1618
+ return dont ? null : state.update(changes, {
1619
+ scrollIntoView: true,
1620
+ userEvent: "input.type"
1621
+ });
1622
+ }
1623
+ function nodeStart(state, pos) {
1624
+ let tree = syntaxTree(state).resolveInner(pos + 1);
1625
+ return tree.parent && tree.from == pos;
1626
+ }
1627
+ function probablyInString(state, pos, quoteToken) {
1628
+ let node = syntaxTree(state).resolveInner(pos, -1);
1629
+ for (let i = 0; i < 5; i++) {
1630
+ if (state.sliceDoc(node.from, node.from + quoteToken.length) == quoteToken)
1631
+ return true;
1632
+ let parent = node.to == pos && node.parent;
1633
+ if (!parent)
1634
+ break;
1635
+ node = parent;
1636
+ }
1637
+ return false;
1638
+ }
1391
1639
 
1392
1640
  /**
1393
1641
  Returns an extension that enables autocompletion.
@@ -1472,4 +1720,4 @@ function setSelectedCompletion(index) {
1472
1720
  return setSelectedEffect.of(index);
1473
1721
  }
1474
1722
 
1475
- export { CompletionContext, acceptCompletion, autocompletion, clearSnippet, closeCompletion, completeAnyWord, completeFromList, completionKeymap, completionStatus, currentCompletions, ifIn, ifNotIn, moveCompletionSelection, nextSnippetField, pickedCompletion, prevSnippetField, selectedCompletion, selectedCompletionIndex, setSelectedCompletion, snippet, snippetCompletion, snippetKeymap, startCompletion };
1723
+ export { CompletionContext, acceptCompletion, autocompletion, clearSnippet, closeBrackets, closeBracketsKeymap, closeCompletion, completeAnyWord, completeFromList, completionKeymap, completionStatus, currentCompletions, deleteBracketPair, ifIn, ifNotIn, insertBracket, moveCompletionSelection, nextSnippetField, pickedCompletion, prevSnippetField, selectedCompletion, selectedCompletionIndex, setSelectedCompletion, snippet, snippetCompletion, snippetKeymap, startCompletion };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemirror/autocomplete",
3
- "version": "0.19.15",
3
+ "version": "0.20.0",
4
4
  "description": "Autocompletion for the CodeMirror code editor",
5
5
  "scripts": {
6
6
  "test": "cm-runtests",
@@ -26,12 +26,10 @@
26
26
  "sideEffects": false,
27
27
  "license": "MIT",
28
28
  "dependencies": {
29
- "@codemirror/language": "^0.19.0",
30
- "@codemirror/state": "^0.19.4",
31
- "@codemirror/text": "^0.19.2",
32
- "@codemirror/tooltip": "^0.19.12",
33
- "@codemirror/view": "^0.19.0",
34
- "@lezer/common": "^0.15.0"
29
+ "@codemirror/language": "^0.20.0",
30
+ "@codemirror/state": "^0.20.0",
31
+ "@codemirror/view": "^0.20.0",
32
+ "@lezer/common": "^0.16.0"
35
33
  },
36
34
  "devDependencies": {
37
35
  "@codemirror/buildhelper": "^0.1.5"