@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.
- package/LICENSE.md +6 -0
- package/dist/bridge.js +4 -0
- package/dist/bridge.js.map +7 -0
- package/dist/effindom.v2.manifest.json +68 -0
- package/dist/fonts/NotoColorEmoji.ttf +0 -0
- package/dist/fonts/NotoEmoji-Regular.ttf +0 -0
- package/dist/fonts/NotoSans-Bold.ttf +0 -0
- package/dist/fonts/NotoSans-BoldItalic.ttf +0 -0
- package/dist/fonts/NotoSans-Italic.ttf +0 -0
- package/dist/fonts/NotoSans-Regular.ttf +0 -0
- package/dist/fonts/NotoSansMono-Bold.ttf +0 -0
- package/dist/fonts/NotoSansMono-Regular.ttf +0 -0
- package/dist/fonts/NotoSansSymbols2-Regular.ttf +0 -0
- package/dist/harness.js +2 -0
- package/dist/harness.js.map +7 -0
- package/dist/index.html +53 -0
- package/dist/runtime/effindom-core-v2.wasm32-simd.JQXIaRaN0-JahfIVFiSLE49WzzCENvef_2EDEm09nJs.wasm +0 -0
- package/dist/runtime/effindom-core-v2.wasm32-simd.y7RzpkMARiFeRkpgiqKQsAfv4Hf17NYdpni-6aLNhMs.js.symbols +10079 -0
- package/dist/runtime/effindom-core-v2.wasm32-simd.yhT7DGUv4soEv4W91WVZl3T7T_ecKojk5_IcnwL79a0.js +1 -0
- package/dist/runtime/effindom-core-v2.wasm32.JSfMkp9ertJzSZxA-_xz3yacrJUhswxlwbqbJLRIuqw.wasm +0 -0
- package/dist/runtime/effindom-core-v2.wasm32.xNgsQv7dCwf8Uy-PfJSoRNyk9-q1OSogUwkk5g6ZBjk.js.symbols +10088 -0
- package/dist/runtime/effindom-core-v2.wasm32.yhT7DGUv4soEv4W91WVZl3T7T_ecKojk5_IcnwL79a0.js +1 -0
- package/dist/runtime/effindom-core-v2.wasm64-simd.GkByf-CPorNOs1CORny_8JjVk8Z3piiFq92r-uw1Syc.js +1 -0
- package/dist/runtime/effindom-core-v2.wasm64-simd.p4P98oRu2wEWxtRRW8RHr27JhGeWvWlziZXDM_z3Nc4.js.symbols +10286 -0
- package/dist/runtime/effindom-core-v2.wasm64-simd.y75FYXRwhQrpaDGYbZWrohGDv0AmjTb-EjXwOjBIgnM.wasm +0 -0
- package/dist/runtime/effindom-core-v2.wasm64.GkByf-CPorNOs1CORny_8JjVk8Z3piiFq92r-uw1Syc.js +1 -0
- package/dist/runtime/effindom-core-v2.wasm64.emhE1_CJs4_zXp8wiQS_5lYpUQ0OchmXgxksi0ykaBs.js.symbols +10298 -0
- package/dist/runtime/effindom-core-v2.wasm64.sO-Yu70cfN8Qs3a5iEp6cbFPaiOchqcMKUzryu4npNo.wasm +0 -0
- package/dist/runtime/effindom-ui-v2.wasm32-simd.0Mas1XD03eYvemryTioWaZOBuBA5ij7MFlTa8CgEZWs.wasm +0 -0
- package/dist/runtime/effindom-ui-v2.wasm32-simd.ThSDClMnSWdwf9d89JZfYor0G1Z6OxR4lOc75rNRuD4.js.symbols +1890 -0
- package/dist/runtime/effindom-ui-v2.wasm32-simd.wved0xEV4EKXVNBU3Sx7giD4faxD2YII9sQ2N_wCP4I.js +2 -0
- package/dist/runtime/effindom-ui-v2.wasm32.H7kYg99bT9ADGh0uUvj6H9Dk1L058nVFLv_4R79IXW8.js.symbols +1900 -0
- package/dist/runtime/effindom-ui-v2.wasm32.tp53X7nHfG_EUq29naDyElfnqhMw2D1Tr1T-BJAYO7w.wasm +0 -0
- package/dist/runtime/effindom-ui-v2.wasm32.wved0xEV4EKXVNBU3Sx7giD4faxD2YII9sQ2N_wCP4I.js +2 -0
- package/dist/runtime/effindom-ui-v2.wasm64-simd.86tk9Z3xIpgTOykET_8Nn9iUVJnp1AzOHW4fVQRGtQE.wasm +0 -0
- package/dist/runtime/effindom-ui-v2.wasm64-simd.RQaXil22Chu63-vxK9oOuX8wUY044kbo190oYIbBU4M.js.symbols +1918 -0
- package/dist/runtime/effindom-ui-v2.wasm64-simd.ZS1KEAg0XQex-VXkfgpBHE8MIoqPF8qpaf8nOjANb_U.js +2 -0
- package/dist/runtime/effindom-ui-v2.wasm64.YSwpMFbr-Q1SBe0Ze8mub1u1PqsvSz3QIYuA3eaUMME.js.symbols +1924 -0
- package/dist/runtime/effindom-ui-v2.wasm64.ZS1KEAg0XQex-VXkfgpBHE8MIoqPF8qpaf8nOjANb_U.js +2 -0
- package/dist/runtime/effindom-ui-v2.wasm64.ioQ9DuM6gR_EjlfRHdF8EvNPBcKCs0PQbbY9-cjTV6Y.wasm +0 -0
- package/dist/runtime/icudt_minimal.962CX1q0-Nbv-OqXPaub5piYTOLumUk-nEvemcvvnpw.dat +0 -0
- package/package.json +62 -0
- package/scripts/build.sh +279 -0
- package/scripts/build_assets.sh +51 -0
- package/scripts/font_assets.sh +52 -0
- package/scripts/generate_manifest.py +121 -0
- package/scripts/stage_package_assets.sh +42 -0
- package/src/bridge/commit-policy.ts +10 -0
- package/src/bridge/events/canvas-geometry.ts +78 -0
- package/src/bridge/events/key-router.ts +187 -0
- package/src/bridge/events/pointer-router.ts +619 -0
- package/src/bridge/events/semantic-hit-testing.ts +27 -0
- package/src/bridge/events.ts +54 -0
- package/src/bridge/find-dialog.ts +690 -0
- package/src/bridge/find-session.ts +158 -0
- package/src/bridge/font-catalog.ts +51 -0
- package/src/bridge/google-fonts.ts +63 -0
- package/src/bridge/incremental-font-packages.ts +216 -0
- package/src/bridge/init.ts +77 -0
- package/src/bridge/interaction/editor-model.ts +371 -0
- package/src/bridge/interaction/editor-mutations.ts +495 -0
- package/src/bridge/interaction/editor-session.ts +628 -0
- package/src/bridge/interaction/logs.ts +23 -0
- package/src/bridge/interaction/text-encoding.ts +51 -0
- package/src/bridge/interaction.ts +86 -0
- package/src/bridge/local-types.ts +105 -0
- package/src/bridge/platform.ts +68 -0
- package/src/bridge/pointer-move-coalescer.ts +41 -0
- package/src/bridge/pull-to-refresh.ts +124 -0
- package/src/bridge/render-loop.ts +268 -0
- package/src/bridge/runtime/asset-manager.ts +202 -0
- package/src/bridge/runtime/find-controller.ts +269 -0
- package/src/bridge/runtime/font-manager.ts +691 -0
- package/src/bridge/runtime/open-canvas-api.ts +72 -0
- package/src/bridge/runtime/semantic-controller.ts +133 -0
- package/src/bridge/runtime/text-documents.ts +234 -0
- package/src/bridge/runtime.ts +315 -0
- package/src/bridge/touch-gesture.ts +159 -0
- package/src/bridge/utils/assets.ts +572 -0
- package/src/bridge/utils/backends.ts +163 -0
- package/src/bridge/utils/encoding.ts +128 -0
- package/src/bridge/utils/fetch.ts +147 -0
- package/src/bridge/utils/heap.ts +118 -0
- package/src/bridge.ts +93 -0
- package/src/clipboard.ts +139 -0
- package/src/core-types.ts +595 -0
- package/src/find-on-page.ts +284 -0
- package/src/harness.ts +53 -0
- package/src/index.ts +40 -0
- package/src/open-canvas.ts +108 -0
- package/src/runtime-config.ts +96 -0
- package/src/semantic.ts +905 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type { AssetLoadResult, CoreModule } from '../../core-types';
|
|
2
|
+
import { writeBytesToHeap } from '../utils/heap';
|
|
3
|
+
import { IncrementalFontManager } from './font-manager';
|
|
4
|
+
|
|
5
|
+
interface BitmapLike {
|
|
6
|
+
readonly width: number;
|
|
7
|
+
readonly height: number;
|
|
8
|
+
close?(): void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const svgTextDecoder = new TextDecoder();
|
|
12
|
+
|
|
13
|
+
function createDecodeCanvas(width: number, height: number): HTMLCanvasElement {
|
|
14
|
+
const canvas = document.createElement('canvas');
|
|
15
|
+
canvas.width = width;
|
|
16
|
+
canvas.height = height;
|
|
17
|
+
return canvas;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function decodeBlobToBitmap(blob: Blob): Promise<ImageBitmap> {
|
|
21
|
+
if (createImageBitmap === undefined) {
|
|
22
|
+
throw new Error('createImageBitmap is unavailable for texture decoding.');
|
|
23
|
+
}
|
|
24
|
+
return await createImageBitmap(blob);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function extractBitmapRgba(bitmap: CanvasImageSource & BitmapLike): Uint8Array {
|
|
28
|
+
const canvas = createDecodeCanvas(bitmap.width, bitmap.height);
|
|
29
|
+
const context = canvas.getContext('2d', { willReadFrequently: true });
|
|
30
|
+
if (context === null) {
|
|
31
|
+
throw new Error('Failed to allocate a 2D canvas for texture decoding.');
|
|
32
|
+
}
|
|
33
|
+
context.clearRect(0, 0, bitmap.width, bitmap.height);
|
|
34
|
+
context.drawImage(bitmap, 0, 0);
|
|
35
|
+
const imageData = context.getImageData(0, 0, bitmap.width, bitmap.height);
|
|
36
|
+
return new Uint8Array(
|
|
37
|
+
imageData.data.buffer.slice(
|
|
38
|
+
imageData.data.byteOffset,
|
|
39
|
+
imageData.data.byteOffset + imageData.data.byteLength,
|
|
40
|
+
),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function parseSvgLength(value: string | null): number | null {
|
|
45
|
+
if (value === null) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const trimmed = value.trim();
|
|
49
|
+
if (trimmed.length === 0) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const match = /^([+-]?(?:\d+\.?\d*|\.\d+))/.exec(trimmed);
|
|
53
|
+
const numericPrefix = match?.[1];
|
|
54
|
+
if (numericPrefix === undefined) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const parsed = Number.parseFloat(numericPrefix);
|
|
58
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseSvgViewBox(value: string | null): AssetLoadResult | null {
|
|
62
|
+
if (value === null) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const parts = value
|
|
66
|
+
.trim()
|
|
67
|
+
.split(/[\s,]+/)
|
|
68
|
+
.map((entry) => Number.parseFloat(entry))
|
|
69
|
+
.filter((entry) => Number.isFinite(entry));
|
|
70
|
+
if (parts.length !== 4) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const width = parts[2];
|
|
74
|
+
const height = parts[3];
|
|
75
|
+
if (width === undefined || height === undefined) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
if (width <= 0 || height <= 0) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
return { width, height };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parseSvgIntrinsicSize(bytes: Uint8Array): AssetLoadResult {
|
|
85
|
+
const markup = svgTextDecoder.decode(bytes);
|
|
86
|
+
const documentRoot = new DOMParser().parseFromString(markup, 'image/svg+xml').documentElement;
|
|
87
|
+
const width = parseSvgLength(documentRoot.getAttribute('width'));
|
|
88
|
+
const height = parseSvgLength(documentRoot.getAttribute('height'));
|
|
89
|
+
if (width !== null && height !== null) {
|
|
90
|
+
return { width, height };
|
|
91
|
+
}
|
|
92
|
+
const viewBox = parseSvgViewBox(documentRoot.getAttribute('viewBox'));
|
|
93
|
+
if (viewBox !== null) {
|
|
94
|
+
return viewBox;
|
|
95
|
+
}
|
|
96
|
+
return { width: 1, height: 1 };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export class AssetManager {
|
|
100
|
+
private readonly loadedSvgs = new Map<number, string>();
|
|
101
|
+
private readonly loadedTextures = new Map<number, string>();
|
|
102
|
+
|
|
103
|
+
public constructor(
|
|
104
|
+
private readonly core: CoreModule,
|
|
105
|
+
private readonly fontManager: IncrementalFontManager,
|
|
106
|
+
private readonly onCommitFrame: () => void,
|
|
107
|
+
) {}
|
|
108
|
+
|
|
109
|
+
public async loadSvg(svgId: number, url: string): Promise<AssetLoadResult> {
|
|
110
|
+
this.loadedSvgs.set(svgId, url);
|
|
111
|
+
const response = await fetch(url);
|
|
112
|
+
if (!response.ok) {
|
|
113
|
+
throw new Error(`Failed to fetch SVG ${url}: ${String(response.status)}`);
|
|
114
|
+
}
|
|
115
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
116
|
+
const size = parseSvgIntrinsicSize(bytes);
|
|
117
|
+
const svgBytes = writeBytesToHeap(this.core, bytes);
|
|
118
|
+
try {
|
|
119
|
+
this.core._ed_register_svg(svgId, svgBytes.ptr, svgBytes.len);
|
|
120
|
+
} finally {
|
|
121
|
+
svgBytes.dispose();
|
|
122
|
+
}
|
|
123
|
+
this.onCommitFrame();
|
|
124
|
+
return size;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
public async loadTexture(textureId: number, url: string): Promise<AssetLoadResult> {
|
|
128
|
+
this.loadedTextures.set(textureId, url);
|
|
129
|
+
const response = await fetch(url);
|
|
130
|
+
if (!response.ok) {
|
|
131
|
+
throw new Error(`Failed to fetch texture ${url}: ${String(response.status)}`);
|
|
132
|
+
}
|
|
133
|
+
const blob = await response.blob();
|
|
134
|
+
const bitmap = await decodeBlobToBitmap(blob);
|
|
135
|
+
const width = bitmap.width;
|
|
136
|
+
const height = bitmap.height;
|
|
137
|
+
try {
|
|
138
|
+
const rgba = extractBitmapRgba(bitmap);
|
|
139
|
+
const textureBytes = writeBytesToHeap(this.core, rgba);
|
|
140
|
+
try {
|
|
141
|
+
this.core._ed_register_texture_rgba(textureId, textureBytes.ptr, width, height, textureBytes.len);
|
|
142
|
+
} finally {
|
|
143
|
+
textureBytes.dispose();
|
|
144
|
+
}
|
|
145
|
+
} finally {
|
|
146
|
+
bitmap.close?.();
|
|
147
|
+
}
|
|
148
|
+
this.onCommitFrame();
|
|
149
|
+
return {
|
|
150
|
+
width,
|
|
151
|
+
height,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
public releaseSvg(svgId: number): void {
|
|
156
|
+
this.loadedSvgs.delete(svgId);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
public releaseTexture(textureId: number): void {
|
|
160
|
+
this.loadedTextures.delete(textureId);
|
|
161
|
+
this.core._ed_unregister_texture(textureId);
|
|
162
|
+
this.onCommitFrame();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
public async replayLoadedAssets(): Promise<void> {
|
|
166
|
+
await Promise.all([
|
|
167
|
+
this.fontManager.replayLoadedFonts(),
|
|
168
|
+
...Array.from(this.loadedSvgs.entries(), async ([svgId, url]) => {
|
|
169
|
+
const response = await fetch(url);
|
|
170
|
+
if (!response.ok) {
|
|
171
|
+
throw new Error(`Failed to refetch SVG ${url}: ${String(response.status)}`);
|
|
172
|
+
}
|
|
173
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
174
|
+
const svgBytes = writeBytesToHeap(this.core, bytes);
|
|
175
|
+
try {
|
|
176
|
+
this.core._ed_register_svg(svgId, svgBytes.ptr, svgBytes.len);
|
|
177
|
+
} finally {
|
|
178
|
+
svgBytes.dispose();
|
|
179
|
+
}
|
|
180
|
+
}),
|
|
181
|
+
...Array.from(this.loadedTextures.entries(), async ([textureId, url]) => {
|
|
182
|
+
const response = await fetch(url);
|
|
183
|
+
if (!response.ok) {
|
|
184
|
+
throw new Error(`Failed to refetch texture ${url}: ${String(response.status)}`);
|
|
185
|
+
}
|
|
186
|
+
const blob = await response.blob();
|
|
187
|
+
const bitmap = await decodeBlobToBitmap(blob);
|
|
188
|
+
try {
|
|
189
|
+
const rgba = extractBitmapRgba(bitmap);
|
|
190
|
+
const textureBytes = writeBytesToHeap(this.core, rgba);
|
|
191
|
+
try {
|
|
192
|
+
this.core._ed_register_texture_rgba(textureId, textureBytes.ptr, bitmap.width, bitmap.height, textureBytes.len);
|
|
193
|
+
} finally {
|
|
194
|
+
textureBytes.dispose();
|
|
195
|
+
}
|
|
196
|
+
} finally {
|
|
197
|
+
bitmap.close?.();
|
|
198
|
+
}
|
|
199
|
+
}),
|
|
200
|
+
]);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { FindOnPageProjector, type ResolvedFindSelection } from '../../find-on-page';
|
|
2
|
+
import type { OpenCanvasFindMatch, OpenCanvasFindState, OpenCanvasTextDocument, UiModule } from '../../core-types';
|
|
3
|
+
import { DEFAULT_OPEN_CANVAS_FIND_OPTIONS, normalizeOpenCanvasFindOptions } from '../find-session';
|
|
4
|
+
import { TextDocumentController } from './text-documents';
|
|
5
|
+
|
|
6
|
+
const OPEN_CANVAS_FIND_BACKGROUND_COLOR = 0xFFEB3B38;
|
|
7
|
+
|
|
8
|
+
interface FindControllerOptions {
|
|
9
|
+
readonly canvas: HTMLCanvasElement;
|
|
10
|
+
readonly ui: UiModule;
|
|
11
|
+
readonly textDocuments: TextDocumentController;
|
|
12
|
+
readonly commitFrame: () => void;
|
|
13
|
+
readonly flushPendingCommit: () => Uint32Array | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class FindController {
|
|
17
|
+
private readonly findProjector: FindOnPageProjector;
|
|
18
|
+
private activeFindMatch: OpenCanvasFindMatch | null = null;
|
|
19
|
+
private activeFindState: OpenCanvasFindState | null = null;
|
|
20
|
+
private selectionPollTimer: ReturnType<typeof setInterval> | null = null;
|
|
21
|
+
private selectionClearTimer: number | null = null;
|
|
22
|
+
|
|
23
|
+
public constructor(private readonly options: FindControllerOptions) {
|
|
24
|
+
const runtimeCanvasId = `ed-canvas-${Math.random().toString(36).slice(2, 10)}`;
|
|
25
|
+
this.findProjector = new FindOnPageProjector(options.canvas, runtimeCanvasId);
|
|
26
|
+
window.__bridgeFindMatch = null;
|
|
27
|
+
window.__bridgeFindState = null;
|
|
28
|
+
|
|
29
|
+
const handleDocumentSelectionChange = () => {
|
|
30
|
+
this.handleSelectionChange();
|
|
31
|
+
};
|
|
32
|
+
document.addEventListener('selectionchange', handleDocumentSelectionChange);
|
|
33
|
+
this.selectionPollTimer = setInterval(() => {
|
|
34
|
+
if (document.hidden) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
this.handleSelectionChange();
|
|
38
|
+
}, 100);
|
|
39
|
+
|
|
40
|
+
this.destroy = (() => {
|
|
41
|
+
if (this.selectionPollTimer !== null) {
|
|
42
|
+
clearInterval(this.selectionPollTimer);
|
|
43
|
+
this.selectionPollTimer = null;
|
|
44
|
+
}
|
|
45
|
+
this.cancelPendingFindClear();
|
|
46
|
+
document.removeEventListener('selectionchange', handleDocumentSelectionChange);
|
|
47
|
+
this.storeFindState(null);
|
|
48
|
+
this.storeFindMatch(null);
|
|
49
|
+
this.findProjector.destroy();
|
|
50
|
+
}).bind(this);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public syncSize(logicalWidth: number, logicalHeight: number): void {
|
|
54
|
+
this.findProjector.syncSize(logicalWidth, logicalHeight);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public syncDocuments(): void {
|
|
58
|
+
this.findProjector.update(this.options.textDocuments.readFindDocuments());
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public getFindDocuments(): readonly OpenCanvasTextDocument[] {
|
|
62
|
+
return this.options.textDocuments.readFindDocuments().map((document) => ({ ...document }));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public getFindState(): OpenCanvasFindState | null {
|
|
66
|
+
return this.activeFindState === null
|
|
67
|
+
? null
|
|
68
|
+
: {
|
|
69
|
+
query: this.activeFindState.query,
|
|
70
|
+
options: { ...this.activeFindState.options },
|
|
71
|
+
matches: this.activeFindState.matches.map((match) => ({ ...match })),
|
|
72
|
+
activeMatchIndex: this.activeFindState.activeMatchIndex,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public activateFindMatch(match: OpenCanvasFindMatch | null, reveal = true): boolean {
|
|
77
|
+
return this.applyRetainedFindMatch(match, reveal);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public setFindState(state: OpenCanvasFindState | null, revealActive = false): boolean {
|
|
81
|
+
return this.applyRetainedFindState(state, revealActive);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
public syncFindSelection(clearOnMissing = false): boolean {
|
|
85
|
+
return this.handleSelectionChange(clearOnMissing);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public clearFindMatch(): boolean {
|
|
89
|
+
this.findProjector.selectMatch(null);
|
|
90
|
+
return this.applyRetainedFindMatch(null, false);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public destroy(): void {}
|
|
94
|
+
|
|
95
|
+
private isBridgeFindDialogOpen(): boolean {
|
|
96
|
+
return document.querySelector('[data-ed-find-dialog="1"][data-ed-open="1"]') !== null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private cancelPendingFindClear(): void {
|
|
100
|
+
if (this.selectionClearTimer !== null) {
|
|
101
|
+
clearTimeout(this.selectionClearTimer);
|
|
102
|
+
this.selectionClearTimer = null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private schedulePendingFindClear(): void {
|
|
107
|
+
this.cancelPendingFindClear();
|
|
108
|
+
this.selectionClearTimer = window.setTimeout(() => {
|
|
109
|
+
this.selectionClearTimer = null;
|
|
110
|
+
if (this.isBridgeFindDialogOpen()) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (this.findProjector.resolveSelection(document.getSelection()) !== null) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (this.activeFindMatch !== null) {
|
|
117
|
+
this.applyRetainedFindState(null, false);
|
|
118
|
+
}
|
|
119
|
+
}, 150);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private storeFindMatch(match: ResolvedFindSelection | null): void {
|
|
123
|
+
this.activeFindMatch = match === null ? null : { ...match };
|
|
124
|
+
window.__bridgeFindMatch = this.activeFindMatch;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private storeFindState(state: OpenCanvasFindState | null): void {
|
|
128
|
+
this.activeFindState = state === null
|
|
129
|
+
? null
|
|
130
|
+
: {
|
|
131
|
+
query: state.query,
|
|
132
|
+
options: { ...state.options },
|
|
133
|
+
matches: state.matches.map((match) => ({ ...match })),
|
|
134
|
+
activeMatchIndex: state.activeMatchIndex,
|
|
135
|
+
};
|
|
136
|
+
window.__bridgeFindState = this.activeFindState;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private applyRetainedFindState(
|
|
140
|
+
state: OpenCanvasFindState | null,
|
|
141
|
+
revealActive: boolean,
|
|
142
|
+
): boolean {
|
|
143
|
+
if (state === null) {
|
|
144
|
+
this.options.ui._ui_clear_text_find_highlights();
|
|
145
|
+
this.options.ui._ui_clear_text_find_match();
|
|
146
|
+
this.options.commitFrame();
|
|
147
|
+
this.options.flushPendingCommit();
|
|
148
|
+
this.storeFindState(null);
|
|
149
|
+
this.storeFindMatch(null);
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const normalizedOptions = normalizeOpenCanvasFindOptions(state.options);
|
|
154
|
+
if (
|
|
155
|
+
state.activeMatchIndex < -1 ||
|
|
156
|
+
state.activeMatchIndex >= state.matches.length ||
|
|
157
|
+
(state.matches.length === 0 && state.activeMatchIndex !== -1)
|
|
158
|
+
) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const normalizedMatches: Array<{ readonly handleArg: number | bigint; readonly match: ResolvedFindSelection }> = [];
|
|
163
|
+
for (const match of state.matches) {
|
|
164
|
+
const range = this.options.textDocuments.resolveTextRange(match.handle, match.start, match.end);
|
|
165
|
+
if (range === null) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
normalizedMatches.push({
|
|
169
|
+
handleArg: range.handleArg,
|
|
170
|
+
match: {
|
|
171
|
+
handle: match.handle,
|
|
172
|
+
start: range.start,
|
|
173
|
+
end: range.end,
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
this.options.ui._ui_clear_text_find_highlights();
|
|
179
|
+
if (normalizedOptions.highlightAll) {
|
|
180
|
+
for (let index = 0; index < normalizedMatches.length; index += 1) {
|
|
181
|
+
if (index === state.activeMatchIndex) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const entry = normalizedMatches[index];
|
|
185
|
+
if (entry === undefined) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (
|
|
189
|
+
this.options.ui._ui_push_text_find_highlight(
|
|
190
|
+
entry.handleArg,
|
|
191
|
+
entry.match.start,
|
|
192
|
+
entry.match.end,
|
|
193
|
+
OPEN_CANVAS_FIND_BACKGROUND_COLOR,
|
|
194
|
+
) === 0
|
|
195
|
+
) {
|
|
196
|
+
this.options.ui._ui_clear_text_find_highlights();
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const activeEntry = state.activeMatchIndex >= 0
|
|
203
|
+
? normalizedMatches[state.activeMatchIndex] ?? null
|
|
204
|
+
: null;
|
|
205
|
+
if (activeEntry === null) {
|
|
206
|
+
this.options.ui._ui_clear_text_find_match();
|
|
207
|
+
} else {
|
|
208
|
+
if (this.options.ui._ui_set_text_find_match(activeEntry.handleArg, activeEntry.match.start, activeEntry.match.end) === 0) {
|
|
209
|
+
this.options.ui._ui_clear_text_find_highlights();
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
if (
|
|
213
|
+
revealActive &&
|
|
214
|
+
this.options.ui._ui_reveal_text_range(activeEntry.handleArg, activeEntry.match.start, activeEntry.match.end) === 0
|
|
215
|
+
) {
|
|
216
|
+
this.options.ui._ui_clear_text_find_highlights();
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
this.options.commitFrame();
|
|
222
|
+
this.options.flushPendingCommit();
|
|
223
|
+
this.storeFindState({
|
|
224
|
+
query: state.query,
|
|
225
|
+
options: normalizedOptions,
|
|
226
|
+
matches: normalizedMatches.map((entry) => ({ ...entry.match })),
|
|
227
|
+
activeMatchIndex: activeEntry === null ? -1 : state.activeMatchIndex,
|
|
228
|
+
});
|
|
229
|
+
this.storeFindMatch(activeEntry?.match ?? null);
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private applyRetainedFindMatch(
|
|
234
|
+
match: ResolvedFindSelection | null,
|
|
235
|
+
reveal: boolean,
|
|
236
|
+
): boolean {
|
|
237
|
+
return this.applyRetainedFindState(
|
|
238
|
+
match === null
|
|
239
|
+
? null
|
|
240
|
+
: {
|
|
241
|
+
query: '',
|
|
242
|
+
options: { ...DEFAULT_OPEN_CANVAS_FIND_OPTIONS },
|
|
243
|
+
matches: [{ ...match }],
|
|
244
|
+
activeMatchIndex: 0,
|
|
245
|
+
},
|
|
246
|
+
reveal,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private handleSelectionChange(clearOnMissing = false): boolean {
|
|
251
|
+
const match = this.findProjector.resolveSelection(document.getSelection());
|
|
252
|
+
if (match === null) {
|
|
253
|
+
if (clearOnMissing && !this.isBridgeFindDialogOpen() && this.activeFindMatch !== null) {
|
|
254
|
+
this.schedulePendingFindClear();
|
|
255
|
+
}
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
this.cancelPendingFindClear();
|
|
259
|
+
if (
|
|
260
|
+
this.activeFindMatch !== null &&
|
|
261
|
+
this.activeFindMatch.handle === match.handle &&
|
|
262
|
+
this.activeFindMatch.start === match.start &&
|
|
263
|
+
this.activeFindMatch.end === match.end
|
|
264
|
+
) {
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
return this.applyRetainedFindMatch(match, true);
|
|
268
|
+
}
|
|
269
|
+
}
|