@adecore/editor 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/dist/attribution.d.ts +9 -8
  2. package/dist/attribution.js +44 -9
  3. package/dist/bidi.d.ts +44 -0
  4. package/dist/bidi.js +26 -0
  5. package/dist/column-selection.js +2 -1
  6. package/dist/controller.d.ts +6 -1
  7. package/dist/controller.js +93 -3
  8. package/dist/editor.css +81 -1
  9. package/dist/engine.js +184 -41
  10. package/dist/fake.d.ts +43 -5
  11. package/dist/fake.js +117 -13
  12. package/dist/find-search.d.ts +26 -0
  13. package/dist/find-search.js +130 -0
  14. package/dist/find.d.ts +50 -2
  15. package/dist/find.js +575 -25
  16. package/dist/geometry-stops.d.ts +6 -0
  17. package/dist/geometry-stops.js +47 -0
  18. package/dist/grammar-preparation.d.ts +29 -0
  19. package/dist/grammar-preparation.js +147 -0
  20. package/dist/index.d.ts +3 -1
  21. package/dist/index.js +2 -0
  22. package/dist/keymap-table.d.ts +10 -1
  23. package/dist/keymap-table.js +37 -1
  24. package/dist/keymap.d.ts +1 -0
  25. package/dist/keymap.js +4 -1
  26. package/dist/layout-changes.d.ts +6 -0
  27. package/dist/layout-changes.js +27 -0
  28. package/dist/layout.d.ts +45 -2
  29. package/dist/layout.js +202 -58
  30. package/dist/offsets.d.ts +1 -0
  31. package/dist/offsets.js +23 -0
  32. package/dist/outline.js +5 -4
  33. package/dist/paint.d.ts +11 -3
  34. package/dist/paint.js +180 -34
  35. package/dist/search-client.d.ts +27 -0
  36. package/dist/search-client.js +210 -0
  37. package/dist/search-protocol.d.ts +103 -0
  38. package/dist/search-protocol.js +3 -0
  39. package/dist/search-worker.d.ts +11 -0
  40. package/dist/search-worker.js +137 -0
  41. package/dist/semantic.d.ts +2 -1
  42. package/dist/semantic.js +6 -1
  43. package/dist/shiki-document.d.ts +8 -0
  44. package/dist/shiki-document.js +93 -0
  45. package/dist/shiki-worker.d.ts +65 -0
  46. package/dist/shiki-worker.js +265 -0
  47. package/dist/shiki.d.ts +1 -0
  48. package/dist/shiki.js +96 -33
  49. package/dist/testing.d.ts +1 -0
  50. package/dist/tokens.d.ts +2 -0
  51. package/dist/tokens.js +37 -7
  52. package/dist/touch.d.ts +27 -0
  53. package/dist/touch.js +100 -0
  54. package/dist/types.d.ts +84 -2
  55. package/dist/uniform-geometry.d.ts +7 -0
  56. package/dist/uniform-geometry.js +49 -0
  57. package/dist/view.d.ts +68 -17
  58. package/dist/view.js +424 -97
  59. package/dist/worker-client.d.ts +37 -0
  60. package/dist/worker-client.js +195 -0
  61. package/package.json +17 -2
  62. package/src/attribution.ts +50 -14
  63. package/src/bidi.ts +32 -0
  64. package/src/column-selection.ts +2 -1
  65. package/src/controller.ts +97 -3
  66. package/src/editor.css +81 -1
  67. package/src/engine.ts +216 -42
  68. package/src/fake.ts +152 -14
  69. package/src/find-search.ts +165 -0
  70. package/src/find.ts +622 -21
  71. package/src/geometry-stops.ts +51 -0
  72. package/src/grammar-preparation.ts +158 -0
  73. package/src/index.ts +10 -0
  74. package/src/keymap-table.ts +44 -2
  75. package/src/keymap.ts +5 -1
  76. package/src/layout-changes.ts +33 -0
  77. package/src/layout.ts +262 -63
  78. package/src/offsets.ts +24 -0
  79. package/src/outline.ts +5 -4
  80. package/src/paint.ts +202 -36
  81. package/src/search-client.ts +306 -0
  82. package/src/search-protocol.ts +100 -0
  83. package/src/search-worker.ts +171 -0
  84. package/src/semantic.ts +8 -2
  85. package/src/shiki-document.ts +143 -0
  86. package/src/shiki-worker.ts +344 -0
  87. package/src/shiki.ts +103 -33
  88. package/src/testing.ts +2 -0
  89. package/src/tokens.ts +36 -7
  90. package/src/touch.ts +134 -0
  91. package/src/types.ts +174 -8
  92. package/src/uniform-geometry.ts +57 -0
  93. package/src/view.ts +500 -102
  94. package/src/worker-client.ts +212 -0
