@adecore/editor-react 0.23.1 → 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 (109) hide show
  1. package/dist/AnchoredPopup.d.ts +2 -1
  2. package/dist/AnchoredPopup.js +2 -2
  3. package/dist/CodeAuthorsCard.d.ts +4 -0
  4. package/dist/CodeAuthorsCard.js +8 -4
  5. package/dist/CompletionPopup.js +7 -3
  6. package/dist/EditorView.d.ts +8 -3
  7. package/dist/EditorView.js +79 -9
  8. package/dist/HoverCard.d.ts +3 -1
  9. package/dist/HoverCard.js +2 -2
  10. package/dist/LanguagePopups.d.ts +8 -4
  11. package/dist/LanguagePopups.js +4 -4
  12. package/dist/PickPopup.js +2 -1
  13. package/dist/RenameCard.js +10 -8
  14. package/dist/RenderedDocs.d.ts +10 -4
  15. package/dist/RenderedDocs.js +10 -6
  16. package/dist/call-hierarchy.js +15 -14
  17. package/dist/code-actions.js +5 -2
  18. package/dist/completion-model.js +3 -1
  19. package/dist/completion.d.ts +4 -2
  20. package/dist/completion.js +49 -27
  21. package/dist/diagnostics-model.d.ts +1 -0
  22. package/dist/diagnostics-model.js +1 -1
  23. package/dist/diagnostics.d.ts +1 -0
  24. package/dist/diagnostics.js +28 -10
  25. package/dist/diff-overlays.js +2 -2
  26. package/dist/editor-language.d.ts +3 -1
  27. package/dist/editor-language.js +10 -0
  28. package/dist/format-on-type.js +2 -3
  29. package/dist/highlights.js +3 -2
  30. package/dist/history.js +3 -2
  31. package/dist/holder.d.ts +3 -3
  32. package/dist/holder.js +2 -1
  33. package/dist/host-types.d.ts +3 -0
  34. package/dist/hover.d.ts +5 -0
  35. package/dist/hover.js +13 -5
  36. package/dist/index.d.ts +6 -2
  37. package/dist/index.js +3 -1
  38. package/dist/key-priority.d.ts +23 -0
  39. package/dist/key-priority.js +39 -0
  40. package/dist/locales/en.json +2 -1
  41. package/dist/locales/nl.json +2 -1
  42. package/dist/models.d.ts +2 -2
  43. package/dist/models.js +2 -2
  44. package/dist/navigation.js +3 -2
  45. package/dist/peek.js +3 -2
  46. package/dist/pick.js +2 -1
  47. package/dist/popup-focus.d.ts +5 -0
  48. package/dist/popup-focus.js +12 -0
  49. package/dist/popups.d.ts +1 -1
  50. package/dist/project-language.js +5 -4
  51. package/dist/project-problems.d.ts +10 -1
  52. package/dist/project-problems.js +34 -10
  53. package/dist/proposal-model.d.ts +0 -1
  54. package/dist/proposal-model.js +0 -8
  55. package/dist/rename-model.d.ts +2 -1
  56. package/dist/rename-model.js +57 -0
  57. package/dist/rename.d.ts +1 -0
  58. package/dist/rename.js +21 -5
  59. package/dist/rendered-docs.js +3 -2
  60. package/dist/signature.js +4 -3
  61. package/dist/snippet-session.d.ts +1 -1
  62. package/dist/snippet-session.js +8 -2
  63. package/dist/symbol-picker.js +3 -2
  64. package/package.json +6 -6
  65. package/src/AnchoredPopup.tsx +4 -2
  66. package/src/CodeAuthorsCard.tsx +21 -12
  67. package/src/CompletionPopup.tsx +11 -1
  68. package/src/EditorView.tsx +122 -11
  69. package/src/HoverCard.tsx +6 -1
  70. package/src/LanguagePopups.tsx +23 -4
  71. package/src/PickPopup.tsx +9 -1
  72. package/src/RenameCard.tsx +15 -9
  73. package/src/RenderedDocs.tsx +30 -8
  74. package/src/call-hierarchy.ts +15 -14
  75. package/src/code-actions.ts +5 -2
  76. package/src/completion-model.ts +3 -1
  77. package/src/completion.ts +48 -28
  78. package/src/diagnostics-model.ts +1 -1
  79. package/src/diagnostics.ts +29 -13
  80. package/src/diff-overlays.tsx +12 -8
  81. package/src/editor-language.ts +12 -1
  82. package/src/format-on-type.ts +2 -3
  83. package/src/highlights.ts +3 -2
  84. package/src/history.ts +3 -2
  85. package/src/holder.ts +5 -4
  86. package/src/host-types.ts +5 -0
  87. package/src/hover.ts +19 -5
  88. package/src/index.ts +6 -2
  89. package/src/key-priority.ts +49 -0
  90. package/src/locales/en.json +2 -1
  91. package/src/locales/nl.json +2 -1
  92. package/src/models.ts +1 -1
  93. package/src/navigation.ts +3 -2
  94. package/src/peek.ts +3 -2
  95. package/src/pick.ts +2 -1
  96. package/src/popup-focus.ts +17 -0
  97. package/src/popups.ts +1 -2
  98. package/src/project-language.ts +5 -4
  99. package/src/project-problems.ts +46 -11
  100. package/src/proposal-model.ts +0 -9
  101. package/src/rename-model.ts +60 -1
  102. package/src/rename.ts +21 -5
  103. package/src/rendered-docs.ts +3 -2
  104. package/src/signature.ts +4 -3
  105. package/src/snippet-session.ts +9 -3
  106. package/src/symbol-picker.ts +3 -2
  107. package/dist/relative-time.d.ts +0 -1
  108. package/dist/relative-time.js +0 -9
  109. package/src/relative-time.ts +0 -10
