@adecore/editor-react 0.23.0 → 0.24.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.
Files changed (130) hide show
  1. package/dist/AnchoredPopup.d.ts +2 -1
  2. package/dist/AnchoredPopup.js +2 -2
  3. package/dist/CallHierarchyPanel.js +6 -2
  4. package/dist/CodeAuthorsCard.d.ts +4 -0
  5. package/dist/CodeAuthorsCard.js +8 -4
  6. package/dist/CompletionPopup.js +7 -3
  7. package/dist/EditorView.d.ts +8 -3
  8. package/dist/EditorView.js +79 -9
  9. package/dist/HoverCard.d.ts +3 -1
  10. package/dist/HoverCard.js +2 -2
  11. package/dist/LanguagePopups.d.ts +8 -4
  12. package/dist/LanguagePopups.js +4 -4
  13. package/dist/PanelResizeHandle.d.ts +8 -0
  14. package/dist/PanelResizeHandle.js +71 -0
  15. package/dist/PeekPanel.js +6 -2
  16. package/dist/PickPopup.js +2 -1
  17. package/dist/RenameCard.js +10 -8
  18. package/dist/RenderedDocs.d.ts +10 -4
  19. package/dist/RenderedDocs.js +10 -6
  20. package/dist/call-hierarchy-model.js +3 -9
  21. package/dist/call-hierarchy.js +17 -17
  22. package/dist/code-actions.js +5 -2
  23. package/dist/completion-model.js +3 -1
  24. package/dist/completion.d.ts +4 -2
  25. package/dist/completion.js +49 -27
  26. package/dist/diagnostics-model.d.ts +1 -0
  27. package/dist/diagnostics-model.js +1 -1
  28. package/dist/diagnostics.d.ts +1 -0
  29. package/dist/diagnostics.js +28 -10
  30. package/dist/diff-overlays.js +2 -2
  31. package/dist/editor-language.d.ts +3 -1
  32. package/dist/editor-language.js +11 -1
  33. package/dist/format-on-type.js +2 -3
  34. package/dist/highlights.js +3 -2
  35. package/dist/history.js +3 -2
  36. package/dist/holder.d.ts +3 -3
  37. package/dist/holder.js +2 -1
  38. package/dist/host-types.d.ts +5 -0
  39. package/dist/hover.d.ts +5 -0
  40. package/dist/hover.js +13 -5
  41. package/dist/index.d.ts +6 -2
  42. package/dist/index.js +3 -1
  43. package/dist/key-priority.d.ts +23 -0
  44. package/dist/key-priority.js +39 -0
  45. package/dist/locales/en.json +5 -1
  46. package/dist/locales/nl.json +5 -1
  47. package/dist/models.d.ts +2 -2
  48. package/dist/models.js +2 -2
  49. package/dist/navigation.js +3 -2
  50. package/dist/panel-size.d.ts +6 -0
  51. package/dist/panel-size.js +25 -0
  52. package/dist/peek.d.ts +3 -1
  53. package/dist/peek.js +8 -3
  54. package/dist/pick.js +2 -1
  55. package/dist/popup-focus.d.ts +5 -0
  56. package/dist/popup-focus.js +12 -0
  57. package/dist/popups.d.ts +1 -1
  58. package/dist/project-language.d.ts +4 -0
  59. package/dist/project-language.js +23 -4
  60. package/dist/project-problems.d.ts +10 -1
  61. package/dist/project-problems.js +34 -10
  62. package/dist/proposal-model.d.ts +0 -1
  63. package/dist/proposal-model.js +0 -8
  64. package/dist/rename-model.d.ts +2 -1
  65. package/dist/rename-model.js +57 -0
  66. package/dist/rename.d.ts +1 -0
  67. package/dist/rename.js +21 -5
  68. package/dist/rendered-docs.d.ts +2 -0
  69. package/dist/rendered-docs.js +15 -8
  70. package/dist/signature.js +4 -3
  71. package/dist/snippet-session.d.ts +1 -1
  72. package/dist/snippet-session.js +8 -2
  73. package/dist/symbol-picker.js +3 -2
  74. package/dist/timers.d.ts +1 -0
  75. package/dist/timers.js +4 -0
  76. package/dist/use-panel-body.d.ts +6 -0
  77. package/dist/use-panel-body.js +13 -0
  78. package/package.json +6 -6
  79. package/src/AnchoredPopup.tsx +4 -2
  80. package/src/CallHierarchyPanel.tsx +9 -3
  81. package/src/CodeAuthorsCard.tsx +21 -12
  82. package/src/CompletionPopup.tsx +11 -1
  83. package/src/EditorView.tsx +122 -11
  84. package/src/HoverCard.tsx +6 -1
  85. package/src/LanguagePopups.tsx +23 -4
  86. package/src/PanelResizeHandle.tsx +104 -0
  87. package/src/PeekPanel.tsx +9 -3
  88. package/src/PickPopup.tsx +9 -1
  89. package/src/RenameCard.tsx +15 -9
  90. package/src/RenderedDocs.tsx +30 -8
  91. package/src/call-hierarchy-model.ts +15 -22
  92. package/src/call-hierarchy.ts +17 -17
  93. package/src/code-actions.ts +5 -2
  94. package/src/completion-model.ts +3 -1
  95. package/src/completion.ts +48 -28
  96. package/src/diagnostics-model.ts +1 -1
  97. package/src/diagnostics.ts +29 -13
  98. package/src/diff-overlays.tsx +12 -8
  99. package/src/editor-language.ts +13 -2
  100. package/src/format-on-type.ts +2 -3
  101. package/src/highlights.ts +3 -2
  102. package/src/history.ts +3 -2
  103. package/src/holder.ts +5 -4
  104. package/src/host-types.ts +9 -0
  105. package/src/hover.ts +19 -5
  106. package/src/index.ts +6 -2
  107. package/src/key-priority.ts +49 -0
  108. package/src/locales/en.json +5 -1
  109. package/src/locales/nl.json +5 -1
  110. package/src/models.ts +1 -1
  111. package/src/navigation.ts +3 -2
  112. package/src/panel-size.ts +28 -0
  113. package/src/peek.ts +8 -3
  114. package/src/pick.ts +2 -1
  115. package/src/popup-focus.ts +17 -0
  116. package/src/popups.ts +1 -2
  117. package/src/project-language.ts +25 -4
  118. package/src/project-problems.ts +46 -11
  119. package/src/proposal-model.ts +0 -9
  120. package/src/rename-model.ts +60 -1
  121. package/src/rename.ts +21 -5
  122. package/src/rendered-docs.ts +17 -8
  123. package/src/signature.ts +4 -3
  124. package/src/snippet-session.ts +9 -3
  125. package/src/symbol-picker.ts +3 -2
  126. package/src/timers.ts +5 -0
  127. package/src/use-panel-body.ts +17 -0
  128. package/dist/relative-time.d.ts +0 -1
  129. package/dist/relative-time.js +0 -9
  130. package/src/relative-time.ts +0 -10
