@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/find.ts CHANGED
@@ -105,33 +105,42 @@ export class FindController {
105
105
  matchesBetween(from: number, to: number): readonly FindMark[] {
106
106
  let low = 0;
107
107
  let high = this.marks.length;
108
+
108
109
  while (low < high) {
109
110
  const middle = (low + high) >>> 1;
111
+
110
112
  if (this.marks[middle]!.to < from) {
111
113
  low = middle + 1;
112
114
  } else {
113
115
  high = middle;
114
116
  }
115
117
  }
118
+
116
119
  const first = low;
117
120
  high = this.marks.length;
121
+
118
122
  while (low < high) {
119
123
  const middle = (low + high) >>> 1;
124
+
120
125
  if (this.marks[middle]!.from <= to) {
121
126
  low = middle + 1;
122
127
  } else {
123
128
  high = middle;
124
129
  }
125
130
  }
131
+
126
132
  return this.marks.slice(first, low);
127
133
  }
128
134
 
129
135
  get currentMatch(): FindMatch | null {
130
136
  if (this.search !== undefined && this.query?.regex) {
131
137
  const mark = this.currentMark;
138
+
132
139
  return this.freshRegex && this.retained?.from === mark?.from && this.retained?.to === mark?.to ? this.retained : null;
133
140
  }
141
+
134
142
  const matches = this.cachedMatches;
143
+
135
144
  if (this.outside !== null && matches !== null) {
136
145
  return {
137
146
  ...this.outside,
@@ -141,6 +150,7 @@ export class FindController {
141
150
  revision: this.model.getRevision()
142
151
  };
143
152
  }
153
+
144
154
  return this.index === null || matches === null ? null : (matches[this.index] ?? null);
145
155
  }
146
156
 
@@ -155,6 +165,7 @@ export class FindController {
155
165
  private cacheKey(): string {
156
166
  const query = this.query;
157
167
  const scope = this.scope;
168
+
158
169
  return JSON.stringify([
159
170
  query?.text,
160
171
  query?.caseSensitive === true,
@@ -171,15 +182,18 @@ export class FindController {
171
182
  const oldKey = this.cacheKey();
172
183
  const wasInSelection = this.query?.inSelection === true;
173
184
  this.query = query === null || query.text === '' ? null : { ...query };
185
+
174
186
  if (this.query?.inSelection !== true) {
175
187
  this.bounds = null;
176
188
  } else if (!wasInSelection) {
177
189
  const selection = this.model.getPrimary();
190
+
178
191
  this.bounds =
179
192
  selection.anchor === selection.head
180
193
  ? null
181
194
  : { from: Math.min(selection.anchor, selection.head), to: Math.max(selection.anchor, selection.head) };
182
195
  }
196
+
183
197
  if (this.cacheKey() !== oldKey) {
184
198
  this.pendingAnchor = undefined;
185
199
  this.pendingStep = undefined;
@@ -187,6 +201,7 @@ export class FindController {
187
201
  this.replaceFailed = undefined;
188
202
  this.replaced = undefined;
189
203
  }
204
+
190
205
  this.refresh();
191
206
  }
192
207
 
@@ -195,6 +210,7 @@ export class FindController {
195
210
  if (this.pendingAnchor !== undefined) {
196
211
  this.pendingAnchor = mapOffset(this.pendingAnchor, changes);
197
212
  }
213
+
198
214
  if (this.bounds !== null) {
199
215
  this.bounds = { from: mapOffset(this.bounds.from, changes), to: mapOffset(this.bounds.to, changes) };
200
216
  }
@@ -209,26 +225,35 @@ export class FindController {
209
225
  this.revision = this.model.getRevision();
210
226
  this.replaced = undefined;
211
227
  }
228
+
212
229
  const previous = this.currentMark;
213
230
  const query = this.query;
231
+
214
232
  if (this.search !== undefined && query?.regex) {
215
233
  this.refreshRegex(previous);
234
+
216
235
  return;
217
236
  }
237
+
218
238
  this.resetRegex(query === null || this.search?.busy === true);
219
239
 
220
240
  if (query === null) {
221
241
  this.marks = [];
222
242
  this.cache = undefined;
223
243
  this.index = null;
244
+
224
245
  return;
225
246
  }
247
+
226
248
  const revision = this.model.getRevision();
227
249
  const key = this.cacheKey();
250
+
228
251
  if (this.cache?.revision !== revision || this.cache.key !== key) {
229
252
  let found: FindMatch[] = [];
253
+
230
254
  try {
231
255
  const inSelection = query.inSelection === true;
256
+
232
257
  if (!inSelection || this.bounds !== null) {
233
258
  found = this.model.find(query.text, {
234
259
  caseSensitive: query.caseSensitive,
@@ -241,8 +266,10 @@ export class FindController {
241
266
  } catch {
242
267
  // A pattern that does not parse finds nothing; the find bar says it is invalid.
243
268
  }
269
+
244
270
  this.cache = { revision, key, matches: found.slice(0, FIND_LIMIT), complete: found.length <= FIND_LIMIT };
245
271
  }
272
+
246
273
  const found = this.cache.matches;
247
274
  this.marks = found;
248
275
  const selection = this.model.getPrimary();
@@ -253,23 +280,32 @@ export class FindController {
253
280
 
254
281
  step(direction: 1 | -1): void {
255
282
  this.replaced = undefined;
283
+
256
284
  if (this.seeking) {
257
285
  this.pendingStep = direction;
286
+
258
287
  return;
259
288
  }
289
+
260
290
  if (this.search !== undefined && this.query?.regex && !this.freshRegex) {
261
291
  this.pendingStep = direction;
292
+
262
293
  if (!this.searching) {
263
294
  this.startRegex(true);
264
295
  }
296
+
265
297
  return;
266
298
  }
299
+
267
300
  const count = this.marks.length;
301
+
268
302
  if (count === 0) {
269
303
  return;
270
304
  }
305
+
271
306
  const incomplete = this.regexCache?.complete === false || this.cache?.complete === false;
272
307
  const current = this.currentMark;
308
+
273
309
  if (
274
310
  this.search !== undefined &&
275
311
  incomplete &&
@@ -277,15 +313,20 @@ export class FindController {
277
313
  (this.outside !== null || (direction === 1 ? this.index === count - 1 : this.index === 0))
278
314
  ) {
279
315
  let offset = direction === -1 ? current.from : current.to;
316
+
280
317
  if (direction === 1 && current.from === current.to) {
281
318
  offset += (this.model.getText().codePointAt(offset) ?? 0) > 0xffff ? 2 : 1;
282
319
  }
320
+
283
321
  if (offset > this.model.getLength()) {
284
322
  offset = 0;
285
323
  }
324
+
286
325
  this.seek(offset, direction);
326
+
287
327
  return;
288
328
  }
329
+
289
330
  this.outside = null;
290
331
  this.index = this.index === null ? (direction === 1 ? 0 : count - 1) : (this.index + direction + count) % count;
291
332
  this.captureFailed = undefined;
@@ -312,6 +353,7 @@ export class FindController {
312
353
  this.searching = false;
313
354
  this.failed = undefined;
314
355
  this.captureFailed = undefined;
356
+
315
357
  if (release) {
316
358
  this.search?.release();
317
359
  }
@@ -321,6 +363,7 @@ export class FindController {
321
363
  if (this.freshRegex) {
322
364
  return;
323
365
  }
366
+
324
367
  const failure = this.failed;
325
368
  const retryBlocked = failure === 'timeout' || failure === 'unavailable';
326
369
  const sameQuery = this.regexCache?.key === this.cacheKey();
@@ -328,43 +371,54 @@ export class FindController {
328
371
  this.cache = undefined;
329
372
  this.marks = [];
330
373
  this.index = null;
374
+
331
375
  if (retryBlocked && sameQuery) {
332
376
  this.failed = failure;
333
377
  this.regexCache = { revision: this.model.getRevision(), key: this.cacheKey(), complete: false };
378
+
334
379
  return;
335
380
  }
381
+
336
382
  this.pendingAnchor ??= previous?.from;
337
383
  this.startRegex(false);
338
384
  }
339
385
 
340
386
  private startRegex(explicit: boolean): void {
341
387
  const query = this.query;
388
+
342
389
  if (query === null || this.search === undefined || this.disposed) {
343
390
  return;
344
391
  }
392
+
345
393
  if (query.inSelection && this.bounds === null) {
346
394
  return;
347
395
  }
396
+
348
397
  if (!explicit && this.model.getLength() > LIVE_REGEX_LIMIT) {
349
398
  this.pendingStep = undefined;
350
399
  this.pendingAnchor = undefined;
400
+
351
401
  return;
352
402
  }
403
+
353
404
  const epoch = this.epoch;
354
405
  const key = this.cacheKey();
355
406
  const revision = this.model.getRevision();
356
407
  this.failed = undefined;
357
408
  this.searching = true;
358
409
  this.cancelSearch?.();
410
+
359
411
  this.cancelSearch = (this.options?.schedule ?? scheduleFind)(
360
412
  () => {
361
413
  this.cancelSearch = undefined;
362
414
  const snapshot = this.snapshot();
415
+
363
416
  void this.search!.request(
364
417
  (client) => {
365
418
  if (epoch !== this.epoch || this.disposed) {
366
419
  throw new EditorSearchError('superseded', 'The find query changed.');
367
420
  }
421
+
368
422
  return client.search(snapshot, query, { ...this.scope, maxResults: FIND_LIMIT, ...(explicit ? { deadline: 10000 } : {}) });
369
423
  },
370
424
  explicit ? 'command' : 'live',
@@ -374,11 +428,14 @@ export class FindController {
374
428
  if (epoch !== this.epoch || revision !== this.model.getRevision() || this.disposed) {
375
429
  return;
376
430
  }
431
+
377
432
  this.regexCache = { revision, key, complete: !result.limited };
378
433
  const marks: FindMark[] = [];
434
+
379
435
  for (let index = 0; index < result.ranges.length; index += 2) {
380
436
  marks.push({ from: result.ranges[index]!, to: result.ranges[index + 1]! });
381
437
  }
438
+
382
439
  this.marks = marks;
383
440
  const selection = this.model.getPrimary();
384
441
  const anchor = this.pendingAnchor ?? Math.min(selection.anchor, selection.head);
@@ -394,22 +451,19 @@ export class FindController {
394
451
  if (epoch !== this.epoch || this.disposed) {
395
452
  return;
396
453
  }
454
+
397
455
  this.searching = false;
456
+
398
457
  if (error instanceof EditorSearchError && error.kind === 'superseded') {
399
458
  this.options?.changed();
400
459
  this.startRegex(this.pendingStep !== undefined);
460
+
401
461
  return;
402
462
  }
463
+
403
464
  this.pendingAnchor = undefined;
404
465
  this.pendingStep = undefined;
405
- this.failed =
406
- error instanceof EditorSearchError
407
- ? error.kind === 'syntax'
408
- ? 'invalid'
409
- : error.kind === 'timeout' || error.kind === 'unavailable'
410
- ? error.kind
411
- : 'error'
412
- : 'error';
466
+ this.failed = searchFailure(error);
413
467
  this.regexCache = { revision, key, complete: false };
414
468
  this.options?.changed();
415
469
  });
@@ -420,9 +474,11 @@ export class FindController {
420
474
 
421
475
  private snapshot(): EditorSearchSnapshot {
422
476
  const revision = this.model.getRevision();
477
+
423
478
  if (this.snapshotMemo?.revision !== revision) {
424
479
  this.snapshotMemo = { revision, text: this.model.getText() };
425
480
  }
481
+
426
482
  return this.snapshotMemo;
427
483
  }
428
484
 
@@ -434,6 +490,7 @@ export class FindController {
434
490
  private preparePreview(): void {
435
491
  this.cancelMatch?.();
436
492
  this.cancelMatch = undefined;
493
+
437
494
  if (
438
495
  this.preview &&
439
496
  this.search !== undefined &&
@@ -453,44 +510,55 @@ export class FindController {
453
510
  if (this.currentMatch !== null || this.search === undefined || !this.query?.regex || !this.freshRegex) {
454
511
  return this.currentMatch;
455
512
  }
513
+
456
514
  const mark = this.currentMark;
515
+
457
516
  if (mark === null) {
458
517
  return null;
459
518
  }
519
+
460
520
  const query = this.query;
461
521
  const epoch = this.epoch;
462
522
  const snapshot = this.snapshot();
523
+
463
524
  if (command) {
464
525
  this.captureFailed = undefined;
465
526
  }
527
+
466
528
  const current = (): boolean =>
467
529
  !this.disposed &&
468
530
  epoch === this.epoch &&
469
531
  this.model.getRevision() === snapshot.revision &&
470
532
  this.currentMark?.from === mark.from &&
471
533
  this.currentMark?.to === mark.to;
534
+
472
535
  try {
473
536
  const found = await this.search.request(
474
537
  (client) => {
475
538
  if (!current()) {
476
539
  throw new EditorSearchError('superseded', 'The current find match changed.');
477
540
  }
541
+
478
542
  return client.match(snapshot, query, { ...mark, deadline: 10000 });
479
543
  },
480
544
  command ? 'command' : 'preview'
481
545
  );
546
+
482
547
  if (!current()) {
483
548
  return null;
484
549
  }
550
+
485
551
  this.retained = found;
486
552
  this.captureFailed = undefined;
487
553
  this.options?.changed();
554
+
488
555
  return found;
489
556
  } catch (error) {
490
557
  if (current() && !(error instanceof EditorSearchError && error.kind === 'superseded')) {
491
558
  this.captureFailed = error instanceof EditorSearchError && (error.kind === 'timeout' || error.kind === 'unavailable') ? error.kind : 'error';
492
559
  this.options?.changed();
493
560
  }
561
+
494
562
  return null;
495
563
  }
496
564
  }
@@ -498,9 +566,12 @@ export class FindController {
498
566
  private chooseCurrent(anchor: number, direction: 1 | -1, limited: boolean): void {
499
567
  if (this.marks.length === 0) {
500
568
  this.index = null;
569
+
501
570
  return;
502
571
  }
572
+
503
573
  const next = direction === 1 ? this.marks.findIndex((mark) => mark.from >= anchor) : this.marks.findLastIndex((mark) => mark.from < anchor);
574
+
504
575
  if (limited && this.search !== undefined && (next === -1 || (direction === -1 && next === this.marks.length - 1))) {
505
576
  this.index = null;
506
577
  this.seek(anchor, direction);
@@ -511,34 +582,43 @@ export class FindController {
511
582
 
512
583
  private seek(offset: number, direction: 1 | -1): void {
513
584
  const query = this.query;
585
+
514
586
  if (this.search === undefined || query === null) {
515
587
  return;
516
588
  }
589
+
517
590
  const epoch = this.epoch;
518
591
  const snapshot = this.snapshot();
519
592
  const scope = this.scope;
593
+
520
594
  const valid = (): boolean => !this.disposed && epoch === this.epoch && snapshot.revision === this.model.getRevision();
595
+
521
596
  this.seeking = true;
522
597
  this.failed = undefined;
523
598
  this.retained = null;
524
599
  this.captureFailed = undefined;
600
+
525
601
  void this.search
526
602
  .request((client) => client.next(snapshot, query, offset, { ...scope, backwards: direction === -1, wrap: true, deadline: 10000 }), 'command', valid)
527
603
  .then((range) => {
528
604
  if (!valid()) {
529
605
  return;
530
606
  }
607
+
531
608
  this.seeking = false;
532
609
  this.outside = null;
533
610
  this.index = range === null ? null : this.marks.findIndex((mark) => mark.from === range.from && mark.to === range.to);
611
+
534
612
  if (this.index === -1) {
535
613
  this.index = null;
536
614
  this.outside = range;
537
615
  }
616
+
538
617
  this.options?.changed(true);
539
618
  this.preparePreview();
540
619
  const pending = this.pendingStep;
541
620
  this.pendingStep = undefined;
621
+
542
622
  if (pending !== undefined) {
543
623
  this.step(pending);
544
624
  }
@@ -556,25 +636,32 @@ export class FindController {
556
636
  if (this.search === undefined) {
557
637
  return null;
558
638
  }
639
+
559
640
  const snapshot = this.snapshot();
560
641
  const epoch = this.epoch;
642
+
561
643
  const valid = (): boolean => !this.disposed && epoch === this.epoch && snapshot.revision === this.model.getRevision();
644
+
562
645
  this.cursorRequests++;
646
+
563
647
  try {
564
648
  const range = await this.search.request(
565
649
  (client) => client.next(snapshot, query, offset, { backwards: direction === -1, wrap: true, deadline: 10000 }),
566
650
  'command',
567
651
  valid
568
652
  );
653
+
569
654
  return valid() ? range : null;
570
655
  } catch (error) {
571
656
  if (valid()) {
572
657
  this.failed = searchFailure(error);
573
658
  this.options?.changed();
574
659
  }
660
+
575
661
  return null;
576
662
  } finally {
577
663
  this.cursorRequests--;
664
+
578
665
  if (this.query === null && this.cursorRequests === 0) {
579
666
  this.search.release();
580
667
  }
@@ -594,13 +681,16 @@ export class FindController {
594
681
  }
595
682
 
596
683
  async replaceOne(replacement: string, preserveCase: boolean, canApply: () => boolean): Promise<boolean> {
597
- const query = this.query,
598
- mark = this.currentMark;
684
+ const query = this.query;
685
+ const mark = this.currentMark;
686
+
599
687
  if (query === null || mark === null || this.search === undefined || !canApply()) {
600
688
  return false;
601
689
  }
690
+
602
691
  const snapshot = this.snapshot();
603
692
  const epoch = this.epoch;
693
+
604
694
  const valid = (): boolean =>
605
695
  !this.disposed &&
606
696
  epoch === this.epoch &&
@@ -608,10 +698,12 @@ export class FindController {
608
698
  canApply() &&
609
699
  this.currentMark?.from === mark.from &&
610
700
  this.currentMark?.to === mark.to;
701
+
611
702
  this.replacing++;
612
703
  this.replaceEpoch = epoch;
613
704
  this.replaceFailed = undefined;
614
705
  this.options?.changed();
706
+
615
707
  try {
616
708
  const page = await this.search.request(
617
709
  (client) =>
@@ -626,26 +718,32 @@ export class FindController {
626
718
  'command',
627
719
  valid
628
720
  );
721
+
629
722
  if (!valid() || page.ranges.length !== 3 || page.ranges[0] !== mark.from || page.ranges[1] !== mark.to || page.next !== null) {
630
723
  return false;
631
724
  }
725
+
632
726
  this.pendingAnchor = mark.to;
727
+
633
728
  const changed = this.model.applyEdits([{ from: mark.from, to: mark.to, text: page.text }], {
634
729
  source: 'command',
635
730
  expectedRevision: snapshot.revision
636
731
  });
732
+
637
733
  if (!changed) {
638
734
  this.pendingAnchor = undefined;
639
735
  }
736
+
640
737
  return changed;
641
738
  } catch (error) {
642
739
  if (valid()) {
643
- const failure = searchFailure(error);
644
- this.reportReplaceFailed(error instanceof EditorSearchError && error.kind === 'range' ? 'limit' : failure === 'invalid' ? 'error' : failure);
740
+ this.reportReplaceFailed(replaceFailure(error));
645
741
  }
742
+
646
743
  return false;
647
744
  } finally {
648
745
  this.replacing--;
746
+
649
747
  if (!this.disposed) {
650
748
  this.options?.changed();
651
749
  }
@@ -654,18 +752,23 @@ export class FindController {
654
752
 
655
753
  async replaceAll(replacement: string, preserveCase: boolean, canApply: () => boolean): Promise<number> {
656
754
  const query = this.query;
755
+
657
756
  if (query === null || this.search === undefined || this.state.noSelection || !canApply()) {
658
757
  return 0;
659
758
  }
759
+
660
760
  const snapshot = this.snapshot();
661
761
  const epoch = this.epoch;
662
762
  const scope = this.scope;
763
+
663
764
  const valid = (): boolean => !this.disposed && epoch === this.epoch && snapshot.revision === this.model.getRevision() && canApply();
765
+
664
766
  this.replacing++;
665
767
  this.replaceEpoch = epoch;
666
768
  this.replaceFailed = undefined;
667
769
  this.replaced = undefined;
668
770
  this.options?.changed();
771
+
669
772
  try {
670
773
  const page = await this.search.request(
671
774
  (client) =>
@@ -679,32 +782,40 @@ export class FindController {
679
782
  'command',
680
783
  valid
681
784
  );
785
+
682
786
  if (!valid()) {
683
787
  return 0;
684
788
  }
789
+
685
790
  if (page.next !== null) {
686
791
  this.replaceFailed = 'limit';
792
+
687
793
  return 0;
688
794
  }
795
+
689
796
  const edits: TextEdit[] = [];
690
797
  let position = 0;
798
+
691
799
  for (let index = 0; index < page.ranges.length; index += 3) {
692
800
  const length = page.ranges[index + 2]!;
693
801
  edits.push({ from: page.ranges[index]!, to: page.ranges[index + 1]!, text: page.text.slice(position, position + length) });
694
802
  position += length;
695
803
  }
804
+
696
805
  const changed = this.model.applyEdits(edits, { source: 'command', expectedRevision: snapshot.revision });
697
806
  const count = changed ? edits.length : 0;
698
807
  this.reportReplaced(count);
808
+
699
809
  return count;
700
810
  } catch (error) {
701
811
  if (!this.disposed && epoch === this.epoch) {
702
- const failure = searchFailure(error);
703
- this.replaceFailed = error instanceof EditorSearchError && error.kind === 'range' ? 'limit' : failure === 'invalid' ? 'error' : failure;
812
+ this.replaceFailed = replaceFailure(error);
704
813
  }
814
+
705
815
  return 0;
706
816
  } finally {
707
817
  this.replacing--;
818
+
708
819
  if (!this.disposed) {
709
820
  this.options?.changed();
710
821
  }
@@ -727,3 +838,13 @@ function searchFailure(error: unknown): NonNullable<EditorFindState['failed']> {
727
838
  : 'error'
728
839
  : 'error';
729
840
  }
841
+
842
+ function replaceFailure(error: unknown): NonNullable<EditorFindState['replaceFailed']> {
843
+ if (error instanceof EditorSearchError && error.kind === 'range') {
844
+ return 'limit';
845
+ }
846
+
847
+ const failure = searchFailure(error);
848
+
849
+ return failure === 'invalid' ? 'error' : failure;
850
+ }
@@ -12,16 +12,20 @@ export function rowOfStop(geometry: LineGeometry, stop: number): number {
12
12
  if (geometry.uniform === undefined) {
13
13
  return geometry.rowOf[stop]!;
14
14
  }
15
+
15
16
  let first = 0;
16
17
  let last = geometry.rowStarts.length;
18
+
17
19
  while (first < last) {
18
20
  const middle = (first + last) >>> 1;
21
+
19
22
  if (geometry.rowStarts[middle]! <= stop) {
20
23
  first = middle + 1;
21
24
  } else {
22
25
  last = middle;
23
26
  }
24
27
  }
28
+
25
29
  return Math.max(0, first - 1);
26
30
  }
27
31
 
@@ -29,7 +33,9 @@ export function xOfStop(geometry: LineGeometry, stop: number, affinity: Affinity
29
33
  if (geometry.uniform === undefined) {
30
34
  return geometry[affinity][stop]!;
31
35
  }
36
+
32
37
  const row = rowOfStop(geometry, stop);
38
+
33
39
  return geometry.uniform[row === 0 ? 'first' : 'continuation'][stop - geometry.rowStarts[row]!]!;
34
40
  }
35
41
 
@@ -37,15 +43,19 @@ export function stopAtOffset(geometry: LineGeometry, offset: number): number {
37
43
  if (geometry.uniform !== undefined) {
38
44
  return Math.min(geometry.text.length, Math.max(0, Math.ceil(offset)));
39
45
  }
46
+
40
47
  let first = 0;
41
48
  let last = geometry.offsets.length;
49
+
42
50
  while (first < last) {
43
51
  const middle = (first + last) >>> 1;
52
+
44
53
  if (geometry.offsets[middle]! < offset) {
45
54
  first = middle + 1;
46
55
  } else {
47
56
  last = middle;
48
57
  }
49
58
  }
59
+
50
60
  return Math.min(geometry.offsets.length - 1, first);
51
61
  }