@codemirror/autocomplete 0.19.6 → 0.19.10

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.19.10 (2022-01-05)
2
+
3
+ ### Bug fixes
4
+
5
+ Make sure the info tooltip is hidden when the selected option is scrolled out of view.
6
+
7
+ Fix a bug in the completion ranking that would sometimes give options that match the input by word start chars higher scores than appropriate.
8
+
9
+ Options are now sorted (ascending) by length when their match score is otherwise identical.
10
+
11
+ ## 0.19.9 (2021-11-26)
12
+
13
+ ### Bug fixes
14
+
15
+ Fix an issue where info tooltips would be visible in an inappropriate position when there was no room to place them properly.
16
+
17
+ ## 0.19.8 (2021-11-17)
18
+
19
+ ### Bug fixes
20
+
21
+ Give the completion tooltip a minimal width, and show ellipsis when completions overflow the tooltip width.
22
+
23
+ ### New features
24
+
25
+ `autocompletion` now accepts an `aboveCursor` option to make the completion tooltip show up above the cursor.
26
+
27
+ ## 0.19.7 (2021-11-16)
28
+
29
+ ### Bug fixes
30
+
31
+ Make option deduplication less aggressive, so that options with different `type` or `apply` fields don't get merged.
32
+
1
33
  ## 0.19.6 (2021-11-12)
2
34
 
3
35
  ### Bug fixes
