@beyondwork/docx-react-component 1.0.36 → 1.0.38
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 +103 -13
- package/package.json +1 -1
- package/src/api/package-version.ts +13 -0
- package/src/api/public-types.ts +402 -1
- package/src/core/commands/index.ts +18 -1
- package/src/core/commands/section-layout-commands.ts +58 -0
- package/src/core/commands/table-grid.ts +431 -0
- package/src/core/commands/table-structure-commands.ts +815 -55
- package/src/core/selection/mapping.ts +6 -0
- package/src/io/docx-session.ts +24 -9
- package/src/io/export/build-app-properties-xml.ts +88 -0
- package/src/io/export/serialize-comments.ts +6 -1
- package/src/io/export/serialize-footnotes.ts +10 -9
- package/src/io/export/serialize-headers-footers.ts +11 -10
- package/src/io/export/serialize-main-document.ts +328 -50
- package/src/io/export/serialize-numbering.ts +114 -24
- package/src/io/export/serialize-tables.ts +87 -11
- package/src/io/export/table-properties-xml.ts +174 -20
- package/src/io/export/twip.ts +66 -0
- package/src/io/normalize/normalize-text.ts +20 -0
- package/src/io/ooxml/parse-footnotes.ts +62 -1
- package/src/io/ooxml/parse-headers-footers.ts +62 -1
- package/src/io/ooxml/parse-main-document.ts +158 -1
- package/src/io/ooxml/parse-tables.ts +249 -0
- package/src/legal/bookmarks.ts +78 -0
- package/src/model/canonical-document.ts +45 -0
- package/src/review/store/scope-tag-diff.ts +130 -0
- package/src/runtime/document-layout.ts +4 -2
- package/src/runtime/document-navigation.ts +2 -306
- package/src/runtime/document-runtime.ts +287 -11
- package/src/runtime/layout/default-page-format.ts +96 -0
- package/src/runtime/layout/docx-font-loader.ts +143 -0
- package/src/runtime/layout/index.ts +233 -0
- package/src/runtime/layout/inert-layout-facet.ts +59 -0
- package/src/runtime/layout/layout-engine-instance.ts +628 -0
- package/src/runtime/layout/layout-invalidation.ts +257 -0
- package/src/runtime/layout/layout-measurement-provider.ts +175 -0
- package/src/runtime/layout/margin-preset-catalog.ts +178 -0
- package/src/runtime/layout/measurement-backend-canvas.ts +307 -0
- package/src/runtime/layout/measurement-backend-empirical.ts +208 -0
- package/src/runtime/layout/page-format-catalog.ts +233 -0
- package/src/runtime/layout/page-fragment-mapper.ts +179 -0
- package/src/runtime/layout/page-graph.ts +452 -0
- package/src/runtime/layout/page-layout-snapshot-adapter.ts +70 -0
- package/src/runtime/layout/page-story-resolver.ts +195 -0
- package/src/runtime/layout/paginated-layout-engine.ts +921 -0
- package/src/runtime/layout/project-block-fragments.ts +91 -0
- package/src/runtime/layout/public-facet.ts +1398 -0
- package/src/runtime/layout/resolved-formatting-document.ts +317 -0
- package/src/runtime/layout/resolved-formatting-state.ts +430 -0
- package/src/runtime/layout/table-render-plan.ts +229 -0
- package/src/runtime/render/block-fragment-projection.ts +35 -0
- package/src/runtime/render/decoration-resolver.ts +189 -0
- package/src/runtime/render/index.ts +57 -0
- package/src/runtime/render/pending-op-delta-reader.ts +129 -0
- package/src/runtime/render/render-frame-types.ts +317 -0
- package/src/runtime/render/render-kernel.ts +755 -0
- package/src/runtime/scope-tag-registry.ts +95 -0
- package/src/runtime/surface-projection.ts +1 -0
- package/src/runtime/text-ack-range.ts +49 -0
- package/src/runtime/view-state.ts +67 -0
- package/src/runtime/workflow-markup.ts +1 -5
- package/src/runtime/workflow-rail-segments.ts +280 -0
- package/src/ui/WordReviewEditor.tsx +99 -15
- package/src/ui/editor-runtime-boundary.ts +10 -1
- package/src/ui/editor-shell-view.tsx +6 -0
- package/src/ui/editor-surface-controller.tsx +3 -0
- package/src/ui/headless/chrome-registry.ts +501 -0
- package/src/ui/headless/scoped-chrome-policy.ts +183 -0
- package/src/ui/headless/selection-tool-context.ts +2 -0
- package/src/ui/headless/selection-tool-resolver.ts +36 -17
- package/src/ui/headless/selection-tool-types.ts +10 -0
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +23 -2
- package/src/ui-tailwind/chrome/role-action-sets.ts +74 -0
- package/src/ui-tailwind/chrome/tw-detach-handle.tsx +147 -0
- package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +163 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +57 -92
- package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +149 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +15 -4
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +274 -138
- package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +90 -0
- package/src/ui-tailwind/chrome-overlay/index.ts +22 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +86 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +178 -0
- package/src/ui-tailwind/chrome-overlay/tw-workspace-view-switcher.tsx +95 -0
- package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +337 -0
- package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +100 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +27 -1
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +20 -2
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +93 -23
- package/src/ui-tailwind/editor-surface/predicted-position-map.ts +78 -0
- package/src/ui-tailwind/editor-surface/predicted-tag-preflight.ts +63 -0
- package/src/ui-tailwind/editor-surface/predicted-tx-gate.ts +39 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +176 -6
- package/src/ui-tailwind/index.ts +33 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +2 -2
- package/src/ui-tailwind/review/tw-rail-card.tsx +150 -0
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +52 -0
- package/src/ui-tailwind/review/tw-review-rail.tsx +166 -11
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +108 -0
- package/src/ui-tailwind/theme/editor-theme.css +505 -144
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +559 -0
- package/src/ui-tailwind/toolbar/tw-scope-posture-menu.tsx +182 -0
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +162 -0
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +2 -2
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +304 -166
- package/src/ui-tailwind/tw-review-workspace.tsx +163 -2
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { EditorState } from "prosemirror-state";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* LocalEditSessionState — internal to the mounted surface.
|
|
5
|
+
*
|
|
6
|
+
* Tracks the current canonical revision token, any predicted text ops that
|
|
7
|
+
* have been dispatched locally but not yet reconciled, and a pre-image per op
|
|
8
|
+
* so the lane can roll back on a `rejected` or `structural-divergence` ack.
|
|
9
|
+
*
|
|
10
|
+
* The lane owns all mutation of this state. No React state, no context, no
|
|
11
|
+
* event emission — purely a synchronous bookkeeping ledger.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface PredictedPreImagePM {
|
|
15
|
+
/** Captured PM state BEFORE the predicted tx. Restored via view.updateState on rollback. */
|
|
16
|
+
preState: EditorState;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type PredictedIntent =
|
|
20
|
+
| { kind: "text.insert"; text: string }
|
|
21
|
+
| { kind: "text.delete-backward" }
|
|
22
|
+
| { kind: "text.delete-forward" }
|
|
23
|
+
| { kind: "paragraph.split" }
|
|
24
|
+
| { kind: "text.insert-hard-break" };
|
|
25
|
+
|
|
26
|
+
export interface PendingOp {
|
|
27
|
+
opId: string;
|
|
28
|
+
intent: PredictedIntent;
|
|
29
|
+
preImagePM: PredictedPreImagePM | null;
|
|
30
|
+
/** Runtime range the predicted tx targeted BEFORE application (selection bounds). */
|
|
31
|
+
fromRuntime: number;
|
|
32
|
+
toRuntime: number;
|
|
33
|
+
/** PM selection head after the predicted tx applied. */
|
|
34
|
+
predictedSelectionHead?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface LocalEditSessionState {
|
|
38
|
+
getBaseRevisionToken(): string;
|
|
39
|
+
getPendingOps(): readonly PendingOp[];
|
|
40
|
+
appendPending(op: PendingOp): void;
|
|
41
|
+
advanceToRevision(ack: { opId: string; newRevisionToken: string }): void;
|
|
42
|
+
rollbackOp(opId: string): PendingOp | null;
|
|
43
|
+
clearAllPending(): PendingOp[];
|
|
44
|
+
hasPending(): boolean;
|
|
45
|
+
isPredicted(opId: string): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* IME composition state. Set to true while the browser is composing an
|
|
48
|
+
* IME input sequence (between `compositionstart` and `compositionend`);
|
|
49
|
+
* the predicted lane must bail from `run()` when composing so IME and
|
|
50
|
+
* prediction do not fight over the same DOM range.
|
|
51
|
+
*/
|
|
52
|
+
isComposing(): boolean;
|
|
53
|
+
setComposing(composing: boolean): void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface CreateLocalEditSessionStateOptions {
|
|
57
|
+
baseRevisionToken: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function createLocalEditSessionState(
|
|
61
|
+
options: CreateLocalEditSessionStateOptions,
|
|
62
|
+
): LocalEditSessionState {
|
|
63
|
+
let baseRevisionToken = options.baseRevisionToken;
|
|
64
|
+
let composing = false;
|
|
65
|
+
const pendingOps: PendingOp[] = [];
|
|
66
|
+
const predictedIds = new Set<string>();
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
getBaseRevisionToken: () => baseRevisionToken,
|
|
70
|
+
getPendingOps: () => pendingOps.slice(),
|
|
71
|
+
isComposing: () => composing,
|
|
72
|
+
setComposing: (value) => { composing = value; },
|
|
73
|
+
appendPending(op) {
|
|
74
|
+
pendingOps.push(op);
|
|
75
|
+
predictedIds.add(op.opId);
|
|
76
|
+
},
|
|
77
|
+
advanceToRevision({ opId, newRevisionToken }) {
|
|
78
|
+
const idx = pendingOps.findIndex((op) => op.opId === opId);
|
|
79
|
+
if (idx >= 0) {
|
|
80
|
+
pendingOps.splice(idx, 1);
|
|
81
|
+
predictedIds.delete(opId);
|
|
82
|
+
}
|
|
83
|
+
baseRevisionToken = newRevisionToken;
|
|
84
|
+
},
|
|
85
|
+
rollbackOp(opId) {
|
|
86
|
+
const idx = pendingOps.findIndex((op) => op.opId === opId);
|
|
87
|
+
if (idx < 0) return null;
|
|
88
|
+
const [op] = pendingOps.splice(idx, 1);
|
|
89
|
+
predictedIds.delete(opId);
|
|
90
|
+
return op;
|
|
91
|
+
},
|
|
92
|
+
clearAllPending() {
|
|
93
|
+
const all = pendingOps.splice(0, pendingOps.length);
|
|
94
|
+
predictedIds.clear();
|
|
95
|
+
return all;
|
|
96
|
+
},
|
|
97
|
+
hasPending: () => pendingOps.length > 0,
|
|
98
|
+
isPredicted: (opId) => predictedIds.has(opId),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export type PerfProbeKind =
|
|
2
2
|
| "typing"
|
|
3
|
+
| "typing.predicted"
|
|
4
|
+
| "typing.reconcile"
|
|
5
|
+
| "typing.divergence"
|
|
3
6
|
| "selection"
|
|
4
7
|
| "runtime.create"
|
|
5
8
|
| "snapshot.surface"
|
|
@@ -10,7 +13,30 @@ export type PerfProbeKind =
|
|
|
10
13
|
| "pm.mount"
|
|
11
14
|
| "shell.render"
|
|
12
15
|
| "workspace.chrome"
|
|
13
|
-
| "selection.sync"
|
|
16
|
+
| "selection.sync"
|
|
17
|
+
| "layout.incremental"
|
|
18
|
+
| "layout.full"
|
|
19
|
+
| "render.frame_build"
|
|
20
|
+
| "render.frame_diff"
|
|
21
|
+
| "render.decoration_resolve"
|
|
22
|
+
| "chrome.overlay_reposition"
|
|
23
|
+
| "chrome.hit_test"
|
|
24
|
+
| "rail.segment_project";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Counter names the FastTextEditLane emits via `incrementInvalidationCounter`.
|
|
28
|
+
* Expose them as a const so integrators can read the shape without duplicating
|
|
29
|
+
* strings.
|
|
30
|
+
*/
|
|
31
|
+
export const PREDICTED_LANE_COUNTERS = {
|
|
32
|
+
applied: "predictions.applied",
|
|
33
|
+
equivalent: "predictions.equivalent",
|
|
34
|
+
adjusted: "predictions.adjusted",
|
|
35
|
+
rejected: "predictions.rejected",
|
|
36
|
+
rollback: "predictions.rollback",
|
|
37
|
+
structuralDivergence: "predictions.structuralDivergence",
|
|
38
|
+
bailBeforePredict: "predictions.bailBeforePredict",
|
|
39
|
+
} as const;
|
|
14
40
|
|
|
15
41
|
export interface PerfProbeSample {
|
|
16
42
|
token: string;
|
|
@@ -26,6 +26,19 @@ export interface CommandBridgeCallbacks extends SelectionSyncCallbacks {
|
|
|
26
26
|
onUndo: () => void;
|
|
27
27
|
onRedo: () => void;
|
|
28
28
|
onBlockedInput?: (command: "paste" | "drop", message: string) => void;
|
|
29
|
+
/**
|
|
30
|
+
* Optional. Fires on `compositionstart` (true) and `compositionend`
|
|
31
|
+
* (false). The surface forwards this to the predicted lane's session
|
|
32
|
+
* so the lane can bail from `run()` while IME is composing.
|
|
33
|
+
*/
|
|
34
|
+
onCompositionChange?: (composing: boolean) => void;
|
|
35
|
+
/**
|
|
36
|
+
* Optional predicted-tx gate plugin. When provided, it replaces the
|
|
37
|
+
* default unconditional filter so the FastTextEditLane can apply
|
|
38
|
+
* registered predicted transactions locally before the canonical commit
|
|
39
|
+
* lands. When absent, the legacy "block all docChanged" behavior applies.
|
|
40
|
+
*/
|
|
41
|
+
gate?: Plugin;
|
|
29
42
|
}
|
|
30
43
|
|
|
31
44
|
const bridgeKey = new PluginKey("command-bridge");
|
|
@@ -69,7 +82,7 @@ export function createCommandBridgePlugins(
|
|
|
69
82
|
): Plugin[] {
|
|
70
83
|
let isComposing = false;
|
|
71
84
|
|
|
72
|
-
const filterPlugin = new Plugin({
|
|
85
|
+
const filterPlugin = callbacks.gate ?? new Plugin({
|
|
73
86
|
key: bridgeKey,
|
|
74
87
|
filterTransaction(tr) {
|
|
75
88
|
if (!tr.docChanged) return true;
|
|
@@ -84,15 +97,20 @@ export function createCommandBridgePlugins(
|
|
|
84
97
|
props: {
|
|
85
98
|
handleDOMEvents: {
|
|
86
99
|
blur() {
|
|
87
|
-
isComposing
|
|
100
|
+
if (isComposing) {
|
|
101
|
+
isComposing = false;
|
|
102
|
+
callbacks.onCompositionChange?.(false);
|
|
103
|
+
}
|
|
88
104
|
return false;
|
|
89
105
|
},
|
|
90
106
|
compositionstart() {
|
|
91
107
|
isComposing = true;
|
|
108
|
+
callbacks.onCompositionChange?.(true);
|
|
92
109
|
return false;
|
|
93
110
|
},
|
|
94
111
|
compositionend() {
|
|
95
112
|
isComposing = false;
|
|
113
|
+
callbacks.onCompositionChange?.(false);
|
|
96
114
|
return false;
|
|
97
115
|
},
|
|
98
116
|
},
|
|
@@ -223,6 +223,19 @@ function subtractInlineOverlaps(
|
|
|
223
223
|
return segments.filter((segment) => segment.from < segment.to);
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
+
/**
|
|
227
|
+
* Rail decorations are now rendered on the `ChromeOverlay` plane via the
|
|
228
|
+
* `TwScopeRailLayer` consumer of `facet.getAllScopeRailSegments()`, not
|
|
229
|
+
* through PM Decoration.node. This function keeps its signature so the
|
|
230
|
+
* call sites below continue to compile; it warms the range cache (which
|
|
231
|
+
* other PM decorations can still consume) but emits no node decoration.
|
|
232
|
+
*
|
|
233
|
+
* Per runtime-rendering-and-chrome-phase.md §5 the rail must live outside
|
|
234
|
+
* the PM NodeView tree so: (a) the user perceives it as chrome, not
|
|
235
|
+
* document content, (b) predicted transactions never flash rail visuals,
|
|
236
|
+
* and (c) the rail can extend into the page-margin gutter, which PM
|
|
237
|
+
* cannot paint through block decorations.
|
|
238
|
+
*/
|
|
226
239
|
function pushRailDecorations(
|
|
227
240
|
decorations: Decoration[],
|
|
228
241
|
doc: PMNode,
|
|
@@ -231,19 +244,11 @@ function pushRailDecorations(
|
|
|
231
244
|
spec: RailDecorationSpec,
|
|
232
245
|
rangeCache: Map<string, Array<{ from: number; to: number }>>,
|
|
233
246
|
): void {
|
|
247
|
+
void decorations;
|
|
248
|
+
void spec;
|
|
234
249
|
const cacheKey = `${from}:${to}`;
|
|
235
|
-
const ranges = rangeCache.get(cacheKey) ?? collectRailRanges(doc, from, to);
|
|
236
250
|
if (!rangeCache.has(cacheKey)) {
|
|
237
|
-
rangeCache.set(cacheKey,
|
|
238
|
-
}
|
|
239
|
-
for (const range of ranges) {
|
|
240
|
-
decorations.push(
|
|
241
|
-
Decoration.node(range.from, range.to, {
|
|
242
|
-
class: spec.className,
|
|
243
|
-
"data-workflow-rail": spec.railKind,
|
|
244
|
-
...spec.attrs,
|
|
245
|
-
}),
|
|
246
|
-
);
|
|
251
|
+
rangeCache.set(cacheKey, collectRailRanges(doc, from, to));
|
|
247
252
|
}
|
|
248
253
|
}
|
|
249
254
|
|
|
@@ -261,19 +266,79 @@ export function buildDecorations(
|
|
|
261
266
|
revisionModel: RevisionDecorationModel | undefined,
|
|
262
267
|
markupDisplay: MarkupDisplay,
|
|
263
268
|
showTrackedChanges = true,
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
269
|
+
suggestionsEnabledOrWorkflowScopes: boolean | readonly WorkflowScope[] = false,
|
|
270
|
+
workflowScopesOrActiveStory?: readonly WorkflowScope[] | EditorStoryTarget,
|
|
271
|
+
activeStoryOrWorkflowCandidates: EditorStoryTarget | readonly WorkflowCandidateRange[] = MAIN_STORY_TARGET,
|
|
272
|
+
workflowCandidatesOrBlockedReasons?: readonly WorkflowCandidateRange[] | readonly WorkflowBlockedCommandReason[],
|
|
273
|
+
workflowBlockedReasonsOrLockedZones?: readonly WorkflowBlockedCommandReason[] | readonly WorkflowLockedZone[],
|
|
274
|
+
workflowLockedZonesOrActiveWorkItemId?: readonly WorkflowLockedZone[] | string | null,
|
|
275
|
+
activeWorkflowWorkItemIdOrScopeIds?: string | null | readonly string[],
|
|
276
|
+
activeWorkflowScopeIdsOrMetadata?: readonly string[] | readonly WorkflowMetadataMarkup[],
|
|
272
277
|
workflowMetadata?: readonly WorkflowMetadataMarkup[],
|
|
273
278
|
): DecorationSet {
|
|
279
|
+
const isStoryTarget = (value: unknown): value is EditorStoryTarget =>
|
|
280
|
+
Boolean(value) &&
|
|
281
|
+
typeof value === "object" &&
|
|
282
|
+
"kind" in (value as Record<string, unknown>) &&
|
|
283
|
+
typeof (value as Record<string, unknown>).kind === "string";
|
|
284
|
+
const isStringArray = (value: unknown): value is readonly string[] =>
|
|
285
|
+
Array.isArray(value) && (value.length === 0 || typeof value[0] === "string");
|
|
286
|
+
const isWorkflowMetadataArray = (value: unknown): value is readonly WorkflowMetadataMarkup[] =>
|
|
287
|
+
Array.isArray(value) &&
|
|
288
|
+
value.length > 0 &&
|
|
289
|
+
typeof value[0] === "object" &&
|
|
290
|
+
value[0] !== null &&
|
|
291
|
+
"metadataId" in (value[0] as Record<string, unknown>);
|
|
292
|
+
|
|
293
|
+
const useLegacyShape =
|
|
294
|
+
typeof suggestionsEnabledOrWorkflowScopes !== "boolean" ||
|
|
295
|
+
isStoryTarget(workflowScopesOrActiveStory);
|
|
296
|
+
const suggestionsEnabled = useLegacyShape ? false : suggestionsEnabledOrWorkflowScopes;
|
|
297
|
+
const workflowScopes = useLegacyShape
|
|
298
|
+
? (Array.isArray(suggestionsEnabledOrWorkflowScopes)
|
|
299
|
+
? suggestionsEnabledOrWorkflowScopes
|
|
300
|
+
: undefined)
|
|
301
|
+
: (workflowScopesOrActiveStory as readonly WorkflowScope[] | undefined);
|
|
302
|
+
const activeStory = useLegacyShape
|
|
303
|
+
? ((isStoryTarget(workflowScopesOrActiveStory)
|
|
304
|
+
? workflowScopesOrActiveStory
|
|
305
|
+
: MAIN_STORY_TARGET) as EditorStoryTarget)
|
|
306
|
+
: ((activeStoryOrWorkflowCandidates as EditorStoryTarget | undefined) ?? MAIN_STORY_TARGET);
|
|
307
|
+
const workflowCandidates = useLegacyShape
|
|
308
|
+
? (Array.isArray(activeStoryOrWorkflowCandidates)
|
|
309
|
+
? activeStoryOrWorkflowCandidates as readonly WorkflowCandidateRange[]
|
|
310
|
+
: undefined)
|
|
311
|
+
: (workflowCandidatesOrBlockedReasons as readonly WorkflowCandidateRange[] | undefined);
|
|
312
|
+
const workflowBlockedReasons = useLegacyShape
|
|
313
|
+
? (Array.isArray(workflowCandidatesOrBlockedReasons)
|
|
314
|
+
? workflowCandidatesOrBlockedReasons as readonly WorkflowBlockedCommandReason[]
|
|
315
|
+
: undefined)
|
|
316
|
+
: (workflowBlockedReasonsOrLockedZones as readonly WorkflowBlockedCommandReason[] | undefined);
|
|
317
|
+
const workflowLockedZones = useLegacyShape
|
|
318
|
+
? (Array.isArray(workflowBlockedReasonsOrLockedZones)
|
|
319
|
+
? workflowBlockedReasonsOrLockedZones as readonly WorkflowLockedZone[]
|
|
320
|
+
: undefined)
|
|
321
|
+
: (workflowLockedZonesOrActiveWorkItemId as readonly WorkflowLockedZone[] | undefined);
|
|
322
|
+
const activeWorkflowWorkItemId = useLegacyShape
|
|
323
|
+
? (typeof workflowLockedZonesOrActiveWorkItemId === "string" || workflowLockedZonesOrActiveWorkItemId === null
|
|
324
|
+
? workflowLockedZonesOrActiveWorkItemId
|
|
325
|
+
: undefined)
|
|
326
|
+
: (activeWorkflowWorkItemIdOrScopeIds as string | null | undefined);
|
|
327
|
+
const activeWorkflowScopeIds = useLegacyShape
|
|
328
|
+
? (isStringArray(activeWorkflowWorkItemIdOrScopeIds)
|
|
329
|
+
? activeWorkflowWorkItemIdOrScopeIds as readonly string[]
|
|
330
|
+
: undefined)
|
|
331
|
+
: (activeWorkflowScopeIdsOrMetadata as readonly string[] | undefined);
|
|
332
|
+
const resolvedWorkflowMetadata = useLegacyShape
|
|
333
|
+
? (isWorkflowMetadataArray(activeWorkflowScopeIdsOrMetadata)
|
|
334
|
+
? activeWorkflowScopeIdsOrMetadata
|
|
335
|
+
: undefined)
|
|
336
|
+
: workflowMetadata;
|
|
337
|
+
|
|
274
338
|
const decorations: Decoration[] = [];
|
|
275
339
|
const railRangeCache = new Map<string, Array<{ from: number; to: number }>>();
|
|
276
340
|
const activeScopeIds = new Set(activeWorkflowScopeIds ?? []);
|
|
341
|
+
const effectiveWorkflowScopes = workflowScopes ?? [];
|
|
277
342
|
const lockedPmRanges = collectLockedPmRanges(workflowLockedZones, activeStory, positionMap);
|
|
278
343
|
|
|
279
344
|
// Walk comment threads and create inline decorations
|
|
@@ -384,8 +449,8 @@ export function buildDecorations(
|
|
|
384
449
|
}
|
|
385
450
|
}
|
|
386
451
|
|
|
387
|
-
if (
|
|
388
|
-
for (const scope of
|
|
452
|
+
if (effectiveWorkflowScopes.length > 0) {
|
|
453
|
+
for (const scope of effectiveWorkflowScopes) {
|
|
389
454
|
const scopeStoryTarget = scope.storyTarget ?? MAIN_STORY_TARGET;
|
|
390
455
|
if (!storyTargetsEqual(scopeStoryTarget, activeStory)) continue;
|
|
391
456
|
const pmRange = buildAnchorPmRange(scope.anchor, positionMap);
|
|
@@ -399,6 +464,11 @@ export function buildDecorations(
|
|
|
399
464
|
);
|
|
400
465
|
|
|
401
466
|
if (pmRange.allowInline && pmRange.from < pmRange.to) {
|
|
467
|
+
// Post-R3a: every workflow scope emits inline decorations with
|
|
468
|
+
// the scope-id attribution. The flat block-tint + gutter rail
|
|
469
|
+
// render on the ChromeOverlay — PM keeps only inline class hooks
|
|
470
|
+
// so selection tools, accessibility, and host scripts can still
|
|
471
|
+
// resolve the active scope at a text offset.
|
|
402
472
|
const visibleScopeSegments = subtractInlineOverlaps(
|
|
403
473
|
{ from: pmRange.from, to: pmRange.to },
|
|
404
474
|
lockedPmRanges.filter((range) => range.to > pmRange.from && range.from < pmRange.to),
|
|
@@ -429,8 +499,8 @@ export function buildDecorations(
|
|
|
429
499
|
}
|
|
430
500
|
}
|
|
431
501
|
|
|
432
|
-
if (
|
|
433
|
-
for (const metadata of
|
|
502
|
+
if (resolvedWorkflowMetadata) {
|
|
503
|
+
for (const metadata of resolvedWorkflowMetadata) {
|
|
434
504
|
const metadataStoryTarget = metadata.storyTarget ?? MAIN_STORY_TARGET;
|
|
435
505
|
if (!storyTargetsEqual(metadataStoryTarget, activeStory)) continue;
|
|
436
506
|
const pmRange = buildAnchorPmRange(metadata.anchor, positionMap);
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { PositionMap } from "./pm-position-map.ts";
|
|
2
|
+
import type { PendingOp } from "./local-edit-session-state.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* PredictedPositionMap — layers pending predicted-op deltas on top of the
|
|
6
|
+
* canonical `PositionMap`.
|
|
7
|
+
*
|
|
8
|
+
* When there are no pending ops this passes through the canonical map
|
|
9
|
+
* unchanged. When predictions are outstanding, selection-sync and external
|
|
10
|
+
* runtime queries use this view to map runtime positions through the
|
|
11
|
+
* applied-but-not-yet-committed local edits.
|
|
12
|
+
*
|
|
13
|
+
* After a reconciled commit, the lane advances the session's base revision
|
|
14
|
+
* token and discards the corresponding predicted op; subsequent queries go
|
|
15
|
+
* through the new canonical map.
|
|
16
|
+
*/
|
|
17
|
+
export function createPredictedPositionMap(
|
|
18
|
+
canonical: PositionMap,
|
|
19
|
+
pendingOps: readonly PendingOp[],
|
|
20
|
+
): PositionMap {
|
|
21
|
+
if (pendingOps.length === 0) return canonical;
|
|
22
|
+
|
|
23
|
+
function opsBefore(runtimePos: number): number {
|
|
24
|
+
let delta = 0;
|
|
25
|
+
for (const op of pendingOps) {
|
|
26
|
+
if (op.fromRuntime <= runtimePos) {
|
|
27
|
+
delta += opSizeDelta(op);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return delta;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
runtimeToPm(runtimePos) {
|
|
35
|
+
return canonical.runtimeToPm(runtimePos) + opsBefore(runtimePos);
|
|
36
|
+
},
|
|
37
|
+
pmToRuntime(pmPos) {
|
|
38
|
+
let adjusted = pmPos;
|
|
39
|
+
for (const op of pendingOps) {
|
|
40
|
+
const opPmStart = canonical.runtimeToPm(op.fromRuntime);
|
|
41
|
+
if (adjusted > opPmStart) {
|
|
42
|
+
adjusted -= opSizeDelta(op);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return canonical.pmToRuntime(Math.max(1, adjusted));
|
|
46
|
+
},
|
|
47
|
+
get pmDocSize() {
|
|
48
|
+
return canonical.pmDocSize + totalDelta(pendingOps);
|
|
49
|
+
},
|
|
50
|
+
get runtimeStorySize() {
|
|
51
|
+
return canonical.runtimeStorySize + totalDelta(pendingOps);
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function totalDelta(pendingOps: readonly PendingOp[]): number {
|
|
57
|
+
let delta = 0;
|
|
58
|
+
for (const op of pendingOps) {
|
|
59
|
+
delta += opSizeDelta(op);
|
|
60
|
+
}
|
|
61
|
+
return delta;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function opSizeDelta(op: PendingOp): number {
|
|
65
|
+
switch (op.intent.kind) {
|
|
66
|
+
case "text.insert":
|
|
67
|
+
return op.intent.text.length;
|
|
68
|
+
case "text.delete-backward":
|
|
69
|
+
case "text.delete-forward":
|
|
70
|
+
return -(op.toRuntime - op.fromRuntime);
|
|
71
|
+
case "paragraph.split":
|
|
72
|
+
return 2;
|
|
73
|
+
case "text.insert-hard-break":
|
|
74
|
+
return 1;
|
|
75
|
+
default:
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
EditorSurfaceSnapshot,
|
|
3
|
+
SurfaceBlockSnapshot,
|
|
4
|
+
} from "../../api/public-types.ts";
|
|
5
|
+
import type { ScopeTagRegistry } from "../../runtime/scope-tag-registry.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Pre-flight check the FastTextEditLane consults before applying a predicted
|
|
9
|
+
* transaction. Returns true when the proposed edit range intersects any tag
|
|
10
|
+
* whose registry behavior is `bailIfCrossed: true` — today, that is fields,
|
|
11
|
+
* SDTs, and opaque (preserve-only) blocks. Such edits would be rolled back by
|
|
12
|
+
* the runtime's workflow / structural-divergence checks anyway; bailing
|
|
13
|
+
* before predicting saves the predicted-then-restored PM churn.
|
|
14
|
+
*
|
|
15
|
+
* Phase 1 scope: top-level surface blocks (paragraph, opaque_block, sdt_block)
|
|
16
|
+
* plus inline field_ref segments inside paragraphs. Does NOT recurse into
|
|
17
|
+
* sdt_block.children (the block boundary already bails). Does NOT walk into
|
|
18
|
+
* `table` blocks or their cells (left to the runtime safety net).
|
|
19
|
+
*
|
|
20
|
+
* Boundary semantics: this uses strict-open intersection
|
|
21
|
+
* (`aFrom < bTo && aTo > bFrom`). A collapsed cursor sitting exactly at
|
|
22
|
+
* a tag boundary is NOT considered intersecting. This intentionally
|
|
23
|
+
* under-bails on boundary-touching cursors: an insert at the left edge
|
|
24
|
+
* of a field is a legal edit (the field shifts), so over-bailing would
|
|
25
|
+
* cost a predicted-tx optimization for no correctness benefit. The
|
|
26
|
+
* downside is that a delete-forward at the left edge or a delete-backward
|
|
27
|
+
* at the right edge of a bail-if-crossed tag will fall through to the
|
|
28
|
+
* runtime, which still rejects the edit — the lane pays one
|
|
29
|
+
* predicted-then-rolled-back PM cycle for those keystrokes. A future
|
|
30
|
+
* phase that takes the predicted intent's direction can tighten this.
|
|
31
|
+
*/
|
|
32
|
+
export function hasBailIfCrossedTagInRange(
|
|
33
|
+
surface: EditorSurfaceSnapshot,
|
|
34
|
+
registry: ScopeTagRegistry,
|
|
35
|
+
fromRuntime: number,
|
|
36
|
+
toRuntime: number,
|
|
37
|
+
): boolean {
|
|
38
|
+
const opaqueBails = registry.get("opaque").bailIfCrossed;
|
|
39
|
+
const sdtBails = registry.get("sdt").bailIfCrossed;
|
|
40
|
+
const fieldBails = registry.get("field").bailIfCrossed;
|
|
41
|
+
for (const block of surface.blocks) {
|
|
42
|
+
if (!intersects(block.from, block.to, fromRuntime, toRuntime)) continue;
|
|
43
|
+
if (block.kind === "opaque_block" && opaqueBails) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
if (block.kind === "sdt_block" && sdtBails) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
if (block.kind === "paragraph" && fieldBails) {
|
|
50
|
+
for (const segment of block.segments) {
|
|
51
|
+
if (segment.kind !== "field_ref") continue;
|
|
52
|
+
if (intersects(segment.from, segment.to, fromRuntime, toRuntime)) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function intersects(aFrom: number, aTo: number, bFrom: number, bTo: number): boolean {
|
|
62
|
+
return aFrom < bTo && aTo > bFrom;
|
|
63
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Plugin, PluginKey, type Transaction } from "prosemirror-state";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Key used to stamp predicted PM transactions with their `opId`. The gate
|
|
5
|
+
* plugin reads this meta and allows a doc-changing transaction through only
|
|
6
|
+
* when the lane has registered the `opId`.
|
|
7
|
+
*/
|
|
8
|
+
export const PREDICTED_META_KEY = "bounded-local-first/predicted";
|
|
9
|
+
|
|
10
|
+
export interface PredictedMeta {
|
|
11
|
+
opId: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface PredictedTxGateOptions {
|
|
15
|
+
/** The lane's `LocalEditSessionState.isPredicted(opId)` — consulted per tx. */
|
|
16
|
+
isPredicted(opId: string): boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const gateKey = new PluginKey("predicted-tx-gate");
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* PredictedTxGate — replaces the unconditional "block every `docChanged`"
|
|
23
|
+
* filter with one that lets through predicted transactions whose `opId` the
|
|
24
|
+
* lane has registered. Unregistered or unstamped doc-changing transactions
|
|
25
|
+
* are still blocked — the runtime remains the canonical mutation path.
|
|
26
|
+
*
|
|
27
|
+
* Selection-only transactions always pass.
|
|
28
|
+
*/
|
|
29
|
+
export function createPredictedTxGate(options: PredictedTxGateOptions): Plugin {
|
|
30
|
+
return new Plugin({
|
|
31
|
+
key: gateKey,
|
|
32
|
+
filterTransaction(tr: Transaction) {
|
|
33
|
+
if (!tr.docChanged) return true;
|
|
34
|
+
const meta = tr.getMeta(PREDICTED_META_KEY) as PredictedMeta | undefined;
|
|
35
|
+
if (!meta) return false;
|
|
36
|
+
return options.isPredicted(meta.opId);
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
}
|