@effindomv2/runtime 0.1.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 (92) hide show
  1. package/LICENSE.md +6 -0
  2. package/dist/bridge.js +4 -0
  3. package/dist/bridge.js.map +7 -0
  4. package/dist/effindom.v2.manifest.json +68 -0
  5. package/dist/fonts/NotoColorEmoji.ttf +0 -0
  6. package/dist/fonts/NotoEmoji-Regular.ttf +0 -0
  7. package/dist/fonts/NotoSans-Bold.ttf +0 -0
  8. package/dist/fonts/NotoSans-BoldItalic.ttf +0 -0
  9. package/dist/fonts/NotoSans-Italic.ttf +0 -0
  10. package/dist/fonts/NotoSans-Regular.ttf +0 -0
  11. package/dist/fonts/NotoSansMono-Bold.ttf +0 -0
  12. package/dist/fonts/NotoSansMono-Regular.ttf +0 -0
  13. package/dist/fonts/NotoSansSymbols2-Regular.ttf +0 -0
  14. package/dist/harness.js +2 -0
  15. package/dist/harness.js.map +7 -0
  16. package/dist/index.html +53 -0
  17. package/dist/runtime/effindom-core-v2.wasm32-simd.JQXIaRaN0-JahfIVFiSLE49WzzCENvef_2EDEm09nJs.wasm +0 -0
  18. package/dist/runtime/effindom-core-v2.wasm32-simd.y7RzpkMARiFeRkpgiqKQsAfv4Hf17NYdpni-6aLNhMs.js.symbols +10079 -0
  19. package/dist/runtime/effindom-core-v2.wasm32-simd.yhT7DGUv4soEv4W91WVZl3T7T_ecKojk5_IcnwL79a0.js +1 -0
  20. package/dist/runtime/effindom-core-v2.wasm32.JSfMkp9ertJzSZxA-_xz3yacrJUhswxlwbqbJLRIuqw.wasm +0 -0
  21. package/dist/runtime/effindom-core-v2.wasm32.xNgsQv7dCwf8Uy-PfJSoRNyk9-q1OSogUwkk5g6ZBjk.js.symbols +10088 -0
  22. package/dist/runtime/effindom-core-v2.wasm32.yhT7DGUv4soEv4W91WVZl3T7T_ecKojk5_IcnwL79a0.js +1 -0
  23. package/dist/runtime/effindom-core-v2.wasm64-simd.GkByf-CPorNOs1CORny_8JjVk8Z3piiFq92r-uw1Syc.js +1 -0
  24. package/dist/runtime/effindom-core-v2.wasm64-simd.p4P98oRu2wEWxtRRW8RHr27JhGeWvWlziZXDM_z3Nc4.js.symbols +10286 -0
  25. package/dist/runtime/effindom-core-v2.wasm64-simd.y75FYXRwhQrpaDGYbZWrohGDv0AmjTb-EjXwOjBIgnM.wasm +0 -0
  26. package/dist/runtime/effindom-core-v2.wasm64.GkByf-CPorNOs1CORny_8JjVk8Z3piiFq92r-uw1Syc.js +1 -0
  27. package/dist/runtime/effindom-core-v2.wasm64.emhE1_CJs4_zXp8wiQS_5lYpUQ0OchmXgxksi0ykaBs.js.symbols +10298 -0
  28. package/dist/runtime/effindom-core-v2.wasm64.sO-Yu70cfN8Qs3a5iEp6cbFPaiOchqcMKUzryu4npNo.wasm +0 -0
  29. package/dist/runtime/effindom-ui-v2.wasm32-simd.0Mas1XD03eYvemryTioWaZOBuBA5ij7MFlTa8CgEZWs.wasm +0 -0
  30. package/dist/runtime/effindom-ui-v2.wasm32-simd.ThSDClMnSWdwf9d89JZfYor0G1Z6OxR4lOc75rNRuD4.js.symbols +1890 -0
  31. package/dist/runtime/effindom-ui-v2.wasm32-simd.wved0xEV4EKXVNBU3Sx7giD4faxD2YII9sQ2N_wCP4I.js +2 -0
  32. package/dist/runtime/effindom-ui-v2.wasm32.H7kYg99bT9ADGh0uUvj6H9Dk1L058nVFLv_4R79IXW8.js.symbols +1900 -0
  33. package/dist/runtime/effindom-ui-v2.wasm32.tp53X7nHfG_EUq29naDyElfnqhMw2D1Tr1T-BJAYO7w.wasm +0 -0
  34. package/dist/runtime/effindom-ui-v2.wasm32.wved0xEV4EKXVNBU3Sx7giD4faxD2YII9sQ2N_wCP4I.js +2 -0
  35. package/dist/runtime/effindom-ui-v2.wasm64-simd.86tk9Z3xIpgTOykET_8Nn9iUVJnp1AzOHW4fVQRGtQE.wasm +0 -0
  36. package/dist/runtime/effindom-ui-v2.wasm64-simd.RQaXil22Chu63-vxK9oOuX8wUY044kbo190oYIbBU4M.js.symbols +1918 -0
  37. package/dist/runtime/effindom-ui-v2.wasm64-simd.ZS1KEAg0XQex-VXkfgpBHE8MIoqPF8qpaf8nOjANb_U.js +2 -0
  38. package/dist/runtime/effindom-ui-v2.wasm64.YSwpMFbr-Q1SBe0Ze8mub1u1PqsvSz3QIYuA3eaUMME.js.symbols +1924 -0
  39. package/dist/runtime/effindom-ui-v2.wasm64.ZS1KEAg0XQex-VXkfgpBHE8MIoqPF8qpaf8nOjANb_U.js +2 -0
  40. package/dist/runtime/effindom-ui-v2.wasm64.ioQ9DuM6gR_EjlfRHdF8EvNPBcKCs0PQbbY9-cjTV6Y.wasm +0 -0
  41. package/dist/runtime/icudt_minimal.962CX1q0-Nbv-OqXPaub5piYTOLumUk-nEvemcvvnpw.dat +0 -0
  42. package/package.json +62 -0
  43. package/scripts/build.sh +279 -0
  44. package/scripts/build_assets.sh +51 -0
  45. package/scripts/font_assets.sh +52 -0
  46. package/scripts/generate_manifest.py +121 -0
  47. package/scripts/stage_package_assets.sh +42 -0
  48. package/src/bridge/commit-policy.ts +10 -0
  49. package/src/bridge/events/canvas-geometry.ts +78 -0
  50. package/src/bridge/events/key-router.ts +187 -0
  51. package/src/bridge/events/pointer-router.ts +619 -0
  52. package/src/bridge/events/semantic-hit-testing.ts +27 -0
  53. package/src/bridge/events.ts +54 -0
  54. package/src/bridge/find-dialog.ts +690 -0
  55. package/src/bridge/find-session.ts +158 -0
  56. package/src/bridge/font-catalog.ts +51 -0
  57. package/src/bridge/google-fonts.ts +63 -0
  58. package/src/bridge/incremental-font-packages.ts +216 -0
  59. package/src/bridge/init.ts +77 -0
  60. package/src/bridge/interaction/editor-model.ts +371 -0
  61. package/src/bridge/interaction/editor-mutations.ts +495 -0
  62. package/src/bridge/interaction/editor-session.ts +628 -0
  63. package/src/bridge/interaction/logs.ts +23 -0
  64. package/src/bridge/interaction/text-encoding.ts +51 -0
  65. package/src/bridge/interaction.ts +86 -0
  66. package/src/bridge/local-types.ts +105 -0
  67. package/src/bridge/platform.ts +68 -0
  68. package/src/bridge/pointer-move-coalescer.ts +41 -0
  69. package/src/bridge/pull-to-refresh.ts +124 -0
  70. package/src/bridge/render-loop.ts +268 -0
  71. package/src/bridge/runtime/asset-manager.ts +202 -0
  72. package/src/bridge/runtime/find-controller.ts +269 -0
  73. package/src/bridge/runtime/font-manager.ts +691 -0
  74. package/src/bridge/runtime/open-canvas-api.ts +72 -0
  75. package/src/bridge/runtime/semantic-controller.ts +133 -0
  76. package/src/bridge/runtime/text-documents.ts +234 -0
  77. package/src/bridge/runtime.ts +315 -0
  78. package/src/bridge/touch-gesture.ts +159 -0
  79. package/src/bridge/utils/assets.ts +572 -0
  80. package/src/bridge/utils/backends.ts +163 -0
  81. package/src/bridge/utils/encoding.ts +128 -0
  82. package/src/bridge/utils/fetch.ts +147 -0
  83. package/src/bridge/utils/heap.ts +118 -0
  84. package/src/bridge.ts +93 -0
  85. package/src/clipboard.ts +139 -0
  86. package/src/core-types.ts +595 -0
  87. package/src/find-on-page.ts +284 -0
  88. package/src/harness.ts +53 -0
  89. package/src/index.ts +40 -0
  90. package/src/open-canvas.ts +108 -0
  91. package/src/runtime-config.ts +96 -0
  92. package/src/semantic.ts +905 -0