@@ -1,6 +1,6 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { createPortal } from 'react-dom';
3
- import { useSyncExternalStore } from 'react';
3
+ import { memo, useSyncExternalStore } from 'react';
4
4
  import { useTranslation } from 'react-i18next';
5
5
  import { DOC_LINK_PREFIX, sectionsOf } from './doc-comment-model.js';
6
6
  import { markdownParts } from './hover-content.js';
@@ -39,12 +39,11 @@ function TagRow({ tag, language, linkable, spaced }) {
39
39
  const link = target === null ? null : /^https?:\/\//i.test(target) ? `[${target}](${target})` : `[\`${target}\`](${DOC_LINK_PREFIX}${encodeURIComponent(target)})`;
40
40
  return (_jsx("dd", { className: `col-start-2 m-0 flex min-w-0 flex-wrap items-baseline gap-x-2 ${spaced ? 'mt-1' : ''}`, children: tag.name === 'example' ? (_jsx("div", { className: "w-full rounded-md bg-surface-sunken px-2 py-1.5", children: _jsx(Signature, { code: tag.text, language: language }) })) : (_jsxs(_Fragment, { children: [tag.subject !== null && target === null && _jsx(Code, { children: tag.subject }), tag.type !== null && _jsx(Code, { faint: true, children: tag.type }), (link !== null || tag.text !== '') && (_jsx("div", { className: "min-w-0 grow basis-48 text-text", children: _jsx(Prose, { text: [link, tag.text].filter((part) => part !== null && part !== '').join(' '), language: language, linkable: linkable, compact: true }) }))] })) }));
41
41
  }
42
- export function RenderedDocBlock({ language, entry }) {
42
+ function DocBlock({ language, comment, parsed, linkable }) {
43
43
  const { t } = useTranslation('editor');
44
44
  const feature = language.renderedDocs;
45
- const { description, tags } = entry.parsed;
45
+ const { description, tags } = parsed;
46
46
  const highlighter = shikiLanguageOf(language.languageId);
47
- const linkable = feature.canFollow;
48
47
  const sections = sectionsOf(tags);
49
48
  const deprecated = sections.find((section) => section.name === 'deprecated');
50
49
  const shown = sections.filter((section) => section.name !== 'deprecated');
@@ -61,7 +60,7 @@ export function RenderedDocBlock({ language, entry }) {
61
60
  return;
62
61
  }
63
62
  if (href.startsWith(DOC_LINK_PREFIX)) {
64
- feature.follow(decodeURIComponent(href.slice(DOC_LINK_PREFIX.length)), entry.comment);
63
+ feature.follow(decodeURIComponent(href.slice(DOC_LINK_PREFIX.length)), comment);
65
64
  }
66
65
  else if (/^(https?:|mailto:)/i.test(href)) {
67
66
  window.open(href, '_blank', 'noopener,noreferrer');
@@ -72,6 +71,8 @@ export function RenderedDocBlock({ language, entry }) {
72
71
  ...section.tags.map((tag, index) => (_jsx(TagRow, { tag: tag, language: highlighter, linkable: linkable, spaced: row > 0 && index === 0 }, `${section.name}:${index}`)))
73
72
  ]) }))] }));
74
73
  }
