@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
|
@@ -33,19 +33,21 @@
|
|
|
33
33
|
} from '../../../schema/inline-construct-policy';
|
|
34
34
|
import {
|
|
35
35
|
cycleHeading,
|
|
36
|
+
demoteEmptyAtxHeading,
|
|
36
37
|
demoteToParagraph,
|
|
37
38
|
insertHardBreak,
|
|
38
39
|
insertLiteralTab,
|
|
39
40
|
type TextEditResult
|
|
40
41
|
} from './text-keydown';
|
|
41
42
|
import { tryGetBlockKindDescriptor } from '../../../schema/block-kind-descriptor';
|
|
42
|
-
import { blockNodeAt } from '../../../tree-operations/node-
|
|
43
|
+
import { blockNodeAt } from '../../../tree-operations/node-primitives';
|
|
43
44
|
import { createTextClipboard } from './text-clipboard';
|
|
44
45
|
import { createTextRender } from './text-render';
|
|
45
46
|
import { createWidgetInteraction } from './widget-interaction';
|
|
46
|
-
import { createEdgePolicyDispatch } from './edge-policy-dispatch';
|
|
47
|
+
import { createEdgePolicyDispatch, keepsBlockKind } from './edge-policy-dispatch';
|
|
47
48
|
import { hidesStructuralSuffix } from './hidden-suffix';
|
|
48
49
|
import { applyLiveRangeEdit, resolveSelectionEdit } from './live-selection-edit';
|
|
50
|
+
import { applyDelimiterAutoPair } from './delimiter-autopair';
|
|
49
51
|
import { createCompositionSeat } from './composition-seat';
|
|
50
52
|
import { createConstructReveal } from './construct-reveal';
|
|
51
53
|
import { assertInvariant } from '../../../assert';
|
|
@@ -56,7 +58,11 @@
|
|
|
56
58
|
handleSharedKeydown,
|
|
57
59
|
handleSharedBeforeInput
|
|
58
60
|
} from '../../../selection/shared-keydown';
|
|
59
|
-
import {
|
|
61
|
+
import {
|
|
62
|
+
createEditableSurface,
|
|
63
|
+
consumePendingRestore,
|
|
64
|
+
withKeydownVerdict
|
|
65
|
+
} from '../editable-surface';
|
|
60
66
|
import { wireSurfaceContexts, useParkFocusOnUnmount } from '../surface-wiring.svelte';
|
|
61
67
|
import {
|
|
62
68
|
domTextOffsetAtNode,
|
|
@@ -76,6 +82,7 @@
|
|
|
76
82
|
import { createAmbientCursorIO } from '../../../ambient/ambient-cursor';
|
|
77
83
|
import { type CommandId } from '../../../schema/commands';
|
|
78
84
|
import { reorderRunCommand } from '../../../editor-actions/reorder-action';
|
|
85
|
+
import { planTypedCompletion } from '../../../editor-actions/enter-completion';
|
|
79
86
|
import {
|
|
80
87
|
perfEnabled,
|
|
81
88
|
recordBlockRender,
|
|
@@ -111,6 +118,12 @@
|
|
|
111
118
|
const ambientPrefixText = $derived(
|
|
112
119
|
typeof ambientPrefix === 'string' ? ambientPrefix : ambientPrefix.text
|
|
113
120
|
);
|
|
121
|
+
// The hanging indent is the prefix's painted width: its text width by default, or what a
|
|
122
|
+
// prefix painting itself as chrome (the task box) declares.
|
|
123
|
+
const ambientIndent = $derived(
|
|
124
|
+
(typeof ambientPrefix === 'string' ? undefined : ambientPrefix.indent) ??
|
|
125
|
+
`${ambientPrefixText.length}ch`
|
|
126
|
+
);
|
|
114
127
|
|
|
115
128
|
const wiring = wireSurfaceContexts();
|
|
116
129
|
const {
|
|
@@ -361,6 +374,7 @@
|
|
|
361
374
|
widgetInteraction.enterWidget(widget, fromTrailingEdge),
|
|
362
375
|
isReading: () => readOnly,
|
|
363
376
|
getEdgeAffinity: () => edgeAffinity.get(),
|
|
377
|
+
noteOutside: edgeAffinity.noteExtreme,
|
|
364
378
|
pendingMarks,
|
|
365
379
|
installedAs: 'block'
|
|
366
380
|
});
|
|
@@ -668,9 +682,9 @@
|
|
|
668
682
|
// Only while this block still owns focus: a blur-commit also arms a pending
|
|
669
683
|
// offset, and restoring would yank the selection back into the blurred block.
|
|
670
684
|
// The clear runs regardless, so a skipped restore is dropped, never re-armed.
|
|
671
|
-
const applied = consumePendingRestore(el ?? null, pendingCursorOffset, (offset) =>
|
|
672
|
-
cursor.setRaw(asRawOffset(offset))
|
|
673
|
-
);
|
|
685
|
+
const applied = consumePendingRestore(el ?? null, pendingCursorOffset, (offset) => {
|
|
686
|
+
if (!widgetInteraction.revealInterior(offset)) cursor.setRaw(asRawOffset(offset));
|
|
687
|
+
});
|
|
674
688
|
tracePendingCursorConsume(pendingCursorOffset, applied);
|
|
675
689
|
pendingCursorOffset = null;
|
|
676
690
|
}
|
|
@@ -815,6 +829,8 @@
|
|
|
815
829
|
if (wiring.dispatchChord(e, { kind: node.kind, runCommand })) return;
|
|
816
830
|
}
|
|
817
831
|
|
|
832
|
+
const onKeyDownTraced = withKeydownVerdict(onKeyDown);
|
|
833
|
+
|
|
818
834
|
/**
|
|
819
835
|
* A native ranged edit inside ONE block, in a mode that paints no delimiter: the engine would
|
|
820
836
|
* write the runs the range crossed literally, so the edit goes through the join seam instead.
|
|
@@ -837,9 +853,33 @@
|
|
|
837
853
|
);
|
|
838
854
|
}
|
|
839
855
|
|
|
856
|
+
// The write takes the ranged edit's road above, so the surface repaints once with the caret
|
|
857
|
+
// inside the pair. During a reveal the caret indexes the DOM text, which has outrun `node.raw`.
|
|
858
|
+
function handleDelimiterAutoPair(e: InputEvent): boolean {
|
|
859
|
+
return applyDelimiterAutoPair(e, {
|
|
860
|
+
text: () => (widgetInteraction.isRevealing() ? readRawText() : getDisplayText()),
|
|
861
|
+
content: () => getContentRange(node),
|
|
862
|
+
caret: () => cursor.getRaw(),
|
|
863
|
+
hasSelection: () => cursor.getRawSelection() !== null,
|
|
864
|
+
isRevealing: widgetInteraction.isRevealing,
|
|
865
|
+
foldReveal: () => widgetInteraction.foldRevealBeforeMutation(),
|
|
866
|
+
markersPaint: () => paintsFocusedMarkers(presentationMode),
|
|
867
|
+
setCaret: (offset) => cursor.setRaw(asRawOffset(offset)),
|
|
868
|
+
seatOutside: edgeAffinity.noteExtreme,
|
|
869
|
+
completesLine: (caret) => planTypedCompletion(node, caret) !== null,
|
|
870
|
+
keepsBlockKind: (text) => keepsBlockKind(node, text),
|
|
871
|
+
write: (text, caretBefore, caretAfter) => {
|
|
872
|
+
const raw = text + trailingLineEnding(node.raw);
|
|
873
|
+
void blockEdit.updateBlockContent(index, raw, caretBefore, caretAfter);
|
|
874
|
+
setPendingCursorOffset(caretAfter, 'delimiter-autopair');
|
|
875
|
+
}
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
|
|
840
879
|
async function onBeforeInput(e: InputEvent): Promise<void> {
|
|
841
880
|
if (await handleSharedBeforeInput(e, sharedCtx)) return;
|
|
842
881
|
if (handleLiveSelectionEdit(e)) return;
|
|
882
|
+
if (handleDelimiterAutoPair(e)) return;
|
|
843
883
|
// Soft-keyboard/IME insertLineBreak slipped past onKeyDown — swallow; Shift+Enter there owns hard breaks.
|
|
844
884
|
if (e.inputType === 'insertLineBreak') {
|
|
845
885
|
e.preventDefault();
|
|
@@ -868,6 +908,13 @@
|
|
|
868
908
|
// Persist a revealed source edit before the caret is gone.
|
|
869
909
|
widgetInteraction.commitRevealOnBlur();
|
|
870
910
|
lastSnapTargetOffset = null;
|
|
911
|
+
demoteEmptyHeadingOnBlur();
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function demoteEmptyHeadingOnBlur(): void {
|
|
915
|
+
if (readOnly || node.kind !== 'heading' || editableSurface.isDetached()) return;
|
|
916
|
+
const demoted = demoteEmptyAtxHeading(node.raw, getContentRange(node));
|
|
917
|
+
if (demoted) void blockEdit.updateBlockContent(index, demoted.newRaw, 0);
|
|
871
918
|
}
|
|
872
919
|
|
|
873
920
|
function onClick(e: MouseEvent): void {
|
|
@@ -933,10 +980,10 @@
|
|
|
933
980
|
contenteditable={readOnly ? 'false' : 'true'}
|
|
934
981
|
aria-readonly={readOnly ? 'true' : undefined}
|
|
935
982
|
role="textbox"
|
|
936
|
-
style:text-indent={ambientPrefixText ? `-${
|
|
937
|
-
style:padding-left={ambientPrefixText ?
|
|
983
|
+
style:text-indent={ambientPrefixText ? `-${ambientIndent}` : null}
|
|
984
|
+
style:padding-left={ambientPrefixText ? ambientIndent : null}
|
|
938
985
|
oninput={onInput}
|
|
939
|
-
onkeydown={
|
|
986
|
+
onkeydown={onKeyDownTraced}
|
|
940
987
|
onbeforeinput={onBeforeInput}
|
|
941
988
|
oncopy={clipboardHandlers.onCopy}
|
|
942
989
|
oncut={clipboardHandlers.onCut}
|
|
@@ -986,7 +1033,7 @@
|
|
|
986
1033
|
}
|
|
987
1034
|
|
|
988
1035
|
.text-editable-block.raw-block {
|
|
989
|
-
font-family: var(--font-
|
|
1036
|
+
font-family: var(--font-code, ui-monospace, monospace);
|
|
990
1037
|
font-size: 0.9em;
|
|
991
1038
|
opacity: 0.85;
|
|
992
1039
|
}
|
|
@@ -998,7 +1045,7 @@
|
|
|
998
1045
|
}
|
|
999
1046
|
|
|
1000
1047
|
.text-editable-block :global(.inline-code-content) {
|
|
1001
|
-
font-family: var(--font-
|
|
1048
|
+
font-family: var(--font-code, ui-monospace, monospace);
|
|
1002
1049
|
font-size: 0.9em;
|
|
1003
1050
|
background: var(--color-bg-secondary, rgba(128, 128, 128, 0.12));
|
|
1004
1051
|
border-radius: 3px;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A typed inline delimiter closes itself: a lone `$`, backtick, `*`, `_` or `~~` would pair with
|
|
3
|
+
* whatever matching run comes later on the line, so the keystroke lands its twin after the caret.
|
|
4
|
+
* Typing the closer over the twin steps past it, a closer typed by hand seats the next byte
|
|
5
|
+
* outside, and the empty pair drops its twin when the first body byte makes it no construct.
|
|
6
|
+
* `applyDelimiterAutoPair` is the one `beforeinput` arm every prose surface runs (G4.65).
|
|
7
|
+
*/
|
|
8
|
+
import { type ContentRange } from '../../../core/inline';
|
|
9
|
+
export type AutoPairEdit = {
|
|
10
|
+
kind: 'write';
|
|
11
|
+
text: string;
|
|
12
|
+
caret: number;
|
|
13
|
+
}
|
|
14
|
+
/** The typed byte completed a construct's closer; what follows belongs outside it. */
|
|
15
|
+
| {
|
|
16
|
+
kind: 'close';
|
|
17
|
+
text: string;
|
|
18
|
+
caret: number;
|
|
19
|
+
}
|
|
20
|
+
/** Nothing written: the caret passes the twin. Over a construct's closer that run may be
|
|
21
|
+
* unpainted, and then only the SIDE moves, which the surface records as an edge affinity. */
|
|
22
|
+
| {
|
|
23
|
+
kind: 'step-over';
|
|
24
|
+
caret: number;
|
|
25
|
+
overConstruct: boolean;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* What a single typed byte does at a collapsed caret, or null to leave the engine its insert.
|
|
29
|
+
* `content` bounds the inline scan (a heading's `# ` is not prose); the caret must lie inside it.
|
|
30
|
+
* `keepsBlockKind` says whether a written line still reloads as this block: a grown `****` is a
|
|
31
|
+
* thematic break and `~~~~` a fence, so such a pair steps past a twin instead, or declines.
|
|
32
|
+
*/
|
|
33
|
+
export declare function resolveDelimiterAutoPair(text: string, content: ContentRange, caret: number, typed: string, keepsBlockKind?: (text: string) => boolean): AutoPairEdit | null;
|
|
34
|
+
/**
|
|
35
|
+
* Inside a revealed source (`$ab|$`) the twin is the live closer, and typing it means "done":
|
|
36
|
+
* the caret steps past it and the caller folds the reveal, so the formula renders at once.
|
|
37
|
+
*/
|
|
38
|
+
export declare function stepsOverRevealedCloser(text: string, caret: number, typed: string): boolean;
|
|
39
|
+
/** Backspace at an empty pair takes both runs, between them (`**|**`) or right after them
|
|
40
|
+
* (`$$|`), as it does in any IDE. Exactly a pair of equal runs, one or two bytes each: a longer
|
|
41
|
+
* run is a fence or a literal the user built by hand. */
|
|
42
|
+
export declare function resolveEmptyPairBackspace(text: string, caret: number): AutoPairEdit | null;
|
|
43
|
+
/** What a prose surface lends the arm. Reactive reads are thunks, so nothing here goes stale. */
|
|
44
|
+
export interface AutoPairSurface {
|
|
45
|
+
/** The bytes the caret indexes: the display text, or the revealed DOM text during a reveal. */
|
|
46
|
+
text(): string;
|
|
47
|
+
content(): ContentRange;
|
|
48
|
+
caret(): number | null;
|
|
49
|
+
hasSelection(): boolean;
|
|
50
|
+
isRevealing(): boolean;
|
|
51
|
+
foldReveal(): {
|
|
52
|
+
settled: Promise<void>;
|
|
53
|
+
} | null;
|
|
54
|
+
/** Whether the closer a step-over passes is on screen; unpainted, only the side moves. */
|
|
55
|
+
markersPaint(): boolean;
|
|
56
|
+
setCaret(offset: number): void;
|
|
57
|
+
/** The arrival the seat reads next: past the construct's delimiters. */
|
|
58
|
+
seatOutside(): void;
|
|
59
|
+
/** One CST write plus the caret it parks. */
|
|
60
|
+
write(text: string, caretBefore: number, caretAfter: number): void;
|
|
61
|
+
/** A step-over that leaves the line one an on-type completer claims (`$$`), which only a
|
|
62
|
+
* content write consults. */
|
|
63
|
+
completesLine?(caret: number): boolean;
|
|
64
|
+
/** The resolver's block-kind guard, for a surface whose line can become another block. */
|
|
65
|
+
keepsBlockKind?(text: string): boolean;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The `beforeinput` arm. True when the press was this seam's: the event is cancelled and the
|
|
69
|
+
* surface has written, moved or re-seated the caret.
|
|
70
|
+
*/
|
|
71
|
+
export declare function applyDelimiterAutoPair(e: InputEvent, surface: AutoPairSurface): boolean;
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A typed inline delimiter closes itself: a lone `$`, backtick, `*`, `_` or `~~` would pair with
|
|
3
|
+
* whatever matching run comes later on the line, so the keystroke lands its twin after the caret.
|
|
4
|
+
* Typing the closer over the twin steps past it, a closer typed by hand seats the next byte
|
|
5
|
+
* outside, and the empty pair drops its twin when the first body byte makes it no construct.
|
|
6
|
+
* `applyDelimiterAutoPair` is the one `beforeinput` arm every prose surface runs (G4.65).
|
|
7
|
+
*/
|
|
8
|
+
import { constructContentRange, inlineDescendants, parseInline } from '../../../core/inline';
|
|
9
|
+
import { isAutoPairTrigger } from '../../../core/inline/scan/plugin-syntax';
|
|
10
|
+
const STEP = { single: true, inside: 'step-over' };
|
|
11
|
+
const BUILTIN = {
|
|
12
|
+
'`': STEP,
|
|
13
|
+
'*': { single: true, inside: 'grow', notAfterWord: true },
|
|
14
|
+
_: { single: true, inside: 'grow', notAfterWord: true },
|
|
15
|
+
// `~5 minutes` would strike the 5 (GFM takes a single tilde), so only `~~` pairs.
|
|
16
|
+
'~': { single: false, inside: 'grow' }
|
|
17
|
+
};
|
|
18
|
+
const isWordByte = (ch) => ch !== undefined && /[\p{L}\p{N}]/u.test(ch);
|
|
19
|
+
function policyOf(ch) {
|
|
20
|
+
return BUILTIN[ch] ?? (isAutoPairTrigger(ch) ? STEP : null);
|
|
21
|
+
}
|
|
22
|
+
// ── The resolver ─────────────────────────────────────────────────────────────
|
|
23
|
+
/**
|
|
24
|
+
* What a single typed byte does at a collapsed caret, or null to leave the engine its insert.
|
|
25
|
+
* `content` bounds the inline scan (a heading's `# ` is not prose); the caret must lie inside it.
|
|
26
|
+
* `keepsBlockKind` says whether a written line still reloads as this block: a grown `****` is a
|
|
27
|
+
* thematic break and `~~~~` a fence, so such a pair steps past a twin instead, or declines.
|
|
28
|
+
*/
|
|
29
|
+
export function resolveDelimiterAutoPair(text, content, caret, typed, keepsBlockKind = () => true) {
|
|
30
|
+
if (typed.length !== 1 || caret < content.start || caret > content.end)
|
|
31
|
+
return null;
|
|
32
|
+
const before = text[caret - 1];
|
|
33
|
+
const after = text[caret];
|
|
34
|
+
const policy = policyOf(typed);
|
|
35
|
+
if (!policy)
|
|
36
|
+
return collapseEmptyPair(text, content, caret, typed);
|
|
37
|
+
if (after === typed && closingRunAt(text, content, caret, typed)) {
|
|
38
|
+
return { kind: 'step-over', caret: caret + 1, overConstruct: true };
|
|
39
|
+
}
|
|
40
|
+
const paired = text.slice(0, caret) + typed + text.slice(caret);
|
|
41
|
+
if (closerEndsAt(paired, content, caret + 1, typed)) {
|
|
42
|
+
return { kind: 'close', text: paired, caret: caret + 1 };
|
|
43
|
+
}
|
|
44
|
+
const pair = (next) => {
|
|
45
|
+
if (keepsBlockKind(next))
|
|
46
|
+
return write(next, caret + 1);
|
|
47
|
+
return after === typed ? { kind: 'step-over', caret: caret + 1, overConstruct: false } : null;
|
|
48
|
+
};
|
|
49
|
+
if (after === typed) {
|
|
50
|
+
if (before === typed) {
|
|
51
|
+
if (policy.inside === 'step-over') {
|
|
52
|
+
return { kind: 'step-over', caret: caret + 1, overConstruct: false };
|
|
53
|
+
}
|
|
54
|
+
return pair(text.slice(0, caret) + typed + typed + text.slice(caret));
|
|
55
|
+
}
|
|
56
|
+
// A byte in front of another run's opener is spelling something out, not opening a span.
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
if (before === typed && !closerEndsAt(text, content, caret, typed)) {
|
|
60
|
+
// `~|` plus `~`: the double run this delimiter pairs on, as long as it IS a lone run.
|
|
61
|
+
const single = text[caret - 2] !== typed;
|
|
62
|
+
if (policy.inside === 'grow' && single) {
|
|
63
|
+
return pair(text.slice(0, caret) + typed.repeat(3) + text.slice(caret));
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
if (!policy.single || (policy.notAfterWord && isWordByte(before)))
|
|
68
|
+
return null;
|
|
69
|
+
return pair(text.slice(0, caret) + typed + typed + text.slice(caret));
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Inside a revealed source (`$ab|$`) the twin is the live closer, and typing it means "done":
|
|
73
|
+
* the caret steps past it and the caller folds the reveal, so the formula renders at once.
|
|
74
|
+
*/
|
|
75
|
+
export function stepsOverRevealedCloser(text, caret, typed) {
|
|
76
|
+
return typed.length === 1 && policyOf(typed) !== null && text[caret] === typed;
|
|
77
|
+
}
|
|
78
|
+
/** Backspace at an empty pair takes both runs, between them (`**|**`) or right after them
|
|
79
|
+
* (`$$|`), as it does in any IDE. Exactly a pair of equal runs, one or two bytes each: a longer
|
|
80
|
+
* run is a fence or a literal the user built by hand. */
|
|
81
|
+
export function resolveEmptyPairBackspace(text, caret) {
|
|
82
|
+
const pair = emptyPairEnding(text, caret) ?? emptyPairAround(text, caret);
|
|
83
|
+
if (!pair)
|
|
84
|
+
return null;
|
|
85
|
+
return write(text.slice(0, pair.start) + text.slice(pair.end), pair.start);
|
|
86
|
+
}
|
|
87
|
+
const write = (text, caret) => ({ kind: 'write', text, caret });
|
|
88
|
+
/**
|
|
89
|
+
* The `beforeinput` arm. True when the press was this seam's: the event is cancelled and the
|
|
90
|
+
* surface has written, moved or re-seated the caret.
|
|
91
|
+
*/
|
|
92
|
+
export function applyDelimiterAutoPair(e, surface) {
|
|
93
|
+
const typing = e.inputType === 'insertText';
|
|
94
|
+
if (!typing && e.inputType !== 'deleteContentBackward')
|
|
95
|
+
return false;
|
|
96
|
+
if (e.isComposing || surface.hasSelection())
|
|
97
|
+
return false;
|
|
98
|
+
const caret = surface.caret();
|
|
99
|
+
if (caret === null)
|
|
100
|
+
return false;
|
|
101
|
+
const text = surface.text();
|
|
102
|
+
if (surface.isRevealing()) {
|
|
103
|
+
if (!typing || !stepsOverRevealedCloser(text, caret, e.data ?? ''))
|
|
104
|
+
return false;
|
|
105
|
+
e.preventDefault();
|
|
106
|
+
surface.setCaret(caret + 1);
|
|
107
|
+
void surface.foldReveal()?.settled;
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
const edit = typing
|
|
111
|
+
? resolveDelimiterAutoPair(text, surface.content(), caret, e.data ?? '', surface.keepsBlockKind)
|
|
112
|
+
: resolveEmptyPairBackspace(text, caret);
|
|
113
|
+
if (!edit)
|
|
114
|
+
return false;
|
|
115
|
+
e.preventDefault();
|
|
116
|
+
switch (edit.kind) {
|
|
117
|
+
case 'step-over':
|
|
118
|
+
if (edit.overConstruct && !surface.markersPaint())
|
|
119
|
+
surface.seatOutside();
|
|
120
|
+
else if (surface.completesLine?.(edit.caret))
|
|
121
|
+
surface.write(text, caret, edit.caret);
|
|
122
|
+
else
|
|
123
|
+
surface.setCaret(edit.caret);
|
|
124
|
+
return true;
|
|
125
|
+
case 'close':
|
|
126
|
+
surface.write(edit.text, caret, edit.caret);
|
|
127
|
+
surface.seatOutside();
|
|
128
|
+
return true;
|
|
129
|
+
case 'write':
|
|
130
|
+
surface.write(edit.text, caret, edit.caret);
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// ── Runs ─────────────────────────────────────────────────────────────────────
|
|
135
|
+
/** Length of the run of `ch` ending just before `at` (0 when `text[at - 1]` is not `ch`). */
|
|
136
|
+
function runBefore(text, at, ch) {
|
|
137
|
+
let n = 0;
|
|
138
|
+
while (text[at - 1 - n] === ch)
|
|
139
|
+
n++;
|
|
140
|
+
return n;
|
|
141
|
+
}
|
|
142
|
+
function runAfter(text, at, ch) {
|
|
143
|
+
let n = 0;
|
|
144
|
+
while (text[at + n] === ch)
|
|
145
|
+
n++;
|
|
146
|
+
return n;
|
|
147
|
+
}
|
|
148
|
+
// `X..X|X..X`: equal runs of a pair delimiter, one or two bytes, with nothing of it either side.
|
|
149
|
+
function emptyPairAround(text, caret) {
|
|
150
|
+
const d = text[caret - 1];
|
|
151
|
+
if (d === undefined || !policyOf(d))
|
|
152
|
+
return null;
|
|
153
|
+
const k = runBefore(text, caret, d);
|
|
154
|
+
if (k > 2 || runAfter(text, caret, d) !== k)
|
|
155
|
+
return null;
|
|
156
|
+
return { start: caret - k, end: caret + k };
|
|
157
|
+
}
|
|
158
|
+
// `XX|`: a stepped-over empty pair with the caret after it. Only the delimiters that step (the
|
|
159
|
+
// emphasis family grows instead, and its `**|` is a double opener with content ahead).
|
|
160
|
+
function emptyPairEnding(text, caret) {
|
|
161
|
+
const d = text[caret - 1];
|
|
162
|
+
if (d === undefined || policyOf(d)?.inside !== 'step-over' || text[caret] === d)
|
|
163
|
+
return null;
|
|
164
|
+
if (runBefore(text, caret, d) !== 2)
|
|
165
|
+
return null;
|
|
166
|
+
return { start: caret - 2, end: caret };
|
|
167
|
+
}
|
|
168
|
+
// ── Constructs ───────────────────────────────────────────────────────────────
|
|
169
|
+
// `X|X` plus the typed byte: keep the twin run only if the pair then parses as a construct.
|
|
170
|
+
function collapseEmptyPair(text, content, caret, typed) {
|
|
171
|
+
const d = text[caret - 1];
|
|
172
|
+
if (d === undefined || !policyOf(d))
|
|
173
|
+
return null;
|
|
174
|
+
const k = runBefore(text, caret, d);
|
|
175
|
+
if (k > 2 || runAfter(text, caret, d) !== k)
|
|
176
|
+
return null;
|
|
177
|
+
const paired = text.slice(0, caret) + typed + text.slice(caret);
|
|
178
|
+
const shifted = { start: content.start, end: content.end + 1 };
|
|
179
|
+
if (constructAt(paired, shifted, caret - k, caret + 1 + k))
|
|
180
|
+
return null;
|
|
181
|
+
return write(text.slice(0, caret) + typed + text.slice(caret + k), caret + 1);
|
|
182
|
+
}
|
|
183
|
+
// The caret sits inside or at the start of the closing run of a construct this delimiter both
|
|
184
|
+
// opens and closes, so the byte under it is that construct's own closer.
|
|
185
|
+
function closingRunAt(text, content, caret, typed) {
|
|
186
|
+
for (const node of inlineDescendants(parseInline(text, content.start, content.end))) {
|
|
187
|
+
if (node.kind === 'text' || text[node.start] !== typed || node.end <= caret)
|
|
188
|
+
continue;
|
|
189
|
+
let closer = node.end;
|
|
190
|
+
while (closer > caret && text[closer - 1] === typed)
|
|
191
|
+
closer--;
|
|
192
|
+
if (closer <= caret)
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
// A construct of `typed`'s family ends exactly at `end`, its last byte in the closing run. The
|
|
198
|
+
// scan's bound grows past the content by the byte a caller has just inserted.
|
|
199
|
+
function closerEndsAt(text, content, end, typed) {
|
|
200
|
+
const bound = Math.max(content.end, end);
|
|
201
|
+
for (const node of inlineDescendants(parseInline(text, content.start, bound))) {
|
|
202
|
+
if (node.kind === 'text' || node.end !== end || text[end - 1] !== typed)
|
|
203
|
+
continue;
|
|
204
|
+
const inner = constructContentRange(node);
|
|
205
|
+
if (!inner || inner.end < end)
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
function constructAt(text, content, start, end) {
|
|
211
|
+
for (const node of inlineDescendants(parseInline(text, content.start, content.end))) {
|
|
212
|
+
if (node.kind !== 'text' && node.start === start && node.end === end)
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
@@ -13,6 +13,8 @@ import { type RawOffset } from '../../../cursor/coordinate-spaces';
|
|
|
13
13
|
import type { EdgeAffinity } from '../../../cursor/edge-affinity';
|
|
14
14
|
import type { PendingMarksState } from '../../../cursor/pending-marks';
|
|
15
15
|
import { type EdgeDeletionSurface } from './construct-edge-delete';
|
|
16
|
+
/** Whether `line` still reloads as `node`'s kind, the auto-pair resolver's guard. */
|
|
17
|
+
export declare function keepsBlockKind(node: NodeView, line: string): boolean;
|
|
16
18
|
/** The subset of the inline-widget vocabulary the internal island policies reuse,
|
|
17
19
|
* expressed in the same terms without leaking into the public API. */
|
|
18
20
|
export type EdgePolicy = Pick<InlineWidgetEditingPolicy, 'onEdge' | 'deleteGranularity'>;
|
|
@@ -70,6 +72,8 @@ export interface EdgePolicyDispatchDeps {
|
|
|
70
72
|
isReading: () => boolean;
|
|
71
73
|
/** The arrival side the typing seat consults; null when no arrival claimed one. */
|
|
72
74
|
getEdgeAffinity: () => EdgeAffinity | null;
|
|
75
|
+
/** A seated delimiter that completed a construct leaves the caret meaning outside it. */
|
|
76
|
+
noteOutside?: () => void;
|
|
73
77
|
/** The constructs a collapsed-caret toggle promised the next insertion. Read AND spent
|
|
74
78
|
* here: the first byte after the chord is the one insertion they were pending for. */
|
|
75
79
|
pendingMarks: PendingMarksState;
|
|
@@ -21,6 +21,12 @@ import { resolveEdgeSeat } from './edge-seat';
|
|
|
21
21
|
import { deleteRangeRaw } from './live-selection-edit';
|
|
22
22
|
import { resolveMarkedInsertion } from './pending-mark-insert';
|
|
23
23
|
import { widgetAtCursor } from './widget-adjacency';
|
|
24
|
+
import { resolveDelimiterAutoPair } from './delimiter-autopair';
|
|
25
|
+
import { soleProseReparse } from './screen-diff';
|
|
26
|
+
/** Whether `line` still reloads as `node`'s kind, the auto-pair resolver's guard. */
|
|
27
|
+
export function keepsBlockKind(node, line) {
|
|
28
|
+
return soleProseReparse(line + trailingLineEnding(node.raw))?.block.kind === node.kind;
|
|
29
|
+
}
|
|
24
30
|
const REPLACE_ISLAND_POLICY = {
|
|
25
31
|
onEdge: 'select',
|
|
26
32
|
deleteGranularity: 'select-then-delete'
|
|
@@ -34,6 +40,11 @@ export function createEdgePolicyDispatch(deps) {
|
|
|
34
40
|
reason: 'an explicit instruction about the very next byte, so it outranks every classification below, which decide by where the caret happens to be',
|
|
35
41
|
claims: handlePendingMarks
|
|
36
42
|
},
|
|
43
|
+
{
|
|
44
|
+
id: 'transitional-hard-break',
|
|
45
|
+
reason: 'a hard break at the end of a block has no following line yet, so its own position is where the next byte starts that line',
|
|
46
|
+
claims: handleTransitionalHardBreak
|
|
47
|
+
},
|
|
37
48
|
{
|
|
38
49
|
id: 'cst-widget',
|
|
39
50
|
reason: 'a key aimed at an atomic construct is the widget branch’s before any byte rule',
|
|
@@ -389,6 +400,35 @@ export function createEdgePolicyDispatch(deps) {
|
|
|
389
400
|
return true;
|
|
390
401
|
}
|
|
391
402
|
// ── Pending marks (the toggle seat) ────────────────────────────────────────
|
|
403
|
+
/**
|
|
404
|
+
* The byte that completes a hard break made at the END of a block. `insertHardBreak` writes
|
|
405
|
+
* `\\` plus the block's ending there, but that ending IS the block's trailing one, so the
|
|
406
|
+
* display stops at the backslash and the caret has nowhere to sit past it. The next
|
|
407
|
+
* printable key is what supplies the following line: it lands AFTER the break rather than
|
|
408
|
+
* between the backslash and its newline, where it would read as content and undo the break.
|
|
409
|
+
*/
|
|
410
|
+
function handleTransitionalHardBreak(e, caretOffset) {
|
|
411
|
+
if (deps.isReading())
|
|
412
|
+
return false;
|
|
413
|
+
if (!isPlainTypingKey(e) || caretOffset === null || hasSelectionHelper())
|
|
414
|
+
return false;
|
|
415
|
+
const d = display();
|
|
416
|
+
// Only at the very end, and only when the block's last byte is the break's backslash.
|
|
417
|
+
if (caretOffset !== d.length || !d.endsWith('\\'))
|
|
418
|
+
return false;
|
|
419
|
+
// An escaped backslash (`\\\\`) is content, not a break.
|
|
420
|
+
if (d.endsWith('\\\\'))
|
|
421
|
+
return false;
|
|
422
|
+
// Backslash before ASCII punctuation is an escape (`\|`, `\*`), never a break's backslash.
|
|
423
|
+
if (/^[!-/:-@[-`{-~]$/.test(e.key))
|
|
424
|
+
return false;
|
|
425
|
+
const ending = trailingLineEnding(deps.node.raw);
|
|
426
|
+
e.preventDefault();
|
|
427
|
+
deps.setSnapTarget(null);
|
|
428
|
+
const next = d + ending + e.key;
|
|
429
|
+
writeDisplay(next, next.length, 'transitional-hard-break', caretOffset);
|
|
430
|
+
return true;
|
|
431
|
+
}
|
|
392
432
|
/**
|
|
393
433
|
* A printable key while a collapsed-caret toggle has marks pending. Marks are the newer
|
|
394
434
|
* instruction about the same bytes, so they outrank the arrival side the seat below reads
|
|
@@ -442,14 +482,28 @@ export function createEdgePolicyDispatch(deps) {
|
|
|
442
482
|
function handleConstructSeat(e, caretOffset) {
|
|
443
483
|
if (!isPlainTypingKey(e) || caretOffset === null || hasSelectionHelper())
|
|
444
484
|
return false;
|
|
485
|
+
// A delimiter typed over its own closer is the auto-pair's step-over, which the beforeinput
|
|
486
|
+
// arm owns (delimiter-autopair.ts); seated outside the run it would be typed instead.
|
|
487
|
+
const content = getContentRange(deps.node);
|
|
488
|
+
const autoPair = resolveDelimiterAutoPair(display(), content, caretOffset, e.key);
|
|
489
|
+
if (autoPair?.kind === 'step-over')
|
|
490
|
+
return false;
|
|
445
491
|
const el = deps.getEl();
|
|
446
492
|
const seat = el && typingSeatAt(el, caretOffset, e.key);
|
|
447
493
|
if (!seat)
|
|
448
494
|
return false;
|
|
449
495
|
e.preventDefault();
|
|
450
496
|
deps.setSnapTarget(null);
|
|
451
|
-
// The
|
|
452
|
-
//
|
|
497
|
+
// The seat decides WHERE the byte lands; what a delimiter keystroke writes there is still
|
|
498
|
+
// the auto-pair's answer, or a byte seated past a run would arrive without its twin. The
|
|
499
|
+
// kind rides the trace: it names the construct whose hidden edge the caret sat at.
|
|
500
|
+
const paired = resolveDelimiterAutoPair(display(), content, seat.offset, e.key, (line) => keepsBlockKind(deps.node, line));
|
|
501
|
+
if (paired && paired.kind !== 'step-over') {
|
|
502
|
+
writeDisplay(paired.text, paired.caret, `seat:${seat.kind}`, caretOffset);
|
|
503
|
+
if (paired.kind === 'close')
|
|
504
|
+
deps.noteOutside?.();
|
|
505
|
+
return true;
|
|
506
|
+
}
|
|
453
507
|
editDisplay(seat.offset, seat.offset, e.key, `seat:${seat.kind}`, caretOffset);
|
|
454
508
|
return true;
|
|
455
509
|
}
|
|
@@ -19,6 +19,8 @@ export function resolveLiveRangeEdit(e, node, cursor, presentationMode, linkRef,
|
|
|
19
19
|
if (!range)
|
|
20
20
|
return null;
|
|
21
21
|
const insert = replacementText(e);
|
|
22
|
+
if (range.start === range.end)
|
|
23
|
+
return parkedCaretInsertion(node, cursor, range.start, insert);
|
|
22
24
|
const edit = resolveSelectionEdit(node, range, insert ?? '', presentationMode, linkRef, ambientPrefix);
|
|
23
25
|
if (!edit)
|
|
24
26
|
return null;
|
|
@@ -26,6 +28,31 @@ export function resolveLiveRangeEdit(e, node, cursor, presentationMode, linkRef,
|
|
|
26
28
|
? { kind: 'swallow' }
|
|
27
29
|
: { kind: 'rewrite', range, raw: edit.raw, caret: edit.caret };
|
|
28
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* A collapsed insertion has no seam to clean, but Chromium inserts at its canonical spelling of
|
|
33
|
+
* the caret's pixel, UPSTREAM across a hidden run, while the DOM caret may sit where a commit
|
|
34
|
+
* parked it, past that run (after a `)` just typed). A target behind the caret is that
|
|
35
|
+
* canonicalization, and the byte belongs where the caret is, the side the keydown seat settled. A
|
|
36
|
+
* target at or past the caret is the engine landing the byte itself, which a split's reopened run
|
|
37
|
+
* relies on: parked at its start, the byte goes inside.
|
|
38
|
+
*/
|
|
39
|
+
function parkedCaretInsertion(node, cursor, engineTarget, insert) {
|
|
40
|
+
if (!insert)
|
|
41
|
+
return null;
|
|
42
|
+
const selection = window.getSelection();
|
|
43
|
+
if (!selection || selection.rangeCount === 0 || !selection.isCollapsed)
|
|
44
|
+
return null;
|
|
45
|
+
const caret = cursor.rawRangeOf(selection.getRangeAt(0))?.start ?? null;
|
|
46
|
+
if (caret === null || engineTarget >= caret)
|
|
47
|
+
return null;
|
|
48
|
+
const display = trimTrailingLineEnding(node.raw);
|
|
49
|
+
return {
|
|
50
|
+
kind: 'rewrite',
|
|
51
|
+
range: { start: caret, end: caret },
|
|
52
|
+
raw: display.slice(0, caret) + insert + display.slice(caret) + trailingLineEnding(node.raw),
|
|
53
|
+
caret: caret + insert.length
|
|
54
|
+
};
|
|
55
|
+
}
|
|
29
56
|
/**
|
|
30
57
|
* The whole surface arm around {@link resolveLiveRangeEdit}: stand down while a source reveal has
|
|
31
58
|
* outrun the CST, resolve, preventDefault what the seam owns, then swallow or hand the rewrite to
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pure raw/caret transforms for TextEditableBlock
|
|
2
|
+
* Pure raw/caret transforms for TextEditableBlock's structural gestures — heading-level
|
|
3
3
|
* swap, demotion to prose, hard-break insertion, literal-tab insertion. The component
|
|
4
4
|
* owns the plumbing; these own the string math.
|
|
5
5
|
*/
|
|
@@ -21,6 +21,12 @@ export declare function dropStructuralPrefix(raw: string, contentStart: number,
|
|
|
21
21
|
/** Drop everything past `contentEnd` but the block's own trailing line ending — the setext
|
|
22
22
|
* underline, which sits after every caret the content range admits. */
|
|
23
23
|
export declare function dropStructuralSuffix(raw: string, contentEnd: number, preEditOffset: number): TextEditResult;
|
|
24
|
+
/**
|
|
25
|
+
* An ATX heading left with no text is a marker standing over nothing: unfocused it paints
|
|
26
|
+
* nothing (the chrome-only stamp is focus-scoped), so the block would survive invisibly and
|
|
27
|
+
* resurface as a `#` on the next click. On blur it becomes the empty paragraph it looks like.
|
|
28
|
+
*/
|
|
29
|
+
export declare function demoteEmptyAtxHeading(raw: string, content: ContentRange): TextEditResult | null;
|
|
24
30
|
/**
|
|
25
31
|
* Re-mark the block's CONTENT with an ATX prefix for `level`, replacing whatever structural bytes
|
|
26
32
|
* the current kind keeps — the same content range {@link demoteToParagraph} reads, so an indented
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pure raw/caret transforms for TextEditableBlock
|
|
2
|
+
* Pure raw/caret transforms for TextEditableBlock's structural gestures — heading-level
|
|
3
3
|
* swap, demotion to prose, hard-break insertion, literal-tab insertion. The component
|
|
4
4
|
* owns the plumbing; these own the string math.
|
|
5
5
|
*/
|
|
@@ -33,6 +33,16 @@ export function dropStructuralSuffix(raw, contentEnd, preEditOffset) {
|
|
|
33
33
|
caretOffset: Math.min(preEditOffset, contentEnd)
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* An ATX heading left with no text is a marker standing over nothing: unfocused it paints
|
|
38
|
+
* nothing (the chrome-only stamp is focus-scoped), so the block would survive invisibly and
|
|
39
|
+
* resurface as a `#` on the next click. On blur it becomes the empty paragraph it looks like.
|
|
40
|
+
*/
|
|
41
|
+
export function demoteEmptyAtxHeading(raw, content) {
|
|
42
|
+
if (content.start === 0 || content.end > content.start)
|
|
43
|
+
return null;
|
|
44
|
+
return demoteToParagraph(raw, content, 0);
|
|
45
|
+
}
|
|
36
46
|
/**
|
|
37
47
|
* Re-mark the block's CONTENT with an ATX prefix for `level`, replacing whatever structural bytes
|
|
38
48
|
* the current kind keeps — the same content range {@link demoteToParagraph} reads, so an indented
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { AmbientPrefix } from '../../../block-component';
|
|
8
8
|
import type { DocumentView, NodeView } from '../../../core/node-views';
|
|
9
|
-
import type
|
|
9
|
+
import { type PresentationMode } from '../../../presentation-mode';
|
|
10
10
|
import type { ResolveImageUrl, ResolveLinkUrl } from '../../../editor-keys';
|
|
11
11
|
import type { LinkReferenceResolver } from '../../../core/inline/link-reference-resolver';
|
|
12
12
|
import { type ImageLoadPolicy } from '../../../core/inline-render';
|