package/dist/layout.js CHANGED
@@ -1,3 +1,15 @@
1
+ import { offsetOfStop, rowOfStop, stopAtOffset, stopCount, xOfStop } from './geometry-stops.js';
2
+ import { BIDI_COLUMNS, bidiControl, visibleBidi } from './bidi.js';
3
+ import { uniformGeometry } from './uniform-geometry.js';
4
+ import { lineChangeMapper } from './layout-changes.js';
5
+ export function subRowBand(top, subRows, lineHeight, window, block = 1) {
6
+ const first = Math.max(0, Math.min(subRows - 1, Math.floor((window.top - top) / lineHeight)));
7
+ const last = Math.max(first, Math.min(subRows - 1, Math.ceil((window.bottom - top) / lineHeight) - 1));
8
+ return {
9
+ first: Math.floor(first / block) * block,
10
+ last: Math.min(subRows - 1, Math.ceil((last + 1) / block) * block - 1)
11
+ };
12
+ }
1
13
  export const TEXT_PADDING = 0;
2
14
  export const BOTTOM_PADDING = 8;
3
15
  export const RIGHT_PADDING = 16;
@@ -10,20 +22,6 @@ const GEOMETRY_CACHE_SIZE = 400;
10
22
  const PLAIN = /^[\x20-\x7e\t]*$/;
11
23
  const GRAPHEME_LIMIT = 20000;
12
24
  const SEGMENTER = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
13
- function lowerBound(values, value) {
14
- let low = 0;
15
- let high = values.length;
16
- while (low < high) {
17
- const middle = (low + high) >>> 1;
18
- if (values[middle] < value) {
19
- low = middle + 1;
20
- }
21
- else {
22
- high = middle;
23
- }
24
- }
25
- return low;
26
- }
27
25
  /* 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. */
28
26
  function characterOffsets(text) {
29
27
  const offsets = [];
@@ -48,7 +46,10 @@ function characterWidth(metrics, text, x) {
48
46
  const tab = metrics.tabSize * metrics.charWidth;
49
47
  return (Math.floor(x / tab) + 1) * tab - x;
50
48
  }
51
- return text.length === 1 && text >= ' ' && text <= '~' ? metrics.charWidth : metrics.measureText(text);
49
+ if (text.length === 1 && text >= ' ' && text <= '~') {
50
+ return metrics.charWidth;
51
+ }
52
+ return bidiControl(text) !== null ? BIDI_COLUMNS * metrics.charWidth : metrics.measureText(text);
52
53
  }
53
54
  /* Where the continuation rows of a wrapped line start, kept to half the row so a deep indent never leaves no room for text. */
