@adecore/editor-react 0.22.1 → 0.23.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 (209) hide show
  1. package/dist/CallHierarchyPanel.d.ts +6 -0
  2. package/dist/CallHierarchyPanel.js +92 -0
  3. package/dist/CompletionPopup.js +11 -3
  4. package/dist/DiffView.d.ts +22 -0
  5. package/dist/DiffView.js +203 -0
  6. package/dist/EditorContextMenu.js +1 -0
  7. package/dist/EditorView.js +18 -1
  8. package/dist/FindReplace.d.ts +1 -1
  9. package/dist/FindReplace.js +41 -11
  10. package/dist/HoverCard.js +2 -2
  11. package/dist/LanguagePopups.js +4 -1
  12. package/dist/Markdown.d.ts +2 -1
  13. package/dist/Markdown.js +8 -4
  14. package/dist/PanelResizeHandle.d.ts +8 -0
  15. package/dist/PanelResizeHandle.js +71 -0
  16. package/dist/PeekPanel.d.ts +3 -0
  17. package/dist/PeekPanel.js +7 -4
  18. package/dist/PickPopup.js +15 -2
  19. package/dist/RenameCard.js +10 -5
  20. package/dist/RenderedDocs.d.ts +9 -0
  21. package/dist/RenderedDocs.js +84 -0
  22. package/dist/SplitDiffView.d.ts +9 -0
  23. package/dist/SplitDiffView.js +276 -0
  24. package/dist/SymbolPicker.d.ts +3 -0
  25. package/dist/SymbolPicker.js +4 -3
  26. package/dist/call-hierarchy-model.d.ts +52 -0
  27. package/dist/call-hierarchy-model.js +93 -0
  28. package/dist/call-hierarchy.d.ts +42 -0
  29. package/dist/call-hierarchy.js +398 -0
  30. package/dist/code-actions-model.d.ts +2 -0
  31. package/dist/code-actions-model.js +12 -10
  32. package/dist/code-actions.js +3 -1
  33. package/dist/completion-model.d.ts +14 -4
  34. package/dist/completion-model.js +40 -33
  35. package/dist/completion.d.ts +4 -0
  36. package/dist/completion.js +257 -42
  37. package/dist/diagnostics.js +8 -6
  38. package/dist/diff-expansion.d.ts +9 -0
  39. package/dist/diff-expansion.js +41 -0
  40. package/dist/diff-overlays.d.ts +15 -0
  41. package/dist/diff-overlays.js +28 -0
  42. package/dist/diff-syntax.d.ts +16 -0
  43. package/dist/diff-syntax.js +49 -0
  44. package/dist/diff-wordmarks.d.ts +5 -0
  45. package/dist/diff-wordmarks.js +26 -0
  46. package/dist/doc-comment-model.d.ts +23 -0
  47. package/dist/doc-comment-model.js +291 -0
  48. package/dist/editor-language.d.ts +13 -1
  49. package/dist/editor-language.js +15 -5
  50. package/dist/editor-react.css +79 -0
  51. package/dist/format-on-type.d.ts +14 -0
  52. package/dist/format-on-type.js +96 -0
  53. package/dist/host-types.d.ts +19 -0
  54. package/dist/hover.d.ts +4 -0
  55. package/dist/hover.js +15 -2
  56. package/dist/index.d.ts +19 -5
  57. package/dist/index.js +9 -1
  58. package/dist/inlay-hints.d.ts +47 -2
  59. package/dist/inlay-hints.js +353 -16
  60. package/dist/language-ids.js +4 -2
  61. package/dist/locales/en.json +117 -3
  62. package/dist/locales/nl.json +117 -3
  63. package/dist/models.d.ts +6 -2
  64. package/dist/models.js +5 -1
  65. package/dist/panel-size.d.ts +6 -0
  66. package/dist/panel-size.js +25 -0
  67. package/dist/patch-decorations.d.ts +27 -0
  68. package/dist/patch-decorations.js +182 -0
  69. package/dist/patch-layout.d.ts +40 -0
  70. package/dist/patch-layout.js +304 -0
  71. package/dist/peek.d.ts +3 -1
  72. package/dist/peek.js +6 -1
  73. package/dist/pick.d.ts +1 -0
  74. package/dist/pick.js +11 -1
  75. package/dist/popups.d.ts +17 -0
  76. package/dist/popups.js +1 -0
  77. package/dist/project-language.d.ts +12 -0
  78. package/dist/project-language.js +52 -2
  79. package/dist/refresher.d.ts +1 -0
  80. package/dist/refresher.js +5 -0
  81. package/dist/rename-model.d.ts +1 -1
  82. package/dist/rename-model.js +3 -7
  83. package/dist/rename.js +5 -8
  84. package/dist/rendered-docs.d.ts +64 -0
  85. package/dist/rendered-docs.js +374 -0
  86. package/dist/row-host.d.ts +2 -0
  87. package/dist/row-host.js +11 -3
  88. package/dist/semantic-delta.d.ts +2 -0
  89. package/dist/semantic-delta.js +27 -0
  90. package/dist/semantic-model.js +16 -0
  91. package/dist/semantic-tokens.d.ts +26 -1
  92. package/dist/semantic-tokens.js +174 -14
  93. package/dist/shortcut-keys.d.ts +2 -0
  94. package/dist/shortcut-keys.js +6 -2
  95. package/dist/signature.js +1 -1
  96. package/dist/snippet-field-transforms.d.ts +30 -0
  97. package/dist/snippet-field-transforms.js +116 -0
  98. package/dist/snippet-indent.d.ts +2 -0
  99. package/dist/snippet-indent.js +49 -0
  100. package/dist/snippet-range-order.d.ts +3 -0
  101. package/dist/snippet-range-order.js +19 -0
  102. package/dist/snippet-session.d.ts +42 -6
  103. package/dist/snippet-session.js +650 -53
  104. package/dist/snippet-transform-client.d.ts +19 -0
  105. package/dist/snippet-transform-client.js +66 -0
  106. package/dist/snippet-transform-format.d.ts +15 -0
  107. package/dist/snippet-transform-format.js +302 -0
  108. package/dist/snippet-transform-protocol.d.ts +45 -0
  109. package/dist/snippet-transform-protocol.js +8 -0
  110. package/dist/snippet-transform-queue.d.ts +32 -0
  111. package/dist/snippet-transform-queue.js +173 -0
  112. package/dist/snippet-transform-worker.d.ts +10 -0
  113. package/dist/snippet-transform-worker.js +41 -0
  114. package/dist/snippet-variable-names.d.ts +1 -0
  115. package/dist/snippet-variable-names.js +36 -0
  116. package/dist/snippet-variables.d.ts +7 -0
  117. package/dist/snippet-variables.js +66 -0
  118. package/dist/snippet.d.ts +30 -3
  119. package/dist/snippet.js +402 -87
  120. package/dist/split-alignment.d.ts +6 -0
  121. package/dist/split-alignment.js +38 -0
  122. package/dist/split-scroll.d.ts +14 -0
  123. package/dist/split-scroll.js +45 -0
  124. package/dist/symbols.d.ts +2 -1
  125. package/dist/symbols.js +39 -7
  126. package/dist/testing.d.ts +5 -1
  127. package/dist/testing.js +12 -0
  128. package/dist/timers.d.ts +1 -0
  129. package/dist/timers.js +4 -0
  130. package/dist/use-panel-body.d.ts +6 -0
  131. package/dist/use-panel-body.js +13 -0
  132. package/dist/workspace-edit.d.ts +25 -3
  133. package/dist/workspace-edit.js +282 -143
  134. package/package.json +10 -5
  135. package/src/CallHierarchyPanel.tsx +244 -0
  136. package/src/CompletionPopup.tsx +12 -1
  137. package/src/DiffView.tsx +262 -0
  138. package/src/EditorContextMenu.tsx +1 -0
  139. package/src/EditorView.tsx +21 -2
  140. package/src/FindReplace.tsx +51 -13
  141. package/src/HoverCard.tsx +23 -21
  142. package/src/LanguagePopups.tsx +5 -0
  143. package/src/Markdown.tsx +10 -3
  144. package/src/PanelResizeHandle.tsx +104 -0
  145. package/src/PeekPanel.tsx +11 -6
  146. package/src/PickPopup.tsx +18 -2
  147. package/src/RenameCard.tsx +18 -0
  148. package/src/RenderedDocs.tsx +155 -0
  149. package/src/SplitDiffView.tsx +337 -0
  150. package/src/SymbolPicker.tsx +4 -3
  151. package/src/call-hierarchy-model.ts +151 -0
  152. package/src/call-hierarchy.ts +440 -0
  153. package/src/code-actions-model.ts +17 -11
  154. package/src/code-actions.ts +3 -1
  155. package/src/completion-model.ts +61 -35
  156. package/src/completion.ts +302 -42
  157. package/src/diagnostics.ts +8 -6
  158. package/src/diff-expansion.ts +49 -0
  159. package/src/diff-overlays.tsx +69 -0
  160. package/src/diff-syntax.ts +67 -0
  161. package/src/diff-wordmarks.ts +31 -0
  162. package/src/doc-comment-model.ts +336 -0
  163. package/src/editor-language.ts +22 -6
  164. package/src/editor-react.css +79 -0
  165. package/src/format-on-type.ts +111 -0
  166. package/src/host-types.ts +23 -0
  167. package/src/hover.ts +18 -3
  168. package/src/index.ts +27 -4
  169. package/src/inlay-hints.ts +394 -18
  170. package/src/language-ids.ts +4 -2
  171. package/src/locales/en.json +117 -3
  172. package/src/locales/nl.json +117 -3
  173. package/src/models.ts +40 -1
  174. package/src/panel-size.ts +28 -0
  175. package/src/patch-decorations.ts +224 -0
  176. package/src/patch-layout.ts +388 -0
  177. package/src/peek.ts +6 -1
  178. package/src/pick.ts +12 -1
  179. package/src/popups.ts +24 -0
  180. package/src/project-language.ts +63 -2
  181. package/src/refresher.ts +6 -0
  182. package/src/rename-model.ts +4 -8
  183. package/src/rename.ts +5 -8
  184. package/src/rendered-docs.ts +430 -0
  185. package/src/row-host.ts +12 -3
  186. package/src/semantic-delta.ts +29 -0
  187. package/src/semantic-model.ts +17 -0
  188. package/src/semantic-tokens.ts +202 -19
  189. package/src/shortcut-keys.ts +6 -2
  190. package/src/signature.ts +1 -1
  191. package/src/snippet-field-transforms.ts +144 -0
  192. package/src/snippet-indent.ts +53 -0
  193. package/src/snippet-range-order.ts +19 -0
  194. package/src/snippet-session.ts +797 -56
  195. package/src/snippet-transform-client.ts +84 -0
  196. package/src/snippet-transform-format.ts +325 -0
  197. package/src/snippet-transform-protocol.ts +24 -0
  198. package/src/snippet-transform-queue.ts +199 -0
  199. package/src/snippet-transform-worker.ts +58 -0
  200. package/src/snippet-variable-names.ts +36 -0
  201. package/src/snippet-variables.ts +74 -0
  202. package/src/snippet.ts +452 -89
  203. package/src/split-alignment.ts +45 -0
  204. package/src/split-scroll.ts +54 -0
  205. package/src/symbols.ts +43 -7
  206. package/src/testing.ts +25 -0
  207. package/src/timers.ts +5 -0
  208. package/src/use-panel-body.ts +17 -0
  209. package/src/workspace-edit.ts +347 -169
