@blankdotpage/cake 0.1.67 → 0.1.69

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 (62) hide show
  1. package/dist/cake/core/mapping/cursor-source-map.d.ts +11 -0
  2. package/dist/cake/core/mapping/cursor-source-map.d.ts.map +1 -1
  3. package/dist/cake/core/mapping/cursor-source-map.js +159 -21
  4. package/dist/cake/core/runtime.d.ts +6 -0
  5. package/dist/cake/core/runtime.d.ts.map +1 -1
  6. package/dist/cake/core/runtime.js +344 -221
  7. package/dist/cake/dom/render.d.ts +32 -2
  8. package/dist/cake/dom/render.d.ts.map +1 -1
  9. package/dist/cake/dom/render.js +401 -118
  10. package/dist/cake/editor/cake-editor.d.ts +11 -2
  11. package/dist/cake/editor/cake-editor.d.ts.map +1 -1
  12. package/dist/cake/editor/cake-editor.js +178 -100
  13. package/dist/cake/editor/internal/editor-text-model.d.ts +49 -0
  14. package/dist/cake/editor/internal/editor-text-model.d.ts.map +1 -0
  15. package/dist/cake/editor/internal/editor-text-model.js +284 -0
  16. package/dist/cake/editor/selection/selection-geometry-dom.d.ts +5 -1
  17. package/dist/cake/editor/selection/selection-geometry-dom.d.ts.map +1 -1
  18. package/dist/cake/editor/selection/selection-geometry-dom.js +4 -5
  19. package/dist/cake/editor/selection/selection-layout-dom.d.ts.map +1 -1
  20. package/dist/cake/editor/selection/selection-layout-dom.js +2 -5
  21. package/dist/cake/editor/selection/selection-layout.d.ts +2 -15
  22. package/dist/cake/editor/selection/selection-layout.d.ts.map +1 -1
  23. package/dist/cake/editor/selection/selection-layout.js +1 -99
  24. package/dist/cake/editor/selection/selection-navigation.d.ts +4 -0
  25. package/dist/cake/editor/selection/selection-navigation.d.ts.map +1 -1
  26. package/dist/cake/editor/selection/selection-navigation.js +1 -2
  27. package/dist/cake/extensions/index.d.ts +2 -1
  28. package/dist/cake/extensions/index.d.ts.map +1 -1
  29. package/dist/cake/extensions/index.js +3 -1
  30. package/dist/cake/extensions/link/link.d.ts.map +1 -1
  31. package/dist/cake/extensions/link/link.js +1 -7
  32. package/dist/cake/extensions/shared/structural-reparse-policy.d.ts +7 -0
  33. package/dist/cake/extensions/shared/structural-reparse-policy.d.ts.map +1 -0
  34. package/dist/cake/extensions/shared/structural-reparse-policy.js +16 -0
  35. package/package.json +5 -2
  36. package/dist/cake/editor/selection/visible-text.d.ts +0 -5
  37. package/dist/cake/editor/selection/visible-text.d.ts.map +0 -1
  38. package/dist/cake/editor/selection/visible-text.js +0 -66
  39. package/dist/cake/engine/cake-engine.d.ts +0 -230
  40. package/dist/cake/engine/cake-engine.d.ts.map +0 -1
  41. package/dist/cake/engine/cake-engine.js +0 -3589
  42. package/dist/cake/engine/selection/selection-geometry-dom.d.ts +0 -24
  43. package/dist/cake/engine/selection/selection-geometry-dom.d.ts.map +0 -1
  44. package/dist/cake/engine/selection/selection-geometry-dom.js +0 -302
  45. package/dist/cake/engine/selection/selection-geometry.d.ts +0 -22
  46. package/dist/cake/engine/selection/selection-geometry.d.ts.map +0 -1
  47. package/dist/cake/engine/selection/selection-geometry.js +0 -158
  48. package/dist/cake/engine/selection/selection-layout-dom.d.ts +0 -50
  49. package/dist/cake/engine/selection/selection-layout-dom.d.ts.map +0 -1
  50. package/dist/cake/engine/selection/selection-layout-dom.js +0 -781
  51. package/dist/cake/engine/selection/selection-layout.d.ts +0 -55
  52. package/dist/cake/engine/selection/selection-layout.d.ts.map +0 -1
  53. package/dist/cake/engine/selection/selection-layout.js +0 -128
  54. package/dist/cake/engine/selection/selection-navigation.d.ts +0 -22
  55. package/dist/cake/engine/selection/selection-navigation.d.ts.map +0 -1
  56. package/dist/cake/engine/selection/selection-navigation.js +0 -229
  57. package/dist/cake/engine/selection/visible-text.d.ts +0 -5
  58. package/dist/cake/engine/selection/visible-text.d.ts.map +0 -1
  59. package/dist/cake/engine/selection/visible-text.js +0 -66
  60. package/dist/cake/react/CakeEditor.d.ts +0 -58
  61. package/dist/cake/react/CakeEditor.d.ts.map +0 -1
  62. package/dist/cake/react/CakeEditor.js +0 -225
@@ -1,5 +1,6 @@
1
- import { CursorSourceBuilder, } from "./mapping/cursor-source-map";
1
+ import { CursorSourceBuilder, createCompositeCursorSourceMap, } from "./mapping/cursor-source-map";
2
2
  import { graphemeSegments } from "../shared/segmenter";
3
+ import { getEditorTextModelForDoc, } from "../editor/internal/editor-text-model";
3
4
  /** Type guard to check if a command is a structural edit */
