@voithos-labs/aragonite 0.10.2 → 0.10.3
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.
- package/README.md +6 -21
- package/THIRD-PARTY-NOTICES.md +25 -0
- package/dist/a11y-strings.d.ts +18 -0
- package/dist/a11y-strings.js +18 -0
- package/dist/action-contracts.d.ts +7 -1
- package/dist/ambient/ambient-dom.js +5 -1
- package/dist/block-component.d.ts +14 -0
- package/dist/components/BlockDragHandle.svelte +35 -24
- package/dist/components/BlockHost.svelte +16 -9
- package/dist/components/Editor.svelte +381 -198
- package/dist/components/Editor.svelte.d.ts +1 -1
- package/dist/components/SelectionOverlay.svelte +17 -3
- package/dist/components/TailInsert.svelte +107 -0
- package/dist/components/TailInsert.svelte.d.ts +17 -0
- package/dist/components/block-content-selector.d.ts +6 -2
- package/dist/components/block-content-selector.js +6 -2
- package/dist/components/blocks/ThematicBreakBlock.svelte +19 -6
- package/dist/components/blocks/ThematicBreakBlock.svelte.d.ts +1 -0
- package/dist/components/blocks/code/CodeBlock.svelte +211 -30
- package/dist/components/blocks/code/CodeBlockRail.svelte +686 -0
- package/dist/components/blocks/code/CodeBlockRail.svelte.d.ts +26 -0
- package/dist/components/blocks/code/code-bootstrap.js +4 -0
- package/dist/components/blocks/code/code-context-actions.d.ts +1 -0
- package/dist/components/blocks/code/code-context-actions.js +24 -0
- package/dist/components/blocks/code/code-fence-exit.d.ts +15 -0
- package/dist/components/blocks/code/code-fence-exit.js +26 -0
- package/dist/components/blocks/code/code-languages.d.ts +6 -0
- package/dist/components/blocks/code/code-languages.js +11 -0
- package/dist/components/blocks/code/code-renderer.js +11 -0
- package/dist/components/blocks/directive/DirectiveContainerBlock.svelte +1 -1
- package/dist/components/blocks/editable-leaf.d.ts +37 -6
- package/dist/components/blocks/editable-leaf.js +243 -29
- package/dist/components/blocks/editable-surface.d.ts +9 -0
- package/dist/components/blocks/editable-surface.js +22 -3
- package/dist/components/blocks/list/ListItemBlock.svelte +3 -3
- package/dist/components/blocks/list/task-checkbox.d.ts +2 -0
- package/dist/components/blocks/list/task-checkbox.js +11 -2
- package/dist/components/blocks/surface-wiring.svelte.d.ts +4 -0
- package/dist/components/blocks/surface-wiring.svelte.js +8 -1
- package/dist/components/blocks/table/TableActionMenu.svelte +210 -86
- package/dist/components/blocks/table/TableActionMenu.svelte.d.ts +5 -0
- package/dist/components/blocks/table/TableBlock.svelte +205 -186
- package/dist/components/blocks/table/TableBlock.svelte.d.ts +1 -0
- package/dist/components/blocks/table/TableCellBlock.svelte +87 -22
- package/dist/components/blocks/table/TableRowBlock.svelte +4 -18
- package/dist/components/blocks/table/TableRowBlock.svelte.d.ts +0 -5
- package/dist/components/blocks/table/cell-clipboard.d.ts +10 -0
- package/dist/components/blocks/table/cell-clipboard.js +34 -1
- package/dist/components/blocks/table/cell-keydown-plan.d.ts +1 -1
- package/dist/components/blocks/table/cell-keydown-plan.js +3 -1
- package/dist/components/blocks/table/table-cell-paste.js +2 -1
- package/dist/components/blocks/table/table-menu-model.d.ts +35 -8
- package/dist/components/blocks/table/table-menu-model.js +36 -17
- package/dist/components/blocks/text/TextEditableBlock.svelte +58 -11
- package/dist/components/blocks/text/delimiter-autopair.d.ts +71 -0
- package/dist/components/blocks/text/delimiter-autopair.js +216 -0
- package/dist/components/blocks/text/edge-policy-dispatch.d.ts +4 -0
- package/dist/components/blocks/text/edge-policy-dispatch.js +56 -2
- package/dist/components/blocks/text/live-selection-edit.js +27 -0
- package/dist/components/blocks/text/text-keydown.d.ts +7 -1
- package/dist/components/blocks/text/text-keydown.js +11 -1
- package/dist/components/blocks/text/text-render.d.ts +1 -1
- package/dist/components/blocks/text/text-render.js +3 -1
- package/dist/components/blocks/text/widget-interaction.d.ts +3 -0
- package/dist/components/blocks/text/widget-interaction.js +144 -22
- package/dist/components/drag-handle.d.ts +35 -0
- package/dist/components/drag-handle.js +126 -0
- package/dist/components/editor-root-focus.d.ts +19 -0
- package/dist/components/editor-root-focus.js +67 -0
- package/dist/components/editor-root-geometry.d.ts +39 -0
- package/dist/components/editor-root-geometry.js +91 -0
- package/dist/components/editor-root-keydown.d.ts +1 -1
- package/dist/components/editor-root-keydown.js +12 -3
- package/dist/components/editor-root-listeners.d.ts +10 -6
- package/dist/components/editor-root-listeners.js +23 -22
- package/dist/components/editor-root-mode-flip.d.ts +36 -0
- package/dist/components/editor-root-mode-flip.js +92 -0
- package/dist/components/image/ImageOverlayHost.svelte +14 -6
- package/dist/components/image/ImageProperties.svelte +512 -70
- package/dist/components/image/ImageProperties.svelte.d.ts +6 -1
- package/dist/components/image/ImageResizeHandles.svelte +59 -34
- package/dist/components/image/image-crop.d.ts +39 -0
- package/dist/components/image/image-crop.js +74 -0
- package/dist/components/image/image-edit-commit.d.ts +1 -0
- package/dist/components/image/image-edit-commit.js +21 -5
- package/dist/components/image/image-source-bytes.js +10 -3
- package/dist/components/image/image-widget-editing.js +1 -0
- package/dist/components/image/widget-dom.js +5 -1
- package/dist/components/link-card/link-card-commit.js +1 -1
- package/dist/components/lrd-map-gate.js +1 -1
- package/dist/components/menu/BlockMenu.svelte +315 -0
- package/dist/components/menu/BlockMenu.svelte.d.ts +34 -0
- package/dist/components/menu/MenuIcon.svelte +153 -0
- package/dist/components/menu/MenuIcon.svelte.d.ts +51 -0
- package/dist/components/menu/clipboard-actions.d.ts +12 -0
- package/dist/components/menu/clipboard-actions.js +42 -0
- package/dist/components/menu/default-context-actions.d.ts +15 -0
- package/dist/components/menu/default-context-actions.js +76 -0
- package/dist/components/menu/flyout-placement.d.ts +6 -0
- package/dist/components/menu/flyout-placement.js +25 -0
- package/dist/core/inline/format-toggle.d.ts +12 -4
- package/dist/core/inline/format-toggle.js +94 -40
- package/dist/core/inline/image-dimensions.d.ts +3 -0
- package/dist/core/inline/image-dimensions.js +46 -10
- package/dist/core/inline/inline-widgets.d.ts +11 -0
- package/dist/core/inline/scan/brackets.js +1 -0
- package/dist/core/inline/scan/plugin-syntax.d.ts +8 -0
- package/dist/core/inline/scan/plugin-syntax.js +15 -1
- package/dist/core/inline-render.d.ts +6 -0
- package/dist/core/inline-render.js +32 -0
- package/dist/core/nodes.d.ts +14 -0
- package/dist/cursor/edge-affinity.js +2 -1
- package/dist/cursor/overlay-remeasure.js +8 -0
- package/dist/cursor/reveal-source.js +7 -2
- package/dist/cursor/widget-offset.d.ts +6 -0
- package/dist/cursor/widget-offset.js +61 -4
- package/dist/debug/interaction-trace.d.ts +4 -0
- package/dist/debug/interaction-trace.js +15 -0
- package/dist/decorations/decoration-state.svelte.js +1 -1
- package/dist/editor-actions/ancestry-folds.d.ts +2 -2
- package/dist/editor-actions/ancestry-folds.js +1 -1
- package/dist/editor-actions/block-edit-scope.js +1 -1
- package/dist/editor-actions/commit/text-batch.d.ts +3 -2
- package/dist/editor-actions/commit/text-batch.js +1 -1
- package/dist/editor-actions/commit/undo-controller.js +4 -2
- package/dist/editor-actions/container-edit.js +2 -1
- package/dist/editor-actions/enter-completion.d.ts +2 -0
- package/dist/editor-actions/enter-completion.js +23 -2
- package/dist/editor-actions/inline-range-commit.js +1 -1
- package/dist/editor-actions/reorder-action.js +19 -10
- package/dist/editor-actions/reorder-drag.js +29 -1
- package/dist/editor-actions/replacement-focus.d.ts +1 -1
- package/dist/editor-actions/replacement-focus.js +1 -1
- package/dist/editor-actions/search-replace.js +1 -1
- package/dist/editor-actions/table-context.d.ts +4 -1
- package/dist/editor-actions/table-context.js +57 -1
- package/dist/editor-events.d.ts +3 -0
- package/dist/editor-keys.d.ts +33 -0
- package/dist/editor-props.d.ts +21 -12
- package/dist/index.d.ts +1 -1
- package/dist/plugin.d.ts +6 -0
- package/dist/plugin.js +11 -0
- package/dist/plugins/latex/BlockMath.svelte +264 -29
- package/dist/plugins/latex/BlockMath.svelte.d.ts +2 -0
- package/dist/plugins/latex/index.d.ts +2 -1
- package/dist/plugins/latex/latex-kind.js +36 -3
- package/dist/plugins/latex/math-completion.js +4 -1
- package/dist/plugins/latex/math-layout.d.ts +15 -0
- package/dist/plugins/latex/math-layout.js +13 -0
- package/dist/plugins/latex/math-source.d.ts +19 -0
- package/dist/plugins/latex/math-source.js +97 -0
- package/dist/plugins/latex/register.d.ts +11 -2
- package/dist/plugins/latex/register.js +3 -1
- package/dist/plugins/latex/renderer.d.ts +3 -3
- package/dist/plugins/latex/renderer.js +13 -6
- package/dist/reactivity/list-windowing.svelte.d.ts +8 -8
- package/dist/reactivity/list-windowing.svelte.js +49 -24
- package/dist/schema/block-completions.d.ts +8 -0
- package/dist/schema/block-completions.js +11 -0
- package/dist/schema/context-actions.d.ts +31 -0
- package/dist/schema/context-actions.js +23 -0
- package/dist/schema/fenced-code-raw.js +31 -1
- package/dist/schema/operations.d.ts +8 -1
- package/dist/schema/reserved-chords.js +25 -4
- package/dist/schema/table-cell-raw.d.ts +1 -1
- package/dist/schema/table-cell-raw.js +1 -1
- package/dist/selection/block-hit-test.js +3 -2
- package/dist/selection/char-endpoint-snap.js +1 -1
- package/dist/selection/clipboard-text.js +6 -1
- package/dist/selection/covered-block.d.ts +10 -0
- package/dist/selection/covered-block.js +24 -0
- package/dist/selection/cross-block/dispatch.d.ts +3 -0
- package/dist/selection/cross-block/dispatch.js +13 -1
- package/dist/selection/cross-block/format-range.d.ts +1 -1
- package/dist/selection/cross-block/format-range.js +4 -15
- package/dist/selection/cross-block/format-toggle.js +1 -1
- package/dist/selection/cross-block/keydown.js +1 -1
- package/dist/selection/cross-block/ops.js +1 -1
- package/dist/selection/cross-block/paste.js +32 -30
- package/dist/selection/cross-block/type-replace.d.ts +3 -2
- package/dist/selection/cross-block/type-replace.js +62 -13
- package/dist/selection/dead-space-caret.d.ts +10 -0
- package/dist/selection/dead-space-caret.js +47 -1
- package/dist/selection/double-click-trim.d.ts +17 -0
- package/dist/selection/double-click-trim.js +57 -0
- package/dist/selection/drag-pointer.d.ts +7 -2
- package/dist/selection/drag-pointer.js +61 -2
- package/dist/selection/gap-caret.js +1 -1
- package/dist/selection/keyboard-extend.js +1 -1
- package/dist/selection/path-lookup.js +1 -1
- package/dist/selection/range-delete-ceremony.js +4 -2
- package/dist/selection/range-delete-chrome.js +2 -1
- package/dist/selection/range-delete-table-coverage.js +2 -1
- package/dist/selection/range-delete-table.js +3 -2
- package/dist/selection/range-delete.js +30 -2
- package/dist/selection/selection-restore.js +1 -1
- package/dist/selection/selection-state.svelte.d.ts +6 -0
- package/dist/selection/selection-state.svelte.js +36 -1
- package/dist/selection/table-endpoint-snap.js +1 -1
- package/dist/selection/table-rect-extend.js +1 -1
- package/dist/styles/editor-theme.css +57 -28
- package/dist/styles/editor.css +217 -18
- package/dist/testing/container-conformance.js +2 -2
- package/dist/testing/inline-conformance.js +2 -1
- package/dist/tree-operations/blockquote.js +1 -1
- package/dist/tree-operations/chain-rebuild.d.ts +63 -0
- package/dist/tree-operations/chain-rebuild.js +142 -0
- package/dist/tree-operations/children.d.ts +1 -1
- package/dist/tree-operations/children.js +1 -1
- package/dist/tree-operations/cleanup.js +1 -1
- package/dist/tree-operations/content-write.d.ts +50 -0
- package/dist/tree-operations/content-write.js +263 -0
- package/dist/tree-operations/index.d.ts +8 -3
- package/dist/tree-operations/index.js +6 -2
- package/dist/tree-operations/list/exit-replacement.js +1 -1
- package/dist/tree-operations/list/unwrap-merge.js +3 -3
- package/dist/tree-operations/node-ops.d.ts +17 -234
- package/dist/tree-operations/node-ops.js +47 -1113
- package/dist/tree-operations/node-primitives.d.ts +75 -0
- package/dist/tree-operations/node-primitives.js +117 -0
- package/dist/tree-operations/paste/apply.js +1 -1
- package/dist/tree-operations/paste/body-write.d.ts +1 -1
- package/dist/tree-operations/paste/body-write.js +2 -2
- package/dist/tree-operations/paste/container-match.js +4 -2
- package/dist/tree-operations/paste/dispatch.js +2 -1
- package/dist/tree-operations/paste/find-enclosing-list.js +1 -1
- package/dist/tree-operations/paste/focus-target.d.ts +1 -1
- package/dist/tree-operations/paste/list-absorb.js +1 -1
- package/dist/tree-operations/paste/list-break-out.js +1 -1
- package/dist/tree-operations/paste/parent-scope.js +1 -1
- package/dist/tree-operations/paste/paste-replacement.js +1 -1
- package/dist/tree-operations/paste/replace-block-at-parent.js +1 -1
- package/dist/tree-operations/path-mutate.d.ts +1 -1
- package/dist/tree-operations/path-mutate.js +2 -1
- package/dist/tree-operations/reorder-unit.js +1 -1
- package/dist/tree-operations/reorder.d.ts +5 -2
- package/dist/tree-operations/reorder.js +55 -2
- package/dist/tree-operations/settle.d.ts +105 -0
- package/dist/tree-operations/settle.js +660 -0
- package/dist/tree-operations/table-grid-clipboard.d.ts +21 -0
- package/dist/tree-operations/table-grid-clipboard.js +90 -0
- package/dist/tree-operations/unshare.d.ts +15 -77
- package/dist/tree-operations/unshare.js +15 -166
- package/docs/guide/consumer-guide.md +126 -96
- package/docs/guide/plugin-api.md +31 -3
- package/docs/guide/plugin-guide.md +26 -3
- package/package.json +4 -2
- package/dist/components/blocks/code/CodeLanguageChip.svelte +0 -127
- package/dist/components/blocks/code/CodeLanguageChip.svelte.d.ts +0 -14
- package/dist/components/blocks/table/TableGrip.svelte +0 -91
- package/dist/components/blocks/table/TableGrip.svelte.d.ts +0 -8
- package/dist/components/blocks/table/table-drop-target.d.ts +0 -1
- package/dist/components/blocks/table/table-drop-target.js +0 -16
- package/dist/components/blocks/table/table-reorder-drag.d.ts +0 -78
- package/dist/components/blocks/table/table-reorder-drag.js +0 -97
|
@@ -27,5 +27,13 @@ export function wireOverlayRemeasure(opts) {
|
|
|
27
27
|
editorRoot.addEventListener('scroll', measure, { passive: true });
|
|
28
28
|
disposers.push(() => editorRoot.removeEventListener('scroll', measure));
|
|
29
29
|
}
|
|
30
|
+
// The block's own box changing under a live range — a paragraph set to a heading, a font
|
|
31
|
+
// load, a resize — moves the text the rects were measured against. Layout-driven, so it
|
|
32
|
+
// fires after the change has painted; absent (jsdom) the scroll paths above still hold.
|
|
33
|
+
if (typeof ResizeObserver === 'function') {
|
|
34
|
+
const observer = new ResizeObserver(() => measure());
|
|
35
|
+
observer.observe(el);
|
|
36
|
+
disposers.push(() => observer.disconnect());
|
|
37
|
+
}
|
|
30
38
|
return () => disposers.forEach((dispose) => dispose());
|
|
31
39
|
}
|
|
@@ -8,13 +8,18 @@
|
|
|
8
8
|
import { tick } from 'svelte';
|
|
9
9
|
import { asRawOffset, toDomTextOffset } from './coordinate-spaces';
|
|
10
10
|
import { restoreCaretAtWalkOffset } from './focused-caret';
|
|
11
|
+
import { clampToLandableRaw } from './widget-offset';
|
|
11
12
|
import { assertInvariant } from '../assert';
|
|
12
13
|
import { checkRevealSourceLength } from '../invariants/inline-transitions';
|
|
13
14
|
export function createSourceReveal(deps) {
|
|
14
15
|
/** Places the caret at a BLOCK-source offset, converting to ambient-included walk space. */
|
|
15
16
|
function placeCaret(container, blockSourceOffset) {
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
// Offset 0 of a source that opens with hidden chrome (a leaf painting `$$` fence lines) is
|
|
18
|
+
// BEFORE that chrome: typing there would land outside the fence. The seat is the nearest
|
|
19
|
+
// landable position, which is a no-op wherever nothing hides.
|
|
20
|
+
const ambient = deps.getAmbientLength();
|
|
21
|
+
const seat = clampToLandableRaw(container, blockSourceOffset, ambient);
|
|
22
|
+
restoreCaretAtWalkOffset(container, toDomTextOffset(asRawOffset(seat), ambient));
|
|
18
23
|
}
|
|
19
24
|
async function reveal(atSourceOffset = 0) {
|
|
20
25
|
assertInvariant('reveal-transition', () => checkRevealSourceLength(deps.source.length, deps.sourceStart, deps.sourceEnd));
|
|
@@ -101,6 +101,12 @@ export declare function landableDomTextBounds(container: ParentNode): {
|
|
|
101
101
|
* questions: a reference label is chrome that stays hidden, so it is not content standing behind
|
|
102
102
|
* the stamp, and it is not the paint the stamp promises either.
|
|
103
103
|
*/
|
|
104
|
+
/**
|
|
105
|
+
* The container's bytes outside its marker chrome — what a painted source holds as CONTENT,
|
|
106
|
+
* whatever the mode paints. A fence with an empty body answers whitespace; the leaf reads that
|
|
107
|
+
* as "nothing here a Backspace could mean but the block".
|
|
108
|
+
*/
|
|
109
|
+
export declare function chromeFreeText(container: ParentNode): string;
|
|
104
110
|
export declare function holdsOnlyMarkerChrome(container: ParentNode): boolean;
|
|
105
111
|
/**
|
|
106
112
|
* Whether the mode paints NOTHING landable here: every walk segment is a hidden marker run, so no
|
|
@@ -85,8 +85,11 @@ export function findDomTextOffsetTarget(container, target) {
|
|
|
85
85
|
const after = positionBeside(seg.last, 'after');
|
|
86
86
|
if (!after)
|
|
87
87
|
continue;
|
|
88
|
-
|
|
88
|
+
// Chromium canonicalizes the slot after a HIDDEN run upstream across it, so a byte typed
|
|
89
|
+
// there lands before the run; text starting at the target is the position that keeps it.
|
|
90
|
+
if (seg.start + seg.len > target || (seg.start + seg.len === target && !seg.hidden)) {
|
|
89
91
|
return after;
|
|
92
|
+
}
|
|
90
93
|
last = after;
|
|
91
94
|
}
|
|
92
95
|
return last;
|
|
@@ -239,7 +242,19 @@ export function revealsNoMarkers(container) {
|
|
|
239
242
|
* An unmounted surface reads as source, where nothing hides and no rewrite has a run to move.
|
|
240
243
|
*/
|
|
241
244
|
export function screenVisibilityOf(container) {
|
|
242
|
-
return screenVisibility(container === null ? 'source' : (markerHidingMode(container) ?? 'source'), { chromePaints: container instanceof Element && container
|
|
245
|
+
return screenVisibility(container === null ? 'source' : (markerHidingMode(container) ?? 'source'), { chromePaints: container instanceof Element && chromeStampPaints(container) });
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* The stamp's own condition, as `styles/editor.css` states it: the container carries
|
|
249
|
+
* `data-content-empty` AND holds focus. The seat is for a caret, so a stamped block nobody is in
|
|
250
|
+
* keeps its chrome hidden and reads as the empty construct it is. Focus containment rather than
|
|
251
|
+
* `:focus-within`, so a selector engine without that pseudo-class (jsdom's) reads the same answer.
|
|
252
|
+
*/
|
|
253
|
+
function chromeStampPaints(container) {
|
|
254
|
+
if (!container.hasAttribute(CONTENT_EMPTY_ATTR))
|
|
255
|
+
return false;
|
|
256
|
+
const active = container.ownerDocument.activeElement;
|
|
257
|
+
return active !== null && container.contains(active);
|
|
243
258
|
}
|
|
244
259
|
/**
|
|
245
260
|
* The extreme walk offsets a caret can occupy in `container`: a hidden marker run or the leading
|
|
@@ -252,6 +267,7 @@ export function landableDomTextBounds(container) {
|
|
|
252
267
|
let start = 0;
|
|
253
268
|
let end = 0;
|
|
254
269
|
let landed = false;
|
|
270
|
+
let lastText = null;
|
|
255
271
|
for (const seg of landingSegments(container, markerHidingMode(container))) {
|
|
256
272
|
if (seg.len === 0)
|
|
257
273
|
continue;
|
|
@@ -265,9 +281,28 @@ export function landableDomTextBounds(container) {
|
|
|
265
281
|
}
|
|
266
282
|
landed = true;
|
|
267
283
|
end = stop;
|
|
284
|
+
lastText = seg.kind === 'text' ? seg.node : null;
|
|
285
|
+
}
|
|
286
|
+
// The position after the last landable run's final `\n` is the start of a line nothing
|
|
287
|
+
// paints: the engine seats no caret there, so an end gate reading it never fires and the
|
|
288
|
+
// caret is stuck on the empty line. A caret anchor gives that line its paint and keeps it.
|
|
289
|
+
if (lastText?.textContent?.endsWith('\n') && !followedByCaretAnchor(lastText, container)) {
|
|
290
|
+
end -= 1;
|
|
268
291
|
}
|
|
269
292
|
return { start: asDomTextOffset(start), end: asDomTextOffset(Math.max(start, end)) };
|
|
270
293
|
}
|
|
294
|
+
/** Whether the next painted node after `text`, at any depth above it, is a caret anchor. */
|
|
295
|
+
function followedByCaretAnchor(text, root) {
|
|
296
|
+
for (let node = text; node && node !== root; node = node.parentNode) {
|
|
297
|
+
let next = node.nextSibling;
|
|
298
|
+
while (next?.nodeType === Node.TEXT_NODE && (next.textContent?.length ?? 0) === 0) {
|
|
299
|
+
next = next.nextSibling;
|
|
300
|
+
}
|
|
301
|
+
if (next)
|
|
302
|
+
return next instanceof HTMLElement && next.tagName === 'BR' && 'caretAnchor' in next.dataset;
|
|
303
|
+
}
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
271
306
|
/**
|
|
272
307
|
* Whether every byte `container` holds is marker chrome, at least one span of it a family the
|
|
273
308
|
* empty-construct override paints. The stamp condition the render path writes as
|
|
@@ -276,6 +311,22 @@ export function landableDomTextBounds(container) {
|
|
|
276
311
|
* questions: a reference label is chrome that stays hidden, so it is not content standing behind
|
|
277
312
|
* the stamp, and it is not the paint the stamp promises either.
|
|
278
313
|
*/
|
|
314
|
+
/**
|
|
315
|
+
* The container's bytes outside its marker chrome — what a painted source holds as CONTENT,
|
|
316
|
+
* whatever the mode paints. A fence with an empty body answers whitespace; the leaf reads that
|
|
317
|
+
* as "nothing here a Backspace could mean but the block".
|
|
318
|
+
*/
|
|
319
|
+
export function chromeFreeText(container) {
|
|
320
|
+
let text = '';
|
|
321
|
+
for (const seg of walkSegments(container, null)) {
|
|
322
|
+
if (seg.kind !== 'text' || seg.len === 0)
|
|
323
|
+
continue;
|
|
324
|
+
if (markerRootOf(seg.node, container) !== null)
|
|
325
|
+
continue;
|
|
326
|
+
text += seg.node.textContent ?? '';
|
|
327
|
+
}
|
|
328
|
+
return text;
|
|
329
|
+
}
|
|
279
330
|
export function holdsOnlyMarkerChrome(container) {
|
|
280
331
|
let chrome = false;
|
|
281
332
|
for (const seg of walkSegments(container, null)) {
|
|
@@ -383,8 +434,7 @@ function markerRootOf(node, root) {
|
|
|
383
434
|
function chromeStandsAloneUnder(root, mode) {
|
|
384
435
|
if (mode === null || !(root instanceof Element))
|
|
385
436
|
return false;
|
|
386
|
-
return screenVisibility(mode, { chromePaints: root
|
|
387
|
-
.chromePaints;
|
|
437
|
+
return screenVisibility(mode, { chromePaints: chromeStampPaints(root) }).chromePaints;
|
|
388
438
|
}
|
|
389
439
|
/**
|
|
390
440
|
* Whether `node` is inside the leading ambient marker island — a container's `- ` / `1. ` prefix,
|
|
@@ -442,8 +492,15 @@ function positionBeside(el, side) {
|
|
|
442
492
|
return { node: sibling, offset: side === 'before' ? (sibling.textContent?.length ?? 0) : 0 };
|
|
443
493
|
}
|
|
444
494
|
const idx = Array.prototype.indexOf.call(parent.childNodes, el);
|
|
495
|
+
// A pending hard break's first anchor (`inline-render.ts`) ends the line the marker sat on;
|
|
496
|
+
// the seat past the marker is the start of the next one, which is after that anchor.
|
|
497
|
+
if (side === 'after' && isBreakAnchor(sibling))
|
|
498
|
+
return { node: parent, offset: idx + 2 };
|
|
445
499
|
return { node: parent, offset: side === 'before' ? idx : idx + 1 };
|
|
446
500
|
}
|
|
501
|
+
function isBreakAnchor(node) {
|
|
502
|
+
return (node instanceof HTMLElement && node.tagName === 'BR' && node.dataset.caretAnchor === 'break');
|
|
503
|
+
}
|
|
447
504
|
/**
|
|
448
505
|
* The classification every walk-space consumer shares: a text node contributes its textContent
|
|
449
506
|
* length, an atomic widget its raw source length (never descended), any other element is
|
|
@@ -17,6 +17,7 @@ export declare function disableInteractionTrace(): void;
|
|
|
17
17
|
export declare function isInteractionTraceEnabled(): boolean;
|
|
18
18
|
/** Empty the buffer without touching the enabled flag — test isolation. */
|
|
19
19
|
export declare function resetInteractionTrace(): void;
|
|
20
|
+
export declare function interactionTraceKeydownCount(): number;
|
|
20
21
|
export declare function interactionTraceSnapshot(): InteractionTraceEntry[];
|
|
21
22
|
/** `changed` names the differing render-key segments, comma-joined. */
|
|
22
23
|
export declare function traceRebuild(changed: string, force: boolean): void;
|
|
@@ -37,3 +38,6 @@ export declare function traceCompositionEnd(): void;
|
|
|
37
38
|
export declare function traceIslandsApplied(count: number): void;
|
|
38
39
|
export declare function traceStickyCapture(x: number): void;
|
|
39
40
|
export declare function traceStickyReset(): void;
|
|
41
|
+
/** `handled` is the event's own `defaultPrevented`: whether the editor claimed the press.
|
|
42
|
+
* Recorded once per keydown, after the surface handler's await chain has settled. */
|
|
43
|
+
export declare function traceKeydownVerdict(key: string, handled: boolean): void;
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
const CAPACITY = 200;
|
|
9
9
|
let enabled = false;
|
|
10
10
|
let buf = [];
|
|
11
|
+
// Monotonic, so the harness poll costs one read and the ring buffer's eviction cannot
|
|
12
|
+
// retract a verdict it already counted.
|
|
13
|
+
let keydownVerdicts = 0;
|
|
11
14
|
// ── Switch and readout ──────────────────────────────────────────────────────
|
|
12
15
|
export function enableInteractionTrace() {
|
|
13
16
|
enabled = true;
|
|
@@ -21,6 +24,10 @@ export function isInteractionTraceEnabled() {
|
|
|
21
24
|
/** Empty the buffer without touching the enabled flag — test isolation. */
|
|
22
25
|
export function resetInteractionTrace() {
|
|
23
26
|
buf = [];
|
|
27
|
+
keydownVerdicts = 0;
|
|
28
|
+
}
|
|
29
|
+
export function interactionTraceKeydownCount() {
|
|
30
|
+
return keydownVerdicts;
|
|
24
31
|
}
|
|
25
32
|
export function interactionTraceSnapshot() {
|
|
26
33
|
return buf.slice();
|
|
@@ -103,3 +110,11 @@ export function traceStickyReset() {
|
|
|
103
110
|
return;
|
|
104
111
|
record('sticky-column', 'reset');
|
|
105
112
|
}
|
|
113
|
+
/** `handled` is the event's own `defaultPrevented`: whether the editor claimed the press.
|
|
114
|
+
* Recorded once per keydown, after the surface handler's await chain has settled. */
|
|
115
|
+
export function traceKeydownVerdict(key, handled) {
|
|
116
|
+
if (!enabled)
|
|
117
|
+
return;
|
|
118
|
+
keydownVerdicts++;
|
|
119
|
+
record('keydown', 'verdict', { key, handled });
|
|
120
|
+
}
|
|
@@ -5,7 +5,7 @@ import { islandPosition } from './island-dom';
|
|
|
5
5
|
import { assertInvariant } from '../assert';
|
|
6
6
|
import { isCommitInProgress } from '../invariants/commit-scope';
|
|
7
7
|
import { contentLengthOf, isProseKind } from '../core/inline';
|
|
8
|
-
import { isBlockNode, nodeAt } from '../tree-operations/node-
|
|
8
|
+
import { isBlockNode, nodeAt } from '../tree-operations/node-primitives';
|
|
9
9
|
import { devWarn } from '../dev-warn';
|
|
10
10
|
import { recordDecorationRun } from '../perf/instruments';
|
|
11
11
|
const EMPTY_MARKS = [];
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Publishing side of the ancestry settle (`tree-operations/
|
|
2
|
+
* Publishing side of the ancestry settle (`tree-operations/chain-rebuild.ts`): a fold at a container's
|
|
3
3
|
* own slot splices its PARENT's children, a scope the commit's own change descriptor never
|
|
4
4
|
* covers, so that scope's ids and refs are resynced here instead.
|
|
5
5
|
*/
|
|
6
6
|
import type { CstNode } from '../core/nodes';
|
|
7
|
-
import type { AncestrySeamFold } from '../tree-operations/
|
|
7
|
+
import type { AncestrySeamFold } from '../tree-operations/chain-rebuild';
|
|
8
8
|
import { type StructuralChange } from '../tree-operations/structural-change';
|
|
9
9
|
import type { EditorActionsDeps } from './deps';
|
|
10
10
|
/** Where the folded container's first byte ended up, as a document path. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Publishing side of the ancestry settle (`tree-operations/
|
|
2
|
+
* Publishing side of the ancestry settle (`tree-operations/chain-rebuild.ts`): a fold at a container's
|
|
3
3
|
* own slot splices its PARENT's children, a scope the commit's own change descriptor never
|
|
4
4
|
* covers, so that scope's ids and refs are resynced here instead.
|
|
5
5
|
*/
|
|
@@ -44,7 +44,7 @@ export function createTopLevelScope(deps, controller) {
|
|
|
44
44
|
// ── Container adapter ────────────────────────────────────────────────────────
|
|
45
45
|
export function createContainerScope(state, deps) {
|
|
46
46
|
return {
|
|
47
|
-
// A fold at this container's own slot detaches it (`tree-operations/
|
|
47
|
+
// A fold at this container's own slot detaches it (`tree-operations/chain-rebuild.ts`), so a
|
|
48
48
|
// post-commit read can find the scope gone rather than merely empty.
|
|
49
49
|
children: () => deps.node?.children ?? [],
|
|
50
50
|
refAt: (i) => state.innerBlockRefs[i],
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
export interface TextBatchDeps {
|
|
7
7
|
/** Capture the pre-edit snapshot for the first keystroke of a batch. */
|
|
8
8
|
pushSnapshot(leafPath: number[], offset: number): void;
|
|
9
|
-
/** Emit the batched `input` edit event when a batch flushes.
|
|
10
|
-
|
|
9
|
+
/** Emit the batched `input` edit event when a batch flushes. Omitted by a batch with no edit
|
|
10
|
+
* channel of its own: a revealed source's bytes reach it once, on the fold's commit. */
|
|
11
|
+
emitInput?(leafPath: number[], byteLength: number): void;
|
|
11
12
|
}
|
|
12
13
|
export interface TextBatch {
|
|
13
14
|
/**
|
|
@@ -17,7 +17,7 @@ export function createTextBatch(deps) {
|
|
|
17
17
|
*/
|
|
18
18
|
function flushPendingInput() {
|
|
19
19
|
if (batchByteLength > 0 && batchPath) {
|
|
20
|
-
deps.emitInput(batchPath, batchByteLength);
|
|
20
|
+
deps.emitInput?.(batchPath, batchByteLength);
|
|
21
21
|
}
|
|
22
22
|
batchPath = null;
|
|
23
23
|
batchByteLength = 0;
|
|
@@ -12,8 +12,10 @@ import { assertInvariant } from '../../assert';
|
|
|
12
12
|
import { beginCommit, endCommit } from '../../invariants/commit-scope';
|
|
13
13
|
import { assignIds } from '../../block-id';
|
|
14
14
|
import { replaceRefs } from '../../reactivity/publish-ref.svelte';
|
|
15
|
-
import { nodeAt
|
|
16
|
-
import {
|
|
15
|
+
import { nodeAt } from '../../tree-operations/node-primitives';
|
|
16
|
+
import { settleSeparator } from '../../tree-operations/settle';
|
|
17
|
+
import { ensureUnsharedPath } from '../../tree-operations/unshare';
|
|
18
|
+
import { attachedChainPrefix, rebuildUnsharedChain } from '../../tree-operations/chain-rebuild';
|
|
17
19
|
import { foldLandingFor, publishAncestryFolds } from '../ancestry-folds';
|
|
18
20
|
import { createTextBatch } from './text-batch';
|
|
19
21
|
import { toEditEvent } from '../../editor-events';
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* ContainerEditActions factory: the debounced checkpoint pusher for raw typing
|
|
3
3
|
* outside the commit primitive, the doc-root reactivity nudge, and `commitContainer`.
|
|
4
4
|
*/
|
|
5
|
-
import { ensureUnsharedPath
|
|
5
|
+
import { ensureUnsharedPath } from '../tree-operations/unshare';
|
|
6
|
+
import { rebuildUnsharedChain } from '../tree-operations/chain-rebuild';
|
|
6
7
|
import { publishAncestryFolds, publishScopeFold } from './ancestry-folds';
|
|
7
8
|
export function createContainerEditActions(deps, controller) {
|
|
8
9
|
return {
|
|
@@ -23,3 +23,5 @@ export interface EnterCompletion {
|
|
|
23
23
|
export declare function withEnterCompletion(blockEdit: BlockEditActions, childAt: (index: number) => NodeView | undefined): BlockEditActions;
|
|
24
24
|
/** The completion a press at `offset` earns, or null when the block or the caret disqualifies it. */
|
|
25
25
|
export declare function planEnterCompletion(node: NodeView | undefined, offset: number): EnterCompletion | null;
|
|
26
|
+
/** The completion a keystroke earns, consulting only the completers that answer on type. */
|
|
27
|
+
export declare function planTypedCompletion(node: NodeView | undefined, offset: number): EnterCompletion | null;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { parse } from '../core/parser';
|
|
7
7
|
import { displayLength, splitLines, trailingLineEnding } from '../core/lines';
|
|
8
8
|
import { getBlockKindDescriptor } from '../schema/block-kind-descriptor';
|
|
9
|
-
import { completeTypedLine } from '../schema/block-completions';
|
|
9
|
+
import { completeLineOnType, completeTypedLine } from '../schema/block-completions';
|
|
10
10
|
/**
|
|
11
11
|
* Wraps a COMPOSED `splitBlock` with the consult, at the two bundle composition sites only. Above
|
|
12
12
|
* the container overrides rather than inside the split body, so a container that replaces
|
|
@@ -26,17 +26,38 @@ export function withEnterCompletion(blockEdit, childAt) {
|
|
|
26
26
|
// `snapshotOffset` is where the caret WAS, so one undo restores the typed line with the
|
|
27
27
|
// caret at its end rather than in front of it.
|
|
28
28
|
await blockEdit.replaceBlock(index, completion.replacement, { replacementIndex: 0, ...completion.caret }, { snapshotOffset: offset });
|
|
29
|
+
},
|
|
30
|
+
// The typed arm: a keystroke that leaves the block as a line an on-type completer claims
|
|
31
|
+
// (see `BlockCompleter.onType`) forms the structure at once, the way a typed ` ``` ` is a
|
|
32
|
+
// fence the moment the parser sees it. The write lands first, so the typed line is its own
|
|
33
|
+
// undo step and the mint replaces the bytes the CST actually holds.
|
|
34
|
+
async updateBlockContent(index, text, preEditOffset, postEditFocusOffset) {
|
|
35
|
+
await blockEdit.updateBlockContent(index, text, preEditOffset, postEditFocusOffset);
|
|
36
|
+
const offset = postEditFocusOffset ?? preEditOffset;
|
|
37
|
+
if (offset === undefined)
|
|
38
|
+
return;
|
|
39
|
+
const completion = planTypedCompletion(childAt(index), offset);
|
|
40
|
+
if (!completion)
|
|
41
|
+
return;
|
|
42
|
+
await blockEdit.replaceBlock(index, completion.replacement, { replacementIndex: 0, ...completion.caret }, { snapshotOffset: offset });
|
|
29
43
|
}
|
|
30
44
|
};
|
|
31
45
|
}
|
|
32
46
|
/** The completion a press at `offset` earns, or null when the block or the caret disqualifies it. */
|
|
33
47
|
export function planEnterCompletion(node, offset) {
|
|
48
|
+
return planCompletion(node, offset, completeTypedLine);
|
|
49
|
+
}
|
|
50
|
+
/** The completion a keystroke earns, consulting only the completers that answer on type. */
|
|
51
|
+
export function planTypedCompletion(node, offset) {
|
|
52
|
+
return planCompletion(node, offset, completeLineOnType);
|
|
53
|
+
}
|
|
54
|
+
function planCompletion(node, offset, consult) {
|
|
34
55
|
if (!node)
|
|
35
56
|
return null;
|
|
36
57
|
const line = wholeTypedLine(node);
|
|
37
58
|
if (line === null || offset !== displayLength(node.raw))
|
|
38
59
|
return null;
|
|
39
|
-
const claim =
|
|
60
|
+
const claim = consult(line);
|
|
40
61
|
if (!claim)
|
|
41
62
|
return null;
|
|
42
63
|
// Through the parser rather than a hand-built node, so the mint is exactly what a reload of
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { docPathFrom } from '../cursor/coordinate-spaces';
|
|
7
7
|
import { expectStateForNode } from '../reactivity/state-registry';
|
|
8
|
-
import { isBlockNode, nodeAt, normalizeOwnRaw, writeOwnRaw } from '../tree-operations/node-
|
|
8
|
+
import { isBlockNode, nodeAt, normalizeOwnRaw, writeOwnRaw } from '../tree-operations/node-primitives';
|
|
9
9
|
import { ensureUnsharedChild, ensureUnsharedPath } from '../tree-operations/unshare';
|
|
10
10
|
export function createInlineRangeCommit(deps) {
|
|
11
11
|
async function commitInlineRange(path, start, end, bytes, caretAfter) {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { CURSOR_START } from '../block-component';
|
|
10
10
|
import { reorderChildrenWithTrivia } from '../tree-operations/reorder';
|
|
11
11
|
import { resolveReorderUnit } from '../tree-operations/reorder-unit';
|
|
12
|
-
import { blockNodeAt, nodeAt } from '../tree-operations/node-
|
|
12
|
+
import { blockNodeAt, nodeAt } from '../tree-operations/node-primitives';
|
|
13
13
|
import { renumberOrderedList } from '../tree-operations/list/ordered-markers';
|
|
14
14
|
import { expectStateForNode } from '../reactivity/state-registry';
|
|
15
15
|
import { readCurrentSelection } from '../selection/native-bridge';
|
|
@@ -25,7 +25,7 @@ export function createReorderAction(deps, controller, onReorder) {
|
|
|
25
25
|
function caretOffset() {
|
|
26
26
|
return readCurrentSelection(deps.selectionState, deps.blockRefs)?.focus.offset ?? 0;
|
|
27
27
|
}
|
|
28
|
-
async function commitReorder(unit, to, offset) {
|
|
28
|
+
async function commitReorder(unit, to, offset, focusAfter) {
|
|
29
29
|
let landing = to;
|
|
30
30
|
if (unit.scope === 'document') {
|
|
31
31
|
await controller.commitStructural({
|
|
@@ -36,11 +36,14 @@ export function createReorderAction(deps, controller, onReorder) {
|
|
|
36
36
|
eventPath: docPathFrom([unit.index])
|
|
37
37
|
},
|
|
38
38
|
mutate: (children) => {
|
|
39
|
-
const settled = reorderChildrenWithTrivia(children, unit.index, to, deps.sharing);
|
|
39
|
+
const settled = reorderChildrenWithTrivia(children, unit.index, to, deps.sharing, true);
|
|
40
40
|
landing = settled.landing;
|
|
41
41
|
return settled.change;
|
|
42
42
|
},
|
|
43
|
-
afterTick: () =>
|
|
43
|
+
afterTick: () => {
|
|
44
|
+
if (focusAfter)
|
|
45
|
+
deps.blockRefs[landing]?.focus(CURSOR_START);
|
|
46
|
+
}
|
|
44
47
|
});
|
|
45
48
|
return { landing, total: deps.doc.children.length };
|
|
46
49
|
}
|
|
@@ -69,7 +72,10 @@ export function createReorderAction(deps, controller, onReorder) {
|
|
|
69
72
|
}
|
|
70
73
|
return settled.change;
|
|
71
74
|
},
|
|
72
|
-
afterTick: () =>
|
|
75
|
+
afterTick: () => {
|
|
76
|
+
if (focusAfter)
|
|
77
|
+
state.innerBlockRefs[landing]?.focus(CURSOR_START);
|
|
78
|
+
}
|
|
73
79
|
});
|
|
74
80
|
// Re-resolved, not `parent`: the ceremony's copy-on-write replaced that node, so the one
|
|
75
81
|
// this action resolved still holds the pre-move children.
|
|
@@ -86,19 +92,22 @@ export function createReorderAction(deps, controller, onReorder) {
|
|
|
86
92
|
return null;
|
|
87
93
|
return { unit, to };
|
|
88
94
|
}
|
|
89
|
-
async function run(fromPath, computeTo) {
|
|
95
|
+
async function run(fromPath, computeTo, focusAfter) {
|
|
90
96
|
const target = resolveAndClamp(fromPath, computeTo);
|
|
91
97
|
if (!target)
|
|
92
98
|
return;
|
|
93
99
|
// Drop any cross-block selection so the overlay doesn't fight the move; the
|
|
94
|
-
// commit's afterTick re-places the caret.
|
|
100
|
+
// commit's afterTick re-places the caret when the caller wants it.
|
|
95
101
|
deps.selectionState.collapse();
|
|
96
|
-
const outcome = await commitReorder(target.unit, target.to, caretOffset());
|
|
102
|
+
const outcome = await commitReorder(target.unit, target.to, caretOffset(), focusAfter);
|
|
97
103
|
if (outcome)
|
|
98
104
|
onReorder?.(outcome.landing, outcome.total);
|
|
99
105
|
}
|
|
100
106
|
return {
|
|
101
|
-
|
|
102
|
-
|
|
107
|
+
// A dropped block is not a block you asked to edit: focusing it opens whatever a caret
|
|
108
|
+
// opens there (an equation reveals its source), which is not what a drag asked for.
|
|
109
|
+
// The keyboard nudge is the opposite — the caret must ride the block it is moving.
|
|
110
|
+
moveReorderUnit: (fromPath, toIndex) => run(fromPath, () => toIndex, false),
|
|
111
|
+
nudgeReorderUnit: (fromPath, dir) => run(fromPath, (index) => index + dir, true)
|
|
103
112
|
};
|
|
104
113
|
}
|
|
@@ -144,9 +144,37 @@ function indexOf(host) {
|
|
|
144
144
|
return readBlockPath(host)?.at(-1) ?? null;
|
|
145
145
|
return readBlockPath(host.querySelector('[data-block-path]'))?.at(-2) ?? null;
|
|
146
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* What the block IS, for the kinds whose text does not read as a label: a table's cells run
|
|
149
|
+
* together into `IngredientAmountWater35 L`, an equation reads as its own source. Prose keeps
|
|
150
|
+
* its first words, which is the best label it could have.
|
|
151
|
+
*/
|
|
152
|
+
const KIND_LABELS = {
|
|
153
|
+
table: 'Table',
|
|
154
|
+
mathBlock: 'Equation',
|
|
155
|
+
fencedCode: 'Code',
|
|
156
|
+
mermaid: 'Diagram',
|
|
157
|
+
thematicBreak: 'Divider',
|
|
158
|
+
details: 'Details',
|
|
159
|
+
image: 'Image'
|
|
160
|
+
};
|
|
147
161
|
function ghostLabel(host) {
|
|
162
|
+
const kind = host.dataset.blockKind ?? '';
|
|
163
|
+
if (kind === 'table')
|
|
164
|
+
return tableLabel(host);
|
|
165
|
+
const named = KIND_LABELS[kind];
|
|
166
|
+
if (named)
|
|
167
|
+
return named;
|
|
148
168
|
const text = (host.textContent ?? '').trim().replace(/\s+/g, ' ');
|
|
149
169
|
if (!text)
|
|
150
|
-
return '
|
|
170
|
+
return host.querySelector('[data-image-widget]') ? 'Image' : 'Block';
|
|
151
171
|
return text.length > 40 ? text.slice(0, 40) + '…' : text;
|
|
152
172
|
}
|
|
173
|
+
/** Body rows × columns: the shape is what tells one table from another at a glance. */
|
|
174
|
+
function tableLabel(host) {
|
|
175
|
+
const rows = host.querySelectorAll('.table-row').length;
|
|
176
|
+
const cols = host.querySelector('.table-row')?.childElementCount ?? 0;
|
|
177
|
+
if (rows === 0 || cols === 0)
|
|
178
|
+
return 'Table';
|
|
179
|
+
return `Table · ${rows} × ${cols}`;
|
|
180
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* probe that picks structural-vs-typing, and the post-commit caret restore.
|
|
4
4
|
*/
|
|
5
5
|
import { updateNodeContent } from '../tree-operations';
|
|
6
|
-
import { type SettledContent } from '../tree-operations/
|
|
6
|
+
import { type SettledContent } from '../tree-operations/content-write';
|
|
7
7
|
import { type AnyBlockKind } from '../core/nodes';
|
|
8
8
|
import type { NodeView } from '../core/node-views';
|
|
9
9
|
import type { StructuralChange } from '../tree-operations/structural-change';
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* probe that picks structural-vs-typing, and the post-commit caret restore.
|
|
4
4
|
*/
|
|
5
5
|
import { updateNodeContent } from '../tree-operations';
|
|
6
|
-
import { settledCaretTarget } from '../tree-operations/
|
|
6
|
+
import { settledCaretTarget } from '../tree-operations/content-write';
|
|
7
7
|
import { makeBlockNode } from '../core/nodes';
|
|
8
8
|
import { readBlockPath } from '../selection/path-lookup';
|
|
9
9
|
// ── Reparse probe ────────────────────────────────────────────────────────────
|
|
@@ -2,7 +2,7 @@ import { parse } from '../core/parser';
|
|
|
2
2
|
import { cloneNode } from '../tree-operations/clone';
|
|
3
3
|
import { spliceMany } from '../tree-operations/splice-many';
|
|
4
4
|
import { getBlockKindDescriptor } from '../schema/block-kind-descriptor';
|
|
5
|
-
import { normalizeBodyWrite, normalizeReplacementTrivia, writeOwnRaw } from '../tree-operations/node-
|
|
5
|
+
import { normalizeBodyWrite, normalizeReplacementTrivia, writeOwnRaw } from '../tree-operations/node-primitives';
|
|
6
6
|
import { rebuildAncestryRaw } from '../schema/container-raw';
|
|
7
7
|
import { replacePreservingFirst, stampStructuralChange } from '../tree-operations/structural-change';
|
|
8
8
|
import { applyRangesToText } from '../search/replace';
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* live here.
|
|
5
5
|
*/
|
|
6
6
|
import type { CellPosition, ContainerEditActions, TableContext } from '../action-contracts';
|
|
7
|
+
import type { GrammarView } from '../schema/block-openers';
|
|
7
8
|
import type { NodeView } from '../core/node-views';
|
|
8
9
|
import type { UndoController } from './deps';
|
|
9
10
|
import type { BlockListState } from '../reactivity/block-list-state.svelte';
|
|
@@ -29,6 +30,8 @@ export interface TableMutationsContextDeps {
|
|
|
29
30
|
controller: UndoController;
|
|
30
31
|
focusCell: (rowIdx: number, colIdx: number, position: CellPosition) => void;
|
|
31
32
|
announceReorder: (message: string) => void;
|
|
33
|
+
/** The instance grammar the cell byte-write door threads (absent: the global one). */
|
|
34
|
+
grammar?: GrammarView;
|
|
32
35
|
}
|
|
33
|
-
export type TableMutationsContext = Pick<TableContext, 'insertRowAbove' | 'insertRowBelow' | 'insertColumnLeft' | 'insertColumnRight' | 'deleteRow' | 'deleteColumn' | 'reorderRowTo' | 'moveRowUp' | 'moveRowDown' | 'reorderColumnTo' | 'moveColumnLeft' | 'moveColumnRight' | 'cycleAlignment' | 'setColumnAlignment'>;
|
|
36
|
+
export type TableMutationsContext = Pick<TableContext, 'insertRowAbove' | 'insertRowBelow' | 'insertColumnLeft' | 'insertColumnRight' | 'deleteRow' | 'deleteColumn' | 'reorderRowTo' | 'moveRowUp' | 'moveRowDown' | 'reorderColumnTo' | 'moveColumnLeft' | 'moveColumnRight' | 'cycleAlignment' | 'setColumnAlignment' | 'pasteGrid'>;
|
|
34
37
|
export declare function createTableMutationsContext(deps: TableMutationsContextDeps): TableMutationsContext;
|
|
@@ -8,8 +8,9 @@ import { extendDocPath, docPathFrom } from '../cursor/coordinate-spaces';
|
|
|
8
8
|
import { getStateForNode } from '../reactivity/state-registry';
|
|
9
9
|
import { assertInvariant } from '../assert';
|
|
10
10
|
import { columnAligned, COLUMN_ALIGNMENT_CLEARED, DELETED_COLUMN, DELETED_ROW, INSERTED_COLUMN, INSERTED_ROW, movedColumnToPosition, movedRowToPosition } from '../a11y-strings';
|
|
11
|
-
import { ensureUnsharedChildren } from '../tree-operations/unshare';
|
|
11
|
+
import { ensureUnsharedChildren, ensureUnsharedSubtree } from '../tree-operations/unshare';
|
|
12
12
|
import { rebuildTableRowRaw, rebuildTableRaw } from '../schema/container-rebuilders';
|
|
13
|
+
import { writeOwnRaw } from '../tree-operations/node-primitives';
|
|
13
14
|
import { reorderChildren } from '../tree-operations/reorder';
|
|
14
15
|
import { insertEmptyRow, insertEmptyColumn, deleteRow as mutDeleteRow, deleteColumn as mutDeleteColumn, moveColumn as mutMoveColumn, cycleAlignment as mutCycleAlignment, setAlignment as mutSetAlignment, canDeleteRow, canDeleteColumn } from '../tree-operations/table-mutations';
|
|
15
16
|
/**
|
|
@@ -105,6 +106,60 @@ export function createTableMutationsContext(deps) {
|
|
|
105
106
|
afterTick: opts.afterTick
|
|
106
107
|
});
|
|
107
108
|
}
|
|
109
|
+
// Grown at the bottom and the right only, so each scope's change is one contiguous insert:
|
|
110
|
+
// rows for the table, cells for every mounted row. Cell texts go through the cell kind's own
|
|
111
|
+
// raw rule in place, and the table raw is rebuilt once, delimiter line included.
|
|
112
|
+
async function pasteGrid(origin, grid) {
|
|
113
|
+
const { myPath, controller, focusCell } = deps;
|
|
114
|
+
const rows = grid.length;
|
|
115
|
+
let cols = 0;
|
|
116
|
+
for (const line of grid)
|
|
117
|
+
cols = Math.max(cols, line.length);
|
|
118
|
+
if (rows === 0 || cols === 0)
|
|
119
|
+
return;
|
|
120
|
+
const { scopes, rowIndices } = mountedColumnScopes();
|
|
121
|
+
await controller.commitMultiScope({
|
|
122
|
+
scopes,
|
|
123
|
+
snapshot: { path: docPathFrom(myPath), offset: 0 },
|
|
124
|
+
mutate: ([tableScope, ...rowScopes]) => {
|
|
125
|
+
// Subtree, not children: the cell writes below land at depth two, and a row's
|
|
126
|
+
// cells stay shared with the undo snapshot when only the rows are unshared —
|
|
127
|
+
// the write would go through it and undo would restore the pasted bytes.
|
|
128
|
+
ensureUnsharedSubtree(tableScope.node, tableScope.sharing);
|
|
129
|
+
assertInvariant('column-scope-alignment', () => rowScopes.every((s, i) => s.node === tableScope.node.children?.[rowIndices[i]])
|
|
130
|
+
? null
|
|
131
|
+
: {
|
|
132
|
+
code: 'column-scope-alignment',
|
|
133
|
+
message: 'pasteGrid: row scopes misaligned with owned table children'
|
|
134
|
+
});
|
|
135
|
+
const table = tableScope.node;
|
|
136
|
+
const oldRows = table.children?.length ?? 0;
|
|
137
|
+
const oldCols = metadataOf(table, 'table').columnCount;
|
|
138
|
+
const needRows = origin.rowIdx + rows;
|
|
139
|
+
const needCols = origin.colIdx + cols;
|
|
140
|
+
for (let r = oldRows; r < needRows; r++)
|
|
141
|
+
insertEmptyRow(table, r - 1, 'below');
|
|
142
|
+
for (let c = oldCols; c < needCols; c++)
|
|
143
|
+
insertEmptyColumn(table, c - 1, 'right');
|
|
144
|
+
grid.forEach((line, r) => {
|
|
145
|
+
const cells = table.children[origin.rowIdx + r].children;
|
|
146
|
+
line.forEach((text, c) => writeOwnRaw(cells[origin.colIdx + c], text, deps.grammar));
|
|
147
|
+
});
|
|
148
|
+
rebuildTableRaw(table);
|
|
149
|
+
const addedRows = needRows - oldRows;
|
|
150
|
+
const addedCols = needCols - oldCols;
|
|
151
|
+
const tableChange = addedRows > 0 ? { op: 'insert', at: oldRows, count: addedRows } : { op: 'noop' };
|
|
152
|
+
const rowChange = addedCols > 0 ? { op: 'insert', at: oldCols, count: addedCols } : { op: 'noop' };
|
|
153
|
+
return [tableChange, ...rowScopes.map(() => rowChange)];
|
|
154
|
+
},
|
|
155
|
+
op: {
|
|
156
|
+
kind: 'tablePasteGrid',
|
|
157
|
+
detail: { rowIdx: origin.rowIdx, colIdx: origin.colIdx, rows, cols },
|
|
158
|
+
eventPath: docPathFrom(myPath)
|
|
159
|
+
},
|
|
160
|
+
afterTick: () => focusCell(origin.rowIdx + rows - 1, origin.colIdx + cols - 1, 'end')
|
|
161
|
+
});
|
|
162
|
+
}
|
|
108
163
|
async function insertColumn(colIdx, side) {
|
|
109
164
|
const { focusCell, focusedCell } = deps;
|
|
110
165
|
const insertAt = side === 'left' ? colIdx : colIdx + 1;
|
|
@@ -187,6 +242,7 @@ export function createTableMutationsContext(deps) {
|
|
|
187
242
|
return {
|
|
188
243
|
insertRowAbove: (rowIdx) => insertRow(rowIdx, 'above'),
|
|
189
244
|
insertRowBelow: (rowIdx) => insertRow(rowIdx, 'below'),
|
|
245
|
+
pasteGrid,
|
|
190
246
|
insertColumnLeft: (colIdx) => insertColumn(colIdx, 'left'),
|
|
191
247
|
insertColumnRight: (colIdx) => insertColumn(colIdx, 'right'),
|
|
192
248
|
reorderRowTo,
|
package/dist/editor-events.d.ts
CHANGED
|
@@ -54,6 +54,9 @@ export interface EditorEventMap {
|
|
|
54
54
|
/** The theme name after a `theme` prop change (never fired at mount), for a plugin
|
|
55
55
|
* that paints its own colors and so cannot pick the change up from CSS. */
|
|
56
56
|
themeChange: string;
|
|
57
|
+
/** `true` when an editor-owned menu (right-click, insert `+`) opens, `false` when it closes,
|
|
58
|
+
* so host chrome over the selection can step aside rather than stack on it. */
|
|
59
|
+
menuChange: boolean;
|
|
57
60
|
}
|
|
58
61
|
export interface EditorEvents {
|
|
59
62
|
on<K extends keyof EditorEventMap>(event: K, handler: (payload: EditorEventMap[K]) => void): () => void;
|