@tanishqkancharla/diffmap 0.1.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/LICENSE +21 -0
- package/README.md +212 -0
- package/bin.js +5 -0
- package/index.html +38 -0
- package/package.json +71 -0
- package/src/annotations.ts +82 -0
- package/src/cli.ts +94 -0
- package/src/codeFont.ts +2 -0
- package/src/contentPlugin.ts +47 -0
- package/src/definitions.ts +320 -0
- package/src/diagramAnnotations.ts +34 -0
- package/src/errors.ts +41 -0
- package/src/extractDocument.ts +20 -0
- package/src/gist/GistApp.tsx +292 -0
- package/src/gist/GistStatus.tsx +137 -0
- package/src/gist/fetchGist.ts +128 -0
- package/src/gist/main.tsx +25 -0
- package/src/gist/route.test.ts +90 -0
- package/src/gist/route.ts +134 -0
- package/src/index.ts +22 -0
- package/src/invokeCwd.ts +10 -0
- package/src/mermaid.ts +29 -0
- package/src/normalizeDiff.ts +39 -0
- package/src/parseFence.ts +91 -0
- package/src/parseViewer.ts +399 -0
- package/src/registry.ts +121 -0
- package/src/serve.ts +350 -0
- package/src/share.ts +94 -0
- package/src/viewer/CallStackDiff.tsx +156 -0
- package/src/viewer/Check.tsx +24 -0
- package/src/viewer/CodeDiff.tsx +26 -0
- package/src/viewer/ComarkView.tsx +209 -0
- package/src/viewer/DiffButton.tsx +24 -0
- package/src/viewer/DoneButton.tsx +32 -0
- package/src/viewer/Fence.tsx +51 -0
- package/src/viewer/FileExcerpt.tsx +65 -0
- package/src/viewer/HtmlBlock.tsx +19 -0
- package/src/viewer/HtmlPlaceholder.tsx +29 -0
- package/src/viewer/MermaidBlock.tsx +126 -0
- package/src/viewer/SourceDiffPanel.tsx +305 -0
- package/src/viewer/TableOfContents.tsx +211 -0
- package/src/viewer/ViewerApp.tsx +257 -0
- package/src/viewer/diagramLinks.ts +74 -0
- package/src/viewer/diffsTheme.ts +4 -0
- package/src/viewer/main.tsx +17 -0
- package/src/viewer/pierre.ts +47 -0
- package/src/viewer/sourceNavigation.ts +104 -0
- package/src/viewer/styles.css +9 -0
- package/src/viewer/viewerMode.ts +9 -0
- package/src/viewer/vite-env.d.ts +4 -0
- package/vite.config.ts +41 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, type ReactNode } from "react";
|
|
2
|
+
import {
|
|
3
|
+
backgroundColor,
|
|
4
|
+
border,
|
|
5
|
+
colors,
|
|
6
|
+
flex,
|
|
7
|
+
flexItem,
|
|
8
|
+
proseHtml,
|
|
9
|
+
proseMaxWidth,
|
|
10
|
+
spacing,
|
|
11
|
+
text,
|
|
12
|
+
} from "maui";
|
|
13
|
+
import { style, useStyles } from "purse-styles";
|
|
14
|
+
import type { ViewerDocument } from "../parseViewer.js";
|
|
15
|
+
import { ComarkView } from "./ComarkView.tsx";
|
|
16
|
+
import { SourceDiffPanel, type SourceSelection } from "./SourceDiffPanel.js";
|
|
17
|
+
import { DoneButton } from "./DoneButton.tsx";
|
|
18
|
+
import { DiffButton } from "./DiffButton.tsx";
|
|
19
|
+
import { TableOfContents } from "./TableOfContents.tsx";
|
|
20
|
+
import { ViewerModeContext, type ViewerMode } from "./viewerMode.ts";
|
|
21
|
+
|
|
22
|
+
type ViewerMeta = {
|
|
23
|
+
title: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function ViewerApp(props: {
|
|
27
|
+
document: ViewerDocument;
|
|
28
|
+
mode: ViewerMode;
|
|
29
|
+
title?: string;
|
|
30
|
+
headerActions?: ReactNode;
|
|
31
|
+
}) {
|
|
32
|
+
const viewerDocument = props.document;
|
|
33
|
+
const meta = useViewerMeta(props.mode === "local");
|
|
34
|
+
const [selection, setSelection] = useState<SourceSelection>();
|
|
35
|
+
const [showDiffPanel, setShowDiffPanel] = useState(false);
|
|
36
|
+
const hasSourceDiffs =
|
|
37
|
+
viewerDocument.sourceDiffs.length > 0 || viewerDocument.hasReferences;
|
|
38
|
+
const diffPanelOpen = hasSourceDiffs && showDiffPanel;
|
|
39
|
+
const body = useStyles(styles.body);
|
|
40
|
+
const [shutDown, setShutDown] = useState(false);
|
|
41
|
+
const parsedTitle =
|
|
42
|
+
props.title ??
|
|
43
|
+
viewerDocument.headings.find((heading) => heading.level === 1)?.text ??
|
|
44
|
+
"diffmap";
|
|
45
|
+
const title =
|
|
46
|
+
props.mode === "local" && meta !== undefined ? meta.title : parsedTitle;
|
|
47
|
+
const shell = useStyles(styles.shell);
|
|
48
|
+
const header = useStyles(styles.header);
|
|
49
|
+
const heading = useStyles(styles.heading);
|
|
50
|
+
const titleClass = useStyles(styles.title);
|
|
51
|
+
const actions = useStyles(styles.actions);
|
|
52
|
+
const article = useStyles(styles.article);
|
|
53
|
+
const prose = useStyles(styles.prose);
|
|
54
|
+
const content = useStyles(proseHtml("md"), styles.content);
|
|
55
|
+
const closed = useStyles(styles.closed);
|
|
56
|
+
const articleRef = useRef<HTMLElement>(null);
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
document.title = title;
|
|
60
|
+
}, [title]);
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
const id = decodeURIComponent(window.location.hash.replace(/^#/, ""));
|
|
64
|
+
if (id === "") return;
|
|
65
|
+
document.getElementById(id)?.scrollIntoView({ block: "start" });
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
if (shutDown) {
|
|
69
|
+
return <main className={closed}>Closed.</main>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<ViewerModeContext.Provider value={props.mode}>
|
|
74
|
+
<div className={shell}>
|
|
75
|
+
<header className={header}>
|
|
76
|
+
<div className={heading}>
|
|
77
|
+
<div className={titleClass}>{title}</div>
|
|
78
|
+
</div>
|
|
79
|
+
<div className={actions}>
|
|
80
|
+
{hasSourceDiffs && (
|
|
81
|
+
<DiffButton
|
|
82
|
+
pressed={showDiffPanel}
|
|
83
|
+
onClick={() => setShowDiffPanel((open) => !open)}
|
|
84
|
+
/>
|
|
85
|
+
)}
|
|
86
|
+
{props.headerActions}
|
|
87
|
+
{props.mode === "local" && (
|
|
88
|
+
<DoneButton
|
|
89
|
+
onClick={() => {
|
|
90
|
+
setShutDown(true);
|
|
91
|
+
// oxlint-disable-next-line typescript/no-floating-promises -- React click callbacks cannot await the server shutdown request.
|
|
92
|
+
void closeViewer();
|
|
93
|
+
}}
|
|
94
|
+
/>
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
</header>
|
|
98
|
+
<div className={body} data-has-source-diffs={diffPanelOpen}>
|
|
99
|
+
<TableOfContents
|
|
100
|
+
headings={viewerDocument.headings}
|
|
101
|
+
articleRef={articleRef}
|
|
102
|
+
/>
|
|
103
|
+
<article ref={articleRef} className={article}>
|
|
104
|
+
<div className={prose}>
|
|
105
|
+
<div className={content} data-diffmap-kind="page">
|
|
106
|
+
<ComarkView
|
|
107
|
+
document={viewerDocument}
|
|
108
|
+
selectedAnnotation={selection?.annotation}
|
|
109
|
+
onSelectAnnotation={(line) => {
|
|
110
|
+
setShowDiffPanel(true);
|
|
111
|
+
setSelection({
|
|
112
|
+
annotation: line,
|
|
113
|
+
reference: line.references[0]!,
|
|
114
|
+
});
|
|
115
|
+
}}
|
|
116
|
+
/>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</article>
|
|
120
|
+
{diffPanelOpen && (
|
|
121
|
+
<SourceDiffPanel
|
|
122
|
+
items={viewerDocument.sourceDiffs}
|
|
123
|
+
selection={selection}
|
|
124
|
+
onSelect={setSelection}
|
|
125
|
+
/>
|
|
126
|
+
)}
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
</ViewerModeContext.Provider>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function useViewerMeta(enabled: boolean) {
|
|
134
|
+
const [meta, setMeta] = useState<ViewerMeta>();
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (!enabled) return;
|
|
137
|
+
// oxlint-disable-next-line typescript/no-floating-promises -- React effects cannot await; this request owns the metadata update.
|
|
138
|
+
void fetch("/__diffmap/meta")
|
|
139
|
+
.then((response) => response.json())
|
|
140
|
+
.then((value) => {
|
|
141
|
+
// SAFETY: the diffmap CLI serves this shape from extractTitle.
|
|
142
|
+
setMeta(value as ViewerMeta);
|
|
143
|
+
});
|
|
144
|
+
}, [enabled]);
|
|
145
|
+
return meta;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function closeViewer() {
|
|
149
|
+
await fetch("/__diffmap/shutdown", { method: "POST" });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const styles = {
|
|
153
|
+
shell: style(flex({ direction: "column" }), {
|
|
154
|
+
width: "100%",
|
|
155
|
+
height: "100vh",
|
|
156
|
+
minWidth: 0,
|
|
157
|
+
minHeight: 0,
|
|
158
|
+
overflow: "hidden",
|
|
159
|
+
backgroundColor: colors.gray[4],
|
|
160
|
+
}),
|
|
161
|
+
header: style(
|
|
162
|
+
flex({ direction: "row", align: "center", justify: "between" }),
|
|
163
|
+
spacing.padding({ x: 6, y: 3 }),
|
|
164
|
+
flexItem({ size: "hug" }),
|
|
165
|
+
border(["bottom"], "border"),
|
|
166
|
+
{
|
|
167
|
+
minWidth: 0,
|
|
168
|
+
backgroundColor: backgroundColor.app,
|
|
169
|
+
},
|
|
170
|
+
),
|
|
171
|
+
heading: style(flex({ direction: "column" }), {
|
|
172
|
+
minWidth: 0,
|
|
173
|
+
}),
|
|
174
|
+
title: style(text({ size: "md", fontWeight: 600, color: "highContrast" }), {
|
|
175
|
+
minWidth: 0,
|
|
176
|
+
overflow: "hidden",
|
|
177
|
+
textOverflow: "ellipsis",
|
|
178
|
+
whiteSpace: "nowrap",
|
|
179
|
+
}),
|
|
180
|
+
actions: style(flex({ direction: "row", align: "center", gap: 3 }), {
|
|
181
|
+
flexShrink: 0,
|
|
182
|
+
}),
|
|
183
|
+
body: style({
|
|
184
|
+
display: "grid",
|
|
185
|
+
gridTemplateColumns: "var(--diffmap-columns)",
|
|
186
|
+
gridTemplateRows: "minmax(0, 1fr)",
|
|
187
|
+
"--diffmap-columns": "minmax(0, max-content) minmax(0, 1fr)",
|
|
188
|
+
flex: "1 1 auto",
|
|
189
|
+
minHeight: 0,
|
|
190
|
+
minWidth: 0,
|
|
191
|
+
overflow: "hidden",
|
|
192
|
+
"&[data-has-source-diffs='true']": {
|
|
193
|
+
"--diffmap-columns":
|
|
194
|
+
"minmax(0, max-content) minmax(0, 1fr) minmax(0, 1fr)",
|
|
195
|
+
},
|
|
196
|
+
"@media (max-width: 900px)": {
|
|
197
|
+
gridTemplateColumns: "minmax(0, 1fr)",
|
|
198
|
+
gridTemplateRows: "minmax(0, 1fr) auto",
|
|
199
|
+
},
|
|
200
|
+
}),
|
|
201
|
+
article: style(spacing.padding({ x: 12, y: 12 }), {
|
|
202
|
+
gridColumn: "2",
|
|
203
|
+
flex: "1 1 auto",
|
|
204
|
+
minWidth: 0,
|
|
205
|
+
minHeight: 0,
|
|
206
|
+
overflowY: "auto",
|
|
207
|
+
backgroundColor: backgroundColor.app,
|
|
208
|
+
"@media (max-width: 900px)": {
|
|
209
|
+
gridColumn: "1",
|
|
210
|
+
},
|
|
211
|
+
}),
|
|
212
|
+
prose: style({
|
|
213
|
+
display: "grid",
|
|
214
|
+
gridTemplateColumns: `minmax(0, 1fr) minmax(0, ${proseMaxWidth}) minmax(0, 1fr)`,
|
|
215
|
+
width: "100%",
|
|
216
|
+
maxWidth: "none",
|
|
217
|
+
minWidth: 0,
|
|
218
|
+
}),
|
|
219
|
+
content: style({
|
|
220
|
+
gridColumn: "2 / 3",
|
|
221
|
+
width: "100%",
|
|
222
|
+
maxWidth: "none",
|
|
223
|
+
minWidth: 0,
|
|
224
|
+
"& h1, & h2, & h3, & h4, & h5, & h6": {
|
|
225
|
+
scrollMarginTop: spacing.value(4),
|
|
226
|
+
},
|
|
227
|
+
"& ul > li[data-task]::before, & ol > li[data-task]::before": {
|
|
228
|
+
content: "none",
|
|
229
|
+
},
|
|
230
|
+
"& ul > li[data-task] > .diffmap-task-checkbox, & ol > li[data-task] > .diffmap-task-checkbox":
|
|
231
|
+
{
|
|
232
|
+
// Maui proseHtml md listPadding.
|
|
233
|
+
position: "absolute",
|
|
234
|
+
left: "-20px",
|
|
235
|
+
top: "6px",
|
|
236
|
+
},
|
|
237
|
+
"& .diffmap-task-checkbox label > span:last-child": {
|
|
238
|
+
position: "absolute",
|
|
239
|
+
width: "1px",
|
|
240
|
+
height: "1px",
|
|
241
|
+
padding: 0,
|
|
242
|
+
margin: "-1px",
|
|
243
|
+
overflow: "hidden",
|
|
244
|
+
clip: "rect(0, 0, 0, 0)",
|
|
245
|
+
whiteSpace: "nowrap",
|
|
246
|
+
border: 0,
|
|
247
|
+
},
|
|
248
|
+
}),
|
|
249
|
+
closed: style(
|
|
250
|
+
text({ size: "md", fontWeight: 500, color: "highContrast" }),
|
|
251
|
+
spacing.padding({ all: 12 }),
|
|
252
|
+
{
|
|
253
|
+
minHeight: "100vh",
|
|
254
|
+
backgroundColor: backgroundColor.app,
|
|
255
|
+
},
|
|
256
|
+
),
|
|
257
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { DiagramAnnotation } from "../diagramAnnotations.js";
|
|
2
|
+
import { DiffmapAnnotationError } from "../errors.js";
|
|
3
|
+
|
|
4
|
+
export function linkDiagram(svg: string, links: DiagramAnnotation[]) {
|
|
5
|
+
const document = new DOMParser().parseFromString(svg, "image/svg+xml");
|
|
6
|
+
for (const markerPart of document.querySelectorAll("marker *")) {
|
|
7
|
+
if (markerPart.getAttribute("fill") === "var(--_arrow)")
|
|
8
|
+
markerPart.setAttribute("fill", "context-stroke");
|
|
9
|
+
if (markerPart.getAttribute("stroke") === "var(--_arrow)")
|
|
10
|
+
markerPart.setAttribute("stroke", "context-stroke");
|
|
11
|
+
}
|
|
12
|
+
const edges = [
|
|
13
|
+
...document.querySelectorAll<SVGElement>(
|
|
14
|
+
".edge, .message, .class-relationship, .er-relationship",
|
|
15
|
+
),
|
|
16
|
+
];
|
|
17
|
+
const labels = [...document.querySelectorAll<SVGElement>(".edge-label")];
|
|
18
|
+
for (const [index, link] of links.entries()) {
|
|
19
|
+
const elements =
|
|
20
|
+
link.target === "node"
|
|
21
|
+
? [...document.querySelectorAll<SVGElement>("[data-id]")].filter(
|
|
22
|
+
(element) => element.dataset.id === link.id,
|
|
23
|
+
)
|
|
24
|
+
: edges.slice(Number(link.id), Number(link.id) + 1);
|
|
25
|
+
if (elements.length === 0)
|
|
26
|
+
return new DiffmapAnnotationError({
|
|
27
|
+
reason: `Mermaid ${link.target}:${link.id} does not exist in this diagram.`,
|
|
28
|
+
});
|
|
29
|
+
for (const element of elements) {
|
|
30
|
+
const wrapper = document.createElementNS(
|
|
31
|
+
"http://www.w3.org/2000/svg",
|
|
32
|
+
"g",
|
|
33
|
+
);
|
|
34
|
+
element.replaceWith(wrapper);
|
|
35
|
+
wrapper.append(element);
|
|
36
|
+
wrapper.setAttribute("data-source-index", String(index));
|
|
37
|
+
wrapper.setAttribute("class", `source-target source-${link.target}`);
|
|
38
|
+
wrapper.setAttribute("role", "button");
|
|
39
|
+
wrapper.setAttribute("tabindex", "0");
|
|
40
|
+
wrapper.setAttribute("aria-controls", "source-diff-panel");
|
|
41
|
+
wrapper.setAttribute("aria-pressed", "false");
|
|
42
|
+
const label = element.getAttribute("data-label");
|
|
43
|
+
wrapper.setAttribute(
|
|
44
|
+
"aria-label",
|
|
45
|
+
`Open source: ${label === null || label.length === 0 ? link.annotation.text : label}`,
|
|
46
|
+
);
|
|
47
|
+
if (link.target !== "edge") continue;
|
|
48
|
+
const edgeIndex = Number(link.id);
|
|
49
|
+
const sameEdge = (candidate: SVGElement) =>
|
|
50
|
+
candidate.dataset.from === element.dataset.from &&
|
|
51
|
+
candidate.dataset.to === element.dataset.to &&
|
|
52
|
+
candidate.dataset.label === element.dataset.label;
|
|
53
|
+
const occurrence = edges.slice(0, edgeIndex).filter(sameEdge).length;
|
|
54
|
+
const edgeLabel = labels.filter(sameEdge)[occurrence];
|
|
55
|
+
if (edgeLabel !== undefined) wrapper.append(edgeLabel);
|
|
56
|
+
const strokes = element.matches("polyline, path, line")
|
|
57
|
+
? [element]
|
|
58
|
+
: [...element.querySelectorAll<SVGElement>("line, polyline, path")];
|
|
59
|
+
for (const stroke of strokes) {
|
|
60
|
+
// SAFETY: cloning an SVG element preserves its element type.
|
|
61
|
+
const hit = stroke.cloneNode(true) as SVGElement;
|
|
62
|
+
hit.removeAttribute("marker-start");
|
|
63
|
+
hit.removeAttribute("marker-end");
|
|
64
|
+
hit.setAttribute("class", "source-hit");
|
|
65
|
+
hit.setAttribute("stroke", "transparent");
|
|
66
|
+
hit.setAttribute("stroke-width", "14");
|
|
67
|
+
hit.setAttribute("fill", "none");
|
|
68
|
+
hit.setAttribute("aria-hidden", "true");
|
|
69
|
+
wrapper.prepend(hit);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return document.documentElement.outerHTML;
|
|
74
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { StrictMode } from "react";
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
import { MauiProvider } from "maui";
|
|
4
|
+
import { viewerDocument } from "virtual:diffmap";
|
|
5
|
+
import { ViewerApp } from "./ViewerApp.tsx";
|
|
6
|
+
import "./styles.css";
|
|
7
|
+
|
|
8
|
+
const root = document.getElementById("root");
|
|
9
|
+
if (root === null) throw new Error("diffmap root element is missing");
|
|
10
|
+
|
|
11
|
+
createRoot(root).render(
|
|
12
|
+
<StrictMode>
|
|
13
|
+
<MauiProvider>
|
|
14
|
+
<ViewerApp document={viewerDocument} mode="local" />
|
|
15
|
+
</MauiProvider>
|
|
16
|
+
</StrictMode>,
|
|
17
|
+
);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { colors, radius, shadow } from "maui";
|
|
2
|
+
import { style } from "purse-styles";
|
|
3
|
+
import { codeFontFamily } from "../codeFont.js";
|
|
4
|
+
import { diffsTheme } from "./diffsTheme.ts";
|
|
5
|
+
|
|
6
|
+
const pierreUnsafeCss = `:host { --diffs-font-family: ${codeFontFamily}; }`;
|
|
7
|
+
|
|
8
|
+
export const sourceSelectionCss = `
|
|
9
|
+
[data-selected-line] {
|
|
10
|
+
--diffs-computed-selected-line-bg: var(--diffs-computed-diff-line-bg);
|
|
11
|
+
}
|
|
12
|
+
[data-line][data-selected-line] {
|
|
13
|
+
box-shadow: inset 4px 0 ${colors.blue[9]};
|
|
14
|
+
}
|
|
15
|
+
[data-column-number][data-selected-line] {
|
|
16
|
+
background-color: ${colors.blue[3]};
|
|
17
|
+
color: ${colors.blue[12]};
|
|
18
|
+
}
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
export const pierreShell = style(radius.md, shadow.subtle, {
|
|
22
|
+
overflow: "hidden",
|
|
23
|
+
minWidth: 0,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export function pierreDiffOptions(input: {
|
|
27
|
+
themeType: "light" | "dark";
|
|
28
|
+
disableFileHeader: boolean;
|
|
29
|
+
}) {
|
|
30
|
+
return {
|
|
31
|
+
theme: diffsTheme,
|
|
32
|
+
themeType: input.themeType,
|
|
33
|
+
overflow: "wrap" as const,
|
|
34
|
+
diffStyle: "unified" as const,
|
|
35
|
+
unsafeCSS: pierreUnsafeCss,
|
|
36
|
+
disableFileHeader: input.disableFileHeader,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function pierreFileOptions(themeType: "light" | "dark") {
|
|
41
|
+
return {
|
|
42
|
+
theme: diffsTheme,
|
|
43
|
+
themeType,
|
|
44
|
+
overflow: "wrap" as const,
|
|
45
|
+
unsafeCSS: pierreUnsafeCss,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import type { CodeViewDiffItem, CodeViewLineSelection } from "@pierre/diffs";
|
|
3
|
+
import type { SourceReference } from "../annotations.js";
|
|
4
|
+
import type { SourceDefinition, DefinitionResponse } from "../definitions.js";
|
|
5
|
+
import { DiffmapDefinitionError } from "../errors.js";
|
|
6
|
+
|
|
7
|
+
export async function requestSource(
|
|
8
|
+
endpoint: "source" | "definition",
|
|
9
|
+
params: URLSearchParams,
|
|
10
|
+
) {
|
|
11
|
+
const response = await fetch(`/__diffmap/${endpoint}?${params}`).catch(
|
|
12
|
+
(cause) =>
|
|
13
|
+
new DiffmapDefinitionError({
|
|
14
|
+
reason: "Could not reach the source service.",
|
|
15
|
+
cause,
|
|
16
|
+
}),
|
|
17
|
+
);
|
|
18
|
+
if (response instanceof Error) return response;
|
|
19
|
+
const result = await response
|
|
20
|
+
.json()
|
|
21
|
+
.then((value) => {
|
|
22
|
+
// SAFETY: the local diffmap source endpoints own this response shape.
|
|
23
|
+
return value as DefinitionResponse;
|
|
24
|
+
})
|
|
25
|
+
.catch(
|
|
26
|
+
(cause) =>
|
|
27
|
+
new DiffmapDefinitionError({
|
|
28
|
+
reason: "Could not read the source response.",
|
|
29
|
+
cause,
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
if (result instanceof Error) return result;
|
|
33
|
+
if (result.error !== undefined)
|
|
34
|
+
return new DiffmapDefinitionError({ reason: result.error });
|
|
35
|
+
if (result.definition === undefined)
|
|
36
|
+
return new DiffmapDefinitionError({ reason: "No definition found." });
|
|
37
|
+
return result.definition;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function useSourceReference(reference: SourceReference | undefined) {
|
|
41
|
+
const [resolution, setResolution] = useState<{
|
|
42
|
+
reference: SourceReference;
|
|
43
|
+
result: SourceDefinition | Error;
|
|
44
|
+
}>();
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (reference?.kind !== "file") return;
|
|
47
|
+
const params = new URLSearchParams({ path: reference.path });
|
|
48
|
+
if (reference.symbol !== undefined) params.set("symbol", reference.symbol);
|
|
49
|
+
if (reference.start !== undefined)
|
|
50
|
+
params.set("start", String(reference.start));
|
|
51
|
+
if (reference.end !== undefined) params.set("end", String(reference.end));
|
|
52
|
+
let active = true;
|
|
53
|
+
// oxlint-disable-next-line typescript/no-floating-promises -- The effect owns its asynchronous source result.
|
|
54
|
+
void requestSource("source", params).then((result) => {
|
|
55
|
+
if (active) setResolution({ reference, result });
|
|
56
|
+
});
|
|
57
|
+
return () => {
|
|
58
|
+
active = false;
|
|
59
|
+
};
|
|
60
|
+
}, [reference]);
|
|
61
|
+
return resolution?.reference === reference ? resolution?.result : undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function matchingSourceDiff(
|
|
65
|
+
source: SourceDefinition,
|
|
66
|
+
items: CodeViewDiffItem[],
|
|
67
|
+
): CodeViewLineSelection | undefined {
|
|
68
|
+
const lines = source.contents.split(/\r?\n/);
|
|
69
|
+
for (const item of items) {
|
|
70
|
+
const diff = item.fileDiff;
|
|
71
|
+
if (diff.name !== source.path) continue;
|
|
72
|
+
const ranges = diff.hunks.flatMap((hunk) => {
|
|
73
|
+
const start = Math.max(source.start, hunk.additionStart);
|
|
74
|
+
const end = Math.min(
|
|
75
|
+
source.end,
|
|
76
|
+
hunk.additionStart + hunk.additionCount - 1,
|
|
77
|
+
);
|
|
78
|
+
return start <= end ? [{ start, end, hunk }] : [];
|
|
79
|
+
});
|
|
80
|
+
if (ranges.length === 0) continue;
|
|
81
|
+
const matches = ranges.every(({ start, end, hunk }) => {
|
|
82
|
+
for (let line = start; line <= end; line++) {
|
|
83
|
+
const index = diff.isPartial
|
|
84
|
+
? hunk.additionLineIndex + line - hunk.additionStart
|
|
85
|
+
: line - 1;
|
|
86
|
+
if (
|
|
87
|
+
diff.additionLines[index]?.replace(/\r?\n$/, "") !== lines[line - 1]
|
|
88
|
+
)
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
return true;
|
|
92
|
+
});
|
|
93
|
+
if (!matches) continue;
|
|
94
|
+
return {
|
|
95
|
+
id: item.id,
|
|
96
|
+
range: {
|
|
97
|
+
start: Math.min(...ranges.map((range) => range.start)),
|
|
98
|
+
end: Math.max(...ranges.map((range) => range.end)),
|
|
99
|
+
side: "additions",
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import react from "@vitejs/plugin-react";
|
|
4
|
+
import { defineConfig } from "vite";
|
|
5
|
+
import { gistAppConfig } from "./vite.gist.config.ts";
|
|
6
|
+
|
|
7
|
+
const packageRoot = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
|
|
9
|
+
export default defineConfig(({ command }) => {
|
|
10
|
+
// Vercel’s Vite preset runs `vite build` with this file. Production must
|
|
11
|
+
// be the gist app (`gist/index.html`), not the local viewer that imports
|
|
12
|
+
// `virtual:diffmap`.
|
|
13
|
+
if (command === "build") return gistAppConfig();
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
// Browser ESM cannot load CJS packages such as Pierre. Vite writes one
|
|
17
|
+
// prebundle into this directory and reuses it until dependencies change.
|
|
18
|
+
cacheDir: path.join(packageRoot, "node_modules/.vite"),
|
|
19
|
+
optimizeDeps: {
|
|
20
|
+
include: [
|
|
21
|
+
"react",
|
|
22
|
+
"react-dom",
|
|
23
|
+
"react/jsx-dev-runtime",
|
|
24
|
+
"maui",
|
|
25
|
+
"purse-styles",
|
|
26
|
+
"@pierre/diffs/react",
|
|
27
|
+
"beautiful-mermaid",
|
|
28
|
+
"errore",
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
plugins: [react()],
|
|
32
|
+
resolve: {
|
|
33
|
+
dedupe: ["react", "react-dom", "purse-styles"],
|
|
34
|
+
},
|
|
35
|
+
clearScreen: false,
|
|
36
|
+
server: {
|
|
37
|
+
port: 4177,
|
|
38
|
+
strictPort: false,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
});
|