@adecore/editor 0.38.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/layout.ts CHANGED
@@ -21,6 +21,7 @@ export interface SubRowBand {
21
21
  export function subRowBand(top: number, subRows: number, lineHeight: number, window: VerticalWindow, block = 1): SubRowBand {
22
22
  const first = Math.max(0, Math.min(subRows - 1, Math.floor((window.top - top) / lineHeight)));
23
23
  const last = Math.max(first, Math.min(subRows - 1, Math.ceil((window.bottom - top) / lineHeight) - 1));
24
+
24
25
  return {
25
26
  first: Math.floor(first / block) * block,
26
27
  last: Math.min(subRows - 1, Math.ceil((last + 1) / block) * block - 1)
@@ -194,22 +195,27 @@ const WRAP_INDENT_STEP = 2;
194
195
  const GEOMETRY_CACHE_SIZE = 400;
195
196
  const PLAIN = /^[\x20-\x7e\t]*$/;
196
197
  const GRAPHEME_LIMIT = 20000;
198
+
197
199
  let segmenter: Intl.Segmenter | undefined;
198
200
 
199
201
  /* Where each character starts, with the length last. A very long line is cut by code unit, since splitting it by grapheme would cost more than it shows. */
200
202
  function characterOffsets(text: string): number[] {
201
203
  const offsets: number[] = [];
204
+
202
205
  if (text.length > GRAPHEME_LIMIT || PLAIN.test(text)) {
203
206
  for (let i = 0; i < text.length; i++) {
204
207
  offsets.push(i);
205
208
  }
206
209
  } else {
207
210
  segmenter ??= new Intl.Segmenter(undefined, { granularity: 'grapheme' });
211
+
208
212
  for (const part of segmenter.segment(text)) {
209
213
  offsets.push(part.index);
210
214
  }
211
215
  }
216
+
212
217
  offsets.push(text.length);
218
+
213
219
  return offsets;
214
220
  }
215
221
 
@@ -220,23 +226,29 @@ function isSpace(character: string | undefined): boolean {
220
226
  function characterWidth(metrics: LayoutMetrics, text: string, x: number): number {
221
227
  if (text === '\t') {
222
228
  const tab = metrics.tabSize * metrics.charWidth;
229
+
223
230
  return (Math.floor(x / tab) + 1) * tab - x;
224
231
  }
232
+
225
233
  if (text.length === 1 && text >= ' ' && text <= '~') {
226
234
  return metrics.charWidth;
227
235
  }
236
+
228
237
  return bidiControl(text) !== null ? BIDI_COLUMNS * metrics.charWidth : metrics.measureText(text);
229
238
  }
230
239
 
231
240
  /* Where the continuation rows of a wrapped line start, kept to half the row so a deep indent never leaves no room for text. */
232
241
  export function wrapIndent(text: string, metrics: LayoutMetrics, limit: number): number {
233
242
  let x = 0;
243
+
234
244
  for (const character of text) {
235
245
  if (!isSpace(character)) {
236
246
  break;
237
247
  }
248
+
238
249
  x += characterWidth(metrics, character, x);
239
250
  }
251
+
240
252
  return Math.min(x + WRAP_INDENT_STEP * metrics.charWidth, limit / 2);
241
253
  }
242
254
 
@@ -253,9 +265,11 @@ function wrapRows(text: string, offsets: readonly number[] | null, metrics: Layo
253
265
  let opportunity = -1;
254
266
  let x = 0;
255
267
  let index = 0;
268
+
256
269
  while (index < count) {
257
270
  const character = offsets === null ? text[index]! : text.slice(offsets[index]!, offsets[index + 1]!);
258
271
  const width = characterWidth(metrics, character, x);
272
+
259
273
  if (index > rowStart && x + width > limit && !isSpace(character)) {
260
274
  const target = opportunity > rowStart ? opportunity : index;
261
275
  breaks.push(target);
@@ -264,15 +278,21 @@ function wrapRows(text: string, offsets: readonly number[] | null, metrics: Layo
264
278
  opportunity = -1;
265
279
  x = indent;
266
280
  index = target;
281
+
267
282
  continue;
268
283
  }
284
+
269
285
  x += width;
286
+
270
287
  if (isSpace(character) && index + 1 < count && !isSpace(text[offsets === null ? index + 1 : offsets[index + 1]!])) {
271
288
  opportunity = index + 1;
272
289
  }
290
+
273
291
  index++;
274
292
  }
293
+
275
294
  triggers.push(count);
295
+
276
296
  return { breaks, triggers };
277
297
  }
278
298
 
@@ -292,6 +312,7 @@ export function scanDenseLine(
292
312
  const wrapped = limit === null ? { breaks: [], triggers: [offsets.length - 1] } : wrapRows(text, offsets, metrics, limit);
293
313
  const breakStops = wrapped.breaks;
294
314
  const indent = limit === null ? 0 : wrapIndent(text, metrics, limit);
315
+
295
316
  const geometry: LineGeometry = {
296
317
  start: line.start,
297
318
  end: line.end,
@@ -309,20 +330,25 @@ export function scanDenseLine(
309
330
  subRows: breakStops.length + 1,
310
331
  width: 0
311
332
  };
333
+
312
334
  let x = 0;
313
335
  let row = 0;
314
336
  let nextBreak = 0;
315
337
  let nextInlay = 0;
316
338
  let runStart = -1;
317
339
  let runX = 0;
340
+
318
341
  const flushRun = (end: number): void => {
319
342
  if (runStart >= 0 && end > runStart) {
320
343
  geometry.runs.push({ from: runStart, to: end, x: runX, subRow: row, text: text.slice(runStart, end) });
321
344
  }
345
+
322
346
  runStart = -1;
323
347
  };
348
+
324
349
  for (let stop = 0; stop < offsets.length; stop++) {
325
350
  const offset = offsets[stop]!;
351
+
326
352
  if (nextBreak < breakStops.length && breakStops[nextBreak] === stop) {
327
353
  flushRun(offset);
328
354
  geometry.rowEnds.push(x);
@@ -331,38 +357,50 @@ export function scanDenseLine(
331
357
  row++;
332
358
  x = indent;
333
359
  }
360
+
334
361
  geometry.before.push(x);
335
362
  geometry.rowOf.push(row);
363
+
336
364
  while (nextInlay < lineInlays.length && lineInlays[nextInlay]!.at - line.start <= offset) {
337
365
  flushRun(offset);
338
366
  const inlay = lineInlays[nextInlay++]!;
339
367
  const shown = visibleBidi(inlay.text);
368
+
340
369
  const width =
341
370
  inlay.composition || inlay.ghost
342
371
  ? metrics.measureText(shown)
343
372
  : Math.max(8, metrics.measureInlay(shown) + 12) + (inlay.gapLeft ?? 0) + (inlay.gapRight ?? 0);
373
+
344
374
  geometry.inlays.push({ inlay, x, width, subRow: row });
345
375
  x += width;
346
376
  }
377
+
347
378
  geometry.after.push(x);
379
+
348
380
  if (stop === offsets.length - 1) {
349
381
  break;
350
382
  }
383
+
351
384
  const character = text.slice(offset, offsets[stop + 1]!);
352
385
  const width = characterWidth(metrics, character, x);
386
+
353
387
  if (character === '\t' || bidiControl(character) !== null) {
354
388
  flushRun(offset);
355
389
  } else if (runStart < 0) {
356
390
  runStart = offset;
357
391
  runX = x;
358
392
  }
393
+
359
394
  x += width;
360
395
  }
396
+
361
397
  flushRun(text.length);
362
398
  geometry.rowEnds.push(x);
399
+
363
400
  for (const end of geometry.rowEnds) {
364
401
  geometry.width = Math.max(geometry.width, end);
365
402
  }
403
+
366
404
  return geometry;
367
405
  }
368
406
 
@@ -375,10 +413,12 @@ export function scanLine(
375
413
  if (limit !== null && line.text.length > GRAPHEME_LIMIT && lineInlays.length === 0 && /^[\x20-\x7e]*$/.test(line.text)) {
376
414
  const wrapped = wrapRows(line.text, null, metrics, limit);
377
415
  const geometry = uniformGeometry(line, metrics, wrapped.breaks, wrapped.triggers, wrapIndent(line.text, metrics, limit));
416
+
378
417
  if (geometry !== null) {
379
418
  return geometry;
380
419
  }
381
420
  }
421
+
382
422
  return scanDenseLine(line, metrics, limit, lineInlays);
383
423
  }
384
424
 
@@ -395,7 +435,6 @@ export interface LayoutConfig {
395
435
  replacements?: readonly BlockReplacement[];
396
436
  }
397
437
 
398
- /* Lines `first` through `last`, both included. */
399
438
  interface LineRange {
400
439
  first: number;
401
440
  last: number;
@@ -449,14 +488,17 @@ class LineRow implements TextRow {
449
488
  function mergeRanges(ranges: LineRange[]): LineRange[] {
450
489
  ranges.sort((left, right) => left.first - right.first);
451
490
  const merged: LineRange[] = [];
491
+
452
492
  for (const range of ranges) {
453
493
  const previous = merged.at(-1);
494
+
454
495
  if (previous !== undefined && range.first <= previous.last + 1) {
455
496
  previous.last = Math.max(previous.last, range.last);
456
497
  } else {
457
498
  merged.push({ ...range });
458
499
  }
459
500
  }
501
+
460
502
  return merged;
461
503
  }
462
504
 
@@ -503,30 +545,38 @@ export class EditorLayout {
503
545
 
504
546
  configure(config: LayoutConfig): void {
505
547
  let measureAgain = false;
548
+
506
549
  if (config.metrics) {
507
550
  this.metrics = config.metrics;
508
551
  this.measuredHeights.clear();
509
552
  measureAgain = true;
510
553
  }
554
+
511
555
  if (config.wrapWidth !== undefined && config.wrapWidth !== this.wrapWidth) {
512
556
  this.wrapWidth = config.wrapWidth;
513
557
  measureAgain = true;
514
558
  }
559
+
515
560
  if (config.inlays) {
516
561
  this.inlays = config.inlays;
517
562
  }
563
+
518
564
  if (config.blocks) {
519
565
  this.blocks = config.blocks;
520
566
  }
567
+
521
568
  if (config.folds) {
522
569
  this.folds = config.folds;
523
570
  this.lineHeightsVersion++;
524
571
  }
572
+
525
573
  if (config.replacements) {
526
574
  this.replacements = config.replacements;
527
575
  this.lineHeightsVersion++;
528
576
  }
577
+
529
578
  let edit: LineEdit | undefined;
579
+
530
580
  if (measureAgain || (config.textChanged && !config.changes?.length)) {
531
581
  this.lineHeightsVersion++;
532
582
  this.geometryCache.clear();
@@ -538,6 +588,7 @@ export class EditorLayout {
538
588
  this.retainGeometry(config.changes);
539
589
  this.lineHeightsVersion++;
540
590
  }
591
+
541
592
  if (config.inlays !== undefined) {
542
593
  if (!config.changes?.length) {
543
594
  for (const line of this.inlaysByLine.keys()) {
@@ -545,20 +596,25 @@ export class EditorLayout {
545
596
  this.measuredRows.delete(line);
546
597
  }
547
598
  }
599
+
548
600
  for (const inlay of config.inlays) {
549
601
  const line = this.document.positionAt(inlay.at).line;
550
602
  this.geometryCache.delete(line);
551
603
  this.measuredRows.delete(line);
552
604
  }
605
+
553
606
  this.lineHeightsVersion++;
554
607
  }
608
+
555
609
  this.width = 0;
610
+
556
611
  if (this.wrapWidth === null) {
557
612
  for (const geometry of this.geometryCache.values()) {
558
613
  this.width = Math.max(this.width, geometry.width + ADDITIONAL_COLUMNS * this.metrics.charWidth);
559
614
  }
560
615
  }
561
616
  }
617
+
562
618
  this.rebuild(edit);
563
619
  }
564
620
 
@@ -566,69 +622,91 @@ export class EditorLayout {
566
622
  private lineEdit(batches: readonly (readonly DocumentChange[])[]): LineEdit | undefined {
567
623
  let from = Number.POSITIVE_INFINITY;
568
624
  let to = Number.NEGATIVE_INFINITY;
625
+
569
626
  for (const changes of batches) {
570
627
  if (Number.isFinite(to)) {
571
628
  to = offsetMapper(changes)(to);
572
629
  }
630
+
573
631
  let delta = 0;
632
+
574
633
  for (const change of changes) {
575
634
  from = Math.min(from, change.from);
576
635
  to = Math.max(to, change.from + delta + change.insertedLength);
577
636
  delta += change.insertedLength - (change.to - change.from);
578
637
  }
579
638
  }
639
+
580
640
  if (!Number.isFinite(from)) {
581
641
  return undefined;
582
642
  }
643
+
583
644
  // The line above too, since an edit at the start of a line can turn the `\r` that ends it into a `\r\n`.
584
645
  const first = Math.max(0, this.document.positionAt(from).line - 1);
585
646
  const newLast = this.document.positionAt(to).line;
586
647
  const oldLast = newLast - (this.document.getLineCount() - this.lineRows.length);
648
+
587
649
  return oldLast < first ? undefined : { first, oldLast, newLast };
588
650
  }
589
651
 
590
652
  private retainGeometry(batches: readonly (readonly DocumentChange[])[]): void {
591
653
  const map = lineChangeMapper(batches);
654
+
592
655
  const remap = (bounds: { start: number; end: number; next: number }): { line: number; start: number; end: number; next: number } | null => {
593
656
  const next = map({ start: bounds.start, end: Math.max(bounds.end, bounds.next - 1) });
657
+
594
658
  if (next === null) {
595
659
  return null;
596
660
  }
661
+
597
662
  const delta = next.start - bounds.start;
663
+
598
664
  return { start: next.start, end: bounds.end + delta, next: bounds.next + delta, line: this.document.positionAt(next.start).line };
599
665
  };
666
+
600
667
  const geometries = new Map<number, LineGeometry>();
668
+
601
669
  for (const geometry of this.geometryCache.values()) {
602
670
  const next = remap(geometry);
671
+
603
672
  if (next !== null && geometry.inlays.length === 0) {
604
673
  geometries.set(next.line, next.start === geometry.start ? geometry : { ...geometry, start: next.start, end: next.end, next: next.next });
605
674
  }
606
675
  }
676
+
607
677
  const measured = new Map<number, { start: number; end: number; next: number; subRows: number }>();
678
+
608
679
  for (const [line, bounds] of this.measuredRows) {
609
680
  const next = remap(bounds);
681
+
610
682
  if (next !== null && !this.inlaysByLine.has(line)) {
611
683
  measured.set(next.line, { start: next.start, end: next.end, next: next.next, subRows: bounds.subRows });
612
684
  }
613
685
  }
686
+
614
687
  this.geometryCache = geometries;
615
688
  this.measuredRows = measured;
616
689
  }
617
690
 
618
691
  private rowCountOf(line: number, length: number): number {
619
692
  const limit = this.wrapWidth;
693
+
620
694
  if (limit === null) {
621
695
  return 1;
622
696
  }
697
+
623
698
  const measured = this.measuredRows.get(line);
699
+
624
700
  if (measured !== undefined) {
625
701
  return measured.subRows;
626
702
  }
703
+
627
704
  return Math.max(1, Math.ceil((length * this.metrics.charWidth) / Math.max(limit, this.metrics.charWidth)));
628
705
  }
629
706
 
630
707
  private rebuild(edit?: LineEdit): void {
631
708
  const structure = this.structure();
709
+
632
710
  if (edit === undefined || !this.patch(edit, structure)) {
633
711
  this.keyed.clear();
634
712
  const built = this.buildRows(structure, 0, this.document.getLineCount() - 1, 0);
@@ -636,52 +714,73 @@ export class EditorLayout {
636
714
  this.lineRows = built.lineRows;
637
715
  this.reflow(0);
638
716
  }
717
+
639
718
  this.special = structure.special;
640
719
  }
641
720
 
642
721
  private structure(): RowStructure {
643
722
  this.inlaysByLine.clear();
723
+
644
724
  for (const inlay of this.inlays) {
645
725
  const line = this.document.positionAt(inlay.at).line;
646
726
  const list = this.inlaysByLine.get(line) ?? [];
647
727
  list.push(inlay);
648
728
  this.inlaysByLine.set(line, list);
649
729
  }
730
+
650
731
  for (const list of this.inlaysByLine.values()) {
651
732
  list.sort((left, right) => left.at - right.at);
652
733
  }
734
+
653
735
  const above = new Map<number, BlockWidget[]>();
654
736
  const below = new Map<number, BlockWidget[]>();
737
+
655
738
  for (const block of this.blocks) {
656
739
  const line = this.document.positionAt(block.at).line;
657
740
  const target = block.placement === 'above' ? above : below;
658
- target.set(line, [...(target.get(line) ?? []), block]);
741
+ const list = target.get(line);
742
+
743
+ if (list === undefined) {
744
+ target.set(line, [block]);
745
+ } else {
746
+ list.push(block);
747
+ }
659
748
  }
749
+
660
750
  const folds = new Map<number, FoldState>();
751
+
661
752
  for (const fold of this.folds) {
662
753
  if (fold.collapsed && fold.endLine > (folds.get(fold.startLine)?.endLine ?? 0)) {
663
754
  folds.set(fold.startLine, fold);
664
755
  }
665
756
  }
757
+
666
758
  const replaced = new Map<number, { replacement: BlockReplacement; endLine: number }>();
759
+
667
760
  for (const replacement of this.replacements) {
668
761
  const startLine = this.document.positionAt(replacement.from).line;
669
762
  const endLine = Math.max(startLine, this.document.positionAt(replacement.to).line);
763
+
670
764
  // A fold the person collapsed shows its own placeholder, so folding a replaced comment keeps working.
671
765
  if (!folds.has(startLine) && !replaced.has(startLine)) {
672
766
  replaced.set(startLine, { replacement, endLine });
673
767
  }
674
768
  }
769
+
675
770
  const special: LineRange[] = [];
771
+
676
772
  for (const line of [...this.inlaysByLine.keys(), ...above.keys(), ...below.keys()]) {
677
773
  special.push({ first: line, last: line });
678
774
  }
775
+
679
776
  for (const fold of folds.values()) {
680
777
  special.push({ first: fold.startLine, last: Math.max(fold.startLine, fold.endLine) });
681
778
  }
779
+
682
780
  for (const [startLine, entry] of replaced) {
683
781
  special.push({ first: startLine, last: entry.endLine });
684
782
  }
783
+
685
784
  return { above, below, folds, replaced, special: mergeRanges(special) };
686
785
  }
687
786
 
@@ -692,84 +791,109 @@ export class EditorLayout {
692
791
  private patch(edit: LineEdit, structure: RowStructure): boolean {
693
792
  const count = this.document.getLineCount();
694
793
  const delta = edit.newLast - edit.oldLast;
794
+
695
795
  if (this.rows.length === 0 || this.lineRows.length + delta !== count) {
696
796
  return false;
697
797
  }
798
+
698
799
  const moved = (range: LineRange): LineRange => {
699
800
  if (range.last < edit.first) {
700
801
  return range;
701
802
  }
803
+
702
804
  if (range.first > edit.oldLast) {
703
805
  return { first: range.first + delta, last: range.last + delta };
704
806
  }
807
+
705
808
  return { first: Math.min(range.first, edit.first), last: range.last > edit.oldLast ? range.last + delta : edit.newLast };
706
809
  };
810
+
707
811
  const ranges = mergeRanges([{ first: edit.first, last: edit.newLast }, ...structure.special, ...this.special.map(moved)]).filter(
708
812
  (range) => range.first < count
709
813
  );
814
+
710
815
  let lines = 0;
816
+
711
817
  for (const range of ranges) {
712
818
  range.last = Math.min(range.last, count - 1);
713
819
  lines += range.last - range.first + 1;
714
820
  }
821
+
715
822
  if (lines > Math.min(PATCH_LIMIT, count / 2)) {
716
823
  return false;
717
824
  }
825
+
718
826
  const rows = this.rows;
719
827
  const built = new Set<LayoutRow>();
720
828
  let first = rows.length;
721
829
  let resized = delta !== 0;
830
+
722
831
  // From the bottom up, so the rows and lines of the ranges above keep their indices. Rows not built again keep their old lines until the end.
723
832
  for (let index = ranges.length - 1; index >= 0; index--) {
724
833
  const range = ranges[index]!;
725
834
  const oldFirst = range.first > edit.newLast ? range.first - delta : range.first;
726
835
  const oldLast = range.last >= edit.newLast ? range.last - delta : range.last;
727
836
  let start = this.lineRows[oldFirst]!;
837
+
728
838
  while (start > 0 && rows[start - 1]!.kind === 'block' && rows[start - 1]!.line >= oldFirst) {
729
839
  start--;
730
840
  }
841
+
731
842
  let end = this.lineRows[oldLast]!;
843
+
732
844
  while (end + 1 < rows.length && rows[end + 1]!.kind === 'block' && rows[end + 1]!.line <= oldLast) {
733
845
  end++;
734
846
  }
847
+
735
848
  const fresh = this.buildRows(structure, range.first, range.last, start);
736
849
  const removed = rows.splice(start, end - start + 1, ...fresh.rows);
737
850
  this.lineRows.splice(oldFirst, oldLast - oldFirst + 1, ...fresh.lineRows);
738
851
  let height = 0;
852
+
739
853
  for (const row of removed) {
740
854
  height -= row.height;
855
+
741
856
  if (this.keyed.get(row.key) === row) {
742
857
  this.keyed.delete(row.key);
743
858
  }
744
859
  }
860
+
745
861
  let top = start > 0 ? rows[start - 1]!.top + rows[start - 1]!.height : 0;
862
+
746
863
  for (const row of fresh.rows) {
747
864
  built.add(row);
748
865
  height += row.height;
749
866
  row.top = top;
750
867
  top += row.height;
751
868
  }
869
+
752
870
  resized ||= height !== 0 || removed.length !== fresh.rows.length;
753
871
  first = start;
754
872
  }
873
+
755
874
  if (resized) {
756
875
  for (let index = first; index < rows.length; index++) {
757
876
  const row = rows[index]!;
758
877
  row.index = index;
878
+
759
879
  if (!built.has(row) && row.line > edit.oldLast) {
760
880
  row.line += delta;
881
+
761
882
  if (row.kind === 'text') {
762
883
  row.lastLine += delta;
763
884
  }
764
885
  }
886
+
765
887
  if (row.kind === 'text') {
766
888
  for (let covered = row.line; covered <= row.lastLine; covered++) {
767
889
  this.lineRows[covered] = index;
768
890
  }
769
891
  }
770
892
  }
893
+
771
894
  this.reflow(first);
772
895
  }
896
+
773
897
  return true;
774
898
  }
775
899
 
@@ -779,8 +903,10 @@ export class EditorLayout {
779
903
  const count = this.document.getLineCount();
780
904
  const rows: LayoutRow[] = [];
781
905
  const lineRows = new Array<number>(to - from + 1);
906
+
782
907
  const addBlock = (widget: BlockWidget, line: number): void => {
783
908
  const key = `block:${widget.id}`;
909
+
784
910
  const row: BlockRow = {
785
911
  key,
786
912
  kind: 'block',
@@ -794,18 +920,23 @@ export class EditorLayout {
794
920
  ? Math.max(1, widget.height ?? DEFAULT_BLOCK_HEIGHT)
795
921
  : (this.measuredHeights.get(key) ?? widget.height ?? DEFAULT_BLOCK_HEIGHT)
796
922
  };
923
+
797
924
  rows.push(row);
798
925
  this.keyed.set(key, row);
799
926
  };
927
+
800
928
  for (let line = from; line <= to; line++) {
801
929
  for (const block of above.get(line) ?? []) {
802
930
  addBlock(block, line);
803
931
  }
932
+
804
933
  const standIn = replaced.get(line);
934
+
805
935
  if (standIn !== undefined) {
806
936
  const last = Math.min(count - 1, standIn.endLine);
807
937
  const key = `replaced:${standIn.replacement.id}`;
808
938
  const rowIndex = index + rows.length;
939
+
809
940
  const row: TextRow = {
810
941
  key,
811
942
  kind: 'text',
@@ -819,17 +950,23 @@ export class EditorLayout {
819
950
  height: this.measuredHeights.get(key) ?? standIn.replacement.height ?? DEFAULT_BLOCK_HEIGHT,
820
951
  replacement: standIn.replacement
821
952
  };
953
+
822
954
  rows.push(row);
823
955
  this.keyed.set(key, row);
956
+
824
957
  for (let covered = line; covered <= last; covered++) {
825
958
  lineRows[covered - from] = rowIndex;
826
959
  }
960
+
827
961
  for (const block of below.get(last) ?? []) {
828
962
  addBlock(block, last);
829
963
  }
964
+
830
965
  line = last;
966
+
831
967
  continue;
832
968
  }
969
+
833
970
  const fold = folds.get(line);
834
971
  const last = Math.max(line, Math.min(count - 1, fold?.endLine ?? line));
835
972
  const tail = fold?.closer !== undefined && last > line ? this.clampToLine(fold.closer, last) : null;
@@ -838,20 +975,25 @@ export class EditorLayout {
838
975
  const bounds = this.wrapWidth === null ? null : this.document.getLine(line);
839
976
  const subRows = this.rowCountOf(line, bounds === null ? 0 : bounds.end - bounds.start);
840
977
  rows.push(new LineRow(rowIndex, line, last, tail, head, subRows, subRows * this.metrics.lineHeight));
978
+
841
979
  for (let covered = line; covered <= last; covered++) {
842
980
  lineRows[covered - from] = rowIndex;
843
981
  }
982
+
844
983
  for (const block of below.get(line) ?? []) {
845
984
  addBlock(block, line);
846
985
  }
986
+
847
987
  line = last;
848
988
  }
989
+
849
990
  return { rows, lineRows };
850
991
  }
851
992
 
852
993
  /* An offset of a fold clamped to its line, since the fold may be a moment older than an edit. */
853
994
  private clampToLine(offset: number, line: number): number {
854
995
  const bounds = this.document.getLine(line);
996
+
855
997
  return Math.min(bounds.end, Math.max(bounds.start, offset));
856
998
  }
857
999
 
@@ -859,11 +1001,13 @@ export class EditorLayout {
859
1001
  private reflow(from: number): void {
860
1002
  const previous = this.rows[from - 1];
861
1003
  let top = previous === undefined ? 0 : previous.top + previous.height;
1004
+
862
1005
  for (let index = from; index < this.rows.length; index++) {
863
1006
  const row = this.rows[index]!;
864
1007
  row.top = top;
865
1008
  top += row.height;
866
1009
  }
1010
+
867
1011
  this.height = top + BOTTOM_PADDING;
868
1012
  }
869
1013
 
@@ -871,8 +1015,10 @@ export class EditorLayout {
871
1015
  rowByKey(key: string): LayoutRow | undefined {
872
1016
  if (key.startsWith('line:')) {
873
1017
  const row = this.rowForLine(Number(key.slice(5)));
1018
+
874
1019
  return row.key === key ? row : undefined;
875
1020
  }
1021
+
876
1022
  return this.keyed.get(key);
877
1023
  }
878
1024
 
@@ -880,71 +1026,89 @@ export class EditorLayout {
880
1026
  setMeasuredHeight(key: string, height: number): boolean {
881
1027
  const row = this.rowByKey(key);
882
1028
  const next = Math.max(1, Math.round(height));
1029
+
883
1030
  if (!row || (row.kind === 'block' && row.widget.fixed) || !Number.isFinite(next) || row.height === next) {
884
1031
  return false;
885
1032
  }
1033
+
886
1034
  this.measuredHeights.set(key, next);
887
1035
  row.height = next;
888
1036
  this.reflow(row.index);
1037
+
889
1038
  return true;
890
1039
  }
891
1040
 
892
1041
  /* The visual lines a drawn row really has, which its estimate may have missed. Returns whether any row changed. */
893
1042
  syncRows(rows: readonly LayoutRow[]): boolean {
894
1043
  let first = this.rows.length;
1044
+
895
1045
  for (const row of rows) {
896
1046
  if (row.kind !== 'text' || row.replacement !== undefined) {
897
1047
  continue;
898
1048
  }
1049
+
899
1050
  const geometry = this.geometry(row);
900
1051
  const subRows = geometry.subRows;
1052
+
901
1053
  if (row.head === null && this.wrapWidth !== null) {
902
1054
  if (!this.measuredRows.has(row.line) && this.measuredRows.size >= GEOMETRY_CACHE_SIZE) {
903
1055
  this.measuredRows.delete(this.measuredRows.keys().next().value!);
904
1056
  }
1057
+
905
1058
  this.measuredRows.set(row.line, { start: geometry.start, end: geometry.end, next: geometry.next, subRows });
906
1059
  } else {
907
1060
  this.measuredRows.delete(row.line);
908
1061
  }
1062
+
909
1063
  if (subRows !== row.subRows) {
910
1064
  row.subRows = subRows;
911
1065
  row.height = subRows * this.metrics.lineHeight;
912
1066
  first = Math.min(first, row.index);
913
1067
  }
914
1068
  }
1069
+
915
1070
  if (first < this.rows.length) {
916
1071
  this.lineHeightsVersion++;
917
1072
  this.reflow(first);
1073
+
918
1074
  return true;
919
1075
  }
1076
+
920
1077
  return false;
921
1078
  }
922
1079
 
923
1080
  rowAt(y: number): LayoutRow {
924
1081
  let low = 0;
925
1082
  let high = this.rows.length - 1;
1083
+
926
1084
  while (low < high) {
927
1085
  const middle = Math.ceil((low + high) / 2);
1086
+
928
1087
  if (this.rows[middle]!.top <= y) {
929
1088
  low = middle;
930
1089
  } else {
931
1090
  high = middle - 1;
932
1091
  }
933
1092
  }
1093
+
934
1094
  return this.rows[low]!;
935
1095
  }
936
1096
 
937
1097
  /* Where a line's text starts after its indentation, which is where a lens row begins. */
938
1098
  indentX(line: number): number {
939
1099
  const row = this.rowForLine(line);
1100
+
940
1101
  if (row.kind !== 'text') {
941
1102
  return TEXT_PADDING;
942
1103
  }
1104
+
943
1105
  const geometry = this.geometry(row);
944
1106
  let stop = 0;
1107
+
945
1108
  while (stop < geometry.text.length && isSpace(geometry.text[stop])) {
946
1109
  stop++;
947
1110
  }
1111
+
948
1112
  return TEXT_PADDING + xOfStop(geometry, Math.min(stop, stopCount(geometry) - 1));
949
1113
  }
950
1114
 
@@ -952,9 +1116,9 @@ export class EditorLayout {
952
1116
  return this.rows[this.lineRows[Math.max(0, Math.min(this.lineRows.length - 1, line))]!]!;
953
1117
  }
954
1118
 
955
- /* Whether a fold hides the line. */
956
1119
  isHidden(line: number): boolean {
957
1120
  const row = this.rowForLine(line);
1121
+
958
1122
  return row.kind === 'text' && line > row.line && line <= row.lastLine;
959
1123
  }
960
1124
 
@@ -962,14 +1126,17 @@ export class EditorLayout {
962
1126
  hides(offset: number): boolean {
963
1127
  const line = this.document.positionAt(offset).line;
964
1128
  const row = this.rowForLine(line);
1129
+
965
1130
  return row.kind === 'text' && (this.isHidden(line) || (row.head !== null && line === row.line && offset > row.head));
966
1131
  }
967
1132
 
968
1133
  visibleRows(top: number, height: number, overscan = 120): LayoutRow[] {
969
1134
  const result: LayoutRow[] = [];
1135
+
970
1136
  for (let index = this.rowAt(top - overscan).index; index < this.rows.length && this.rows[index]!.top < top + height + overscan; index++) {
971
1137
  result.push(this.rows[index]!);
972
1138
  }
1139
+
973
1140
  return result;
974
1141
  }
975
1142
 
@@ -978,6 +1145,7 @@ export class EditorLayout {
978
1145
  // The first line of a fold that starts within it, laid out up to there; seldom drawn, so never kept.
979
1146
  const line = this.document.getLine(row.line);
980
1147
  const shown = { ...line, end: row.head, text: line.text.slice(0, row.head - line.start) };
1148
+
981
1149
  return scanLine(
982
1150
  shown,
983
1151
  this.metrics,
@@ -985,18 +1153,25 @@ export class EditorLayout {
985
1153
  (this.inlaysByLine.get(row.line) ?? []).filter((inlay) => inlay.at <= row.head!)
986
1154
  );
987
1155
  }
1156
+
988
1157
  const cached = this.geometryCache.get(row.line);
1158
+
989
1159
  if (cached) {
990
1160
  return cached;
991
1161
  }
1162
+
992
1163
  const result = scanLine(this.document.getLine(row.line), this.metrics, this.wrapWidth, this.inlaysByLine.get(row.line) ?? []);
1164
+
993
1165
  if (this.wrapWidth === null) {
994
1166
  this.width = Math.max(this.width, result.width + ADDITIONAL_COLUMNS * this.metrics.charWidth);
995
1167
  }
1168
+
996
1169
  if (this.geometryCache.size >= GEOMETRY_CACHE_SIZE) {
997
1170
  this.geometryCache.delete(this.geometryCache.keys().next().value!);
998
1171
  }
1172
+
999
1173
  this.geometryCache.set(row.line, result);
1174
+
1000
1175
  return result;
1001
1176
  }
1002
1177
 
@@ -1014,25 +1189,30 @@ export class EditorLayout {
1014
1189
  */
1015
1190
  offsetAtX(geometry: LineGeometry, subRow: number, x: number): { offset: number; rowEnd: boolean } {
1016
1191
  const local = x - TEXT_PADDING;
1192
+
1017
1193
  for (const box of geometry.inlays) {
1018
1194
  if (box.subRow === subRow && local >= box.x && local <= box.x + box.width) {
1019
1195
  return { offset: box.inlay.at, rowEnd: false };
1020
1196
  }
1021
1197
  }
1198
+
1022
1199
  const first = geometry.rowStarts[subRow] ?? 0;
1023
1200
  const last = subRow + 1 < geometry.rowStarts.length ? geometry.rowStarts[subRow + 1]! : stopCount(geometry) - 1;
1024
1201
  const wrapped = subRow + 1 < geometry.rowStarts.length;
1025
1202
  let best = first;
1026
1203
  let distance = Math.abs(local - xOfStop(geometry, first));
1204
+
1027
1205
  // The stop that starts the next visual line stands in for the end of this one.
1028
1206
  for (let stop = first + 1; stop <= last; stop++) {
1029
1207
  const edge = stop === last && wrapped ? geometry.rowEnds[subRow]! : xOfStop(geometry, stop);
1030
1208
  const candidate = Math.abs(local - edge);
1209
+
1031
1210
  if (candidate < distance) {
1032
1211
  distance = candidate;
1033
1212
  best = stop;
1034
1213
  }
1035
1214
  }
1215
+
1036
1216
  return { offset: geometry.start + offsetOfStop(geometry, best), rowEnd: wrapped && best === last };
1037
1217
  }
1038
1218
 
@@ -1040,12 +1220,15 @@ export class EditorLayout {
1040
1220
  caret(offset: number, affinity: Affinity = 'after', rowEnd = false): LayoutRect {
1041
1221
  const position = this.document.positionAt(offset);
1042
1222
  const row = this.rowForLine(position.line);
1223
+
1043
1224
  if (row.kind !== 'text') {
1044
1225
  return { x: TEXT_PADDING, y: row.top, width: 1, height: Math.min(row.height, this.metrics.lineHeight) };
1045
1226
  }
1227
+
1046
1228
  const geometry = this.geometry(row);
1047
1229
  const stop = this.stopOf(geometry, offset);
1048
1230
  const subRow = rowOfStop(geometry, stop);
1231
+
1049
1232
  if (rowEnd && subRow > 0 && geometry.rowStarts[subRow] === stop) {
1050
1233
  return {
1051
1234
  x: TEXT_PADDING + geometry.rowEnds[subRow - 1]!,
@@ -1054,6 +1237,7 @@ export class EditorLayout {
1054
1237
  height: this.metrics.lineHeight
1055
1238
  };
1056
1239
  }
1240
+
1057
1241
  return {
1058
1242
  x: TEXT_PADDING + xOfStop(geometry, stop, affinity),
1059
1243
  y: row.top + subRow * this.metrics.lineHeight,
@@ -1065,10 +1249,13 @@ export class EditorLayout {
1065
1249
  /* A height snapped up to the top of the visual line it is in, so a page scrolls to whole lines. */
1066
1250
  lineBase(y: number): number {
1067
1251
  const row = this.rowAt(y);
1252
+
1068
1253
  if (row.kind !== 'text') {
1069
1254
  return y;
1070
1255
  }
1256
+
1071
1257
  const base = row.top + Math.floor((y - row.top) / this.metrics.lineHeight) * this.metrics.lineHeight;
1258
+
1072
1259
  return y > base && y < base + this.metrics.lineHeight ? base : y;
1073
1260
  }
1074
1261
 
@@ -1080,9 +1267,11 @@ export class EditorLayout {
1080
1267
  const position = this.document.positionAt(offset);
1081
1268
  const row = this.rowForLine(position.line);
1082
1269
  const line = this.document.getLine(row.kind === 'text' ? row.line : position.line);
1270
+
1083
1271
  if (row.kind !== 'text') {
1084
1272
  return { start: line.start, end: line.end, next: line.next, y: row.top };
1085
1273
  }
1274
+
1086
1275
  const geometry = this.geometry(row);
1087
1276
  const lastLine = this.document.getLine(row.lastLine);
1088
1277
  const hidden = position.line > row.line;
@@ -1090,6 +1279,7 @@ export class EditorLayout {
1090
1279
  const last = subRow + 1 >= geometry.subRows;
1091
1280
  const start = line.start + offsetOfStop(geometry, geometry.rowStarts[subRow]!);
1092
1281
  const end = last ? lastLine.end : line.start + offsetOfStop(geometry, geometry.rowStarts[subRow + 1]!);
1282
+
1093
1283
  return { start, end, next: last ? lastLine.next : end, y: row.top + subRow * this.metrics.lineHeight };
1094
1284
  }
1095
1285
 
@@ -1099,11 +1289,14 @@ export class EditorLayout {
1099
1289
 
1100
1290
  hitTestRow(x: number, y: number): { offset: number; rowEnd: boolean } {
1101
1291
  const row = this.rowAt(y);
1292
+
1102
1293
  if (row.kind === 'block') {
1103
1294
  return { offset: row.widget.at, rowEnd: false };
1104
1295
  }
1296
+
1105
1297
  const geometry = this.geometry(row);
1106
1298
  const subRow = Math.max(0, Math.min(geometry.subRows - 1, Math.floor((y - row.top) / this.metrics.lineHeight)));
1299
+
1107
1300
  return this.offsetAtX(geometry, subRow, x);
1108
1301
  }
1109
1302
 
@@ -1111,6 +1304,7 @@ export class EditorLayout {
1111
1304
  rectangles(from: number, to: number, rows: readonly LayoutRow[], window?: VerticalWindow): LayoutRect[] {
1112
1305
  const result: LayoutRect[] = [];
1113
1306
  const lineHeight = this.metrics.lineHeight;
1307
+
1114
1308
  for (const row of rows) {
1115
1309
  if (
1116
1310
  row.kind !== 'text' ||
@@ -1119,14 +1313,18 @@ export class EditorLayout {
1119
1313
  ) {
1120
1314
  continue;
1121
1315
  }
1316
+
1122
1317
  const geometry = this.geometry(row);
1318
+
1123
1319
  if (from === to || from > geometry.end || to <= geometry.start) {
1124
1320
  continue;
1125
1321
  }
1322
+
1126
1323
  const lastStop = stopCount(geometry) - 1;
1127
1324
  const fromStop = this.stopOf(geometry, Math.max(from, geometry.start));
1128
1325
  const toStop = this.stopOf(geometry, Math.min(to, geometry.end));
1129
1326
  const band = window === undefined ? { first: 0, last: geometry.subRows - 1 } : subRowBand(row.top, geometry.subRows, lineHeight, window);
1327
+
1130
1328
  for (let subRow = band.first; subRow <= band.last; subRow++) {
1131
1329
  const last = subRow + 1 === geometry.subRows;
1132
1330
  const rowFirst = geometry.rowStarts[subRow]!;
@@ -1134,11 +1332,14 @@ export class EditorLayout {
1134
1332
  const first = Math.max(fromStop, rowFirst);
1135
1333
  const stop = Math.min(toStop, rowEnd);
1136
1334
  const newline = last && to > geometry.end;
1335
+
1137
1336
  if (first >= stop && !newline) {
1138
1337
  continue;
1139
1338
  }
1339
+
1140
1340
  const startX = first === fromStop ? xOfStop(geometry, first, 'after') : xOfStop(geometry, first);
1141
1341
  const endX = stop < rowEnd || last ? xOfStop(geometry, stop) : geometry.rowEnds[subRow]!;
1342
+
1142
1343
  result.push({
1143
1344
  x: TEXT_PADDING + startX,
1144
1345
  y: row.top + subRow * lineHeight,
@@ -1147,6 +1348,7 @@ export class EditorLayout {
1147
1348
  });
1148
1349
  }
1149
1350
  }
1351
+
1150
1352
  return result;
1151
1353
  }
1152
1354
 
@@ -1158,43 +1360,57 @@ export class EditorLayout {
1158
1360
  verticalOffset(offset: number, direction: -1 | 1, x: number, distance = this.metrics.lineHeight): number {
1159
1361
  const caret = this.caret(offset);
1160
1362
  let y = caret.y + direction * distance;
1363
+
1161
1364
  if (y < 0) {
1162
1365
  return 0;
1163
1366
  }
1367
+
1164
1368
  let row = this.rowAt(y);
1369
+
1165
1370
  while (row.kind === 'block' || (row.replacement !== undefined && row.replacement.enterable !== true)) {
1166
1371
  const next = this.rows[row.index + direction];
1372
+
1167
1373
  if (!next) {
1168
1374
  return direction < 0 ? 0 : this.document.getLine(this.document.getLineCount() - 1).end;
1169
1375
  }
1376
+
1170
1377
  row = next;
1171
1378
  y = direction < 0 ? row.top + row.height - 1 : row.top;
1172
1379
  }
1380
+
1173
1381
  if (y >= this.height - BOTTOM_PADDING) {
1174
1382
  return this.document.getLine(this.document.getLineCount() - 1).end;
1175
1383
  }
1384
+
1176
1385
  return this.hitTest(x, y);
1177
1386
  }
1178
1387
 
1179
1388
  /* One character left or right, over the end of a line onto the next visible one. */
1180
1389
  horizontalOffset(offset: number, direction: -1 | 1): number {
1181
1390
  const row = this.rowForLine(this.document.positionAt(offset).line);
1391
+
1182
1392
  if (row.kind === 'text') {
1183
1393
  const geometry = this.geometry(row);
1184
1394
  const stop = this.stopOf(geometry, offset);
1185
1395
  const next = stop + direction;
1396
+
1186
1397
  if (next >= 0 && next < stopCount(geometry) && offset >= geometry.start && offset <= geometry.end) {
1187
1398
  return geometry.start + offsetOfStop(geometry, next);
1188
1399
  }
1189
1400
  }
1401
+
1190
1402
  for (let index = row.index + direction; index >= 0 && index < this.rows.length; index += direction) {
1191
1403
  const next = this.rows[index]!;
1404
+
1192
1405
  if (next.kind !== 'text') {
1193
1406
  continue;
1194
1407
  }
1408
+
1195
1409
  const line = this.document.getLine(next.line);
1410
+
1196
1411
  return direction < 0 ? line.end : line.start;
1197
1412
  }
1413
+
1198
1414
  return offset;
1199
1415
  }
1200
1416
  }