@@ -1,44 +1,227 @@
1
+ import type { EditorRange, EditorSemanticToken } from '@adecore/editor';
2
+ import { StaleResultError, type SemanticTokens, type SemanticTokensDelta, type SemanticTokensLegend } from '@adecore/lsp';
1
3
  import type { EditorLanguage } from './editor-language.ts';
4
+ import { covers, withMargin } from './inlay-hints.ts';
5
+ import { lineTextOf } from './line-text.ts';
2
6
  import { Refresher } from './refresher.ts';
7
+ import { applySemanticEdits } from './semantic-delta.ts';
3
8
  import { decodeSemanticTokens } from './semantic-model.ts';
4
9
  import { realTimers, type Timers } from './timers.ts';
5
10
 
6
- const METHOD = 'textDocument/semanticTokens/full';
11
+ const FULL = 'textDocument/semanticTokens/full';
12
+ const DELTA = 'textDocument/semanticTokens/full/delta';
13
+ const RANGE = 'textDocument/semanticTokens/range';
7
14
  const PAUSE_MS = 200;
15
+ const SCROLL_PAUSE_MS = 100;
16
+
17
+ /* A document with more lines or characters than this is first asked for what is in view, when the server answers ranges. */
18
+ export const DEFAULT_RANGE_LINES = 5000;
19
+ export const DEFAULT_RANGE_CHARACTERS = 500_000;
20
+
21
+ /* What decides when a document is too large to wait for in full. */
22
+ export interface SemanticTokensOptions {
23
+ /* Lines above which a document is asked for its visible range first. */
24
+ rangeLines?: number;
25
+ /* Characters above which a document is asked for its visible range first. */
26
+ rangeCharacters?: number;
27
+ }
28
+
29
+ /* The last full answer, which the next delta is computed against. */
30
+ interface Held {
31
+ resultId: string;
32
+ data: number[];
33
+ }
8
34
 