74
+ /* Drawn again only when its comment or its links change, since every scroll of the editor draws the popups again. */
75
+ export const RenderedDocBlock = memo(DocBlock);
75
76
  /*
76
77
  * The documentation comments the editor drew as rows, filled with their formatted text. It portals into
77
78
  * the element the editor made for each row, so the editor decides what is in view; a comment is parsed
@@ -80,5 +81,8 @@ export function RenderedDocBlock({ language, entry }) {
80
81
  export function RenderedDocs({ language }) {
81
82
  const feature = language.renderedDocs;
82
83
  useSyncExternalStore(feature.subscribe, feature.getVersion, feature.getVersion);
83
- return _jsx(_Fragment, { children: feature.entries().map((entry) => createPortal(_jsx(RenderedDocBlock, { language: language, entry: entry }), entry.container, entry.id)) });
84
+ const linkable = feature.canFollow;
85
+ return (_jsx(_Fragment, { children: feature
86
+ .entries()
87
+ .map((entry) => createPortal(_jsx(RenderedDocBlock, { language: language, comment: entry.comment, parsed: entry.parsed, linkable: linkable }), entry.container, entry.id)) }));
84
88
  }
@@ -24,19 +24,13 @@ export function rootsOf(items) {
24
24
  shown: 0
25
25
  }));
26
26
  }
27
- function compareItems(left, right) {
28
- return left.name.localeCompare(right.name) || left.uri.localeCompare(right.uri) || left.range.start.line - right.range.start.line;
29
- }
30
27
  /*
31
- * The children of a node made from the calls a server returned, listed by name the way the symbols of a
32
- * file are. The call sites of a caller are in its own file; those of a callee are in the file of the node
33
- * that was asked about.
28
+ * The children of a node made from the calls a server returned, in the order the server gave them.
29
+ * The call sites of a caller are in its own file; those of a callee are in the file of the node that was asked about.
34
30
  */
