@adecore/editor 0.39.0 → 0.40.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/dist/controller.d.ts +1 -0
- package/dist/controller.js +17 -28
- package/dist/engine.js +2 -9
- package/dist/find.js +12 -13
- package/dist/layout.js +7 -2
- package/dist/paint.d.ts +2 -0
- package/dist/paint.js +23 -20
- package/dist/view.d.ts +3 -2
- package/dist/view.js +46 -59
- package/package.json +6 -6
- package/src/attribution.ts +14 -0
- package/src/code-block.ts +25 -0
- package/src/column-selection.ts +10 -0
- package/src/controller.ts +189 -28
- package/src/engine.ts +87 -9
- package/src/fake.ts +79 -0
- package/src/find-search.ts +20 -0
- package/src/find.ts +135 -14
- package/src/geometry-stops.ts +10 -0
- package/src/grammar-preparation.ts +23 -0
- package/src/index.ts +1 -0
- package/src/input.ts +13 -0
- package/src/keymap-table.ts +9 -0
- package/src/keymap.ts +25 -0
- package/src/layout-changes.ts +7 -0
- package/src/layout.ts +219 -3
- package/src/lens.ts +3 -0
- package/src/listeners.ts +1 -0
- package/src/metrics.ts +14 -0
- package/src/modifier-gesture.ts +17 -0
- package/src/native-input.ts +27 -0
- package/src/offsets.ts +10 -0
- package/src/outline.ts +38 -0
- package/src/overview.ts +13 -0
- package/src/paint.ts +140 -20
- package/src/php-html.ts +1 -0
- package/src/pointer.ts +50 -0
- package/src/scroll-animation.ts +10 -0
- package/src/scroll.ts +7 -0
- package/src/search-client.ts +32 -0
- package/src/search-worker.ts +38 -0
- package/src/semantic.ts +12 -0
- package/src/shiki-document.ts +19 -0
- package/src/shiki-worker.ts +68 -0
- package/src/shiki.ts +37 -0
- package/src/testing.ts +17 -0
- package/src/theme-scopes.ts +14 -0
- package/src/tokens.ts +31 -0
- package/src/touch.ts +16 -0
- package/src/tracked-range.ts +5 -0
- package/src/uniform-geometry.ts +10 -0
- package/src/view.ts +481 -75
- package/src/worker-client.ts +54 -0
package/src/pointer.ts
CHANGED
|
@@ -82,6 +82,7 @@ function overshoot(value: number, low: number, high: number): number {
|
|
|
82
82
|
if (value < low) {
|
|
83
83
|
return value - low;
|
|
84
84
|
}
|
|
85
|
+
|
|
85
86
|
return value > high ? value - high : 0;
|
|
86
87
|
}
|
|
87
88
|
|
|
@@ -107,6 +108,7 @@ export class PointerSelection {
|
|
|
107
108
|
const head = this.host.offsetAt(event.clientX, event.clientY);
|
|
108
109
|
const selections = model.getSelections();
|
|
109
110
|
const base = event.altKey && area === 'text' ? selections : [];
|
|
111
|
+
|
|
110
112
|
this.drag = {
|
|
111
113
|
id: event.pointerId,
|
|
112
114
|
anchor: event.shiftKey && !event.altKey ? model.getPrimary().anchor : head,
|
|
@@ -120,17 +122,23 @@ export class PointerSelection {
|
|
|
120
122
|
x: event.clientX,
|
|
121
123
|
y: event.clientY
|
|
122
124
|
};
|
|
125
|
+
|
|
123
126
|
this.host.focus();
|
|
124
127
|
viewport.setPointerCapture?.(event.pointerId);
|
|
128
|
+
|
|
125
129
|
// Alt on a caret that is there takes it away; there is no selection to drag after that.
|
|
126
130
|
if (area === 'text' && event.altKey && count === 1 && selections.length > 1 && selections.some((selection) => selection.head === head)) {
|
|
127
131
|
model.setSelections(selections.filter((selection) => selection.head !== head));
|
|
128
132
|
this.drag = undefined;
|
|
133
|
+
|
|
129
134
|
return;
|
|
130
135
|
}
|
|
136
|
+
|
|
131
137
|
const selected = count === 1 && area === 'text' && !event.shiftKey && !event.altKey ? this.host.selectionAt(event.clientX, event.clientY) : null;
|
|
138
|
+
|
|
132
139
|
if (selected !== null) {
|
|
133
140
|
this.drag.unit = 'drop';
|
|
141
|
+
|
|
134
142
|
this.drag.carry = {
|
|
135
143
|
from: Math.min(selected.anchor, selected.head),
|
|
136
144
|
to: Math.max(selected.anchor, selected.head),
|
|
@@ -146,6 +154,7 @@ export class PointerSelection {
|
|
|
146
154
|
this.selectUnit('line', this.host.lineAt(head), base);
|
|
147
155
|
} else if (count === 2) {
|
|
148
156
|
const word = model.wordSelectionAt(head, this.host.camelHumps());
|
|
157
|
+
|
|
149
158
|
if (word === null) {
|
|
150
159
|
this.update();
|
|
151
160
|
} else {
|
|
@@ -154,6 +163,7 @@ export class PointerSelection {
|
|
|
154
163
|
} else {
|
|
155
164
|
this.update();
|
|
156
165
|
}
|
|
166
|
+
|
|
157
167
|
if (this.drag && this.frame === undefined) {
|
|
158
168
|
this.frame = viewport.ownerDocument.defaultView?.requestAnimationFrame?.(() => this.autoScroll());
|
|
159
169
|
}
|
|
@@ -163,11 +173,14 @@ export class PointerSelection {
|
|
|
163
173
|
private pressLineNumber(event: PointerEvent, offset: number): void {
|
|
164
174
|
const { model } = this.host;
|
|
165
175
|
const primary = model.getPrimary();
|
|
176
|
+
|
|
166
177
|
if (event.shiftKey && primary.anchor !== primary.head) {
|
|
167
178
|
model.setSelections([this.tweakedByLines(primary, offset)]);
|
|
168
179
|
this.drag = undefined;
|
|
180
|
+
|
|
169
181
|
return;
|
|
170
182
|
}
|
|
183
|
+
|
|
171
184
|
this.selectUnit('line', this.host.lineAt(offset), []);
|
|
172
185
|
}
|
|
173
186
|
|
|
@@ -175,6 +188,7 @@ export class PointerSelection {
|
|
|
175
188
|
if (this.drag === undefined) {
|
|
176
189
|
return;
|
|
177
190
|
}
|
|
191
|
+
|
|
178
192
|
this.drag.unit = unit;
|
|
179
193
|
this.drag.saved = saved;
|
|
180
194
|
this.drag.selections = base;
|
|
@@ -189,33 +203,43 @@ export class PointerSelection {
|
|
|
189
203
|
const first = host.visualLine(from);
|
|
190
204
|
const last = host.visualLine(to - 1);
|
|
191
205
|
const clicked = host.visualLine(offset);
|
|
206
|
+
|
|
192
207
|
if (clicked.y < first.y) {
|
|
193
208
|
return { anchor: to, head: clicked.start };
|
|
194
209
|
}
|
|
210
|
+
|
|
195
211
|
if (clicked.y > last.y) {
|
|
196
212
|
return { anchor: from, head: clicked.end };
|
|
197
213
|
}
|
|
214
|
+
|
|
198
215
|
if (first.y === last.y) {
|
|
199
216
|
return { anchor: selection.head, head: selection.head };
|
|
200
217
|
}
|
|
218
|
+
|
|
201
219
|
if (selection.anchor === to) {
|
|
202
220
|
const line = clicked.y === first.y ? host.visualLine(first.next) : clicked;
|
|
221
|
+
|
|
203
222
|
return { anchor: to, head: line.start };
|
|
204
223
|
}
|
|
224
|
+
|
|
205
225
|
const line = clicked.y === last.y ? host.visualLine(last.start - 1) : clicked;
|
|
226
|
+
|
|
206
227
|
return { anchor: from, head: line.end };
|
|
207
228
|
}
|
|
208
229
|
|
|
209
230
|
/* Pointer events carry no click count, so the presses are counted here. */
|
|
210
231
|
private countClick(event: PointerEvent): number {
|
|
211
232
|
const last = this.lastPress;
|
|
233
|
+
|
|
212
234
|
const repeated =
|
|
213
235
|
last !== undefined &&
|
|
214
236
|
event.timeStamp - last.time <= MULTI_CLICK_MS &&
|
|
215
237
|
Math.abs(event.clientX - last.x) <= MULTI_CLICK_DISTANCE &&
|
|
216
238
|
Math.abs(event.clientY - last.y) <= MULTI_CLICK_DISTANCE;
|
|
239
|
+
|
|
217
240
|
const count = repeated ? Math.min(3, last.count + 1) : 1;
|
|
218
241
|
this.lastPress = { time: event.timeStamp, x: event.clientX, y: event.clientY, count };
|
|
242
|
+
|
|
219
243
|
return count;
|
|
220
244
|
}
|
|
221
245
|
|
|
@@ -223,6 +247,7 @@ export class PointerSelection {
|
|
|
223
247
|
if (!this.drag || event.pointerId !== this.drag.id) {
|
|
224
248
|
return;
|
|
225
249
|
}
|
|
250
|
+
|
|
226
251
|
this.drag.x = event.clientX;
|
|
227
252
|
this.drag.y = event.clientY;
|
|
228
253
|
this.update();
|
|
@@ -232,18 +257,24 @@ export class PointerSelection {
|
|
|
232
257
|
end(event?: PointerEvent): void {
|
|
233
258
|
const { viewport } = this.host;
|
|
234
259
|
const carry = this.drag?.carry;
|
|
260
|
+
|
|
235
261
|
if (this.drag) {
|
|
236
262
|
viewport.releasePointerCapture?.(this.drag.id);
|
|
237
263
|
}
|
|
264
|
+
|
|
238
265
|
this.drag = undefined;
|
|
239
266
|
this.cancelFrame();
|
|
267
|
+
|
|
240
268
|
if (carry === undefined) {
|
|
241
269
|
return;
|
|
242
270
|
}
|
|
271
|
+
|
|
243
272
|
this.host.setDropCaret(null);
|
|
273
|
+
|
|
244
274
|
if (event?.type === 'pointercancel') {
|
|
245
275
|
return;
|
|
246
276
|
}
|
|
277
|
+
|
|
247
278
|
if (!carry.active) {
|
|
248
279
|
this.host.model.setSelections([{ anchor: carry.pressOffset, head: carry.pressOffset }]);
|
|
249
280
|
} else if (carry.target !== null && event !== undefined) {
|
|
@@ -257,8 +288,10 @@ export class PointerSelection {
|
|
|
257
288
|
if (Math.abs(drag.x - carry.startX) < CARRY_THRESHOLD && Math.abs(drag.y - carry.startY) < CARRY_THRESHOLD) {
|
|
258
289
|
return;
|
|
259
290
|
}
|
|
291
|
+
|
|
260
292
|
carry.active = true;
|
|
261
293
|
}
|
|
294
|
+
|
|
262
295
|
const offset = this.host.offsetAt(drag.x, drag.y);
|
|
263
296
|
carry.target = offset >= carry.from && offset <= carry.to ? null : offset;
|
|
264
297
|
this.host.setDropCaret(carry.target);
|
|
@@ -273,6 +306,7 @@ export class PointerSelection {
|
|
|
273
306
|
if (this.frame !== undefined) {
|
|
274
307
|
this.host.viewport.ownerDocument.defaultView?.cancelAnimationFrame?.(this.frame);
|
|
275
308
|
}
|
|
309
|
+
|
|
276
310
|
this.frame = undefined;
|
|
277
311
|
}
|
|
278
312
|
|
|
@@ -280,17 +314,23 @@ export class PointerSelection {
|
|
|
280
314
|
if (!this.drag) {
|
|
281
315
|
return;
|
|
282
316
|
}
|
|
317
|
+
|
|
283
318
|
if (this.drag.carry !== undefined) {
|
|
284
319
|
this.updateCarry(this.drag, this.drag.carry);
|
|
320
|
+
|
|
285
321
|
return;
|
|
286
322
|
}
|
|
323
|
+
|
|
287
324
|
if (this.drag.box && this.drag.unit === 'character') {
|
|
288
325
|
const box = this.host.columnSelections(this.drag.anchorPoint, this.host.contentPoint(this.drag.x, this.drag.y));
|
|
326
|
+
|
|
289
327
|
if (box) {
|
|
290
328
|
this.host.model.setSelections(this.drag.keep ? [...this.drag.selections, ...box] : box);
|
|
329
|
+
|
|
291
330
|
return;
|
|
292
331
|
}
|
|
293
332
|
}
|
|
333
|
+
|
|
294
334
|
const head = this.host.offsetAt(this.drag.x, this.drag.y);
|
|
295
335
|
const { anchor, target } = this.reach(this.drag, head);
|
|
296
336
|
this.host.model.setSelections([...(this.drag.add ? this.drag.selections : []), { anchor, head: target }]);
|
|
@@ -300,35 +340,45 @@ export class PointerSelection {
|
|
|
300
340
|
private reach(drag: Drag, head: number): { anchor: number; target: number } {
|
|
301
341
|
const { model } = this.host;
|
|
302
342
|
const backwards = head < drag.saved.from;
|
|
343
|
+
|
|
303
344
|
if (drag.unit === 'word') {
|
|
304
345
|
const camel = this.host.camelHumps();
|
|
346
|
+
|
|
305
347
|
return backwards
|
|
306
348
|
? { anchor: drag.saved.to, target: model.wordStartBefore(head, camel) }
|
|
307
349
|
: { anchor: drag.saved.from, target: model.wordEndAfter(head, camel) };
|
|
308
350
|
}
|
|
351
|
+
|
|
309
352
|
if (drag.unit === 'line') {
|
|
310
353
|
const line = this.host.visualLine(head);
|
|
354
|
+
|
|
311
355
|
return backwards ? { anchor: drag.saved.to, target: line.start } : { anchor: drag.saved.from, target: line.next };
|
|
312
356
|
}
|
|
357
|
+
|
|
313
358
|
return { anchor: drag.anchor, target: head };
|
|
314
359
|
}
|
|
315
360
|
|
|
316
361
|
/* Holding the pointer past an edge of the view keeps scrolling that way. */
|
|
317
362
|
private autoScroll(): void {
|
|
318
363
|
const { viewport } = this.host;
|
|
364
|
+
|
|
319
365
|
if (!this.drag) {
|
|
320
366
|
this.frame = undefined;
|
|
367
|
+
|
|
321
368
|
return;
|
|
322
369
|
}
|
|
370
|
+
|
|
323
371
|
const rect = viewport.getBoundingClientRect();
|
|
324
372
|
const dy = overshoot(this.drag.y, rect.top, rect.bottom);
|
|
325
373
|
const dx = overshoot(this.drag.x, rect.left, rect.right);
|
|
374
|
+
|
|
326
375
|
if (dy || dx) {
|
|
327
376
|
viewport.scrollTop += Math.max(-SCROLL_STEP, Math.min(SCROLL_STEP, dy));
|
|
328
377
|
viewport.scrollLeft += Math.max(-SCROLL_STEP, Math.min(SCROLL_STEP, dx));
|
|
329
378
|
this.host.scrolled();
|
|
330
379
|
this.update();
|
|
331
380
|
}
|
|
381
|
+
|
|
332
382
|
this.frame = viewport.ownerDocument.defaultView?.requestAnimationFrame?.(() => this.autoScroll());
|
|
333
383
|
}
|
|
334
384
|
}
|
package/src/scroll-animation.ts
CHANGED
|
@@ -13,6 +13,7 @@ const FULL_LINES = 11;
|
|
|
13
13
|
export function scrollDuration(distance: number, lineHeight: number): number {
|
|
14
14
|
const lines = distance / lineHeight;
|
|
15
15
|
const part = Math.min(1, Math.max(0, (lines - FREE_LINES) / (FULL_LINES - FREE_LINES)));
|
|
16
|
+
|
|
16
17
|
return Math.round(part * SCROLL_DURATION_MS);
|
|
17
18
|
}
|
|
18
19
|
|
|
@@ -21,28 +22,37 @@ export function easeOut(fraction: number): number {
|
|
|
21
22
|
if (fraction <= 0) {
|
|
22
23
|
return 0;
|
|
23
24
|
}
|
|
25
|
+
|
|
24
26
|
if (fraction >= 1) {
|
|
25
27
|
return 1;
|
|
26
28
|
}
|
|
29
|
+
|
|
27
30
|
const x1 = 0;
|
|
28
31
|
const y1 = 0;
|
|
29
32
|
const x2 = 0.58;
|
|
30
33
|
const y2 = 1;
|
|
34
|
+
|
|
31
35
|
const bezier = (t: number, a: number, b: number): number => 3 * (1 - t) * (1 - t) * t * a + 3 * (1 - t) * t * t * b + t * t * t;
|
|
36
|
+
|
|
32
37
|
let low = 0;
|
|
33
38
|
let high = 1;
|
|
34
39
|
let t = fraction;
|
|
40
|
+
|
|
35
41
|
for (let step = 0; step < 24; step++) {
|
|
36
42
|
const x = bezier(t, x1, x2);
|
|
43
|
+
|
|
37
44
|
if (Math.abs(x - fraction) < 1e-5) {
|
|
38
45
|
break;
|
|
39
46
|
}
|
|
47
|
+
|
|
40
48
|
if (x < fraction) {
|
|
41
49
|
low = t;
|
|
42
50
|
} else {
|
|
43
51
|
high = t;
|
|
44
52
|
}
|
|
53
|
+
|
|
45
54
|
t = (low + high) / 2;
|
|
46
55
|
}
|
|
56
|
+
|
|
47
57
|
return bezier(t, y1, y2);
|
|
48
58
|
}
|
package/src/scroll.ts
CHANGED
|
@@ -43,6 +43,7 @@ function clamp(value: number, maximum: number): number {
|
|
|
43
43
|
function centerOf(request: ScrollRequest): number {
|
|
44
44
|
const { target, view, lineHeight, topBound } = request;
|
|
45
45
|
const third = target.y - Math.trunc(view.height / 3);
|
|
46
|
+
|
|
46
47
|
return third < topBound ? third : target.y - Math.trunc(Math.max(0, view.height - lineHeight) / 2);
|
|
47
48
|
}
|
|
48
49
|
|
|
@@ -53,6 +54,7 @@ function verticalPosition(request: ScrollRequest, kind: ScrollKind): number {
|
|
|
53
54
|
const above = view.y > topBound;
|
|
54
55
|
const below = view.y + height < bottomBound;
|
|
55
56
|
let position = view.y;
|
|
57
|
+
|
|
56
58
|
if (kind === 'center') {
|
|
57
59
|
position = center;
|
|
58
60
|
} else if (kind === 'centerUp') {
|
|
@@ -70,7 +72,9 @@ function verticalPosition(request: ScrollRequest, kind: ScrollKind): number {
|
|
|
70
72
|
const highest = target.y - height < 0 ? 0 : bottomBound - height;
|
|
71
73
|
position = Math.min(topBound, Math.max(highest, view.y));
|
|
72
74
|
}
|
|
75
|
+
|
|
73
76
|
const inset = request.inset ?? 0;
|
|
77
|
+
|
|
74
78
|
return Math.min(Math.max(inset, position), Math.max(inset, content.height - height));
|
|
75
79
|
}
|
|
76
80
|
|
|
@@ -80,6 +84,7 @@ function horizontalPosition(request: ScrollRequest, kind: ScrollKind): number {
|
|
|
80
84
|
const left = target.x - SIDE_MARGIN * charWidth;
|
|
81
85
|
const right = target.x + SIDE_MARGIN * charWidth;
|
|
82
86
|
let position = view.x;
|
|
87
|
+
|
|
83
88
|
if (right - left > width) {
|
|
84
89
|
position = target.x - Math.trunc(width / 2);
|
|
85
90
|
} else if (left < view.x) {
|
|
@@ -87,6 +92,7 @@ function horizontalPosition(request: ScrollRequest, kind: ScrollKind): number {
|
|
|
87
92
|
} else if (right > view.x + width) {
|
|
88
93
|
position = right - width;
|
|
89
94
|
}
|
|
95
|
+
|
|
90
96
|
return clamp(position, content.width - width);
|
|
91
97
|
}
|
|
92
98
|
|
|
@@ -95,6 +101,7 @@ export function scrollPosition(request: ScrollRequest, kind: ScrollKind): { x: n
|
|
|
95
101
|
const { target, view } = request;
|
|
96
102
|
const inView = target.x >= view.x && target.x < view.x + view.width && target.y >= view.y && target.y < view.y + view.height;
|
|
97
103
|
const effective: ScrollKind = request.refrain && inView && (kind === 'center' || kind === 'centerDown' || kind === 'centerUp') ? 'relative' : kind;
|
|
104
|
+
|
|
98
105
|
return {
|
|
99
106
|
x: request.horizontal ? horizontalPosition(request, effective) : view.x,
|
|
100
107
|
y: verticalPosition(request, effective) - (request.inset ?? 0)
|
package/src/search-client.ts
CHANGED
|
@@ -62,6 +62,7 @@ function validateSearch(snapshot: EditorSearchSnapshot, query: EditorFindQuery,
|
|
|
62
62
|
const from = options.from ?? 0;
|
|
63
63
|
const to = options.to ?? snapshot.text.length;
|
|
64
64
|
const maximum = options.maxResults ?? SEARCH_LIMIT;
|
|
65
|
+
|
|
65
66
|
if (
|
|
66
67
|
!Number.isSafeInteger(snapshot.revision) ||
|
|
67
68
|
snapshot.revision < 0 ||
|
|
@@ -73,12 +74,15 @@ function validateSearch(snapshot: EditorSearchSnapshot, query: EditorFindQuery,
|
|
|
73
74
|
) {
|
|
74
75
|
throw new EditorSearchError('range', 'Search bounds and revision must identify the current document.');
|
|
75
76
|
}
|
|
77
|
+
|
|
76
78
|
if (!Number.isInteger(maximum) || maximum < 0 || maximum > SEARCH_LIMIT) {
|
|
77
79
|
throw new EditorSearchError('range', `Search results must be limited to ${SEARCH_LIMIT} matches or fewer.`);
|
|
78
80
|
}
|
|
81
|
+
|
|
79
82
|
if (options.deadline !== undefined && (!Number.isFinite(options.deadline) || options.deadline <= 0 || options.deadline > 2147483647)) {
|
|
80
83
|
throw new EditorSearchError('range', 'A request deadline must fit a positive browser timer.');
|
|
81
84
|
}
|
|
85
|
+
|
|
82
86
|
if (query.regex) {
|
|
83
87
|
try {
|
|
84
88
|
// Compilation reports syntax errors without executing the expression on the page.
|
|
@@ -92,11 +96,13 @@ function validateSearch(snapshot: EditorSearchSnapshot, query: EditorFindQuery,
|
|
|
92
96
|
function splitsSurrogate(text: string, offset: number): boolean {
|
|
93
97
|
const previous = text.charCodeAt(offset - 1),
|
|
94
98
|
current = text.charCodeAt(offset);
|
|
99
|
+
|
|
95
100
|
return previous >= 0xd800 && previous <= 0xdbff && current >= 0xdc00 && current <= 0xdfff;
|
|
96
101
|
}
|
|
97
102
|
|
|
98
103
|
export function createSearchClient(options: EditorSearchClientOptions): EditorSearchClient {
|
|
99
104
|
let revision: number | undefined;
|
|
105
|
+
|
|
100
106
|
const transport = createWorkerRequestClient<EditorSearchInput, Exclude<EditorSearchOutput, { type: 'ready' }>>({
|
|
101
107
|
...options,
|
|
102
108
|
accepts: (reply, input) => reply.revision === input.revision,
|
|
@@ -108,12 +114,14 @@ export function createSearchClient(options: EditorSearchClientOptions): EditorSe
|
|
|
108
114
|
const { text, ...rest } = input;
|
|
109
115
|
const message = { ...rest, ...(revision !== input.revision ? { text } : {}) };
|
|
110
116
|
revision = input.revision;
|
|
117
|
+
|
|
111
118
|
return message;
|
|
112
119
|
},
|
|
113
120
|
stopped: () => {
|
|
114
121
|
revision = undefined;
|
|
115
122
|
}
|
|
116
123
|
});
|
|
124
|
+
|
|
117
125
|
const run = <Result>(
|
|
118
126
|
snapshot: EditorSearchSnapshot,
|
|
119
127
|
query: EditorFindQuery,
|
|
@@ -131,6 +139,7 @@ export function createSearchClient(options: EditorSearchClientOptions): EditorSe
|
|
|
131
139
|
if (reply.type === 'error') {
|
|
132
140
|
throw new EditorSearchError(reply.kind, reply.message);
|
|
133
141
|
}
|
|
142
|
+
|
|
134
143
|
return read(reply);
|
|
135
144
|
})
|
|
136
145
|
.catch((error) => {
|
|
@@ -141,6 +150,7 @@ export function createSearchClient(options: EditorSearchClientOptions): EditorSe
|
|
|
141
150
|
: new EditorSearchError('error', error instanceof Error ? error.message : String(error));
|
|
142
151
|
});
|
|
143
152
|
};
|
|
153
|
+
|
|
144
154
|
return {
|
|
145
155
|
search(snapshot, query, bounds = {}) {
|
|
146
156
|
return run(
|
|
@@ -160,6 +170,7 @@ export function createSearchClient(options: EditorSearchClientOptions): EditorSe
|
|
|
160
170
|
if (reply.type !== 'matches') {
|
|
161
171
|
throw new EditorSearchError('error', 'The worker returned an unexpected search response.');
|
|
162
172
|
}
|
|
173
|
+
|
|
163
174
|
return { revision: reply.revision, ranges: reply.ranges, limited: reply.limited };
|
|
164
175
|
}
|
|
165
176
|
);
|
|
@@ -167,7 +178,9 @@ export function createSearchClient(options: EditorSearchClientOptions): EditorSe
|
|
|
167
178
|
match(snapshot, query, range) {
|
|
168
179
|
const from = range.from,
|
|
169
180
|
to = range.to;
|
|
181
|
+
|
|
170
182
|
const text = snapshot.text;
|
|
183
|
+
|
|
171
184
|
return run(
|
|
172
185
|
snapshot,
|
|
173
186
|
query,
|
|
@@ -177,12 +190,15 @@ export function createSearchClient(options: EditorSearchClientOptions): EditorSe
|
|
|
177
190
|
if (reply.type !== 'match') {
|
|
178
191
|
throw new EditorSearchError('error', 'The worker returned an unexpected match response.');
|
|
179
192
|
}
|
|
193
|
+
|
|
180
194
|
if (reply.match === null) {
|
|
181
195
|
return null;
|
|
182
196
|
}
|
|
197
|
+
|
|
183
198
|
if (reply.match.from !== from || reply.match.to !== to) {
|
|
184
199
|
throw new EditorSearchError('error', 'The worker returned a different match range.');
|
|
185
200
|
}
|
|
201
|
+
|
|
186
202
|
return { ...reply.match, text: text.slice(from, to), revision: reply.revision };
|
|
187
203
|
}
|
|
188
204
|
);
|
|
@@ -197,7 +213,9 @@ export function createSearchClient(options: EditorSearchClientOptions): EditorSe
|
|
|
197
213
|
if (reply.type !== 'next') {
|
|
198
214
|
throw new EditorSearchError('error', 'The worker returned an unexpected cursor response.');
|
|
199
215
|
}
|
|
216
|
+
|
|
200
217
|
const range = reply.range;
|
|
218
|
+
|
|
201
219
|
if (
|
|
202
220
|
range !== null &&
|
|
203
221
|
(!Number.isInteger(range.from) ||
|
|
@@ -210,6 +228,7 @@ export function createSearchClient(options: EditorSearchClientOptions): EditorSe
|
|
|
210
228
|
) {
|
|
211
229
|
throw new EditorSearchError('error', 'The worker returned invalid cursor bounds.');
|
|
212
230
|
}
|
|
231
|
+
|
|
213
232
|
return range;
|
|
214
233
|
}
|
|
215
234
|
);
|
|
@@ -218,6 +237,7 @@ export function createSearchClient(options: EditorSearchClientOptions): EditorSe
|
|
|
218
237
|
const { from, to, preserveCase } = options;
|
|
219
238
|
const maxEdits = options.maxEdits ?? 1000;
|
|
220
239
|
const maxCharacters = options.maxCharacters ?? MAX_REPLACEMENT_CHARACTERS;
|
|
240
|
+
|
|
221
241
|
return run(
|
|
222
242
|
snapshot,
|
|
223
243
|
query,
|
|
@@ -239,15 +259,19 @@ export function createSearchClient(options: EditorSearchClientOptions): EditorSe
|
|
|
239
259
|
if (reply.type !== 'replacements') {
|
|
240
260
|
throw new EditorSearchError('error', 'The worker returned an unexpected replacement response.');
|
|
241
261
|
}
|
|
262
|
+
|
|
242
263
|
if (reply.ranges.length % 3 !== 0 || reply.ranges.length / 3 > maxEdits || reply.text.length > maxCharacters) {
|
|
243
264
|
throw new EditorSearchError('error', 'The worker returned an invalid replacement page.');
|
|
244
265
|
}
|
|
266
|
+
|
|
245
267
|
let characters = 0;
|
|
246
268
|
let previous = from ?? 0;
|
|
247
269
|
let previousEmpty = false;
|
|
270
|
+
|
|
248
271
|
for (let index = 0; index < reply.ranges.length; index += 3) {
|
|
249
272
|
const start = reply.ranges[index]!,
|
|
250
273
|
end = reply.ranges[index + 1]!;
|
|
274
|
+
|
|
251
275
|
if (
|
|
252
276
|
start < previous ||
|
|
253
277
|
(previousEmpty && start === previous) ||
|
|
@@ -258,25 +282,33 @@ export function createSearchClient(options: EditorSearchClientOptions): EditorSe
|
|
|
258
282
|
) {
|
|
259
283
|
throw new EditorSearchError('error', 'The worker returned invalid replacement bounds.');
|
|
260
284
|
}
|
|
285
|
+
|
|
261
286
|
previous = end;
|
|
262
287
|
previousEmpty = start === end;
|
|
263
288
|
characters += reply.ranges[index + 2]!;
|
|
264
289
|
}
|
|
290
|
+
|
|
265
291
|
if (characters !== reply.text.length) {
|
|
266
292
|
throw new EditorSearchError('error', 'The worker returned inconsistent replacement lengths.');
|
|
267
293
|
}
|
|
294
|
+
|
|
268
295
|
if (reply.next !== null) {
|
|
269
296
|
const index = reply.ranges.length - 3;
|
|
297
|
+
|
|
270
298
|
if (index < 0) {
|
|
271
299
|
throw new EditorSearchError('error', 'The worker returned an empty replacement continuation.');
|
|
272
300
|
}
|
|
301
|
+
|
|
273
302
|
const start = reply.ranges[index],
|
|
274
303
|
end = reply.ranges[index + 1];
|
|
304
|
+
|
|
275
305
|
const resume = end! + (start === end ? ((snapshot.text.codePointAt(end!) ?? 0) > 0xffff ? 2 : 1) : 0);
|
|
306
|
+
|
|
276
307
|
if (reply.next !== resume || reply.next > (to ?? snapshot.text.length)) {
|
|
277
308
|
throw new EditorSearchError('error', 'The worker returned an invalid replacement continuation.');
|
|
278
309
|
}
|
|
279
310
|
}
|
|
311
|
+
|
|
280
312
|
return { revision: reply.revision, ranges: reply.ranges, text: reply.text, next: reply.next };
|
|
281
313
|
},
|
|
282
314
|
() => {
|
package/src/search-worker.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from './search-protocol.ts';
|
|
10
10
|
|
|
11
11
|
export { createSearchClient, EditorSearchError, type EditorSearchClient, type EditorSearchClientOptions, type EditorSearchErrorKind } from './search-client.ts';
|
|
12
|
+
|
|
12
13
|
export type {
|
|
13
14
|
EditorSearchInput,
|
|
14
15
|
EditorSearchOptions,
|
|
@@ -36,15 +37,19 @@ function findOptions(request: EditorSearchInput): FindOptions {
|
|
|
36
37
|
|
|
37
38
|
function captureMatch(found: FindMatch) {
|
|
38
39
|
let characters = 0;
|
|
40
|
+
|
|
39
41
|
for (const value of found.captures) {
|
|
40
42
|
characters += value?.length ?? 0;
|
|
41
43
|
}
|
|
44
|
+
|
|
42
45
|
for (const value of Object.values(found.groups ?? {})) {
|
|
43
46
|
characters += value?.length ?? 0;
|
|
44
47
|
}
|
|
48
|
+
|
|
45
49
|
if (characters > MAX_CAPTURE_CHARACTERS) {
|
|
46
50
|
throw new RangeError('Search captures exceed the response size limit.');
|
|
47
51
|
}
|
|
52
|
+
|
|
48
53
|
return { from: found.from, to: found.to, captures: found.captures, groups: found.groups, regex: found.regex };
|
|
49
54
|
}
|
|
50
55
|
|
|
@@ -52,28 +57,37 @@ function searchReply(request: EditorSearchInput, snapshot: EditorSearchSnapshot)
|
|
|
52
57
|
const { text } = snapshot;
|
|
53
58
|
const options = findOptions(request);
|
|
54
59
|
const header = { id: request.id, revision: request.revision };
|
|
60
|
+
|
|
55
61
|
if (request.type === 'next') {
|
|
56
62
|
let found: FindMatch | null = null;
|
|
63
|
+
|
|
57
64
|
if (request.backwards) {
|
|
58
65
|
let last: FindMatch | null = null;
|
|
66
|
+
|
|
59
67
|
for (const match of iterateMatches(text, request.query.text, request.revision, options)) {
|
|
60
68
|
last = match;
|
|
69
|
+
|
|
61
70
|
if (match.from < request.offset) {
|
|
62
71
|
found = match;
|
|
63
72
|
} else if (found !== null || request.wrap === false) {
|
|
64
73
|
break;
|
|
65
74
|
}
|
|
66
75
|
}
|
|
76
|
+
|
|
67
77
|
found ??= request.wrap === false ? null : last;
|
|
68
78
|
} else {
|
|
69
79
|
found = findMatchFrom(text, request.query.text, request.revision, request.offset, { ...options, wrap: request.wrap });
|
|
70
80
|
}
|
|
81
|
+
|
|
71
82
|
return { type: 'next', ...header, range: found === null ? null : { from: found.from, to: found.to } };
|
|
72
83
|
}
|
|
84
|
+
|
|
73
85
|
if (request.type === 'match') {
|
|
74
86
|
const found = iterateMatches(text, request.query.text, request.revision, { ...options, maxResults: 1 }).next().value;
|
|
87
|
+
|
|
75
88
|
return { type: 'match', ...header, match: found && found.from === request.from && found.to === request.to ? captureMatch(found) : null };
|
|
76
89
|
}
|
|
90
|
+
|
|
77
91
|
if (request.type === 'replacements') {
|
|
78
92
|
if (
|
|
79
93
|
!Number.isInteger(request.maxEdits) ||
|
|
@@ -85,58 +99,77 @@ function searchReply(request: EditorSearchInput, snapshot: EditorSearchSnapshot)
|
|
|
85
99
|
) {
|
|
86
100
|
throw new RangeError('Replacement pages need a positive edit limit and a bounded character limit.');
|
|
87
101
|
}
|
|
102
|
+
|
|
88
103
|
const ranges: number[] = [],
|
|
89
104
|
pieces: string[] = [];
|
|
105
|
+
|
|
90
106
|
let characters = 0;
|
|
91
107
|
let resume = request.from ?? 0;
|
|
92
108
|
let next: number | null = null;
|
|
109
|
+
|
|
93
110
|
for (const match of iterateMatches(text, request.query.text, request.revision, { ...options, maxResults: request.maxEdits + 1 })) {
|
|
94
111
|
if (pieces.length === request.maxEdits) {
|
|
95
112
|
next = resume;
|
|
113
|
+
|
|
96
114
|
break;
|
|
97
115
|
}
|
|
116
|
+
|
|
98
117
|
const replacement = replacementText(text, match, request.replacement, false, request.preserveCase, request.maxCharacters);
|
|
118
|
+
|
|
99
119
|
if (characters + replacement.length > request.maxCharacters) {
|
|
100
120
|
if (pieces.length === 0) {
|
|
101
121
|
throw new RangeError('One replacement exceeds the response size limit.');
|
|
102
122
|
}
|
|
123
|
+
|
|
103
124
|
next = resume;
|
|
125
|
+
|
|
104
126
|
break;
|
|
105
127
|
}
|
|
128
|
+
|
|
106
129
|
characters += replacement.length;
|
|
107
130
|
ranges.push(match.from, match.to, replacement.length);
|
|
108
131
|
pieces.push(replacement);
|
|
109
132
|
// Resuming after an empty match must skip the same full code point as the matcher.
|
|
110
133
|
resume = match.to + (match.from === match.to ? ((text.codePointAt(match.to) ?? 0) > 0xffff ? 2 : 1) : 0);
|
|
111
134
|
}
|
|
135
|
+
|
|
112
136
|
return { type: 'replacements', ...header, ranges: Uint32Array.from(ranges), text: pieces.join(''), next };
|
|
113
137
|
}
|
|
138
|
+
|
|
114
139
|
if (!Number.isInteger(request.maxResults) || request.maxResults < 0 || request.maxResults > SEARCH_LIMIT) {
|
|
115
140
|
throw new RangeError(`Search results must be limited to ${SEARCH_LIMIT} matches or fewer.`);
|
|
116
141
|
}
|
|
142
|
+
|
|
117
143
|
const offsets: number[] = [];
|
|
118
144
|
let limited = false;
|
|
145
|
+
|
|
119
146
|
for (const match of iterateMatches(text, request.query.text, request.revision, { ...options, maxResults: request.maxResults + 1 })) {
|
|
120
147
|
if (offsets.length === request.maxResults * 2) {
|
|
121
148
|
limited = true;
|
|
149
|
+
|
|
122
150
|
break;
|
|
123
151
|
}
|
|
152
|
+
|
|
124
153
|
offsets.push(match.from, match.to);
|
|
125
154
|
}
|
|
155
|
+
|
|
126
156
|
return { type: 'matches', ...header, ranges: Uint32Array.from(offsets), limited };
|
|
127
157
|
}
|
|
128
158
|
|
|
129
159
|
/* A running RegExp can only be interrupted by terminating its worker. */
|
|
130
160
|
export function serveEditorSearch(endpoint: EditorSearchWorkerEndpoint): () => void {
|
|
131
161
|
let snapshot: EditorSearchSnapshot | undefined;
|
|
162
|
+
|
|
132
163
|
const listener = ({ data: request }: MessageEvent<EditorSearchInput>): void => {
|
|
133
164
|
try {
|
|
134
165
|
if (!Number.isSafeInteger(request.revision) || request.revision < 0) {
|
|
135
166
|
throw new RangeError('The search revision must be a nonnegative safe integer.');
|
|
136
167
|
}
|
|
168
|
+
|
|
137
169
|
if (request.text !== undefined) {
|
|
138
170
|
snapshot = { revision: request.revision, text: request.text };
|
|
139
171
|
}
|
|
172
|
+
|
|
140
173
|
if (snapshot === undefined || snapshot.revision !== request.revision) {
|
|
141
174
|
endpoint.postMessage({
|
|
142
175
|
type: 'error',
|
|
@@ -145,9 +178,12 @@ export function serveEditorSearch(endpoint: EditorSearchWorkerEndpoint): () => v
|
|
|
145
178
|
kind: 'snapshot',
|
|
146
179
|
message: 'The search worker needs the text of this revision.'
|
|
147
180
|
});
|
|
181
|
+
|
|
148
182
|
return;
|
|
149
183
|
}
|
|
184
|
+
|
|
150
185
|
const response = searchReply(request, snapshot);
|
|
186
|
+
|
|
151
187
|
endpoint.postMessage(
|
|
152
188
|
response,
|
|
153
189
|
response.type === 'matches' || response.type === 'replacements' ? { transfer: [response.ranges.buffer] } : undefined
|
|
@@ -162,8 +198,10 @@ export function serveEditorSearch(endpoint: EditorSearchWorkerEndpoint): () => v
|
|
|
162
198
|
});
|
|
163
199
|
}
|
|
164
200
|
};
|
|
201
|
+
|
|
165
202
|
endpoint.addEventListener('message', listener);
|
|
166
203
|
endpoint.postMessage({ type: 'ready' });
|
|
204
|
+
|
|
167
205
|
return () => {
|
|
168
206
|
endpoint.removeEventListener('message', listener);
|
|
169
207
|
snapshot = undefined;
|