@@ -1,4 +1,5 @@
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';
@@ -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,7 +224,7 @@ 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
230
  revealRow(this.timers, container);
@@ -231,7 +232,7 @@ export class CallHierarchyFeature {
231
232
  }
232
233
  patch(id, change) {
233
234
  const node = this.nodes.get(id);
234
- const view = this.language.popups.getState().callHierarchy ?? null;
235
+ const view = this.language.popups.getState().callHierarchy;
235
236
  if (node === undefined || view === null) {
236
237
  return;
237
238
  }
@@ -291,7 +292,7 @@ export class CallHierarchyFeature {
291
292
  this.texts.set(uri, text);
292
293
  }
293
294
  }
294
- const view = this.language.popups.getState().callHierarchy ?? null;
295
+ const view = this.language.popups.getState().callHierarchy;
295
296
  if (view === null || view.active !== node.id) {
296
297
  return;
297
298
  }
@@ -314,7 +315,7 @@ export class CallHierarchyFeature {
314
315
  return false;
315
316
  }
316
317
  key(event) {
317
- const view = this.language.popups.getState().callHierarchy ?? null;
318
+ const view = this.language.popups.getState().callHierarchy;
318
319
  if (view === null || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey || event.isComposing) {
319
320
  return false;
320
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;
@@ -11,6 +11,7 @@ import { FormatOnTypeFeature } from './format-on-type.js';
11
11
  import { HighlightsFeature } from './highlights.js';
12
12
  import { HistoryFeature } from './history.js';
13
13
  import { HoverFeature } from './hover.js';
14
+ import { KeyDispatcher } from './key-priority.js';
14
15
  import { NavigationFeature, locationRow } from './navigation.js';
15
16
  import { PeekFeature } from './peek.js';
16
17
  import { PickFeature } from './pick.js';
@@ -62,6 +63,7 @@ export class EditorLanguage {
62
63
  formatOnType;
63
64
  renderedDocs;
64
65
  disposers = [];
66
+ keys = new KeyDispatcher();
65
67
  disposed = false;
66
68
  constructor(project, editor, uri, languageId, timers = realTimers, options = {}) {
67
69
  this.editor = editor;
@@ -71,6 +73,7 @@ export class EditorLanguage {
71
73
  this.project = project;
72
74
  this.languageId = languageId;
73
75
  this.document = project.acquire(uri, languageId, editor);
76
+ this.onDispose(editor.onKeyDown((event) => this.keys.dispatch(event)));
74
77
  this.pick = new PickFeature(this, timers);
75
78
  this.diagnostics = new DiagnosticsFeature(this);
76
79
  this.hover = new HoverFeature(this, timers);
@@ -154,6 +157,13 @@ export class EditorLanguage {
154
157
  providers.dispose();
155
158
  };
156
159
  }
160
+ /*
161
+ * Lets `handler` see the editor's keys before every handler of a lower `priority`, the first to take a key
162
+ * winning. `KEY_PRIORITY` holds the features'; the default, 0, comes after all of them.
163
+ */
164
+ onKey(handler, priority = 0) {
165
+ return this.keys.add(handler, priority);
166
+ }
157
167
  /* Runs `dispose` when the editor lets go of its document. */
158
168
  onDispose(dispose) {
159
169
  this.disposers.push(dispose);
@@ -20,12 +20,11 @@ function overlaps(edits) {
20
20
  * second caret, a read-only editor or a snippet session, whose tab stops an edit could move. An answer is applied
21
21
  * only while the text is exactly what it was asked about, and the caret where it was.
22
22
  *
23
- * The edits join the undo step of the typed character, so one undo takes both back. On by default where the server offers it; `configure(false)`
24
- * turns it off for a host that has a setting for it.
23
+ * The edits join the undo step of the typed character, so one undo takes both back. Off by default; a host enables it explicitly with `configure(true)`.
25
24
  */
26
25
  export class FormatOnTypeFeature {
27
26
  language;
28
- enabled = true;
27
+ enabled = false;
29
28
  /* Raised by every change of the text, so an answer for an older text is told from the one for the text that is. */
30
29
  revision = 0;
31
30
  controller = new AbortController();
@@ -1,3 +1,4 @@
1
+ import { KEY_PRIORITY } from './key-priority.js';
1
2
  import { comparePositions, rangeHolds } from './diagnostics-model.js';
2
3
  import { isIdentifierCharacter } from './completion-model.js';
3
4
  import { Refresher } from './refresher.js';
@@ -57,13 +58,13 @@ export class HighlightsFeature {
57
58
  this.current = [];
58
59
  this.refresher.later();
59
60
  }),
60
- editor.onKeyDown((event) => {
61
+ language.onKey((event) => {
61
62
  const next = isShortcut(this.language.shortcuts.nextHighlight, event);
62
63
  if (!next && !isShortcut(this.language.shortcuts.previousHighlight, event)) {
63
64
  return false;
64
65
  }
65
66
  return this.step(next ? 1 : -1);
66
- })
67
+ }, KEY_PRIORITY.highlights)
67
68
  ];
68
69
  language.onDispose(() => {
69
70
  for (const off of offs) {
package/dist/history.js CHANGED
@@ -1,10 +1,11 @@
1
+ import { KEY_PRIORITY } from './key-priority.js';
1
2
  import { isShortcut } from './shortcut-keys.js';
2
3
  /* Back, Forward and the list of recent locations over the history the whole project shares. */
3
4
  export class HistoryFeature {
4
5
  language;
5
6
  constructor(language) {
6
7
  this.language = language;
7
- const off = language.editor.onKeyDown((event) => {
8
+ const off = language.onKey((event) => {
8
9
  if (isShortcut(this.language.shortcuts.historyBack, event)) {
9
10
  this.back();
10
11
  return true;
@@ -18,7 +19,7 @@ export class HistoryFeature {
18
19
  return true;
19
20
  }
20
21
  return false;
21
- });
22
+ }, KEY_PRIORITY.history);
22
23
  language.onDispose(off);
23
24
  }
24
25
  /* Returns to where the caret was before the last jump; false when there is no such place. */
package/dist/holder.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export declare function createHolder<T>(): {
2
- get: () => T | null;
3
- set: (next: T | null) => void;
4
- subscribe: (listener: () => void) => () => void;
2
+ get(): T | null;
3
+ set(value: T | null): void;
4
+ subscribe(listener: () => void): () => void;
5
5
  };