@@ -0,0 +1,495 @@
1
+ import type { BridgeRuntime } from '../../core-types';
2
+ import { commitIfVisualWork } from '../commit-policy';
3
+ import { handleToBigInt } from '../utils/encoding';
4
+ import { writeUtf8ToHeap } from '../utils/heap';
5
+ import { advanceCodeUnitIndex, codeUnitIndexToUtf8ByteOffset, retreatCodeUnitIndex, utf8ByteLength } from './text-encoding';
6
+ import {
7
+ applyUtf8ByteReplacementEdit,
8
+ buildClampedTextboxEdit,
9
+ computeReplacementEdit,
10
+ mapPendingBatchCurrentIndexToBaseIndex,
11
+ type HiddenEditorWindow,
12
+ type HiddenTextEditor,
13
+ type PendingLocalReplacementEcho,
14
+ type PendingLocalSelectionEcho,
15
+ type PendingPasteInput,
16
+ type PendingTextMutationBatch,
17
+ type ReplacementEdit,
18
+ } from './editor-model';
19
+
20
+ const MAX_BUFFERED_TYPING_MUTATIONS = 5;
21
+
22
+ function currentInteractionTimeMs(): bigint {
23
+ return BigInt(Math.floor(performance.now()));
24
+ }
25
+
26
+ export interface EditorMutationController {
27
+ applyActiveTextDeletion(forward: boolean): boolean;
28
+ attachHiddenEditorListeners(editor: HiddenTextEditor): void;
29
+ clearPendingTextMutations(): void;
30
+ flushPendingTextMutationsToRuntime(): void;
31
+ hasPendingTextMutations(): boolean;
32
+ materializePendingTextMutations(): boolean;
33
+ reset(): void;
34
+ }
35
+
36
+ interface EditorMutationControllerOptions {
37
+ readonly runtimeRef: { current: BridgeRuntime | null };
38
+ readonly textByHandle: Record<string, string>;
39
+ readonly selectionsByHandle: Record<string, { start: number; end: number }>;
40
+ readonly textByteLengthsByHandle: Record<string, number>;
41
+ getActiveEditor(): HiddenTextEditor;
42
+ getActiveEditorWindow(): HiddenEditorWindow;
43
+ getActiveTextEditable(): boolean;
44
+ getActiveTextHandle(): bigint | null;
45
+ isActiveEditorFocused(): boolean;
46
+ setPendingLocalReplacementEcho(value: PendingLocalReplacementEcho | null): void;
47
+ setPendingLocalSelectionEcho(value: PendingLocalSelectionEcho | null): void;
48
+ syncFocusedInputState(): void;
49
+ updateActiveEditorWindowText(text: string): void;
50
+ }
51
+
52
+ export function createEditorMutationController(
53
+ options: EditorMutationControllerOptions,
54
+ ): EditorMutationController {
55
+ let hiddenInputIsComposing = false;
56
+ let pendingTextMutationFlushFrame: number | null = null;
57
+ let pendingTextMutationBatch: PendingTextMutationBatch | null = null;
58
+ let pendingPasteInput: PendingPasteInput | null = null;
59
+ let pendingPasteText = '';
60
+
61
+ const clearPendingTextMutations = (): void => {
62
+ if (pendingTextMutationFlushFrame !== null) {
63
+ cancelAnimationFrame(pendingTextMutationFlushFrame);
64
+ pendingTextMutationFlushFrame = null;
65
+ }
66
+ pendingTextMutationBatch = null;
67
+ pendingPasteInput = null;
68
+ pendingPasteText = '';
69
+ };
70
+
71
+ const materializePendingTextMutations = (): boolean => {
72
+ const runtime = options.runtimeRef.current;
73
+ const batch = pendingTextMutationBatch;
74
+ if (runtime === null || batch === null) {
75
+ return false;
76
+ }
77
+ pendingTextMutationBatch = null;
78
+ const handle = handleToBigInt(batch.handle);
79
+ const absoluteCaret = batch.docStart + codeUnitIndexToUtf8ByteOffset(batch.currentWindowText, batch.caret);
80
+ runtime.ui._ui_set_interaction_time(batch.interactionTime);
81
+ const replacement = computeReplacementEdit(batch.baseWindowText, batch.currentWindowText);
82
+ if (replacement === null) {
83
+ options.setPendingLocalSelectionEcho({
84
+ handle: batch.handle,
85
+ start: absoluteCaret,
86
+ end: absoluteCaret,
87
+ });
88
+ runtime.ui._ui_set_text_selection_range(handle, absoluteCaret, absoluteCaret);
89
+ return true;
90
+ }
91
+
92
+ const absoluteStart = batch.docStart + codeUnitIndexToUtf8ByteOffset(batch.baseWindowText, replacement.start);
93
+ const absoluteEnd = batch.docStart + codeUnitIndexToUtf8ByteOffset(batch.baseWindowText, replacement.end);
94
+ options.setPendingLocalReplacementEcho({
95
+ handle: batch.handle,
96
+ start: absoluteStart,
97
+ end: absoluteEnd,
98
+ text: replacement.insertedText,
99
+ });
100
+ options.setPendingLocalSelectionEcho({
101
+ handle: batch.handle,
102
+ start: absoluteCaret,
103
+ end: absoluteCaret,
104
+ });
105
+ const heapString = writeUtf8ToHeap(runtime.ui, replacement.insertedText);
106
+ try {
107
+ runtime.ui._ui_replace_text_range(
108
+ handle,
109
+ absoluteStart,
110
+ absoluteEnd,
111
+ heapString.ptr,
112
+ heapString.len,
113
+ absoluteCaret,
114
+ );
115
+ } finally {
116
+ heapString.dispose();
117
+ }
118
+ return true;
119
+ };
120
+
121
+ const restorePendingTypingState = (editor: HiddenTextEditor): boolean => {
122
+ const batch = pendingTextMutationBatch;
123
+ const activeTextHandle = options.getActiveTextHandle();
124
+ const activeEditorWindow = options.getActiveEditorWindow();
125
+ if (
126
+ batch === null ||
127
+ batch.kind !== 'typing' ||
128
+ batch.mutationCount < MAX_BUFFERED_TYPING_MUTATIONS ||
129
+ activeTextHandle === null ||
130
+ batch.handle !== activeTextHandle.toString() ||
131
+ batch.docStart !== activeEditorWindow.docStart
132
+ ) {
133
+ return false;
134
+ }
135
+ editor.value = batch.currentWindowText;
136
+ editor.setSelectionRange(batch.caret, batch.caret, 'none');
137
+ return true;
138
+ };
139
+
140
+ const flushPendingTextMutationsToRuntime = (): void => {
141
+ const runtime = options.runtimeRef.current;
142
+ if (runtime === null || pendingTextMutationBatch === null) {
143
+ return;
144
+ }
145
+ if (materializePendingTextMutations()) {
146
+ runtime.commitFrame();
147
+ }
148
+ };
149
+
150
+ const schedulePendingTextMutationFlush = (): void => {
151
+ if (pendingTextMutationFlushFrame !== null) {
152
+ return;
153
+ }
154
+ pendingTextMutationFlushFrame = requestAnimationFrame(() => {
155
+ pendingTextMutationFlushFrame = null;
156
+ options.runtimeRef.current?.flushPendingCommit();
157
+ });
158
+ };
159
+
160
+ const commitReplacementEdit = (
161
+ previousText: string,
162
+ nextText: string,
163
+ replacement: ReplacementEdit,
164
+ caret: number,
165
+ kind: 'typing' | 'paste' = 'typing',
166
+ replacePendingBatch = false,
167
+ ): void => {
168
+ const runtime = options.runtimeRef.current;
169
+ const activeTextHandle = options.getActiveTextHandle();
170
+ const activeEditorWindow = options.getActiveEditorWindow();
171
+ if (runtime === null || activeTextHandle === null) {
172
+ return;
173
+ }
174
+ const clampedStart = Math.max(0, Math.min(replacement.start, previousText.length));
175
+ const clampedEnd = Math.max(clampedStart, Math.min(replacement.end, previousText.length));
176
+ const activeHandleKey = activeTextHandle.toString();
177
+ if (pendingTextMutationBatch === null && runtime.hasPendingCommit()) {
178
+ runtime.flushPendingCommit();
179
+ }
180
+
181
+ const absoluteStart = activeEditorWindow.docStart + codeUnitIndexToUtf8ByteOffset(previousText, clampedStart);
182
+ const absoluteEnd = activeEditorWindow.docStart + codeUnitIndexToUtf8ByteOffset(previousText, clampedEnd);
183
+ const intendedAbsoluteCaret =
184
+ activeEditorWindow.docStart + codeUnitIndexToUtf8ByteOffset(nextText, Math.max(0, Math.min(caret, nextText.length)));
185
+ let fullPreviousText = options.textByHandle[activeHandleKey] ?? '';
186
+ if (
187
+ replacePendingBatch &&
188
+ pendingTextMutationBatch !== null &&
189
+ pendingTextMutationBatch.handle === activeHandleKey &&
190
+ pendingTextMutationBatch.docStart === activeEditorWindow.docStart
191
+ ) {
192
+ const pendingReplacement = computeReplacementEdit(
193
+ pendingTextMutationBatch.baseWindowText,
194
+ pendingTextMutationBatch.currentWindowText,
195
+ );
196
+ if (pendingReplacement !== null) {
197
+ const pendingAbsoluteStart =
198
+ pendingTextMutationBatch.docStart +
199
+ codeUnitIndexToUtf8ByteOffset(pendingTextMutationBatch.baseWindowText, pendingReplacement.start);
200
+ const pendingAbsoluteEnd = pendingAbsoluteStart + utf8ByteLength(pendingReplacement.insertedText);
201
+ fullPreviousText = applyUtf8ByteReplacementEdit(
202
+ fullPreviousText,
203
+ pendingAbsoluteStart,
204
+ pendingAbsoluteEnd,
205
+ pendingTextMutationBatch.baseWindowText.slice(pendingReplacement.start, pendingReplacement.end),
206
+ );
207
+ }
208
+ }
209
+ const clampedEdit = buildClampedTextboxEdit(
210
+ fullPreviousText,
211
+ absoluteStart,
212
+ absoluteEnd,
213
+ replacement.insertedText,
214
+ intendedAbsoluteCaret,
215
+ );
216
+ if (clampedEdit.clampChanged) {
217
+ if (pendingTextMutationBatch !== null) {
218
+ if (materializePendingTextMutations()) {
219
+ runtime.commitFrame();
220
+ }
221
+ }
222
+ if (runtime.hasPendingCommit()) {
223
+ runtime.flushPendingCommit();
224
+ }
225
+ clearPendingTextMutations();
226
+ options.textByHandle[activeHandleKey] = clampedEdit.fullNextText;
227
+ options.textByteLengthsByHandle[activeHandleKey] = utf8ByteLength(clampedEdit.fullNextText);
228
+ options.selectionsByHandle[activeHandleKey] = { start: clampedEdit.caretByte, end: clampedEdit.caretByte };
229
+ options.syncFocusedInputState();
230
+ } else {
231
+ options.textByHandle[activeHandleKey] = clampedEdit.fullNextText;
232
+ options.textByteLengthsByHandle[activeHandleKey] = utf8ByteLength(clampedEdit.fullNextText);
233
+ options.selectionsByHandle[activeHandleKey] = { start: clampedEdit.caretByte, end: clampedEdit.caretByte };
234
+ options.updateActiveEditorWindowText(nextText);
235
+ }
236
+
237
+ if (clampedEdit.replacement === null) {
238
+ return;
239
+ }
240
+ if (clampedEdit.clampChanged) {
241
+ const replacementStartByte = codeUnitIndexToUtf8ByteOffset(fullPreviousText, clampedEdit.replacement.start);
242
+ const replacementEndByte = codeUnitIndexToUtf8ByteOffset(fullPreviousText, clampedEdit.replacement.end);
243
+ options.setPendingLocalReplacementEcho({
244
+ handle: activeHandleKey,
245
+ start: replacementStartByte,
246
+ end: replacementEndByte,
247
+ text: clampedEdit.replacement.insertedText,
248
+ });
249
+ options.setPendingLocalSelectionEcho({
250
+ handle: activeHandleKey,
251
+ start: clampedEdit.caretByte,
252
+ end: clampedEdit.caretByte,
253
+ });
254
+ runtime.ui._ui_set_interaction_time(currentInteractionTimeMs());
255
+ const heapString = writeUtf8ToHeap(runtime.ui, clampedEdit.replacement.insertedText);
256
+ try {
257
+ runtime.ui._ui_replace_text_range(
258
+ activeTextHandle,
259
+ replacementStartByte,
260
+ replacementEndByte,
261
+ heapString.ptr,
262
+ heapString.len,
263
+ clampedEdit.caretByte,
264
+ );
265
+ } finally {
266
+ heapString.dispose();
267
+ }
268
+ runtime.commitFrame();
269
+ return;
270
+ }
271
+
272
+ if (
273
+ pendingTextMutationBatch !== null &&
274
+ (
275
+ pendingTextMutationBatch.handle !== activeHandleKey ||
276
+ pendingTextMutationBatch.docStart !== activeEditorWindow.docStart ||
277
+ (!replacePendingBatch && pendingTextMutationBatch.kind !== kind)
278
+ )
279
+ ) {
280
+ if (materializePendingTextMutations()) {
281
+ runtime.commitFrame();
282
+ }
283
+ runtime.flushPendingCommit();
284
+ }
285
+
286
+ const activeEditor = options.getActiveEditor();
287
+ pendingTextMutationBatch = pendingTextMutationBatch === null || replacePendingBatch
288
+ ? {
289
+ handle: activeHandleKey,
290
+ docStart: activeEditorWindow.docStart,
291
+ baseWindowText: previousText,
292
+ currentWindowText: activeEditor.value,
293
+ caret: activeEditor.selectionStart ?? activeEditor.value.length,
294
+ interactionTime: currentInteractionTimeMs(),
295
+ kind,
296
+ mutationCount: 1,
297
+ }
298
+ : {
299
+ ...pendingTextMutationBatch,
300
+ currentWindowText: activeEditor.value,
301
+ caret: activeEditor.selectionStart ?? activeEditor.value.length,
302
+ interactionTime: currentInteractionTimeMs(),
303
+ mutationCount: kind === 'paste' ? 1 : pendingTextMutationBatch.mutationCount + 1,
304
+ };
305
+ schedulePendingTextMutationFlush();
306
+ runtime.requestFrame();
307
+ };
308
+
309
+ const commitImeEdit = (
310
+ text: string,
311
+ caret: number,
312
+ kind: 'typing' | 'paste' = 'typing',
313
+ replacePendingBatch = false,
314
+ ): void => {
315
+ const previousText = options.getActiveEditorWindow().text;
316
+ const replacement = computeReplacementEdit(previousText, text);
317
+ if (replacement === null) {
318
+ const runtime = options.runtimeRef.current;
319
+ if (runtime !== null) {
320
+ commitIfVisualWork(runtime);
321
+ }
322
+ return;
323
+ }
324
+ commitReplacementEdit(previousText, text, replacement, caret, kind, replacePendingBatch);
325
+ };
326
+
327
+ const applyActiveTextDeletion = (forward: boolean): boolean => {
328
+ if (
329
+ options.getActiveTextHandle() === null ||
330
+ !options.getActiveTextEditable() ||
331
+ hiddenInputIsComposing ||
332
+ !options.isActiveEditorFocused()
333
+ ) {
334
+ return false;
335
+ }
336
+ const editor = options.getActiveEditor();
337
+ const text = editor.value;
338
+ const selectionStart = editor.selectionStart ?? text.length;
339
+ const selectionEnd = editor.selectionEnd ?? selectionStart;
340
+ const rangeStart = Math.min(selectionStart, selectionEnd);
341
+ const rangeEnd = Math.max(selectionStart, selectionEnd);
342
+ let nextText = text;
343
+ let nextCaret = rangeStart;
344
+ let replacementStart = rangeStart;
345
+ let replacementEnd = rangeEnd;
346
+ if (rangeStart !== rangeEnd) {
347
+ nextText = text.slice(0, rangeStart) + text.slice(rangeEnd);
348
+ } else if (forward) {
349
+ if (rangeStart >= text.length) {
350
+ return true;
351
+ }
352
+ replacementEnd = advanceCodeUnitIndex(text, rangeStart);
353
+ nextText = text.slice(0, rangeStart) + text.slice(replacementEnd);
354
+ } else {
355
+ if (rangeStart === 0) {
356
+ return true;
357
+ }
358
+ replacementStart = retreatCodeUnitIndex(text, rangeStart);
359
+ nextCaret = replacementStart;
360
+ nextText = text.slice(0, nextCaret) + text.slice(rangeStart);
361
+ }
362
+ editor.value = nextText;
363
+ editor.setSelectionRange(nextCaret, nextCaret, 'none');
364
+ commitReplacementEdit(
365
+ text,
366
+ nextText,
367
+ {
368
+ start: replacementStart,
369
+ end: replacementEnd,
370
+ insertedText: '',
371
+ },
372
+ nextCaret,
373
+ );
374
+ return true;
375
+ };
376
+
377
+ const attachHiddenEditorListeners = (editor: HiddenTextEditor): void => {
378
+ editor.addEventListener('paste', (event) => {
379
+ const clipboardEvent = event as ClipboardEvent;
380
+ pendingPasteText = clipboardEvent.clipboardData?.getData('text/plain') ?? '';
381
+ });
382
+
383
+ editor.addEventListener('beforeinput', (event) => {
384
+ const activeTextHandle = options.getActiveTextHandle();
385
+ const activeEditorWindow = options.getActiveEditorWindow();
386
+ if (!(event instanceof InputEvent) || event.inputType !== 'insertFromPaste' || activeTextHandle === null) {
387
+ return;
388
+ }
389
+ const handleKey = activeTextHandle.toString();
390
+ if (
391
+ pendingTextMutationBatch !== null &&
392
+ pendingTextMutationBatch.handle === handleKey &&
393
+ pendingTextMutationBatch.docStart === activeEditorWindow.docStart &&
394
+ pendingTextMutationBatch.kind !== 'paste'
395
+ ) {
396
+ flushPendingTextMutationsToRuntime();
397
+ }
398
+ const selectionStart = editor.selectionStart ?? editor.value.length;
399
+ const selectionEnd = editor.selectionEnd ?? selectionStart;
400
+ pendingPasteInput = {
401
+ handle: handleKey,
402
+ docStart: activeEditorWindow.docStart,
403
+ selectionStart,
404
+ selectionEnd,
405
+ text: typeof event.data === 'string' ? event.data : pendingPasteText,
406
+ };
407
+ pendingPasteText = '';
408
+ });
409
+
410
+ editor.addEventListener('compositionstart', () => {
411
+ hiddenInputIsComposing = true;
412
+ });
413
+
414
+ editor.addEventListener('input', (event) => {
415
+ const activeTextHandle = options.getActiveTextHandle();
416
+ const activeEditorWindow = options.getActiveEditorWindow();
417
+ if (hiddenInputIsComposing) {
418
+ return;
419
+ }
420
+ if (event instanceof InputEvent && event.inputType === 'insertText' && restorePendingTypingState(editor)) {
421
+ return;
422
+ }
423
+ if (event instanceof InputEvent && event.inputType === 'insertFromPaste') {
424
+ const pasteInput = pendingPasteInput;
425
+ pendingPasteInput = null;
426
+ pendingPasteText = '';
427
+ if (
428
+ pasteInput !== null &&
429
+ activeTextHandle !== null &&
430
+ activeTextHandle.toString() === pasteInput.handle &&
431
+ activeEditorWindow.docStart === pasteInput.docStart
432
+ ) {
433
+ const pendingBatch = pendingTextMutationBatch !== null &&
434
+ pendingTextMutationBatch.handle === pasteInput.handle &&
435
+ pendingTextMutationBatch.docStart === pasteInput.docStart &&
436
+ pendingTextMutationBatch.kind === 'paste'
437
+ ? pendingTextMutationBatch
438
+ : null;
439
+ const baseWindowText = pendingBatch?.baseWindowText ?? activeEditorWindow.text;
440
+ let selectionStart = pasteInput.selectionStart;
441
+ let selectionEnd = pasteInput.selectionEnd;
442
+ if (pendingBatch !== null) {
443
+ selectionStart = mapPendingBatchCurrentIndexToBaseIndex(pendingBatch, selectionStart);
444
+ selectionEnd = mapPendingBatchCurrentIndexToBaseIndex(pendingBatch, selectionEnd);
445
+ }
446
+ const rangeStart = Math.min(selectionStart, selectionEnd);
447
+ const rangeEnd = Math.max(selectionStart, selectionEnd);
448
+ const nextText =
449
+ `${baseWindowText.slice(0, rangeStart)}${pasteInput.text}${baseWindowText.slice(rangeEnd)}`;
450
+ const nextCaret = rangeStart + pasteInput.text.length;
451
+ if (editor.value !== nextText) {
452
+ editor.value = nextText;
453
+ }
454
+ editor.setSelectionRange(nextCaret, nextCaret, 'none');
455
+ commitReplacementEdit(
456
+ baseWindowText,
457
+ nextText,
458
+ {
459
+ start: rangeStart,
460
+ end: rangeEnd,
461
+ insertedText: pasteInput.text,
462
+ },
463
+ nextCaret,
464
+ 'paste',
465
+ pendingBatch !== null,
466
+ );
467
+ return;
468
+ }
469
+ }
470
+ pendingPasteInput = null;
471
+ pendingPasteText = '';
472
+ commitImeEdit(editor.value, editor.selectionStart ?? editor.value.length, 'typing');
473
+ });
474
+
475
+ editor.addEventListener('compositionend', () => {
476
+ hiddenInputIsComposing = false;
477
+ pendingPasteInput = null;
478
+ pendingPasteText = '';
479
+ commitImeEdit(editor.value, editor.selectionStart ?? editor.value.length, 'typing');
480
+ });
481
+ };
482
+
483
+ return {
484
+ applyActiveTextDeletion,
485
+ attachHiddenEditorListeners,
486
+ clearPendingTextMutations,
487
+ flushPendingTextMutationsToRuntime,
488
+ hasPendingTextMutations: () => pendingTextMutationBatch !== null,
489
+ materializePendingTextMutations,
490
+ reset: () => {
491
+ hiddenInputIsComposing = false;
492
+ clearPendingTextMutations();
493
+ },
494
+ };
495
+ }