@codemirror/autocomplete 0.19.15 → 0.20.2

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,35 @@
1
+ ## 0.20.2 (2022-05-24)
2
+
3
+ ### New features
4
+
5
+ The package now exports an `insertCompletionText` helper that implements the default behavior for applying a completion.
6
+
7
+ ## 0.20.1 (2022-05-16)
8
+
9
+ ### New features
10
+
11
+ The new `closeOnBlur` option determines whether the completion tooltip is closed when the editor loses focus.
12
+
13
+ `CompletionResult` objects with `filter: false` may now have a `getMatch` property that determines the matched range in the options.
14
+
15
+ ## 0.20.0 (2022-04-20)
16
+
17
+ ### Breaking changes
18
+
19
+ `CompletionResult.span` has been renamed to `validFor`, and may now hold a function as well as a regular expression.
20
+
21
+ ### Bug fixes
22
+
23
+ Remove code that dropped any options beyond the 300th one when matching and sorting option lists.
24
+
25
+ Completion will now apply to all cursors when there are multiple cursors.
26
+
27
+ ### New features
28
+
29
+ `CompletionResult.update` can now be used to implement quick autocompletion updates in a synchronous way.
30
+
31
+ The @codemirror/closebrackets package was merged into this one.
32
+
1
33
  ## 0.19.15 (2022-03-23)
2
34
 
3
35
  ### 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
  /**
@@ -155,20 +153,35 @@ This annotation is added to transactions that are produced by
155
153
  picking a completion.
156
154
  */
157
155
  const pickedCompletion = state.Annotation.define();
156
+ /**
157
+ Helper function that returns a transaction spec which inserts a
158
+ completion's text in the main selection range, and any other
159
+ selection range that has the same text in front of it.
160
+ */
161
+ function insertCompletionText(state$1, text, from, to) {
162
+ return state$1.changeByRange(range => {
163
+ if (range == state$1.selection.main)
164
+ return {
165
+ changes: { from: from, to: to, insert: text },
166
+ range: state.EditorSelection.cursor(from + text.length)
167
+ };
168
+ let len = to - from;
169
+ if (!range.empty ||
170
+ len && state$1.sliceDoc(range.from - len, range.from) != state$1.sliceDoc(from, to))
171
+ return { range };
172
+ return {
173
+ changes: { from: range.from - len, to: range.from, insert: text },
174
+ range: state.EditorSelection.cursor(range.from - len + text.length)
175
+ };
176
+ });
177
+ }
158
178
  function applyCompletion(view, option) {
159
- let apply = option.completion.apply || option.completion.label;
179
+ const apply = option.completion.apply || option.completion.label;
160
180
  let result = option.source;
161
- if (typeof apply == "string") {
162
- view.dispatch({
163
- changes: { from: result.from, to: result.to, insert: apply },
164
- selection: { anchor: result.from + apply.length },
165
- userEvent: "input.complete",
166
- annotations: pickedCompletion.of(option.completion)
167
- });
168
- }
169
- else {
181
+ if (typeof apply == "string")
182
+ view.dispatch(insertCompletionText(view.state, apply, result.from, result.to));
183
+ else
170
184
  apply(view, option.completion, result.from, result.to);
171
- }
172
185
  }
173
186
  const SourceCache = new WeakMap();
