@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.
Files changed (53) hide show
  1. package/dist/controller.d.ts +1 -0
  2. package/dist/controller.js +17 -28
  3. package/dist/engine.js +2 -9
  4. package/dist/find.js +12 -13
  5. package/dist/layout.js +7 -2
  6. package/dist/paint.d.ts +2 -0
  7. package/dist/paint.js +23 -20
  8. package/dist/view.d.ts +3 -2
  9. package/dist/view.js +46 -59
  10. package/package.json +6 -6
  11. package/src/attribution.ts +14 -0
  12. package/src/code-block.ts +25 -0
  13. package/src/column-selection.ts +10 -0
  14. package/src/controller.ts +189 -28
  15. package/src/engine.ts +87 -9
  16. package/src/fake.ts +79 -0
  17. package/src/find-search.ts +20 -0
  18. package/src/find.ts +135 -14
  19. package/src/geometry-stops.ts +10 -0
  20. package/src/grammar-preparation.ts +23 -0
  21. package/src/index.ts +1 -0
  22. package/src/input.ts +13 -0
  23. package/src/keymap-table.ts +9 -0
  24. package/src/keymap.ts +25 -0
  25. package/src/layout-changes.ts +7 -0
  26. package/src/layout.ts +219 -3
  27. package/src/lens.ts +3 -0
  28. package/src/listeners.ts +1 -0
  29. package/src/metrics.ts +14 -0
  30. package/src/modifier-gesture.ts +17 -0
  31. package/src/native-input.ts +27 -0
  32. package/src/offsets.ts +10 -0
  33. package/src/outline.ts +38 -0
  34. package/src/overview.ts +13 -0
  35. package/src/paint.ts +140 -20
  36. package/src/php-html.ts +1 -0
  37. package/src/pointer.ts +50 -0
  38. package/src/scroll-animation.ts +10 -0
  39. package/src/scroll.ts +7 -0
  40. package/src/search-client.ts +32 -0
  41. package/src/search-worker.ts +38 -0
  42. package/src/semantic.ts +12 -0
  43. package/src/shiki-document.ts +19 -0
  44. package/src/shiki-worker.ts +68 -0
  45. package/src/shiki.ts +37 -0
  46. package/src/testing.ts +17 -0
  47. package/src/theme-scopes.ts +14 -0
  48. package/src/tokens.ts +31 -0
  49. package/src/touch.ts +16 -0
  50. package/src/tracked-range.ts +5 -0
  51. package/src/uniform-geometry.ts +10 -0
  52. package/src/view.ts +481 -75
  53. 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
  }
@@ -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)
@@ -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
  () => {
@@ -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;