54
55
  export function wrapIndent(text, metrics, limit) {
@@ -65,20 +66,22 @@ export function wrapIndent(text, metrics, limit) {
65
66
  * The stops at which a wrapped line starts a new visual line, after the space a word ends in when
66
67
  * there is one on the visual line and in the middle of a word when there is not.
67
68
  */
68
- export function wrapStops(text, offsets, metrics, limit) {
69
+ function wrapRows(text, offsets, metrics, limit) {
69
70
  const indent = wrapIndent(text, metrics, limit);
70
71
  const breaks = [];
71
- const count = offsets.length - 1;
72
+ const triggers = [];
73
+ const count = offsets === null ? text.length : offsets.length - 1;
72
74
  let rowStart = 0;
73
75
  let opportunity = -1;
74
76
  let x = 0;
75
77
  let index = 0;
76
78
  while (index < count) {
77
- const character = text.slice(offsets[index], offsets[index + 1]);
79
+ const character = offsets === null ? text[index] : text.slice(offsets[index], offsets[index + 1]);
78
80
  const width = characterWidth(metrics, character, x);
79
81
  if (index > rowStart && x + width > limit && !isSpace(character)) {
80
82
  const target = opportunity > rowStart ? opportunity : index;
81
83
  breaks.push(target);
84
+ triggers.push(index);
82
85
  rowStart = target;
83
86
  opportunity = -1;
84
87
  x = indent;
@@ -86,22 +89,28 @@ export function wrapStops(text, offsets, metrics, limit) {
86
89
  continue;
87
90
  }
88
91
  x += width;
89
- if (isSpace(character) && index + 1 < count && !isSpace(text[offsets[index + 1]])) {
92
+ if (isSpace(character) && index + 1 < count && !isSpace(text[offsets === null ? index + 1 : offsets[index + 1]])) {
90
93
  opportunity = index + 1;
91
94
  }
92
95
  index++;
93
96
  }
94
- return breaks;
97
+ triggers.push(count);
98
+ return { breaks, triggers };
99
+ }
100
+ export function wrapStops(text, offsets, metrics, limit) {
101
+ return wrapRows(text, offsets, metrics, limit).breaks;
95
102
  }
96
103
  /* A line of text laid out against its metrics; wrapped at `limit` pixels unless that is null. */
97
- export function scanLine(line, metrics, limit, lineInlays) {
104
+ export function scanDenseLine(line, metrics, limit, lineInlays) {
98
105
  const text = line.text;
99
106
  const offsets = characterOffsets(text);
100
- const breakStops = limit === null ? [] : wrapStops(text, offsets, metrics, limit);
107
+ const wrapped = limit === null ? { breaks: [], triggers: [offsets.length - 1] } : wrapRows(text, offsets, metrics, limit);
108
+ const breakStops = wrapped.breaks;
101
109
  const indent = limit === null ? 0 : wrapIndent(text, metrics, limit);
102
110
  const geometry = {
103
111
  start: line.start,
104
112
  end: line.end,
113
+ next: line.next ?? line.end,
105
114
  text,
106
115
  offsets,
107
116
  before: [],
@@ -109,6 +118,7 @@ export function scanLine(line, metrics, limit, lineInlays) {
109
118
  rowOf: [],
110
119
  rowStarts: [0],
111
120
  rowEnds: [],
121
+ rowTriggers: wrapped.triggers,
112
122
  runs: [],
113
123
  inlays: [],
114
124
  subRows: breakStops.length + 1,
@@ -122,7 +132,7 @@ export function scanLine(line, metrics, limit, lineInlays) {
122
132
  let runX = 0;
123
133
  const flushRun = (end) => {
124
134
  if (runStart >= 0 && end > runStart) {
125
- geometry.runs.push({ from: line.start + runStart, to: line.start + end, x: runX, subRow: row, text: text.slice(runStart, end) });
135
+ geometry.runs.push({ from: runStart, to: end, x: runX, subRow: row, text: text.slice(runStart, end) });
126
136
  }
127
137
  runStart = -1;
128
138
  };
@@ -141,7 +151,10 @@ export function scanLine(line, metrics, limit, lineInlays) {
141
151
  while (nextInlay < lineInlays.length && lineInlays[nextInlay].at - line.start <= offset) {
142
152
  flushRun(offset);
143
153
  const inlay = lineInlays[nextInlay++];
144
- const width = inlay.composition || inlay.ghost ? metrics.measureText(inlay.text) : Math.max(8, metrics.measureInlay(inlay.text) + 12);
154
+ const shown = visibleBidi(inlay.text);
155
+ const width = inlay.composition || inlay.ghost
156
+ ? metrics.measureText(shown)
157
+ : Math.max(8, metrics.measureInlay(shown) + 12) + (inlay.gapLeft ?? 0) + (inlay.gapRight ?? 0);
145
158
  geometry.inlays.push({ inlay, x, width, subRow: row });
146
159
  x += width;
147
160
  }
@@ -151,7 +164,7 @@ export function scanLine(line, metrics, limit, lineInlays) {
151
164
  }
152
165
  const character = text.slice(offset, offsets[stop + 1]);
153
166
  const width = characterWidth(metrics, character, x);
154
- if (character === '\t') {
167
+ if (character === '\t' || bidiControl(character) !== null) {
155
168
  flushRun(offset);
156
169
  }
157
170
  else if (runStart < 0) {
@@ -162,9 +175,21 @@ export function scanLine(line, metrics, limit, lineInlays) {
162
175
  }
163
176
  flushRun(text.length);
164
177
  geometry.rowEnds.push(x);
165
- geometry.width = Math.max(...geometry.rowEnds);
178
+ for (const end of geometry.rowEnds) {
179
+ geometry.width = Math.max(geometry.width, end);
180
+ }
166
181
  return geometry;
167
182
  }
183
+ export function scanLine(line, metrics, limit, lineInlays) {
184
+ if (limit !== null && line.text.length > GRAPHEME_LIMIT && lineInlays.length === 0 && /^[\x20-\x7e]*$/.test(line.text)) {
185
+ const wrapped = wrapRows(line.text, null, metrics, limit);
186
+ const geometry = uniformGeometry(line, metrics, wrapped.breaks, wrapped.triggers, wrapIndent(line.text, metrics, limit));
187
+ if (geometry !== null) {
188
+ return geometry;
189
+ }
190
+ }
191
+ return scanDenseLine(line, metrics, limit, lineInlays);
192
+ }
168
193
  /*
169
194
  * The rows of the document as they are drawn: one per line, taller for a wrapped line, one for a
170
195
  * widget, none for a line a fold hides. Only the rows that are on screen are ever measured; the
@@ -173,6 +198,7 @@ export function scanLine(line, metrics, limit, lineInlays) {
173
198
  export class EditorLayout {
174
199
  rows = [];
175
200
  height = 0;
201
+ lineHeightsVersion = 0;
176
202
  /* The widest line measured since the text last changed, and the room past it; what a horizontal scroll bar needs. */
177
203
  width = 0;
178
204
  lineRows = [];
@@ -183,6 +209,7 @@ export class EditorLayout {
183
209
  inlays = [];
184
210
  blocks = [];
185
211
  folds = [];
212
+ replacements = [];
186
213
  wrapWidth = null;
187
214
  document;
188
215
  metrics;
@@ -207,21 +234,79 @@ export class EditorLayout {
207
234
  }
208
235
  if (config.inlays) {
209
236
  this.inlays = config.inlays;
210
- measureAgain = true;
211
237
  }
212
238
  if (config.blocks) {
213
239
  this.blocks = config.blocks;
214
240
  }
215
241
  if (config.folds) {
216
242
  this.folds = config.folds;
243
+ this.lineHeightsVersion++;
217
244
  }
218
- if (measureAgain || config.textChanged) {
245
+ if (config.replacements) {
246
+ this.replacements = config.replacements;
247
+ this.lineHeightsVersion++;
248
+ }
249
+ if (measureAgain || (config.textChanged && !config.changes?.length)) {
250
+ this.lineHeightsVersion++;
219
251
  this.geometryCache.clear();
220
252
  this.measuredRows.clear();
221
253
  this.width = 0;
222
254
  }
255
+ else {
256
+ if (config.changes?.length) {
257
+ this.retainGeometry(config.changes);
258
+ this.lineHeightsVersion++;
259
+ }
260
+ if (config.inlays !== undefined) {
261
+ if (!config.changes?.length) {
262
+ for (const line of this.inlaysByLine.keys()) {
263
+ this.geometryCache.delete(line);
264
+ this.measuredRows.delete(line);
265
+ }
266
+ }
267
+ for (const inlay of config.inlays) {
268
+ const line = this.document.positionAt(inlay.at).line;
269
+ this.geometryCache.delete(line);
270
+ this.measuredRows.delete(line);
271
+ }
272
+ this.lineHeightsVersion++;
273
+ }
274
+ this.width = 0;
275
+ if (this.wrapWidth === null) {
276
+ for (const geometry of this.geometryCache.values()) {
277
+ this.width = Math.max(this.width, geometry.width + ADDITIONAL_COLUMNS * this.metrics.charWidth);
278
+ }
279
+ }
280
+ }
223
281
  this.rebuild();
224
282
  }
283
+ retainGeometry(batches) {
284
+ const map = lineChangeMapper(batches);
285
+ const remap = (bounds) => {
286
+ const next = map({ start: bounds.start, end: Math.max(bounds.end, bounds.next - 1) });
287
+ if (next === null) {
288
+ return null;
289
+ }
290
+ const delta = next.start - bounds.start;
291
+ return { start: next.start, end: bounds.end + delta, next: bounds.next + delta, line: this.document.positionAt(next.start).line };
292
+ };
293
+ const geometries = new Map();
294
+ for (const geometry of this.geometryCache.values()) {
295
+ const next = remap(geometry);
296
+ if (next !== null && geometry.inlays.length === 0) {
297
+ geometries.set(next.line, next.start === geometry.start ? geometry : { ...geometry, start: next.start, end: next.end, next: next.next });
298
+ }
299
+ }
300
+ const measured = new Map();
301
+ for (const [line, bounds] of this.measuredRows) {
302
+ const next = remap(bounds);
303
+ if (next !== null && !this.inlaysByLine.has(line)) {
304
+ measured.set(next.line, { start: next.start, end: next.end, next: next.next, subRows: bounds.subRows });
305
+ }
306
+ }
307
+ this.geometryCache = geometries;
308
+ this.measuredRows = measured;
309
+ }
225
310
  rowCountOf(line, length) {
226
311
  const limit = this.wrapWidth;
227
312
  if (limit === null) {
@@ -229,7 +314,7 @@ export class EditorLayout {
229
314
  }
230
315
  const measured = this.measuredRows.get(line);
231
316
  if (measured !== undefined) {
232
- return measured;
317
+ return measured.subRows;
233
318
  }
234
319
  return Math.max(1, Math.ceil((length * this.metrics.charWidth) / Math.max(limit, this.metrics.charWidth)));
235
320
  }
@@ -257,6 +342,15 @@ export class EditorLayout {
257
342
  folds.set(fold.startLine, fold);
258
343
  }
259
344
  }
345
+ const replaced = new Map();
346
+ for (const replacement of this.replacements) {
347
+ const startLine = this.document.positionAt(replacement.from).line;
348
+ const endLine = Math.max(startLine, this.document.positionAt(replacement.to).line);
349
+ // A fold the person collapsed shows its own placeholder, so folding a replaced comment keeps working.
350
+ if (!folds.has(startLine) && !replaced.has(startLine)) {
351
+ replaced.set(startLine, { replacement, endLine });
352
+ }
353
+ }
260
354
  const count = this.document.getLineCount();
261
355
  this.rows = [];
262
356
  this.lineRows = new Array(count);
@@ -269,13 +363,44 @@ export class EditorLayout {
269
363
  line,
270
364
  widget,
271
365
  top: 0,
272
- height: widget.lens ? this.metrics.lineHeight : (this.measuredHeights.get(key) ?? widget.height ?? DEFAULT_BLOCK_HEIGHT)
366
+ height: widget.lens
367
+ ? this.metrics.lineHeight
368
+ : widget.fixed
369
+ ? Math.max(1, widget.height ?? DEFAULT_BLOCK_HEIGHT)
370
+ : (this.measuredHeights.get(key) ?? widget.height ?? DEFAULT_BLOCK_HEIGHT)
273
371
  });
274
372
  };
275
373
  for (let line = 0; line < count; line++) {
276
374
  for (const block of above.get(line) ?? []) {
277
375
  addBlock(block, line);
278
376
  }
377
+ const standIn = replaced.get(line);
378
+ if (standIn !== undefined) {
379
+ const last = Math.min(count - 1, standIn.endLine);
380
+ const key = `replaced:${standIn.replacement.id}`;
381
+ const index = this.rows.length;
382
+ this.rows.push({
383
+ key,
384
+ kind: 'text',
385
+ index,
386
+ line,
387
+ lastLine: last,
388
+ tail: null,
389
+ head: null,
390
+ subRows: 1,
391
+ top: 0,
392
+ height: this.measuredHeights.get(key) ?? standIn.replacement.height ?? DEFAULT_BLOCK_HEIGHT,
393
+ replacement: standIn.replacement
394
+ });
395
+ for (let covered = line; covered <= last; covered++) {
396
+ this.lineRows[covered] = index;
397
+ }
398
+ for (const block of below.get(last) ?? []) {
399
+ addBlock(block, last);
400
+ }
401
+ line = last;
402
+ continue;
403
+ }
279
404
  const fold = folds.get(line);
280
405
  const last = Math.max(line, Math.min(count - 1, fold?.endLine ?? line));
281
406
  const tail = fold?.closer !== undefined && last > line ? this.clampToLine(fold.closer, last) : null;
@@ -322,7 +447,7 @@ export class EditorLayout {
322
447
  setMeasuredHeight(key, height) {
323
448
  const row = this.rows.find((candidate) => candidate.key === key);
324
449
  const next = Math.max(1, Math.round(height));
325
- if (!row || !Number.isFinite(next) || row.height === next) {
450
+ if (!row || (row.kind === 'block' && row.widget.fixed) || !Number.isFinite(next) || row.height === next) {
326
451
  return false;
327
452
  }
328
453
  this.measuredHeights.set(key, next);
@@ -334,18 +459,28 @@ export class EditorLayout {
334
459
  syncRows(rows) {
335
460
  let changed = false;
336
461
  for (const row of rows) {
337
- if (row.kind !== 'text') {
462
+ if (row.kind !== 'text' || row.replacement !== undefined) {
338
463
  continue;
339
464
  }
340
- const subRows = this.geometry(row).subRows;
465
+ const geometry = this.geometry(row);
466
+ const subRows = geometry.subRows;
467
+ if (row.head === null && this.wrapWidth !== null) {
468
+ if (!this.measuredRows.has(row.line) && this.measuredRows.size >= GEOMETRY_CACHE_SIZE) {
469
+ this.measuredRows.delete(this.measuredRows.keys().next().value);
470
+ }
471
+ this.measuredRows.set(row.line, { start: geometry.start, end: geometry.end, next: geometry.next, subRows });
472
+ }
473
+ else {
474
+ this.measuredRows.delete(row.line);
475
+ }
341
476
  if (subRows !== row.subRows) {
342
- this.measuredRows.set(row.line, subRows);
343
477
  row.subRows = subRows;
344
478
  row.height = subRows * this.metrics.lineHeight;
345
479
  changed = true;
346
480
  }
347
481
  }
348
482
  if (changed) {
483
+ this.lineHeightsVersion++;
349
484
  this.reflow();
350
485
  }
351
486
  return changed;
@@ -375,7 +510,7 @@ export class EditorLayout {
375
510
  while (stop < geometry.text.length && isSpace(geometry.text[stop])) {
376
511
  stop++;
377
512
  }
378
- return TEXT_PADDING + geometry.before[Math.min(stop, geometry.before.length - 1)];
513
+ return TEXT_PADDING + xOfStop(geometry, Math.min(stop, stopCount(geometry) - 1));
379
514
  }
380
515
  rowForLine(line) {
381
516
  return this.rows[this.lineRows[Math.max(0, Math.min(this.lineRows.length - 1, line))]];
@@ -420,10 +555,10 @@ export class EditorLayout {
420
555
  return result;
421
556
  }
422
557
  stopOf(geometry, offset) {
423
- return Math.min(geometry.offsets.length - 1, lowerBound(geometry.offsets, Math.max(0, offset - geometry.start)));
558
+ return stopAtOffset(geometry, Math.max(0, offset - geometry.start));
424
559
  }
425
560
  xAt(geometry, offset, affinity = 'after') {
426
- return TEXT_PADDING + geometry[affinity][this.stopOf(geometry, offset)];
561
+ return TEXT_PADDING + xOfStop(geometry, this.stopOf(geometry, offset), affinity);
427
562
  }
428
563
  /*
429
564
  * The offset nearest an x on one visual line of a laid out line. `rowEnd` is set when that is the
@@ -437,20 +572,20 @@ export class EditorLayout {
437
572
  }
438
573
  }
439
574
  const first = geometry.rowStarts[subRow] ?? 0;
440
- const last = subRow + 1 < geometry.rowStarts.length ? geometry.rowStarts[subRow + 1] : geometry.offsets.length - 1;
575
+ const last = subRow + 1 < geometry.rowStarts.length ? geometry.rowStarts[subRow + 1] : stopCount(geometry) - 1;
441
576
  const wrapped = subRow + 1 < geometry.rowStarts.length;
442
577
  let best = first;
443
- let distance = Math.abs(local - geometry.before[first]);
578
+ let distance = Math.abs(local - xOfStop(geometry, first));
444
579
  // The stop that starts the next visual line stands in for the end of this one.
445
580
  for (let stop = first + 1; stop <= last; stop++) {
446
- const edge = stop === last && wrapped ? geometry.rowEnds[subRow] : geometry.before[stop];
581
+ const edge = stop === last && wrapped ? geometry.rowEnds[subRow] : xOfStop(geometry, stop);
447
582
  const candidate = Math.abs(local - edge);
448
583
  if (candidate < distance) {
449
584
  distance = candidate;
450
585
  best = stop;
451
586
  }
452
587
  }
453
- return { offset: geometry.start + geometry.offsets[best], rowEnd: wrapped && best === last };
588
+ return { offset: geometry.start + offsetOfStop(geometry, best), rowEnd: wrapped && best === last };
454
589
  }
455
590
  /* With `rowEnd`, an offset a wrap starts a visual line at is drawn at the end of the line before it. */
456
591
  caret(offset, affinity = 'after', rowEnd = false) {
@@ -461,7 +596,7 @@ export class EditorLayout {
461
596
  }
462
597
  const geometry = this.geometry(row);
463
598
  const stop = this.stopOf(geometry, offset);
464
- const subRow = geometry.rowOf[stop];
599
+ const subRow = rowOfStop(geometry, stop);
465
600
  if (rowEnd && subRow > 0 && geometry.rowStarts[subRow] === stop) {
466
601
  return {
467
602
  x: TEXT_PADDING + geometry.rowEnds[subRow - 1],
@@ -470,7 +605,12 @@ export class EditorLayout {
470
605
  height: this.metrics.lineHeight
471
606
  };
472
607
  }
473
- return { x: TEXT_PADDING + geometry[affinity][stop], y: row.top + subRow * this.metrics.lineHeight, width: 1, height: this.metrics.lineHeight };
608
+ return {
609
+ x: TEXT_PADDING + xOfStop(geometry, stop, affinity),
610
+ y: row.top + subRow * this.metrics.lineHeight,
611
+ width: 1,
612
+ height: this.metrics.lineHeight
613
+ };
474
614
  }
475
615
  /* A height snapped up to the top of the visual line it is in, so a page scrolls to whole lines. */
476
616
  lineBase(y) {
@@ -495,10 +635,10 @@ export class EditorLayout {
495
635
  const geometry = this.geometry(row);
496
636
  const lastLine = this.document.getLine(row.lastLine);
497
637
  const hidden = position.line > row.line;
498
- const subRow = hidden ? geometry.subRows - 1 : geometry.rowOf[this.stopOf(geometry, offset)];
638
+ const subRow = hidden ? geometry.subRows - 1 : rowOfStop(geometry, this.stopOf(geometry, offset));
499
639
  const last = subRow + 1 >= geometry.subRows;
500
- const start = line.start + geometry.offsets[geometry.rowStarts[subRow]];
501
- const end = last ? lastLine.end : line.start + geometry.offsets[geometry.rowStarts[subRow + 1]];
640
+ const start = line.start + offsetOfStop(geometry, geometry.rowStarts[subRow]);
641
+ const end = last ? lastLine.end : line.start + offsetOfStop(geometry, geometry.rowStarts[subRow + 1]);
502
642
  return { start, end, next: last ? lastLine.next : end, y: row.top + subRow * this.metrics.lineHeight };
503
643
  }
504
644
  hitTest(x, y) {
@@ -514,21 +654,24 @@ export class EditorLayout {
514
654
  return this.offsetAtX(geometry, subRow, x);
515
655
  }
516
656
  /* The boxes a range is drawn in, one per visual line it touches, among the rows given. */
517
- rectangles(from, to, rows) {
657
+ rectangles(from, to, rows, window) {
518
658
  const result = [];
519
659
  const lineHeight = this.metrics.lineHeight;
520
660
  for (const row of rows) {
521
- if (row.kind !== 'text') {
661
+ if (row.kind !== 'text' ||
662
+ row.replacement !== undefined ||
663
+ (window !== undefined && (row.top >= window.bottom || row.top + row.height <= window.top))) {
522
664
  continue;
523
665
  }
524
666
  const geometry = this.geometry(row);
525
667
  if (from === to || from > geometry.end || to <= geometry.start) {
526
668
  continue;
527
669
  }
528
- const lastStop = geometry.offsets.length - 1;
670
+ const lastStop = stopCount(geometry) - 1;
529
671
  const fromStop = this.stopOf(geometry, Math.max(from, geometry.start));
530
672
  const toStop = this.stopOf(geometry, Math.min(to, geometry.end));
531
- for (let subRow = 0; subRow < geometry.subRows; subRow++) {
673
+ const band = window === undefined ? { first: 0, last: geometry.subRows - 1 } : subRowBand(row.top, geometry.subRows, lineHeight, window);
674
+ for (let subRow = band.first; subRow <= band.last; subRow++) {
532
675
  const last = subRow + 1 === geometry.subRows;
533
676
  const rowFirst = geometry.rowStarts[subRow];
534
677
  const rowEnd = last ? lastStop : geometry.rowStarts[subRow + 1];
@@ -538,8 +681,8 @@ export class EditorLayout {
538
681
  if (first >= stop && !newline) {
539
682
  continue;
540
683
  }
541
- const startX = first === fromStop ? geometry.after[first] : geometry.before[first];
542
- const endX = stop < rowEnd || last ? geometry.before[stop] : geometry.rowEnds[subRow];
684
+ const startX = first === fromStop ? xOfStop(geometry, first, 'after') : xOfStop(geometry, first);
685
+ const endX = stop < rowEnd || last ? xOfStop(geometry, stop) : geometry.rowEnds[subRow];
543
686
  result.push({
544
687
  x: TEXT_PADDING + startX,
545
688
  y: row.top + subRow * lineHeight,
@@ -551,8 +694,9 @@ export class EditorLayout {
551
694
  return result;
552
695
  }
553
696
  /*
554
- * The offset a visual line above or below, `distance` pixels away, at an x. Widgets are walked
555
- * over, and the document's ends stop the caret on the first or the last offset.
697
+ * The offset a visual line above or below, `distance` pixels away, at an x. Widgets and the rows of
698
+ * replaced lines, unless the host made them enterable, are walked over, and the document's ends stop
699
+ * the caret on the first or the last offset.
556
700
  */
557
701
  verticalOffset(offset, direction, x, distance = this.metrics.lineHeight) {
558
702
  const caret = this.caret(offset);
@@ -561,10 +705,10 @@ export class EditorLayout {
561
705
  return 0;
562
706
  }
563
707
  let row = this.rowAt(y);
564
- while (row.kind === 'block') {
708
+ while (row.kind === 'block' || (row.replacement !== undefined && row.replacement.enterable !== true)) {
565
709
  const next = this.rows[row.index + direction];
566
710
  if (!next) {
567
- return offset;
711
+ return direction < 0 ? 0 : this.document.getLine(this.document.getLineCount() - 1).end;
568
712
  }
569
713
  row = next;
570
714
  y = direction < 0 ? row.top + row.height - 1 : row.top;
@@ -581,8 +725,8 @@ export class EditorLayout {
581
725
  const geometry = this.geometry(row);
582
726
  const stop = this.stopOf(geometry, offset);
583
727
  const next = stop + direction;
584
- if (next >= 0 && next < geometry.offsets.length && offset >= geometry.start && offset <= geometry.end) {
585
- return geometry.start + geometry.offsets[next];
728
+ if (next >= 0 && next < stopCount(geometry) && offset >= geometry.start && offset <= geometry.end) {
729
+ return geometry.start + offsetOfStop(geometry, next);
586
730
  }
587
731
  }
588
732
  for (let index = row.index + direction; index >= 0 && index < this.rows.length; index += direction) {
package/dist/offsets.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  import type { DocumentChange } from '@adecore/editor-core';
2
2
  export declare function mapOffset(offset: number, changes: readonly DocumentChange[]): number;
3
+ export declare function offsetMapper(changes: readonly DocumentChange[]): (offset: number) => number;
package/dist/offsets.js CHANGED
@@ -12,3 +12,26 @@ export function mapOffset(offset, changes) {
12
12
  }
13
13
  return offset + delta;
14
14
  }
15
+ /* Build once per sorted, nonoverlapping batch when many offsets need to follow it. */
16
+ export function offsetMapper(changes) {
17
+ const deltas = new Float64Array(changes.length + 1);
18
+ for (let index = 0; index < changes.length; index++) {
19
+ const change = changes[index];
20
+ deltas[index + 1] = deltas[index] + change.insertedLength - (change.to - change.from);
21
+ }
22
+ return (offset) => {
23
+ let low = 0, high = changes.length;
24
+ while (low < high) {
25
+ const middle = (low + high) >>> 1;
26
+ const change = changes[middle];
27
+ if (change.to < offset || (change.to === offset && change.from !== change.to)) {
28
+ low = middle + 1;
29
+ }
30
+ else {
31
+ high = middle;
32
+ }
33
+ }
34
+ const change = changes[low];
35
+ return change !== undefined && change.from <= offset ? change.from + deltas[low] + change.insertedLength : offset + deltas[low];
36
+ };
37
+ }
package/dist/outline.js CHANGED
@@ -1,4 +1,4 @@
1
- import { mapOffset } from './offsets.js';
1
+ import { offsetMapper } from './offsets.js';
2
2
  const MODIFIERS = '(?:export\\s+|default\\s+|declare\\s+|abstract\\s+|async\\s+|pub(?:\\([^)]*\\))?\\s+|public\\s+|private\\s+|protected\\s+|static\\s+|final\\s+|readonly\\s+|override\\s+|unsafe\\s+|extern\\s+)*';
3
3
  const DECLARATION = new RegExp(`^${MODIFIERS}(function\\*?|class|interface|enum|type|namespace|module|trait|impl|struct|fn|def|func|object|record)\\s+([A-Za-z_$][\\w$]*)`);
4
4
  const VARIABLE = new RegExp(`^${MODIFIERS}(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)`);
@@ -126,12 +126,13 @@ export class Outline {
126
126
  });
127
127
  }
128
128
  edited(batches) {
129
+ const mappers = batches.map(offsetMapper);
129
130
  const move = (entries) => entries.map((entry) => {
130
131
  let from = entry.from;
131
132
  let to = entry.to;
132
- for (const changes of batches) {
133
- from = mapOffset(from, changes);
134
- to = mapOffset(to, changes);
133
+ for (const map of mappers) {
134
+ from = map(from);
135
+ to = map(to);
135
136
  }
136
137
  return { ...entry, from, to };
137
138
  });
package/dist/paint.d.ts CHANGED
@@ -1,17 +1,18 @@
1
1
  import { type AttributedLines } from './attribution.ts';
2
- import { type EditorLayout, type LayoutRect, type LayoutRow, type LineGeometry } from './layout.ts';
3
- import type { EditorChangeKind, LineToken } from './types.ts';
2
+ import { type EditorLayout, type LayoutRect, type LayoutRow, type LineGeometry, type SubRowBand, type VerticalWindow } from './layout.ts';
3
+ import type { EditorChangeKind, EditorGutterIcon, LineToken } from './types.ts';
4
4
  export interface RowPaint {
5
5
  layoutVersion: number;
6
6
  whitespace: boolean;
7
7
  tokensOf(line: number): readonly LineToken[] | null;
8
8
  viewportWidth: number;
9
+ window: VerticalWindow;
9
10
  hostLeft: number;
10
11
  hostWidth: number;
11
12
  onUnfold(line: number): void;
12
13
  }
13
14
  export declare function styleSpan(span: HTMLElement, token: LineToken): void;
14
- export declare function lineElement(document: Document, geometry: LineGeometry, tokens: readonly LineToken[] | null, lineHeight: number, firstRowOnly: boolean, whitespace?: boolean): HTMLElement;
15
+ export declare function lineElement(document: Document, geometry: LineGeometry, tokens: readonly LineToken[] | null, lineHeight: number, band: SubRowBand, whitespace?: boolean): HTMLElement;
15
16
  export declare class RowPainter {
16
17
  private readonly rows;
17
18
  private readonly code;
@@ -22,16 +23,20 @@ export declare class RowPainter {
22
23
  private paintText;
23
24
  private buildText;
24
25
  private appendTail;
26
+ private paintReplacement;
25
27
  private paintBlock;
26
28
  private fillBlock;
27
29
  clear(): void;
28
30
  }
29
31
  export interface GutterPaint {
32
+ lineNumber(line: number): string | null;
30
33
  activeLines: ReadonlySet<number>;
31
34
  changes: ReadonlyMap<number, EditorChangeKind>;
32
35
  attribution: ReadonlyMap<number, AttributedLines>;
33
36
  highlights: ReadonlyMap<number, AttributedLines>;
34
37
  foldable: ReadonlyMap<number, boolean>;
38
+ foldName(line: number, collapsed: boolean): string;
39
+ lineLabel(line: number): string | null;
35
40
  action: {
36
41
  line: number;
37
42
  label: string;
@@ -39,6 +44,8 @@ export interface GutterPaint {
39
44
  markers: ReadonlyMap<number, {
40
45
  id: string;
41
46
  label: string;
47
+ icon?: EditorGutterIcon;
48
+ pressed?: boolean;
42
49
  }>;
43
50
  }
44
51
  export declare function paintGutter(container: HTMLElement, layout: EditorLayout, rows: readonly LayoutRow[], paint: GutterPaint): void;
@@ -73,6 +80,7 @@ export interface RemoteCaret {
73
80
  export declare function paintRemoteCarets(container: HTMLElement, carets: readonly RemoteCaret[]): void;
74
81
  export interface StickyEntry {
75
82
  line: number;
83
+ label: string | null;
76
84
  geometry: LineGeometry;
77
85
  tokens: readonly LineToken[] | null;
78
86
  }