9
35
  /*
10
36
  * Colors from what the language servers know, over the grammar's guess. It asks after a pause in typing
11
37
  * and when a server's offer changed, and keeps drawing the last answer meanwhile so the colors never blink out.
38
+ * A server that offers deltas is asked for the change since its last answer; one that offers ranges is asked
39
+ * for the lines in view first when the document is large, and for them alone when it offers nothing else.
12
40
  */
13
41
  export class SemanticTokensFeature {
14
42
  private readonly refresher: Refresher;
43
+ private readonly scrolled: Refresher;
44
+ private readonly language: EditorLanguage;
45
+ private readonly options: SemanticTokensOptions;
46
+ private held: Held | null = null;
47
+ private heldLegend = '';
48
+ /* True once the whole document is drawn from a full answer; a range answer then no longer paints. */
49
+ private complete = false;
50
+ /* The lines a range answer drew, null when nothing of that kind is on screen. */
51
+ private covered: EditorRange | null = null;
52
+ private drawn: EditorSemanticToken[] = [];
53
+ private epoch = 0;
15
54
 
16
- constructor(language: EditorLanguage, timers: Timers = realTimers) {
17
- const { editor, project, uri } = language;
18
- this.refresher = new Refresher(
19
- async (signal) => {
20
- const { service } = project;
21
- const legend = service.providerOptions(METHOD, uri)?.legend;
22
- if (!service.supports(METHOD, uri) || legend === undefined) {
23
- return;
24
- }
25
- const tokens = await service.semanticTokens(uri, { signal });
26
- if (!signal.aborted) {
27
- editor.setSemanticTokens(tokens === null ? null : decodeSemanticTokens(tokens, legend));
28
- }
29
- },
30
- PAUSE_MS,
31
- timers
32
- );
33
- const edits = editor.onTextChange(() => this.refresher.later());
55
+ constructor(language: EditorLanguage, timers: Timers = realTimers, options: SemanticTokensOptions = {}) {
56
+ const { editor } = language;
57
+ this.language = language;
58
+ this.options = options;
59
+ this.refresher = new Refresher((signal) => this.refresh(signal), PAUSE_MS, timers);
60
+ this.scrolled = new Refresher((signal) => this.fillViewport(signal), SCROLL_PAUSE_MS, timers);
61
+ const edits = editor.onTextChange(() => {
62
+ this.epoch++;
63
+ this.covered = null;
64
+ this.refresher.later();
65
+ });
66
+ const view = editor.onViewChange(() => {
67
+ if (!this.complete && this.covered !== null && !covers(this.covered, editor.getVisibleRange())) {
68
+ this.scrolled.later();
69
+ }
70
+ });
34
71
  const providers = language.onProvidersChanged(() => {
72
+ this.epoch++;
73
+ this.scrolled.cancel();
74
+ this.forget();
35
75
  this.refresher.now();
36
76
  });
37
77
  language.onDispose(() => {
38
78
  edits();
79
+ view();
39
80
  providers();
40
81
  this.refresher.dispose();
82
+ this.scrolled.dispose();
83
+ this.forget();
41
84
  editor.setSemanticTokens(null);
42
85
  });
43
86
  }
87
+
88
+ /* Drops what the next delta would be computed against, which a refresh, a restart or a new legend makes meaningless. */
89
+ private forget(): void {
90
+ this.held = null;
91
+ this.complete = false;
92
+ this.covered = null;
93
+ }
94
+
95
+ private legend(): SemanticTokensLegend | undefined {
96
+ const { service } = this.language.project;
97
+ const { uri } = this.language;
98
+ const legend = (service.providerOptions(FULL, uri) ?? service.providerOptions(RANGE, uri))?.legend;
99
+ const key = legend === undefined ? '' : JSON.stringify(legend);
100
+ if (key !== this.heldLegend) {
101
+ this.heldLegend = key;
102
+ this.forget();
103
+ }
104
+ return legend;
105
+ }
106
+
107
+ private isLarge(): boolean {
108
+ const { editor } = this.language;
109
+ const end = editor.positionAt(Number.MAX_SAFE_INTEGER);
110
+ return (
111
+ end.line + 1 > (this.options.rangeLines ?? DEFAULT_RANGE_LINES) || editor.offsetAt(end) > (this.options.rangeCharacters ?? DEFAULT_RANGE_CHARACTERS)
112
+ );
113
+ }
114
+
115
+ private viewport(): EditorRange {
116
+ const { editor } = this.language;
117
+ return withMargin(editor.getVisibleRange(), editor.positionAt(Number.MAX_SAFE_INTEGER), (line) => ({
118
+ line,
119
+ character: lineTextOf(editor, line).length
120
+ }));
121
+ }
122
+
123
+ private async refresh(signal: AbortSignal): Promise<void> {
124
+ const { service } = this.language.project;
125
+ const { editor, uri } = this.language;
126
+ const legend = this.legend();
127
+ const full = service.supports(FULL, uri);
128
+ const range = service.supports(RANGE, uri);
129
+ if (legend === undefined || (!full && !range)) {
130
+ return;
131
+ }
132
+ if (!full || (range && !this.complete && this.held === null && this.isLarge())) {
133
+ await this.askRange(this.viewport(), legend, signal, true);
134
+ if (!full) {
135
+ return;
136
+ }
137
+ }
138
+ const epoch = this.epoch;
139
+ const tokens = await this.askFull(signal);
140
+ if (signal.aborted || epoch !== this.epoch) {
141
+ return;
142
+ }
143
+ this.complete = tokens !== null;
144
+ this.covered = null;
145
+ this.drawn = [];
146
+ editor.setSemanticTokens(tokens === null ? null : decodeSemanticTokens(tokens, legend));
147
+ }
148
+
149
+ /* The whole document: as a delta when the last answer can be built on, else in full. Null when the server has no tokens. */
150
+ private async askFull(signal: AbortSignal): Promise<SemanticTokens | null> {
151
+ const { service } = this.language.project;
152
+ const { uri } = this.language;
153
+ const base = this.held;
154
+ if (base !== null && service.supports(DELTA, uri)) {
155
+ const answer = await service.semanticTokensDelta(uri, base.resultId, { signal }).catch((error: unknown) => {
156
+ if (signal.aborted || error instanceof StaleResultError) {
157
+ throw error;
158
+ }
159
+ // A server that no longer knows our result id is asked in full instead, once.
160
+ return null;
161
+ });
162
+ signal.throwIfAborted();
163
+ if (answer !== null && this.held === base) {
164
+ const tokens = this.fromDelta(base, answer);
165
+ if (tokens !== null) {
166
+ this.hold(tokens);
167
+ return tokens;
168
+ }
169
+ }
170
+ this.held = null;
171
+ }
172
+ const tokens = await service.semanticTokens(uri, { signal });
173
+ signal.throwIfAborted();
174
+ this.hold(tokens);
175
+ return tokens;
176
+ }
177
+
178
+ /* Null when the answer is a delta that does not fit the data it should be applied to. */
179
+ private fromDelta(base: Held, answer: SemanticTokens | SemanticTokensDelta): SemanticTokens | null {
180
+ if (!('edits' in answer)) {
181
+ return answer;
182
+ }
183
+ const data = applySemanticEdits(base.data, answer.edits);
184
+ return data === null ? null : { ...(answer.resultId === undefined ? {} : { resultId: answer.resultId }), data };
185
+ }
186
+
187
+ private hold(tokens: SemanticTokens | null): void {
188
+ this.held = tokens?.resultId === undefined ? null : { resultId: tokens.resultId, data: tokens.data };
189
+ }
190
+
191
+ /* Draws the lines of a range answer. A fresh one replaces everything drawn, an extension keeps the lines it did not cover. */
192
+ private async askRange(range: EditorRange, legend: SemanticTokensLegend, signal: AbortSignal, fresh: boolean): Promise<void> {
193
+ const { service } = this.language.project;
194
+ const { editor, uri } = this.language;
195
+ const epoch = this.epoch;
196
+ const before = this.covered;
197
+ const tokens = await service.semanticTokensRange(uri, range, { signal });
198
+ if (signal.aborted || epoch !== this.epoch || this.complete) {
199
+ return;
200
+ }
201
+ const decoded = tokens === null ? [] : decodeSemanticTokens(tokens, legend);
202
+ if (fresh || before === null || range.start.line > before.end.line + 1 || range.end.line < before.start.line - 1) {
203
+ this.covered = range;
204
+ this.drawn = decoded;
205
+ } else {
206
+ const kept = this.drawn.filter((token) => token.line < range.start.line || token.line > range.end.line);
207
+ this.drawn = [...kept, ...decoded];
208
+ this.covered = {
209
+ start: before.start.line < range.start.line ? before.start : range.start,
210
+ end: before.end.line > range.end.line ? before.end : range.end
211
+ };
212
+ }
213
+ editor.setSemanticTokens(this.drawn);
214
+ if (!this.complete && this.covered !== null && !covers(this.covered, editor.getVisibleRange())) {
215
+ this.scrolled.later();
216
+ }
217
+ }
218
+
219
+ private async fillViewport(signal: AbortSignal): Promise<void> {
220
+ const { service } = this.language.project;
221
+ const legend = this.legend();
222
+ if (legend === undefined || !service.supports(RANGE, this.language.uri) || this.complete) {
223
+ return;
224
+ }
225
+ await this.askRange(this.viewport(), legend, signal, false);
226
+ }
44
227
  }
@@ -1,4 +1,4 @@
1
- import { chordOf, KEYMAP_IDS, parseChord, type Keymap } from '@adecore/editor/keymap';
1
+ import { chordOf, KEYMAP_IDS, parseChord, type Keymap, typesInsteadOfChord } from '@adecore/editor/keymap';
2
2
  import type { KeyChord } from '@adecore/editor';
3
3
  import { matchesShortcut } from '@adecore/ui';
4
4
 
@@ -16,6 +16,10 @@ export function shortcutsOf(keymap: Keymap, apple: boolean) {
16
16
  return { ...shortcuts, rename: shortcuts.renameSymbol };
17
17
  }
18
18
 
19
+ /* Ctrl and Alt together that type a character, as AltGr does on some layouts, are no shortcut. */
19
20
  export function isShortcut(target: LanguageShortcut | null, event: KeyboardEvent): boolean {
20
- return target !== null && matchesShortcut(target, event, target.apple);
21
+ if (target === null || typesInsteadOfChord(event, target.apple)) {
22
+ return false;
23
+ }
24
+ return matchesShortcut(target, event, target.apple);
21
25
  }
package/src/signature.ts CHANGED
@@ -31,7 +31,7 @@ export class SignatureFeature {
31
31
  editor.onCaret(() => this.retrigger()),
32
32
  editor.onBlur(() => this.close()),
33
33
  editor.onKeyDown((event) => {
34
- if (event.key === 'Escape' && this.isOpen) {
34
+ if (event.key === 'Escape' && this.isOpen && this.language.popups.getState().callHierarchy == null) {
35
35
  this.close();
36
36
  return true;
37
37
  }
@@ -0,0 +1,144 @@
1
+ import type { Editor, EditorContentChange, EditorRange } from '@adecore/editor';
2
+ import { comparePositions, shiftPosition } from './diagnostics-model.ts';
3
+ import type { SnippetTransform } from './snippet.ts';
4
+ import type { SnippetTransformLease } from './snippet-transform-queue.ts';
5
+ import type { SnippetTransformJob } from './snippet-transform-protocol.ts';
6
+
7
+ export interface FieldTransformTarget {
8
+ readonly id: symbol;
9
+ range: EditorRange;
10
+ readonly value: string;
11
+ readonly transform: SnippetTransform;
12
+ readonly parents?: readonly number[];
13
+ readonly owner: object;
14
+ readonly frame: object;
15
+ readonly order: readonly number[];
16
+ readonly ancestors?: readonly symbol[];
17
+ }
18
+ interface Entry {
19
+ readonly targets: FieldTransformTarget[];
20
+ readonly current: () => boolean;
21
+ readonly done: Promise<void>;
22
+ readonly finish: () => void;
23
+ cancelled: boolean;
24
+ }
25
+
26
+ /* Navigation frames can pop before a worker replies; their tracked targets live independently. */
27
+ export class SnippetFieldTransforms {
28
+ private readonly entries = new Set<Entry>();
29
+ private applying: Entry | null = null;
30
+ private readonly editor: Editor;
31
+ private readonly lease: SnippetTransformLease | null;
32
+ private readonly apply: (targets: readonly FieldTransformTarget[], values: readonly string[]) => void;
33
+ constructor(editor: Editor, lease: SnippetTransformLease | null, apply: (targets: readonly FieldTransformTarget[], values: readonly string[]) => void) {
34
+ this.editor = editor;
35
+ this.lease = lease;
36
+ this.apply = apply;
37
+ }
38
+ get pending(): boolean {
39
+ return this.entries.size > 0;
40
+ }
41
+ get ranges(): number {
42
+ return [...this.entries].reduce((sum, entry) => sum + entry.targets.length, 0);
43
+ }
44
+ async settle(): Promise<void> {
45
+ await Promise.all([...this.entries].map((entry) => entry.done));
46
+ }
47
+ enqueue(targets: FieldTransformTarget[], current: () => boolean): void {
48
+ if (targets.length === 0 || this.lease === null) {
49
+ return;
50
+ }
51
+ let finish!: () => void;
52
+ const done = new Promise<void>((resolve) => {
53
+ finish = resolve;
54
+ });
55
+ const entry: Entry = { targets, current, done, finish, cancelled: false };
56
+ 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
+ });
70
+ const valid = (): boolean => !entry.cancelled && entry.current() && targets.every((target) => this.editor.textInRange(target.range) === target.value);
71
+ void this.lease
72
+ .request(jobs, { kind: 'exit', current: valid })
73
+ .then((values) => {
74
+ if (!valid()) {
75
+ return;
76
+ }
77
+ this.applying = entry;
78
+ try {
79
+ this.apply(
80
+ targets,
81
+ slots.map((slot) => values[slot]!)
82
+ );
83
+ } finally {
84
+ this.applying = null;
85
+ }
86
+ })
87
+ .catch(() => {
88
+ /* Failed exits retain the raw field value without retrying. */
89
+ })
90
+ .finally(() => {
91
+ this.entries.delete(entry);
92
+ finish();
93
+ });
94
+ }
95
+ prune(): void {
96
+ for (const entry of this.entries) {
97
+ if (!entry.current()) {
98
+ this.cancel(entry);
99
+ }
100
+ }
101
+ }
102
+ clear(): void {
103
+ for (const entry of this.entries) {
104
+ this.cancel(entry);
105
+ }
106
+ }
107
+ private cancel(entry: Entry): void {
108
+ entry.cancelled = true;
109
+ this.entries.delete(entry);
110
+ entry.finish();
111
+ }
112
+ edited(changes: readonly EditorContentChange[], detached?: <Range extends EditorRange>(range: Range) => Range): void {
113
+ for (const entry of this.entries) {
114
+ if (entry === this.applying) {
115
+ continue;
116
+ }
117
+ for (const target of entry.targets) {
118
+ let { start, end } = target.range;
119
+ for (const change of changes) {
120
+ const empty = comparePositions(start, end) === 0;
121
+ const insert = comparePositions(change.range.start, change.range.end) === 0;
122
+ const overlaps = insert
123
+ ? empty
124
+ ? comparePositions(start, change.range.start) === 0
125
+ : comparePositions(start, change.range.start) < 0 && comparePositions(change.range.start, end) < 0
126
+ : empty
127
+ ? comparePositions(change.range.start, start) <= 0 && comparePositions(start, change.range.end) < 0
128
+ : comparePositions(change.range.start, end) < 0 && comparePositions(start, change.range.end) < 0;
129
+ if (overlaps && !(detached !== undefined && empty && insert)) {
130
+ this.cancel(entry);
131
+ break;
132
+ }
133
+ const insertAtEnd = insert && !empty && comparePositions(end, change.range.start) === 0;
134
+ start = shiftPosition(start, change);
135
+ end = insertAtEnd ? end : shiftPosition(end, change);
136
+ }
137
+ target.range = detached === undefined ? { start, end } : detached({ ...target.range, order: target.order });
138
+ if (entry.cancelled) {
139
+ break;
140
+ }
141
+ }
142
+ }
143
+ }
144
+ }
@@ -0,0 +1,53 @@
1
+ import { snippetOrderOf, withSnippetOrder } from './snippet-range-order.ts';
2
+ import { MAX_SNIPPET_TEXT, type ParsedSnippet, type SnippetRange } from './snippet.ts';
3
+
4
+ /* Indentation edits map the offsets once, including child projections in copied parents. */
5
+ export function indentSnippet(parsed: ParsedSnippet, before: string): ParsedSnippet {
6
+ const prefix = /^[\t ]*/.exec(before)?.[0] ?? '';
7
+ if (prefix === '' || !parsed.text.includes('\n')) {
8
+ return parsed;
9
+ }
10
+ const insertions: number[] = [];
11
+ for (let offset = parsed.text.indexOf('\n'); offset !== -1; offset = parsed.text.indexOf('\n', offset + 1)) {
12
+ insertions.push(offset + 1);
13
+ }
14
+ if (parsed.text.length + insertions.length * prefix.length > MAX_SNIPPET_TEXT) {
15
+ return { text: parsed.text, stops: [], limited: true };
16
+ }
17
+ const shift = (offset: number, after = true): number => {
18
+ let low = 0;
19
+ let high = insertions.length;
20
+ while (low < high) {
21
+ const middle = (low + high) >>> 1;
22
+ if (insertions[middle]! < offset || (after && insertions[middle] === offset)) {
23
+ low = middle + 1;
24
+ } else {
25
+ high = middle;
26
+ }
27
+ }
28
+ return offset + low * prefix.length;
29
+ };
30
+ const range = <Range extends SnippetRange>(range: Range): Range =>
31
+ withSnippetOrder(
32
+ {
33
+ ...range,
34
+ start: shift(range.start),
35
+ end: shift(range.end, range.start === range.end || range.tail === true)
36
+ },
37
+ snippetOrderOf(range)
38
+ );
39
+ return {
40
+ text: parsed.text.replace(/\n/g, '\n' + prefix),
41
+ stops: parsed.stops.map((stop) => range(stop)),
42
+ ...(parsed.transforms === undefined ? {} : { transforms: parsed.transforms.map((transform) => range(transform)) }),
43
+ ...(parsed.fields === undefined
44
+ ? {}
45
+ : {
46
+ fields: parsed.fields.map((field) => ({
47
+ ...field,
48
+ ranges: field.ranges.map((child) => range(child)),
49
+ ...(field.linked === undefined ? {} : { linked: field.linked.map((child) => range(child)) })
50
+ }))
51
+ })
52
+ };
53
+ }
@@ -0,0 +1,19 @@
1
+ /* Source order disambiguates empty stops without changing the public parsed range shape. */
2
+ const orders = new WeakMap<object, number>();
3
+ export function snippetOrderOf(range: object): number | undefined {
4
+ return orders.get(range);
5
+ }
6
+ export function withSnippetOrder<Range extends object>(range: Range, order: number | undefined): Range {
7
+ if (order !== undefined) {
8
+ orders.set(range, order);
9
+ }
10
+ return range;
11
+ }
12
+ export function compareSnippetOrder(left: readonly number[], right: readonly number[]): number {
13
+ for (let index = 0; index < Math.min(left.length, right.length); index++) {
14
+ if (left[index] !== right[index]) {
15
+ return left[index]! - right[index]!;
16
+ }
17
+ }
18
+ return left.length - right.length;
19
+ }