@adecore/editor 0.22.0 → 0.23.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/attribution.d.ts +9 -8
- package/dist/attribution.js +44 -9
- package/dist/bidi.d.ts +44 -0
- package/dist/bidi.js +26 -0
- package/dist/column-selection.js +2 -1
- package/dist/controller.d.ts +6 -1
- package/dist/controller.js +93 -3
- package/dist/editor.css +81 -1
- package/dist/engine.js +184 -41
- package/dist/fake.d.ts +43 -5
- package/dist/fake.js +117 -13
- package/dist/find-search.d.ts +26 -0
- package/dist/find-search.js +130 -0
- package/dist/find.d.ts +50 -2
- package/dist/find.js +575 -25
- package/dist/geometry-stops.d.ts +6 -0
- package/dist/geometry-stops.js +47 -0
- package/dist/grammar-preparation.d.ts +29 -0
- package/dist/grammar-preparation.js +147 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/keymap-table.d.ts +10 -1
- package/dist/keymap-table.js +37 -1
- package/dist/keymap.d.ts +1 -0
- package/dist/keymap.js +4 -1
- package/dist/layout-changes.d.ts +6 -0
- package/dist/layout-changes.js +27 -0
- package/dist/layout.d.ts +45 -2
- package/dist/layout.js +202 -58
- package/dist/offsets.d.ts +1 -0
- package/dist/offsets.js +23 -0
- package/dist/outline.js +5 -4
- package/dist/paint.d.ts +11 -3
- package/dist/paint.js +180 -34
- package/dist/search-client.d.ts +27 -0
- package/dist/search-client.js +210 -0
- package/dist/search-protocol.d.ts +103 -0
- package/dist/search-protocol.js +3 -0
- package/dist/search-worker.d.ts +11 -0
- package/dist/search-worker.js +137 -0
- package/dist/semantic.d.ts +2 -1
- package/dist/semantic.js +6 -1
- package/dist/shiki-document.d.ts +8 -0
- package/dist/shiki-document.js +93 -0
- package/dist/shiki-worker.d.ts +65 -0
- package/dist/shiki-worker.js +265 -0
- package/dist/shiki.d.ts +1 -0
- package/dist/shiki.js +96 -33
- package/dist/testing.d.ts +1 -0
- package/dist/tokens.d.ts +2 -0
- package/dist/tokens.js +37 -7
- package/dist/touch.d.ts +27 -0
- package/dist/touch.js +100 -0
- package/dist/types.d.ts +84 -2
- package/dist/uniform-geometry.d.ts +7 -0
- package/dist/uniform-geometry.js +49 -0
- package/dist/view.d.ts +68 -17
- package/dist/view.js +424 -97
- package/dist/worker-client.d.ts +37 -0
- package/dist/worker-client.js +195 -0
- package/package.json +17 -2
- package/src/attribution.ts +50 -14
- package/src/bidi.ts +32 -0
- package/src/column-selection.ts +2 -1
- package/src/controller.ts +97 -3
- package/src/editor.css +81 -1
- package/src/engine.ts +216 -42
- package/src/fake.ts +152 -14
- package/src/find-search.ts +165 -0
- package/src/find.ts +622 -21
- package/src/geometry-stops.ts +51 -0
- package/src/grammar-preparation.ts +158 -0
- package/src/index.ts +10 -0
- package/src/keymap-table.ts +44 -2
- package/src/keymap.ts +5 -1
- package/src/layout-changes.ts +33 -0
- package/src/layout.ts +262 -63
- package/src/offsets.ts +24 -0
- package/src/outline.ts +5 -4
- package/src/paint.ts +202 -36
- package/src/search-client.ts +306 -0
- package/src/search-protocol.ts +100 -0
- package/src/search-worker.ts +171 -0
- package/src/semantic.ts +8 -2
- package/src/shiki-document.ts +143 -0
- package/src/shiki-worker.ts +344 -0
- package/src/shiki.ts +103 -33
- package/src/testing.ts +2 -0
- package/src/tokens.ts +36 -7
- package/src/touch.ts +134 -0
- package/src/types.ts +174 -8
- package/src/uniform-geometry.ts +57 -0
- package/src/view.ts +500 -102
- package/src/worker-client.ts +212 -0
package/dist/paint.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { offsetOfStop, rowOfStop, stopAtOffset, stopCount, xOfStop } from './geometry-stops.js';
|
|
2
|
+
import { colorValue, tintValue } from './attribution.js';
|
|
3
|
+
import { scanLine, subRowBand } from './layout.js';
|
|
4
|
+
import { BIDI_CONTROLS, BIDI_PATTERN, visibleBidi } from './bidi.js';
|
|
3
5
|
const ITALIC = 1;
|
|
4
6
|
const BOLD = 2;
|
|
5
7
|
const UNDERLINE = 4;
|
|
@@ -64,36 +66,61 @@ function fillRun(run, geometry, from, to, tokens, whitespace, tokensFrom = geome
|
|
|
64
66
|
}
|
|
65
67
|
}
|
|
66
68
|
}
|
|
67
|
-
|
|
68
|
-
export function lineElement(document, geometry, tokens, lineHeight, firstRowOnly, whitespace = false) {
|
|
69
|
+
export function lineElement(document, geometry, tokens, lineHeight, band, whitespace = false) {
|
|
69
70
|
const element = document.createElement('div');
|
|
70
71
|
element.className = 'se-line';
|
|
71
72
|
element.style.width = `${geometry.width}px`;
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
let first = 0;
|
|
74
|
+
let last = geometry.runs.length;
|
|
75
|
+
while (first < last) {
|
|
76
|
+
const middle = (first + last) >>> 1;
|
|
77
|
+
if (geometry.runs[middle].subRow < band.first) {
|
|
78
|
+
first = middle + 1;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
last = middle;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
for (let index = first; index < geometry.runs.length; index++) {
|
|
85
|
+
const run = geometry.runs[index];
|
|
86
|
+
if (run.subRow > band.last) {
|
|
87
|
+
break;
|
|
75
88
|
}
|
|
76
89
|
const span = document.createElement('span');
|
|
77
90
|
span.className = 'se-run';
|
|
78
91
|
span.style.left = `${run.x}px`;
|
|
79
92
|
span.style.top = `${run.subRow * lineHeight}px`;
|
|
80
|
-
fillRun(span, geometry, run.from, run.to, tokens, whitespace);
|
|
93
|
+
fillRun(span, geometry, geometry.start + run.from, geometry.start + run.to, tokens, whitespace);
|
|
81
94
|
element.append(span);
|
|
82
95
|
}
|
|
83
96
|
if (whitespace) {
|
|
84
|
-
appendTabSigns(element, geometry, lineHeight,
|
|
97
|
+
appendTabSigns(element, geometry, lineHeight, band);
|
|
85
98
|
}
|
|
99
|
+
appendBidiSigns(element, geometry, lineHeight, band);
|
|
86
100
|
for (const box of geometry.inlays) {
|
|
87
|
-
if (
|
|
101
|
+
if (box.subRow < band.first || box.subRow > band.last) {
|
|
88
102
|
continue;
|
|
89
103
|
}
|
|
90
104
|
const span = document.createElement('span');
|
|
91
105
|
span.className = box.inlay.ghost ? 'se-ghost' : box.inlay.composition ? 'se-composition' : 'se-inlay';
|
|
92
106
|
span.dataset.inlayId = box.inlay.id;
|
|
93
|
-
|
|
94
|
-
|
|
107
|
+
const { parts } = box.inlay;
|
|
108
|
+
if (parts === undefined) {
|
|
109
|
+
span.textContent = visibleBidi(box.inlay.text);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
parts.forEach((part, index) => {
|
|
113
|
+
const element = document.createElement('span');
|
|
114
|
+
element.className = 'se-inlay-part';
|
|
115
|
+
element.dataset.part = String(index);
|
|
116
|
+
element.textContent = visibleBidi(part);
|
|
117
|
+
span.append(element);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
const gapLeft = box.inlay.gapLeft ?? 0;
|
|
121
|
+
span.style.left = `${box.x + gapLeft}px`;
|
|
95
122
|
span.style.top = `${box.subRow * lineHeight}px`;
|
|
96
|
-
span.style.width = `${box.width}px`;
|
|
123
|
+
span.style.width = `${box.width - gapLeft - (box.inlay.gapRight ?? 0)}px`;
|
|
97
124
|
element.append(span);
|
|
98
125
|
}
|
|
99
126
|
return element;
|
|
@@ -125,7 +152,10 @@ export class RowPainter {
|
|
|
125
152
|
}
|
|
126
153
|
let measured = false;
|
|
127
154
|
for (const row of rows) {
|
|
128
|
-
if (row.kind === 'text') {
|
|
155
|
+
if (row.kind === 'text' && row.replacement !== undefined) {
|
|
156
|
+
measured = this.paintReplacement(row, row.replacement, paint) || measured;
|
|
157
|
+
}
|
|
158
|
+
else if (row.kind === 'text') {
|
|
129
159
|
this.paintText(row, paint);
|
|
130
160
|
}
|
|
131
161
|
else {
|
|
@@ -137,6 +167,10 @@ export class RowPainter {
|
|
|
137
167
|
paintText(row, paint) {
|
|
138
168
|
const geometry = this.layout.geometry(row);
|
|
139
169
|
const tokens = paint.tokensOf(row.line);
|
|
170
|
+
// Block boundaries keep a scroll within the band from rebuilding its text DOM.
|
|
171
|
+
const band = geometry.subRows <= 64
|
|
172
|
+
? { first: 0, last: geometry.subRows - 1 }
|
|
173
|
+
: subRowBand(row.top, geometry.subRows, this.layout.metrics.lineHeight, paint.window, 64);
|
|
140
174
|
const folded = row.lastLine > row.line;
|
|
141
175
|
const lastLine = folded && row.tail !== null ? this.layout.document.getLine(row.lastLine) : null;
|
|
142
176
|
const tail = lastLine === null ? '' : lastLine.text.slice(row.tail - lastLine.start);
|
|
@@ -149,8 +183,10 @@ export class RowPainter {
|
|
|
149
183
|
entry.whitespace !== paint.whitespace ||
|
|
150
184
|
entry.folded !== folded ||
|
|
151
185
|
entry.tail !== tail ||
|
|
152
|
-
entry.tailTokens !== tailTokens
|
|
153
|
-
|
|
186
|
+
entry.tailTokens !== tailTokens ||
|
|
187
|
+
entry.band?.first !== band.first ||
|
|
188
|
+
entry.band?.last !== band.last) {
|
|
189
|
+
const element = this.buildText(row, geometry, tokens, paint, tail, tailTokens, band);
|
|
154
190
|
if (entry) {
|
|
155
191
|
entry.element.replaceWith(element);
|
|
156
192
|
}
|
|
@@ -166,7 +202,8 @@ export class RowPainter {
|
|
|
166
202
|
folded,
|
|
167
203
|
tail,
|
|
168
204
|
tailTokens,
|
|
169
|
-
revision: 0
|
|
205
|
+
revision: 0,
|
|
206
|
+
band
|
|
170
207
|
};
|
|
171
208
|
this.rows.set(row.key, entry);
|
|
172
209
|
}
|
|
@@ -174,11 +211,11 @@ export class RowPainter {
|
|
|
174
211
|
entry.element.style.height = `${row.height}px`;
|
|
175
212
|
entry.element.dataset.line = String(row.line);
|
|
176
213
|
}
|
|
177
|
-
buildText(row, geometry, tokens, paint, tail, tailTokens) {
|
|
214
|
+
buildText(row, geometry, tokens, paint, tail, tailTokens, band) {
|
|
178
215
|
const document = this.code.ownerDocument;
|
|
179
216
|
const lineHeight = this.layout.metrics.lineHeight;
|
|
180
|
-
const element = lineElement(document, geometry, tokens, lineHeight,
|
|
181
|
-
if (row.lastLine > row.line) {
|
|
217
|
+
const element = lineElement(document, geometry, tokens, lineHeight, band, paint.whitespace);
|
|
218
|
+
if (row.lastLine > row.line && band.last === geometry.subRows - 1) {
|
|
182
219
|
const chip = document.createElement('button');
|
|
183
220
|
chip.type = 'button';
|
|
184
221
|
chip.className = 'se-fold-chip';
|
|
@@ -208,9 +245,42 @@ export class RowPainter {
|
|
|
208
245
|
span.className = 'se-run';
|
|
209
246
|
span.style.left = `${left + run.x}px`;
|
|
210
247
|
span.style.top = `${(header.subRows - 1) * this.layout.metrics.lineHeight}px`;
|
|
211
|
-
fillRun(span, geometry, run.from, run.to, tokens, false, lineStart);
|
|
248
|
+
fillRun(span, geometry, geometry.start + run.from, geometry.start + run.to, tokens, false, lineStart);
|
|
212
249
|
element.append(span);
|
|
213
250
|
}
|
|
251
|
+
appendBidiSigns(element, geometry, this.layout.metrics.lineHeight, { first: 0, last: 0 }, left, (header.subRows - 1) * this.layout.metrics.lineHeight);
|
|
252
|
+
}
|
|
253
|
+
/* The lines the host draws itself: an element of its own, indented as the first line is and as tall as its content. */
|
|
254
|
+
paintReplacement(row, replacement, paint) {
|
|
255
|
+
const revision = replacement.revision ?? 0;
|
|
256
|
+
let entry = this.rows.get(row.key);
|
|
257
|
+
if (!entry) {
|
|
258
|
+
const element = this.code.ownerDocument.createElement('div');
|
|
259
|
+
element.className = 'se-block se-replacement';
|
|
260
|
+
element.dataset.rowKey = row.key;
|
|
261
|
+
element.dataset.replacementId = replacement.id;
|
|
262
|
+
element.setAttribute('role', 'group');
|
|
263
|
+
if (replacement.label !== undefined) {
|
|
264
|
+
element.setAttribute('aria-label', replacement.label);
|
|
265
|
+
}
|
|
266
|
+
replacement.render(element);
|
|
267
|
+
this.code.append(element);
|
|
268
|
+
entry = { element, text: '', tokens: null, layoutVersion: 0, whitespace: false, folded: false, tail: '', tailTokens: null, revision };
|
|
269
|
+
this.rows.set(row.key, entry);
|
|
270
|
+
}
|
|
271
|
+
else if (entry.revision !== revision) {
|
|
272
|
+
entry.element.replaceChildren();
|
|
273
|
+
replacement.render(entry.element);
|
|
274
|
+
entry.revision = revision;
|
|
275
|
+
}
|
|
276
|
+
const indent = this.layout.indentX(row.line);
|
|
277
|
+
entry.element.style.top = `${row.top}px`;
|
|
278
|
+
entry.element.style.left = `${indent}px`;
|
|
279
|
+
entry.element.style.width = `${Math.max(80, paint.viewportWidth - indent)}px`;
|
|
280
|
+
entry.element.style.height = '';
|
|
281
|
+
this.observer?.observe(entry.element);
|
|
282
|
+
const height = entry.element.getBoundingClientRect?.().height ?? 0;
|
|
283
|
+
return height > 0 && this.layout.setMeasuredHeight(row.key, height);
|
|
214
284
|
}
|
|
215
285
|
paintBlock(row, paint) {
|
|
216
286
|
const { widget } = row;
|
|
@@ -223,7 +293,7 @@ export class RowPainter {
|
|
|
223
293
|
element.dataset.widgetId = widget.id;
|
|
224
294
|
this.fillBlock(element, widget);
|
|
225
295
|
this.code.append(element);
|
|
226
|
-
if (!widget.lens) {
|
|
296
|
+
if (!widget.lens && !widget.fixed) {
|
|
227
297
|
this.observer?.observe(element);
|
|
228
298
|
}
|
|
229
299
|
entry = { element, text: '', tokens: null, layoutVersion: 0, whitespace: false, folded: false, tail: '', tailTokens: null, revision };
|
|
@@ -247,6 +317,13 @@ export class RowPainter {
|
|
|
247
317
|
else {
|
|
248
318
|
entry.element.style.width = `${Math.max(80, paint.viewportWidth)}px`;
|
|
249
319
|
}
|
|
320
|
+
if (widget.fixed) {
|
|
321
|
+
this.observer?.unobserve(entry.element);
|
|
322
|
+
entry.element.style.height = `${row.height}px`;
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
entry.element.style.height = '';
|
|
326
|
+
this.observer?.observe(entry.element);
|
|
250
327
|
const height = entry.element.getBoundingClientRect?.().height ?? 0;
|
|
251
328
|
return height > 0 && this.layout.setMeasuredHeight(row.key, height);
|
|
252
329
|
}
|
|
@@ -286,13 +363,33 @@ export function paintGutter(container, layout, rows, paint) {
|
|
|
286
363
|
item.className = paint.activeLines.has(row.line) ? 'se-line-number se-active-number' : 'se-line-number';
|
|
287
364
|
item.style.top = `${row.top}px`;
|
|
288
365
|
item.style.height = `${layout.metrics.lineHeight}px`;
|
|
289
|
-
const
|
|
366
|
+
const highlight = paint.highlights.get(row.line);
|
|
367
|
+
const fill = highlight?.fill;
|
|
290
368
|
if (fill !== undefined) {
|
|
291
369
|
item.style.background = colorValue(fill);
|
|
292
370
|
}
|
|
371
|
+
if (highlight?.bar !== undefined) {
|
|
372
|
+
item.classList.add('se-line-marked');
|
|
373
|
+
item.style.height = `${row.height}px`;
|
|
374
|
+
item.style.setProperty('--se-line', `${layout.metrics.lineHeight}px`);
|
|
375
|
+
item.style.setProperty('--se-mark', colorValue(highlight.color));
|
|
376
|
+
if (fill === undefined) {
|
|
377
|
+
item.style.setProperty('--se-tint', tintValue(highlight.color));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
293
380
|
const number = document.createElement('span');
|
|
294
|
-
number.textContent =
|
|
381
|
+
number.textContent = paint.lineNumber(row.line);
|
|
382
|
+
const label = paint.lineLabel(row.line);
|
|
383
|
+
if (label !== null) {
|
|
384
|
+
number.setAttribute('aria-hidden', 'true');
|
|
385
|
+
}
|
|
295
386
|
item.append(number);
|
|
387
|
+
if (label !== null) {
|
|
388
|
+
const hidden = document.createElement('span');
|
|
389
|
+
hidden.className = 'se-sr-only';
|
|
390
|
+
hidden.textContent = label;
|
|
391
|
+
item.append(hidden);
|
|
392
|
+
}
|
|
296
393
|
if (paint.action?.line === row.line) {
|
|
297
394
|
const button = gutterButton(document, 'se-gutter-action', paint.action.label);
|
|
298
395
|
button.dataset.gutterAction = String(row.line);
|
|
@@ -302,6 +399,12 @@ export function paintGutter(container, layout, rows, paint) {
|
|
|
302
399
|
if (marker !== undefined) {
|
|
303
400
|
const button = gutterButton(document, 'se-gutter-marker', marker.label);
|
|
304
401
|
button.dataset.gutterMarker = marker.id;
|
|
402
|
+
if (marker.icon !== undefined) {
|
|
403
|
+
button.dataset.icon = marker.icon;
|
|
404
|
+
}
|
|
405
|
+
if (marker.pressed !== undefined) {
|
|
406
|
+
button.setAttribute('aria-pressed', String(marker.pressed));
|
|
407
|
+
}
|
|
305
408
|
item.append(button);
|
|
306
409
|
}
|
|
307
410
|
const sign = paint.highlights.get(row.line)?.sign;
|
|
@@ -309,9 +412,18 @@ export function paintGutter(container, layout, rows, paint) {
|
|
|
309
412
|
const mark = document.createElement('span');
|
|
310
413
|
mark.className = 'se-line-sign';
|
|
311
414
|
mark.textContent = sign;
|
|
415
|
+
if (label !== null) {
|
|
416
|
+
mark.setAttribute('aria-hidden', 'true');
|
|
417
|
+
}
|
|
312
418
|
mark.style.color = colorValue(paint.highlights.get(row.line).color);
|
|
313
419
|
item.append(mark);
|
|
314
420
|
}
|
|
421
|
+
if (highlight?.bar !== undefined) {
|
|
422
|
+
const mark = document.createElement('span');
|
|
423
|
+
mark.className = `se-line-bar se-line-bar-${highlight.bar}`;
|
|
424
|
+
mark.setAttribute('aria-hidden', 'true');
|
|
425
|
+
item.append(mark);
|
|
426
|
+
}
|
|
315
427
|
const bar = paint.attribution.get(row.line);
|
|
316
428
|
if (bar) {
|
|
317
429
|
const mark = document.createElement('span');
|
|
@@ -323,7 +435,7 @@ export function paintGutter(container, layout, rows, paint) {
|
|
|
323
435
|
}
|
|
324
436
|
const change = paint.changes.get(row.line);
|
|
325
437
|
// The bar of an agent stands where the mark of a change would, and a removal is a triangle between lines that the bar never hides.
|
|
326
|
-
if (change && (!bar || change === 'deleted')) {
|
|
438
|
+
if (change && (!bar || change === 'deleted') && highlight?.bar === undefined) {
|
|
327
439
|
const mark = document.createElement('span');
|
|
328
440
|
mark.className = `se-change se-change-${change}`;
|
|
329
441
|
if (change !== 'deleted') {
|
|
@@ -333,7 +445,8 @@ export function paintGutter(container, layout, rows, paint) {
|
|
|
333
445
|
}
|
|
334
446
|
const collapsed = paint.foldable.get(row.line);
|
|
335
447
|
if (collapsed !== undefined) {
|
|
336
|
-
const button = gutterButton(document, collapsed ? 'se-fold-toggle se-folded' : 'se-fold-toggle', collapsed
|
|
448
|
+
const button = gutterButton(document, collapsed ? 'se-fold-toggle se-folded' : 'se-fold-toggle', paint.foldName(row.line, collapsed));
|
|
449
|
+
button.setAttribute('aria-expanded', String(!collapsed));
|
|
337
450
|
button.dataset.foldLine = String(row.line);
|
|
338
451
|
item.append(button);
|
|
339
452
|
}
|
|
@@ -415,10 +528,10 @@ export function paintSticky(container, entries, lineHeight) {
|
|
|
415
528
|
row.dataset.depth = String(depth);
|
|
416
529
|
const number = document.createElement('span');
|
|
417
530
|
number.className = 'se-sticky-number';
|
|
418
|
-
number.textContent =
|
|
531
|
+
number.textContent = entry.label;
|
|
419
532
|
const code = document.createElement('div');
|
|
420
533
|
code.className = 'se-sticky-code';
|
|
421
|
-
code.append(lineElement(document, entry.geometry, entry.tokens, lineHeight,
|
|
534
|
+
code.append(lineElement(document, entry.geometry, entry.tokens, lineHeight, { first: 0, last: 0 }));
|
|
422
535
|
row.append(number, code);
|
|
423
536
|
return row;
|
|
424
537
|
}));
|
|
@@ -448,20 +561,53 @@ export function arrowSign(document, width, lineHeight, className) {
|
|
|
448
561
|
return svg;
|
|
449
562
|
}
|
|
450
563
|
/* An arrow over each tab of a line. */
|
|
451
|
-
function appendTabSigns(element, geometry, lineHeight,
|
|
564
|
+
function appendTabSigns(element, geometry, lineHeight, band) {
|
|
565
|
+
if (geometry.uniform !== undefined) {
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
452
568
|
const document = element.ownerDocument;
|
|
453
|
-
|
|
454
|
-
|
|
569
|
+
const end = geometry.rowStarts[band.last + 1] ?? stopCount(geometry) - 1;
|
|
570
|
+
for (let stop = geometry.rowStarts[band.first]; stop < end; stop++) {
|
|
571
|
+
if (geometry.text[offsetOfStop(geometry, stop)] !== '\t') {
|
|
455
572
|
continue;
|
|
456
573
|
}
|
|
457
|
-
const left = geometry
|
|
458
|
-
const right = geometry
|
|
574
|
+
const left = xOfStop(geometry, stop);
|
|
575
|
+
const right = rowOfStop(geometry, stop + 1) === rowOfStop(geometry, stop) ? xOfStop(geometry, stop + 1) : geometry.rowEnds[rowOfStop(geometry, stop)];
|
|
459
576
|
if (right - left < 3) {
|
|
460
577
|
continue;
|
|
461
578
|
}
|
|
462
579
|
const sign = arrowSign(document, right - left - 2, lineHeight, 'se-tab-sign');
|
|
463
580
|
sign.style.left = `${left + 1}px`;
|
|
464
|
-
sign.style.top = `${geometry
|
|
581
|
+
sign.style.top = `${rowOfStop(geometry, stop) * lineHeight}px`;
|
|
582
|
+
element.append(sign);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
/*
|
|
586
|
+
* A boxed label over each bidirectional control character, which no run carries, so the control never reaches the text nodes
|
|
587
|
+
* where it would reorder the line. `top` and `originX` place a line that is drawn as part of another one.
|
|
588
|
+
*/
|
|
589
|
+
function appendBidiSigns(element, geometry, lineHeight, band, originX = 0, top) {
|
|
590
|
+
if (geometry.uniform !== undefined) {
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
const document = element.ownerDocument;
|
|
594
|
+
const from = offsetOfStop(geometry, geometry.rowStarts[band.first]);
|
|
595
|
+
const to = geometry.rowStarts[band.last + 1] === undefined ? geometry.text.length : offsetOfStop(geometry, geometry.rowStarts[band.last + 1]);
|
|
596
|
+
const pattern = new RegExp(BIDI_PATTERN.source, 'g');
|
|
597
|
+
pattern.lastIndex = from;
|
|
598
|
+
for (let match = pattern.exec(geometry.text); match !== null && match.index < to; match = pattern.exec(geometry.text)) {
|
|
599
|
+
const stop = stopAtOffset(geometry, match.index);
|
|
600
|
+
const row = rowOfStop(geometry, stop);
|
|
601
|
+
const control = BIDI_CONTROLS[match[0]];
|
|
602
|
+
const left = xOfStop(geometry, stop);
|
|
603
|
+
const right = rowOfStop(geometry, stop + 1) === row ? xOfStop(geometry, stop + 1) : geometry.rowEnds[row];
|
|
604
|
+
const sign = document.createElement('span');
|
|
605
|
+
sign.className = 'se-bidi';
|
|
606
|
+
sign.textContent = control.label;
|
|
607
|
+
sign.style.left = `${originX + left}px`;
|
|
608
|
+
sign.style.top = `${top ?? row * lineHeight}px`;
|
|
609
|
+
sign.style.width = `${right - left}px`;
|
|
610
|
+
sign.style.height = `${lineHeight}px`;
|
|
465
611
|
element.append(sign);
|
|
466
612
|
}
|
|
467
613
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { FindMatch } from '@adecore/editor-core';
|
|
2
|
+
import type { EditorFindQuery } from './types.ts';
|
|
3
|
+
import { type EditorSearchRange, type EditorSearchNextOptions, type EditorSearchReplacementOptions, type EditorSearchReplacements, type EditorSearchOptions, type EditorSearchResult, type EditorSearchSnapshot } from './search-protocol.ts';
|
|
4
|
+
export type EditorSearchErrorKind = 'timeout' | 'superseded' | 'unavailable' | 'syntax' | 'range' | 'snapshot' | 'error' | 'disposed';
|
|
5
|
+
export declare class EditorSearchError extends Error {
|
|
6
|
+
readonly kind: EditorSearchErrorKind;
|
|
7
|
+
constructor(kind: EditorSearchErrorKind, message: string);
|
|
8
|
+
}
|
|
9
|
+
export interface EditorSearchClientOptions {
|
|
10
|
+
readonly workerFactory: () => Pick<Worker, 'addEventListener' | 'removeEventListener' | 'postMessage' | 'terminate'>;
|
|
11
|
+
readonly deadline?: number;
|
|
12
|
+
readonly schedule?: (callback: () => void, delay: number) => () => void;
|
|
13
|
+
}
|
|
14
|
+
export interface EditorSearchClient {
|
|
15
|
+
search(snapshot: EditorSearchSnapshot, query: EditorFindQuery, options?: EditorSearchOptions): Promise<EditorSearchResult>;
|
|
16
|
+
match(snapshot: EditorSearchSnapshot, query: EditorFindQuery, range: {
|
|
17
|
+
readonly from: number;
|
|
18
|
+
readonly to: number;
|
|
19
|
+
readonly deadline?: number;
|
|
20
|
+
}): Promise<FindMatch | null>;
|
|
21
|
+
next(snapshot: EditorSearchSnapshot, query: EditorFindQuery, offset: number, options?: EditorSearchNextOptions): Promise<EditorSearchRange | null>;
|
|
22
|
+
replacements(snapshot: EditorSearchSnapshot, query: EditorFindQuery, replacement: string, options?: EditorSearchReplacementOptions): Promise<EditorSearchReplacements>;
|
|
23
|
+
cancel(): void;
|
|
24
|
+
release(): void;
|
|
25
|
+
dispose(): void;
|
|
26
|
+
}
|
|
27
|
+
export declare function createSearchClient(options: EditorSearchClientOptions): EditorSearchClient;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { createWorkerRequestClient, WorkerRequestError } from './worker-client.js';
|
|
2
|
+
import { SEARCH_LIMIT, MAX_REPLACEMENT_PAGE, MAX_REPLACEMENT_CHARACTERS } from './search-protocol.js';
|
|
3
|
+
export class EditorSearchError extends Error {
|
|
4
|
+
kind;
|
|
5
|
+
constructor(kind, message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'EditorSearchError';
|
|
8
|
+
this.kind = kind;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function validateSearch(snapshot, query, options) {
|
|
12
|
+
const from = options.from ?? 0;
|
|
13
|
+
const to = options.to ?? snapshot.text.length;
|
|
14
|
+
const maximum = options.maxResults ?? SEARCH_LIMIT;
|
|
15
|
+
if (!Number.isSafeInteger(snapshot.revision) ||
|
|
16
|
+
snapshot.revision < 0 ||
|
|
17
|
+
!Number.isInteger(from) ||
|
|
18
|
+
!Number.isInteger(to) ||
|
|
19
|
+
from < 0 ||
|
|
20
|
+
to < from ||
|
|
21
|
+
to > snapshot.text.length) {
|
|
22
|
+
throw new EditorSearchError('range', 'Search bounds and revision must identify the current document.');
|
|
23
|
+
}
|
|
24
|
+
if (!Number.isInteger(maximum) || maximum < 0 || maximum > SEARCH_LIMIT) {
|
|
25
|
+
throw new EditorSearchError('range', `Search results must be limited to ${SEARCH_LIMIT} matches or fewer.`);
|
|
26
|
+
}
|
|
27
|
+
if (options.deadline !== undefined && (!Number.isFinite(options.deadline) || options.deadline <= 0 || options.deadline > 2147483647)) {
|
|
28
|
+
throw new EditorSearchError('range', 'A request deadline must fit a positive browser timer.');
|
|
29
|
+
}
|
|
30
|
+
if (query.regex) {
|
|
31
|
+
try {
|
|
32
|
+
// Compilation reports syntax errors without executing the expression on the page.
|
|
33
|
+
new RegExp(query.text, `gmu${query.caseSensitive ? '' : 'i'}`);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
throw new EditorSearchError('syntax', error instanceof Error ? error.message : String(error));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function splitsSurrogate(text, offset) {
|
|
41
|
+
const previous = text.charCodeAt(offset - 1), current = text.charCodeAt(offset);
|
|
42
|
+
return previous >= 0xd800 && previous <= 0xdbff && current >= 0xdc00 && current <= 0xdfff;
|
|
43
|
+
}
|
|
44
|
+
export function createSearchClient(options) {
|
|
45
|
+
let revision;
|
|
46
|
+
const transport = createWorkerRequestClient({
|
|
47
|
+
...options,
|
|
48
|
+
accepts: (reply, input) => reply.revision === input.revision,
|
|
49
|
+
failure: (reply) => reply.type === 'error'
|
|
50
|
+
? { error: new EditorSearchError(reply.kind, reply.message), restart: reply.kind !== 'syntax' && reply.kind !== 'range' }
|
|
51
|
+
: undefined,
|
|
52
|
+
prepare: (input) => {
|
|
53
|
+
const { text, ...rest } = input;
|
|
54
|
+
const message = { ...rest, ...(revision !== input.revision ? { text } : {}) };
|
|
55
|
+
revision = input.revision;
|
|
56
|
+
return message;
|
|
57
|
+
},
|
|
58
|
+
stopped: () => {
|
|
59
|
+
revision = undefined;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
const run = (snapshot, query, bounds, input, read, validate) => {
|
|
63
|
+
return transport
|
|
64
|
+
.request(input, bounds.deadline, () => {
|
|
65
|
+
validateSearch(snapshot, query, bounds);
|
|
66
|
+
validate?.();
|
|
67
|
+
})
|
|
68
|
+
.then((reply) => {
|
|
69
|
+
if (reply.type === 'error') {
|
|
70
|
+
throw new EditorSearchError(reply.kind, reply.message);
|
|
71
|
+
}
|
|
72
|
+
return read(reply);
|
|
73
|
+
})
|
|
74
|
+
.catch((error) => {
|
|
75
|
+
throw error instanceof WorkerRequestError
|
|
76
|
+
? new EditorSearchError(error.kind, error.message)
|
|
77
|
+
: error instanceof EditorSearchError
|
|
78
|
+
? error
|
|
79
|
+
: new EditorSearchError('error', error instanceof Error ? error.message : String(error));
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
return {
|
|
83
|
+
search(snapshot, query, bounds = {}) {
|
|
84
|
+
return run(snapshot, query, bounds, (id) => ({
|
|
85
|
+
type: 'search',
|
|
86
|
+
id,
|
|
87
|
+
revision: snapshot.revision,
|
|
88
|
+
text: snapshot.text,
|
|
89
|
+
query: { ...query },
|
|
90
|
+
...bounds,
|
|
91
|
+
maxResults: bounds.maxResults ?? SEARCH_LIMIT
|
|
92
|
+
}), (reply) => {
|
|
93
|
+
if (reply.type !== 'matches') {
|
|
94
|
+
throw new EditorSearchError('error', 'The worker returned an unexpected search response.');
|
|
95
|
+
}
|
|
96
|
+
return { revision: reply.revision, ranges: reply.ranges, limited: reply.limited };
|
|
97
|
+
});
|
|
98
|
+
},
|
|
99
|
+
match(snapshot, query, range) {
|
|
100
|
+
const from = range.from, to = range.to;
|
|
101
|
+
const text = snapshot.text;
|
|
102
|
+
return run(snapshot, query, { from, to, maxResults: 1, deadline: range.deadline }, (id) => ({ type: 'match', id, revision: snapshot.revision, text, query: { ...query }, from, to }), (reply) => {
|
|
103
|
+
if (reply.type !== 'match') {
|
|
104
|
+
throw new EditorSearchError('error', 'The worker returned an unexpected match response.');
|
|
105
|
+
}
|
|
106
|
+
if (reply.match === null) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
if (reply.match.from !== from || reply.match.to !== to) {
|
|
110
|
+
throw new EditorSearchError('error', 'The worker returned a different match range.');
|
|
111
|
+
}
|
|
112
|
+
return { ...reply.match, text: text.slice(from, to), revision: reply.revision };
|
|
113
|
+
});
|
|
114
|
+
},
|
|
115
|
+
next(snapshot, query, offset, bounds = {}) {
|
|
116
|
+
return run(snapshot, query, { from: bounds.from, to: bounds.to, maxResults: 1, deadline: bounds.deadline }, (id) => ({ type: 'next', id, revision: snapshot.revision, text: snapshot.text, query: { ...query }, offset, ...bounds }), (reply) => {
|
|
117
|
+
if (reply.type !== 'next') {
|
|
118
|
+
throw new EditorSearchError('error', 'The worker returned an unexpected cursor response.');
|
|
119
|
+
}
|
|
120
|
+
const range = reply.range;
|
|
121
|
+
if (range !== null &&
|
|
122
|
+
(!Number.isInteger(range.from) ||
|
|
123
|
+
!Number.isInteger(range.to) ||
|
|
124
|
+
range.from < (bounds.from ?? 0) ||
|
|
125
|
+
range.to < range.from ||
|
|
126
|
+
range.to > (bounds.to ?? snapshot.text.length) ||
|
|
127
|
+
splitsSurrogate(snapshot.text, range.from) ||
|
|
128
|
+
splitsSurrogate(snapshot.text, range.to))) {
|
|
129
|
+
throw new EditorSearchError('error', 'The worker returned invalid cursor bounds.');
|
|
130
|
+
}
|
|
131
|
+
return range;
|
|
132
|
+
});
|
|
133
|
+
},
|
|
134
|
+
replacements(snapshot, query, replacement, options = {}) {
|
|
135
|
+
const { from, to, preserveCase } = options;
|
|
136
|
+
const maxEdits = options.maxEdits ?? 1000;
|
|
137
|
+
const maxCharacters = options.maxCharacters ?? MAX_REPLACEMENT_CHARACTERS;
|
|
138
|
+
return run(snapshot, query, { from, to, maxResults: 1, deadline: options.deadline }, (id) => ({
|
|
139
|
+
type: 'replacements',
|
|
140
|
+
id,
|
|
141
|
+
revision: snapshot.revision,
|
|
142
|
+
text: snapshot.text,
|
|
143
|
+
query: { ...query },
|
|
144
|
+
from,
|
|
145
|
+
to,
|
|
146
|
+
preserveCase,
|
|
147
|
+
replacement,
|
|
148
|
+
maxEdits,
|
|
149
|
+
maxCharacters
|
|
150
|
+
}), (reply) => {
|
|
151
|
+
if (reply.type !== 'replacements') {
|
|
152
|
+
throw new EditorSearchError('error', 'The worker returned an unexpected replacement response.');
|
|
153
|
+
}
|
|
154
|
+
if (reply.ranges.length % 3 !== 0 || reply.ranges.length / 3 > maxEdits || reply.text.length > maxCharacters) {
|
|
155
|
+
throw new EditorSearchError('error', 'The worker returned an invalid replacement page.');
|
|
156
|
+
}
|
|
157
|
+
let characters = 0;
|
|
158
|
+
let previous = from ?? 0;
|
|
159
|
+
let previousEmpty = false;
|
|
160
|
+
for (let index = 0; index < reply.ranges.length; index += 3) {
|
|
161
|
+
const start = reply.ranges[index], end = reply.ranges[index + 1];
|
|
162
|
+
if (start < previous ||
|
|
163
|
+
(previousEmpty && start === previous) ||
|
|
164
|
+
end < start ||
|
|
165
|
+
end > (to ?? snapshot.text.length) ||
|
|
166
|
+
splitsSurrogate(snapshot.text, start) ||
|
|
167
|
+
splitsSurrogate(snapshot.text, end)) {
|
|
168
|
+
throw new EditorSearchError('error', 'The worker returned invalid replacement bounds.');
|
|
169
|
+
}
|
|
170
|
+
previous = end;
|
|
171
|
+
previousEmpty = start === end;
|
|
172
|
+
characters += reply.ranges[index + 2];
|
|
173
|
+
}
|
|
174
|
+
if (characters !== reply.text.length) {
|
|
175
|
+
throw new EditorSearchError('error', 'The worker returned inconsistent replacement lengths.');
|
|
176
|
+
}
|
|
177
|
+
if (reply.next !== null) {
|
|
178
|
+
const index = reply.ranges.length - 3;
|
|
179
|
+
if (index < 0) {
|
|
180
|
+
throw new EditorSearchError('error', 'The worker returned an empty replacement continuation.');
|
|
181
|
+
}
|
|
182
|
+
const start = reply.ranges[index], end = reply.ranges[index + 1];
|
|
183
|
+
const resume = end + (start === end ? ((snapshot.text.codePointAt(end) ?? 0) > 0xffff ? 2 : 1) : 0);
|
|
184
|
+
if (reply.next !== resume || reply.next > (to ?? snapshot.text.length)) {
|
|
185
|
+
throw new EditorSearchError('error', 'The worker returned an invalid replacement continuation.');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return { revision: reply.revision, ranges: reply.ranges, text: reply.text, next: reply.next };
|
|
189
|
+
}, () => {
|
|
190
|
+
if (!Number.isInteger(maxEdits) ||
|
|
191
|
+
maxEdits < 1 ||
|
|
192
|
+
maxEdits > MAX_REPLACEMENT_PAGE ||
|
|
193
|
+
!Number.isInteger(maxCharacters) ||
|
|
194
|
+
maxCharacters < 0 ||
|
|
195
|
+
maxCharacters > MAX_REPLACEMENT_CHARACTERS) {
|
|
196
|
+
throw new EditorSearchError('range', 'Replacement pages need a positive edit limit and a bounded character limit.');
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
},
|
|
200
|
+
cancel() {
|
|
201
|
+
transport.cancel();
|
|
202
|
+
},
|
|
203
|
+
release() {
|
|
204
|
+
transport.release();
|
|
205
|
+
},
|
|
206
|
+
dispose() {
|
|
207
|
+
transport.dispose();
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|