@voithos-labs/aragonite 0.10.2 → 0.10.4
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 +19 -0
- package/dist/a11y-strings.js +19 -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 +37 -24
- package/dist/components/BlockHost.svelte +18 -11
- package/dist/components/Editor.svelte +434 -198
- package/dist/components/Editor.svelte.d.ts +1 -1
- package/dist/components/SelectionOverlay.svelte +25 -17
- package/dist/components/SelectionOverlay.svelte.d.ts +1 -1
- 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 +704 -0
- package/dist/components/blocks/code/CodeBlockRail.svelte.d.ts +26 -0
- package/dist/components/blocks/code/code-bootstrap.js +16 -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 +22 -3
- 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 +248 -30
- package/dist/components/blocks/editable-surface.d.ts +9 -0
- package/dist/components/blocks/editable-surface.js +22 -3
- package/dist/components/blocks/list/ListBlock.svelte +1 -0
- package/dist/components/blocks/list/ListItemBlock.svelte +14 -5
- package/dist/components/blocks/list/ListItemBlock.svelte.d.ts +2 -0
- 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/click-snap-guard.d.ts +3 -0
- package/dist/components/blocks/text/click-snap-guard.js +8 -0
- 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 +89 -20
- package/dist/components/blocks/text/live-selection-edit.d.ts +5 -5
- package/dist/components/blocks/text/live-selection-edit.js +37 -7
- package/dist/components/blocks/text/text-clipboard.js +2 -2
- 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 +181 -37
- package/dist/components/drag-handle.d.ts +41 -0
- package/dist/components/drag-handle.js +134 -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 +3 -6
- package/dist/components/editor-root-listeners.js +3 -24
- 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 +67 -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/SelectionToolbar.svelte +385 -0
- package/dist/components/menu/SelectionToolbar.svelte.d.ts +16 -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 +19 -0
- package/dist/core/inline/inline-widgets.js +5 -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/transparency.js +3 -3
- 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/height-oracle.d.ts +2 -3
- package/dist/cursor/height-oracle.js +0 -1
- package/dist/cursor/overlay-remeasure.js +8 -0
- package/dist/cursor/reveal-source.js +7 -2
- package/dist/cursor/scroll-hold.d.ts +10 -0
- package/dist/cursor/scroll-hold.js +22 -0
- package/dist/cursor/scrollport.d.ts +9 -0
- package/dist/cursor/scrollport.js +30 -1
- package/dist/cursor/visual-lines.d.ts +5 -4
- package/dist/cursor/visual-lines.js +56 -11
- package/dist/cursor/widget-edge-snap.d.ts +28 -0
- package/dist/cursor/widget-edge-snap.js +38 -0
- package/dist/cursor/widget-offset.d.ts +9 -0
- package/dist/cursor/widget-offset.js +71 -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/decorations/reserved-attrs.js +1 -0
- 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/focus/focus-dispatch.js +7 -4
- package/dist/editor-actions/focus/focus-landing.d.ts +10 -1
- package/dist/editor-actions/focus/focus-landing.js +20 -6
- package/dist/editor-actions/inline-range-commit.js +1 -1
- package/dist/editor-actions/plugin/container.d.ts +7 -0
- package/dist/editor-actions/plugin/container.js +3 -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 +24 -12
- package/dist/index.d.ts +1 -1
- package/dist/plugin.d.ts +7 -0
- package/dist/plugin.js +16 -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 +60 -10
- 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 +103 -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/plugins/mermaid/MermaidBlock.svelte +19 -9
- package/dist/plugins/parrot/ParrotBlock.svelte +3 -1
- package/dist/reactivity/list-windowing.svelte.d.ts +8 -8
- package/dist/reactivity/list-windowing.svelte.js +75 -30
- package/dist/schema/block-completions.d.ts +8 -0
- package/dist/schema/block-completions.js +11 -0
- package/dist/schema/commands.d.ts +9 -5
- package/dist/schema/commands.js +12 -7
- 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 +37 -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 +3 -29
- package/dist/selection/cross-block/ops.js +1 -1
- package/dist/selection/cross-block/paste.js +31 -35
- package/dist/selection/cross-block/type-replace.d.ts +3 -2
- package/dist/selection/cross-block/type-replace.js +52 -13
- package/dist/selection/dead-space-caret.d.ts +13 -0
- package/dist/selection/dead-space-caret.js +57 -1
- package/dist/selection/drag-pointer.d.ts +26 -2
- package/dist/selection/drag-pointer.js +103 -2
- package/dist/selection/gap-caret.js +1 -1
- package/dist/selection/keyboard-extend.d.ts +3 -2
- package/dist/selection/keyboard-extend.js +4 -3
- package/dist/selection/multi-click.d.ts +42 -0
- package/dist/selection/multi-click.js +203 -0
- package/dist/selection/native-bridge.d.ts +3 -0
- package/dist/selection/native-bridge.js +31 -2
- package/dist/selection/path-lookup.d.ts +10 -2
- package/dist/selection/path-lookup.js +11 -4
- package/dist/selection/pointer-gesture.d.ts +9 -0
- package/dist/selection/pointer-gesture.js +11 -0
- package/dist/selection/primitives.d.ts +7 -0
- package/dist/selection/primitives.js +19 -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.d.ts +4 -0
- package/dist/selection/range-delete.js +31 -3
- package/dist/selection/selection-drop.d.ts +34 -0
- package/dist/selection/selection-drop.js +253 -0
- 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 +75 -31
- package/dist/styles/editor.css +268 -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.d.ts +3 -1
- package/dist/tree-operations/paste/replace-block-at-parent.js +5 -2
- package/dist/tree-operations/paste/replacement-parse.d.ts +17 -0
- package/dist/tree-operations/paste/replacement-parse.js +22 -0
- 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 +136 -104
- package/docs/guide/plugin-api.md +73 -29
- package/docs/guide/plugin-guide.md +55 -23
- package/package.json +9 -7
- 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
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scroll-position hold across a block's render swap. A swap that shortens a block clamps the
|
|
3
|
+
* scrollport against the transient layout on the way to its settled height, and the clamp is
|
|
4
|
+
* never given back. A reveal in flight outranks the hold and owns the position; host mode does
|
|
5
|
+
* not, since native anchoring cannot undo a max-scroll clamp. Only a measure-then-fit surface
|
|
6
|
+
* needs this, and today that is one: an editable leaf's source is content-sized at mount.
|
|
7
|
+
*/
|
|
8
|
+
import { tick } from 'svelte';
|
|
9
|
+
import { userScrollportFor } from './scroll-ancestors';
|
|
10
|
+
import { createScrollport } from './scrollport';
|
|
11
|
+
/** Capture the position of whatever scrolls `el`; await the result AFTER the state flip that
|
|
12
|
+
* changes the block's height. */
|
|
13
|
+
export function captureScrollPosition(el, isRevealInFlight) {
|
|
14
|
+
const port = el ? createScrollport(userScrollportFor(el)) : null;
|
|
15
|
+
const before = port?.scrollTop() ?? 0;
|
|
16
|
+
return async () => {
|
|
17
|
+
await tick();
|
|
18
|
+
if (!port || isRevealInFlight() || port.scrollTop() === before)
|
|
19
|
+
return;
|
|
20
|
+
port.setScrollTop(before);
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -14,7 +14,16 @@ export interface Scrollport {
|
|
|
14
14
|
contentWidth(): number;
|
|
15
15
|
scrollTop(): number;
|
|
16
16
|
setScrollTop(value: number): void;
|
|
17
|
+
/** Move by `delta`. The only relative write: see {@link withRelativeScroll}. */
|
|
18
|
+
scrollBy(delta: number): void;
|
|
17
19
|
/** Fires on user and programmatic scrolls alike; returns the unsubscribe. */
|
|
18
20
|
subscribe(onScroll: () => void): () => void;
|
|
19
21
|
}
|
|
20
22
|
export declare function createScrollport(target: UserScrollport): Scrollport;
|
|
23
|
+
/**
|
|
24
|
+
* Adds the relative write every corrector goes through. A scroller snaps a fractional write to
|
|
25
|
+
* a whole device pixel and reports the snapped value back, so a run of relative corrections (a
|
|
26
|
+
* mode flip fires one per re-measured block) would drop that fraction every time and slide the
|
|
27
|
+
* reader's content by the sum. The refused fraction carries into the next call instead.
|
|
28
|
+
*/
|
|
29
|
+
export declare function withRelativeScroll(base: Omit<Scrollport, 'scrollBy'>): Scrollport;
|
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
export function createScrollport(target) {
|
|
2
|
-
return target === window ? pageScrollport() : elementScrollport(target);
|
|
2
|
+
return withRelativeScroll(target === window ? pageScrollport() : elementScrollport(target));
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Adds the relative write every corrector goes through. A scroller snaps a fractional write to
|
|
6
|
+
* a whole device pixel and reports the snapped value back, so a run of relative corrections (a
|
|
7
|
+
* mode flip fires one per re-measured block) would drop that fraction every time and slide the
|
|
8
|
+
* reader's content by the sum. The refused fraction carries into the next call instead.
|
|
9
|
+
*/
|
|
10
|
+
export function withRelativeScroll(base) {
|
|
11
|
+
let carried = 0;
|
|
12
|
+
let written = null;
|
|
13
|
+
return {
|
|
14
|
+
...base,
|
|
15
|
+
setScrollTop(value) {
|
|
16
|
+
base.setScrollTop(value);
|
|
17
|
+
carried = 0;
|
|
18
|
+
written = null;
|
|
19
|
+
},
|
|
20
|
+
scrollBy(delta) {
|
|
21
|
+
const from = base.scrollTop();
|
|
22
|
+
// Anything that moved the port since our own write (the reader, a reveal) leaves the
|
|
23
|
+
// carried fraction describing a position nobody holds any more.
|
|
24
|
+
const target = from + (written === from ? carried : 0) + delta;
|
|
25
|
+
base.setScrollTop(target);
|
|
26
|
+
written = base.scrollTop();
|
|
27
|
+
const refused = target - written;
|
|
28
|
+
// Only the snap's own fraction carries; a clamp at either end is a real refusal.
|
|
29
|
+
carried = Math.abs(refused) < 1 ? refused : 0;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
3
32
|
}
|
|
4
33
|
// ── Internal ───────────────────────────────────────────────────────────────
|
|
5
34
|
function elementScrollport(el) {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Whether the cursor sits on the first or last visual line of a wrapping element. Offsets
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Whether the cursor sits on the first or last visual line of a wrapping element. Offsets alone
|
|
3
|
+
* can't answer it past 2 wrapped lines, so the cursor's line is compared to the edge line's.
|
|
4
|
+
* Collapsed ranges beside non-text children (dimmed markers, atomic islands) measure to nothing,
|
|
5
|
+
* so the edge line is measured around real text, and a rect-less caret reads the box it sits
|
|
6
|
+
* against.
|
|
6
7
|
*/
|
|
7
8
|
/** The first rect that can position a caret: the leading client rect when it has real
|
|
8
9
|
* height, else the bounding rect. `widthTolerant` also accepts a zero-height rect with width. */
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Whether the cursor sits on the first or last visual line of a wrapping element. Offsets
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Whether the cursor sits on the first or last visual line of a wrapping element. Offsets alone
|
|
3
|
+
* can't answer it past 2 wrapped lines, so the cursor's line is compared to the edge line's.
|
|
4
|
+
* Collapsed ranges beside non-text children (dimmed markers, atomic islands) measure to nothing,
|
|
5
|
+
* so the edge line is measured around real text, and a rect-less caret reads the box it sits
|
|
6
|
+
* against.
|
|
6
7
|
*/
|
|
7
8
|
import { domDescendants } from './dom-walk';
|
|
8
9
|
import { FALLBACK_LINE_HEIGHT } from './typography-estimates';
|
|
@@ -61,6 +62,7 @@ export function findLastTextNode(root) {
|
|
|
61
62
|
export function isAtFirstVisualLine(el, fallbackOffset, contentStart) {
|
|
62
63
|
return isAtEdgeVisualLine(el, () => fallbackOffset <= contentStart, {
|
|
63
64
|
isEmpty: (el.textContent ?? '').length === 0,
|
|
65
|
+
toStart: true,
|
|
64
66
|
boundaryTop: () => {
|
|
65
67
|
const firstText = findFirstTextNode(el);
|
|
66
68
|
const top = firstText ? getCharRangeTop(firstText, 0, false) : null;
|
|
@@ -71,6 +73,7 @@ export function isAtFirstVisualLine(el, fallbackOffset, contentStart) {
|
|
|
71
73
|
export function isAtLastVisualLine(el, fallbackOffset, contentEnd) {
|
|
72
74
|
return isAtEdgeVisualLine(el, () => fallbackOffset >= contentEnd, {
|
|
73
75
|
isEmpty: contentEnd === 0,
|
|
76
|
+
toStart: false,
|
|
74
77
|
boundaryTop: () => {
|
|
75
78
|
const lastText = findLastTextNode(el);
|
|
76
79
|
const top = lastText ? getCharRangeTop(lastText, lastText.textContent.length, true) : null;
|
|
@@ -80,7 +83,7 @@ export function isAtLastVisualLine(el, fallbackOffset, contentEnd) {
|
|
|
80
83
|
}
|
|
81
84
|
// ── Internal ────────────────────────────────────────────────────────────────
|
|
82
85
|
/** The shared skeleton of the two edge predicates: `fallback` answers where geometry cannot — a
|
|
83
|
-
* dropped range,
|
|
86
|
+
* dropped range, a caret no box can be found for, an unmeasurable boundary line — and
|
|
84
87
|
* `boundaryTop` measures the edge line each side's own way. */
|
|
85
88
|
function isAtEdgeVisualLine(el, fallback, edge) {
|
|
86
89
|
const sel = window.getSelection();
|
|
@@ -89,19 +92,61 @@ function isAtEdgeVisualLine(el, fallback, edge) {
|
|
|
89
92
|
if (edge.isEmpty)
|
|
90
93
|
return true;
|
|
91
94
|
const cursorRange = sel.getRangeAt(0);
|
|
95
|
+
const lineHeight = parseFloat(getComputedStyle(el).lineHeight) || FALLBACK_LINE_HEIGHT;
|
|
96
|
+
const tolerance = lineHeight * SAME_LINE_TOLERANCE;
|
|
92
97
|
const cursorTop = getRangeTop(cursorRange);
|
|
93
|
-
if (cursorTop === null)
|
|
94
|
-
|
|
98
|
+
if (cursorTop === null) {
|
|
99
|
+
if (!cursorRange.collapsed)
|
|
100
|
+
return true;
|
|
101
|
+
// A caret beside an atomic island sits at an element-level position and measures to no rect
|
|
102
|
+
// of its own: it rides the island's box, and is at the edge line when nothing reaches past.
|
|
103
|
+
const band = neighbourBand(cursorRange);
|
|
104
|
+
const contents = bandOfRange(contentsRange(el));
|
|
105
|
+
if (!band || !contents)
|
|
106
|
+
return fallback();
|
|
107
|
+
return edge.toStart
|
|
108
|
+
? band.top < contents.top + tolerance
|
|
109
|
+
: band.bottom > contents.bottom - tolerance;
|
|
110
|
+
}
|
|
95
111
|
const edgeTop = edge.boundaryTop();
|
|
96
112
|
if (edgeTop === null)
|
|
97
113
|
return fallback();
|
|
98
|
-
|
|
99
|
-
return Math.abs(cursorTop - edgeTop) < lineHeight * SAME_LINE_TOLERANCE;
|
|
114
|
+
return Math.abs(cursorTop - edgeTop) < tolerance;
|
|
100
115
|
}
|
|
101
|
-
/** The
|
|
102
|
-
|
|
116
|
+
/** The vertical band of the box a rect-less caret sits against: the child it precedes, else the
|
|
117
|
+
* one it follows. Null where the caret is not at an element-level position. */
|
|
118
|
+
function neighbourBand(range) {
|
|
119
|
+
const container = range.startContainer;
|
|
120
|
+
if (container.nodeType !== Node.ELEMENT_NODE)
|
|
121
|
+
return null;
|
|
122
|
+
const children = container.childNodes;
|
|
123
|
+
return (nodeBand(children[range.startOffset], false) ?? nodeBand(children[range.startOffset - 1], true));
|
|
124
|
+
}
|
|
125
|
+
function nodeBand(node, fromEnd) {
|
|
126
|
+
if (!node)
|
|
127
|
+
return null;
|
|
128
|
+
const range = document.createRange();
|
|
129
|
+
range.selectNode(node);
|
|
130
|
+
// A node that wraps has one rect per line, and the caret touches the line on its own side.
|
|
131
|
+
const rects = range.getClientRects();
|
|
132
|
+
if (rects.length === 0)
|
|
133
|
+
return bandOfRange(range);
|
|
134
|
+
return bandOf(rects[fromEnd ? rects.length - 1 : 0]);
|
|
135
|
+
}
|
|
136
|
+
function contentsRange(el) {
|
|
103
137
|
const range = document.createRange();
|
|
104
138
|
range.selectNodeContents(el);
|
|
139
|
+
return range;
|
|
140
|
+
}
|
|
141
|
+
function bandOfRange(range) {
|
|
142
|
+
return bandOf(range.getBoundingClientRect());
|
|
143
|
+
}
|
|
144
|
+
function bandOf(rect) {
|
|
145
|
+
return rect.height > 0 ? { top: rect.top, bottom: rect.bottom } : null;
|
|
146
|
+
}
|
|
147
|
+
/** The boundary line's collapsed-contents fallback measurement. */
|
|
148
|
+
function collapsedContentsTop(el, toStart) {
|
|
149
|
+
const range = contentsRange(el);
|
|
105
150
|
range.collapse(toStart);
|
|
106
151
|
return getRangeTop(range);
|
|
107
152
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which atomic island's raw edge a point lands on: the NEAREST edge among the islands the caller
|
|
3
|
+
* measures, so a run of flush islands answers the one the point is actually beside. A point inside
|
|
4
|
+
* one reads its kind: a character-like island names the edge on the point's side, while an island
|
|
5
|
+
* that selects whole declines and keeps its own click handling.
|
|
6
|
+
*/
|
|
7
|
+
export interface WidgetEdgeCandidate {
|
|
8
|
+
/** The island's own raw source span; the caret seats at one end or the other. */
|
|
9
|
+
start: number;
|
|
10
|
+
end: number;
|
|
11
|
+
rect: Pick<DOMRect, 'left' | 'right' | 'top' | 'bottom'>;
|
|
12
|
+
/** The kind reads as one character, so a press ON it names an edge by side rather than
|
|
13
|
+
* selecting the island whole. */
|
|
14
|
+
seatsInside: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface WidgetEdgeSeat {
|
|
17
|
+
offset: number;
|
|
18
|
+
/** The press landed ON the island: the engine answers such a point with a position in the
|
|
19
|
+
* neighbouring text, so its own caret is no reason to stand this seat down. */
|
|
20
|
+
inside: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The raw offset a point snaps to, in document order for ties. Null where the point snaps to none:
|
|
24
|
+
* inside an island that selects whole, whose own click handling owns it, or with no island to
|
|
25
|
+
* either side. A null `y` is a point with no line to compare against, so vertical distance drops
|
|
26
|
+
* out and horizontal containment alone reads as inside.
|
|
27
|
+
*/
|
|
28
|
+
export declare function nearestWidgetEdgeSeat(candidates: Iterable<WidgetEdgeCandidate>, x: number, y: number | null): WidgetEdgeSeat | null;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which atomic island's raw edge a point lands on: the NEAREST edge among the islands the caller
|
|
3
|
+
* measures, so a run of flush islands answers the one the point is actually beside. A point inside
|
|
4
|
+
* one reads its kind: a character-like island names the edge on the point's side, while an island
|
|
5
|
+
* that selects whole declines and keeps its own click handling.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* The raw offset a point snaps to, in document order for ties. Null where the point snaps to none:
|
|
9
|
+
* inside an island that selects whole, whose own click handling owns it, or with no island to
|
|
10
|
+
* either side. A null `y` is a point with no line to compare against, so vertical distance drops
|
|
11
|
+
* out and horizontal containment alone reads as inside.
|
|
12
|
+
*/
|
|
13
|
+
export function nearestWidgetEdgeSeat(candidates, x, y) {
|
|
14
|
+
let best = null;
|
|
15
|
+
for (const { start, end, rect, seatsInside } of candidates) {
|
|
16
|
+
const withinRow = y === null || (rect.top < y && y < rect.bottom);
|
|
17
|
+
if (rect.left < x && x < rect.right && withinRow) {
|
|
18
|
+
if (!seatsInside)
|
|
19
|
+
return null;
|
|
20
|
+
return { offset: x < (rect.left + rect.right) / 2 ? start : end, inside: true };
|
|
21
|
+
}
|
|
22
|
+
// Rows before columns: a point past the end of one line must not reach an island on
|
|
23
|
+
// another line that happens to sit at the same x.
|
|
24
|
+
const rowGap = y === null ? 0 : Math.max(rect.top - y, y - rect.bottom, 0);
|
|
25
|
+
if (x <= rect.left)
|
|
26
|
+
best = nearer(best, { offset: start, rowGap, gap: rect.left - x });
|
|
27
|
+
if (x >= rect.right)
|
|
28
|
+
best = nearer(best, { offset: end, rowGap, gap: x - rect.right });
|
|
29
|
+
}
|
|
30
|
+
return best === null ? null : { offset: best.offset, inside: false };
|
|
31
|
+
}
|
|
32
|
+
function nearer(held, next) {
|
|
33
|
+
if (!held)
|
|
34
|
+
return next;
|
|
35
|
+
if (next.rowGap !== held.rowGap)
|
|
36
|
+
return next.rowGap < held.rowGap ? next : held;
|
|
37
|
+
return next.gap < held.gap ? next : held;
|
|
38
|
+
}
|
|
@@ -42,6 +42,9 @@ export declare function findDomTextOffsetTarget(container: ParentNode, target: D
|
|
|
42
42
|
* against the ambient-adjusted argument.
|
|
43
43
|
*/
|
|
44
44
|
export declare function widgetsIntersectingRange(container: HTMLElement, start: DomTextOffset, end: DomTextOffset): HTMLElement[];
|
|
45
|
+
/** The walk's text with marker chrome and widget bytes blanked to spaces, at the walk's own
|
|
46
|
+
* offsets: what a rule reading words (the multi-click ladder) sees as content. */
|
|
47
|
+
export declare function maskedWalkText(container: ParentNode): string;
|
|
45
48
|
/** Total walk length of `container` — its one-past-end walk position. */
|
|
46
49
|
export declare function containerDomTextLength(container: ParentNode): DomTextOffset;
|
|
47
50
|
/**
|
|
@@ -101,6 +104,12 @@ export declare function landableDomTextBounds(container: ParentNode): {
|
|
|
101
104
|
* questions: a reference label is chrome that stays hidden, so it is not content standing behind
|
|
102
105
|
* the stamp, and it is not the paint the stamp promises either.
|
|
103
106
|
*/
|
|
107
|
+
/**
|
|
108
|
+
* The container's bytes outside its marker chrome — what a painted source holds as CONTENT,
|
|
109
|
+
* whatever the mode paints. A fence with an empty body answers whitespace; the leaf reads that
|
|
110
|
+
* as "nothing here a Backspace could mean but the block".
|
|
111
|
+
*/
|
|
112
|
+
export declare function chromeFreeText(container: ParentNode): string;
|
|
104
113
|
export declare function holdsOnlyMarkerChrome(container: ParentNode): boolean;
|
|
105
114
|
/**
|
|
106
115
|
* 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;
|
|
@@ -108,6 +111,16 @@ export function widgetsIntersectingRange(container, start, end) {
|
|
|
108
111
|
}
|
|
109
112
|
return out;
|
|
110
113
|
}
|
|
114
|
+
/** The walk's text with marker chrome and widget bytes blanked to spaces, at the walk's own
|
|
115
|
+
* offsets: what a rule reading words (the multi-click ladder) sees as content. */
|
|
116
|
+
export function maskedWalkText(container) {
|
|
117
|
+
let out = '';
|
|
118
|
+
for (const seg of walkSegments(container, null)) {
|
|
119
|
+
const content = seg.kind === 'text' && markerRootOf(seg.node, container) === null;
|
|
120
|
+
out += content ? (seg.node.textContent ?? '') : ' '.repeat(seg.len);
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
111
124
|
/** Total walk length of `container` — its one-past-end walk position. */
|
|
112
125
|
export function containerDomTextLength(container) {
|
|
113
126
|
let count = 0;
|
|
@@ -239,7 +252,19 @@ export function revealsNoMarkers(container) {
|
|
|
239
252
|
* An unmounted surface reads as source, where nothing hides and no rewrite has a run to move.
|
|
240
253
|
*/
|
|
241
254
|
export function screenVisibilityOf(container) {
|
|
242
|
-
return screenVisibility(container === null ? 'source' : (markerHidingMode(container) ?? 'source'), { chromePaints: container instanceof Element && container
|
|
255
|
+
return screenVisibility(container === null ? 'source' : (markerHidingMode(container) ?? 'source'), { chromePaints: container instanceof Element && chromeStampPaints(container) });
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* The stamp's own condition, as `styles/editor.css` states it: the container carries
|
|
259
|
+
* `data-content-empty` AND holds focus. The seat is for a caret, so a stamped block nobody is in
|
|
260
|
+
* keeps its chrome hidden and reads as the empty construct it is. Focus containment rather than
|
|
261
|
+
* `:focus-within`, so a selector engine without that pseudo-class (jsdom's) reads the same answer.
|
|
262
|
+
*/
|
|
263
|
+
function chromeStampPaints(container) {
|
|
264
|
+
if (!container.hasAttribute(CONTENT_EMPTY_ATTR))
|
|
265
|
+
return false;
|
|
266
|
+
const active = container.ownerDocument.activeElement;
|
|
267
|
+
return active !== null && container.contains(active);
|
|
243
268
|
}
|
|
244
269
|
/**
|
|
245
270
|
* The extreme walk offsets a caret can occupy in `container`: a hidden marker run or the leading
|
|
@@ -252,6 +277,7 @@ export function landableDomTextBounds(container) {
|
|
|
252
277
|
let start = 0;
|
|
253
278
|
let end = 0;
|
|
254
279
|
let landed = false;
|
|
280
|
+
let lastText = null;
|
|
255
281
|
for (const seg of landingSegments(container, markerHidingMode(container))) {
|
|
256
282
|
if (seg.len === 0)
|
|
257
283
|
continue;
|
|
@@ -265,9 +291,28 @@ export function landableDomTextBounds(container) {
|
|
|
265
291
|
}
|
|
266
292
|
landed = true;
|
|
267
293
|
end = stop;
|
|
294
|
+
lastText = seg.kind === 'text' ? seg.node : null;
|
|
295
|
+
}
|
|
296
|
+
// The position after the last landable run's final `\n` is the start of a line nothing
|
|
297
|
+
// paints: the engine seats no caret there, so an end gate reading it never fires and the
|
|
298
|
+
// caret is stuck on the empty line. A caret anchor gives that line its paint and keeps it.
|
|
299
|
+
if (lastText?.textContent?.endsWith('\n') && !followedByCaretAnchor(lastText, container)) {
|
|
300
|
+
end -= 1;
|
|
268
301
|
}
|
|
269
302
|
return { start: asDomTextOffset(start), end: asDomTextOffset(Math.max(start, end)) };
|
|
270
303
|
}
|
|
304
|
+
/** Whether the next painted node after `text`, at any depth above it, is a caret anchor. */
|
|
305
|
+
function followedByCaretAnchor(text, root) {
|
|
306
|
+
for (let node = text; node && node !== root; node = node.parentNode) {
|
|
307
|
+
let next = node.nextSibling;
|
|
308
|
+
while (next?.nodeType === Node.TEXT_NODE && (next.textContent?.length ?? 0) === 0) {
|
|
309
|
+
next = next.nextSibling;
|
|
310
|
+
}
|
|
311
|
+
if (next)
|
|
312
|
+
return next instanceof HTMLElement && next.tagName === 'BR' && 'caretAnchor' in next.dataset;
|
|
313
|
+
}
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
271
316
|
/**
|
|
272
317
|
* Whether every byte `container` holds is marker chrome, at least one span of it a family the
|
|
273
318
|
* empty-construct override paints. The stamp condition the render path writes as
|
|
@@ -276,6 +321,22 @@ export function landableDomTextBounds(container) {
|
|
|
276
321
|
* questions: a reference label is chrome that stays hidden, so it is not content standing behind
|
|
277
322
|
* the stamp, and it is not the paint the stamp promises either.
|
|
278
323
|
*/
|
|
324
|
+
/**
|
|
325
|
+
* The container's bytes outside its marker chrome — what a painted source holds as CONTENT,
|
|
326
|
+
* whatever the mode paints. A fence with an empty body answers whitespace; the leaf reads that
|
|
327
|
+
* as "nothing here a Backspace could mean but the block".
|
|
328
|
+
*/
|
|
329
|
+
export function chromeFreeText(container) {
|
|
330
|
+
let text = '';
|
|
331
|
+
for (const seg of walkSegments(container, null)) {
|
|
332
|
+
if (seg.kind !== 'text' || seg.len === 0)
|
|
333
|
+
continue;
|
|
334
|
+
if (markerRootOf(seg.node, container) !== null)
|
|
335
|
+
continue;
|
|
336
|
+
text += seg.node.textContent ?? '';
|
|
337
|
+
}
|
|
338
|
+
return text;
|
|
339
|
+
}
|
|
279
340
|
export function holdsOnlyMarkerChrome(container) {
|
|
280
341
|
let chrome = false;
|
|
281
342
|
for (const seg of walkSegments(container, null)) {
|
|
@@ -383,8 +444,7 @@ function markerRootOf(node, root) {
|
|
|
383
444
|
function chromeStandsAloneUnder(root, mode) {
|
|
384
445
|
if (mode === null || !(root instanceof Element))
|
|
385
446
|
return false;
|
|
386
|
-
return screenVisibility(mode, { chromePaints: root
|
|
387
|
-
.chromePaints;
|
|
447
|
+
return screenVisibility(mode, { chromePaints: chromeStampPaints(root) }).chromePaints;
|
|
388
448
|
}
|
|
389
449
|
/**
|
|
390
450
|
* Whether `node` is inside the leading ambient marker island — a container's `- ` / `1. ` prefix,
|
|
@@ -442,8 +502,15 @@ function positionBeside(el, side) {
|
|
|
442
502
|
return { node: sibling, offset: side === 'before' ? (sibling.textContent?.length ?? 0) : 0 };
|
|
443
503
|
}
|
|
444
504
|
const idx = Array.prototype.indexOf.call(parent.childNodes, el);
|
|
505
|
+
// A pending hard break's first anchor (`inline-render.ts`) ends the line the marker sat on;
|
|
506
|
+
// the seat past the marker is the start of the next one, which is after that anchor.
|
|
507
|
+
if (side === 'after' && isBreakAnchor(sibling))
|
|
508
|
+
return { node: parent, offset: idx + 2 };
|
|
445
509
|
return { node: parent, offset: side === 'before' ? idx : idx + 1 };
|
|
446
510
|
}
|
|
511
|
+
function isBreakAnchor(node) {
|
|
512
|
+
return (node instanceof HTMLElement && node.tagName === 'BR' && node.dataset.caretAnchor === 'break');
|
|
513
|
+
}
|
|
447
514
|
/**
|
|
448
515
|
* The classification every walk-space consumer shares: a text node contributes its textContent
|
|
449
516
|
* 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
|