35
31
  export function childrenOf(direction, parent, calls) {
36
32
  const entries = calls.map((call) => ({ item: 'from' in call ? call.from : call.to, sites: call.fromRanges }));
37
- return entries
38
- .sort((left, right) => compareItems(left.item, right.item))
39
- .map((entry, index) => ({
33
+ return entries.map((entry, index) => ({
40
34
  id: `${parent.id}.${index}`,
41
35
  parent: parent.id,
42
36
  level: parent.level + 1,
@@ -1,10 +1,11 @@
1
1
  import { StaleResultError } from '@adecore/lsp';
2
+ import { KEY_PRIORITY } from './key-priority.js';
2
3
  import { CALL_LEVEL_LIMIT, childrenOf, moreIdOf, rootsOf, rowsOf, statusIdOf, stepsOf, targetOf } from './call-hierarchy-model.js';
3
4
  import { PEEK_HEIGHT } from './peek.js';
4
5
  import { snippetOf } from './peek-model.js';
5
6
  import { isShortcut } from './shortcut-keys.js';
6
7
  import { messageOf } from './error-message.js';
7
- import { realTimers } from './timers.js';
8
+ import { realTimers, revealRow } from './timers.js';
8
9
  const TOAST_ID = 'language-call-hierarchy';
9
10
  const PREPARE = 'textDocument/prepareCallHierarchy';
10
11
  /* The row the panel stands in is as tall as a peek's, plus the segmented control that is a pixel taller than the peek's title bar. */
@@ -34,7 +35,7 @@ export class CallHierarchyFeature {
34
35
  this.timers = timers;
35
36
  const { editor } = language;
36
37
  const offs = [
37
- editor.onKeyDown((event) => {
38
+ language.onKey((event) => {
38
39
  if (isShortcut(this.language.shortcuts.callHierarchy, event)) {
39
40
  if (this.isOpen) {
40
41
  this.setDirection(this.direction === 'incoming' ? 'outgoing' : 'incoming');
@@ -45,7 +46,7 @@ export class CallHierarchyFeature {
45
46
  return true;
46
47
  }
47
48
  return this.key(event);
48
- }),
49
+ }, KEY_PRIORITY.callHierarchy),
49
50
  editor.onTextChange(() => this.close())
50
51
  ];
51
52
  language.onDispose(() => {
@@ -56,7 +57,7 @@ export class CallHierarchyFeature {
56
57
  });
57
58
  }
58
59
  get isOpen() {
59
- return (this.language.popups.getState().callHierarchy ?? null) !== null;
60
+ return this.language.popups.getState().callHierarchy !== null;
60
61
  }
61
62
  /* Whether a server of this document answers the three requests the tree is made of. */
62
63
  get supported() {
@@ -111,7 +112,7 @@ export class CallHierarchyFeature {
111
112
  }
112
113
  close() {
113
114
  this.reset();
114
- if (this.language.popups.getState().callHierarchy == null) {
115
+ if (this.language.popups.getState().callHierarchy === null) {
115
116
  return;
116
117
  }
117
118
  this.language.popups.setState({ callHierarchy: null });
@@ -121,7 +122,7 @@ export class CallHierarchyFeature {
121
122
  }
122
123
  /* Starts the tree over from the same symbols for the other direction. */
123
124
  setDirection(direction) {
124
- const view = this.language.popups.getState().callHierarchy ?? null;
125
+ const view = this.language.popups.getState().callHierarchy;
125
126
  if (view === null || view.direction === direction) {
126
127
  return;
127
128
  }
@@ -145,7 +146,7 @@ export class CallHierarchyFeature {
145
146
  }
146
147
  if (node.expanded) {
147
148
  this.patch(id, { expanded: false });
148
- const view = this.language.popups.getState().callHierarchy ?? null;
149
+ const view = this.language.popups.getState().callHierarchy;
149
150
  if (view !== null && this.hasAncestor(view.active, id)) {
150
151
  this.select(id);
151
152
  }
@@ -170,7 +171,7 @@ export class CallHierarchyFeature {
170
171
  }
171
172
  this.patch(parent, { shown: Math.min(node.children.length, node.shown + CALL_LEVEL_LIMIT) });
172
173
  const first = node.children[node.shown];
173
- const view = this.language.popups.getState().callHierarchy ?? null;
174
+ const view = this.language.popups.getState().callHierarchy;
174
175
  // The row that offered more is gone once everything shows, so the selection moves on to what it brought.
175
176
  if (first !== undefined && view?.active === moreIdOf(parent)) {
176
177
  this.select(first);
@@ -178,7 +179,7 @@ export class CallHierarchyFeature {
178
179
  }
179
180
  /* Makes a row the active one and shows the code of its call. */
180
181
  select(id, site = 0) {
181
- const view = this.language.popups.getState().callHierarchy ?? null;
182
+ const view = this.language.popups.getState().callHierarchy;
182
183
  const node = this.nodes.get(id);
183
184
  if (view === null || !view.rows.some((row) => row.id === id)) {
184
185
  return;
@@ -192,14 +193,14 @@ export class CallHierarchyFeature {
192
193
  }
193
194
  /* Steps through the places a call is made from when the active one has more than one. */
194
195
  stepSite(direction) {
195
- const view = this.language.popups.getState().callHierarchy ?? null;
196
+ const view = this.language.popups.getState().callHierarchy;
196
197
  if (view !== null && view.sites > 1) {
197
198
  this.select(view.active, (view.site + direction + view.sites) % view.sites);
198
199
  }
199
200
  }
200
201
  /* Goes to the call of a row, or to the symbol of a root, and closes the tree. */
201
202
  go(id) {
202
- const view = this.language.popups.getState().callHierarchy ?? null;
203
+ const view = this.language.popups.getState().callHierarchy;
203
204
  const node = this.nodes.get(id);
204
205
  if (view === null || node === undefined) {
205
206
  return;
@@ -223,16 +224,15 @@ export class CallHierarchyFeature {
223
224
  this.nodes = new Map(roots.map((root) => [root.id, root]));
224
225
  }
225
226
  mounted(container) {
226
- const view = this.language.popups.getState().callHierarchy ?? null;
227
+ const view = this.language.popups.getState().callHierarchy;
227
228
  if (view !== null && view.container !== container) {
228
229
  this.language.popups.setState({ callHierarchy: { ...view, container } });
229
- // The row opens under the caret line, which may sit at the bottom of the view with the tree below the fold. The element is in the page a turn later.
230
- this.timers.set(() => container.scrollIntoView?.({ block: 'nearest' }), 0);
230
+ revealRow(this.timers, container);
231
231
  }
232
232
  }
233
233
  patch(id, change) {
234
234
  const node = this.nodes.get(id);
235
- const view = this.language.popups.getState().callHierarchy ?? null;
235
+ const view = this.language.popups.getState().callHierarchy;
236
236
  if (node === undefined || view === null) {
237
237
  return;
238
238
  }
@@ -292,7 +292,7 @@ export class CallHierarchyFeature {
292
292
  this.texts.set(uri, text);
293
293
  }
294
294
  }
295
- const view = this.language.popups.getState().callHierarchy ?? null;
295
+ const view = this.language.popups.getState().callHierarchy;
296
296
  if (view === null || view.active !== node.id) {
297
297
  return;
298
298
  }
@@ -315,7 +315,7 @@ export class CallHierarchyFeature {
315
315
  return false;
316
316
  }
317
317
  key(event) {
318
- const view = this.language.popups.getState().callHierarchy ?? null;
318
+ const view = this.language.popups.getState().callHierarchy;
319
319
  if (view === null || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey || event.isComposing) {
320
320
  return false;
321
321
  }
@@ -1,4 +1,5 @@
1
1
  import { ErrorCodes, LspError, StaleResultError } from '@adecore/lsp';
2
+ import { KEY_PRIORITY } from './key-priority.js';
2
3
  import { ACTION_GROUPS, REFACTOR_GROUPS, actionsOf, diagnosticsAt, fixableOnLine, isHint, mergeEntries, previewOf } from './code-actions-model.js';
3
4
  import { Refresher } from './refresher.js';
4
5
  import { isShortcut } from './shortcut-keys.js';
@@ -40,11 +41,13 @@ export class CodeActionsFeature {
40
41
  const offs = [
41
42
  editor.onCaret(() => {
42
43
  editor.setGutterAction(null);
44
+ // An answer for the caret's last place would light the bulb on the line it moved to.
45
+ this.refresher.cancel();
43
46
  this.refresher.later();
44
47
  }),
45
48
  editor.onTextChange(() => editor.setGutterAction(null)),
46
49
  editor.onGutterAction(() => void this.open()),
47
- editor.onKeyDown((event) => {
50
+ language.onKey((event) => {
48
51
  if (isShortcut(this.language.shortcuts.codeActions, event)) {
49
52
  void this.open();
50
53
  return true;
@@ -54,7 +57,7 @@ export class CodeActionsFeature {
54
57
  return true;
55
58
  }
56
59
  return false;
57
- }),
60
+ }, KEY_PRIORITY.codeActions),
58
61
  language.diagnostics.onChange(() => this.refresher.later()),
59
62
  providers
60
63
  ];
@@ -1,3 +1,4 @@
1
+ import { labelCollator } from '@adecore/ui/format';
1
2
  import { applyCompletionDefaults } from '@adecore/lsp';
2
3
  import { comparePositions } from './diagnostics-model.js';
3
4
  import { escapeMarkdown, isEmptyHover, splitSignatures } from './hover-content.js';
@@ -156,6 +157,7 @@ export function prefixFor(item, caret, lineBefore, textBetween) {
156
157
  * preselected one, and the server's own order. `recency` is higher for a choice made more recently, and 0 for none.
157
158
  */
158
159
  export function rankCompletions(items, prefixOf, recency = () => 0, limit = 150) {
160
+ const { compare } = labelCollator();
159
161
  return items
160
162
  .map((item, index) => {
161
163
  const prefix = prefixOf(item);
@@ -165,7 +167,7 @@ export function rankCompletions(items, prefixOf, recency = () => 0, limit = 150)
165
167
  .sort((left, right) => left.score - right.score ||
166
168
  right.recent - left.recent ||
167
169
  Number(right.item.preselect === true) - Number(left.item.preselect === true) ||
168
- (left.item.sortText ?? left.item.label).localeCompare(right.item.sortText ?? right.item.label) ||
170
+ compare(left.item.sortText ?? left.item.label, right.item.sortText ?? right.item.label) ||
169
171
  left.index - right.index)
170
172
  .slice(0, limit)
171
173
  .map(({ item, score, prefix }) => ({ item, score, prefix }));
@@ -15,8 +15,10 @@ export declare class CompletionFeature {
15
15
  private server;
16
16
  private request;
17
17
  private controller;
18
+ private resolving;
19
+ private accepting;
20
+ private rankedFor;
18
21
  private requestTimer;
19
- private refreshTimer;
20
22
  private resolveTimer;
21
23
  private textRevision;
22
24
  private acceptSequence;
@@ -27,6 +29,7 @@ export declare class CompletionFeature {
27
29
  invoke(): void;
28
30
  move(delta: number): void;
29
31
  accept(replace: boolean, index?: number, commit?: string): Promise<void>;
32
+ private insert;
30
33
  private refuseLargeCompletion;
31
34
  private placeMirroredCarets;
32
35
  private surroundings;
@@ -38,7 +41,6 @@ export declare class CompletionFeature {
38
41
  private edited;
39
42
  private caretMoved;
40
43
  private triggerCharacters;
41
- private scheduleRefresh;
42
44
  private beginRequest;
43
45
  private ask;
44
46
  private refilter;
@@ -1,4 +1,5 @@
1
1
  import { SnippetTransformQueueError } from './snippet-transform-queue.js';
2
+ import { KEY_PRIORITY } from './key-priority.js';
2
3
  import { SnippetTransformError } from './snippet-transform-client.js';
3
4
  import { MAX_TRANSFORM_JOBS, MAX_TRANSFORM_WORK } from './snippet-transform-protocol.js';
4
5
  import { fileUriToPath, StaleResultError } from '@adecore/lsp';
@@ -14,10 +15,9 @@ import { textBeforeOf } from './line-text.js';
14
15
  import { isEmptyRange } from './proposal-model.js';
15
16
  import { isShortcut } from './shortcut-keys.js';
16
17
  import { realTimers } from './timers.js';
18
+ import { messageOf } from './error-message.js';
17
19
  const METHOD = 'textDocument/completion';
18
20
  const START_DELAY_MS = 80;
19
- /* After typing stops, the list is asked for again at the new text: a server only resolves an item for the text it was made for. */
20
- const REFRESH_DELAY_MS = 150;
21
21
  const RESOLVE_DELAY_MS = 120;
22
22
  const PAGE = 8;
23
23
  const LINES_BACK = 60;
@@ -78,8 +78,11 @@ export class CompletionFeature {
78
78
  server = '';
79
79
  request = 0;
80
80
  controller = null;
81
+ resolving = null;
82
+ accepting = null;
83
+ /* What the list was ranked for last: the caret event and the text event of one keystroke both refilter, and only the first has to rank. */
84
+ rankedFor = null;
81
85
  requestTimer;
82
- refreshTimer;
83
86
  resolveTimer;
84
87
  textRevision = 0;
85
88
  acceptSequence = 0;
@@ -90,16 +93,18 @@ export class CompletionFeature {
90
93
  this.timers = timers;
91
94
  const { editor } = language;
92
95
  const offs = [
93
- editor.onKeyDown((event) => this.key(event)),
96
+ language.onKey((event) => this.key(event), KEY_PRIORITY.completion),
94
97
  editor.onTextChange((change) => this.edited(change)),
95
98
  editor.onCaret((position) => this.caretMoved(position)),
96
99
  editor.onBlur(() => {
97
100
  this.acceptSequence++;
101
+ this.accepting?.abort();
98
102
  this.close();
99
103
  })
100
104
  ];
101
105
  language.onDispose(() => {
102
106
  this.disposed = true;
107
+ this.accepting?.abort();
103
108
  for (const off of offs) {
104
109
  off();
105
110
  }
@@ -126,14 +131,26 @@ export class CompletionFeature {
126
131
  this.publish();
127
132
  this.scheduleResolve();
128
133
  }
129
- /* Picks a row by a click or a key. */
134
+ /* Picks a row by a click or a key. It never rejects: a failure is told to the person, so a caller can leave the promise alone. */
130
135
  async accept(replace, index = this.active, commit) {
136
+ try {
137
+ await this.insert(replace, index, commit);
138
+ }
139
+ catch (error) {
140
+ const { project } = this.language;
141
+ project.host.notify?.({ id: TOAST_ID, kind: 'error', title: project.i18n.t('editor:language.completion.failed', { message: messageOf(error) }) });
142
+ }
143
+ }
144
+ async insert(replace, index, commit) {
131
145
  const entry = this.ranked[index];
132
146
  if (entry === undefined) {
133
147
  return;
134
148
  }
135
149
  const { editor, project, uri, snippets } = this.language;
136
150
  const acceptSequence = ++this.acceptSequence;
151
+ this.accepting?.abort();
152
+ const accepting = new AbortController();
153
+ this.accepting = accepting;
137
154
  const snippetToken = snippets.token;
138
155
  const inSnippet = snippets.isActive;
139
156
  const exitChanges = [];
@@ -160,7 +177,7 @@ export class CompletionFeature {
160
177
  this.close();
161
178
  if (needsResolve) {
162
179
  // Resolving can add the import an item needs; an item that cannot be resolved is still inserted as it is.
163
- item = await this.resolveNow(entry.item);
180
+ item = await this.resolveNow(entry.item, accepting.signal);
164
181
  }
165
182
  if (!current()) {
166
183
  return;
@@ -390,10 +407,10 @@ export class CompletionFeature {
390
407
  * made for. When the person typed on since, the list is asked for again at the new text and the same
391
408
  * suggestion is resolved in that one; an item that cannot be resolved is still inserted as it is.
392
409
  */
393
- async resolveNow(item) {
410
+ async resolveNow(item, signal) {
394
411
  const { editor, project, uri } = this.language;
395
412
  try {
396
- return { ...item, ...(await project.service.resolveCompletion(uri, item)) };
413
+ return { ...item, ...(await project.service.resolveCompletion(uri, item, { signal })) };
397
414
  }
398
415
  catch (error) {
399
416
  if (!(error instanceof StaleResultError)) {
@@ -401,8 +418,8 @@ export class CompletionFeature {
401
418
  }
402
419
  }
403
420
  try {
404
- const fresh = itemsOf(await project.service.completion(uri, editor.getCaret(), { triggerKind: 1 })).items.find((candidate) => candidate.label === item.label && candidate.kind === item.kind);
405
- return fresh === undefined ? item : { ...fresh, ...(await project.service.resolveCompletion(uri, fresh).catch(() => fresh)) };
421
+ const fresh = itemsOf(await project.service.completion(uri, editor.getCaret(), { triggerKind: 1 }, { signal })).items.find((candidate) => candidate.label === item.label && candidate.kind === item.kind);
422
+ return fresh === undefined ? item : { ...fresh, ...(await project.service.resolveCompletion(uri, fresh, { signal }).catch(() => fresh)) };
406
423
  }
407
424
  catch {
408
425
  return item;
@@ -411,11 +428,12 @@ export class CompletionFeature {
411
428
  close() {
412
429
  this.request++;
413
430
  this.controller?.abort();
431
+ this.resolving?.abort();
414
432
  this.timers.clear(this.requestTimer);
415
- this.timers.clear(this.refreshTimer);
416
433
  this.timers.clear(this.resolveTimer);
417
434
  this.pool = [];
418
435
  this.ranked = [];
436
+ this.rankedFor = null;
419
437
  this.touched = false;
420
438
  this.explicit = false;
421
439
  this.activeKey = null;
@@ -498,7 +516,6 @@ export class CompletionFeature {
498
516
  else if (this.isOpen) {
499
517
  if (text === '' || typedIdentifier) {
500
518
  this.refilter();
501
- this.scheduleRefresh();
502
519
  }
503
520
  else {
504
521
  this.close();
@@ -507,6 +524,10 @@ export class CompletionFeature {
507
524
  else if (text.length === 1 && typedIdentifier && isEmptyRange(range)) {
508
525
  this.beginRequest({ triggerKind: 1 }, START_DELAY_MS);
509
526
  }
527
+ else if (text !== '') {
528
+ // A request the last letter started, still waiting or out, would open the list after a character that ends the word.
529
+ this.close();
530
+ }
510
531
  }
511
532
  caretMoved(position) {
512
533
  if (this.start === null) {
@@ -523,19 +544,6 @@ export class CompletionFeature {
523
544
  const { project, uri } = this.language;
524
545
  return project.service.providerOptions(METHOD, uri)?.triggerCharacters ?? [];
525
546
  }
526
- scheduleRefresh() {
527
- const { project, uri } = this.language;
528
- this.timers.clear(this.refreshTimer);
529
- if (this.start === null || !project.service.supports(METHOD, uri)) {
530
- return;
531
- }
532
- const request = this.request;
533
- this.refreshTimer = this.timers.set(() => {
534
- if (request === this.request && this.start !== null) {
535
- void this.ask({ triggerKind: 1 }, request, true);
536
- }
537
- }, REFRESH_DELAY_MS);
538
- }
539
547
  beginRequest(context, delay) {
540
548
  const { project, uri } = this.language;
541
549
  if (!project.service.supports(METHOD, uri)) {
@@ -600,6 +608,11 @@ export class CompletionFeature {
600
608
  return;
601
609
  }
602
610
  const lineBefore = textBeforeOf(editor, caret);
611
+ const typedKey = `${caret.line}:${caret.character}:${lineBefore}`;
612
+ if (this.rankedFor?.pool === this.pool && this.rankedFor.key === typedKey) {
613
+ return;
614
+ }
615
+ this.rankedFor = { pool: this.pool, key: typedKey };
603
616
  // Most items of a list replace the same range, so the text under it is read once and not once per item.
604
617
  const typed = new Map();
605
618
  const textBetween = (range) => {
@@ -635,20 +648,29 @@ export class CompletionFeature {
635
648
  scheduleResolve() {
636
649
  const { project, uri } = this.language;
637
650
  this.timers.clear(this.resolveTimer);
651
+ this.resolving?.abort();
638
652
  const entry = this.ranked[this.active];
639
653
  if (entry === undefined || this.resolved.has(entry.item) || project.service.providerOptions('completionItem/resolve', uri) === undefined) {
640
654
  return;
641
655
  }
656
+ const request = this.request;
642
657
  this.resolveTimer = this.timers.set(() => {
658
+ const controller = new AbortController();
659
+ this.resolving = controller;
643
660
  void project.service
644
- .resolveCompletion(uri, entry.item)
661
+ .resolveCompletion(uri, entry.item, { signal: controller.signal })
645
662
  .then((item) => {
646
663
  this.resolved.set(entry.item, { ...entry.item, ...item });
647
664
  if (this.ranked[this.active]?.item === entry.item) {
648
665
  this.publish();
649
666
  }
650
667
  })
651
- .catch(() => undefined);
668
+ .catch((error) => {
669
+ // A server only resolves an item for the text it was made for, so only an item that is wanted and stale costs a new list.
670
+ if (error instanceof StaleResultError && !controller.signal.aborted && request === this.request && this.start !== null) {
671
+ void this.ask({ triggerKind: 1 }, request, true);
672
+ }
673
+ });
652
674
  }, RESOLVE_DELAY_MS);
653
675
  }
654
676
  publish() {
@@ -9,6 +9,7 @@ export declare function severityOf(diagnostic: Diagnostic): EditorMarkerSeverity
9
9
  export declare function markerOf(diagnostic: Diagnostic): EditorMarker;
10
10
  export declare function problemCountsOf(diagnostics: readonly Diagnostic[]): ProblemCounts;
11
11
  export declare function comparePositions(left: EditorPosition, right: EditorPosition): number;
12
+ export declare function endOfInsertion(start: EditorPosition, text: string): EditorPosition;
12
13
  export declare function shiftPosition(position: EditorPosition, change: EditorContentChange): EditorPosition;
13
14
  export declare function shiftRange(range: EditorRange, changes: readonly EditorContentChange[]): EditorRange;
14
15
  export declare function rangeHolds(range: EditorRange, position: EditorPosition): boolean;
@@ -26,7 +26,7 @@ export function comparePositions(left, right) {
26
26
  return left.line === right.line ? left.character - right.character : left.line - right.line;
27
27
  }
28
28
  /* Where an insertion of `text` at `start` ends, counting a line break of any kind once. */
29
- function endOfInsertion(start, text) {
29
+ export function endOfInsertion(start, text) {
30
30
  const lines = text.split(/\r\n|\r|\n/);
31
31
  return lines.length === 1
32
32
  ? { line: start.line, character: start.character + text.length }
@@ -21,4 +21,5 @@ export declare class DiagnosticsFeature {
21
21
  private reveal;
22
22
  private shift;
23
23
  private rebuild;
24
+ private collect;
24
25
  }
@@ -1,3 +1,5 @@
1
+ import { KEY_PRIORITY } from './key-priority.js';
2
+ import { sameRange } from './hover-content.js';
1
3
  import { isShortcut } from './shortcut-keys.js';
2
4
  import { comparePositions, markerOf, neighborProblem, problemCountsOf, problemsAt, severityOf, shiftRange } from './diagnostics-model.js';
3
5
  /*
@@ -16,19 +18,19 @@ export class DiagnosticsFeature {
16
18
  const reports = project.service.onDiagnostics((report) => {
17
19
  if (report.uri === uri) {
18
20
  this.reports.set(report.source, report.diagnostics);
19
- this.rebuild(true);
21
+ this.rebuild();
20
22
  }
21
23
  });
22
24
  const edits = editor.onTextChange((change) => this.shift(change.changes));
23
25
  // These keys are the editor's while it has the keyboard; elsewhere the menu's commands answer.
24
- const keys = editor.onKeyDown((event) => {
26
+ const keys = language.onKey((event) => {
25
27
  const next = isShortcut(this.language.shortcuts.nextProblem, event);
26
28
  if (!next && !isShortcut(this.language.shortcuts.previousProblem, event)) {
27
29
  return false;
28
30
  }
29
31
  this.step(next ? 1 : -1);
30
32
  return true;
31
- });
33
+ }, KEY_PRIORITY.diagnostics);
32
34
  language.onDispose(() => {
33
35
  keys();
34
36
  reports.dispose();
@@ -80,19 +82,35 @@ export class DiagnosticsFeature {
80
82
  const severity = project.i18n.t(`editor:language.announce.severity.${severityOf(problem.diagnostic)}`);
81
83
  editor.announce(project.i18n.t('editor:language.announce.problem', { severity, message: problem.diagnostic.message }));
82
84
  }
85
+ /* The problems follow the edit as the markers do. Their number does not change, so nobody is told. */
83
86
  shift(changes) {
87
+ let moved = false;
84
88
  for (const [server, diagnostics] of this.reports) {
85
- this.reports.set(server, diagnostics.map((diagnostic) => ({ ...diagnostic, range: shiftRange(diagnostic.range, changes) })));
89
+ let shifted = null;
90
+ for (const [index, diagnostic] of diagnostics.entries()) {
91
+ const range = shiftRange(diagnostic.range, changes);
92
+ if (!sameRange(range, diagnostic.range)) {
93
+ shifted ??= [...diagnostics];
94
+ shifted[index] = { ...diagnostic, range };
95
+ }
96
+ }
97
+ if (shifted !== null) {
98
+ this.reports.set(server, shifted);
99
+ moved = true;
100
+ }
86
101
  }
87
- this.rebuild(false);
88
- }
89
- rebuild(draw) {
90
- this.list = [...this.reports].flatMap(([server, diagnostics]) => diagnostics.map((diagnostic) => ({ diagnostic, server })));
91
- if (draw) {
92
- this.language.editor.setMarkers(this.list.map((problem) => markerOf(problem.diagnostic)));
102
+ if (moved) {
103
+ this.collect();
93
104
  }
105
+ }
106
+ rebuild() {
107
+ this.collect();
108
+ this.language.editor.setMarkers(this.list.map((problem) => markerOf(problem.diagnostic)));
94
109
  for (const listener of [...this.listeners]) {
95
110
  listener();
96
111
  }
97
112
  }
113
+ collect() {
114
+ this.list = [...this.reports].flatMap(([server, diagnostics]) => diagnostics.map((diagnostic) => ({ diagnostic, server })));
115
+ }
98
116
  }
@@ -2,7 +2,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
2
2
  import { useSyncExternalStore } from 'react';
3
3
  import { createPortal } from 'react-dom';
4
4
  import { useTranslation } from 'react-i18next';
5
- import { Button } from '@adecore/ui';
5
+ import { Button, Tooltip } from '@adecore/ui';
6
6
  import { formatNumber } from '@adecore/ui/format';
7
7
  import { RowHost, LineActionHost } from './row-host.js';
8
8
  export function DiffOverlays({ mounted, display, unfold, blocked }) {
@@ -15,7 +15,7 @@ export function DiffOverlays({ mounted, display, unfold, blocked }) {
15
15
  return null;
16
16
  }
17
17
  const text = t('diff.hidden', { count: formatNumber(Math.max(row.oldCount, row.newCount)) });
18
- return createPortal(_jsx("div", { className: "adecore-editor-diff-gap", children: row.unfoldable ? (_jsx(Button, { size: "xs", disabled: blocked?.has(row.id), title: blocked?.has(row.id) ? t('diff.contextLimit') : undefined, onClick: () => unfold(row.id), children: text })) : (_jsx("span", { children: text })) }), container, `gap-${row.id}`);
18
+ return createPortal(_jsx("div", { className: "adecore-editor-diff-gap", children: row.unfoldable && blocked?.has(row.id) === true ? (_jsx(Tooltip, { label: t('diff.contextLimit'), children: _jsx("span", { className: "inline-flex", children: _jsx(Button, { size: "xs", disabled: true, children: text }) }) })) : row.unfoldable ? (_jsx(Button, { size: "xs", onClick: () => unfold(row.id), children: text })) : (_jsx("span", { children: text })) }), container, `gap-${row.id}`);
19
19
  }), display.rows.map((row, index) => {
20
20
  if (row.noNewline === undefined) {
21
21
  return null;
@@ -1,5 +1,5 @@
1
1
  import { type Keymap } from '@adecore/editor/keymap';
2
- import type { Editor, EditorPosition } from '@adecore/editor';
2
+ import type { Editor, EditorKeyHandler, EditorPosition } from '@adecore/editor';
3
3
  import type { Location } from '@adecore/lsp';
4
4
  import { CallHierarchyFeature } from './call-hierarchy.ts';
5
5
  import { CodeActionsFeature } from './code-actions.ts';
@@ -61,6 +61,7 @@ export declare class EditorLanguage {
61
61
  readonly formatOnType: FormatOnTypeFeature;
62
62
  readonly renderedDocs: RenderedDocsFeature;
63
63
  private readonly disposers;
64
+ private readonly keys;
64
65
  private disposed;
65
66
  constructor(project: ProjectLanguage, editor: Editor, uri: string, languageId: string, timers?: Timers, options?: EditorLanguageOptions);
66
67
  get uri(): string;
@@ -71,6 +72,7 @@ export declare class EditorLanguage {
71
72
  visit(place: Place): void;
72
73
  locations(places: readonly Location[], anchor: EditorPosition, title: string): void;
73
74
  onProvidersChanged(listener: () => void): () => void;
75
+ onKey(handler: EditorKeyHandler, priority?: number): () => void;
74
76
  onDispose(dispose: () => void): void;
75
77
  dispose(): void;
76
78
  private placeOpenedCaret;