@codemirror/autocomplete 6.4.2 → 6.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## 6.5.0 (2023-04-13)
2
+
3
+ ### Bug fixes
4
+
5
+ When `closeBrackets` skips a bracket, it now generates a change that overwrites the bracket.
6
+
7
+ Replace the entire selected range when picking a completion with a non-cursor selection active.
8
+
9
+ ### New features
10
+
11
+ Completions can now provide a `section` field that is used to group them into sections.
12
+
13
+ The new `positionInfo` option can be used to provide custom logic for positioning the info tooltips.
14
+
1
15
  ## 6.4.2 (2023-02-17)
2
16
 
3
17
  ### 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" });
@@ -343,6 +339,7 @@ const completionConfig = state.Facet.define({
343
339
  aboveCursor: false,
344
340
  icons: true,
345
341
  addToOptions: [],
342
+ positionInfo: defaultPositionInfo,
346
343
  compareCompletions: (a, b) => a.label.localeCompare(b.label),
347
344
  interactionDelay: 75
348
345
  }, {
@@ -358,6 +355,36 @@ const completionConfig = state.Facet.define({
358
355
  function joinClass(a, b) {
359
356
  return a ? b ? a + " " + b : a : b;
360
357
  }
358
+ function defaultPositionInfo(view$1, list, option, info, space) {
359
+ let rtl = view$1.textDirection == view.Direction.RTL, left = rtl, narrow = false;
360
+ let side = "top", offset, maxWidth;
361
+ let spaceLeft = list.left - space.left, spaceRight = space.right - list.right;
362
+ let infoWidth = info.right - info.left, infoHeight = info.bottom - info.top;
363
+ if (left && spaceLeft < Math.min(infoWidth, spaceRight))
364
+ left = false;
365
+ else if (!left && spaceRight < Math.min(infoWidth, spaceLeft))
366
+ left = true;
367
+ if (infoWidth <= (left ? spaceLeft : spaceRight)) {
368
+ offset = Math.max(space.top, Math.min(option.top, space.bottom - infoHeight)) - list.top;
369
+ maxWidth = Math.min(400 /* Info.Width */, left ? spaceLeft : spaceRight);
370
+ }
371
+ else {
372
+ narrow = true;
373
+ maxWidth = Math.min(400 /* Info.Width */, (rtl ? list.right : space.right - list.left) - 30 /* Info.Margin */);
374
+ let spaceBelow = space.bottom - list.bottom;
375
+ if (spaceBelow >= infoHeight || spaceBelow > list.top) { // Below the completion
376
+ offset = option.bottom - list.top;
377
+ }
378
+ else { // Above it
379
+ side = "bottom";
380
+ offset = list.bottom - option.top;
381
+ }
382
+ }
383
+ return {
384
+ style: `${side}: ${offset}px; max-width: ${maxWidth}px`,
385
+ class: "cm-completionInfo-" + (narrow ? (rtl ? "left-narrow" : "right-narrow") : left ? "left" : "right")
386
+ };
387
+ }
361
388
 
362
389
  function optionContent(config) {
363
390
  let content = config.addToOptions.slice();
@@ -422,9 +449,9 @@ class CompletionTooltip {
422
449
  this.view = view;
423
450
  this.stateField = stateField;
424
451
  this.info = null;
425
- this.placeInfo = {
452
+ this.placeInfoReq = {
426
453
  read: () => this.measureInfo(),
427
- write: (pos) => this.positionInfo(pos),
454
+ write: (pos) => this.placeInfo(pos),
428
455
  key: this
429
456
  };
430
457
  this.space = null;
@@ -451,7 +478,7 @@ class CompletionTooltip {
451
478
  this.list = this.dom.appendChild(this.createListBox(options, cState.id, this.range));
452
479
  this.list.addEventListener("scroll", () => {
453
480
  if (this.info)
454
- this.view.requestMeasure(this.placeInfo);
481
+ this.view.requestMeasure(this.placeInfoReq);
455
482
  });
456
483
  }
457
484
  mount() { this.updateSel(); }
@@ -481,7 +508,7 @@ class CompletionTooltip {
481
508
  positioned(space) {
482
509
  this.space = space;
483
510
  if (this.info)
484
- this.view.requestMeasure(this.placeInfo);
511
+ this.view.requestMeasure(this.placeInfoReq);
485
512
  }
486
513
  updateSel() {
487
514
  let cState = this.view.state.field(this.stateField), open = cState.open;
@@ -491,7 +518,7 @@ class CompletionTooltip {
491
518
  this.list = this.dom.appendChild(this.createListBox(open.options, cState.id, this.range));
492
519
  this.list.addEventListener("scroll", () => {
493
520
  if (this.info)
494
- this.view.requestMeasure(this.placeInfo);
521
+ this.view.requestMeasure(this.placeInfoReq);
495
522
  });
496
523
  }
497
524
  if (this.updateSelectedOption(open.selected)) {
@@ -522,12 +549,15 @@ class CompletionTooltip {
522
549
  dom.className = "cm-tooltip cm-completionInfo";
523
550
  dom.appendChild(content);
524
551
  this.dom.appendChild(dom);
525
- this.view.requestMeasure(this.placeInfo);
552
+ this.view.requestMeasure(this.placeInfoReq);
526
553
  }
527
554
  updateSelectedOption(selected) {
528
555
  let set = null;
529
556
  for (let opt = this.list.firstChild, i = this.range.from; opt; opt = opt.nextSibling, i++) {
530
- if (i == selected) {
557
+ if (opt.nodeName != "LI" || !opt.id) {
558
+ i--; // A section header
559
+ }
560
+ else if (i == selected) {
531
561
  if (!opt.hasAttribute("aria-selected")) {
532
562
  opt.setAttribute("aria-selected", "true");
533
563
  set = opt;
@@ -557,41 +587,17 @@ class CompletionTooltip {
557
587
  if (selRect.top > Math.min(space.bottom, listRect.bottom) - 10 ||
558
588
  selRect.bottom < Math.max(space.top, listRect.top) + 10)
559
589
  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
- };
590
+ return this.view.state.facet(completionConfig).positionInfo(this.view, listRect, selRect, infoRect, space);
584
591
  }
585
- positionInfo(pos) {
592
+ placeInfo(pos) {
586
593
  if (this.info) {
587
594
  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;
595
+ if (pos.style)
596
+ this.info.style.cssText = pos.style;
597
+ this.info.className = "cm-tooltip cm-completionInfo " + (pos.class || "");
592
598
  }
593
599
  else {
594
- this.info.style.top = "-1e6px";
600
+ this.info.style.cssText = "top: -1e6px";
595
601
  }
596
602
  }
597
603
  }
@@ -601,8 +607,22 @@ class CompletionTooltip {
601
607
  ul.setAttribute("role", "listbox");
602
608
  ul.setAttribute("aria-expanded", "true");
603
609
  ul.setAttribute("aria-label", this.view.state.phrase("Completions"));
610
+ let curSection = null;
604
611
  for (let i = range.from; i < range.to; i++) {
605
- let { completion, match } = options[i];
612
+ let { completion, match } = options[i], { section } = completion;
613
+ if (section) {
614
+ let name = typeof section == "string" ? section : section.name;
615
+ if (name != curSection && (i > range.from || range.from == 0)) {
616
+ curSection = name;
617
+ if (typeof section != "string" && section.header) {
618
+ ul.appendChild(section.header(section));
619
+ }
620
+ else {
621
+ let header = ul.appendChild(document.createElement("completion-section"));
622
+ header.textContent = name;
623
+ }
624
+ }
625
+ }
606
626
  const li = ul.appendChild(document.createElement("li"));
607
627
  li.id = id + "-" + i;
608
628
  li.setAttribute("role", "option");
@@ -643,32 +663,55 @@ function score(option) {
643
663
  (option.type ? 1 : 0);
644
664
  }
645
665
  function sortOptions(active, state) {
646
- let options = [], i = 0;
666
+ let options = [];
667
+ let sections = null;
668
+ let addOption = (option) => {
669
+ options.push(option);
670
+ let { section } = option.completion;
671
+ if (section) {
672
+ if (!sections)
673
+ sections = [];
674
+ let name = typeof section == "string" ? section : section.name;
675
+ if (!sections.some(s => s.name == name))
676
+ sections.push(typeof section == "string" ? { name } : section);
677
+ }
678
+ };
647
679
  for (let a of active)
648
680
  if (a.hasResult()) {
649
681
  if (a.result.filter === false) {
650
682
  let getMatch = a.result.getMatch;
651
683
  for (let option of a.result.options) {
652
- let match = [1e9 - i++];
684
+ let match = [1e9 - options.length];
653
685
  if (getMatch)
654
686
  for (let n of getMatch(option))
655
687
  match.push(n);
656
- options.push(new Option(option, a, match));
688
+ addOption(new Option(option, a, match, match[0]));
657
689
  }
658
690
  }
659
691
  else {
660
692
  let matcher = new FuzzyMatcher(state.sliceDoc(a.from, a.to)), match;
661
693
  for (let option of a.result.options)
662
694
  if (match = matcher.match(option.label)) {
663
- if (option.boost != null)
664
- match[0] += option.boost;
665
- options.push(new Option(option, a, match));
695
+ addOption(new Option(option, a, match, match[0] + (option.boost || 0)));
666
696
  }
667
697
  }
668
698
  }
699
+ if (sections) {
700
+ let sectionOrder = Object.create(null), pos = 0;
701
+ 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); };
702
+ for (let s of sections.sort(cmp)) {
703
+ pos -= 1e5;
704
+ sectionOrder[s.name] = pos;
705
+ }
706
+ for (let option of options) {
707
+ let { section } = option.completion;
708
+ if (section)
709
+ option.score += sectionOrder[typeof section == "string" ? section : section.name];
710
+ }
711
+ }
669
712
  let result = [], prev = null;
670
713
  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))) {
714
+ for (let opt of options.sort((a, b) => (b.score - a.score) || compare(a.completion, b.completion))) {
672
715
  if (!prev || prev.label != opt.completion.label || prev.detail != opt.completion.detail ||
673
716
  (prev.type != null && opt.completion.type != null && prev.type != opt.completion.type) ||
674
717
  prev.apply != opt.completion.apply)
@@ -1103,13 +1146,21 @@ const baseTheme = view.EditorView.baseTheme({
1103
1146
  listStyle: "none",
1104
1147
  margin: 0,
1105
1148
  padding: 0,
1149
+ "& > li, & > completion-section": {
1150
+ padding: "1px 3px",
1151
+ lineHeight: 1.2
1152
+ },
1106
1153
  "& > li": {
1107
1154
  overflowX: "hidden",
1108
1155
  textOverflow: "ellipsis",
1109
- cursor: "pointer",
1110
- padding: "1px 3px",
1111
- lineHeight: 1.2
1156
+ cursor: "pointer"
1112
1157
  },
1158
+ "& > completion-section": {
1159
+ display: "list-item",
1160
+ borderBottom: "1px solid silver",
1161
+ paddingLeft: "0.5em",
1162
+ opacity: 0.7
1163
+ }
1113
1164
  }
1114
1165
  },
1115
1166
  "&light .cm-tooltip-autocomplete ul li[aria-selected]": {
@@ -1543,9 +1594,6 @@ const closeBracketEffect = state.StateEffect.define({
1543
1594
  return mapped == null ? undefined : mapped;
1544
1595
  }
1545
1596
  });
1546
- const skipBracketEffect = state.StateEffect.define({
1547
- map(value, mapping) { return mapping.mapPos(value); }
1548
- });
1549
1597
  const closedBracket = new class extends state.RangeValue {
1550
1598
  };
1551
1599
  closedBracket.startSide = 1;
@@ -1560,12 +1608,9 @@ const bracketState = state.StateField.define({
1560
1608
  value = state.RangeSet.empty;
1561
1609
  }
1562
1610
  value = value.map(tr.changes);
1563
- for (let effect of tr.effects) {
1611
+ for (let effect of tr.effects)
1564
1612
  if (effect.is(closeBracketEffect))
1565
1613
  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
1614
  return value;
1570
1615
  }
1571
1616
  });
@@ -1693,15 +1738,15 @@ function handleOpen(state$1, open, close, closeBefore) {
1693
1738
  });
1694
1739
  }
1695
1740
  function handleClose(state$1, _open, close) {
1696
- let dont = null, moved = state$1.selection.ranges.map(range => {
1741
+ let dont = null, changes = state$1.changeByRange(range => {
1697
1742
  if (range.empty && nextChar(state$1.doc, range.head) == close)
1698
- return state.EditorSelection.cursor(range.head + close.length);
1699
- return dont = range;
1743
+ return { changes: { from: range.head, to: range.head + close.length, insert: close },
1744
+ range: state.EditorSelection.cursor(range.head + close.length) };
1745
+ return dont = { range };
1700
1746
  });
1701
- return dont ? null : state$1.update({
1702
- selection: state.EditorSelection.create(moved, state$1.selection.mainIndex),
1747
+ return dont ? null : state$1.update(changes, {
1703
1748
  scrollIntoView: true,
1704
- effects: state$1.selection.ranges.map(({ from }) => skipBracketEffect.of(from))
1749
+ userEvent: "input.type"
1705
1750
  });
1706
1751
  }
1707
1752
  // Handles cases where the open and close token are the same, and
@@ -1722,8 +1767,9 @@ function handleSame(state$1, token, allowTriple, config) {
1722
1767
  }
1723
1768
  else if (closedBracketAt(state$1, pos)) {
1724
1769
  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) };
1770
+ let content = isTriple ? token + token + token : token;
1771
+ return { changes: { from: pos, to: pos + content.length, insert: content },
1772
+ range: state.EditorSelection.cursor(pos + content.length) };
1727
1773
  }
1728
1774
  }
1729
1775
  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
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';
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" });
@@ -339,6 +335,7 @@ const completionConfig = /*@__PURE__*/Facet.define({
339
335
  aboveCursor: false,
340
336
  icons: true,
341
337
  addToOptions: [],
338
+ positionInfo: defaultPositionInfo,
342
339
  compareCompletions: (a, b) => a.label.localeCompare(b.label),
343
340
  interactionDelay: 75
344
341
  }, {
@@ -354,6 +351,36 @@ const completionConfig = /*@__PURE__*/Facet.define({
354
351
  function joinClass(a, b) {
355
352
  return a ? b ? a + " " + b : a : b;
356
353
  }
354
+ function defaultPositionInfo(view, list, option, info, space) {
355
+ let rtl = view.textDirection == Direction.RTL, left = rtl, narrow = false;
356
+ let side = "top", offset, maxWidth;
357
+ let spaceLeft = list.left - space.left, spaceRight = space.right - list.right;
358
+ let infoWidth = info.right - info.left, infoHeight = info.bottom - info.top;
359
+ if (left && spaceLeft < Math.min(infoWidth, spaceRight))
360
+ left = false;
361
+ else if (!left && spaceRight < Math.min(infoWidth, spaceLeft))
362
+ left = true;
363
+ if (infoWidth <= (left ? spaceLeft : spaceRight)) {
364
+ offset = Math.max(space.top, Math.min(option.top, space.bottom - infoHeight)) - list.top;
365
+ maxWidth = Math.min(400 /* Info.Width */, left ? spaceLeft : spaceRight);
366
+ }
367
+ else {
368
+ narrow = true;
369
+ maxWidth = Math.min(400 /* Info.Width */, (rtl ? list.right : space.right - list.left) - 30 /* Info.Margin */);
370
+ let spaceBelow = space.bottom - list.bottom;
371
+ if (spaceBelow >= infoHeight || spaceBelow > list.top) { // Below the completion
372
+ offset = option.bottom - list.top;
373
+ }
374
+ else { // Above it
375
+ side = "bottom";
376
+ offset = list.bottom - option.top;
377
+ }
378
+ }
379
+ return {
380
+ style: `${side}: ${offset}px; max-width: ${maxWidth}px`,
381
+ class: "cm-completionInfo-" + (narrow ? (rtl ? "left-narrow" : "right-narrow") : left ? "left" : "right")
382
+ };
383
+ }
357
384
 
358
385
  function optionContent(config) {
359
386
  let content = config.addToOptions.slice();
@@ -418,9 +445,9 @@ class CompletionTooltip {
418
445
  this.view = view;
419
446
  this.stateField = stateField;
420
447
  this.info = null;
421
- this.placeInfo = {
448
+ this.placeInfoReq = {
422
449
  read: () => this.measureInfo(),
423
- write: (pos) => this.positionInfo(pos),
450
+ write: (pos) => this.placeInfo(pos),
424
451
  key: this
425
452
  };
426
453
  this.space = null;
@@ -447,7 +474,7 @@ class CompletionTooltip {
447
474
  this.list = this.dom.appendChild(this.createListBox(options, cState.id, this.range));
448
475
  this.list.addEventListener("scroll", () => {
449
476
  if (this.info)
450
- this.view.requestMeasure(this.placeInfo);
477
+ this.view.requestMeasure(this.placeInfoReq);
451
478
  });
452
479
  }
453
480
  mount() { this.updateSel(); }
@@ -477,7 +504,7 @@ class CompletionTooltip {
477
504
  positioned(space) {
478
505
  this.space = space;
479
506
  if (this.info)
480
- this.view.requestMeasure(this.placeInfo);
507
+ this.view.requestMeasure(this.placeInfoReq);
481
508
  }
482
509
  updateSel() {
483
510
  let cState = this.view.state.field(this.stateField), open = cState.open;
@@ -487,7 +514,7 @@ class CompletionTooltip {
487
514
  this.list = this.dom.appendChild(this.createListBox(open.options, cState.id, this.range));
488
515
  this.list.addEventListener("scroll", () => {
489
516
  if (this.info)
490
- this.view.requestMeasure(this.placeInfo);
517
+ this.view.requestMeasure(this.placeInfoReq);
491
518
  });
492
519
  }
493
520
  if (this.updateSelectedOption(open.selected)) {
@@ -518,12 +545,15 @@ class CompletionTooltip {
518
545
  dom.className = "cm-tooltip cm-completionInfo";
519
546
  dom.appendChild(content);
520
547
  this.dom.appendChild(dom);
521
- this.view.requestMeasure(this.placeInfo);
548
+ this.view.requestMeasure(this.placeInfoReq);
522
549
  }
523
550
  updateSelectedOption(selected) {
524
551
  let set = null;
525
552
  for (let opt = this.list.firstChild, i = this.range.from; opt; opt = opt.nextSibling, i++) {
526
- if (i == selected) {
553
+ if (opt.nodeName != "LI" || !opt.id) {
554
+ i--; // A section header
555
+ }
556
+ else if (i == selected) {
527
557
  if (!opt.hasAttribute("aria-selected")) {
528
558
  opt.setAttribute("aria-selected", "true");
529
559
  set = opt;
@@ -553,41 +583,17 @@ class CompletionTooltip {
553
583
  if (selRect.top > Math.min(space.bottom, listRect.bottom) - 10 ||
554
584
  selRect.bottom < Math.max(space.top, listRect.top) + 10)
555
585
  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
- };
586
+ return this.view.state.facet(completionConfig).positionInfo(this.view, listRect, selRect, infoRect, space);
580
587
  }
581
- positionInfo(pos) {
588
+ placeInfo(pos) {
582
589
  if (this.info) {
583
590
  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;
591
+ if (pos.style)
592
+ this.info.style.cssText = pos.style;
593
+ this.info.className = "cm-tooltip cm-completionInfo " + (pos.class || "");
588
594
  }
589
595
  else {
590
- this.info.style.top = "-1e6px";
596
+ this.info.style.cssText = "top: -1e6px";
591
597
  }
592
598
  }
593
599
  }
@@ -597,8 +603,22 @@ class CompletionTooltip {
597
603
  ul.setAttribute("role", "listbox");
598
604
  ul.setAttribute("aria-expanded", "true");
599
605
  ul.setAttribute("aria-label", this.view.state.phrase("Completions"));
606
+ let curSection = null;
600
607
  for (let i = range.from; i < range.to; i++) {
601
- let { completion, match } = options[i];
608
+ let { completion, match } = options[i], { section } = completion;
609
+ if (section) {
610
+ let name = typeof section == "string" ? section : section.name;
611
+ if (name != curSection && (i > range.from || range.from == 0)) {
612
+ curSection = name;
613
+ if (typeof section != "string" && section.header) {
614
+ ul.appendChild(section.header(section));
615
+ }
616
+ else {
617
+ let header = ul.appendChild(document.createElement("completion-section"));
618
+ header.textContent = name;
619
+ }
620
+ }
621
+ }
602
622
  const li = ul.appendChild(document.createElement("li"));
603
623
  li.id = id + "-" + i;
604
624
  li.setAttribute("role", "option");
@@ -639,32 +659,55 @@ function score(option) {
639
659
  (option.type ? 1 : 0);
640
660
  }
641
661
  function sortOptions(active, state) {
642
- let options = [], i = 0;
662
+ let options = [];
663
+ let sections = null;
664
+ let addOption = (option) => {
665
+ options.push(option);
666
+ let { section } = option.completion;
667
+ if (section) {
668
+ if (!sections)
669
+ sections = [];
670
+ let name = typeof section == "string" ? section : section.name;
671
+ if (!sections.some(s => s.name == name))
672
+ sections.push(typeof section == "string" ? { name } : section);
673
+ }
674
+ };
643
675
  for (let a of active)
644
676
  if (a.hasResult()) {
645
677
  if (a.result.filter === false) {
646
678
  let getMatch = a.result.getMatch;
647
679
  for (let option of a.result.options) {
648
- let match = [1e9 - i++];
680
+ let match = [1e9 - options.length];
649
681
  if (getMatch)
650
682
  for (let n of getMatch(option))
651
683
  match.push(n);
652
- options.push(new Option(option, a, match));
684
+ addOption(new Option(option, a, match, match[0]));
653
685
  }
654
686
  }
655
687
  else {
656
688
  let matcher = new FuzzyMatcher(state.sliceDoc(a.from, a.to)), match;
657
689
  for (let option of a.result.options)
658
690
  if (match = matcher.match(option.label)) {
659
- if (option.boost != null)
660
- match[0] += option.boost;
661
- options.push(new Option(option, a, match));
691
+ addOption(new Option(option, a, match, match[0] + (option.boost || 0)));
662
692
  }
663
693
  }
664
694
  }
695
+ if (sections) {
696
+ let sectionOrder = Object.create(null), pos = 0;
697
+ 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); };
698
+ for (let s of sections.sort(cmp)) {
699
+ pos -= 1e5;
700
+ sectionOrder[s.name] = pos;
701
+ }
702
+ for (let option of options) {
703
+ let { section } = option.completion;
704
+ if (section)
705
+ option.score += sectionOrder[typeof section == "string" ? section : section.name];
706
+ }
707
+ }
665
708
  let result = [], prev = null;
666
709
  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))) {
710
+ for (let opt of options.sort((a, b) => (b.score - a.score) || compare(a.completion, b.completion))) {
668
711
  if (!prev || prev.label != opt.completion.label || prev.detail != opt.completion.detail ||
669
712
  (prev.type != null && opt.completion.type != null && prev.type != opt.completion.type) ||
670
713
  prev.apply != opt.completion.apply)
@@ -1099,13 +1142,21 @@ const baseTheme = /*@__PURE__*/EditorView.baseTheme({
1099
1142
  listStyle: "none",
1100
1143
  margin: 0,
1101
1144
  padding: 0,
1145
+ "& > li, & > completion-section": {
1146
+ padding: "1px 3px",
1147
+ lineHeight: 1.2
1148
+ },
1102
1149
  "& > li": {
1103
1150
  overflowX: "hidden",
1104
1151
  textOverflow: "ellipsis",
1105
- cursor: "pointer",
1106
- padding: "1px 3px",
1107
- lineHeight: 1.2
1152
+ cursor: "pointer"
1108
1153
  },
1154
+ "& > completion-section": {
1155
+ display: "list-item",
1156
+ borderBottom: "1px solid silver",
1157
+ paddingLeft: "0.5em",
1158
+ opacity: 0.7
1159
+ }
1109
1160
  }
1110
1161
  },
1111
1162
  "&light .cm-tooltip-autocomplete ul li[aria-selected]": {
@@ -1539,9 +1590,6 @@ const closeBracketEffect = /*@__PURE__*/StateEffect.define({
1539
1590
  return mapped == null ? undefined : mapped;
1540
1591
  }
1541
1592
  });
1542
- const skipBracketEffect = /*@__PURE__*/StateEffect.define({
1543
- map(value, mapping) { return mapping.mapPos(value); }
1544
- });
1545
1593
  const closedBracket = /*@__PURE__*/new class extends RangeValue {
1546
1594
  };
1547
1595
  closedBracket.startSide = 1;
@@ -1556,12 +1604,9 @@ const bracketState = /*@__PURE__*/StateField.define({
1556
1604
  value = RangeSet.empty;
1557
1605
  }
1558
1606
  value = value.map(tr.changes);
1559
- for (let effect of tr.effects) {
1607
+ for (let effect of tr.effects)
1560
1608
  if (effect.is(closeBracketEffect))
1561
1609
  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
1610
  return value;
1566
1611
  }
1567
1612
  });
@@ -1689,15 +1734,15 @@ function handleOpen(state, open, close, closeBefore) {
1689
1734
  });
1690
1735
  }
1691
1736
  function handleClose(state, _open, close) {
1692
- let dont = null, moved = state.selection.ranges.map(range => {
1737
+ let dont = null, changes = state.changeByRange(range => {
1693
1738
  if (range.empty && nextChar(state.doc, range.head) == close)
1694
- return EditorSelection.cursor(range.head + close.length);
1695
- return dont = range;
1739
+ return { changes: { from: range.head, to: range.head + close.length, insert: close },
1740
+ range: EditorSelection.cursor(range.head + close.length) };
1741
+ return dont = { range };
1696
1742
  });
1697
- return dont ? null : state.update({
1698
- selection: EditorSelection.create(moved, state.selection.mainIndex),
1743
+ return dont ? null : state.update(changes, {
1699
1744
  scrollIntoView: true,
1700
- effects: state.selection.ranges.map(({ from }) => skipBracketEffect.of(from))
1745
+ userEvent: "input.type"
1701
1746
  });
1702
1747
  }
1703
1748
  // Handles cases where the open and close token are the same, and
@@ -1718,8 +1763,9 @@ function handleSame(state, token, allowTriple, config) {
1718
1763
  }
1719
1764
  else if (closedBracketAt(state, pos)) {
1720
1765
  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) };
1766
+ let content = isTriple ? token + token + token : token;
1767
+ return { changes: { from: pos, to: pos + content.length, insert: content },
1768
+ range: EditorSelection.cursor(pos + content.length) };
1723
1769
  }
1724
1770
  }
1725
1771
  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.0",
4
4
  "description": "Autocompletion for the CodeMirror code editor",
5
5
  "scripts": {
6
6
  "test": "cm-runtests",