@adecore/editor-react 0.39.0 → 0.40.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/dist/CallHierarchyPanel.js +30 -55
  2. package/dist/CodeAuthorsCard.js +0 -1
  3. package/dist/CompletionPopup.js +150 -181
  4. package/dist/DiffView.d.ts +1 -1
  5. package/dist/DiffView.js +2 -29
  6. package/dist/PickPopup.js +54 -80
  7. package/dist/SplitDiffView.js +27 -57
  8. package/dist/SymbolPicker.js +2 -8
  9. package/dist/code-actions-model.js +2 -9
  10. package/dist/code-actions.js +10 -11
  11. package/dist/code-vision.js +2 -1
  12. package/dist/completion-model.js +2 -2
  13. package/dist/completion.js +13 -34
  14. package/dist/definition-link.d.ts +1 -0
  15. package/dist/definition-link.js +22 -18
  16. package/dist/diagnostics-model.d.ts +1 -0
  17. package/dist/diagnostics-model.js +3 -1
  18. package/dist/diagnostics.js +1 -2
  19. package/dist/diff-syntax.d.ts +1 -0
  20. package/dist/diff-syntax.js +31 -0
  21. package/dist/doc-comment-model.d.ts +1 -0
  22. package/dist/doc-comment-model.js +1 -1
  23. package/dist/folding.js +2 -3
  24. package/dist/format-on-type.js +6 -10
  25. package/dist/hover-content.d.ts +0 -2
  26. package/dist/hover-content.js +1 -9
  27. package/dist/inlay-hints.d.ts +0 -1
  28. package/dist/inlay-hints.js +11 -41
  29. package/dist/navigation.js +4 -14
  30. package/dist/peek.js +2 -12
  31. package/dist/popup-focus.d.ts +3 -1
  32. package/dist/popup-focus.js +33 -0
  33. package/dist/recent-choices.d.ts +0 -1
  34. package/dist/recent-choices.js +0 -4
  35. package/dist/rendered-docs.d.ts +1 -0
  36. package/dist/rendered-docs.js +15 -28
  37. package/dist/snippet-field-transforms.d.ts +5 -0
  38. package/dist/snippet-field-transforms.js +27 -16
  39. package/dist/snippet-session.js +8 -20
  40. package/dist/symbol-links.js +3 -10
  41. package/dist/symbol-picker-model.d.ts +1 -0
  42. package/dist/symbol-picker-model.js +7 -0
  43. package/dist/unique.d.ts +10 -0
  44. package/dist/unique.js +15 -0
  45. package/package.json +9 -9
  46. package/src/AnchoredPopup.tsx +2 -0
  47. package/src/BaselineStatus.tsx +1 -0
  48. package/src/CallHierarchyPanel.tsx +16 -6
  49. package/src/ChangeReview.tsx +7 -0
  50. package/src/CodeAuthorsCard.tsx +2 -5
  51. package/src/CompletionPopup.tsx +21 -15
  52. package/src/DiffView.tsx +53 -30
  53. package/src/EditorContextMenu.tsx +7 -0
  54. package/src/EditorView.tsx +28 -0
  55. package/src/FindReplace.tsx +8 -0
  56. package/src/HoverCard.tsx +8 -0
  57. package/src/HoverSections.tsx +11 -0
  58. package/src/LanguagePopups.tsx +1 -0
  59. package/src/PanelResizeHandle.tsx +8 -0
  60. package/src/PathText.tsx +1 -0
  61. package/src/PeekPanel.tsx +13 -0
  62. package/src/PickPopup.tsx +7 -6
  63. package/src/RenameCard.tsx +13 -0
  64. package/src/RenderedDocs.tsx +11 -0
  65. package/src/SplitDiffView.tsx +91 -56
  66. package/src/SymbolPicker.tsx +10 -18
  67. package/src/call-hierarchy-model.ts +10 -0
  68. package/src/call-hierarchy.ts +82 -0
  69. package/src/code-actions-model.ts +21 -9
  70. package/src/code-actions.ts +83 -7
  71. package/src/code-authors.ts +20 -0
  72. package/src/code-vision-model.ts +37 -0
  73. package/src/code-vision.ts +47 -3
  74. package/src/completion-call.ts +19 -0
  75. package/src/completion-model.ts +58 -2
  76. package/src/completion.ts +156 -30
  77. package/src/conflict-model.ts +4 -0
  78. package/src/context-menu-rows.ts +1 -0
  79. package/src/context-menu.ts +7 -0
  80. package/src/definition-link.ts +42 -18
  81. package/src/diagnostics-model.ts +19 -1
  82. package/src/diagnostics.ts +31 -2
  83. package/src/diff-expansion.ts +9 -0
  84. package/src/diff-overlays.tsx +8 -0
  85. package/src/diff-syntax.ts +54 -0
  86. package/src/diff-wordmarks.ts +8 -0
  87. package/src/doc-comment-model.ts +51 -1
  88. package/src/editor-language.ts +11 -0
  89. package/src/find-query.ts +7 -0
  90. package/src/folding.ts +10 -3
  91. package/src/format-on-type.ts +23 -10
  92. package/src/highlight-layers.ts +3 -0
  93. package/src/highlights.ts +18 -0
  94. package/src/history.ts +11 -0
  95. package/src/holder.ts +3 -0
  96. package/src/hover-content.ts +34 -17
  97. package/src/hover.ts +57 -0
  98. package/src/index.ts +4 -0
  99. package/src/inlay-hints.ts +86 -47
  100. package/src/key-priority.ts +2 -0
  101. package/src/line-edits.ts +8 -0
  102. package/src/models.ts +6 -0
  103. package/src/navigation-history.ts +5 -0
  104. package/src/navigation.ts +28 -13
  105. package/src/panel-size.ts +7 -0
  106. package/src/patch-decorations.ts +39 -0
  107. package/src/patch-layout.ts +47 -0
  108. package/src/peek-model.ts +21 -0
  109. package/src/peek.ts +49 -13
  110. package/src/pick.ts +35 -0
  111. package/src/popup-focus.ts +11 -1
  112. package/src/popup-placement.ts +5 -0
  113. package/src/popups.ts +2 -6
  114. package/src/project-language.ts +33 -0
  115. package/src/project-problems.ts +11 -0
  116. package/src/proposal-model.ts +40 -0
  117. package/src/recent-choices.ts +3 -5
  118. package/src/refresher.ts +3 -0
  119. package/src/rename-model.ts +28 -0
  120. package/src/rename.ts +56 -0
  121. package/src/rendered-docs.ts +89 -19
  122. package/src/review-group.ts +4 -0
  123. package/src/row-host.ts +12 -0
  124. package/src/selection-ranges.ts +6 -0
  125. package/src/semantic-delta.ts +7 -0
  126. package/src/semantic-model.ts +11 -0
  127. package/src/semantic-tokens.ts +37 -0
  128. package/src/shortcut-keys.ts +3 -0
  129. package/src/signature-model.ts +7 -0
  130. package/src/signature.ts +27 -0
  131. package/src/snippet-field-transforms.ts +48 -13
  132. package/src/snippet-indent.ts +10 -0
  133. package/src/snippet-range-order.ts +5 -0
  134. package/src/snippet-session.ts +173 -31
  135. package/src/snippet-transform-client.ts +14 -0
  136. package/src/snippet-transform-format.ts +98 -0
  137. package/src/snippet-transform-protocol.ts +5 -0
  138. package/src/snippet-transform-queue.ts +53 -0
  139. package/src/snippet-transform-worker.ts +14 -0
  140. package/src/snippet-variables.ts +9 -0
  141. package/src/snippet.ts +107 -0
  142. package/src/split-alignment.ts +10 -0
  143. package/src/split-scroll.ts +8 -0
  144. package/src/symbol-links.ts +9 -12
  145. package/src/symbol-picker-model.ts +25 -0
  146. package/src/symbol-picker.ts +9 -0
  147. package/src/symbols.ts +21 -0
  148. package/src/testing.ts +7 -0
  149. package/src/timers.ts +5 -0
  150. package/src/unique.ts +23 -0
  151. package/src/use-problem-counts.ts +4 -0
  152. package/src/view-state.ts +9 -0
  153. package/src/word-diff.ts +5 -0
  154. package/src/workspace-edit.ts +84 -0
