@beyondwork/docx-react-component 1.0.19 → 1.0.21
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 +44 -25
- package/src/api/public-types.ts +336 -0
- package/src/api/session-state.ts +2 -0
- package/src/core/commands/formatting-commands.ts +1 -1
- package/src/core/commands/index.ts +14 -2
- package/src/core/search/search-text.ts +28 -0
- package/src/core/state/editor-state.ts +3 -0
- package/src/index.ts +21 -0
- package/src/io/docx-session.ts +363 -17
- package/src/io/export/serialize-comments.ts +104 -34
- package/src/io/export/serialize-footnotes.ts +198 -1
- package/src/io/export/serialize-headers-footers.ts +203 -10
- package/src/io/export/serialize-main-document.ts +83 -3
- package/src/io/export/split-review-boundaries.ts +181 -19
- package/src/io/normalize/normalize-text.ts +82 -8
- package/src/io/ooxml/highlight-colors.ts +39 -0
- package/src/io/ooxml/parse-comments.ts +85 -19
- package/src/io/ooxml/parse-fields.ts +396 -0
- package/src/io/ooxml/parse-footnotes.ts +240 -2
- package/src/io/ooxml/parse-headers-footers.ts +431 -7
- package/src/io/ooxml/parse-inline-media.ts +15 -1
- package/src/io/ooxml/parse-main-document.ts +396 -14
- package/src/io/ooxml/parse-revisions.ts +317 -38
- package/src/legal/bookmarks.ts +44 -0
- package/src/legal/cross-references.ts +59 -1
- package/src/model/canonical-document.ts +117 -1
- package/src/model/snapshot.ts +85 -1
- package/src/review/store/revision-store.ts +6 -0
- package/src/review/store/revision-types.ts +1 -0
- package/src/runtime/document-navigation.ts +52 -13
- package/src/runtime/document-runtime.ts +1521 -75
- package/src/runtime/read-only-diagnostics-runtime.ts +8 -0
- package/src/runtime/session-capabilities.ts +33 -3
- package/src/runtime/surface-projection.ts +86 -25
- package/src/runtime/table-schema.ts +2 -2
- package/src/runtime/view-state.ts +24 -6
- package/src/runtime/workflow-markup.ts +349 -0
- package/src/ui/WordReviewEditor.tsx +915 -1314
- package/src/ui/editor-command-bag.ts +120 -0
- package/src/ui/editor-runtime-boundary.ts +1448 -0
- package/src/ui/editor-shell-view.tsx +134 -0
- package/src/ui/editor-surface-controller.tsx +55 -0
- package/src/ui/headless/revision-decoration-model.ts +4 -4
- package/src/ui/runtime-snapshot-selectors.ts +197 -0
- package/src/ui/workflow-surface-blocked-rails.ts +94 -0
- package/src/ui-tailwind/chrome/tw-alert-banner.tsx +18 -2
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +129 -0
- package/src/ui-tailwind/chrome/tw-layout-panel.tsx +114 -0
- package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +34 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +27 -2
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +128 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +86 -14
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +2 -2
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +237 -0
- package/src/ui-tailwind/editor-surface/pm-position-map.ts +1 -1
- package/src/ui-tailwind/editor-surface/pm-schema.ts +139 -8
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +98 -48
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +55 -0
- package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +7 -1
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +190 -48
- package/src/ui-tailwind/page-chrome-model.ts +27 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +7 -7
- package/src/ui-tailwind/review/tw-health-panel.tsx +31 -2
- package/src/ui-tailwind/review/tw-review-rail.tsx +3 -3
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +15 -15
- package/src/ui-tailwind/theme/editor-theme.css +130 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +543 -5
- package/src/ui-tailwind/tw-review-workspace.tsx +316 -19
- package/src/validation/compatibility-engine.ts +27 -4
- package/src/validation/compatibility-report.ts +1 -0
- package/src/validation/docx-comment-proof.ts +220 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
PageLayoutSnapshot,
|
|
5
|
+
SectionPageNumberingPatch,
|
|
6
|
+
SectionBreakType,
|
|
7
|
+
SectionLayoutPatch,
|
|
8
|
+
} from "../../api/public-types";
|
|
9
|
+
import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection";
|
|
10
|
+
|
|
11
|
+
export interface TwLayoutPanelProps {
|
|
12
|
+
pageLayout: PageLayoutSnapshot;
|
|
13
|
+
readOnly: boolean;
|
|
14
|
+
onInsertSectionBreak?: (type: SectionBreakType) => void;
|
|
15
|
+
onDeleteSectionBreak?: (sectionIndex: number) => void;
|
|
16
|
+
onUpdateSectionLayout?: (sectionIndex: number, patch: SectionLayoutPatch) => void;
|
|
17
|
+
onSetSectionPageNumbering?: (
|
|
18
|
+
sectionIndex: number,
|
|
19
|
+
patch: SectionPageNumberingPatch | null,
|
|
20
|
+
) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function TwLayoutPanel(props: TwLayoutPanelProps) {
|
|
24
|
+
const nextOrientation = props.pageLayout.orientation === "portrait" ? "landscape" : "portrait";
|
|
25
|
+
const titlePageEnabled = props.pageLayout.differentFirstPage;
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="mt-3 flex flex-wrap items-center gap-2 rounded-xl border border-border bg-canvas px-3 py-2 shadow-sm">
|
|
29
|
+
<span className="text-[10px] font-semibold uppercase tracking-[0.12em] text-tertiary">
|
|
30
|
+
Section
|
|
31
|
+
</span>
|
|
32
|
+
<ToolbarButton
|
|
33
|
+
ariaLabel="Insert next-page section break"
|
|
34
|
+
disabled={props.readOnly || !props.onInsertSectionBreak}
|
|
35
|
+
onClick={() => props.onInsertSectionBreak?.("nextPage")}
|
|
36
|
+
>
|
|
37
|
+
Next-page break
|
|
38
|
+
</ToolbarButton>
|
|
39
|
+
<ToolbarButton
|
|
40
|
+
ariaLabel={`Switch section to ${nextOrientation}`}
|
|
41
|
+
disabled={props.readOnly || !props.onUpdateSectionLayout}
|
|
42
|
+
onClick={() =>
|
|
43
|
+
props.onUpdateSectionLayout?.(props.pageLayout.sectionIndex, {
|
|
44
|
+
pageSize: {
|
|
45
|
+
orientation: nextOrientation,
|
|
46
|
+
width: props.pageLayout.pageHeight,
|
|
47
|
+
height: props.pageLayout.pageWidth,
|
|
48
|
+
},
|
|
49
|
+
})}
|
|
50
|
+
>
|
|
51
|
+
{nextOrientation === "landscape" ? "Landscape" : "Portrait"}
|
|
52
|
+
</ToolbarButton>
|
|
53
|
+
<ToolbarButton
|
|
54
|
+
ariaLabel="Delete current section break"
|
|
55
|
+
disabled={props.readOnly || props.pageLayout.sectionIndex === 0 || !props.onDeleteSectionBreak}
|
|
56
|
+
onClick={() => props.onDeleteSectionBreak?.(props.pageLayout.sectionIndex)}
|
|
57
|
+
>
|
|
58
|
+
Delete break
|
|
59
|
+
</ToolbarButton>
|
|
60
|
+
<ToolbarButton
|
|
61
|
+
ariaLabel="Restart page numbering at 1"
|
|
62
|
+
disabled={props.readOnly || !props.onSetSectionPageNumbering}
|
|
63
|
+
onClick={() =>
|
|
64
|
+
props.onSetSectionPageNumbering?.(props.pageLayout.sectionIndex, {
|
|
65
|
+
...(props.pageLayout.pageNumbering ?? {}),
|
|
66
|
+
start: 1,
|
|
67
|
+
})}
|
|
68
|
+
>
|
|
69
|
+
Restart numbering
|
|
70
|
+
</ToolbarButton>
|
|
71
|
+
<ToolbarButton
|
|
72
|
+
ariaLabel="Use roman page numbering"
|
|
73
|
+
disabled={props.readOnly || !props.onSetSectionPageNumbering}
|
|
74
|
+
onClick={() =>
|
|
75
|
+
props.onSetSectionPageNumbering?.(props.pageLayout.sectionIndex, {
|
|
76
|
+
...(props.pageLayout.pageNumbering ?? {}),
|
|
77
|
+
format: "roman",
|
|
78
|
+
})}
|
|
79
|
+
>
|
|
80
|
+
Roman numerals
|
|
81
|
+
</ToolbarButton>
|
|
82
|
+
<ToolbarButton
|
|
83
|
+
ariaLabel="Toggle different first page"
|
|
84
|
+
disabled={props.readOnly || !props.onUpdateSectionLayout}
|
|
85
|
+
onClick={() =>
|
|
86
|
+
props.onUpdateSectionLayout?.(props.pageLayout.sectionIndex, {
|
|
87
|
+
titlePage: !titlePageEnabled,
|
|
88
|
+
})}
|
|
89
|
+
>
|
|
90
|
+
{titlePageEnabled ? "Same first page" : "Different first page"}
|
|
91
|
+
</ToolbarButton>
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function ToolbarButton(props: {
|
|
97
|
+
ariaLabel: string;
|
|
98
|
+
children: React.ReactNode;
|
|
99
|
+
disabled: boolean;
|
|
100
|
+
onClick?: () => void;
|
|
101
|
+
}) {
|
|
102
|
+
return (
|
|
103
|
+
<button
|
|
104
|
+
type="button"
|
|
105
|
+
aria-label={props.ariaLabel}
|
|
106
|
+
disabled={props.disabled}
|
|
107
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
108
|
+
onClick={props.onClick}
|
|
109
|
+
className="inline-flex h-8 items-center rounded-md px-2 text-xs font-medium text-primary transition-colors hover:bg-surface disabled:cursor-not-allowed disabled:opacity-40"
|
|
110
|
+
>
|
|
111
|
+
{props.children}
|
|
112
|
+
</button>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export interface ActiveObjectContext {
|
|
4
|
+
kind: "textbox" | "shape";
|
|
5
|
+
display: "inline" | "floating";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface TwObjectContextToolbarProps {
|
|
9
|
+
activeObject: ActiveObjectContext;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function TwObjectContextToolbar(props: TwObjectContextToolbarProps) {
|
|
13
|
+
const label = props.activeObject.kind === "textbox" ? "Text box" : "Shape";
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div
|
|
17
|
+
data-testid="object-context-toolbar"
|
|
18
|
+
className="flex flex-wrap items-center gap-2 rounded-xl border border-border bg-canvas px-3 py-2 shadow-sm"
|
|
19
|
+
>
|
|
20
|
+
<span className="text-[10px] font-semibold uppercase tracking-[0.12em] text-tertiary">
|
|
21
|
+
Object
|
|
22
|
+
</span>
|
|
23
|
+
<span className="rounded-full bg-surface px-2 py-1 text-[10px] font-medium uppercase tracking-[0.1em] text-secondary">
|
|
24
|
+
{label}
|
|
25
|
+
</span>
|
|
26
|
+
<span className="rounded-full bg-surface px-2 py-1 text-[10px] font-medium uppercase tracking-[0.1em] text-secondary">
|
|
27
|
+
{props.activeObject.display}
|
|
28
|
+
</span>
|
|
29
|
+
<span className="text-xs text-secondary">
|
|
30
|
+
Object selection is active.
|
|
31
|
+
</span>
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { forwardRef } from "react";
|
|
2
2
|
import type { FocusEventHandler } from "react";
|
|
3
3
|
import * as Tooltip from "@radix-ui/react-tooltip";
|
|
4
|
-
import { Bold, Italic, MessageSquare, Underline } from "lucide-react";
|
|
4
|
+
import { Baseline, Bold, Highlighter, Italic, MessageSquare, Underline } from "lucide-react";
|
|
5
5
|
|
|
6
6
|
import type { SelectionToolbarModel } from "../../ui/headless/selection-toolbar-model";
|
|
7
7
|
import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection";
|
|
@@ -14,6 +14,8 @@ export interface TwSelectionToolbarProps {
|
|
|
14
14
|
onToggleBold?: () => void;
|
|
15
15
|
onToggleItalic?: () => void;
|
|
16
16
|
onToggleUnderline?: () => void;
|
|
17
|
+
onSetTextColor?: (color: string) => void;
|
|
18
|
+
onSetHighlightColor?: (color: string | null) => void;
|
|
17
19
|
onAddComment?: () => void;
|
|
18
20
|
}
|
|
19
21
|
|
|
@@ -60,6 +62,20 @@ export const TwSelectionToolbar = forwardRef<HTMLDivElement, TwSelectionToolbarP
|
|
|
60
62
|
disabled={formattingDisabled}
|
|
61
63
|
onClick={props.onToggleUnderline}
|
|
62
64
|
/>
|
|
65
|
+
<ToolbarActionButton
|
|
66
|
+
icon={<Baseline className="h-3.5 w-3.5" />}
|
|
67
|
+
label="Text color blue"
|
|
68
|
+
pressed={false}
|
|
69
|
+
disabled={formattingDisabled}
|
|
70
|
+
onClick={() => props.onSetTextColor?.("#1660a8")}
|
|
71
|
+
/>
|
|
72
|
+
<ToolbarActionButton
|
|
73
|
+
icon={<Highlighter className="h-3.5 w-3.5" />}
|
|
74
|
+
label="Highlight yellow"
|
|
75
|
+
pressed={false}
|
|
76
|
+
disabled={formattingDisabled}
|
|
77
|
+
onClick={() => props.onSetHighlightColor?.("#ffff00")}
|
|
78
|
+
/>
|
|
63
79
|
|
|
64
80
|
<div className="mx-0.5 h-4 w-px bg-border" />
|
|
65
81
|
|
|
@@ -86,9 +102,18 @@ export const TwSelectionToolbar = forwardRef<HTMLDivElement, TwSelectionToolbarP
|
|
|
86
102
|
</Tooltip.Portal>
|
|
87
103
|
</Tooltip.Root>
|
|
88
104
|
|
|
89
|
-
{
|
|
105
|
+
{model.previewText ? (
|
|
90
106
|
<>
|
|
91
107
|
<div className="mx-0.5 h-4 w-px bg-border" />
|
|
108
|
+
<span className="max-w-[8rem] truncate text-[11px] text-secondary">
|
|
109
|
+
{model.previewText}
|
|
110
|
+
</span>
|
|
111
|
+
</>
|
|
112
|
+
) : null}
|
|
113
|
+
|
|
114
|
+
{contextLabel ? (
|
|
115
|
+
<>
|
|
116
|
+
{!model.previewText ? <div className="mx-0.5 h-4 w-px bg-border" /> : null}
|
|
92
117
|
<span
|
|
93
118
|
className={`min-w-0 max-w-[11rem] truncate rounded-full px-2 py-0.5 text-[10px] font-medium tracking-[0.08em] ${
|
|
94
119
|
model.badges.some((badge) => badge.tone === "accent")
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import type { StyleCatalogSnapshot } from "../../api/public-types";
|
|
4
|
+
import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection";
|
|
5
|
+
|
|
6
|
+
export interface TwTableContextToolbarProps {
|
|
7
|
+
disabled: boolean;
|
|
8
|
+
tableStyles: StyleCatalogSnapshot["tables"];
|
|
9
|
+
onSetTableStyle?: (styleId: string) => void;
|
|
10
|
+
onAddRowBefore?: () => void;
|
|
11
|
+
onAddRowAfter?: () => void;
|
|
12
|
+
onAddColumnBefore?: () => void;
|
|
13
|
+
onAddColumnAfter?: () => void;
|
|
14
|
+
onDeleteRow?: () => void;
|
|
15
|
+
onDeleteColumn?: () => void;
|
|
16
|
+
onMergeCells?: () => void;
|
|
17
|
+
onSplitCell?: () => void;
|
|
18
|
+
onSetCellBackground?: (color: string) => void;
|
|
19
|
+
onDeleteTable?: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const CELL_COLORS = [
|
|
23
|
+
"#ffffff",
|
|
24
|
+
"#f0f0ee",
|
|
25
|
+
"#dbeafe",
|
|
26
|
+
"#fef3c7",
|
|
27
|
+
"#dcfce7",
|
|
28
|
+
"#fce7f3",
|
|
29
|
+
] as const;
|
|
30
|
+
|
|
31
|
+
export function TwTableContextToolbar(props: TwTableContextToolbarProps) {
|
|
32
|
+
return (
|
|
33
|
+
<div
|
|
34
|
+
data-testid="table-context-toolbar"
|
|
35
|
+
className="flex flex-wrap items-center gap-2 rounded-xl border border-border bg-canvas px-3 py-2 shadow-sm"
|
|
36
|
+
>
|
|
37
|
+
<span className="text-[10px] font-semibold uppercase tracking-[0.12em] text-tertiary">
|
|
38
|
+
Table
|
|
39
|
+
</span>
|
|
40
|
+
|
|
41
|
+
<select
|
|
42
|
+
aria-label="Table style"
|
|
43
|
+
className="h-8 rounded-md border border-border bg-canvas px-2 text-xs text-primary disabled:opacity-40"
|
|
44
|
+
disabled={props.disabled || props.tableStyles.length === 0 || !props.onSetTableStyle}
|
|
45
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
46
|
+
onChange={(event) => props.onSetTableStyle?.(event.target.value)}
|
|
47
|
+
defaultValue=""
|
|
48
|
+
>
|
|
49
|
+
<option value="" disabled>Table style</option>
|
|
50
|
+
{props.tableStyles.map((style) => (
|
|
51
|
+
<option key={style.styleId} value={style.styleId}>
|
|
52
|
+
{style.displayName}
|
|
53
|
+
</option>
|
|
54
|
+
))}
|
|
55
|
+
</select>
|
|
56
|
+
|
|
57
|
+
<ToolbarButton ariaLabel="Add row above" disabled={props.disabled} onClick={props.onAddRowBefore}>
|
|
58
|
+
Row above
|
|
59
|
+
</ToolbarButton>
|
|
60
|
+
<ToolbarButton ariaLabel="Add row below" disabled={props.disabled} onClick={props.onAddRowAfter}>
|
|
61
|
+
Row below
|
|
62
|
+
</ToolbarButton>
|
|
63
|
+
<ToolbarButton ariaLabel="Delete row" disabled={props.disabled} onClick={props.onDeleteRow}>
|
|
64
|
+
Delete row
|
|
65
|
+
</ToolbarButton>
|
|
66
|
+
<ToolbarButton ariaLabel="Add column left" disabled={props.disabled} onClick={props.onAddColumnBefore}>
|
|
67
|
+
Column left
|
|
68
|
+
</ToolbarButton>
|
|
69
|
+
<ToolbarButton ariaLabel="Add column right" disabled={props.disabled} onClick={props.onAddColumnAfter}>
|
|
70
|
+
Column right
|
|
71
|
+
</ToolbarButton>
|
|
72
|
+
<ToolbarButton ariaLabel="Delete column" disabled={props.disabled} onClick={props.onDeleteColumn}>
|
|
73
|
+
Delete column
|
|
74
|
+
</ToolbarButton>
|
|
75
|
+
<ToolbarButton ariaLabel="Merge cells" disabled={props.disabled} onClick={props.onMergeCells}>
|
|
76
|
+
Merge
|
|
77
|
+
</ToolbarButton>
|
|
78
|
+
<ToolbarButton ariaLabel="Split cell" disabled={props.disabled} onClick={props.onSplitCell}>
|
|
79
|
+
Split
|
|
80
|
+
</ToolbarButton>
|
|
81
|
+
|
|
82
|
+
<div className="flex items-center gap-1">
|
|
83
|
+
<span className="text-[11px] text-secondary">Fill</span>
|
|
84
|
+
{CELL_COLORS.map((color) => (
|
|
85
|
+
<button
|
|
86
|
+
key={color}
|
|
87
|
+
type="button"
|
|
88
|
+
aria-label={`Set cell fill ${color}`}
|
|
89
|
+
disabled={props.disabled || !props.onSetCellBackground}
|
|
90
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
91
|
+
onClick={() => props.onSetCellBackground?.(color)}
|
|
92
|
+
className="h-6 w-6 rounded border border-border disabled:opacity-40"
|
|
93
|
+
style={{ backgroundColor: color }}
|
|
94
|
+
/>
|
|
95
|
+
))}
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<ToolbarButton ariaLabel="Delete table" danger disabled={props.disabled} onClick={props.onDeleteTable}>
|
|
99
|
+
Delete table
|
|
100
|
+
</ToolbarButton>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function ToolbarButton(props: {
|
|
106
|
+
ariaLabel: string;
|
|
107
|
+
children: React.ReactNode;
|
|
108
|
+
danger?: boolean;
|
|
109
|
+
disabled: boolean;
|
|
110
|
+
onClick?: () => void;
|
|
111
|
+
}) {
|
|
112
|
+
return (
|
|
113
|
+
<button
|
|
114
|
+
type="button"
|
|
115
|
+
aria-label={props.ariaLabel}
|
|
116
|
+
disabled={props.disabled || !props.onClick}
|
|
117
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
118
|
+
onClick={props.onClick}
|
|
119
|
+
className={`inline-flex h-8 items-center rounded-md px-2 text-xs font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-40 ${
|
|
120
|
+
props.danger
|
|
121
|
+
? "text-danger hover:bg-danger/10"
|
|
122
|
+
: "text-primary hover:bg-surface"
|
|
123
|
+
}`}
|
|
124
|
+
>
|
|
125
|
+
{props.children}
|
|
126
|
+
</button>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
@@ -1,4 +1,16 @@
|
|
|
1
|
-
export type PerfProbeKind =
|
|
1
|
+
export type PerfProbeKind =
|
|
2
|
+
| "typing"
|
|
3
|
+
| "selection"
|
|
4
|
+
| "runtime.create"
|
|
5
|
+
| "snapshot.surface"
|
|
6
|
+
| "snapshot.compatibility"
|
|
7
|
+
| "snapshot.navigation"
|
|
8
|
+
| "pm.rebuild"
|
|
9
|
+
| "pm.decorations"
|
|
10
|
+
| "pm.mount"
|
|
11
|
+
| "shell.render"
|
|
12
|
+
| "workspace.chrome"
|
|
13
|
+
| "selection.sync";
|
|
2
14
|
|
|
3
15
|
export interface PerfProbeSample {
|
|
4
16
|
token: string;
|
|
@@ -18,14 +30,13 @@ interface PerfProbeState {
|
|
|
18
30
|
pending?: Record<string, PendingProbe>;
|
|
19
31
|
samples?: PerfProbeSample[];
|
|
20
32
|
maxSamples?: number;
|
|
33
|
+
invalidationCounts?: Record<string, number>;
|
|
21
34
|
}
|
|
22
35
|
|
|
23
36
|
export interface PerfProbeSummary {
|
|
24
37
|
samples: PerfProbeSample[];
|
|
25
|
-
latest:
|
|
26
|
-
|
|
27
|
-
selection: PerfProbeSample | null;
|
|
28
|
-
};
|
|
38
|
+
latest: Partial<Record<PerfProbeKind, PerfProbeSample | null>>;
|
|
39
|
+
invalidationCounts: Record<string, number>;
|
|
29
40
|
}
|
|
30
41
|
|
|
31
42
|
declare global {
|
|
@@ -69,16 +80,47 @@ export function finishPerfProbe(token: string | null | undefined): PerfProbeSamp
|
|
|
69
80
|
recordedAt: Date.now(),
|
|
70
81
|
};
|
|
71
82
|
|
|
72
|
-
state
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
83
|
+
pushSample(state, sample);
|
|
84
|
+
|
|
85
|
+
return sample;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function recordPerfSample(
|
|
89
|
+
kind: PerfProbeKind,
|
|
90
|
+
durationMs = 0,
|
|
91
|
+
): PerfProbeSample | null {
|
|
92
|
+
const state = getEnabledState();
|
|
93
|
+
if (!state) {
|
|
94
|
+
return null;
|
|
77
95
|
}
|
|
78
96
|
|
|
97
|
+
const token = `${kind}-${state.nextToken ?? 0}`;
|
|
98
|
+
state.nextToken = (state.nextToken ?? 0) + 1;
|
|
99
|
+
const sample: PerfProbeSample = {
|
|
100
|
+
token,
|
|
101
|
+
kind,
|
|
102
|
+
durationMs,
|
|
103
|
+
recordedAt: Date.now(),
|
|
104
|
+
};
|
|
105
|
+
pushSample(state, sample);
|
|
79
106
|
return sample;
|
|
80
107
|
}
|
|
81
108
|
|
|
109
|
+
export function incrementInvalidationCounter(
|
|
110
|
+
counter: string,
|
|
111
|
+
amount = 1,
|
|
112
|
+
): number {
|
|
113
|
+
const state = getEnabledState();
|
|
114
|
+
if (!state) {
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
state.invalidationCounts ??= {};
|
|
119
|
+
state.invalidationCounts[counter] =
|
|
120
|
+
(state.invalidationCounts[counter] ?? 0) + amount;
|
|
121
|
+
return state.invalidationCounts[counter]!;
|
|
122
|
+
}
|
|
123
|
+
|
|
82
124
|
export function getLatestPerfSummary(): PerfProbeSummary | null {
|
|
83
125
|
const state = getEnabledState();
|
|
84
126
|
const samples = state?.samples ?? [];
|
|
@@ -88,13 +130,22 @@ export function getLatestPerfSummary(): PerfProbeSummary | null {
|
|
|
88
130
|
|
|
89
131
|
return {
|
|
90
132
|
samples: [...samples],
|
|
91
|
-
latest:
|
|
92
|
-
|
|
93
|
-
selection: [...samples].reverse().find((sample) => sample.kind === "selection") ?? null,
|
|
94
|
-
},
|
|
133
|
+
latest: buildLatestSampleMap(samples),
|
|
134
|
+
invalidationCounts: { ...(state.invalidationCounts ?? {}) },
|
|
95
135
|
};
|
|
96
136
|
}
|
|
97
137
|
|
|
138
|
+
export function resetPerfProbeState(): void {
|
|
139
|
+
const state = getEnabledState();
|
|
140
|
+
if (!state) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
state.nextToken = 0;
|
|
144
|
+
state.pending = {};
|
|
145
|
+
state.samples = [];
|
|
146
|
+
state.invalidationCounts = {};
|
|
147
|
+
}
|
|
148
|
+
|
|
98
149
|
function getEnabledState(): PerfProbeState | null {
|
|
99
150
|
if (typeof window === "undefined") {
|
|
100
151
|
return null;
|
|
@@ -105,3 +156,24 @@ function getEnabledState(): PerfProbeState | null {
|
|
|
105
156
|
}
|
|
106
157
|
return state;
|
|
107
158
|
}
|
|
159
|
+
|
|
160
|
+
function pushSample(state: PerfProbeState, sample: PerfProbeSample): void {
|
|
161
|
+
state.samples ??= [];
|
|
162
|
+
state.samples.push(sample);
|
|
163
|
+
const maxSamples = state.maxSamples ?? 20;
|
|
164
|
+
if (state.samples.length > maxSamples) {
|
|
165
|
+
state.samples.splice(0, state.samples.length - maxSamples);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function buildLatestSampleMap(
|
|
170
|
+
samples: PerfProbeSample[],
|
|
171
|
+
): Partial<Record<PerfProbeKind, PerfProbeSample | null>> {
|
|
172
|
+
const latest: Partial<Record<PerfProbeKind, PerfProbeSample | null>> = {};
|
|
173
|
+
for (const sample of [...samples].reverse()) {
|
|
174
|
+
if (latest[sample.kind] === undefined) {
|
|
175
|
+
latest[sample.kind] = sample;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return latest;
|
|
179
|
+
}
|
|
@@ -72,9 +72,9 @@ export function createCommandBridgePlugins(
|
|
|
72
72
|
return;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
const {
|
|
75
|
+
const { anchor, head } = view.state.selection;
|
|
76
76
|
callbacks.onSelectionChange(
|
|
77
|
-
createSelectionSnapshot(posMap.pmToRuntime(
|
|
77
|
+
createSelectionSnapshot(posMap.pmToRuntime(anchor), posMap.pmToRuntime(head)),
|
|
78
78
|
);
|
|
79
79
|
}
|
|
80
80
|
},
|