@beyondwork/docx-react-component 1.0.75 → 1.0.77
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/package.json +1 -1
- package/src/api/v3/ai/resolve.ts +104 -4
- package/src/io/ooxml/parse-bookmark-references.ts +123 -0
- package/src/io/ooxml/parse-footnotes.ts +26 -3
- package/src/io/ooxml/parse-headers-footers.ts +96 -1
- package/src/io/ooxml/parse-main-document.ts +256 -4
- package/src/io/ooxml/parse-shapes.ts +29 -1
- package/src/io/ooxml/table-opaque-preservation.ts +70 -5
- package/src/runtime/scopes/action-validation.ts +39 -12
- package/src/runtime/scopes/index.ts +3 -0
- package/src/runtime/scopes/resolve-reference.ts +99 -43
- package/src/session/import/loader-types.ts +26 -0
- package/src/session/import/loader.ts +12 -2
- package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +44 -5
- package/src/ui-tailwind/editor-surface/perf-probe.ts +3 -0
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +44 -0
- package/src/ui-tailwind/editor-surface/preserve-position.ts +230 -0
- package/src/ui-tailwind/editor-surface/scroll-anchor.ts +8 -5
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +49 -5
|
@@ -1,16 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Reference resolution + position queries.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* (ambiguous), a detached placeholder (scopeId was once valid), or a
|
|
7
|
-
* `not-found`.
|
|
4
|
+
* Two distinct surfaces live in this module now, matching the two
|
|
5
|
+
* distinct semantics (KI-P9 fix):
|
|
8
6
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* `
|
|
7
|
+
* 1. `resolveReference(ref)` — **identity lookup**. Takes a durable
|
|
8
|
+
* `ScopeReference` (`scope-id`, `semantic-path`, or
|
|
9
|
+
* `natural-language` hint) and returns the scope it names. Safe
|
|
10
|
+
* across mutations: `scope-id` survives text replaces; missing
|
|
11
|
+
* references return typed `not-found` / `detached`. Callers that
|
|
12
|
+
* hold a `ScopeReference` across a mutation window should only ever
|
|
13
|
+
* be holding one of these durable kinds.
|
|
14
|
+
*
|
|
15
|
+
* 2. `queryScopeAtPosition(at)` / `queryScopeInRange(from, to)` —
|
|
16
|
+
* **one-shot positional queries**. Return a `ScopeHandle | null`
|
|
17
|
+
* against the *current* document state. The caller's next use of
|
|
18
|
+
* the returned handle MUST be through its `scopeId` — the position
|
|
19
|
+
* is consumed by the query and never round-trips as a reference.
|
|
20
|
+
* This is the click-to-scope / hit-test / selection-to-scope
|
|
21
|
+
* surface. Positions that lived at time T1 are meaningless at time
|
|
22
|
+
* T2 after any intervening mutation, so these functions refuse to
|
|
23
|
+
* be part of the stored-reference vocabulary.
|
|
24
|
+
*
|
|
25
|
+
* Before 2026-04-24, `ScopeReference` had `offset` and `range` variants
|
|
26
|
+
* that flowed through `resolveReference` with the same
|
|
27
|
+
* `{status:"resolved", handle, confidence}` shape as identity lookups.
|
|
28
|
+
* That conflation is the KI-P9 trap — documented in
|
|
29
|
+
* `docs/testing/scopes.md §6c` and `KNOWN-ISSUES.md` KI-P9. Removed.
|
|
30
|
+
*
|
|
31
|
+
* Natural-language hints continue to resolve through `resolveReference`
|
|
32
|
+
* with `confidence: "low"`; richer NL matching is a later slice.
|
|
14
33
|
*/
|
|
15
34
|
|
|
16
35
|
import type { CanonicalDocument } from "../../model/canonical-document.ts";
|
|
@@ -23,11 +42,28 @@ import { buildScopePositionMap, type ScopePositionRange } from "./position-map.t
|
|
|
23
42
|
import { resolveScopeRange, scopeSpecificity } from "./scope-range.ts";
|
|
24
43
|
import type { ScopeHandle } from "./semantic-scope-types.ts";
|
|
25
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Durable references to a scope. Each kind is safe to cache / store /
|
|
47
|
+
* round-trip through a tool-call boundary and re-resolve after
|
|
48
|
+
* arbitrary mutations:
|
|
49
|
+
*
|
|
50
|
+
* - `scope-id` — identity primitive. Preserved through text edits;
|
|
51
|
+
* returns `not-found` / `detached` cleanly on delete.
|
|
52
|
+
* - `semantic-path` — structural path (`body/paragraph/5`). Stable
|
|
53
|
+
* when block structure doesn't change; returns `not-found` when
|
|
54
|
+
* the path no longer resolves. Fragile on KI-001-class fixtures
|
|
55
|
+
* where a neighbour block drop shifts all later indices by 1.
|
|
56
|
+
* - `natural-language` — substring heuristic. Always
|
|
57
|
+
* `confidence: "low"`; callers must surface that to humans.
|
|
58
|
+
*
|
|
59
|
+
* Positional references (`offset`, `range`) are **not** part of this
|
|
60
|
+
* union — see `queryScopeAtPosition` / `queryScopeInRange` for those.
|
|
61
|
+
* They are one-shot queries that return a `ScopeHandle | null`; the
|
|
62
|
+
* position never becomes a stored reference. Cf. KI-P9.
|
|
63
|
+
*/
|
|
26
64
|
export type ScopeReference =
|
|
27
65
|
| { readonly kind: "scope-id"; readonly value: string }
|
|
28
66
|
| { readonly kind: "semantic-path"; readonly path: readonly string[] }
|
|
29
|
-
| { readonly kind: "offset"; readonly at: number }
|
|
30
|
-
| { readonly kind: "range"; readonly from: number; readonly to: number }
|
|
31
67
|
| { readonly kind: "natural-language"; readonly hint: string };
|
|
32
68
|
|
|
33
69
|
export type ResolveReferenceResult =
|
|
@@ -161,50 +197,74 @@ function innermostContaining(
|
|
|
161
197
|
return best?.entry ?? null;
|
|
162
198
|
}
|
|
163
199
|
|
|
164
|
-
|
|
200
|
+
/**
|
|
201
|
+
* Inputs for the one-shot positional query functions. Narrower than
|
|
202
|
+
* `ResolveReferenceInputs` because the query functions never need NL
|
|
203
|
+
* overlay labels — they're pure structural lookups.
|
|
204
|
+
*/
|
|
205
|
+
export interface QueryScopePositionInputs {
|
|
206
|
+
readonly document:
|
|
207
|
+
| Pick<CanonicalDocument, "content" | "docId" | "review">
|
|
208
|
+
| CanonicalDocumentEnvelope;
|
|
209
|
+
readonly overlay?: WorkflowOverlay | null;
|
|
210
|
+
readonly scopes?: readonly EnumeratedScope[];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Return the innermost scope whose range contains `at` in the current
|
|
215
|
+
* document state, or `null` if no scope contains the position.
|
|
216
|
+
*
|
|
217
|
+
* **One-shot query, not a stored reference.** The returned handle is
|
|
218
|
+
* the durable object — its `scopeId` may be stored and re-resolved via
|
|
219
|
+
* `resolveReference({kind:"scope-id", value: handle.scopeId})`. The
|
|
220
|
+
* input position must NOT be stored; it is meaningful only against the
|
|
221
|
+
* document state it was queried against. See KI-P9 for the trap this
|
|
222
|
+
* separation closes.
|
|
223
|
+
*
|
|
224
|
+
* Precision: marker-backed handles (workflow / comment / revision
|
|
225
|
+
* scopes anchored by inline `scope_marker_*` nodes) are returned with
|
|
226
|
+
* full confidence; derived handles (paragraph / heading / list-item /
|
|
227
|
+
* field / table etc. enumerated from the canonical tree) are just as
|
|
228
|
+
* authoritative — the confidence distinction that previously lived on
|
|
229
|
+
* the `{status:"resolved", confidence}` return went away with
|
|
230
|
+
* `resolveReference`'s offset/range cases. Callers that need to know
|
|
231
|
+
* whether the hit is marker-backed can read `handle.provenance`.
|
|
232
|
+
*/
|
|
233
|
+
export function queryScopeAtPosition(
|
|
165
234
|
at: number,
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
235
|
+
inputs: QueryScopePositionInputs,
|
|
236
|
+
): ScopeHandle | null {
|
|
237
|
+
const scopes = scopesFor({ ...inputs, overlay: inputs.overlay ?? null });
|
|
238
|
+
const positionMap = buildScopePositionMap(inputs.document);
|
|
169
239
|
const hit = innermostContaining(
|
|
170
240
|
scopes,
|
|
171
241
|
positionMap,
|
|
172
242
|
(range) => range.from <= at && at <= range.to,
|
|
173
243
|
);
|
|
174
|
-
|
|
175
|
-
return { status: "not-found", reason: `no scope contains offset ${at}` };
|
|
176
|
-
}
|
|
177
|
-
return {
|
|
178
|
-
status: "resolved",
|
|
179
|
-
handle: hit.handle,
|
|
180
|
-
confidence: hit.handle.provenance === "marker-backed" ? "high" : "medium",
|
|
181
|
-
};
|
|
244
|
+
return hit?.handle ?? null;
|
|
182
245
|
}
|
|
183
246
|
|
|
184
|
-
|
|
247
|
+
/**
|
|
248
|
+
* Return the innermost scope that fully contains the range `[from, to]`
|
|
249
|
+
* in the current document state, or `null` if no single scope contains
|
|
250
|
+
* the whole range. Same one-shot-query semantics as
|
|
251
|
+
* `queryScopeAtPosition`.
|
|
252
|
+
*/
|
|
253
|
+
export function queryScopeInRange(
|
|
185
254
|
from: number,
|
|
186
255
|
to: number,
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
): ResolveReferenceResult {
|
|
256
|
+
inputs: QueryScopePositionInputs,
|
|
257
|
+
): ScopeHandle | null {
|
|
190
258
|
const low = Math.min(from, to);
|
|
191
259
|
const high = Math.max(from, to);
|
|
260
|
+
const scopes = scopesFor({ ...inputs, overlay: inputs.overlay ?? null });
|
|
261
|
+
const positionMap = buildScopePositionMap(inputs.document);
|
|
192
262
|
const hit = innermostContaining(
|
|
193
263
|
scopes,
|
|
194
264
|
positionMap,
|
|
195
265
|
(range) => range.from <= low && high <= range.to,
|
|
196
266
|
);
|
|
197
|
-
|
|
198
|
-
return {
|
|
199
|
-
status: "not-found",
|
|
200
|
-
reason: `no scope fully contains range [${low}, ${high}]`,
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
return {
|
|
204
|
-
status: "resolved",
|
|
205
|
-
handle: hit.handle,
|
|
206
|
-
confidence: hit.handle.provenance === "marker-backed" ? "high" : "medium",
|
|
207
|
-
};
|
|
267
|
+
return hit?.handle ?? null;
|
|
208
268
|
}
|
|
209
269
|
|
|
210
270
|
/**
|
|
@@ -329,10 +389,6 @@ export function resolveReference(
|
|
|
329
389
|
return resolveByScopeId(reference.value, scopes, inputs.overlay, positionMap);
|
|
330
390
|
case "semantic-path":
|
|
331
391
|
return resolveBySemanticPath(reference.path, scopes);
|
|
332
|
-
case "offset":
|
|
333
|
-
return resolveByOffset(reference.at, scopes, positionMap);
|
|
334
|
-
case "range":
|
|
335
|
-
return resolveByRange(reference.from, reference.to, scopes, positionMap);
|
|
336
392
|
case "natural-language":
|
|
337
393
|
return resolveByNaturalLanguage(
|
|
338
394
|
reference.hint,
|
|
@@ -137,6 +137,32 @@ export interface LoadDocxEditorSessionOptions {
|
|
|
137
137
|
* inspect every marker.
|
|
138
138
|
*/
|
|
139
139
|
stripCosmeticMarkers?: boolean;
|
|
140
|
+
/**
|
|
141
|
+
* Phase 2 bookmark-strip allowlist. When `stripCosmeticMarkers` is
|
|
142
|
+
* `true` (the default), the parser's reference scan retains
|
|
143
|
+
* bookmarks whose name is referenced by a `<w:hyperlink w:anchor>`
|
|
144
|
+
* or `<w:instrText>` (REF / PAGEREF / NOTEREF / TOC) AND any name
|
|
145
|
+
* listed here. Use this when the host depends on a stable
|
|
146
|
+
* host-authored bookmark name (e.g. `placeholder_party_name`,
|
|
147
|
+
* `signature_block_2`) that the parser's automatic scan cannot
|
|
148
|
+
* infer is load-bearing.
|
|
149
|
+
*
|
|
150
|
+
* Default: `[]` (only the automatic scan retains names).
|
|
151
|
+
*
|
|
152
|
+
* Always-retained, regardless of this list:
|
|
153
|
+
* - `_Toc*` (when any TOC field exists in the document)
|
|
154
|
+
* - `_Ref*` and any other name explicitly cited by a hyperlink
|
|
155
|
+
* anchor or REF/PAGEREF/NOTEREF instruction
|
|
156
|
+
* - `bw:scope:*` (workflow scope markers — converted to
|
|
157
|
+
* first-class scope markers by the parser before strip runs)
|
|
158
|
+
* - everything (defensive blanket-retain) when the document
|
|
159
|
+
* contains a `<w:dataBinding>` whose xpath could reference
|
|
160
|
+
* bookmarks via paths the scanner cannot statically analyze
|
|
161
|
+
*
|
|
162
|
+
* See `services/debug/docs/phase-2-bookmark-strip-audit-2026-04-24.md`
|
|
163
|
+
* for the corpus categorization that informed this contract.
|
|
164
|
+
*/
|
|
165
|
+
retainedBookmarkNames?: ReadonlyArray<string>;
|
|
140
166
|
}
|
|
141
167
|
|
|
142
168
|
/**
|
|
@@ -540,7 +540,12 @@ export async function loadDocxSessionAsync(
|
|
|
540
540
|
mediaParts,
|
|
541
541
|
mainDocumentPath,
|
|
542
542
|
chartPartLookup,
|
|
543
|
-
{
|
|
543
|
+
{
|
|
544
|
+
stripCosmeticMarkers: options.stripCosmeticMarkers !== false,
|
|
545
|
+
...(options.retainedBookmarkNames !== undefined
|
|
546
|
+
? { retainedBookmarkNames: options.retainedBookmarkNames }
|
|
547
|
+
: {}),
|
|
548
|
+
},
|
|
544
549
|
);
|
|
545
550
|
} finally {
|
|
546
551
|
if (options.telemetryBus) setActiveParseTelemetryBus(undefined);
|
|
@@ -1313,7 +1318,12 @@ export function loadDocxSessionSync(
|
|
|
1313
1318
|
mediaParts,
|
|
1314
1319
|
mainDocumentPath,
|
|
1315
1320
|
chartPartLookup,
|
|
1316
|
-
{
|
|
1321
|
+
{
|
|
1322
|
+
stripCosmeticMarkers: options.stripCosmeticMarkers !== false,
|
|
1323
|
+
...(options.retainedBookmarkNames !== undefined
|
|
1324
|
+
? { retainedBookmarkNames: options.retainedBookmarkNames }
|
|
1325
|
+
: {}),
|
|
1326
|
+
},
|
|
1317
1327
|
);
|
|
1318
1328
|
} finally {
|
|
1319
1329
|
if (options.telemetryBus) setActiveParseTelemetryBus(undefined);
|
|
@@ -123,7 +123,7 @@ export function createFastTextEditLane(
|
|
|
123
123
|
const fromRuntime = positionMap.pmToRuntime(fromPm);
|
|
124
124
|
const toRuntime = positionMap.pmToRuntime(toPm);
|
|
125
125
|
|
|
126
|
-
pushLaneDebug({
|
|
126
|
+
const debugEntry = pushLaneDebug({
|
|
127
127
|
opId,
|
|
128
128
|
intent: intent.kind,
|
|
129
129
|
pmFrom: fromPm,
|
|
@@ -137,6 +137,7 @@ export function createFastTextEditLane(
|
|
|
137
137
|
if (options.shouldBailBeforePredict?.(intent, fromRuntime, toRuntime)) {
|
|
138
138
|
const ack = options.dispatchRuntimeCommand(toRuntimeCommand(intent, opId));
|
|
139
139
|
incrementInvalidationCounter(PREDICTED_LANE_COUNTERS.bailBeforePredict);
|
|
140
|
+
markLaneDebugReconciled(debugEntry, ack.kind, true);
|
|
140
141
|
options.probe?.markReconciled(opId, ack.kind);
|
|
141
142
|
switch (ack.kind) {
|
|
142
143
|
case "equivalent":
|
|
@@ -183,6 +184,7 @@ export function createFastTextEditLane(
|
|
|
183
184
|
op.predictedSelectionHead = view.state.selection.head;
|
|
184
185
|
|
|
185
186
|
const ack = options.dispatchRuntimeCommand(toRuntimeCommand(intent, opId));
|
|
187
|
+
markLaneDebugReconciled(debugEntry, ack.kind, false);
|
|
186
188
|
options.probe?.markReconciled(opId, ack.kind);
|
|
187
189
|
|
|
188
190
|
switch (ack.kind) {
|
|
@@ -280,6 +282,14 @@ interface LaneDebugEntry {
|
|
|
280
282
|
runtimeStorySize: number;
|
|
281
283
|
fromRuntime: number;
|
|
282
284
|
toRuntime: number;
|
|
285
|
+
/** Dispatch → reconcile observation. Filled by `markLaneDebugReconciled`. */
|
|
286
|
+
ackKind?: TextCommandAck["kind"];
|
|
287
|
+
/** Wall-clock ms between `pushLaneDebug` and `markLaneDebugReconciled`. */
|
|
288
|
+
reconcileMs?: number;
|
|
289
|
+
/** Whether the lane short-circuited to dispatch-only (no predicted TX). */
|
|
290
|
+
bailed?: boolean;
|
|
291
|
+
/** Wall-clock timestamp at push time — used to compute reconcileMs. */
|
|
292
|
+
startedAtMs: number;
|
|
283
293
|
}
|
|
284
294
|
|
|
285
295
|
declare global {
|
|
@@ -294,19 +304,48 @@ declare global {
|
|
|
294
304
|
* buffer is capped at 200 entries; consumers can read it from the browser
|
|
295
305
|
* console to diagnose cursor position mismatches between PM and the runtime.
|
|
296
306
|
*
|
|
307
|
+
* Returns the pushed entry (or `null` when the buffer isn't enabled) so
|
|
308
|
+
* the caller can mutate it in place with ack-kind + reconcile timing
|
|
309
|
+
* via `markLaneDebugReconciled` once the runtime dispatch returns.
|
|
310
|
+
*
|
|
297
311
|
* To enable in the browser console:
|
|
298
312
|
* window.__DOCX_LANE_DEBUG__ = [];
|
|
299
313
|
* Then type, then:
|
|
300
314
|
* JSON.stringify(window.__DOCX_LANE_DEBUG__, null, 2)
|
|
301
315
|
*/
|
|
302
|
-
function pushLaneDebug(
|
|
303
|
-
|
|
316
|
+
function pushLaneDebug(
|
|
317
|
+
entry: Omit<LaneDebugEntry, "startedAtMs">,
|
|
318
|
+
): LaneDebugEntry | null {
|
|
319
|
+
if (typeof window === "undefined") return null;
|
|
304
320
|
const buffer = window.__DOCX_LANE_DEBUG__;
|
|
305
|
-
if (!Array.isArray(buffer)) return;
|
|
306
|
-
|
|
321
|
+
if (!Array.isArray(buffer)) return null;
|
|
322
|
+
const full: LaneDebugEntry = {
|
|
323
|
+
...entry,
|
|
324
|
+
startedAtMs:
|
|
325
|
+
typeof performance !== "undefined" && typeof performance.now === "function"
|
|
326
|
+
? performance.now()
|
|
327
|
+
: Date.now(),
|
|
328
|
+
};
|
|
329
|
+
buffer.push(full);
|
|
307
330
|
if (buffer.length > 200) {
|
|
308
331
|
buffer.splice(0, buffer.length - 200);
|
|
309
332
|
}
|
|
333
|
+
return full;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function markLaneDebugReconciled(
|
|
337
|
+
entry: LaneDebugEntry | null,
|
|
338
|
+
ackKind: TextCommandAck["kind"],
|
|
339
|
+
bailed: boolean,
|
|
340
|
+
): void {
|
|
341
|
+
if (!entry) return;
|
|
342
|
+
entry.ackKind = ackKind;
|
|
343
|
+
entry.bailed = bailed;
|
|
344
|
+
const now =
|
|
345
|
+
typeof performance !== "undefined" && typeof performance.now === "function"
|
|
346
|
+
? performance.now()
|
|
347
|
+
: Date.now();
|
|
348
|
+
entry.reconcileMs = now - entry.startedAtMs;
|
|
310
349
|
}
|
|
311
350
|
|
|
312
351
|
function buildTxCompat(
|
|
@@ -20,9 +20,26 @@ import type {
|
|
|
20
20
|
WorkflowScope,
|
|
21
21
|
} from "../../api/public-types";
|
|
22
22
|
import { MAIN_STORY_TARGET, storyTargetsEqual } from "../../api/public-types.ts";
|
|
23
|
+
import {
|
|
24
|
+
incrementInvalidationCounter,
|
|
25
|
+
recordPerfSample,
|
|
26
|
+
} from "./perf-probe";
|
|
23
27
|
import type { PositionMap } from "./pm-position-map";
|
|
24
28
|
import type { Node as PMNode } from "prosemirror-model";
|
|
25
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Cheap wall-clock delta helper for sub-section probes. `performance.now`
|
|
32
|
+
* is always defined in the browser surface where this runs; the guard
|
|
33
|
+
* keeps headless Node contexts (tests) from throwing when the global
|
|
34
|
+
* isn't polyfilled.
|
|
35
|
+
*/
|
|
36
|
+
function nowMs(): number {
|
|
37
|
+
return typeof performance !== "undefined" &&
|
|
38
|
+
typeof performance.now === "function"
|
|
39
|
+
? performance.now()
|
|
40
|
+
: Date.now();
|
|
41
|
+
}
|
|
42
|
+
|
|
26
43
|
type RailDecorationSpec = {
|
|
27
44
|
railKind: "scope" | "candidate" | "blocked";
|
|
28
45
|
className: string;
|
|
@@ -394,6 +411,8 @@ export function buildDecorations(
|
|
|
394
411
|
const lockedPmRanges = collectLockedPmRanges(workflowLockedZones, activeStory, positionMap);
|
|
395
412
|
|
|
396
413
|
// Walk comment threads and create inline decorations
|
|
414
|
+
const commentsStartMs = nowMs();
|
|
415
|
+
let commentCount = 0;
|
|
397
416
|
if (commentModel) {
|
|
398
417
|
for (const thread of commentModel.threads) {
|
|
399
418
|
const cls = getCommentHighlightClass(
|
|
@@ -413,11 +432,18 @@ export function buildDecorations(
|
|
|
413
432
|
"data-comment-id": thread.commentId,
|
|
414
433
|
}),
|
|
415
434
|
);
|
|
435
|
+
commentCount += 1;
|
|
416
436
|
}
|
|
417
437
|
}
|
|
418
438
|
}
|
|
439
|
+
recordPerfSample("pm.decorations.comments", nowMs() - commentsStartMs);
|
|
440
|
+
if (commentCount > 0) {
|
|
441
|
+
incrementInvalidationCounter("pm.decorations.comments.count", commentCount);
|
|
442
|
+
}
|
|
419
443
|
|
|
420
444
|
// Walk revision entries and create inline decorations.
|
|
445
|
+
const revisionsStartMs = nowMs();
|
|
446
|
+
let revisionCount = 0;
|
|
421
447
|
// Deletion hiding in clean mode ALWAYS applies, even when showTrackedChanges is off.
|
|
422
448
|
// Visual styling (underlines, colors) only applies when showTrackedChanges is on.
|
|
423
449
|
if (revisionModel) {
|
|
@@ -442,6 +468,7 @@ export function buildDecorations(
|
|
|
442
468
|
"data-revision-id": rev.revisionId,
|
|
443
469
|
}),
|
|
444
470
|
);
|
|
471
|
+
revisionCount += 1;
|
|
445
472
|
}
|
|
446
473
|
continue;
|
|
447
474
|
}
|
|
@@ -477,6 +504,7 @@ export function buildDecorations(
|
|
|
477
504
|
return el;
|
|
478
505
|
}, { side: 1, key: `${rev.revisionId}-close` }),
|
|
479
506
|
);
|
|
507
|
+
revisionCount += 1;
|
|
480
508
|
} else if (rev.kind === "deletion") {
|
|
481
509
|
decorations.push(
|
|
482
510
|
Decoration.inline(pmFrom, pmTo, {
|
|
@@ -484,6 +512,7 @@ export function buildDecorations(
|
|
|
484
512
|
"data-revision-id": rev.revisionId,
|
|
485
513
|
}),
|
|
486
514
|
);
|
|
515
|
+
revisionCount += 1;
|
|
487
516
|
}
|
|
488
517
|
continue;
|
|
489
518
|
}
|
|
@@ -511,9 +540,16 @@ export function buildDecorations(
|
|
|
511
540
|
"data-revision-id": rev.revisionId,
|
|
512
541
|
}),
|
|
513
542
|
);
|
|
543
|
+
revisionCount += 1;
|
|
514
544
|
}
|
|
515
545
|
}
|
|
546
|
+
recordPerfSample("pm.decorations.revisions", nowMs() - revisionsStartMs);
|
|
547
|
+
if (revisionCount > 0) {
|
|
548
|
+
incrementInvalidationCounter("pm.decorations.revisions.count", revisionCount);
|
|
549
|
+
}
|
|
516
550
|
|
|
551
|
+
const workflowStartMs = nowMs();
|
|
552
|
+
const workflowDecorationsBefore = decorations.length;
|
|
517
553
|
if (effectiveWorkflowScopes.length > 0) {
|
|
518
554
|
for (const scope of effectiveWorkflowScopes) {
|
|
519
555
|
const scopeStoryTarget = scope.storyTarget ?? MAIN_STORY_TARGET;
|
|
@@ -655,6 +691,14 @@ export function buildDecorations(
|
|
|
655
691
|
}, railRangeCache);
|
|
656
692
|
}
|
|
657
693
|
}
|
|
694
|
+
recordPerfSample("pm.decorations.workflow", nowMs() - workflowStartMs);
|
|
695
|
+
const workflowDecorationCount = decorations.length - workflowDecorationsBefore;
|
|
696
|
+
if (workflowDecorationCount > 0) {
|
|
697
|
+
incrementInvalidationCounter(
|
|
698
|
+
"pm.decorations.workflow.count",
|
|
699
|
+
workflowDecorationCount,
|
|
700
|
+
);
|
|
701
|
+
}
|
|
658
702
|
|
|
659
703
|
return DecorationSet.create(doc, decorations);
|
|
660
704
|
}
|