@@ -16,6 +16,7 @@ const SCROLL_PAUSE_MS = 100;
16
16
 
17
17
  /* A document with more lines or characters than this is first asked for what is in view, when the server answers ranges. */
18
18
  export const DEFAULT_RANGE_LINES = 5000;
19
+
19
20
  export const DEFAULT_RANGE_CHARACTERS = 500_000;
20
21
 
21
22
  /* What decides when a document is too large to wait for in full. */
@@ -45,10 +46,13 @@ export class SemanticTokensFeature {
45
46
  private readonly options: SemanticTokensOptions;
46
47
  private held: Held | null = null;
47
48
  private heldLegend = '';
49
+
48
50
  /* True once the whole document is drawn from a full answer; a range answer then no longer paints. */
49
51
  private complete = false;
52
+
50
53
  /* The lines a range answer drew, null when nothing of that kind is on screen. */
51
54
  private covered: EditorRange | null = null;
55
+
52
56
  private drawn: EditorSemanticToken[] = [];
53
57
  private epoch = 0;
54
58
 
@@ -58,22 +62,26 @@ export class SemanticTokensFeature {
58
62
  this.options = options;
59
63
  this.refresher = new Refresher((signal) => this.refresh(signal), PAUSE_MS, timers);
60
64
  this.scrolled = new Refresher((signal) => this.fillViewport(signal), SCROLL_PAUSE_MS, timers);
65
+
61
66
  const edits = editor.onTextChange(() => {
62
67
  this.epoch++;
63
68
  this.covered = null;
64
69
  this.refresher.later();
65
70
  });
71
+
66
72
  const view = editor.onViewChange(() => {
67
73
  if (!this.complete && this.covered !== null && !covers(this.covered, editor.getVisibleRange())) {
68
74
  this.scrolled.later();
69
75
  }
70
76
  });
77
+
71
78
  const providers = language.onProvidersChanged(() => {
72
79
  this.epoch++;
73
80
  this.scrolled.cancel();
74
81
  this.forget();
75
82
  this.refresher.now();
76
83
  });
84
+
77
85
  language.onDispose(() => {
78
86
  edits();
79
87
  view();
@@ -97,16 +105,19 @@ export class SemanticTokensFeature {
97
105
  const { uri } = this.language;
98
106
  const legend = (service.providerOptions(FULL, uri) ?? service.providerOptions(RANGE, uri))?.legend;
99
107
  const key = legend === undefined ? '' : JSON.stringify(legend);
108
+
100
109
  if (key !== this.heldLegend) {
101
110
  this.heldLegend = key;
102
111
  this.forget();
103
112
  }
113
+
104
114
  return legend;
105
115
  }
106
116
 
107
117
  private isLarge(): boolean {
108
118
  const { editor } = this.language;
109
119
  const end = editor.positionAt(Number.MAX_SAFE_INTEGER);
120
+
110
121
  return (
111
122
  end.line + 1 > (this.options.rangeLines ?? DEFAULT_RANGE_LINES) || editor.offsetAt(end) > (this.options.rangeCharacters ?? DEFAULT_RANGE_CHARACTERS)
112
123
  );
@@ -114,6 +125,7 @@ export class SemanticTokensFeature {
114
125
 
115
126
  private viewport(): EditorRange {
116
127
  const { editor } = this.language;
128
+
117
129
  return withMargin(editor.getVisibleRange(), editor.positionAt(Number.MAX_SAFE_INTEGER), (line) => ({
118
130
  line,
119
131
  character: lineTextOf(editor, line).length
@@ -126,20 +138,26 @@ export class SemanticTokensFeature {
126
138
  const legend = this.legend();
127
139
  const full = service.supports(FULL, uri);
128
140
  const range = service.supports(RANGE, uri);
141
+
129
142
  if (legend === undefined || (!full && !range)) {
130
143
  return;
131
144
  }
145
+
132
146
  if (!full || (range && !this.complete && this.held === null && this.isLarge())) {
133
147
  await this.askRange(this.viewport(), legend, signal, true);
148
+
134
149
  if (!full) {
135
150
  return;
136
151
  }
137
152
  }
153
+
138
154
  const epoch = this.epoch;
139
155
  const tokens = await this.askFull(signal);
156
+
140
157
  if (signal.aborted || epoch !== this.epoch) {
141
158
  return;
142
159
  }
160
+
143
161
  this.complete = tokens !== null;
144
162
  this.covered = null;
145
163
  this.drawn = [];
@@ -151,27 +169,36 @@ export class SemanticTokensFeature {
151
169
  const { service } = this.language.project;
152
170
  const { uri } = this.language;
153
171
  const base = this.held;
172
+
154
173
  if (base !== null && service.supports(DELTA, uri)) {
155
174
  const answer = await service.semanticTokensDelta(uri, base.resultId, { signal }).catch((error: unknown) => {
156
175
  if (signal.aborted || error instanceof StaleResultError) {
157
176
  throw error;
158
177
  }
178
+
159
179
  // A server that no longer knows our result id is asked in full instead, once.
160
180
  return null;
161
181
  });
182
+
162
183
  signal.throwIfAborted();
184
+
163
185
  if (answer !== null && this.held === base) {
164
186
  const tokens = this.fromDelta(base, answer);
187
+
165
188
  if (tokens !== null) {
166
189
  this.hold(tokens);
190
+
167
191
  return tokens;
168
192
  }
169
193
  }
194
+
170
195
  this.held = null;
171
196
  }
197
+
172
198
  const tokens = await service.semanticTokens(uri, { signal });
173
199
  signal.throwIfAborted();
174
200
  this.hold(tokens);
201
+
175
202
  return tokens;
176
203
  }
177
204
 
@@ -180,7 +207,9 @@ export class SemanticTokensFeature {
180
207
  if (!('edits' in answer)) {
181
208
  return answer;
182
209
  }
210
+
183
211
  const data = applySemanticEdits(base.data, answer.edits);
212
+
184
213
  return data === null ? null : { ...(answer.resultId === undefined ? {} : { resultId: answer.resultId }), data };
185
214
  }
186
215
 
@@ -195,22 +224,28 @@ export class SemanticTokensFeature {
195
224
  const epoch = this.epoch;
196
225
  const before = this.covered;
197
226
  const tokens = await service.semanticTokensRange(uri, range, { signal });
227
+
198
228
  if (signal.aborted || epoch !== this.epoch || this.complete) {
199
229
  return;
200
230
  }
231
+
201
232
  const decoded = tokens === null ? [] : decodeSemanticTokens(tokens, legend);
233
+
202
234
  if (fresh || before === null || range.start.line > before.end.line + 1 || range.end.line < before.start.line - 1) {
203
235
  this.covered = range;
204
236
  this.drawn = decoded;
205
237
  } else {
206
238
  const kept = this.drawn.filter((token) => token.line < range.start.line || token.line > range.end.line);
207
239
  this.drawn = [...kept, ...decoded];
240
+
208
241
  this.covered = {
209
242
  start: before.start.line < range.start.line ? before.start : range.start,
210
243
  end: before.end.line > range.end.line ? before.end : range.end
211
244
  };
212
245
  }
246
+
213
247
  editor.setSemanticTokens(this.drawn);
248
+
214
249
  if (!this.complete && this.covered !== null && !covers(this.covered, editor.getVisibleRange())) {
215
250
  this.scrolled.later();
216
251
  }
@@ -219,9 +254,11 @@ export class SemanticTokensFeature {
219
254
  private async fillViewport(signal: AbortSignal): Promise<void> {
220
255
  const { service } = this.language.project;
221
256
  const legend = this.legend();
257
+
222
258
  if (legend === undefined || !service.supports(RANGE, this.language.uri) || this.complete) {
223
259
  return;
224
260
  }
261
+
225
262
  await this.askRange(this.viewport(), legend, signal, false);
226
263
  }
227
264
  }
@@ -10,9 +10,11 @@ export function shortcutsOf(keymap: Keymap, apple: boolean) {
10
10
  const shortcuts = Object.fromEntries(
11
11
  KEYMAP_IDS.map((id) => {
12
12
  const text = chordOf(id, apple, keymap);
13
+
13
14
  return [id, text === null ? null : { ...parseChord(text), apple }];
14
15
  })
15
16
  ) as Record<keyof Keymap, LanguageShortcut | null>;
17
+
16
18
  return { ...shortcuts, rename: shortcuts.renameSymbol };
17
19
  }
18
20
 
@@ -21,5 +23,6 @@ export function isShortcut(target: LanguageShortcut | null, event: KeyboardEvent
21
23
  if (target === null || typesInsteadOfChord(event, target.apple)) {
22
24
  return false;
23
25
  }
26
+
24
27
  return matchesShortcut(target, event, target.apple);
25
28
  }
@@ -17,14 +17,19 @@ export interface SignatureViewModel {
17
17
  /* Where a parameter's label stands in the signature's: given as a pair of offsets, or as the text to find in it. */
18
18
  function parameterSpan(signature: SignatureInformation, index: number): { start: number; end: number } | null {
19
19
  const parameter = signature.parameters?.[index];
20
+
20
21
  if (parameter === undefined) {
21
22
  return null;
22
23
  }
24
+
23
25
  if (typeof parameter.label !== 'string') {
24
26
  const [start, end] = parameter.label;
27
+
25
28
  return start >= 0 && end <= signature.label.length && start < end ? { start, end } : null;
26
29
  }
30
+
27
31
  const start = signature.label.indexOf(parameter.label);
32
+
28
33
  return start < 0 ? null : { start, end: start + parameter.label.length };
29
34
  }
30
35
 
@@ -32,11 +37,13 @@ export function signatureViewOf(help: SignatureHelp | null): SignatureViewModel
32
37
  if (help === null || help.signatures.length === 0) {
33
38
  return null;
34
39
  }
40
+
35
41
  const index = Math.min(Math.max(0, help.activeSignature ?? 0), help.signatures.length - 1);
36
42
  const signature = help.signatures[index]!;
37
43
  const parameterIndex = signature.activeParameter ?? help.activeParameter ?? 0;
38
44
  const active = parameterSpan(signature, parameterIndex);
39
45
  const parameter = signature.parameters?.[parameterIndex];
46
+
40
47
  return {
41
48
  label: signature.label,
42
49
  active,
package/src/signature.ts CHANGED
@@ -27,6 +27,7 @@ export class SignatureFeature {
27
27
  this.language = language;
28
28
  this.timers = timers;
29
29
  const { editor } = language;
30
+
30
31
  const offs = [
31
32
  editor.onTextChange((change) => this.edited(change)),
32
33
  editor.onCaret(() => this.retrigger()),
@@ -34,22 +35,29 @@ export class SignatureFeature {
34
35
  language.onKey((event) => {
35
36
  if (event.key === 'Escape' && this.isOpen && this.language.popups.getState().callHierarchy === null) {
36
37
  this.close();
38
+
37
39
  return true;
38
40
  }
41
+
39
42
  if (isShortcut(this.language.shortcuts.parameterInfo, event)) {
40
43
  this.invoke();
44
+
41
45
  return true;
42
46
  }
47
+
43
48
  if ((event.key === 'ArrowUp' || event.key === 'ArrowDown') && !event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) {
44
49
  return this.cycle(event.key === 'ArrowDown' ? 1 : -1);
45
50
  }
51
+
46
52
  return false;
47
53
  }, KEY_PRIORITY.signature)
48
54
  ];
55
+
49
56
  language.onDispose(() => {
50
57
  for (const off of offs) {
51
58
  off();
52
59
  }
60
+
53
61
  this.close();
54
62
  });
55
63
  }
@@ -71,17 +79,22 @@ export class SignatureFeature {
71
79
  /* Shows the next or previous overload; false when the card has fewer than two, which leaves the arrow to the caret. */
72
80
  private cycle(step: 1 | -1): boolean {
73
81
  const { help } = this;
82
+
74
83
  if (help === null || help.signatures.length < 2 || !this.isOpen) {
75
84
  return false;
76
85
  }
86
+
77
87
  const count = help.signatures.length;
78
88
  const next = { ...help, activeSignature: (Math.min(Math.max(0, help.activeSignature ?? 0), count - 1) + step + count) % count };
79
89
  const model = signatureViewOf(next);
90
+
80
91
  if (model === null || this.anchor === null) {
81
92
  return false;
82
93
  }
94
+
83
95
  this.help = next;
84
96
  this.language.popups.setState({ signature: { anchor: this.anchor, model } });
97
+
85
98
  return true;
86
99
  }
87
100
 
@@ -91,6 +104,7 @@ export class SignatureFeature {
91
104
  this.timers.clear(this.timer);
92
105
  this.help = null;
93
106
  this.anchor = null;
107
+
94
108
  if (this.isOpen) {
95
109
  this.language.popups.setState({ signature: null });
96
110
  }
@@ -99,17 +113,22 @@ export class SignatureFeature {
99
113
  private options(): { triggers: readonly string[]; retriggers: readonly string[] } | null {
100
114
  const { project, uri } = this.language;
101
115
  const options = project.service.providerOptions(METHOD, uri);
116
+
102
117
  return options === undefined ? null : { triggers: options.triggerCharacters ?? [], retriggers: options.retriggerCharacters ?? [] };
103
118
  }
104
119
 
105
120
  private edited(change: EditorTextChange): void {
106
121
  const options = this.options();
122
+
107
123
  if (change.source !== 'input' || options === null) {
108
124
  this.close();
125
+
109
126
  return;
110
127
  }
128
+
111
129
  const text = change.changes.length === 1 ? change.changes[0]!.text : '';
112
130
  const last = Array.from(text).at(-1) ?? '';
131
+
113
132
  if (text.length === 1 && options.triggers.includes(last)) {
114
133
  this.ask({ triggerKind: 2, triggerCharacter: last, isRetrigger: this.isOpen });
115
134
  } else if (this.isOpen) {
@@ -129,9 +148,11 @@ export class SignatureFeature {
129
148
 
130
149
  private ask(context: Omit<SignatureHelpContext, 'activeSignatureHelp'>): void {
131
150
  const { project, uri } = this.language;
151
+
132
152
  if (!project.service.supports(METHOD, uri)) {
133
153
  return;
134
154
  }
155
+
135
156
  this.timers.clear(this.timer);
136
157
  const request = ++this.request;
137
158
  const full: SignatureHelpContext = { ...context, ...(this.help === null ? {} : { activeSignatureHelp: this.help }) };
@@ -145,19 +166,25 @@ export class SignatureFeature {
145
166
  const controller = new AbortController();
146
167
  this.controller = controller;
147
168
  let help: SignatureHelp | null;
169
+
148
170
  try {
149
171
  help = await project.service.signatureHelp(uri, caret, context, { signal: controller.signal });
150
172
  } catch {
151
173
  return;
152
174
  }
175
+
153
176
  if (request !== this.request) {
154
177
  return;
155
178
  }
179
+
156
180
  const model = signatureViewOf(help);
181
+
157
182
  if (model === null) {
158
183
  this.close();
184
+
159
185
  return;
160
186
  }
187
+
161
188
  this.help = help;
162
189
  this.anchor ??= caret;
163
190
  this.language.popups.setState({ signature: { anchor: this.anchor, model } });
@@ -15,6 +15,28 @@ export interface FieldTransformTarget {
15
15
  readonly order: readonly number[];
16
16
  readonly ancestors?: readonly symbol[];
17
17
  }
18
+
19
+ /* Each distinct job once, so a transform that several ranges share is computed once; `slotOf` is its index in `jobs`. */
20
+ export function jobTable(): { readonly jobs: SnippetTransformJob[]; readonly slotOf: (job: SnippetTransformJob) => number } {
21
+ const jobs: SnippetTransformJob[] = [];
22
+ const keys = new Map<string, number>();
23
+
24
+ const slotOf = (job: SnippetTransformJob): number => {
25
+ const key = JSON.stringify(job);
26
+ let slot = keys.get(key);
27
+
28
+ if (slot === undefined) {
29
+ slot = jobs.length;
30
+ keys.set(key, slot);
31
+ jobs.push(job);
32
+ }
33
+
34
+ return slot;
35
+ };
36
+
37
+ return { jobs, slotOf };
38
+ }
39
+
18
40
  interface Entry {
19
41
  readonly targets: FieldTransformTarget[];
20
42
  readonly current: () => boolean;
@@ -30,51 +52,52 @@ export class SnippetFieldTransforms {
30
52
  private readonly editor: Editor;
31
53
  private readonly lease: SnippetTransformLease | null;
32
54
  private readonly apply: (targets: readonly FieldTransformTarget[], values: readonly string[]) => void;
55
+
33
56
  constructor(editor: Editor, lease: SnippetTransformLease | null, apply: (targets: readonly FieldTransformTarget[], values: readonly string[]) => void) {
34
57
  this.editor = editor;
35
58
  this.lease = lease;
36
59
  this.apply = apply;
37
60
  }
61
+
38
62
  get pending(): boolean {
39
63
  return this.entries.size > 0;
40
64
  }
65
+
41
66
  get ranges(): number {
42
67
  return [...this.entries].reduce((sum, entry) => sum + entry.targets.length, 0);
43
68
  }
69
+
44
70
  async settle(): Promise<void> {
45
71
  await Promise.all([...this.entries].map((entry) => entry.done));
46
72
  }
73
+
47
74
  enqueue(targets: FieldTransformTarget[], current: () => boolean): void {
48
75
  if (targets.length === 0 || this.lease === null) {
49
76
  return;
50
77
  }
78
+
51
79
  let finish!: () => void;
80
+
52
81
  const done = new Promise<void>((resolve) => {
53
82
  finish = resolve;
54
83
  });
84
+
55
85
  const entry: Entry = { targets, current, done, finish, cancelled: false };
56
86
  this.entries.add(entry);
57
- const jobs: SnippetTransformJob[] = [],
58
- keys = new Map<string, number>();
59
- const slots = targets.map((target) => {
60
- const job = { ...target.transform, value: target.value },
61
- key = JSON.stringify(job);
62
- let slot = keys.get(key);
63
- if (slot === undefined) {
64
- slot = jobs.length;
65
- keys.set(key, slot);
66
- jobs.push(job);
67
- }
68
- return slot;
69
- });
87
+ const { jobs, slotOf } = jobTable();
88
+ const slots = targets.map((target) => slotOf({ ...target.transform, value: target.value }));
89
+
70
90
  const valid = (): boolean => !entry.cancelled && entry.current() && targets.every((target) => this.editor.textInRange(target.range) === target.value);
91
+
71
92
  void this.lease
72
93
  .request(jobs, { kind: 'exit', current: valid })
73
94
  .then((values) => {
74
95
  if (!valid()) {
75
96
  return;
76
97
  }
98
+
77
99
  this.applying = entry;
100
+
78
101
  try {
79
102
  this.apply(
80
103
  targets,
@@ -92,6 +115,7 @@ export class SnippetFieldTransforms {
92
115
  finish();
93
116
  });
94
117
  }
118
+
95
119
  prune(): void {
96
120
  for (const entry of this.entries) {
97
121
  if (!entry.current()) {
@@ -99,26 +123,32 @@ export class SnippetFieldTransforms {
99
123
  }
100
124
  }
101
125
  }
126
+
102
127
  clear(): void {
103
128
  for (const entry of this.entries) {
104
129
  this.cancel(entry);
105
130
  }
106
131
  }
132
+
107
133
  private cancel(entry: Entry): void {
108
134
  entry.cancelled = true;
109
135
  this.entries.delete(entry);
110
136
  entry.finish();
111
137
  }
138
+
112
139
  edited(changes: readonly EditorContentChange[], detached?: <Range extends EditorRange>(range: Range) => Range): void {
113
140
  for (const entry of this.entries) {
114
141
  if (entry === this.applying) {
115
142
  continue;
116
143
  }
144
+
117
145
  for (const target of entry.targets) {
118
146
  let { start, end } = target.range;
147
+
119
148
  for (const change of changes) {
120
149
  const empty = comparePositions(start, end) === 0;
121
150
  const insert = comparePositions(change.range.start, change.range.end) === 0;
151
+
122
152
  const overlaps = insert
123
153
  ? empty
124
154
  ? comparePositions(start, change.range.start) === 0
@@ -126,15 +156,20 @@ export class SnippetFieldTransforms {
126
156
  : empty
127
157
  ? comparePositions(change.range.start, start) <= 0 && comparePositions(start, change.range.end) < 0
128
158
  : comparePositions(change.range.start, end) < 0 && comparePositions(start, change.range.end) < 0;
159
+
129
160
  if (overlaps && !(detached !== undefined && empty && insert)) {
130
161
  this.cancel(entry);
162
+
131
163
  break;
132
164
  }
165
+
133
166
  const insertAtEnd = insert && !empty && comparePositions(end, change.range.start) === 0;
134
167
  start = shiftPosition(start, change);
135
168
  end = insertAtEnd ? end : shiftPosition(end, change);
136
169
  }
170
+
137
171
  target.range = detached === undefined ? { start, end } : detached({ ...target.range, order: target.order });
172
+
138
173
  if (entry.cancelled) {
139
174
  break;
140
175
  }
@@ -4,29 +4,38 @@ import { MAX_SNIPPET_TEXT, type ParsedSnippet, type SnippetRange } from './snipp
4
4
  /* Indentation edits map the offsets once, including child projections in copied parents. */
5
5
  export function indentSnippet(parsed: ParsedSnippet, before: string): ParsedSnippet {
6
6
  const prefix = /^[\t ]*/.exec(before)?.[0] ?? '';
7
+
7
8
  if (prefix === '' || !parsed.text.includes('\n')) {
8
9
  return parsed;
9
10
  }
11
+
10
12
  const insertions: number[] = [];
13
+
11
14
  for (let offset = parsed.text.indexOf('\n'); offset !== -1; offset = parsed.text.indexOf('\n', offset + 1)) {
12
15
  insertions.push(offset + 1);
13
16
  }
17
+
14
18
  if (parsed.text.length + insertions.length * prefix.length > MAX_SNIPPET_TEXT) {
15
19
  return { text: parsed.text, stops: [], limited: true };
16
20
  }
21
+
17
22
  const shift = (offset: number, after = true): number => {
18
23
  let low = 0;
19
24
  let high = insertions.length;
25
+
20
26
  while (low < high) {
21
27
  const middle = (low + high) >>> 1;
28
+
22
29
  if (insertions[middle]! < offset || (after && insertions[middle] === offset)) {
23
30
  low = middle + 1;
24
31
  } else {
25
32
  high = middle;
26
33
  }
27
34
  }
35
+
28
36
  return offset + low * prefix.length;
29
37
  };
38
+
30
39
  const range = <Range extends SnippetRange>(range: Range): Range =>
31
40
  withSnippetOrder(
32
41
  {
@@ -36,6 +45,7 @@ export function indentSnippet(parsed: ParsedSnippet, before: string): ParsedSnip
36
45
  },
37
46
  snippetOrderOf(range)
38
47
  );
48
+
39
49
  return {
40
50
  text: parsed.text.replace(/\n/g, '\n' + prefix),
41
51
  stops: parsed.stops.map((stop) => range(stop)),
@@ -1,19 +1,24 @@
1
1
  /* Source order disambiguates empty stops without changing the public parsed range shape. */
2
2
  const orders = new WeakMap<object, number>();
3
+
3
4
  export function snippetOrderOf(range: object): number | undefined {
4
5
  return orders.get(range);
5
6
  }
7
+
6
8
  export function withSnippetOrder<Range extends object>(range: Range, order: number | undefined): Range {
7
9
  if (order !== undefined) {
8
10
  orders.set(range, order);
9
11
  }
12
+
10
13
  return range;
11
14
  }
15
+
12
16
  export function compareSnippetOrder(left: readonly number[], right: readonly number[]): number {
13
17
  for (let index = 0; index < Math.min(left.length, right.length); index++) {
14
18
  if (left[index] !== right[index]) {
15
19
  return left[index]! - right[index]!;
16
20
  }
17
21
  }
22
+
18
23
  return left.length - right.length;
19
24
  }