@codemirror/autocomplete 6.4.2 → 6.5.1

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,23 @@
1
+ ## 6.5.1 (2023-04-13)
2
+
3
+ ### Bug fixes
4
+
5
+ Keep completions open when interaction with an info tooltip moves focus out of the editor.
6
+
7
+ ## 6.5.0 (2023-04-13)
8
+
9
+ ### Bug fixes
10
+
11
+ When `closeBrackets` skips a bracket, it now generates a change that overwrites the bracket.
12
+
13
+ Replace the entire selected range when picking a completion with a non-cursor selection active.
14
+
15
+ ### New features
16
+
17
+ Completions can now provide a `section` field that is used to group them into sections.
18
+
19
+ The new `positionInfo` option can be used to provide custom logic for positioning the info tooltips.
20
+
1
21
  ## 6.4.2 (2023-02-17)
2
22
 
3
23
  ### Bug fixes
package/dist/index.cjs CHANGED
@@ -137,13 +137,14 @@ function ifNotIn(nodes, source) {
137
137
  };
138
138
  }
139
139
  class Option {
140
- constructor(completion, source, match) {
140
+ constructor(completion, source, match, score) {
141
141
  this.completion = completion;
142
142
  this.source = source;
143
143
  this.match = match;
144
+ this.score = score;
144
145
  }
145
146
  }
146
- function cur(state) { return state.selection.main.head; }
147
+ function cur(state) { return state.selection.main.from; }
147
148
  // Make sure the given regexp has a $ at its end and, if `start` is
148
149
  // true, a ^ at its start.
149
150
  function ensureAnchor(expr, start) {
@@ -165,18 +166,13 @@ completion's text in the main selection range, and any other
165
166
  selection range that has the same text in front of it.
166
167
  */
167
168
  function insertCompletionText(state$1, text, from, to) {
169
+ let { main } = state$1.selection, len = to - from;
168
170
  return Object.assign(Object.assign({}, state$1.changeByRange(range => {
169
- if (range == state$1.selection.main)
170
- return {
171
- changes: { from: from, to: to, insert: text },
172
- range: state.EditorSelection.cursor(from + text.length)
173
- };
174
- let len = to - from;
175
- if (!range.empty ||
176
- len && state$1.sliceDoc(range.from - len, range.from) != state$1.sliceDoc(from, to))
171
+ if (range != main && len &&
172
+ state$1.sliceDoc(range.from - len, range.from + to - main.from) != state$1.sliceDoc(from, to))
177
173
  return { range };
178
174
  return {
179
- changes: { from: range.from - len, to: range.from, insert: text },
175
+ changes: { from: range.from - len, to: to == main.from ? range.to : range.from + to - main.from, insert: text },
180
176
  range: state.EditorSelection.cursor(range.from - len + text.length)
181
177
  };
182
178
  })), { userEvent: "input.complete" });
@@ -198,6 +194,8 @@ function asSource(source) {
198
194
  SourceCache.set(source, known = completeFromList(source));
199
195
  return known;
200
196
  }
197
+ const startCompletionEffect = state.StateEffect.define();
198
+ const closeCompletionEffect = state.StateEffect.define();
201
199
 
202
200
  // A pattern matcher for fuzzy completion matching. Create an instance
203
201
  // once for a pattern, and then use that to match any number of
@@ -343,6 +341,7 @@ const completionConfig = state.Facet.define({
343
341
  aboveCursor: false,
344
342
  icons: true,
345
343
  addToOptions: [],
344
+ positionInfo: defaultPositionInfo,
346
345
  compareCompletions: (a, b) => a.label.localeCompare(b.label),
347
346
  interactionDelay: 75
348
347
  }, {
@@ -358,6 +357,36 @@ const completionConfig = state.Facet.define({
358
357
  function joinClass(a, b) {
359
358
  return a ? b ? a + " " + b : a : b;
360
359
  }
360
+ function defaultPositionInfo(view$1, list, option, info, space) {
361
+ let rtl = view$1.textDirection == view.Direction.RTL, left = rtl, narrow = false;
362
+ let side = "top", offset, maxWidth;
363
+ let spaceLeft = list.left - space.left, spaceRight = space.right - list.right;
364
+ let infoWidth = info.right - info.left, infoHeight = info.bottom - info.top;
365
+ if (left && spaceLeft < Math.min(infoWidth, spaceRight))
366
+ left = false;
367
+ else if (!left && spaceRight < Math.min(infoWidth, spaceLeft))
368
+ left = true;
369
+ if (infoWidth <= (left ? spaceLeft : spaceRight)) {
370
+ offset = Math.max(space.top, Math.min(option.top, space.bottom - infoHeight)) - list.top;
371
+ maxWidth = Math.min(400 /* Info.Width */, left ? spaceLeft : spaceRight);
372
+ }
373
+ else {
374
+ narrow = true;
375
+ maxWidth = Math.min(400 /* Info.Width */, (rtl ? list.right : space.right - list.left) - 30 /* Info.Margin */);
376
+ let spaceBelow = space.bottom - list.bottom;
377
+ if (spaceBelow >= infoHeight || spaceBelow > list.top) { // Below the completion
378
+ offset = option.bottom - list.top;
379
+ }
380
+ else { // Above it
381
+ side = "bottom";
382
+ offset = list.bottom - option.top;
383
+ }
384
+ }
385
+ return {
386
+ style: `${side}: ${offset}px; max-width: ${maxWidth}px`,
387
+ class: "cm-completionInfo-" + (narrow ? (rtl ? "left-narrow" : "right-narrow") : left ? "left" : "right")
388
+ };
389
+ }
361
390
 
362
391
  function optionContent(config) {
363
392
  let content = config.addToOptions.slice();
@@ -422,9 +451,9 @@ class CompletionTooltip {
422
451
  this.view = view;
423
452
  this.stateField = stateField;
424
453
  this.info = null;
425
- this.placeInfo = {
454
+ this.placeInfoReq = {
426
455
  read: () => this.measureInfo(),
427
- write: (pos) => this.positionInfo(pos),
456
+ write: (pos) => this.placeInfo(pos),
428
457
  key: this
429
458
  };
430
459
  this.space = null;
@@ -448,10 +477,16 @@ class CompletionTooltip {
448
477
  }
449
478
  }
450
479
  });
480
+ this.dom.addEventListener("focusout", (e) => {
481
+ let state = view.state.field(this.stateField, false);
482
+ if (state && state.tooltip && view.state.facet(completionConfig).closeOnBlur &&
483
+ e.relatedTarget != view.contentDOM)
484
+ view.dispatch({ effects: closeCompletionEffect.of(null) });
485
+ });
451
486
  this.list = this.dom.appendChild(this.createListBox(options, cState.id, this.range));
452
487
  this.list.addEventListener("scroll", () => {
453
488
  if (this.info)
454
- this.view.requestMeasure(this.placeInfo);
489
+ this.view.requestMeasure(this.placeInfoReq);
455
490
  });
456
491
  }
457
492
  mount() { this.updateSel(); }
@@ -481,7 +516,7 @@ class CompletionTooltip {
481
516
  positioned(space) {
482
517
  this.space = space;
483
518
  if (this.info)
484
- this.view.requestMeasure(this.placeInfo);
519
+ this.view.requestMeasure(this.placeInfoReq);
485
520
  }
486
521
  updateSel() {
487
522
  let cState = this.view.state.field(this.stateField), open = cState.open;
@@ -491,7 +526,7 @@ class CompletionTooltip {
491
526
  this.list = this.dom.appendChild(this.createListBox(open.options, cState.id, this.range));
492
527
  this.list.addEventListener("scroll", () => {
493
528
  if (this.info)
494
- this.view.requestMeasure(this.placeInfo);
529
+ this.view.requestMeasure(this.placeInfoReq);
495
530
  });
496
531
  }
497
532
  if (this.updateSelectedOption(open.selected)) {
@@ -522,12 +557,15 @@ class CompletionTooltip {
522
557
  dom.className = "cm-tooltip cm-completionInfo";
523
558
  dom.appendChild(content);
524
559
  this.dom.appendChild(dom);
525
- this.view.requestMeasure(this.placeInfo);
560
+ this.view.requestMeasure(this.placeInfoReq);
526
561
  }
527
562
  updateSelectedOption(selected) {
528
563
  let set = null;
529
564
  for (let opt = this.list.firstChild, i = this.range.from; opt; opt = opt.nextSibling, i++) {
530
- if (i == selected) {
565
+ if (opt.nodeName != "LI" || !opt.id) {
566
+ i--; // A section header
567
+ }
568
+ else if (i == selected) {
531
569
  if (!opt.hasAttribute("aria-selected")) {
532
570
  opt.setAttribute("aria-selected", "true");
533
571
  set = opt;
@@ -557,41 +595,17 @@ class CompletionTooltip {
557
595
  if (selRect.top > Math.min(space.bottom, listRect.bottom) - 10 ||
558
596
  selRect.bottom < Math.max(space.top, listRect.top) + 10)
559
597
  return null;
560
- let rtl = this.view.textDirection == view.Direction.RTL, left = rtl, narrow = false, maxWidth;
561
- let top = "", bottom = "";
562
- let spaceLeft = listRect.left - space.left, spaceRight = space.right - listRect.right;
563
- if (left && spaceLeft < Math.min(infoRect.width, spaceRight))
564
- left = false;
565
- else if (!left && spaceRight < Math.min(infoRect.width, spaceLeft))
566
- left = true;
567
- if (infoRect.width <= (left ? spaceLeft : spaceRight)) {
568
- top = (Math.max(space.top, Math.min(selRect.top, space.bottom - infoRect.height)) - listRect.top) + "px";
569
- maxWidth = Math.min(400 /* Info.Width */, left ? spaceLeft : spaceRight) + "px";
570
- }
571
- else {
572
- narrow = true;
573
- maxWidth = Math.min(400 /* Info.Width */, (rtl ? listRect.right : space.right - listRect.left) - 30 /* Info.Margin */) + "px";
574
- let spaceBelow = space.bottom - listRect.bottom;
575
- if (spaceBelow >= infoRect.height || spaceBelow > listRect.top) // Below the completion
576
- top = (selRect.bottom - listRect.top) + "px";
577
- else // Above it
578
- bottom = (listRect.bottom - selRect.top) + "px";
579
- }
580
- return {
581
- top, bottom, maxWidth,
582
- class: narrow ? (rtl ? "left-narrow" : "right-narrow") : left ? "left" : "right",
583
- };
598
+ return this.view.state.facet(completionConfig).positionInfo(this.view, listRect, selRect, infoRect, space);
584
599
  }
585
- positionInfo(pos) {
600
+ placeInfo(pos) {
586
601
  if (this.info) {
587
602
  if (pos) {
588
- this.info.style.top = pos.top;
589
- this.info.style.bottom = pos.bottom;
590
- this.info.style.maxWidth = pos.maxWidth;
591
- this.info.className = "cm-tooltip cm-completionInfo cm-completionInfo-" + pos.class;
603
+ if (pos.style)
604
+ this.info.style.cssText = pos.style;
605
+ this.info.className = "cm-tooltip cm-completionInfo " + (pos.class || "");
592
606
  }
593
607
  else {
594
- this.info.style.top = "-1e6px";
608
+ this.info.style.cssText = "top: -1e6px";
595
609
  }
596
610
  }
597
611
  }
@@ -601,8 +615,22 @@ class CompletionTooltip {
601
615
  ul.setAttribute("role", "listbox");
602
616
  ul.setAttribute("aria-expanded", "true");
603
617
  ul.setAttribute("aria-label", this.view.state.phrase("Completions"));
618
+ let curSection = null;
604
619
  for (let i = range.from; i < range.to; i++) {
605
- let { completion, match } = options[i];
620
+ let { completion, match } = options[i], { section } = completion;
621
+ if (section) {
622
+ let name = typeof section == "string" ? section : section.name;
623
+ if (name != curSection && (i > range.from || range.from == 0)) {
624
+ curSection = name;
625
+ if (typeof section != "string" && section.header) {
626
+ ul.appendChild(section.header(section));
627
+ }
628
+ else {
629
+ let header = ul.appendChild(document.createElement("completion-section"));
630
+ header.textContent = name;
631
+ }
632
+ }
633
+ }
606
634
  const li = ul.appendChild(document.createElement("li"));
607
635
  li.id = id + "-" + i;
608
636
  li.setAttribute("role", "option");
@@ -643,32 +671,55 @@ function score(option) {
643
671
  (option.type ? 1 : 0);
644
672
  }
645
673
  function sortOptions(active, state) {
646
- let options = [], i = 0;
674
+ let options = [];
675
+ let sections = null;
676
+ let addOption = (option) => {
677
+ options.push(option);
678
+ let { section } = option.completion;
679
+ if (section) {
680
+ if (!sections)
681
+ sections = [];
682
+ let name = typeof section == "string" ? section : section.name;
683
+ if (!sections.some(s => s.name == name))
684
+ sections.push(typeof section == "string" ? { name } : section);
685
+ }
686
+ };
647
687
  for (let a of active)
648
688
  if (a.hasResult()) {
649
689
  if (a.result.filter === false) {
650
690
  let getMatch = a.result.getMatch;
651
691
  for (let option of a.result.options) {
652
- let match = [1e9 - i++];
692
+ let match = [1e9 - options.length];
653
693
  if (getMatch)
654
694
  for (let n of getMatch(option))
655
695
  match.push(n);
656
- options.push(new Option(option, a, match));
696
+ addOption(new Option(option, a, match, match[0]));
657
697
  }
658
698
  }
659
699
  else {
660
700
  let matcher = new FuzzyMatcher(state.sliceDoc(a.from, a.to)), match;
661
701
  for (let option of a.result.options)
662
702
  if (match = matcher.match(option.label)) {
663
- if (option.boost != null)
664
- match[0] += option.boost;
665
- options.push(new Option(option, a, match));
703
+ addOption(new Option(option, a, match, match[0] + (option.boost || 0)));
666
704
  }
667
705
  }
668
706
  }
707
+ if (sections) {
708
+ let sectionOrder = Object.create(null), pos = 0;
709
+ let cmp = (a, b) => { var _a, _b; return ((_a = a.rank) !== null && _a !== void 0 ? _a : 1e9) - ((_b = b.rank) !== null && _b !== void 0 ? _b : 1e9) || (a.name < b.name ? -1 : 1); };
710
+ for (let s of sections.sort(cmp)) {
711
+ pos -= 1e5;
712
+ sectionOrder[s.name] = pos;
713
+ }
714
+ for (let option of options) {
715
+ let { section } = option.completion;
716
+ if (section)
717
+ option.score += sectionOrder[typeof section == "string" ? section : section.name];
718
+ }
719
+ }
669
720
  let result = [], prev = null;
670
721
  let compare = state.facet(completionConfig).compareCompletions;
671
- for (let opt of options.sort((a, b) => (b.match[0] - a.match[0]) || compare(a.completion, b.completion))) {
722
+ for (let opt of options.sort((a, b) => (b.score - a.score) || compare(a.completion, b.completion))) {
672
723
  if (!prev || prev.label != opt.completion.label || prev.detail != opt.completion.detail ||
673
724
  (prev.type != null && opt.completion.type != null && prev.type != opt.completion.type) ||
674
725
  prev.apply != opt.completion.apply)
@@ -862,8 +913,6 @@ function checkValid(validFor, state, from, to) {
862
913
  let text = state.sliceDoc(from, to);
863
914
  return typeof validFor == "function" ? validFor(text, from, to, state) : ensureAnchor(validFor, true).test(text);
864
915
  }
865
- const startCompletionEffect = state.StateEffect.define();
866
- const closeCompletionEffect = state.StateEffect.define();
867
916
  const setActiveEffect = state.StateEffect.define({
868
917
  map(sources, mapping) { return sources.map(s => s.map(mapping)); }
869
918
  });
@@ -1070,10 +1119,13 @@ const completionPlugin = view.ViewPlugin.fromClass(class {
1070
1119
  }
1071
1120
  }, {
1072
1121
  eventHandlers: {
1073
- blur() {
1122
+ blur(event) {
1074
1123
  let state = this.view.state.field(completionState, false);
1075
- if (state && state.tooltip && this.view.state.facet(completionConfig).closeOnBlur)
1076
- this.view.dispatch({ effects: closeCompletionEffect.of(null) });
1124
+ if (state && state.tooltip && this.view.state.facet(completionConfig).closeOnBlur) {
1125
+ let dialog = state.open && view.getTooltip(this.view, state.open.tooltip);
1126
+ if (!dialog || !dialog.dom.contains(event.relatedTarget))
1127
+ this.view.dispatch({ effects: closeCompletionEffect.of(null) });
1128
+ }
1077
1129
  },
1078
1130
  compositionstart() {
1079
1131
  this.composing = 1 /* CompositionState.Started */;
@@ -1103,13 +1155,21 @@ const baseTheme = view.EditorView.baseTheme({
1103
1155
  listStyle: "none",
1104
1156
  margin: 0,
1105
1157
  padding: 0,
1158
+ "& > li, & > completion-section": {
1159
+ padding: "1px 3px",
1160
+ lineHeight: 1.2
1161
+ },
1106
1162
  "& > li": {
1107
1163
  overflowX: "hidden",
1108
1164
  textOverflow: "ellipsis",
1109
- cursor: "pointer",
1110
- padding: "1px 3px",
1111
- lineHeight: 1.2
1165
+ cursor: "pointer"
1112
1166
  },
1167
+ "& > completion-section": {
1168
+ display: "list-item",
1169
+ borderBottom: "1px solid silver",
1170
+ paddingLeft: "0.5em",
1171
+ opacity: 0.7
1172
+ }
1113
1173
  }
1114
1174
  },
1115
1175
  "&light .cm-tooltip-autocomplete ul li[aria-selected]": {
@@ -1543,9 +1603,6 @@ const closeBracketEffect = state.StateEffect.define({
1543
1603
  return mapped == null ? undefined : mapped;
1544
1604
  }
1545
1605
  });
1546
- const skipBracketEffect = state.StateEffect.define({
1547
- map(value, mapping) { return mapping.mapPos(value); }
1548
- });
1549
1606
  const closedBracket = new class extends state.RangeValue {
1550
1607
  };
1551
1608
  closedBracket.startSide = 1;
@@ -1560,12 +1617,9 @@ const bracketState = state.StateField.define({
1560
1617
  value = state.RangeSet.empty;
1561
1618
  }
1562
1619
  value = value.map(tr.changes);
1563
- for (let effect of tr.effects) {
1620
+ for (let effect of tr.effects)
1564
1621
  if (effect.is(closeBracketEffect))
1565
1622
  value = value.update({ add: [closedBracket.range(effect.value, effect.value + 1)] });
1566
- else if (effect.is(skipBracketEffect))
1567
- value = value.update({ filter: from => from != effect.value });
1568
- }
1569
1623
  return value;
1570
1624
  }
1571
1625
  });
@@ -1693,15 +1747,15 @@ function handleOpen(state$1, open, close, closeBefore) {
1693
1747
  });
1694
1748
  }
1695
1749
  function handleClose(state$1, _open, close) {
1696
- let dont = null, moved = state$1.selection.ranges.map(range => {
1750
+ let dont = null, changes = state$1.changeByRange(range => {
1697
1751
  if (range.empty && nextChar(state$1.doc, range.head) == close)
1698
- return state.EditorSelection.cursor(range.head + close.length);
1699
- return dont = range;
1752
+ return { changes: { from: range.head, to: range.head + close.length, insert: close },
1753
+ range: state.EditorSelection.cursor(range.head + close.length) };
1754
+ return dont = { range };
1700
1755
  });
1701
- return dont ? null : state$1.update({
1702
- selection: state.EditorSelection.create(moved, state$1.selection.mainIndex),
1756
+ return dont ? null : state$1.update(changes, {
1703
1757
  scrollIntoView: true,
1704
- effects: state$1.selection.ranges.map(({ from }) => skipBracketEffect.of(from))
1758
+ userEvent: "input.type"
1705
1759
  });
1706
1760
  }
1707
1761
  // Handles cases where the open and close token are the same, and
@@ -1722,8 +1776,9 @@ function handleSame(state$1, token, allowTriple, config) {
1722
1776
  }
1723
1777
  else if (closedBracketAt(state$1, pos)) {
1724
1778
  let isTriple = allowTriple && state$1.sliceDoc(pos, pos + token.length * 3) == token + token + token;
1725
- return { range: state.EditorSelection.cursor(pos + token.length * (isTriple ? 3 : 1)),
1726
- effects: skipBracketEffect.of(pos) };
1779
+ let content = isTriple ? token + token + token : token;
1780
+ return { changes: { from: pos, to: pos + content.length, insert: content },
1781
+ range: state.EditorSelection.cursor(pos + content.length) };
1727
1782
  }
1728
1783
  }
1729
1784
  else if (allowTriple && state$1.sliceDoc(pos - 2 * token.length, pos) == token + token &&
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as _codemirror_state from '@codemirror/state';
2
2
  import { EditorState, TransactionSpec, Transaction, StateCommand, Facet, Extension, StateEffect } from '@codemirror/state';
3
- import { EditorView, KeyBinding, Command } from '@codemirror/view';
3
+ import { EditorView, Rect, KeyBinding, Command } from '@codemirror/view';
4
4
  import * as _lezer_common from '@lezer/common';
5
5
 
6
6
  interface CompletionConfig {
@@ -79,6 +79,19 @@ interface CompletionConfig {
79
79
  position: number;
80
80
  }[];
81
81
  /**
82
+ By default, [info](https://codemirror.net/6/docs/ref/#autocomplet.Completion.info) tooltips are
83
+ placed to the side of the selected. This option can be used to
84
+ override that. It will be given rectangles for the list of
85
+ completions, the selected option, the info element, and the
86
+ availble [tooltip space](https://codemirror.net/6/docs/ref/#view.tooltips^config.tooltipSpace),
87
+ and should return style and/or class strings for the info
88
+ element.
89
+ */
90
+ positionInfo?: (view: EditorView, list: Rect, option: Rect, info: Rect, space: Rect) => {
91
+ style?: string;
92
+ class?: string;
93
+ };
94
+ /**
82
95
  The comparison function to use when sorting completions with the same
83
96
  match score. Defaults to using
84
97
  [`localeCompare`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare).
@@ -143,6 +156,39 @@ interface Completion {
143
156
  down the list, a positive number moves it up.
144
157
  */
145
158
  boost?: number;
159
+ /**
160
+ Can be used to divide the completion list into sections.
161
+ Completions in a given section (matched by name) will be grouped
162
+ together, with a heading above them. Options without section
163
+ will appear above all sections. A string value is equivalent to
164
+ a `{name}` object.
165
+ */
166
+ section?: string | CompletionSection;
167
+ }
168
+ /**
169
+ Object used to describe a completion
170
+ [section](https://codemirror.net/6/docs/ref/#autocomplete.Completion.section). It is recommended to
171
+ create a shared object used by all the completions in a given
172
+ section.
173
+ */
174
+ interface CompletionSection {
175
+ /**
176
+ The name of the section. If no `render` method is present, this
177
+ will be displayed above the options.
178
+ */
179
+ name: string;
180
+ /**
181
+ An optional function that renders the section header. Since the
182
+ headers are shown inside a list, you should make sure the
183
+ resulting element has a `display: list-item` style.
184
+ */
185
+ header?: (section: CompletionSection) => HTMLElement;
186
+ /**
187
+ By default, sections are ordered alphabetically by name. To
188
+ specify an explicit order, `rank` can be used. Sections with a
189
+ lower rank will be shown above sections with a higher rank.
190
+ */
191
+ rank?: number;
146
192
  }
147
193
  /**
148
194
  An instance of this is passed to completion source functions.
@@ -488,4 +534,4 @@ the currently selected completion.
488
534
  */
489
535
  declare function setSelectedCompletion(index: number): StateEffect<unknown>;
490
536
 
491
- 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 };
537
+ export { CloseBracketConfig, Completion, CompletionContext, CompletionResult, CompletionSection, 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,5 +1,5 @@
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';
1
+ import { Annotation, StateEffect, EditorSelection, codePointAt, codePointSize, fromCodePoint, Facet, combineConfig, StateField, Prec, Text, MapMode, RangeValue, RangeSet, CharCategory } from '@codemirror/state';
2
+ import { Direction, logException, showTooltip, EditorView, ViewPlugin, getTooltip, Decoration, WidgetType, keymap } from '@codemirror/view';
3
3
  import { syntaxTree, indentUnit } from '@codemirror/language';
4
4
 
5
5
  /**
@@ -133,13 +133,14 @@ function ifNotIn(nodes, source) {
133
133
  };
134
134
  }
135
135
  class Option {
136
- constructor(completion, source, match) {
136
+ constructor(completion, source, match, score) {
137
137
  this.completion = completion;
138
138
  this.source = source;
139
139
  this.match = match;
140
+ this.score = score;
140
141
  }
141
142
  }
142
- function cur(state) { return state.selection.main.head; }
143
+ function cur(state) { return state.selection.main.from; }
143
144
  // Make sure the given regexp has a $ at its end and, if `start` is
144
145
  // true, a ^ at its start.
145
146
  function ensureAnchor(expr, start) {
@@ -161,18 +162,13 @@ completion's text in the main selection range, and any other
161
162
  selection range that has the same text in front of it.
162
163
  */
163
164
  function insertCompletionText(state, text, from, to) {
165
+ let { main } = state.selection, len = to - from;
164
166
  return Object.assign(Object.assign({}, state.changeByRange(range => {
165
- if (range == state.selection.main)
166
- return {
167
- changes: { from: from, to: to, insert: text },
168
- range: EditorSelection.cursor(from + text.length)
169
- };
170
- let len = to - from;
171
- if (!range.empty ||
172
- len && state.sliceDoc(range.from - len, range.from) != state.sliceDoc(from, to))
167
+ if (range != main && len &&
168
+ state.sliceDoc(range.from - len, range.from + to - main.from) != state.sliceDoc(from, to))
173
169
  return { range };
174
170
  return {
175
- changes: { from: range.from - len, to: range.from, insert: text },
171
+ changes: { from: range.from - len, to: to == main.from ? range.to : range.from + to - main.from, insert: text },
176
172
  range: EditorSelection.cursor(range.from - len + text.length)
177
173
  };
178
174
  })), { userEvent: "input.complete" });
@@ -194,6 +190,8 @@ function asSource(source) {
194
190
  SourceCache.set(source, known = completeFromList(source));
195
191
  return known;
196
192
  }
193
+ const startCompletionEffect = /*@__PURE__*/StateEffect.define();
194
+ const closeCompletionEffect = /*@__PURE__*/StateEffect.define();
197
195
 
198
196
  // A pattern matcher for fuzzy completion matching. Create an instance
199
197
  // once for a pattern, and then use that to match any number of
@@ -339,6 +337,7 @@ const completionConfig = /*@__PURE__*/Facet.define({
339
337
  aboveCursor: false,
340
338
  icons: true,
341
339
  addToOptions: [],
340
+ positionInfo: defaultPositionInfo,
342
341
  compareCompletions: (a, b) => a.label.localeCompare(b.label),
343
342
  interactionDelay: 75
344
343
  }, {
@@ -354,6 +353,36 @@ const completionConfig = /*@__PURE__*/Facet.define({
354
353
  function joinClass(a, b) {
355
354
  return a ? b ? a + " " + b : a : b;
356
355
  }
356
+ function defaultPositionInfo(view, list, option, info, space) {
357
+ let rtl = view.textDirection == Direction.RTL, left = rtl, narrow = false;
358
+ let side = "top", offset, maxWidth;
359
+ let spaceLeft = list.left - space.left, spaceRight = space.right - list.right;
360
+ let infoWidth = info.right - info.left, infoHeight = info.bottom - info.top;
361
+ if (left && spaceLeft < Math.min(infoWidth, spaceRight))
362
+ left = false;
363
+ else if (!left && spaceRight < Math.min(infoWidth, spaceLeft))
364
+ left = true;
365
+ if (infoWidth <= (left ? spaceLeft : spaceRight)) {
366
+ offset = Math.max(space.top, Math.min(option.top, space.bottom - infoHeight)) - list.top;
367
+ maxWidth = Math.min(400 /* Info.Width */, left ? spaceLeft : spaceRight);
368
+ }
369
+ else {
370
+ narrow = true;
371
+ maxWidth = Math.min(400 /* Info.Width */, (rtl ? list.right : space.right - list.left) - 30 /* Info.Margin */);
372
+ let spaceBelow = space.bottom - list.bottom;
373
+ if (spaceBelow >= infoHeight || spaceBelow > list.top) { // Below the completion
374
+ offset = option.bottom - list.top;
375
+ }
376
+ else { // Above it
377
+ side = "bottom";
378
+ offset = list.bottom - option.top;
379
+ }
380
+ }
381
+ return {
382
+ style: `${side}: ${offset}px; max-width: ${maxWidth}px`,
383
+ class: "cm-completionInfo-" + (narrow ? (rtl ? "left-narrow" : "right-narrow") : left ? "left" : "right")
384
+ };
385
+ }
357
386
 
358
387
  function optionContent(config) {
359
388
  let content = config.addToOptions.slice();
@@ -418,9 +447,9 @@ class CompletionTooltip {
418
447
  this.view = view;
419
448
  this.stateField = stateField;
420
449
  this.info = null;
421
- this.placeInfo = {
450
+ this.placeInfoReq = {
422
451
  read: () => this.measureInfo(),
423
- write: (pos) => this.positionInfo(pos),
452
+ write: (pos) => this.placeInfo(pos),
424
453
  key: this
425
454
  };
426
455
  this.space = null;
@@ -444,10 +473,16 @@ class CompletionTooltip {
444
473
  }
445
474
  }
446
475
  });
476
+ this.dom.addEventListener("focusout", (e) => {
477
+ let state = view.state.field(this.stateField, false);
478
+ if (state && state.tooltip && view.state.facet(completionConfig).closeOnBlur &&
479
+ e.relatedTarget != view.contentDOM)
480
+ view.dispatch({ effects: closeCompletionEffect.of(null) });
481
+ });
447
482
  this.list = this.dom.appendChild(this.createListBox(options, cState.id, this.range));
448
483
  this.list.addEventListener("scroll", () => {
449
484
  if (this.info)
450
- this.view.requestMeasure(this.placeInfo);
485
+ this.view.requestMeasure(this.placeInfoReq);
451
486
  });
452
487
  }
453
488
  mount() { this.updateSel(); }
@@ -477,7 +512,7 @@ class CompletionTooltip {
477
512
  positioned(space) {
478
513
  this.space = space;
479
514
  if (this.info)
480
- this.view.requestMeasure(this.placeInfo);
515
+ this.view.requestMeasure(this.placeInfoReq);
481
516
  }
482
517
  updateSel() {
483
518
  let cState = this.view.state.field(this.stateField), open = cState.open;
@@ -487,7 +522,7 @@ class CompletionTooltip {
487
522
  this.list = this.dom.appendChild(this.createListBox(open.options, cState.id, this.range));
488
523
  this.list.addEventListener("scroll", () => {
489
524
  if (this.info)
490
- this.view.requestMeasure(this.placeInfo);
525
+ this.view.requestMeasure(this.placeInfoReq);
491
526
  });
492
527
  }
493
528
  if (this.updateSelectedOption(open.selected)) {
@@ -518,12 +553,15 @@ class CompletionTooltip {
518
553
  dom.className = "cm-tooltip cm-completionInfo";
519
554
  dom.appendChild(content);
520
555
  this.dom.appendChild(dom);
521
- this.view.requestMeasure(this.placeInfo);
556
+ this.view.requestMeasure(this.placeInfoReq);
522
557
  }
523
558
  updateSelectedOption(selected) {
524
559
  let set = null;
525
560
  for (let opt = this.list.firstChild, i = this.range.from; opt; opt = opt.nextSibling, i++) {
526
- if (i == selected) {
561
+ if (opt.nodeName != "LI" || !opt.id) {
562
+ i--; // A section header
563
+ }
564
+ else if (i == selected) {
527
565
  if (!opt.hasAttribute("aria-selected")) {
528
566
  opt.setAttribute("aria-selected", "true");
529
567
  set = opt;
@@ -553,41 +591,17 @@ class CompletionTooltip {
553
591
  if (selRect.top > Math.min(space.bottom, listRect.bottom) - 10 ||
554
592
  selRect.bottom < Math.max(space.top, listRect.top) + 10)
555
593
  return null;
556
- let rtl = this.view.textDirection == Direction.RTL, left = rtl, narrow = false, maxWidth;
557
- let top = "", bottom = "";
558
- let spaceLeft = listRect.left - space.left, spaceRight = space.right - listRect.right;
559
- if (left && spaceLeft < Math.min(infoRect.width, spaceRight))
560
- left = false;
561
- else if (!left && spaceRight < Math.min(infoRect.width, spaceLeft))
562
- left = true;
563
- if (infoRect.width <= (left ? spaceLeft : spaceRight)) {
564
- top = (Math.max(space.top, Math.min(selRect.top, space.bottom - infoRect.height)) - listRect.top) + "px";
565
- maxWidth = Math.min(400 /* Info.Width */, left ? spaceLeft : spaceRight) + "px";
566
- }
567
- else {
568
- narrow = true;
569
- maxWidth = Math.min(400 /* Info.Width */, (rtl ? listRect.right : space.right - listRect.left) - 30 /* Info.Margin */) + "px";
570
- let spaceBelow = space.bottom - listRect.bottom;
571
- if (spaceBelow >= infoRect.height || spaceBelow > listRect.top) // Below the completion
572
- top = (selRect.bottom - listRect.top) + "px";
573
- else // Above it
574
- bottom = (listRect.bottom - selRect.top) + "px";
575
- }
576
- return {
577
- top, bottom, maxWidth,
578
- class: narrow ? (rtl ? "left-narrow" : "right-narrow") : left ? "left" : "right",
579
- };
594
+ return this.view.state.facet(completionConfig).positionInfo(this.view, listRect, selRect, infoRect, space);
580
595
  }
581
- positionInfo(pos) {
596
+ placeInfo(pos) {
582
597
  if (this.info) {
583
598
  if (pos) {
584
- this.info.style.top = pos.top;
585
- this.info.style.bottom = pos.bottom;
586
- this.info.style.maxWidth = pos.maxWidth;
587
- this.info.className = "cm-tooltip cm-completionInfo cm-completionInfo-" + pos.class;
599
+ if (pos.style)
600
+ this.info.style.cssText = pos.style;
601
+ this.info.className = "cm-tooltip cm-completionInfo " + (pos.class || "");
588
602
  }
589
603
  else {
590
- this.info.style.top = "-1e6px";
604
+ this.info.style.cssText = "top: -1e6px";
591
605
  }
592
606
  }
593
607
  }
@@ -597,8 +611,22 @@ class CompletionTooltip {
597
611
  ul.setAttribute("role", "listbox");
598
612
  ul.setAttribute("aria-expanded", "true");
599
613
  ul.setAttribute("aria-label", this.view.state.phrase("Completions"));
614
+ let curSection = null;
600
615
  for (let i = range.from; i < range.to; i++) {
601
- let { completion, match } = options[i];
616
+ let { completion, match } = options[i], { section } = completion;
617
+ if (section) {
618
+ let name = typeof section == "string" ? section : section.name;
619
+ if (name != curSection && (i > range.from || range.from == 0)) {
620
+ curSection = name;
621
+ if (typeof section != "string" && section.header) {
622
+ ul.appendChild(section.header(section));
623
+ }
624
+ else {
625
+ let header = ul.appendChild(document.createElement("completion-section"));
626
+ header.textContent = name;
627
+ }
628
+ }
629
+ }
602
630
  const li = ul.appendChild(document.createElement("li"));
603
631
  li.id = id + "-" + i;
604
632
  li.setAttribute("role", "option");
@@ -639,32 +667,55 @@ function score(option) {
639
667
  (option.type ? 1 : 0);
640
668
  }
641
669
  function sortOptions(active, state) {
642
- let options = [], i = 0;
670
+ let options = [];
671
+ let sections = null;
672
+ let addOption = (option) => {
673
+ options.push(option);
674
+ let { section } = option.completion;
675
+ if (section) {
676
+ if (!sections)
677
+ sections = [];
678
+ let name = typeof section == "string" ? section : section.name;
679
+ if (!sections.some(s => s.name == name))
680
+ sections.push(typeof section == "string" ? { name } : section);
681
+ }
682
+ };
643
683
  for (let a of active)
644
684
  if (a.hasResult()) {
645
685
  if (a.result.filter === false) {
646
686
  let getMatch = a.result.getMatch;
647
687
  for (let option of a.result.options) {
648
- let match = [1e9 - i++];
688
+ let match = [1e9 - options.length];
649
689
  if (getMatch)
650
690
  for (let n of getMatch(option))
651
691
  match.push(n);
652
- options.push(new Option(option, a, match));
692
+ addOption(new Option(option, a, match, match[0]));
653
693
  }
654
694
  }
655
695
  else {
656
696
  let matcher = new FuzzyMatcher(state.sliceDoc(a.from, a.to)), match;
657
697
  for (let option of a.result.options)
658
698
  if (match = matcher.match(option.label)) {
659
- if (option.boost != null)
660
- match[0] += option.boost;
661
- options.push(new Option(option, a, match));
699
+ addOption(new Option(option, a, match, match[0] + (option.boost || 0)));
662
700
  }
663
701
  }
664
702
  }
703
+ if (sections) {
704
+ let sectionOrder = Object.create(null), pos = 0;
705
+ let cmp = (a, b) => { var _a, _b; return ((_a = a.rank) !== null && _a !== void 0 ? _a : 1e9) - ((_b = b.rank) !== null && _b !== void 0 ? _b : 1e9) || (a.name < b.name ? -1 : 1); };
706
+ for (let s of sections.sort(cmp)) {
707
+ pos -= 1e5;
708
+ sectionOrder[s.name] = pos;
709
+ }
710
+ for (let option of options) {
711
+ let { section } = option.completion;
712
+ if (section)
713
+ option.score += sectionOrder[typeof section == "string" ? section : section.name];
714
+ }
715
+ }
665
716
  let result = [], prev = null;
666
717
  let compare = state.facet(completionConfig).compareCompletions;
667
- for (let opt of options.sort((a, b) => (b.match[0] - a.match[0]) || compare(a.completion, b.completion))) {
718
+ for (let opt of options.sort((a, b) => (b.score - a.score) || compare(a.completion, b.completion))) {
668
719
  if (!prev || prev.label != opt.completion.label || prev.detail != opt.completion.detail ||
669
720
  (prev.type != null && opt.completion.type != null && prev.type != opt.completion.type) ||
670
721
  prev.apply != opt.completion.apply)
@@ -858,8 +909,6 @@ function checkValid(validFor, state, from, to) {
858
909
  let text = state.sliceDoc(from, to);
859
910
  return typeof validFor == "function" ? validFor(text, from, to, state) : ensureAnchor(validFor, true).test(text);
860
911
  }
861
- const startCompletionEffect = /*@__PURE__*/StateEffect.define();
862
- const closeCompletionEffect = /*@__PURE__*/StateEffect.define();
863
912
  const setActiveEffect = /*@__PURE__*/StateEffect.define({
864
913
  map(sources, mapping) { return sources.map(s => s.map(mapping)); }
865
914
  });
@@ -1066,10 +1115,13 @@ const completionPlugin = /*@__PURE__*/ViewPlugin.fromClass(class {
1066
1115
  }
1067
1116
  }, {
1068
1117
  eventHandlers: {
1069
- blur() {
1118
+ blur(event) {
1070
1119
  let state = this.view.state.field(completionState, false);
1071
- if (state && state.tooltip && this.view.state.facet(completionConfig).closeOnBlur)
1072
- this.view.dispatch({ effects: closeCompletionEffect.of(null) });
1120
+ if (state && state.tooltip && this.view.state.facet(completionConfig).closeOnBlur) {
1121
+ let dialog = state.open && getTooltip(this.view, state.open.tooltip);
1122
+ if (!dialog || !dialog.dom.contains(event.relatedTarget))
1123
+ this.view.dispatch({ effects: closeCompletionEffect.of(null) });
1124
+ }
1073
1125
  },
1074
1126
  compositionstart() {
1075
1127
  this.composing = 1 /* CompositionState.Started */;
@@ -1099,13 +1151,21 @@ const baseTheme = /*@__PURE__*/EditorView.baseTheme({
1099
1151
  listStyle: "none",
1100
1152
  margin: 0,
1101
1153
  padding: 0,
1154
+ "& > li, & > completion-section": {
1155
+ padding: "1px 3px",
1156
+ lineHeight: 1.2
1157
+ },
1102
1158
  "& > li": {
1103
1159
  overflowX: "hidden",
1104
1160
  textOverflow: "ellipsis",
1105
- cursor: "pointer",
1106
- padding: "1px 3px",
1107
- lineHeight: 1.2
1161
+ cursor: "pointer"
1108
1162
  },
1163
+ "& > completion-section": {
1164
+ display: "list-item",
1165
+ borderBottom: "1px solid silver",
1166
+ paddingLeft: "0.5em",
1167
+ opacity: 0.7
1168
+ }
1109
1169
  }
1110
1170
  },
1111
1171
  "&light .cm-tooltip-autocomplete ul li[aria-selected]": {
@@ -1539,9 +1599,6 @@ const closeBracketEffect = /*@__PURE__*/StateEffect.define({
1539
1599
  return mapped == null ? undefined : mapped;
1540
1600
  }
1541
1601
  });
1542
- const skipBracketEffect = /*@__PURE__*/StateEffect.define({
1543
- map(value, mapping) { return mapping.mapPos(value); }
1544
- });
1545
1602
  const closedBracket = /*@__PURE__*/new class extends RangeValue {
1546
1603
  };
1547
1604
  closedBracket.startSide = 1;
@@ -1556,12 +1613,9 @@ const bracketState = /*@__PURE__*/StateField.define({
1556
1613
  value = RangeSet.empty;
1557
1614
  }
1558
1615
  value = value.map(tr.changes);
1559
- for (let effect of tr.effects) {
1616
+ for (let effect of tr.effects)
1560
1617
  if (effect.is(closeBracketEffect))
1561
1618
  value = value.update({ add: [closedBracket.range(effect.value, effect.value + 1)] });
1562
- else if (effect.is(skipBracketEffect))
1563
- value = value.update({ filter: from => from != effect.value });
1564
- }
1565
1619
  return value;
1566
1620
  }
1567
1621
  });
@@ -1689,15 +1743,15 @@ function handleOpen(state, open, close, closeBefore) {
1689
1743
  });
1690
1744
  }
1691
1745
  function handleClose(state, _open, close) {
1692
- let dont = null, moved = state.selection.ranges.map(range => {
1746
+ let dont = null, changes = state.changeByRange(range => {
1693
1747
  if (range.empty && nextChar(state.doc, range.head) == close)
1694
- return EditorSelection.cursor(range.head + close.length);
1695
- return dont = range;
1748
+ return { changes: { from: range.head, to: range.head + close.length, insert: close },
1749
+ range: EditorSelection.cursor(range.head + close.length) };
1750
+ return dont = { range };
1696
1751
  });
1697
- return dont ? null : state.update({
1698
- selection: EditorSelection.create(moved, state.selection.mainIndex),
1752
+ return dont ? null : state.update(changes, {
1699
1753
  scrollIntoView: true,
1700
- effects: state.selection.ranges.map(({ from }) => skipBracketEffect.of(from))
1754
+ userEvent: "input.type"
1701
1755
  });
1702
1756
  }
1703
1757
  // Handles cases where the open and close token are the same, and
@@ -1718,8 +1772,9 @@ function handleSame(state, token, allowTriple, config) {
1718
1772
  }
1719
1773
  else if (closedBracketAt(state, pos)) {
1720
1774
  let isTriple = allowTriple && state.sliceDoc(pos, pos + token.length * 3) == token + token + token;
1721
- return { range: EditorSelection.cursor(pos + token.length * (isTriple ? 3 : 1)),
1722
- effects: skipBracketEffect.of(pos) };
1775
+ let content = isTriple ? token + token + token : token;
1776
+ return { changes: { from: pos, to: pos + content.length, insert: content },
1777
+ range: EditorSelection.cursor(pos + content.length) };
1723
1778
  }
1724
1779
  }
1725
1780
  else if (allowTriple && state.sliceDoc(pos - 2 * token.length, pos) == token + token &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemirror/autocomplete",
3
- "version": "6.4.2",
3
+ "version": "6.5.1",
4
4
  "description": "Autocompletion for the CodeMirror code editor",
5
5
  "scripts": {
6
6
  "test": "cm-runtests",