4
5
  export function isStructuralEdit(command) {
5
6
  return (command.type === "insert" ||
@@ -17,7 +18,6 @@ export function isApplyEditCommand(command) {
17
18
  command.type === "delete-backward" ||
18
19
  command.type === "delete-forward");
19
20
  }
20
- const PLAIN_INSERT_TEXT_PATTERN = /^[\p{L}\p{N}\p{M}]+$/u;
21
21
  const defaultSelection = { start: 0, end: 0, affinity: "forward" };
22
22
  function removeFromArray(arr, value) {
23
23
  const index = arr.indexOf(value);
@@ -36,6 +36,7 @@ export function createRuntimeForTests(extensions) {
36
36
  const normalizeBlockFns = [];
37
37
  const normalizeInlineFns = [];
38
38
  const onEditFns = [];
39
+ const structuralReparsePolicies = [];
39
40
  const domInlineRenderers = [];
40
41
  const domBlockRenderers = [];
41
42
  const editor = {
@@ -102,6 +103,10 @@ export function createRuntimeForTests(extensions) {
102
103
  onEditFns.push(fn);
103
104
  return () => removeFromArray(onEditFns, fn);
104
105
  },
106
+ registerStructuralReparsePolicy: (fn) => {
107
+ structuralReparsePolicies.push(fn);
108
+ return () => removeFromArray(structuralReparsePolicies, fn);
109
+ },
105
110
  registerOnPasteText: () => {
106
111
  return () => { };
107
112
  },
@@ -136,27 +141,42 @@ export function createRuntimeForTests(extensions) {
136
141
  normalizeBlockFns,
137
142
  normalizeInlineFns,
138
143
  onEditFns,
144
+ structuralReparsePolicies,
139
145
  domInlineRenderers,
140
146
  domBlockRenderers,
141
147
  });
142
148
  return runtime;
143
149
  }
144
150
  export function createRuntimeFromRegistry(registry) {
145
- const { toggleMarkerToSpec, inclusiveAtEndByKind, parseBlockFns, parseInlineFns, serializeBlockFns, serializeInlineFns, normalizeBlockFns, normalizeInlineFns, onEditFns, domInlineRenderers, domBlockRenderers, } = registry;
151
+ const { toggleMarkerToSpec, inclusiveAtEndByKind, parseBlockFns, parseInlineFns, serializeBlockFns, serializeInlineFns, normalizeBlockFns, normalizeInlineFns, onEditFns, structuralReparsePolicies, domInlineRenderers, domBlockRenderers, } = registry;
146
152
  const isInclusiveAtEnd = (kind) => inclusiveAtEndByKind.get(kind) ?? true;
147
- const context = {
153
+ const removedBlockSentinel = Symbol("removed-block");
154
+ const removedInlineSentinel = Symbol("removed-inline");
155
+ const normalizedDocCache = new WeakMap();
156
+ const normalizedBlockCache = new WeakMap();
157
+ const normalizedInlineCache = new WeakMap();
158
+ const serializedDocCache = new WeakMap();
159
+ const serializedBlockCache = new WeakMap();
160
+ const serializedInlineCache = new WeakMap();
161
+ const segmentedDocCache = new WeakMap();
162
+ const emptyCursorMap = new CursorSourceBuilder().build().map;
163
+ const emptySerialized = {
164
+ source: "",
165
+ map: emptyCursorMap,
166
+ };
167
+ const extensionContext = {
148
168
  parseInline: (source, start, end) => parseInlineRange(source, start, end),
149
169
  serializeInline: (inline) => serializeInline(inline),
150
170
  serializeBlock: (block) => serializeBlock(block),
151
171
  };
152
172
  function parseBlockAt(source, start) {
153
173
  for (const parseBlock of parseBlockFns) {
154
- const result = parseBlock(source, start, context);
174
+ const result = parseBlock(source, start, extensionContext);
155
175
  if (result) {
156
176
  return result;
157
177
  }
158
178
  }
159
- return parseLiteralBlock(source, start, context);
179
+ return parseLiteralBlock(source, start, extensionContext);
160
180
  }
161
181
  function parseInlineRange(source, start, end) {
162
182
  const inlines = [];
@@ -164,7 +184,7 @@ export function createRuntimeFromRegistry(registry) {
164
184
  while (pos < end) {
165
185
  let matched = false;
166
186
  for (const parseInline of parseInlineFns) {
167
- const result = parseInline(source, pos, end, context);
187
+ const result = parseInline(source, pos, end, extensionContext);
168
188
  if (result) {
169
189
  inlines.push(result.inline);
170
190
  pos = result.nextPos;
@@ -200,6 +220,10 @@ export function createRuntimeFromRegistry(registry) {
200
220
  return { type: "doc", blocks };
201
221
  }
202
222
  function serialize(doc) {
223
+ const cached = serializedDocCache.get(doc);
224
+ if (cached) {
225
+ return cached;
226
+ }
203
227
  const builder = new CursorSourceBuilder();
204
228
  const blocks = doc.blocks;
205
229
  blocks.forEach((block, index) => {
@@ -209,41 +233,67 @@ export function createRuntimeFromRegistry(registry) {
209
233
  builder.appendText("\n");
210
234
  }
211
235
  });
212
- return builder.build();
236
+ const serialized = builder.build();
237
+ serializedDocCache.set(doc, serialized);
238
+ return serialized;
213
239
  }
214
240
  function serializeBlock(block) {
241
+ const cached = serializedBlockCache.get(block);
242
+ if (cached) {
243
+ return cached;
244
+ }
215
245
  for (const serializeBlockFn of serializeBlockFns) {
216
- const result = serializeBlockFn(block, context);
246
+ const result = serializeBlockFn(block, extensionContext);
217
247
  if (result) {
248
+ serializedBlockCache.set(block, result);
218
249
  return result;
219
250
  }
220
251
  }
221
252
  if (block.type === "paragraph") {
222
- return serializeParagraph(block, serializeInline);
253
+ const result = serializeParagraph(block, (inline) => serializeInline(inline));
254
+ serializedBlockCache.set(block, result);
255
+ return result;
223
256
  }
224
257
  if (block.type === "block-wrapper") {
225
- return serializeBlockWrapper(block, serializeBlock);
258
+ const result = serializeBlockWrapper(block, (child) => serializeBlock(child));
259
+ serializedBlockCache.set(block, result);
260
+ return result;
226
261
  }
227
- return { source: "", map: new CursorSourceBuilder().build().map };
262
+ serializedBlockCache.set(block, emptySerialized);
263
+ return emptySerialized;
228
264
  }
229
265
  function serializeInline(inline) {
266
+ const cached = serializedInlineCache.get(inline);
267
+ if (cached) {
268
+ return cached;
269
+ }
230
270
  for (const serializeInlineFn of serializeInlineFns) {
231
- const result = serializeInlineFn(inline, context);
271
+ const result = serializeInlineFn(inline, extensionContext);
232
272
  if (result) {
273
+ serializedInlineCache.set(inline, result);
233
274
  return result;
234
275
  }
235
276
  }
236
277
  if (inline.type === "text") {
237
278
  const builder = new CursorSourceBuilder();
238
279
  builder.appendText(inline.text);
239
- return builder.build();
280
+ const result = builder.build();
281
+ serializedInlineCache.set(inline, result);
282
+ return result;
240
283
  }
241
284
  if (inline.type === "inline-wrapper") {
242
- return serializeInlineWrapper(inline, serializeInline);
285
+ const result = serializeInlineWrapper(inline, (child) => serializeInline(child));
286
+ serializedInlineCache.set(inline, result);
287
+ return result;
243
288
  }
244
- return { source: "", map: new CursorSourceBuilder().build().map };
289
+ serializedInlineCache.set(inline, emptySerialized);
290
+ return emptySerialized;
245
291
  }
246
292
  function normalize(doc) {
293
+ const cached = normalizedDocCache.get(doc);
294
+ if (cached) {
295
+ return cached;
296
+ }
247
297
  let changed = false;
248
298
  const blocks = [];
249
299
  for (const block of doc.blocks) {
@@ -257,19 +307,28 @@ export function createRuntimeFromRegistry(registry) {
257
307
  }
258
308
  blocks.push(normalized);
259
309
  }
260
- if (!changed) {
261
- return doc;
310
+ const normalized = changed
311
+ ? {
312
+ type: "doc",
313
+ blocks,
314
+ }
315
+ : doc;
316
+ normalizedDocCache.set(doc, normalized);
317
+ if (normalized !== doc) {
318
+ normalizedDocCache.set(normalized, normalized);
262
319
  }
263
- return {
264
- type: "doc",
265
- blocks,
266
- };
320
+ return normalized;
267
321
  }
268
322
  function normalizeBlock(block) {
323
+ const cached = normalizedBlockCache.get(block);
324
+ if (cached !== undefined) {
325
+ return cached === removedBlockSentinel ? null : cached;
326
+ }
269
327
  let next = block;
270
328
  for (const normalizeBlockFn of normalizeBlockFns) {
271
329
  const result = normalizeBlockFn(next);
272
330
  if (result === null) {
331
+ normalizedBlockCache.set(block, removedBlockSentinel);
273
332
  return null;
274
333
  }
275
334
  next = result;
@@ -278,46 +337,53 @@ export function createRuntimeFromRegistry(registry) {
278
337
  let changed = next !== block;
279
338
  const content = [];
280
339
  for (const inline of next.content) {
281
- const normalized = normalizeInline(inline);
282
- if (normalized === null) {
340
+ const normalizedInline = normalizeInline(inline);
341
+ if (normalizedInline === null) {
283
342
  changed = true;
284
343
  continue;
285
344
  }
286
- if (normalized !== inline) {
345
+ if (normalizedInline !== inline) {
287
346
  changed = true;
288
347
  }
289
- content.push(normalized);
348
+ content.push(normalizedInline);
290
349
  }
291
350
  if (!changed) {
351
+ normalizedBlockCache.set(block, next);
292
352
  return next;
293
353
  }
294
- return {
354
+ const normalized = {
295
355
  ...next,
296
356
  content,
297
357
  };
358
+ normalizedBlockCache.set(block, normalized);
359
+ return normalized;
298
360
  }
299
361
  if (next.type === "block-wrapper") {
300
362
  let changed = next !== block;
301
363
  const blocks = [];
302
364
  for (const child of next.blocks) {
303
- const normalized = normalizeBlock(child);
304
- if (normalized === null) {
365
+ const normalizedChild = normalizeBlock(child);
366
+ if (normalizedChild === null) {
305
367
  changed = true;
306
368
  continue;
307
369
  }
308
- if (normalized !== child) {
370
+ if (normalizedChild !== child) {
309
371
  changed = true;
310
372
  }
311
- blocks.push(normalized);
373
+ blocks.push(normalizedChild);
312
374
  }
313
375
  if (!changed) {
376
+ normalizedBlockCache.set(block, next);
314
377
  return next;
315
378
  }
316
- return {
379
+ const normalized = {
317
380
  ...next,
318
381
  blocks,
319
382
  };
383
+ normalizedBlockCache.set(block, normalized);
384
+ return normalized;
320
385
  }
386
+ normalizedBlockCache.set(block, next);
321
387
  return next;
322
388
  }
323
389
  function applyInlineNormalizers(inline) {
@@ -332,8 +398,13 @@ export function createRuntimeFromRegistry(registry) {
332
398
  return next;
333
399
  }
334
400
  function normalizeInline(inline) {
401
+ const cached = normalizedInlineCache.get(inline);
402
+ if (cached !== undefined) {
403
+ return cached === removedInlineSentinel ? null : cached;
404
+ }
335
405
  const pre = applyInlineNormalizers(inline);
336
406
  if (!pre) {
407
+ normalizedInlineCache.set(inline, removedInlineSentinel);
337
408
  return null;
338
409
  }
339
410
  let next = pre;
@@ -345,35 +416,193 @@ export function createRuntimeFromRegistry(registry) {
345
416
  .filter((child) => child !== null),
346
417
  };
347
418
  }
348
- return applyInlineNormalizers(next);
419
+ const normalized = applyInlineNormalizers(next);
420
+ normalizedInlineCache.set(inline, normalized ?? removedInlineSentinel);
421
+ return normalized;
349
422
  }
350
- function createState(source, selection = defaultSelection) {
351
- const doc = parse(source);
352
- const normalized = normalize(doc);
353
- const serialized = serialize(normalized);
423
+ function createTopLevelBlockSegment(block) {
424
+ const serialized = serializeBlock(block);
354
425
  return {
426
+ block,
355
427
  source: serialized.source,
356
- selection,
357
428
  map: serialized.map,
358
- doc: normalized,
359
- runtime: runtime,
429
+ sourceLength: serialized.source.length,
430
+ cursorLength: serialized.map.cursorLength,
360
431
  };
361
432
  }
362
- function createStateFromDoc(doc, selection = defaultSelection) {
433
+ function hasSharedTopLevelBlockIdentity(previousDoc, nextDoc) {
434
+ if (previousDoc === nextDoc) {
435
+ return true;
436
+ }
437
+ if (previousDoc.blocks.length === 0 || nextDoc.blocks.length === 0) {
438
+ return false;
439
+ }
440
+ const previousBlocks = new Set(previousDoc.blocks);
441
+ return nextDoc.blocks.some((block) => previousBlocks.has(block));
442
+ }
443
+ function buildTopLevelSegments(doc, previous) {
444
+ const previousByBlock = new Map();
445
+ if (previous) {
446
+ for (const segment of previous.segments) {
447
+ previousByBlock.set(segment.block, segment);
448
+ }
449
+ }
450
+ return doc.blocks.map((block) => {
451
+ const reused = previousByBlock.get(block);
452
+ return reused ?? createTopLevelBlockSegment(block);
453
+ });
454
+ }
455
+ function buildPrefixIndexes(segments) {
456
+ const cursorStarts = [];
457
+ const sourceStarts = [];
458
+ let cursorOffset = 0;
459
+ let sourceOffset = 0;
460
+ for (let i = 0; i < segments.length; i += 1) {
461
+ const segment = segments[i];
462
+ cursorStarts.push(cursorOffset);
463
+ sourceStarts.push(sourceOffset);
464
+ cursorOffset += segment?.cursorLength ?? 0;
465
+ sourceOffset += segment?.sourceLength ?? 0;
466
+ if (i < segments.length - 1) {
467
+ cursorOffset += 1;
468
+ sourceOffset += 1;
469
+ }
470
+ }
471
+ return {
472
+ cursorStarts,
473
+ sourceStarts,
474
+ totalCursorLength: cursorOffset,
475
+ totalSourceLength: sourceOffset,
476
+ };
477
+ }
478
+ function serializeSegmentRange(segments, startIndex, endIndex) {
479
+ if (startIndex >= endIndex) {
480
+ return "";
481
+ }
482
+ let source = "";
483
+ for (let i = startIndex; i < endIndex; i += 1) {
484
+ const segment = segments[i];
485
+ if (!segment) {
486
+ continue;
487
+ }
488
+ source += segment.source;
489
+ if (i < segments.length - 1) {
490
+ source += "\n";
491
+ }
492
+ }
493
+ return source;
494
+ }
495
+ function sourceOffsetForBlockStart(state, blockIndex) {
496
+ if (blockIndex <= 0) {
497
+ return 0;
498
+ }
499
+ if (blockIndex >= state.segments.length) {
500
+ return state.source.length;
501
+ }
502
+ return state.sourceStarts[blockIndex] ?? state.source.length;
503
+ }
504
+ function buildSegmentedSource(segments, previous) {
505
+ if (!previous) {
506
+ return serializeSegmentRange(segments, 0, segments.length);
507
+ }
508
+ const previousSegments = previous.segments;
509
+ let prefix = 0;
510
+ const maxPrefix = Math.min(previousSegments.length, segments.length);
511
+ while (prefix < maxPrefix &&
512
+ previousSegments[prefix]?.block === segments[prefix]?.block) {
513
+ prefix += 1;
514
+ }
515
+ let suffix = 0;
516
+ const maxSuffix = Math.min(previousSegments.length - prefix, segments.length - prefix);
517
+ while (suffix < maxSuffix &&
518
+ previousSegments[previousSegments.length - 1 - suffix]?.block ===
519
+ segments[segments.length - 1 - suffix]?.block) {
520
+ suffix += 1;
521
+ }
522
+ if (prefix === previousSegments.length && prefix === segments.length) {
523
+ return previous.source;
524
+ }
525
+ const oldStart = sourceOffsetForBlockStart(previous, prefix);
526
+ const oldEnd = sourceOffsetForBlockStart(previous, previousSegments.length - suffix);
527
+ const middle = serializeSegmentRange(segments, prefix, segments.length - suffix);
528
+ return (previous.source.slice(0, oldStart) +
529
+ middle +
530
+ previous.source.slice(oldEnd));
531
+ }
532
+ function buildSegmentedDocState(doc, previous) {
533
+ const cached = segmentedDocCache.get(doc);
534
+ if (cached) {
535
+ return cached;
536
+ }
537
+ const segments = buildTopLevelSegments(doc, previous);
538
+ const { cursorStarts, sourceStarts, totalCursorLength, totalSourceLength, } = buildPrefixIndexes(segments);
539
+ const source = buildSegmentedSource(segments, previous);
540
+ const map = createCompositeCursorSourceMap({
541
+ segments: segments.map((segment) => ({
542
+ map: segment.map,
543
+ cursorLength: segment.cursorLength,
544
+ sourceLength: segment.sourceLength,
545
+ })),
546
+ cursorStarts,
547
+ sourceStarts,
548
+ cursorLength: totalCursorLength,
549
+ });
550
+ const segmented = {
551
+ doc,
552
+ segments,
553
+ cursorStarts,
554
+ sourceStarts,
555
+ totalCursorLength,
556
+ totalSourceLength,
557
+ source,
558
+ map,
559
+ };
560
+ segmentedDocCache.set(doc, segmented);
561
+ return segmented;
562
+ }
563
+ function buildStateFromDoc(doc, selection, options) {
363
564
  const normalized = normalize(doc);
364
- const serialized = serialize(normalized);
565
+ const mode = options?.mode ?? "incremental";
566
+ const previousSegmented = mode === "incremental" && options?.previousState
567
+ ? segmentedDocCache.get(options.previousState.doc)
568
+ : undefined;
569
+ const reusablePrevious = previousSegmented &&
570
+ hasSharedTopLevelBlockIdentity(previousSegmented.doc, normalized)
571
+ ? previousSegmented
572
+ : undefined;
573
+ const segmented = buildSegmentedDocState(normalized, reusablePrevious);
365
574
  return {
366
- source: serialized.source,
575
+ source: segmented.source,
367
576
  selection,
368
- map: serialized.map,
577
+ map: segmented.map,
369
578
  doc: normalized,
370
579
  runtime: runtime,
371
580
  };
372
581
  }
373
- function canReuseStructuralStateWithoutReparse(command) {
374
- return (command.type === "insert" &&
375
- command.text.length > 0 &&
376
- PLAIN_INSERT_TEXT_PATTERN.test(command.text));
582
+ function createState(source, selection = defaultSelection) {
583
+ const doc = parse(source);
584
+ return buildStateFromDoc(doc, selection, { mode: "full" });
585
+ }
586
+ function createStateFromDoc(doc, selection = defaultSelection, options) {
587
+ return buildStateFromDoc(doc, selection, options);
588
+ }
589
+ function isIncrementalDerivationCandidate(command, selection) {
590
+ if (selection.start !== selection.end) {
591
+ return false;
592
+ }
593
+ if (command.type === "insert") {
594
+ return command.text.length > 0 && !command.text.includes("\n");
595
+ }
596
+ return (command.type === "delete-backward" ||
597
+ command.type === "delete-forward" ||
598
+ command.type === "insert-line-break" ||
599
+ command.type === "insert-hard-line-break");
600
+ }
601
+ function shouldReparseAfterStructuralEdit(command) {
602
+ if (structuralReparsePolicies.length === 0) {
603
+ return true;
604
+ }
605
+ return structuralReparsePolicies.some((policy) => policy(command));
377
606
  }
378
607
  function applyEdit(command, state) {
379
608
  // Extensions can either:
@@ -485,14 +714,19 @@ export function createRuntimeFromRegistry(registry) {
485
714
  }
486
715
  return state;
487
716
  }
488
- // Structural edits operate directly on the current doc tree. To support
489
- // markdown-first behavior while typing, we reparse from the resulting
490
- // source and then remap the caret through source space so it stays stable
491
- // even when marker characters become source-only.
492
- const interim = createStateFromDoc(structural.doc);
717
+ // Structural edits operate on the doc tree first. Common collapsed
718
+ // typing/edit flows stay on the incremental segmented path when policy
719
+ // allows; everything else takes the explicit full fallback path through
720
+ // source + parse.
721
+ const shouldReparse = shouldReparseAfterStructuralEdit(command);
722
+ const useIncrementalSegmentedDerivation = !shouldReparse && isIncrementalDerivationCandidate(command, selection);
723
+ const interim = createStateFromDoc(structural.doc, defaultSelection, {
724
+ mode: useIncrementalSegmentedDerivation ? "incremental" : "full",
725
+ previousState: useIncrementalSegmentedDerivation ? state : undefined,
726
+ });
493
727
  const interimAffinity = structural.nextAffinity ?? "forward";
494
728
  const caretSource = interim.map.cursorToSource(structural.nextCursor, interimAffinity);
495
- if (canReuseStructuralStateWithoutReparse(command)) {
729
+ if (useIncrementalSegmentedDerivation) {
496
730
  const caretCursor = interim.map.sourceToCursor(caretSource, interimAffinity);
497
731
  return {
498
732
  ...interim,
@@ -530,11 +764,12 @@ export function createRuntimeFromRegistry(registry) {
530
764
  return state;
531
765
  }
532
766
  function applyStructuralEdit(command, doc, selection) {
533
- const lines = flattenDocToLines(doc);
767
+ const textModel = getEditorTextModelForDoc(doc);
768
+ const lines = textModel.getStructuralLines();
534
769
  if (lines.length === 0) {
535
770
  return null;
536
771
  }
537
- const docCursorLength = cursorLengthForLines(lines);
772
+ const docCursorLength = textModel.getCursorLength();
538
773
  const cursorStart = Math.max(0, Math.min(docCursorLength, Math.min(selection.start, selection.end)));
539
774
  const cursorEnd = Math.max(0, Math.min(docCursorLength, Math.max(selection.start, selection.end)));
540
775
  const affinity = selection.affinity ?? "forward";
@@ -576,12 +811,12 @@ export function createRuntimeFromRegistry(registry) {
576
811
  const shouldReplacePlaceholder = command.type === "insert" &&
577
812
  replaceText.length > 0 &&
578
813
  range.start === range.end &&
579
- graphemeAtCursor(lines, range.start) === "\u200B";
814
+ textModel.getGraphemeAtCursor(range.start) === "\u200B";
580
815
  const effectiveRange = shouldReplacePlaceholder
581
816
  ? { start: range.start, end: Math.min(docCursorLength, range.start + 1) }
582
817
  : range;
583
- const startLoc = resolveCursorToLine(lines, effectiveRange.start);
584
- const endLoc = resolveCursorToLine(lines, effectiveRange.end);
818
+ const startLoc = textModel.resolveOffsetToLine(effectiveRange.start);
819
+ const endLoc = textModel.resolveOffsetToLine(effectiveRange.end);
585
820
  const startLine = lines[startLoc.lineIndex];
586
821
  const endLine = lines[endLoc.lineIndex];
587
822
  if (!startLine || !endLine) {
@@ -609,8 +844,9 @@ export function createRuntimeFromRegistry(registry) {
609
844
  let nextBlocks = updateBlocksAtPath(doc.blocks, prevLine.parentPath, (blocks) => blocks.map((block, index) => index === prevLine.indexInParent ? nextPrevBlock : block));
610
845
  nextBlocks = updateBlocksAtPath(nextBlocks, endLine.parentPath, (blocks) => blocks.filter((_, index) => index !== endLine.indexInParent));
611
846
  const nextDoc = { ...doc, blocks: nextBlocks };
612
- const nextLines = flattenDocToLines(nextDoc);
613
- const lineStarts = getLineStartOffsets(nextLines);
847
+ const nextModel = getEditorTextModelForDoc(nextDoc);
848
+ const nextLines = nextModel.getStructuralLines();
849
+ const lineStarts = nextModel.getLineOffsets();
614
850
  const mergedLineIndex = nextLines.findIndex((line) => pathsEqual(line.path, prevLine.path));
615
851
  const mergedLine = nextLines[mergedLineIndex];
616
852
  const nextCursor = mergedLineIndex >= 0
@@ -667,8 +903,9 @@ export function createRuntimeFromRegistry(registry) {
667
903
  ...doc,
668
904
  blocks: updateBlocksAtPath(doc.blocks, parentPath, () => nextParentBlocks),
669
905
  };
670
- const nextLines = flattenDocToLines(nextDoc);
671
- const lineStarts = getLineStartOffsets(nextLines);
906
+ const nextModel = getEditorTextModelForDoc(nextDoc);
907
+ const nextLines = nextModel.getStructuralLines();
908
+ const lineStarts = nextModel.getLineOffsets();
672
909
  const nextLineIndex = Math.min(nextLines.length - 1, startLoc.lineIndex + 1);
673
910
  return {
674
911
  doc: nextDoc,
@@ -687,8 +924,9 @@ export function createRuntimeFromRegistry(registry) {
687
924
  ...doc,
688
925
  blocks: updateBlocksAtPath(doc.blocks, parentPath, () => ensured),
689
926
  };
690
- const nextLines = flattenDocToLines(nextDoc);
691
- const lineStarts = getLineStartOffsets(nextLines);
927
+ const nextModel = getEditorTextModelForDoc(nextDoc);
928
+ const nextLines = nextModel.getStructuralLines();
929
+ const lineStarts = nextModel.getLineOffsets();
692
930
  const nextLineIndex = Math.min(nextLines.length - 1, startLoc.lineIndex);
693
931
  return {
694
932
  doc: nextDoc,
@@ -718,8 +956,9 @@ export function createRuntimeFromRegistry(registry) {
718
956
  ...doc,
719
957
  blocks: updateBlocksAtPath(doc.blocks, parentPath, () => nextParentBlocks),
720
958
  };
721
- const nextLines = flattenDocToLines(nextDoc);
722
- const lineStarts = getLineStartOffsets(nextLines);
959
+ const nextModel = getEditorTextModelForDoc(nextDoc);
960
+ const nextLines = nextModel.getStructuralLines();
961
+ const lineStarts = nextModel.getLineOffsets();
723
962
  const nextLineIndex = Math.max(0, startLoc.lineIndex - 1);
724
963
  return {
725
964
  doc: nextDoc,
@@ -776,8 +1015,9 @@ export function createRuntimeFromRegistry(registry) {
776
1015
  ...doc,
777
1016
  blocks: updateBlocksAtPath(doc.blocks, wrapperParentPath, () => nextParentBlocks),
778
1017
  };
779
- const nextLines = flattenDocToLines(nextDoc);
780
- const lineStarts = getLineStartOffsets(nextLines);
1018
+ const nextModel = getEditorTextModelForDoc(nextDoc);
1019
+ const nextLines = nextModel.getStructuralLines();
1020
+ const lineStarts = nextModel.getLineOffsets();
781
1021
  const insertedPath = [...wrapperParentPath, wrapperIndexInParent + 1];
782
1022
  const insertedLineIndex = nextLines.findIndex((line) => pathsEqual(line.path, insertedPath));
783
1023
  const nextCursor = insertedLineIndex >= 0 ? (lineStarts[insertedLineIndex] ?? 0) : 0;
@@ -790,12 +1030,12 @@ export function createRuntimeFromRegistry(registry) {
790
1030
  }
791
1031
  const hasSelectedText = effectiveRange.start !== effectiveRange.end;
792
1032
  const baseMarks = hasSelectedText
793
- ? commonMarksAcrossSelection(lines, effectiveRange.start, effectiveRange.end, doc)
1033
+ ? commonMarksAcrossSelection(textModel, lines, effectiveRange.start, effectiveRange.end)
794
1034
  : marksAtCursor(startRuns, startLoc.offsetInLine, affinity);
795
1035
  const insertDoc = parse(replaceText);
796
- const insertLines = flattenDocToLines(insertDoc);
1036
+ const insertModel = getEditorTextModelForDoc(insertDoc);
797
1037
  const insertBlocks = insertDoc.blocks;
798
- const insertCursorLength = cursorLengthForLines(insertLines);
1038
+ const insertCursorLength = insertModel.getCursorLength();
799
1039
  const replacementBlocks = buildReplacementBlocks({
800
1040
  baseMarks,
801
1041
  beforeRuns,
@@ -811,9 +1051,10 @@ export function createRuntimeFromRegistry(registry) {
811
1051
  ...doc,
812
1052
  blocks: updateBlocksAtPath(doc.blocks, parentPath, () => nextParentBlocks),
813
1053
  };
814
- const nextDocCursorLength = cursorLengthForLines(flattenDocToLines(nextDoc));
1054
+ const nextModel = getEditorTextModelForDoc(nextDoc);
1055
+ const nextDocCursorLength = nextModel.getCursorLength();
815
1056
  const nextCursor = Math.max(0, Math.min(nextDocCursorLength, effectiveRange.start + insertCursorLength));
816
- const nextLines = flattenDocToLines(nextDoc);
1057
+ const nextLines = nextModel.getStructuralLines();
817
1058
  const around = marksAroundCursor(nextDoc, nextCursor);
818
1059
  const fallbackAffinity = command.type === "delete-backward"
819
1060
  ? "backward"
@@ -830,8 +1071,8 @@ export function createRuntimeFromRegistry(registry) {
830
1071
  // If the cursor is at the start of a non-first line (i.e., right after a
831
1072
  // serialized newline), keep affinity "forward" so selection anchors in the
832
1073
  // following line, not at the end of the previous one.
833
- const nextLoc = resolveCursorToLine(nextLines, nextCursor);
834
- const lineStarts = getLineStartOffsets(nextLines);
1074
+ const nextLoc = nextModel.resolveOffsetToLine(nextCursor);
1075
+ const lineStarts = nextModel.getLineOffsets();
835
1076
  if (nextLoc.lineIndex > 0 &&
836
1077
  nextLoc.offsetInLine === 0 &&
837
1078
  nextCursor === (lineStarts[nextLoc.lineIndex] ?? 0)) {
@@ -843,128 +1084,6 @@ export function createRuntimeFromRegistry(registry) {
843
1084
  nextAffinity,
844
1085
  };
845
1086
  }
846
- function cursorLengthForLines(lines) {
847
- let length = 0;
848
- for (const line of lines) {
849
- length += line.cursorLength;
850
- if (line.hasNewline) {
851
- length += 1;
852
- }
853
- }
854
- return length;
855
- }
856
- function flattenDocToLines(doc) {
857
- const entries = [];
858
- const visit = (blocks, prefix) => {
859
- blocks.forEach((block, index) => {
860
- const path = [...prefix, index];
861
- if (block.type === "block-wrapper") {
862
- visit(block.blocks, path);
863
- return;
864
- }
865
- entries.push({ path, block });
866
- });
867
- };
868
- visit(doc.blocks, []);
869
- if (entries.length === 0) {
870
- return [
871
- {
872
- path: [0],
873
- parentPath: [],
874
- indexInParent: 0,
875
- block: { type: "paragraph", content: [] },
876
- text: "",
877
- cursorLength: 0,
878
- hasNewline: false,
879
- },
880
- ];
881
- }
882
- return entries.map((entry, i) => {
883
- const parentPath = entry.path.slice(0, -1);
884
- const indexInParent = entry.path[entry.path.length - 1] ?? 0;
885
- const text = blockVisibleText(entry.block);
886
- return {
887
- path: entry.path,
888
- parentPath,
889
- indexInParent,
890
- block: entry.block,
891
- text,
892
- cursorLength: graphemeSegments(text).length,
893
- hasNewline: i < entries.length - 1,
894
- };
895
- });
896
- }
897
- function resolveCursorToLine(lines, cursorOffset) {
898
- const total = cursorLengthForLines(lines);
899
- const clamped = Math.max(0, Math.min(cursorOffset, total));
900
- let start = 0;
901
- for (let i = 0; i < lines.length; i += 1) {
902
- const line = lines[i];
903
- const end = start + line.cursorLength;
904
- if (clamped <= end || i === lines.length - 1) {
905
- return {
906
- lineIndex: i,
907
- offsetInLine: Math.max(0, Math.min(clamped - start, line.cursorLength)),
908
- };
909
- }
910
- start = end + (line.hasNewline ? 1 : 0);
911
- if (line.hasNewline && clamped === start) {
912
- return {
913
- lineIndex: Math.min(lines.length - 1, i + 1),
914
- offsetInLine: 0,
915
- };
916
- }
917
- }
918
- return {
919
- lineIndex: lines.length - 1,
920
- offsetInLine: lines[lines.length - 1]?.cursorLength ?? 0,
921
- };
922
- }
923
- function getLineStartOffsets(lines) {
924
- const offsets = [];
925
- let current = 0;
926
- for (const line of lines) {
927
- offsets.push(current);
928
- current += line.cursorLength + (line.hasNewline ? 1 : 0);
929
- }
930
- return offsets;
931
- }
932
- function graphemeAtCursor(lines, cursorOffset) {
933
- const loc = resolveCursorToLine(lines, cursorOffset);
934
- const line = lines[loc.lineIndex];
935
- if (!line) {
936
- return null;
937
- }
938
- if (loc.offsetInLine >= line.cursorLength) {
939
- return null;
940
- }
941
- const segments = Array.from(graphemeSegments(line.text));
942
- return segments[loc.offsetInLine]?.segment ?? null;
943
- }
944
- function blockVisibleText(block) {
945
- if (block.type === "paragraph") {
946
- return block.content.map(inlineVisibleText).join("");
947
- }
948
- if (block.type === "block-atom") {
949
- return "";
950
- }
951
- if (block.type === "block-wrapper") {
952
- return block.blocks.map(blockVisibleText).join("\n");
953
- }
954
- return "";
955
- }
956
- function inlineVisibleText(inline) {
957
- if (inline.type === "text") {
958
- return inline.text;
959
- }
960
- if (inline.type === "inline-wrapper") {
961
- return inline.children.map(inlineVisibleText).join("");
962
- }
963
- if (inline.type === "inline-atom") {
964
- return " ";
965
- }
966
- return "";
967
- }
968
1087
  function stableStringify(value) {
969
1088
  if (value === null || value === undefined) {
970
1089
  return "";
@@ -1135,13 +1254,14 @@ export function createRuntimeFromRegistry(registry) {
1135
1254
  return null;
1136
1255
  }
1137
1256
  function marksAroundCursor(doc, cursorOffset) {
1138
- const lines = flattenDocToLines(doc);
1139
- const loc = resolveCursorToLine(lines, cursorOffset);
1257
+ const textModel = getEditorTextModelForDoc(doc);
1258
+ const lines = textModel.getStructuralLines();
1259
+ const loc = textModel.resolveOffsetToLine(cursorOffset);
1140
1260
  const line = lines[loc.lineIndex];
1141
1261
  if (!line) {
1142
1262
  return { left: [], right: [] };
1143
1263
  }
1144
- const block = getBlockAtPath(doc.blocks, line.path);
1264
+ const block = line.block;
1145
1265
  if (!block || block.type !== "paragraph") {
1146
1266
  return { left: [], right: [] };
1147
1267
  }
@@ -1315,19 +1435,19 @@ export function createRuntimeFromRegistry(registry) {
1315
1435
  });
1316
1436
  return blocks;
1317
1437
  }
1318
- function commonMarksAcrossSelection(lines, startCursor, endCursor, doc) {
1438
+ function commonMarksAcrossSelection(textModel, lines, startCursor, endCursor) {
1319
1439
  if (startCursor === endCursor) {
1320
1440
  return [];
1321
1441
  }
1322
- const startLoc = resolveCursorToLine(lines, startCursor);
1323
- const endLoc = resolveCursorToLine(lines, endCursor);
1442
+ const startLoc = textModel.resolveOffsetToLine(startCursor);
1443
+ const endLoc = textModel.resolveOffsetToLine(endCursor);
1324
1444
  const slices = [];
1325
1445
  for (let lineIndex = startLoc.lineIndex; lineIndex <= endLoc.lineIndex; lineIndex += 1) {
1326
1446
  const line = lines[lineIndex];
1327
1447
  if (!line) {
1328
1448
  continue;
1329
1449
  }
1330
- const block = getBlockAtPath(doc.blocks, line.path);
1450
+ const block = line.block;
1331
1451
  if (!block || block.type !== "paragraph") {
1332
1452
  continue;
1333
1453
  }
@@ -1539,8 +1659,7 @@ export function createRuntimeFromRegistry(registry) {
1539
1659
  // otherwise we create ambiguous marker sequences (e.g., *italic**​*** doesn't parse).
1540
1660
  const betweenLen = insertAtForward - insertAtBackward;
1541
1661
  const preferBackward = insertAtBackward !== insertAtForward && openLen <= betweenLen;
1542
- const insertAt = placeholderPos ??
1543
- (preferBackward ? insertAtBackward : insertAtForward);
1662
+ const insertAt = placeholderPos ?? (preferBackward ? insertAtBackward : insertAtForward);
1544
1663
  const nextSource = placeholderPos !== null
1545
1664
  ? source.slice(0, insertAt) +
1546
1665
  openMarker +
@@ -1566,9 +1685,10 @@ export function createRuntimeFromRegistry(registry) {
1566
1685
  }
1567
1686
  const cursorStart = Math.min(selection.start, selection.end);
1568
1687
  const cursorEnd = Math.max(selection.start, selection.end);
1569
- const linesForSelection = flattenDocToLines(state.doc);
1570
- const startLoc = resolveCursorToLine(linesForSelection, cursorStart);
1571
- const endLoc = resolveCursorToLine(linesForSelection, cursorEnd);
1688
+ const selectionModel = getEditorTextModelForDoc(state.doc);
1689
+ const linesForSelection = selectionModel.getStructuralLines();
1690
+ const startLoc = selectionModel.resolveOffsetToLine(cursorStart);
1691
+ const endLoc = selectionModel.resolveOffsetToLine(cursorEnd);
1572
1692
  const splitRunsOnNewlines = (runs) => {
1573
1693
  const split = [];
1574
1694
  for (const run of runs) {
@@ -1606,7 +1726,7 @@ export function createRuntimeFromRegistry(registry) {
1606
1726
  if (startInLine === endInLine) {
1607
1727
  continue;
1608
1728
  }
1609
- const block = getBlockAtPath(state.doc.blocks, line.path);
1729
+ const block = line.block;
1610
1730
  if (!block || block.type !== "paragraph") {
1611
1731
  continue;
1612
1732
  }
@@ -1743,22 +1863,23 @@ export function createRuntimeFromRegistry(registry) {
1743
1863
  }
1744
1864
  function serializeSelection(state, selection) {
1745
1865
  const normalized = normalizeSelection(selection);
1746
- const lines = flattenDocToLines(state.doc);
1747
- const docCursorLength = cursorLengthForLines(lines);
1866
+ const textModel = getEditorTextModelForDoc(state.doc);
1867
+ const lines = textModel.getStructuralLines();
1868
+ const docCursorLength = textModel.getCursorLength();
1748
1869
  const cursorStart = Math.max(0, Math.min(docCursorLength, Math.min(normalized.start, normalized.end)));
1749
1870
  const cursorEnd = Math.max(0, Math.min(docCursorLength, Math.max(normalized.start, normalized.end)));
1750
1871
  if (cursorStart === cursorEnd) {
1751
1872
  return "";
1752
1873
  }
1753
- const startLoc = resolveCursorToLine(lines, cursorStart);
1754
- const endLoc = resolveCursorToLine(lines, cursorEnd);
1874
+ const startLoc = textModel.resolveOffsetToLine(cursorStart);
1875
+ const endLoc = textModel.resolveOffsetToLine(cursorEnd);
1755
1876
  const blocks = [];
1756
1877
  for (let lineIndex = startLoc.lineIndex; lineIndex <= endLoc.lineIndex; lineIndex += 1) {
1757
1878
  const line = lines[lineIndex];
1758
1879
  if (!line) {
1759
1880
  continue;
1760
1881
  }
1761
- const block = getBlockAtPath(state.doc.blocks, line.path);
1882
+ const block = line.block;
1762
1883
  if (!block || block.type !== "paragraph") {
1763
1884
  continue;
1764
1885
  }
@@ -1827,15 +1948,16 @@ export function createRuntimeFromRegistry(registry) {
1827
1948
  }
1828
1949
  function serializeSelectionToHtml(state, selection) {
1829
1950
  const normalized = normalizeSelection(selection);
1830
- const lines = flattenDocToLines(state.doc);
1831
- const docCursorLength = cursorLengthForLines(lines);
1951
+ const textModel = getEditorTextModelForDoc(state.doc);
1952
+ const lines = textModel.getStructuralLines();
1953
+ const docCursorLength = textModel.getCursorLength();
1832
1954
  const cursorStart = Math.max(0, Math.min(docCursorLength, Math.min(normalized.start, normalized.end)));
1833
1955
  const cursorEnd = Math.max(0, Math.min(docCursorLength, Math.max(normalized.start, normalized.end)));
1834
1956
  if (cursorStart === cursorEnd) {
1835
1957
  return "";
1836
1958
  }
1837
- const startLoc = resolveCursorToLine(lines, cursorStart);
1838
- const endLoc = resolveCursorToLine(lines, cursorEnd);
1959
+ const startLoc = textModel.resolveOffsetToLine(cursorStart);
1960
+ const endLoc = textModel.resolveOffsetToLine(cursorEnd);
1839
1961
  let html = "";
1840
1962
  let activeList = null;
1841
1963
  const closeList = () => {
@@ -1859,7 +1981,7 @@ export function createRuntimeFromRegistry(registry) {
1859
1981
  if (!line) {
1860
1982
  continue;
1861
1983
  }
1862
- const block = getBlockAtPath(state.doc.blocks, line.path);
1984
+ const block = line.block;
1863
1985
  if (!block || block.type !== "paragraph") {
1864
1986
  continue;
1865
1987
  }
@@ -1942,6 +2064,7 @@ export function createRuntimeFromRegistry(registry) {
1942
2064
  parse,
1943
2065
  serialize,
1944
2066
  createState,
2067
+ createStateFromDoc,
1945
2068
  updateSelection,
1946
2069
  serializeSelection,
1947
2070
  serializeSelectionToHtml,