174
187
  function asSource(source) {
@@ -194,10 +207,10 @@ class FuzzyMatcher {
194
207
  this.precise = [];
195
208
  this.byWord = [];
196
209
  for (let p = 0; p < pattern.length;) {
197
- let char = text.codePointAt(pattern, p), size = text.codePointSize(char);
210
+ let char = state.codePointAt(pattern, p), size = state.codePointSize(char);
198
211
  this.chars.push(char);
199
212
  let part = pattern.slice(p, p + size), upper = part.toUpperCase();
200
- this.folded.push(text.codePointAt(upper == part ? part.toLowerCase() : upper, 0));
213
+ this.folded.push(state.codePointAt(upper == part ? part.toLowerCase() : upper, 0));
201
214
  p += size;
202
215
  }
203
216
  this.astral = pattern.length != this.chars.length;
@@ -218,9 +231,9 @@ class FuzzyMatcher {
218
231
  // For single-character queries, only match when they occur right
219
232
  // at the start
220
233
  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;
234
+ let first = state.codePointAt(word, 0);
235
+ return first == chars[0] ? [0, 0, state.codePointSize(first)]
236
+ : first == folded[0] ? [-200 /* CaseFold */, 0, state.codePointSize(first)] : null;
224
237
  }
225
238
  let direct = word.indexOf(this.pattern);
226
239
  if (direct == 0)
@@ -228,10 +241,10 @@ class FuzzyMatcher {
228
241
  let len = chars.length, anyTo = 0;
229
242
  if (direct < 0) {
230
243
  for (let i = 0, e = Math.min(word.length, 200); i < e && anyTo < len;) {
231
- let next = text.codePointAt(word, i);
244
+ let next = state.codePointAt(word, i);
232
245
  if (next == chars[anyTo] || next == folded[anyTo])
233
246
  any[anyTo++] = i;
234
- i += text.codePointSize(next);
247
+ i += state.codePointSize(next);
235
248
  }
236
249
  // No match, exit immediately
237
250
  if (anyTo < len)
@@ -249,7 +262,7 @@ class FuzzyMatcher {
249
262
  let hasLower = /[a-z]/.test(word), wordAdjacent = true;
250
263
  // Go over the option's text, scanning for the various kinds of matches
251
264
  for (let i = 0, e = Math.min(word.length, 200), prevType = 0 /* NonWord */; i < e && byWordTo < len;) {
252
- let next = text.codePointAt(word, i);
265
+ let next = state.codePointAt(word, i);
253
266
  if (direct < 0) {
254
267
  if (preciseTo < len && next == chars[preciseTo])
255
268
  precise[preciseTo++] = i;
@@ -267,7 +280,7 @@ class FuzzyMatcher {
267
280
  }
268
281
  let ch, type = next < 0xff
269
282
  ? (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 */);
283
+ : ((ch = state.fromCodePoint(next)) != ch.toLowerCase() ? 1 /* Upper */ : ch != ch.toUpperCase() ? 2 /* Lower */ : 0 /* NonWord */);
271
284
  if (!i || type == 1 /* Upper */ && hasLower || prevType == 0 /* NonWord */ && type != 0 /* NonWord */) {
272
285
  if (chars[byWordTo] == next || (folded[byWordTo] == next && (byWordFolded = true)))
273
286
  byWord[byWordTo++] = i;
@@ -275,7 +288,7 @@ class FuzzyMatcher {
275
288
  wordAdjacent = false;
276
289
  }
277
290
  prevType = type;
278
- i += text.codePointSize(next);
291
+ i += state.codePointSize(next);
279
292
  }
280
293
  if (byWordTo == len && byWord[0] == 0 && wordAdjacent)
281
294
  return this.result(-100 /* ByWord */ + (byWordFolded ? -200 /* CaseFold */ : 0), byWord, word);
@@ -293,7 +306,7 @@ class FuzzyMatcher {
293
306
  result(score, positions, word) {
294
307
  let result = [score - word.length], i = 1;
295
308
  for (let pos of positions) {
296
- let to = pos + (this.astral ? text.codePointSize(text.codePointAt(word, pos)) : 1);
309
+ let to = pos + (this.astral ? state.codePointSize(state.codePointAt(word, pos)) : 1);
297
310
  if (i > 1 && result[i - 1] == pos)
298
311
  result[i - 1] = to;
299
312
  else {
@@ -310,6 +323,7 @@ const completionConfig = state.Facet.define({
310
323
  return state.combineConfig(configs, {
311
324
  activateOnTyping: true,
312
325
  override: null,
326
+ closeOnBlur: true,
313
327
  maxRenderedOptions: 100,
314
328
  defaultKeymap: true,
315
329
  optionClass: () => "",
@@ -318,6 +332,7 @@ const completionConfig = state.Facet.define({
318
332
  addToOptions: []
319
333
  }, {
320
334
  defaultKeymap: (a, b) => a && b,
335
+ closeOnBlur: (a, b) => a && b,
321
336
  icons: (a, b) => a && b,
322
337
  optionClass: (a, b) => c => joinClass(a(c), b(c)),
323
338
  addToOptions: (a, b) => a.concat(b)
@@ -552,7 +567,6 @@ function scrollIntoView(container, element) {
552
567
  container.scrollTop += self.bottom - parent.bottom;
553
568
  }
554
569
 
555
- const MaxOptions = 300;
556
570
  // Used to pick a preferred option when two options with the same
557
571
  // label occur in the result.
558
572
  function score(option) {
@@ -564,8 +578,14 @@ function sortOptions(active, state) {
564
578
  for (let a of active)
565
579
  if (a.hasResult()) {
566
580
  if (a.result.filter === false) {
567
- for (let option of a.result.options)
568
- options.push(new Option(option, a, [1e9 - i++]));
581
+ let getMatch = a.result.getMatch;
582
+ for (let option of a.result.options) {
583
+ let match = [1e9 - i++];
584
+ if (getMatch)
585
+ for (let n of getMatch(option))
586
+ match.push(n);
587
+ options.push(new Option(option, a, match));
588
+ }
569
589
  }
570
590
  else {
571
591
  let matcher = new FuzzyMatcher(state.sliceDoc(a.from, a.to)), match;
@@ -579,10 +599,9 @@ function sortOptions(active, state) {
579
599
  }
580
600
  let result = [], prev = null;
581
601
  for (let opt of options.sort(cmpOption)) {
582
- if (result.length == MaxOptions)
583
- break;
584
602
  if (!prev || prev.label != opt.completion.label || prev.detail != opt.completion.detail ||
585
- prev.type != opt.completion.type || prev.apply != opt.completion.apply)
603
+ (prev.type != null && opt.completion.type != null && prev.type != opt.completion.type) ||
604
+ prev.apply != opt.completion.apply)
586
605
  result.push(opt);
587
606
  else if (score(opt.completion) > score(prev))
588
607
  result[result.length - 1] = opt;
@@ -732,24 +751,27 @@ class ActiveSource {
732
751
  }
733
752
  }
734
753
  class ActiveResult extends ActiveSource {
735
- constructor(source, explicitPos, result, from, to, span) {
754
+ constructor(source, explicitPos, result, from, to) {
736
755
  super(source, 2 /* Result */, explicitPos);
737
756
  this.result = result;
738
757
  this.from = from;
739
758
  this.to = to;
740
- this.span = span;
741
759
  }
742
760
  hasResult() { return true; }
743
761
  handleUserEvent(tr, type, conf) {
762
+ var _a;
744
763
  let from = tr.changes.mapPos(this.from), to = tr.changes.mapPos(this.to, 1);
745
764
  let pos = cur(tr.state);
746
765
  if ((this.explicitPos < 0 ? pos <= from : pos < this.from) ||
747
766
  pos > to ||
748
767
  type == "delete" && cur(tr.startState) == this.from)
749
768
  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);
769
+ let explicitPos = this.explicitPos < 0 ? -1 : tr.changes.mapPos(this.explicitPos), updated;
770
+ if (checkValid(this.result.validFor, tr.state, from, to))
771
+ return new ActiveResult(this.source, explicitPos, this.result, from, to);
772
+ if (this.result.update &&
773
+ (updated = this.result.update(this.result, from, to, new CompletionContext(tr.state, pos, explicitPos >= 0))))
774
+ return new ActiveResult(this.source, explicitPos, updated, updated.from, (_a = updated.to) !== null && _a !== void 0 ? _a : cur(tr.state));
753
775
  return new ActiveSource(this.source, 1 /* Pending */, explicitPos);
754
776
  }
755
777
  handleChange(tr) {
@@ -757,9 +779,15 @@ class ActiveResult extends ActiveSource {
757
779
  }
758
780
  map(mapping) {
759
781
  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);
782
+ new ActiveResult(this.source, this.explicitPos < 0 ? -1 : mapping.mapPos(this.explicitPos), this.result, mapping.mapPos(this.from), mapping.mapPos(this.to, 1));
761
783
  }
762
784
  }
785
+ function checkValid(validFor, state, from, to) {
786
+ if (!validFor)
787
+ return false;
788
+ let text = state.sliceDoc(from, to);
789
+ return typeof validFor == "function" ? validFor(text, from, to, state) : ensureAnchor(validFor, true).test(text);
790
+ }
763
791
  const startCompletionEffect = state.StateEffect.define();
764
792
  const closeCompletionEffect = state.StateEffect.define();
765
793
  const setActiveEffect = state.StateEffect.define({
@@ -770,7 +798,7 @@ const completionState = state.StateField.define({
770
798
  create() { return CompletionState.start(); },
771
799
  update(value, tr) { return value.update(tr); },
772
800
  provide: f => [
773
- tooltip.showTooltip.from(f, val => val.tooltip),
801
+ view.showTooltip.from(f, val => val.tooltip),
774
802
  view.EditorView.contentAttributes.from(f, state => state.attrs)
775
803
  ]
776
804
  });
@@ -781,20 +809,20 @@ Returns a command that moves the completion selection forward or
781
809
  backward by the given amount.
782
810
  */
783
811
  function moveCompletionSelection(forward, by = "option") {
784
- return (view) => {
785
- let cState = view.state.field(completionState, false);
812
+ return (view$1) => {
813
+ let cState = view$1.state.field(completionState, false);
786
814
  if (!cState || !cState.open || Date.now() - cState.open.timestamp < CompletionInteractMargin)
787
815
  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);
816
+ let step = 1, tooltip;
817
+ if (by == "page" && (tooltip = view.getTooltip(view$1, cState.open.tooltip)))
818
+ step = Math.max(2, Math.floor(tooltip.dom.offsetHeight /
819
+ tooltip.dom.querySelector("li").offsetHeight) - 1);
792
820
  let selected = cState.open.selected + step * (forward ? 1 : -1), { length } = cState.open.options;
793
821
  if (selected < 0)
794
822
  selected = by == "page" ? 0 : length - 1;
795
823
  else if (selected >= length)
796
824
  selected = by == "page" ? length - 1 : 0;
797
- view.dispatch({ effects: setSelectedEffect.of(selected) });
825
+ view$1.dispatch({ effects: setSelectedEffect.of(selected) });
798
826
  return true;
799
827
  };
800
828
  }
@@ -933,7 +961,7 @@ const completionPlugin = view.ViewPlugin.fromClass(class {
933
961
  continue;
934
962
  this.running.splice(i--, 1);
935
963
  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);
964
+ 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
965
  // Replay the transactions that happened since the start of
938
966
  // the request and see if that preserves the result
939
967
  for (let tr of query.updates)
@@ -965,6 +993,11 @@ const completionPlugin = view.ViewPlugin.fromClass(class {
965
993
  }
966
994
  }, {
967
995
  eventHandlers: {
996
+ blur() {
997
+ let state = this.view.state.field(completionState, false);
998
+ if (state && state.tooltip && this.view.state.facet(completionConfig).closeOnBlur)
999
+ this.view.dispatch({ effects: closeCompletionEffect.of(null) });
1000
+ },
968
1001
  compositionstart() {
969
1002
  this.composing = 1 /* Started */;
970
1003
  },
@@ -1206,8 +1239,9 @@ function fieldSelection(ranges, field) {
1206
1239
  return state.EditorSelection.create(ranges.filter(r => r.field == field).map(r => state.EditorSelection.range(r.from, r.to)));
1207
1240
  }
1208
1241
  /**
1209
- Convert a snippet template to a function that can apply it.
1210
- Snippets are written using syntax like this:
1242
+ Convert a snippet template to a function that can
1243
+ [apply](https://codemirror.net/6/docs/ref/#autocomplete.Completion.apply) it. Snippets are written
1244
+ using syntax like this:
1211
1245
 
1212
1246
  "for (let ${index} = 0; ${index} < ${end}; ${index}++) {\n\t${}\n}"
1213
1247
 
@@ -1390,8 +1424,238 @@ const completeAnyWord = context => {
1390
1424
  return null;
1391
1425
  let from = token ? token.from : context.pos;
1392
1426
  let options = collectWords(context.state.doc, wordCache(wordChars), re, 50000 /* Range */, from);
1393
- return { from, options, span: mapRE(re, s => "^" + s) };
1427
+ return { from, options, validFor: mapRE(re, s => "^" + s) };
1428
+ };
1429
+
1430
+ const defaults = {
1431
+ brackets: ["(", "[", "{", "'", '"'],
1432
+ before: ")]}:;>"
1394
1433
  };
1434
+ const closeBracketEffect = state.StateEffect.define({
1435
+ map(value, mapping) {
1436
+ let mapped = mapping.mapPos(value, -1, state.MapMode.TrackAfter);
1437
+ return mapped == null ? undefined : mapped;
1438
+ }
1439
+ });
1440
+ const skipBracketEffect = state.StateEffect.define({
1441
+ map(value, mapping) { return mapping.mapPos(value); }
1442
+ });
1443
+ const closedBracket = new class extends state.RangeValue {
1444
+ };
1445
+ closedBracket.startSide = 1;
1446
+ closedBracket.endSide = -1;
1447
+ const bracketState = state.StateField.define({
1448
+ create() { return state.RangeSet.empty; },
1449
+ update(value, tr) {
1450
+ if (tr.selection) {
1451
+ let lineStart = tr.state.doc.lineAt(tr.selection.main.head).from;
1452
+ let prevLineStart = tr.startState.doc.lineAt(tr.startState.selection.main.head).from;
1453
+ if (lineStart != tr.changes.mapPos(prevLineStart, -1))
1454
+ value = state.RangeSet.empty;
1455
+ }
1456
+ value = value.map(tr.changes);
1457
+ for (let effect of tr.effects) {
1458
+ if (effect.is(closeBracketEffect))
1459
+ value = value.update({ add: [closedBracket.range(effect.value, effect.value + 1)] });
1460
+ else if (effect.is(skipBracketEffect))
1461
+ value = value.update({ filter: from => from != effect.value });
1462
+ }
1463
+ return value;
1464
+ }
1465
+ });
1466
+ /**
1467
+ Extension to enable bracket-closing behavior. When a closeable
1468
+ bracket is typed, its closing bracket is immediately inserted
1469
+ after the cursor. When closing a bracket directly in front of a
1470
+ closing bracket inserted by the extension, the cursor moves over
1471
+ that bracket.
1472
+ */
1473
+ function closeBrackets() {
1474
+ return [inputHandler, bracketState];
1475
+ }
1476
+ const definedClosing = "()[]{}<>";
1477
+ function closing(ch) {
1478
+ for (let i = 0; i < definedClosing.length; i += 2)
1479
+ if (definedClosing.charCodeAt(i) == ch)
1480
+ return definedClosing.charAt(i + 1);
1481
+ return state.fromCodePoint(ch < 128 ? ch : ch + 1);
1482
+ }
1483
+ function config(state, pos) {
1484
+ return state.languageDataAt("closeBrackets", pos)[0] || defaults;
1485
+ }
1486
+ const android = typeof navigator == "object" && /Android\b/.test(navigator.userAgent);
1487
+ const inputHandler = view.EditorView.inputHandler.of((view, from, to, insert) => {
1488
+ if ((android ? view.composing : view.compositionStarted) || view.state.readOnly)
1489
+ return false;
1490
+ let sel = view.state.selection.main;
1491
+ if (insert.length > 2 || insert.length == 2 && state.codePointSize(state.codePointAt(insert, 0)) == 1 ||
1492
+ from != sel.from || to != sel.to)
1493
+ return false;
1494
+ let tr = insertBracket(view.state, insert);
1495
+ if (!tr)
1496
+ return false;
1497
+ view.dispatch(tr);
1498
+ return true;
1499
+ });
1500
+ /**
1501
+ Command that implements deleting a pair of matching brackets when
1502
+ the cursor is between them.
1503
+ */
1504
+ const deleteBracketPair = ({ state: state$1, dispatch }) => {
1505
+ if (state$1.readOnly)
1506
+ return false;
1507
+ let conf = config(state$1, state$1.selection.main.head);
1508
+ let tokens = conf.brackets || defaults.brackets;
1509
+ let dont = null, changes = state$1.changeByRange(range => {
1510
+ if (range.empty) {
1511
+ let before = prevChar(state$1.doc, range.head);
1512
+ for (let token of tokens) {
1513
+ if (token == before && nextChar(state$1.doc, range.head) == closing(state.codePointAt(token, 0)))
1514
+ return { changes: { from: range.head - token.length, to: range.head + token.length },
1515
+ range: state.EditorSelection.cursor(range.head - token.length),
1516
+ userEvent: "delete.backward" };
1517
+ }
1518
+ }
1519
+ return { range: dont = range };
1520
+ });
1521
+ if (!dont)
1522
+ dispatch(state$1.update(changes, { scrollIntoView: true }));
1523
+ return !dont;
1524
+ };
1525
+ /**
1526
+ Close-brackets related key bindings. Binds Backspace to
1527
+ [`deleteBracketPair`](https://codemirror.net/6/docs/ref/#autocomplete.deleteBracketPair).
1528
+ */
1529
+ const closeBracketsKeymap = [
1530
+ { key: "Backspace", run: deleteBracketPair }
1531
+ ];
1532
+ /**
1533
+ Implements the extension's behavior on text insertion. If the
1534
+ given string counts as a bracket in the language around the
1535
+ selection, and replacing the selection with it requires custom
1536
+ behavior (inserting a closing version or skipping past a
1537
+ previously-closed bracket), this function returns a transaction
1538
+ representing that custom behavior. (You only need this if you want
1539
+ to programmatically insert brackets—the
1540
+ [`closeBrackets`](https://codemirror.net/6/docs/ref/#autocomplete.closeBrackets) extension will
1541
+ take care of running this for user input.)
1542
+ */
1543
+ function insertBracket(state$1, bracket) {
1544
+ let conf = config(state$1, state$1.selection.main.head);
1545
+ let tokens = conf.brackets || defaults.brackets;
1546
+ for (let tok of tokens) {
1547
+ let closed = closing(state.codePointAt(tok, 0));
1548
+ if (bracket == tok)
1549
+ return closed == tok ? handleSame(state$1, tok, tokens.indexOf(tok + tok + tok) > -1)
1550
+ : handleOpen(state$1, tok, closed, conf.before || defaults.before);
1551
+ if (bracket == closed && closedBracketAt(state$1, state$1.selection.main.from))
1552
+ return handleClose(state$1, tok, closed);
1553
+ }
1554
+ return null;
1555
+ }
1556
+ function closedBracketAt(state, pos) {
1557
+ let found = false;
1558
+ state.field(bracketState).between(0, state.doc.length, from => {
1559
+ if (from == pos)
1560
+ found = true;
1561
+ });
1562
+ return found;
1563
+ }
1564
+ function nextChar(doc, pos) {
1565
+ let next = doc.sliceString(pos, pos + 2);
1566
+ return next.slice(0, state.codePointSize(state.codePointAt(next, 0)));
1567
+ }
1568
+ function prevChar(doc, pos) {
1569
+ let prev = doc.sliceString(pos - 2, pos);
1570
+ return state.codePointSize(state.codePointAt(prev, 0)) == prev.length ? prev : prev.slice(1);
1571
+ }
1572
+ function handleOpen(state$1, open, close, closeBefore) {
1573
+ let dont = null, changes = state$1.changeByRange(range => {
1574
+ if (!range.empty)
1575
+ return { changes: [{ insert: open, from: range.from }, { insert: close, from: range.to }],
1576
+ effects: closeBracketEffect.of(range.to + open.length),
1577
+ range: state.EditorSelection.range(range.anchor + open.length, range.head + open.length) };
1578
+ let next = nextChar(state$1.doc, range.head);
1579
+ if (!next || /\s/.test(next) || closeBefore.indexOf(next) > -1)
1580
+ return { changes: { insert: open + close, from: range.head },
1581
+ effects: closeBracketEffect.of(range.head + open.length),
1582
+ range: state.EditorSelection.cursor(range.head + open.length) };
1583
+ return { range: dont = range };
1584
+ });
1585
+ return dont ? null : state$1.update(changes, {
1586
+ scrollIntoView: true,
1587
+ userEvent: "input.type"
1588
+ });
1589
+ }
1590
+ function handleClose(state$1, _open, close) {
1591
+ let dont = null, moved = state$1.selection.ranges.map(range => {
1592
+ if (range.empty && nextChar(state$1.doc, range.head) == close)
1593
+ return state.EditorSelection.cursor(range.head + close.length);
1594
+ return dont = range;
1595
+ });
1596
+ return dont ? null : state$1.update({
1597
+ selection: state.EditorSelection.create(moved, state$1.selection.mainIndex),
1598
+ scrollIntoView: true,
1599
+ effects: state$1.selection.ranges.map(({ from }) => skipBracketEffect.of(from))
1600
+ });
1601
+ }
1602
+ // Handles cases where the open and close token are the same, and
1603
+ // possibly triple quotes (as in `"""abc"""`-style quoting).
1604
+ function handleSame(state$1, token, allowTriple) {
1605
+ let dont = null, changes = state$1.changeByRange(range => {
1606
+ if (!range.empty)
1607
+ return { changes: [{ insert: token, from: range.from }, { insert: token, from: range.to }],
1608
+ effects: closeBracketEffect.of(range.to + token.length),
1609
+ range: state.EditorSelection.range(range.anchor + token.length, range.head + token.length) };
1610
+ let pos = range.head, next = nextChar(state$1.doc, pos);
1611
+ if (next == token) {
1612
+ if (nodeStart(state$1, pos)) {
1613
+ return { changes: { insert: token + token, from: pos },
1614
+ effects: closeBracketEffect.of(pos + token.length),
1615
+ range: state.EditorSelection.cursor(pos + token.length) };
1616
+ }
1617
+ else if (closedBracketAt(state$1, pos)) {
1618
+ let isTriple = allowTriple && state$1.sliceDoc(pos, pos + token.length * 3) == token + token + token;
1619
+ return { range: state.EditorSelection.cursor(pos + token.length * (isTriple ? 3 : 1)),
1620
+ effects: skipBracketEffect.of(pos) };
1621
+ }
1622
+ }
1623
+ else if (allowTriple && state$1.sliceDoc(pos - 2 * token.length, pos) == token + token &&
1624
+ nodeStart(state$1, pos - 2 * token.length)) {
1625
+ return { changes: { insert: token + token + token + token, from: pos },
1626
+ effects: closeBracketEffect.of(pos + token.length),
1627
+ range: state.EditorSelection.cursor(pos + token.length) };
1628
+ }
1629
+ else if (state$1.charCategorizer(pos)(next) != state.CharCategory.Word) {
1630
+ let prev = state$1.sliceDoc(pos - 1, pos);
1631
+ if (prev != token && state$1.charCategorizer(pos)(prev) != state.CharCategory.Word && !probablyInString(state$1, pos, token))
1632
+ return { changes: { insert: token + token, from: pos },
1633
+ effects: closeBracketEffect.of(pos + token.length),
1634
+ range: state.EditorSelection.cursor(pos + token.length) };
1635
+ }
1636
+ return { range: dont = range };
1637
+ });
1638
+ return dont ? null : state$1.update(changes, {
1639
+ scrollIntoView: true,
1640
+ userEvent: "input.type"
1641
+ });
1642
+ }
1643
+ function nodeStart(state, pos) {
1644
+ let tree = language.syntaxTree(state).resolveInner(pos + 1);
1645
+ return tree.parent && tree.from == pos;
1646
+ }
1647
+ function probablyInString(state, pos, quoteToken) {
1648
+ let node = language.syntaxTree(state).resolveInner(pos, -1);
1649
+ for (let i = 0; i < 5; i++) {
1650
+ if (state.sliceDoc(node.from, node.from + quoteToken.length) == quoteToken)
1651
+ return true;
1652
+ let parent = node.to == pos && node.parent;
1653
+ if (!parent)
1654
+ break;
1655
+ node = parent;
1656
+ }
1657
+ return false;
1658
+ }
1395
1659
 
1396
1660
  /**
1397
1661
  Returns an extension that enables autocompletion.
@@ -1480,14 +1744,19 @@ exports.CompletionContext = CompletionContext;
1480
1744
  exports.acceptCompletion = acceptCompletion;
1481
1745
  exports.autocompletion = autocompletion;
1482
1746
  exports.clearSnippet = clearSnippet;
1747
+ exports.closeBrackets = closeBrackets;
1748
+ exports.closeBracketsKeymap = closeBracketsKeymap;
1483
1749
  exports.closeCompletion = closeCompletion;
1484
1750
  exports.completeAnyWord = completeAnyWord;
1485
1751
  exports.completeFromList = completeFromList;
1486
1752
  exports.completionKeymap = completionKeymap;
1487
1753
  exports.completionStatus = completionStatus;
1488
1754
  exports.currentCompletions = currentCompletions;
1755
+ exports.deleteBracketPair = deleteBracketPair;
1489
1756
  exports.ifIn = ifIn;
1490
1757
  exports.ifNotIn = ifNotIn;
1758
+ exports.insertBracket = insertBracket;
1759
+ exports.insertCompletionText = insertCompletionText;
1491
1760
  exports.moveCompletionSelection = moveCompletionSelection;
1492
1761
  exports.nextSnippetField = nextSnippetField;
1493
1762
  exports.pickedCompletion = pickedCompletion;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _codemirror_state from '@codemirror/state';
2
- import { EditorState, Transaction, StateCommand, Facet, Extension, StateEffect } from '@codemirror/state';
2
+ import { EditorState, TransactionSpec, Transaction, StateCommand, Facet, Extension, StateEffect } from '@codemirror/state';
3
3
  import { EditorView, KeyBinding, Command } from '@codemirror/view';
4
4
  import * as _lezer_common from '@lezer/common';
5
5
 
@@ -18,6 +18,11 @@ interface CompletionConfig {
18
18
  */
19
19
  override?: readonly CompletionSource[] | null;
20
20
  /**
21
+ Determines whether the completion tooltip is closed when the
22
+ editor loses focus. Defaults to true.
23
+ */
24
+ closeOnBlur?: boolean;
25
+ /**
21
26
  The maximum number of options to render to the DOM.
22
27
  */
23
28
  maxRenderedOptions?: number;
@@ -52,8 +57,7 @@ interface CompletionConfig {
52
57
  completion, and should produce a DOM node to show. `position`
53
58
  determines where in the DOM the result appears, relative to
54
59
  other added widgets and the standard content. The default icons
55
- have position 20, the label position 50, and the detail position
56
- 70.
60
+ have position 20, the label position 50, and the detail position 70.
57
61
  */
58
62
  addToOptions?: {
59
63
  render: (completion: Completion, state: EditorState) => Node | null;
@@ -226,33 +230,57 @@ interface CompletionResult {
226
230
  */
227
231
  options: readonly Completion[];
228
232
  /**
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.
233
+ When given, further typing or deletion that causes the part of
234
+ the document between ([mapped](https://codemirror.net/6/docs/ref/#state.ChangeDesc.mapPos)) `from`
235
+ and `to` to match this regular expression or predicate function
236
+ will not query the completion source again, but continue with
237
+ this list of options. This can help a lot with responsiveness,
238
+ since it allows the completion list to be updated synchronously.
235
239
  */
236
- span?: RegExp;
240
+ validFor?: RegExp | ((text: string, from: number, to: number, state: EditorState) => boolean);
237
241
  /**
238
242
  By default, the library filters and scores completions. Set
239
243
  `filter` to `false` to disable this, and cause your completions
240
244
  to all be included, in the order they were given. When there are
241
245
  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.
246
+ list of completions. `validFor` must not be given when `filter`
247
+ is `false`, because it only works when filtering.
244
248
  */
245
249
  filter?: boolean;
250
+ /**
251
+ When [`filter`](https://codemirror.net/6/docs/ref/#autocomplete.CompletionResult.filter) is set to
252
+ `false`, this may be provided to compute the ranges on the label
253
+ that match the input. Should return an array of numbers where
254
+ each pair of adjacent numbers provide the start and end of a
255
+ range.
256
+ */
257
+ getMatch?: (completion: Completion) => readonly number[];
258
+ /**
259
+ Synchronously update the completion result after typing or
260
+ deletion. If given, this should not do any expensive work, since
261
+ it will be called during editor state updates. The function
262
+ should make sure (similar to
263
+ [`validFor`](https://codemirror.net/6/docs/ref/#autocomplete.CompletionResult.validFor)) that the
264
+ completion still applies in the new state.
265
+ */
266
+ update?: (current: CompletionResult, from: number, to: number, context: CompletionContext) => CompletionResult | null;
246
267
  }
247
268
  /**
248
269
  This annotation is added to transactions that are produced by
249
270
  picking a completion.
250
271
  */
251
272
  declare const pickedCompletion: _codemirror_state.AnnotationType<Completion>;
273
+ /**
274
+ Helper function that returns a transaction spec which inserts a
275
+ completion's text in the main selection range, and any other
276
+ selection range that has the same text in front of it.
277
+ */
278
+ declare function insertCompletionText(state: EditorState, text: string, from: number, to: number): TransactionSpec;
252
279
 
253
280
  /**
254
- Convert a snippet template to a function that can apply it.
255
- Snippets are written using syntax like this:
281
+ Convert a snippet template to a function that can
282
+ [apply](https://codemirror.net/6/docs/ref/#autocomplete.Completion.apply) it. Snippets are written
283
+ using syntax like this:
256
284
 
257
285
  "for (let ${index} = 0; ${index} < ${end}; ${index}++) {\n\t${}\n}"
258
286
 
@@ -331,6 +359,56 @@ return those as completions.
331
359
  */
332
360
  declare const completeAnyWord: CompletionSource;
333
361
 
362
+ /**
363
+ Configures bracket closing behavior for a syntax (via
364
+ [language data](https://codemirror.net/6/docs/ref/#state.EditorState.languageDataAt)) using the `"closeBrackets"`
365
+ identifier.
366
+ */
367
+ interface CloseBracketConfig {
368
+ /**
369
+ The opening brackets to close. Defaults to `["(", "[", "{", "'",
370
+ '"']`. Brackets may be single characters or a triple of quotes
371
+ (as in `"''''"`).
372
+ */
373
+ brackets?: string[];
374
+ /**
375
+ Characters in front of which newly opened brackets are
376
+ automatically closed. Closing always happens in front of
377
+ whitespace. Defaults to `")]}:;>"`.
378
+ */
379
+ before?: string;
380
+ }
381
+ /**
382
+ Extension to enable bracket-closing behavior. When a closeable
383
+ bracket is typed, its closing bracket is immediately inserted
384
+ after the cursor. When closing a bracket directly in front of a
385
+ closing bracket inserted by the extension, the cursor moves over
386
+ that bracket.
387
+ */
388
+ declare function closeBrackets(): Extension;
389
+ /**
390
+ Command that implements deleting a pair of matching brackets when
391
+ the cursor is between them.
392
+ */
393
+ declare const deleteBracketPair: StateCommand;
394
+ /**
395
+ Close-brackets related key bindings. Binds Backspace to
396
+ [`deleteBracketPair`](https://codemirror.net/6/docs/ref/#autocomplete.deleteBracketPair).
397
+ */
398
+ declare const closeBracketsKeymap: readonly KeyBinding[];
399
+ /**
400
+ Implements the extension's behavior on text insertion. If the
401
+ given string counts as a bracket in the language around the
402
+ selection, and replacing the selection with it requires custom
403
+ behavior (inserting a closing version or skipping past a
404
+ previously-closed bracket), this function returns a transaction
405
+ representing that custom behavior. (You only need this if you want
406
+ to programmatically insert brackets—the
407
+ [`closeBrackets`](https://codemirror.net/6/docs/ref/#autocomplete.closeBrackets) extension will
408
+ take care of running this for user input.)
409
+ */
410
+ declare function insertBracket(state: EditorState, bracket: string): Transaction | null;
411
+
334
412
  /**
335
413
  Returns an extension that enables autocompletion.
336
414
  */
@@ -373,4 +451,4 @@ the currently selected completion.
373
451
  */
374
452
  declare function setSelectedCompletion(index: number): StateEffect<unknown>;
375
453
 
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 };
454
+ export { CloseBracketConfig, Completion, CompletionContext, CompletionResult, CompletionSource, acceptCompletion, autocompletion, clearSnippet, closeBrackets, closeBracketsKeymap, closeCompletion, completeAnyWord, completeFromList, completionKeymap, completionStatus, currentCompletions, deleteBracketPair, ifIn, ifNotIn, insertBracket, insertCompletionText, 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
  /**
@@ -151,20 +149,35 @@ This annotation is added to transactions that are produced by
151
149
  picking a completion.
152
150
  */
153
151
  const pickedCompletion = /*@__PURE__*/Annotation.define();
152
+ /**
153
+ Helper function that returns a transaction spec which inserts a
154
+ completion's text in the main selection range, and any other
155
+ selection range that has the same text in front of it.
156
+ */
157
+ function insertCompletionText(state, text, from, to) {
158
+ return state.changeByRange(range => {
159
+ if (range == state.selection.main)
160
+ return {
161
+ changes: { from: from, to: to, insert: text },
162
+ range: EditorSelection.cursor(from + text.length)
163
+ };
164
+ let len = to - from;
165
+ if (!range.empty ||
166
+ len && state.sliceDoc(range.from - len, range.from) != state.sliceDoc(from, to))
167
+ return { range };
168
+ return {
169
+ changes: { from: range.from - len, to: range.from, insert: text },
170
+ range: EditorSelection.cursor(range.from - len + text.length)
171
+ };
172
+ });
173
+ }
154
174
  function applyCompletion(view, option) {
155
- let apply = option.completion.apply || option.completion.label;
175
+ const apply = option.completion.apply || option.completion.label;
156
176
  let result = option.source;
157
- if (typeof apply == "string") {
158
- view.dispatch({
159
- changes: { from: result.from, to: result.to, insert: apply },
160
- selection: { anchor: result.from + apply.length },
161
- userEvent: "input.complete",
162
- annotations: pickedCompletion.of(option.completion)
163
- });
164
- }
165
- else {
177
+ if (typeof apply == "string")
178
+ view.dispatch(insertCompletionText(view.state, apply, result.from, result.to));
179
+ else
166
180
  apply(view, option.completion, result.from, result.to);
167
- }
168
181
  }
169
182
  const SourceCache = /*@__PURE__*/new WeakMap();
170
183
  function asSource(source) {
@@ -306,6 +319,7 @@ const completionConfig = /*@__PURE__*/Facet.define({
306
319
  return combineConfig(configs, {
307
320
  activateOnTyping: true,
308
321
  override: null,
322
+ closeOnBlur: true,
309
323
  maxRenderedOptions: 100,
310
324
  defaultKeymap: true,
311
325
  optionClass: () => "",
@@ -314,6 +328,7 @@ const completionConfig = /*@__PURE__*/Facet.define({
314
328
  addToOptions: []
315
329
  }, {
316
330
  defaultKeymap: (a, b) => a && b,
331
+ closeOnBlur: (a, b) => a && b,
317
332
  icons: (a, b) => a && b,
318
333
  optionClass: (a, b) => c => joinClass(a(c), b(c)),
319
334
  addToOptions: (a, b) => a.concat(b)
@@ -548,7 +563,6 @@ function scrollIntoView(container, element) {
548
563
  container.scrollTop += self.bottom - parent.bottom;
549
564
  }
550
565
 
551
- const MaxOptions = 300;
552
566
  // Used to pick a preferred option when two options with the same
553
567
  // label occur in the result.
554
568
  function score(option) {
@@ -560,8 +574,14 @@ function sortOptions(active, state) {
560
574
  for (let a of active)
561
575
  if (a.hasResult()) {
562
576
  if (a.result.filter === false) {
563
- for (let option of a.result.options)
564
- options.push(new Option(option, a, [1e9 - i++]));
577
+ let getMatch = a.result.getMatch;
578
+ for (let option of a.result.options) {
579
+ let match = [1e9 - i++];
580
+ if (getMatch)
581
+ for (let n of getMatch(option))
582
+ match.push(n);
583
+ options.push(new Option(option, a, match));
584
+ }
565
585
  }
566
586
  else {
567
587
  let matcher = new FuzzyMatcher(state.sliceDoc(a.from, a.to)), match;
@@ -575,10 +595,9 @@ function sortOptions(active, state) {
575
595
  }
576
596
  let result = [], prev = null;
577
597
  for (let opt of options.sort(cmpOption)) {
578
- if (result.length == MaxOptions)
579
- break;
580
598
  if (!prev || prev.label != opt.completion.label || prev.detail != opt.completion.detail ||
581
- prev.type != opt.completion.type || prev.apply != opt.completion.apply)
599
+ (prev.type != null && opt.completion.type != null && prev.type != opt.completion.type) ||
600
+ prev.apply != opt.completion.apply)
582
601
  result.push(opt);
583
602
  else if (score(opt.completion) > score(prev))
584
603
  result[result.length - 1] = opt;
@@ -728,24 +747,27 @@ class ActiveSource {
728
747
  }
729
748
  }
730
749
  class ActiveResult extends ActiveSource {
731
- constructor(source, explicitPos, result, from, to, span) {
750
+ constructor(source, explicitPos, result, from, to) {
732
751
  super(source, 2 /* Result */, explicitPos);
733
752
  this.result = result;
734
753
  this.from = from;
735
754
  this.to = to;
736
- this.span = span;
737
755
  }
738
756
  hasResult() { return true; }
739
757
  handleUserEvent(tr, type, conf) {
758
+ var _a;
740
759
  let from = tr.changes.mapPos(this.from), to = tr.changes.mapPos(this.to, 1);
741
760
  let pos = cur(tr.state);
742
761
  if ((this.explicitPos < 0 ? pos <= from : pos < this.from) ||
743
762
  pos > to ||
744
763
  type == "delete" && cur(tr.startState) == this.from)
745
764
  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);
765
+ let explicitPos = this.explicitPos < 0 ? -1 : tr.changes.mapPos(this.explicitPos), updated;
766
+ if (checkValid(this.result.validFor, tr.state, from, to))
767
+ return new ActiveResult(this.source, explicitPos, this.result, from, to);
768
+ if (this.result.update &&
769
+ (updated = this.result.update(this.result, from, to, new CompletionContext(tr.state, pos, explicitPos >= 0))))
770
+ return new ActiveResult(this.source, explicitPos, updated, updated.from, (_a = updated.to) !== null && _a !== void 0 ? _a : cur(tr.state));
749
771
  return new ActiveSource(this.source, 1 /* Pending */, explicitPos);
750
772
  }
751
773
  handleChange(tr) {
@@ -753,9 +775,15 @@ class ActiveResult extends ActiveSource {
753
775
  }
754
776
  map(mapping) {
755
777
  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);
778
+ new ActiveResult(this.source, this.explicitPos < 0 ? -1 : mapping.mapPos(this.explicitPos), this.result, mapping.mapPos(this.from), mapping.mapPos(this.to, 1));
757
779
  }
758
780
  }
781
+ function checkValid(validFor, state, from, to) {
782
+ if (!validFor)
783
+ return false;
784
+ let text = state.sliceDoc(from, to);
785
+ return typeof validFor == "function" ? validFor(text, from, to, state) : ensureAnchor(validFor, true).test(text);
786
+ }
759
787
  const startCompletionEffect = /*@__PURE__*/StateEffect.define();
760
788
  const closeCompletionEffect = /*@__PURE__*/StateEffect.define();
761
789
  const setActiveEffect = /*@__PURE__*/StateEffect.define({
@@ -929,7 +957,7 @@ const completionPlugin = /*@__PURE__*/ViewPlugin.fromClass(class {
929
957
  continue;
930
958
  this.running.splice(i--, 1);
931
959
  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);
960
+ 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
961
  // Replay the transactions that happened since the start of
934
962
  // the request and see if that preserves the result
935
963
  for (let tr of query.updates)
@@ -961,6 +989,11 @@ const completionPlugin = /*@__PURE__*/ViewPlugin.fromClass(class {
961
989
  }
962
990
  }, {
963
991
  eventHandlers: {
992
+ blur() {
993
+ let state = this.view.state.field(completionState, false);
994
+ if (state && state.tooltip && this.view.state.facet(completionConfig).closeOnBlur)
995
+ this.view.dispatch({ effects: closeCompletionEffect.of(null) });
996
+ },
964
997
  compositionstart() {
965
998
  this.composing = 1 /* Started */;
966
999
  },
@@ -1202,8 +1235,9 @@ function fieldSelection(ranges, field) {
1202
1235
  return EditorSelection.create(ranges.filter(r => r.field == field).map(r => EditorSelection.range(r.from, r.to)));
1203
1236
  }
1204
1237
  /**
1205
- Convert a snippet template to a function that can apply it.
1206
- Snippets are written using syntax like this:
1238
+ Convert a snippet template to a function that can
1239
+ [apply](https://codemirror.net/6/docs/ref/#autocomplete.Completion.apply) it. Snippets are written
1240
+ using syntax like this:
1207
1241
 
1208
1242
  "for (let ${index} = 0; ${index} < ${end}; ${index}++) {\n\t${}\n}"
1209
1243
 
@@ -1386,8 +1420,238 @@ const completeAnyWord = context => {
1386
1420
  return null;
1387
1421
  let from = token ? token.from : context.pos;
1388
1422
  let options = collectWords(context.state.doc, wordCache(wordChars), re, 50000 /* Range */, from);
1389
- return { from, options, span: mapRE(re, s => "^" + s) };
1423
+ return { from, options, validFor: mapRE(re, s => "^" + s) };
1424
+ };
1425
+
1426
+ const defaults = {
1427
+ brackets: ["(", "[", "{", "'", '"'],
1428
+ before: ")]}:;>"
1390
1429
  };
1430
+ const closeBracketEffect = /*@__PURE__*/StateEffect.define({
1431
+ map(value, mapping) {
1432
+ let mapped = mapping.mapPos(value, -1, MapMode.TrackAfter);
1433
+ return mapped == null ? undefined : mapped;
1434
+ }
1435
+ });
1436
+ const skipBracketEffect = /*@__PURE__*/StateEffect.define({
1437
+ map(value, mapping) { return mapping.mapPos(value); }
1438
+ });
1439
+ const closedBracket = /*@__PURE__*/new class extends RangeValue {
1440
+ };
1441
+ closedBracket.startSide = 1;
1442
+ closedBracket.endSide = -1;
1443
+ const bracketState = /*@__PURE__*/StateField.define({
1444
+ create() { return RangeSet.empty; },
1445
+ update(value, tr) {
1446
+ if (tr.selection) {
1447
+ let lineStart = tr.state.doc.lineAt(tr.selection.main.head).from;
1448
+ let prevLineStart = tr.startState.doc.lineAt(tr.startState.selection.main.head).from;
1449
+ if (lineStart != tr.changes.mapPos(prevLineStart, -1))
1450
+ value = RangeSet.empty;
1451
+ }
1452
+ value = value.map(tr.changes);
1453
+ for (let effect of tr.effects) {
1454
+ if (effect.is(closeBracketEffect))
1455
+ value = value.update({ add: [closedBracket.range(effect.value, effect.value + 1)] });
1456
+ else if (effect.is(skipBracketEffect))
1457
+ value = value.update({ filter: from => from != effect.value });
1458
+ }
1459
+ return value;
1460
+ }
1461
+ });
1462
+ /**
1463
+ Extension to enable bracket-closing behavior. When a closeable
1464
+ bracket is typed, its closing bracket is immediately inserted
1465
+ after the cursor. When closing a bracket directly in front of a
1466
+ closing bracket inserted by the extension, the cursor moves over
1467
+ that bracket.
1468
+ */
1469
+ function closeBrackets() {
1470
+ return [inputHandler, bracketState];
1471
+ }
1472
+ const definedClosing = "()[]{}<>";
1473
+ function closing(ch) {
1474
+ for (let i = 0; i < definedClosing.length; i += 2)
1475
+ if (definedClosing.charCodeAt(i) == ch)
1476
+ return definedClosing.charAt(i + 1);
1477
+ return fromCodePoint(ch < 128 ? ch : ch + 1);
1478
+ }
1479
+ function config(state, pos) {
1480
+ return state.languageDataAt("closeBrackets", pos)[0] || defaults;
1481
+ }
1482
+ const android = typeof navigator == "object" && /*@__PURE__*//Android\b/.test(navigator.userAgent);
1483
+ const inputHandler = /*@__PURE__*/EditorView.inputHandler.of((view, from, to, insert) => {
1484
+ if ((android ? view.composing : view.compositionStarted) || view.state.readOnly)
1485
+ return false;
1486
+ let sel = view.state.selection.main;
1487
+ if (insert.length > 2 || insert.length == 2 && codePointSize(codePointAt(insert, 0)) == 1 ||
1488
+ from != sel.from || to != sel.to)
1489
+ return false;
1490
+ let tr = insertBracket(view.state, insert);
1491
+ if (!tr)
1492
+ return false;
1493
+ view.dispatch(tr);
1494
+ return true;
1495
+ });
1496
+ /**
1497
+ Command that implements deleting a pair of matching brackets when
1498
+ the cursor is between them.
1499
+ */
1500
+ const deleteBracketPair = ({ state, dispatch }) => {
1501
+ if (state.readOnly)
1502
+ return false;
1503
+ let conf = config(state, state.selection.main.head);
1504
+ let tokens = conf.brackets || defaults.brackets;
1505
+ let dont = null, changes = state.changeByRange(range => {
1506
+ if (range.empty) {
1507
+ let before = prevChar(state.doc, range.head);
1508
+ for (let token of tokens) {
1509
+ if (token == before && nextChar(state.doc, range.head) == closing(codePointAt(token, 0)))
1510
+ return { changes: { from: range.head - token.length, to: range.head + token.length },
1511
+ range: EditorSelection.cursor(range.head - token.length),
1512
+ userEvent: "delete.backward" };
1513
+ }
1514
+ }
1515
+ return { range: dont = range };
1516
+ });
1517
+ if (!dont)
1518
+ dispatch(state.update(changes, { scrollIntoView: true }));
1519
+ return !dont;
1520
+ };
1521
+ /**
1522
+ Close-brackets related key bindings. Binds Backspace to
1523
+ [`deleteBracketPair`](https://codemirror.net/6/docs/ref/#autocomplete.deleteBracketPair).
1524
+ */
1525
+ const closeBracketsKeymap = [
1526
+ { key: "Backspace", run: deleteBracketPair }
1527
+ ];
1528
+ /**
1529
+ Implements the extension's behavior on text insertion. If the
1530
+ given string counts as a bracket in the language around the
1531
+ selection, and replacing the selection with it requires custom
1532
+ behavior (inserting a closing version or skipping past a
1533
+ previously-closed bracket), this function returns a transaction
1534
+ representing that custom behavior. (You only need this if you want
1535
+ to programmatically insert brackets—the
1536
+ [`closeBrackets`](https://codemirror.net/6/docs/ref/#autocomplete.closeBrackets) extension will
1537
+ take care of running this for user input.)
1538
+ */
1539
+ function insertBracket(state, bracket) {
1540
+ let conf = config(state, state.selection.main.head);
1541
+ let tokens = conf.brackets || defaults.brackets;
1542
+ for (let tok of tokens) {
1543
+ let closed = closing(codePointAt(tok, 0));
1544
+ if (bracket == tok)
1545
+ return closed == tok ? handleSame(state, tok, tokens.indexOf(tok + tok + tok) > -1)
1546
+ : handleOpen(state, tok, closed, conf.before || defaults.before);
1547
+ if (bracket == closed && closedBracketAt(state, state.selection.main.from))
1548
+ return handleClose(state, tok, closed);
1549
+ }
1550
+ return null;
1551
+ }
1552
+ function closedBracketAt(state, pos) {
1553
+ let found = false;
1554
+ state.field(bracketState).between(0, state.doc.length, from => {
1555
+ if (from == pos)
1556
+ found = true;
1557
+ });
1558
+ return found;
1559
+ }
1560
+ function nextChar(doc, pos) {
1561
+ let next = doc.sliceString(pos, pos + 2);
1562
+ return next.slice(0, codePointSize(codePointAt(next, 0)));
1563
+ }
1564
+ function prevChar(doc, pos) {
1565
+ let prev = doc.sliceString(pos - 2, pos);
1566
+ return codePointSize(codePointAt(prev, 0)) == prev.length ? prev : prev.slice(1);
1567
+ }
1568
+ function handleOpen(state, open, close, closeBefore) {
1569
+ let dont = null, changes = state.changeByRange(range => {
1570
+ if (!range.empty)
1571
+ return { changes: [{ insert: open, from: range.from }, { insert: close, from: range.to }],
1572
+ effects: closeBracketEffect.of(range.to + open.length),
1573
+ range: EditorSelection.range(range.anchor + open.length, range.head + open.length) };
1574
+ let next = nextChar(state.doc, range.head);
1575
+ if (!next || /\s/.test(next) || closeBefore.indexOf(next) > -1)
1576
+ return { changes: { insert: open + close, from: range.head },
1577
+ effects: closeBracketEffect.of(range.head + open.length),
1578
+ range: EditorSelection.cursor(range.head + open.length) };
1579
+ return { range: dont = range };
1580
+ });
1581
+ return dont ? null : state.update(changes, {
1582
+ scrollIntoView: true,
1583
+ userEvent: "input.type"
1584
+ });
1585
+ }
1586
+ function handleClose(state, _open, close) {
1587
+ let dont = null, moved = state.selection.ranges.map(range => {
1588
+ if (range.empty && nextChar(state.doc, range.head) == close)
1589
+ return EditorSelection.cursor(range.head + close.length);
1590
+ return dont = range;
1591
+ });
1592
+ return dont ? null : state.update({
1593
+ selection: EditorSelection.create(moved, state.selection.mainIndex),
1594
+ scrollIntoView: true,
1595
+ effects: state.selection.ranges.map(({ from }) => skipBracketEffect.of(from))
1596
+ });
1597
+ }
1598
+ // Handles cases where the open and close token are the same, and
1599
+ // possibly triple quotes (as in `"""abc"""`-style quoting).
1600
+ function handleSame(state, token, allowTriple) {
1601
+ let dont = null, changes = state.changeByRange(range => {
1602
+ if (!range.empty)
1603
+ return { changes: [{ insert: token, from: range.from }, { insert: token, from: range.to }],
1604
+ effects: closeBracketEffect.of(range.to + token.length),
1605
+ range: EditorSelection.range(range.anchor + token.length, range.head + token.length) };
1606
+ let pos = range.head, next = nextChar(state.doc, pos);
1607
+ if (next == token) {
1608
+ if (nodeStart(state, pos)) {
1609
+ return { changes: { insert: token + token, from: pos },
1610
+ effects: closeBracketEffect.of(pos + token.length),
1611
+ range: EditorSelection.cursor(pos + token.length) };
1612
+ }
1613
+ else if (closedBracketAt(state, pos)) {
1614
+ let isTriple = allowTriple && state.sliceDoc(pos, pos + token.length * 3) == token + token + token;
1615
+ return { range: EditorSelection.cursor(pos + token.length * (isTriple ? 3 : 1)),
1616
+ effects: skipBracketEffect.of(pos) };
1617
+ }
1618
+ }
1619
+ else if (allowTriple && state.sliceDoc(pos - 2 * token.length, pos) == token + token &&
1620
+ nodeStart(state, pos - 2 * token.length)) {
1621
+ return { changes: { insert: token + token + token + token, from: pos },
1622
+ effects: closeBracketEffect.of(pos + token.length),
1623
+ range: EditorSelection.cursor(pos + token.length) };
1624
+ }
1625
+ else if (state.charCategorizer(pos)(next) != CharCategory.Word) {
1626
+ let prev = state.sliceDoc(pos - 1, pos);
1627
+ if (prev != token && state.charCategorizer(pos)(prev) != CharCategory.Word && !probablyInString(state, pos, token))
1628
+ return { changes: { insert: token + token, from: pos },
1629
+ effects: closeBracketEffect.of(pos + token.length),
1630
+ range: EditorSelection.cursor(pos + token.length) };
1631
+ }
1632
+ return { range: dont = range };
1633
+ });
1634
+ return dont ? null : state.update(changes, {
1635
+ scrollIntoView: true,
1636
+ userEvent: "input.type"
1637
+ });
1638
+ }
1639
+ function nodeStart(state, pos) {
1640
+ let tree = syntaxTree(state).resolveInner(pos + 1);
1641
+ return tree.parent && tree.from == pos;
1642
+ }
1643
+ function probablyInString(state, pos, quoteToken) {
1644
+ let node = syntaxTree(state).resolveInner(pos, -1);
1645
+ for (let i = 0; i < 5; i++) {
1646
+ if (state.sliceDoc(node.from, node.from + quoteToken.length) == quoteToken)
1647
+ return true;
1648
+ let parent = node.to == pos && node.parent;
1649
+ if (!parent)
1650
+ break;
1651
+ node = parent;
1652
+ }
1653
+ return false;
1654
+ }
1391
1655
 
1392
1656
  /**
1393
1657
  Returns an extension that enables autocompletion.
@@ -1472,4 +1736,4 @@ function setSelectedCompletion(index) {
1472
1736
  return setSelectedEffect.of(index);
1473
1737
  }
1474
1738
 
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 };
1739
+ export { CompletionContext, acceptCompletion, autocompletion, clearSnippet, closeBrackets, closeBracketsKeymap, closeCompletion, completeAnyWord, completeFromList, completionKeymap, completionStatus, currentCompletions, deleteBracketPair, ifIn, ifNotIn, insertBracket, insertCompletionText, 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.2",
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"