package/dist/index.cjs CHANGED
@@ -246,7 +246,7 @@ class FuzzyMatcher {
246
246
  let byWordTo = 0, byWordFolded = false;
247
247
  // If we've found a partial adjacent match, these track its state
248
248
  let adjacentTo = 0, adjacentStart = -1, adjacentEnd = -1;
249
- let hasLower = /[a-z]/.test(word);
249
+ let hasLower = /[a-z]/.test(word), wordAdjacent = true;
250
250
  // Go over the option's text, scanning for the various kinds of matches
251
251
  for (let i = 0, e = Math.min(word.length, 200), prevType = 0 /* NonWord */; i < e && byWordTo < len;) {
252
252
  let next = text.codePointAt(word, i);
@@ -268,26 +268,30 @@ class FuzzyMatcher {
268
268
  let ch, type = next < 0xff
269
269
  ? (next >= 48 && next <= 57 || next >= 97 && next <= 122 ? 2 /* Lower */ : next >= 65 && next <= 90 ? 1 /* Upper */ : 0 /* NonWord */)
270
270
  : ((ch = text.fromCodePoint(next)) != ch.toLowerCase() ? 1 /* Upper */ : ch != ch.toUpperCase() ? 2 /* Lower */ : 0 /* NonWord */);
271
- if ((type == 1 /* Upper */ && hasLower || prevType == 0 /* NonWord */ && type != 0 /* NonWord */) &&
272
- (chars[byWordTo] == next || (folded[byWordTo] == next && (byWordFolded = true))))
273
- byWord[byWordTo++] = i;
271
+ if (!i || type == 1 /* Upper */ && hasLower || prevType == 0 /* NonWord */ && type != 0 /* NonWord */) {
272
+ if (chars[byWordTo] == next || (folded[byWordTo] == next && (byWordFolded = true)))
273
+ byWord[byWordTo++] = i;
274
+ else if (byWord.length)
275
+ wordAdjacent = false;
276
+ }
274
277
  prevType = type;
275
278
  i += text.codePointSize(next);
276
279
  }
277
- if (byWordTo == len && byWord[0] == 0)
280
+ if (byWordTo == len && byWord[0] == 0 && wordAdjacent)
278
281
  return this.result(-100 /* ByWord */ + (byWordFolded ? -200 /* CaseFold */ : 0), byWord, word);
279
282
  if (adjacentTo == len && adjacentStart == 0)
280
- return [-200 /* CaseFold */, 0, adjacentEnd];
283
+ return [-200 /* CaseFold */ - word.length, 0, adjacentEnd];
281
284
  if (direct > -1)
282
- return [-700 /* NotStart */, direct, direct + this.pattern.length];
285
+ return [-700 /* NotStart */ - word.length, direct, direct + this.pattern.length];
283
286
  if (adjacentTo == len)
284
- return [-200 /* CaseFold */ + -700 /* NotStart */, adjacentStart, adjacentEnd];
287
+ return [-200 /* CaseFold */ + -700 /* NotStart */ - word.length, adjacentStart, adjacentEnd];
285
288
  if (byWordTo == len)
286
- return this.result(-100 /* ByWord */ + (byWordFolded ? -200 /* CaseFold */ : 0) + -700 /* NotStart */, byWord, word);
289
+ return this.result(-100 /* ByWord */ + (byWordFolded ? -200 /* CaseFold */ : 0) + -700 /* NotStart */ +
290
+ (wordAdjacent ? 0 : -1100 /* Gap */), byWord, word);
287
291
  return chars.length == 2 ? null : this.result((any[0] ? -700 /* NotStart */ : 0) + -200 /* CaseFold */ + -1100 /* Gap */, any, word);
288
292
  }
289
293
  result(score, positions, word) {
290
- let result = [score], i = 1;
294
+ let result = [score - word.length], i = 1;
291
295
  for (let pos of positions) {
292
296
  let to = pos + (this.astral ? text.codePointSize(text.codePointAt(word, pos)) : 1);
293
297
  if (i > 1 && result[i - 1] == pos)
@@ -309,6 +313,7 @@ const completionConfig = state.Facet.define({
309
313
  maxRenderedOptions: 100,
310
314
  defaultKeymap: true,
311
315
  optionClass: () => "",
316
+ aboveCursor: false,
312
317
  icons: true,
313
318
  addToOptions: []
314
319
  }, {
@@ -482,12 +487,14 @@ class CompletionTooltip {
482
487
  let sel = this.dom.querySelector("[aria-selected]");
483
488
  if (!sel || !this.info)
484
489
  return null;
485
- let rect = this.dom.getBoundingClientRect(), infoRect = this.info.getBoundingClientRect();
486
- let top = Math.min(sel.getBoundingClientRect().top, innerHeight - infoRect.height) - rect.top;
487
- if (top < 0 || top > this.list.clientHeight - 10)
490
+ let listRect = this.dom.getBoundingClientRect();
491
+ let infoRect = this.info.getBoundingClientRect();
492
+ let selRect = sel.getBoundingClientRect();
493
+ if (selRect.top > Math.min(innerHeight, listRect.bottom) - 10 || selRect.bottom < Math.max(0, listRect.top) + 10)
488
494
  return null;
495
+ let top = Math.max(0, Math.min(selRect.top, innerHeight - infoRect.height)) - listRect.top;
489
496
  let left = this.view.textDirection == view.Direction.RTL;
490
- let spaceLeft = rect.left, spaceRight = innerWidth - rect.right;
497
+ let spaceLeft = listRect.left, spaceRight = innerWidth - listRect.right;
491
498
  if (left && spaceLeft < Math.min(infoRect.width, spaceRight))
492
499
  left = false;
493
500
  else if (!left && spaceRight < Math.min(infoRect.width, spaceLeft))
@@ -495,10 +502,12 @@ class CompletionTooltip {
495
502
  return { top, left };
496
503
  }
497
504
  positionInfo(pos) {
498
- if (this.info && pos) {
499
- this.info.style.top = pos.top + "px";
500
- this.info.classList.toggle("cm-completionInfo-left", pos.left);
501
- this.info.classList.toggle("cm-completionInfo-right", !pos.left);
505
+ if (this.info) {
506
+ this.info.style.top = (pos ? pos.top : -1e6) + "px";
507
+ if (pos) {
508
+ this.info.classList.toggle("cm-completionInfo-left", pos.left);
509
+ this.info.classList.toggle("cm-completionInfo-right", !pos.left);
510
+ }
502
511
  }
503
512
  }
504
513
  createListBox(options, id, range) {
@@ -570,7 +579,8 @@ function sortOptions(active, state) {
570
579
  for (let opt of options.sort(cmpOption)) {
571
580
  if (result.length == MaxOptions)
572
581
  break;
573
- if (!prev || prev.label != opt.completion.label || prev.detail != opt.completion.detail)
582
+ if (!prev || prev.label != opt.completion.label || prev.detail != opt.completion.detail ||
583
+ prev.type != opt.completion.type || prev.apply != opt.completion.apply)
574
584
  result.push(opt);
575
585
  else if (score(opt.completion) > score(prev))
576
586
  result[result.length - 1] = opt;
@@ -590,7 +600,7 @@ class CompletionDialog {
590
600
  return selected == this.selected || selected >= this.options.length ? this
591
601
  : new CompletionDialog(this.options, makeAttrs(id, selected), this.tooltip, this.timestamp, selected);
592
602
  }
593
- static build(active, state, id, prev) {
603
+ static build(active, state, id, prev, conf) {
594
604
  let options = sortOptions(active, state);
595
605
  if (!options.length)
596
606
  return null;
@@ -604,7 +614,8 @@ class CompletionDialog {
604
614
  }
605
615
  return new CompletionDialog(options, makeAttrs(id, selected), {
606
616
  pos: active.reduce((a, b) => b.hasResult() ? Math.min(a, b.from) : a, 1e8),
607
- create: completionTooltip(completionState)
617
+ create: completionTooltip(completionState),
618
+ above: conf.aboveCursor,
608
619
  }, prev ? prev.timestamp : Date.now(), selected);
609
620
  }
610
621
  map(changes) {
@@ -632,7 +643,7 @@ class CompletionState {
632
643
  if (active.length == this.active.length && active.every((a, i) => a == this.active[i]))
633
644
  active = this.active;
634
645
  let open = tr.selection || active.some(a => a.hasResult() && tr.changes.touchesRange(a.from, a.to)) ||
635
- !sameResults(active, this.active) ? CompletionDialog.build(active, state, this.id, this.open)
646
+ !sameResults(active, this.active) ? CompletionDialog.build(active, state, this.id, this.open, conf)
636
647
  : this.open && tr.docChanged ? this.open.map(tr.changes) : this.open;
637
648
  if (!open && active.every(a => a.state != 1 /* Pending */) && active.some(a => a.hasResult()))
638
649
  active = active.map(a => a.hasResult() ? new ActiveSource(a.source, 0 /* Inactive */) : a);
@@ -968,16 +979,19 @@ const baseTheme = view.EditorView.baseTheme({
968
979
  "& > ul": {
969
980
  fontFamily: "monospace",
970
981
  whiteSpace: "nowrap",
971
- overflow: "auto",
982
+ overflow: "hidden auto",
972
983
  maxWidth_fallback: "700px",
973
984
  maxWidth: "min(700px, 95vw)",
985
+ minWidth: "250px",
974
986
  maxHeight: "10em",
975
987
  listStyle: "none",
976
988
  margin: 0,
977
989
  padding: 0,
978
990
  "& > li": {
991
+ overflowX: "hidden",
992
+ textOverflow: "ellipsis",
979
993
  cursor: "pointer",
980
- padding: "1px 1em 1px 3px",
994
+ padding: "1px 3px",
981
995
  lineHeight: 1.2
982
996
  },
983
997
  }
package/dist/index.d.ts CHANGED
@@ -30,6 +30,12 @@ interface CompletionConfig {
30
30
  */
31
31
  defaultKeymap?: boolean;
32
32
  /**
33
+ By default, completions are shown below the cursor when there is
34
+ space. Setting this to true will make the extension put the
35
+ completions above the cursor when possible.
36
+ */
37
+ aboveCursor?: boolean;
38
+ /**
33
39
  This can be used to add additional CSS classes to completion
34
40
  options.
35
41
  */
package/dist/index.js CHANGED
@@ -242,7 +242,7 @@ class FuzzyMatcher {
242
242
  let byWordTo = 0, byWordFolded = false;
243
243
  // If we've found a partial adjacent match, these track its state
244
244
  let adjacentTo = 0, adjacentStart = -1, adjacentEnd = -1;
245
- let hasLower = /[a-z]/.test(word);
245
+ let hasLower = /[a-z]/.test(word), wordAdjacent = true;
246
246
  // Go over the option's text, scanning for the various kinds of matches
247
247
  for (let i = 0, e = Math.min(word.length, 200), prevType = 0 /* NonWord */; i < e && byWordTo < len;) {
248
248
  let next = codePointAt(word, i);
@@ -264,26 +264,30 @@ class FuzzyMatcher {
264
264
  let ch, type = next < 0xff
265
265
  ? (next >= 48 && next <= 57 || next >= 97 && next <= 122 ? 2 /* Lower */ : next >= 65 && next <= 90 ? 1 /* Upper */ : 0 /* NonWord */)
266
266
  : ((ch = fromCodePoint(next)) != ch.toLowerCase() ? 1 /* Upper */ : ch != ch.toUpperCase() ? 2 /* Lower */ : 0 /* NonWord */);
267
- if ((type == 1 /* Upper */ && hasLower || prevType == 0 /* NonWord */ && type != 0 /* NonWord */) &&
268
- (chars[byWordTo] == next || (folded[byWordTo] == next && (byWordFolded = true))))
269
- byWord[byWordTo++] = i;
267
+ if (!i || type == 1 /* Upper */ && hasLower || prevType == 0 /* NonWord */ && type != 0 /* NonWord */) {
268
+ if (chars[byWordTo] == next || (folded[byWordTo] == next && (byWordFolded = true)))
269
+ byWord[byWordTo++] = i;
270
+ else if (byWord.length)
271
+ wordAdjacent = false;
272
+ }
270
273
  prevType = type;
271
274
  i += codePointSize(next);
272
275
  }
273
- if (byWordTo == len && byWord[0] == 0)
276
+ if (byWordTo == len && byWord[0] == 0 && wordAdjacent)
274
277
  return this.result(-100 /* ByWord */ + (byWordFolded ? -200 /* CaseFold */ : 0), byWord, word);
275
278
  if (adjacentTo == len && adjacentStart == 0)
276
- return [-200 /* CaseFold */, 0, adjacentEnd];
279
+ return [-200 /* CaseFold */ - word.length, 0, adjacentEnd];
277
280
  if (direct > -1)
278
- return [-700 /* NotStart */, direct, direct + this.pattern.length];
281
+ return [-700 /* NotStart */ - word.length, direct, direct + this.pattern.length];
279
282
  if (adjacentTo == len)
280
- return [-200 /* CaseFold */ + -700 /* NotStart */, adjacentStart, adjacentEnd];
283
+ return [-200 /* CaseFold */ + -700 /* NotStart */ - word.length, adjacentStart, adjacentEnd];
281
284
  if (byWordTo == len)
282
- return this.result(-100 /* ByWord */ + (byWordFolded ? -200 /* CaseFold */ : 0) + -700 /* NotStart */, byWord, word);
285
+ return this.result(-100 /* ByWord */ + (byWordFolded ? -200 /* CaseFold */ : 0) + -700 /* NotStart */ +
286
+ (wordAdjacent ? 0 : -1100 /* Gap */), byWord, word);
283
287
  return chars.length == 2 ? null : this.result((any[0] ? -700 /* NotStart */ : 0) + -200 /* CaseFold */ + -1100 /* Gap */, any, word);
284
288
  }
285
289
  result(score, positions, word) {
286
- let result = [score], i = 1;
290
+ let result = [score - word.length], i = 1;
287
291
  for (let pos of positions) {
288
292
  let to = pos + (this.astral ? codePointSize(codePointAt(word, pos)) : 1);
289
293
  if (i > 1 && result[i - 1] == pos)
@@ -305,6 +309,7 @@ const completionConfig = /*@__PURE__*/Facet.define({
305
309
  maxRenderedOptions: 100,
306
310
  defaultKeymap: true,
307
311
  optionClass: () => "",
312
+ aboveCursor: false,
308
313
  icons: true,
309
314
  addToOptions: []
310
315
  }, {
@@ -478,12 +483,14 @@ class CompletionTooltip {
478
483
  let sel = this.dom.querySelector("[aria-selected]");
479
484
  if (!sel || !this.info)
480
485
  return null;
481
- let rect = this.dom.getBoundingClientRect(), infoRect = this.info.getBoundingClientRect();
482
- let top = Math.min(sel.getBoundingClientRect().top, innerHeight - infoRect.height) - rect.top;
483
- if (top < 0 || top > this.list.clientHeight - 10)
486
+ let listRect = this.dom.getBoundingClientRect();
487
+ let infoRect = this.info.getBoundingClientRect();
488
+ let selRect = sel.getBoundingClientRect();
489
+ if (selRect.top > Math.min(innerHeight, listRect.bottom) - 10 || selRect.bottom < Math.max(0, listRect.top) + 10)
484
490
  return null;
491
+ let top = Math.max(0, Math.min(selRect.top, innerHeight - infoRect.height)) - listRect.top;
485
492
  let left = this.view.textDirection == Direction.RTL;
486
- let spaceLeft = rect.left, spaceRight = innerWidth - rect.right;
493
+ let spaceLeft = listRect.left, spaceRight = innerWidth - listRect.right;
487
494
  if (left && spaceLeft < Math.min(infoRect.width, spaceRight))
488
495
  left = false;
489
496
  else if (!left && spaceRight < Math.min(infoRect.width, spaceLeft))
@@ -491,10 +498,12 @@ class CompletionTooltip {
491
498
  return { top, left };
492
499
  }
493
500
  positionInfo(pos) {
494
- if (this.info && pos) {
495
- this.info.style.top = pos.top + "px";
496
- this.info.classList.toggle("cm-completionInfo-left", pos.left);
497
- this.info.classList.toggle("cm-completionInfo-right", !pos.left);
501
+ if (this.info) {
502
+ this.info.style.top = (pos ? pos.top : -1e6) + "px";
503
+ if (pos) {
504
+ this.info.classList.toggle("cm-completionInfo-left", pos.left);
505
+ this.info.classList.toggle("cm-completionInfo-right", !pos.left);
506
+ }
498
507
  }
499
508
  }
500
509
  createListBox(options, id, range) {
@@ -566,7 +575,8 @@ function sortOptions(active, state) {
566
575
  for (let opt of options.sort(cmpOption)) {
567
576
  if (result.length == MaxOptions)
568
577
  break;
569
- if (!prev || prev.label != opt.completion.label || prev.detail != opt.completion.detail)
578
+ if (!prev || prev.label != opt.completion.label || prev.detail != opt.completion.detail ||
579
+ prev.type != opt.completion.type || prev.apply != opt.completion.apply)
570
580
  result.push(opt);
571
581
  else if (score(opt.completion) > score(prev))
572
582
  result[result.length - 1] = opt;
@@ -586,7 +596,7 @@ class CompletionDialog {
586
596
  return selected == this.selected || selected >= this.options.length ? this
587
597
  : new CompletionDialog(this.options, makeAttrs(id, selected), this.tooltip, this.timestamp, selected);
588
598
  }
589
- static build(active, state, id, prev) {
599
+ static build(active, state, id, prev, conf) {
590
600
  let options = sortOptions(active, state);
591
601
  if (!options.length)
592
602
  return null;
@@ -600,7 +610,8 @@ class CompletionDialog {
600
610
  }
601
611
  return new CompletionDialog(options, makeAttrs(id, selected), {
602
612
  pos: active.reduce((a, b) => b.hasResult() ? Math.min(a, b.from) : a, 1e8),
603
- create: completionTooltip(completionState)
613
+ create: completionTooltip(completionState),
614
+ above: conf.aboveCursor,
604
615
  }, prev ? prev.timestamp : Date.now(), selected);
605
616
  }
606
617
  map(changes) {
@@ -628,7 +639,7 @@ class CompletionState {
628
639
  if (active.length == this.active.length && active.every((a, i) => a == this.active[i]))
629
640
  active = this.active;
630
641
  let open = tr.selection || active.some(a => a.hasResult() && tr.changes.touchesRange(a.from, a.to)) ||
631
- !sameResults(active, this.active) ? CompletionDialog.build(active, state, this.id, this.open)
642
+ !sameResults(active, this.active) ? CompletionDialog.build(active, state, this.id, this.open, conf)
632
643
  : this.open && tr.docChanged ? this.open.map(tr.changes) : this.open;
633
644
  if (!open && active.every(a => a.state != 1 /* Pending */) && active.some(a => a.hasResult()))
634
645
  active = active.map(a => a.hasResult() ? new ActiveSource(a.source, 0 /* Inactive */) : a);
@@ -964,16 +975,19 @@ const baseTheme = /*@__PURE__*/EditorView.baseTheme({
964
975
  "& > ul": {
965
976
  fontFamily: "monospace",
966
977
  whiteSpace: "nowrap",
967
- overflow: "auto",
978
+ overflow: "hidden auto",
968
979
  maxWidth_fallback: "700px",
969
980
  maxWidth: "min(700px, 95vw)",
981
+ minWidth: "250px",
970
982
  maxHeight: "10em",
971
983
  listStyle: "none",
972
984
  margin: 0,
973
985
  padding: 0,
974
986
  "& > li": {
987
+ overflowX: "hidden",
988
+ textOverflow: "ellipsis",
975
989
  cursor: "pointer",
976
- padding: "1px 1em 1px 3px",
990
+ padding: "1px 3px",
977
991
  lineHeight: 1.2
978
992
  },
979
993
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemirror/autocomplete",
3
- "version": "0.19.6",
3
+ "version": "0.19.10",
4
4
  "description": "Autocompletion for the CodeMirror code editor",
5
5
  "scripts": {
6
6
  "test": "cm-runtests",