@tanishqkancharla/diffmap 0.1.3 → 0.1.5
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 +25 -14
- package/package.json +3 -3
- package/src/cli.test.ts +50 -0
- package/src/cli.ts +126 -51
- package/src/contentPlugin.ts +5 -3
- package/src/errors.ts +6 -1
- package/src/gist/GistApp.tsx +34 -21
- package/src/gist/GistStatus.tsx +8 -43
- package/src/gist/main.tsx +2 -2
- package/src/gist/pin.ts +14 -0
- package/src/github/GitHubApp.tsx +235 -0
- package/src/github/fetchRepo.test.ts +17 -0
- package/src/github/fetchRepo.ts +300 -0
- package/src/github/patch.test.ts +30 -0
- package/src/github/patch.ts +40 -0
- package/src/github/pin.ts +14 -0
- package/src/github/route.test.ts +68 -0
- package/src/github/route.ts +175 -0
- package/src/host/HostApp.tsx +22 -0
- package/src/host/HostHome.tsx +73 -0
- package/src/host/files.ts +10 -0
- package/src/host/home.md +194 -0
- package/src/host/home.test.ts +82 -0
- package/src/index.ts +1 -0
- package/src/mermaid.ts +29 -1
- package/src/packageVersion.ts +7 -0
- package/src/parseViewer.test.ts +185 -0
- package/src/parseViewer.ts +27 -3
- package/src/serve.test.ts +306 -0
- package/src/serve.ts +23 -5
- package/src/share.ts +5 -1
- package/src/update.test.ts +19 -0
- package/src/update.ts +285 -0
- package/src/viewer/CallStackDiff.tsx +26 -6
- package/src/viewer/CloseServerButton.tsx +16 -0
- package/src/viewer/FileExcerpt.tsx +52 -3
- package/src/viewer/HtmlPlaceholder.tsx +6 -1
- package/src/viewer/SourceDiffPanel.tsx +155 -59
- package/src/viewer/TableOfContents.tsx +230 -39
- package/src/viewer/TocButton.tsx +27 -0
- package/src/viewer/ViewerApp.tsx +182 -47
- package/src/viewer/main.tsx +10 -3
- package/src/viewer/sourceHeader.test.ts +131 -0
- package/src/viewer/sourceHeader.ts +103 -0
- package/src/viewer/sourceNavigation.ts +81 -3
- package/src/viewer/viewerMode.ts +1 -1
- package/src/viewer/vite-env.d.ts +2 -1
- package/vite.config.ts +1 -1
- package/src/viewer/Check.tsx +0 -24
- package/src/viewer/DoneButton.tsx +0 -32
package/src/viewer/ViewerApp.tsx
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useRef,
|
|
5
|
+
useState,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
} from "react";
|
|
2
8
|
import {
|
|
3
9
|
backgroundColor,
|
|
4
10
|
border,
|
|
5
11
|
colors,
|
|
6
12
|
flex,
|
|
7
13
|
flexItem,
|
|
14
|
+
H1,
|
|
15
|
+
P,
|
|
8
16
|
proseHtml,
|
|
9
17
|
proseMaxWidth,
|
|
10
18
|
spacing,
|
|
@@ -14,9 +22,15 @@ import { style, useStyles } from "purse-styles";
|
|
|
14
22
|
import type { ViewerDocument } from "../parseViewer.js";
|
|
15
23
|
import { ComarkView } from "./ComarkView.tsx";
|
|
16
24
|
import { SourceDiffPanel, type SourceSelection } from "./SourceDiffPanel.js";
|
|
17
|
-
import {
|
|
25
|
+
import { CloseServerButton } from "./CloseServerButton.tsx";
|
|
18
26
|
import { DiffButton } from "./DiffButton.tsx";
|
|
19
|
-
import {
|
|
27
|
+
import { TocButton } from "./TocButton.tsx";
|
|
28
|
+
import {
|
|
29
|
+
hasTableOfContents,
|
|
30
|
+
TableOfContents,
|
|
31
|
+
TOC_OPEN_MIN_WIDTH_PX,
|
|
32
|
+
TOC_SIDE_MARGIN_PX,
|
|
33
|
+
} from "./TableOfContents.tsx";
|
|
20
34
|
import { ViewerModeContext, type ViewerMode } from "./viewerMode.ts";
|
|
21
35
|
|
|
22
36
|
type ViewerMeta = {
|
|
@@ -53,7 +67,64 @@ export function ViewerApp(props: {
|
|
|
53
67
|
const prose = useStyles(styles.prose);
|
|
54
68
|
const content = useStyles(proseHtml("md"), styles.content);
|
|
55
69
|
const closed = useStyles(styles.closed);
|
|
70
|
+
const closedCopy = useStyles(styles.closedCopy);
|
|
71
|
+
const stage = useStyles(styles.stage);
|
|
56
72
|
const articleRef = useRef<HTMLElement>(null);
|
|
73
|
+
const hasToc = hasTableOfContents(viewerDocument.headings);
|
|
74
|
+
const tocDismissed = useRef(false);
|
|
75
|
+
const tocPinned = useRef(false);
|
|
76
|
+
const tocFits = useRef(window.innerWidth >= TOC_OPEN_MIN_WIDTH_PX);
|
|
77
|
+
const remeasureToc = useRef<() => void>(() => {});
|
|
78
|
+
const [tocOpen, setTocOpen] = useState<TocMode | undefined>(() =>
|
|
79
|
+
hasToc && window.innerWidth >= TOC_OPEN_MIN_WIDTH_PX ? "space" : undefined,
|
|
80
|
+
);
|
|
81
|
+
const tocExpanded = tocOpen !== undefined;
|
|
82
|
+
const onDwellOpen = useCallback(() => {
|
|
83
|
+
setTocOpen((mode) =>
|
|
84
|
+
mode === "click" || mode === "space" ? mode : "dwell",
|
|
85
|
+
);
|
|
86
|
+
}, []);
|
|
87
|
+
const onDwellClose = useCallback(() => {
|
|
88
|
+
setTocOpen((mode) => {
|
|
89
|
+
if (mode === "click" || mode === "space") return mode;
|
|
90
|
+
if (tocFits.current && !tocDismissed.current) {
|
|
91
|
+
return tocPinned.current ? "click" : "space";
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
});
|
|
95
|
+
}, []);
|
|
96
|
+
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (!hasToc) return;
|
|
99
|
+
const measure = () => {
|
|
100
|
+
const node = articleRef.current;
|
|
101
|
+
if (node !== null && node.clientWidth === 0) return;
|
|
102
|
+
const width = node?.clientWidth ?? window.innerWidth;
|
|
103
|
+
const next = width >= TOC_OPEN_MIN_WIDTH_PX;
|
|
104
|
+
const previous = tocFits.current;
|
|
105
|
+
tocFits.current = next;
|
|
106
|
+
if (previous === next) return;
|
|
107
|
+
if (!next) {
|
|
108
|
+
setTocOpen(undefined);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
setTocOpen((mode) => {
|
|
112
|
+
if (mode === "click" || mode === "dwell") return mode;
|
|
113
|
+
if (tocDismissed.current) return undefined;
|
|
114
|
+
return tocPinned.current ? "click" : "space";
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
remeasureToc.current = measure;
|
|
118
|
+
const observer = new ResizeObserver(measure);
|
|
119
|
+
const articleNode = articleRef.current;
|
|
120
|
+
if (articleNode) observer.observe(articleNode);
|
|
121
|
+
window.addEventListener("resize", measure);
|
|
122
|
+
return () => {
|
|
123
|
+
observer.disconnect();
|
|
124
|
+
window.removeEventListener("resize", measure);
|
|
125
|
+
remeasureToc.current = () => {};
|
|
126
|
+
};
|
|
127
|
+
}, [hasToc]);
|
|
57
128
|
|
|
58
129
|
useEffect(() => {
|
|
59
130
|
document.title = title;
|
|
@@ -66,7 +137,14 @@ export function ViewerApp(props: {
|
|
|
66
137
|
}, []);
|
|
67
138
|
|
|
68
139
|
if (shutDown) {
|
|
69
|
-
return
|
|
140
|
+
return (
|
|
141
|
+
<main className={closed}>
|
|
142
|
+
<div className={closedCopy}>
|
|
143
|
+
<H1>Closed spec</H1>
|
|
144
|
+
<P>The local server stopped.</P>
|
|
145
|
+
</div>
|
|
146
|
+
</main>
|
|
147
|
+
);
|
|
70
148
|
}
|
|
71
149
|
|
|
72
150
|
return (
|
|
@@ -74,18 +152,37 @@ export function ViewerApp(props: {
|
|
|
74
152
|
<div className={shell}>
|
|
75
153
|
<header className={header}>
|
|
76
154
|
<div className={heading}>
|
|
155
|
+
{hasToc && (
|
|
156
|
+
<TocButton
|
|
157
|
+
expanded={tocExpanded}
|
|
158
|
+
onClick={() => {
|
|
159
|
+
if (tocExpanded) {
|
|
160
|
+
tocDismissed.current = true;
|
|
161
|
+
tocPinned.current = false;
|
|
162
|
+
setTocOpen(undefined);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
tocDismissed.current = false;
|
|
166
|
+
tocPinned.current = true;
|
|
167
|
+
setTocOpen("click");
|
|
168
|
+
}}
|
|
169
|
+
/>
|
|
170
|
+
)}
|
|
77
171
|
<div className={titleClass}>{title}</div>
|
|
78
172
|
</div>
|
|
79
173
|
<div className={actions}>
|
|
80
174
|
{hasSourceDiffs && (
|
|
81
175
|
<DiffButton
|
|
82
176
|
pressed={showDiffPanel}
|
|
83
|
-
onClick={() =>
|
|
177
|
+
onClick={() => {
|
|
178
|
+
setShowDiffPanel((open) => !open);
|
|
179
|
+
window.requestAnimationFrame(() => remeasureToc.current());
|
|
180
|
+
}}
|
|
84
181
|
/>
|
|
85
182
|
)}
|
|
86
183
|
{props.headerActions}
|
|
87
184
|
{props.mode === "local" && (
|
|
88
|
-
<
|
|
185
|
+
<CloseServerButton
|
|
89
186
|
onClick={() => {
|
|
90
187
|
setShutDown(true);
|
|
91
188
|
// oxlint-disable-next-line typescript/no-floating-promises -- React click callbacks cannot await the server shutdown request.
|
|
@@ -95,41 +192,53 @@ export function ViewerApp(props: {
|
|
|
95
192
|
)}
|
|
96
193
|
</div>
|
|
97
194
|
</header>
|
|
98
|
-
<div className={
|
|
99
|
-
<
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
195
|
+
<div className={stage}>
|
|
196
|
+
<div className={body} data-has-source-diffs={diffPanelOpen}>
|
|
197
|
+
<article ref={articleRef} className={article}>
|
|
198
|
+
<div className={prose} data-has-toc={hasToc ? "true" : "false"}>
|
|
199
|
+
<div className={content} data-diffmap-kind="page">
|
|
200
|
+
<ComarkView
|
|
201
|
+
document={viewerDocument}
|
|
202
|
+
selectedAnnotation={selection?.annotation}
|
|
203
|
+
onSelectAnnotation={(line) => {
|
|
204
|
+
setShowDiffPanel(true);
|
|
205
|
+
setSelection({
|
|
206
|
+
annotation: line,
|
|
207
|
+
reference: line.references[0]!,
|
|
208
|
+
});
|
|
209
|
+
window.requestAnimationFrame(() =>
|
|
210
|
+
remeasureToc.current(),
|
|
211
|
+
);
|
|
212
|
+
}}
|
|
213
|
+
/>
|
|
214
|
+
</div>
|
|
215
|
+
{hasToc && (
|
|
216
|
+
<TableOfContents
|
|
217
|
+
headings={viewerDocument.headings}
|
|
218
|
+
articleRef={articleRef}
|
|
219
|
+
open={tocExpanded}
|
|
220
|
+
onDwellOpen={onDwellOpen}
|
|
221
|
+
onDwellClose={onDwellClose}
|
|
222
|
+
/>
|
|
223
|
+
)}
|
|
117
224
|
</div>
|
|
118
|
-
</
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
225
|
+
</article>
|
|
226
|
+
{diffPanelOpen && (
|
|
227
|
+
<SourceDiffPanel
|
|
228
|
+
items={viewerDocument.sourceDiffs}
|
|
229
|
+
selection={selection}
|
|
230
|
+
onSelect={setSelection}
|
|
231
|
+
/>
|
|
232
|
+
)}
|
|
233
|
+
</div>
|
|
127
234
|
</div>
|
|
128
235
|
</div>
|
|
129
236
|
</ViewerModeContext.Provider>
|
|
130
237
|
);
|
|
131
238
|
}
|
|
132
239
|
|
|
240
|
+
type TocMode = "click" | "dwell" | "space";
|
|
241
|
+
|
|
133
242
|
function useViewerMeta(enabled: boolean) {
|
|
134
243
|
const [meta, setMeta] = useState<ViewerMeta>();
|
|
135
244
|
useEffect(() => {
|
|
@@ -159,7 +268,7 @@ const styles = {
|
|
|
159
268
|
backgroundColor: colors.gray[4],
|
|
160
269
|
}),
|
|
161
270
|
header: style(
|
|
162
|
-
flex({ direction: "row",
|
|
271
|
+
flex({ direction: "row", alignItems: "center", justifyContent: "between" }),
|
|
163
272
|
spacing.padding({ x: 6, y: 3 }),
|
|
164
273
|
flexItem({ size: "hug" }),
|
|
165
274
|
border(["bottom"], "border"),
|
|
@@ -168,8 +277,9 @@ const styles = {
|
|
|
168
277
|
backgroundColor: backgroundColor.app,
|
|
169
278
|
},
|
|
170
279
|
),
|
|
171
|
-
heading: style(flex({ direction: "
|
|
280
|
+
heading: style(flex({ direction: "row", alignItems: "center", gap: 3 }), {
|
|
172
281
|
minWidth: 0,
|
|
282
|
+
flex: "1 1 auto",
|
|
173
283
|
}),
|
|
174
284
|
title: style(text({ size: "md", fontWeight: 600, color: "highContrast" }), {
|
|
175
285
|
minWidth: 0,
|
|
@@ -177,37 +287,46 @@ const styles = {
|
|
|
177
287
|
textOverflow: "ellipsis",
|
|
178
288
|
whiteSpace: "nowrap",
|
|
179
289
|
}),
|
|
180
|
-
actions: style(flex({ direction: "row",
|
|
290
|
+
actions: style(flex({ direction: "row", alignItems: "center", gap: 3 }), {
|
|
181
291
|
flexShrink: 0,
|
|
182
292
|
}),
|
|
293
|
+
stage: style(flex({ direction: "column" }), {
|
|
294
|
+
position: "relative",
|
|
295
|
+
flex: "1 1 auto",
|
|
296
|
+
minWidth: 0,
|
|
297
|
+
minHeight: 0,
|
|
298
|
+
}),
|
|
183
299
|
body: style({
|
|
184
300
|
display: "grid",
|
|
185
|
-
gridTemplateColumns: "var(--diffmap-columns)",
|
|
186
301
|
gridTemplateRows: "minmax(0, 1fr)",
|
|
187
|
-
"--diffmap-columns": "minmax(0, max-content) minmax(0, 1fr)",
|
|
188
302
|
flex: "1 1 auto",
|
|
189
303
|
minHeight: 0,
|
|
190
304
|
minWidth: 0,
|
|
191
305
|
overflow: "hidden",
|
|
306
|
+
backgroundColor: backgroundColor.app,
|
|
307
|
+
"--diffmap-columns": "minmax(0, 1fr)",
|
|
308
|
+
"--diffmap-areas": '"article"',
|
|
309
|
+
gridTemplateColumns: "var(--diffmap-columns)",
|
|
310
|
+
gridTemplateAreas: "var(--diffmap-areas)",
|
|
192
311
|
"&[data-has-source-diffs='true']": {
|
|
193
|
-
"--diffmap-columns":
|
|
194
|
-
|
|
312
|
+
"--diffmap-columns": "minmax(0, 1fr) minmax(0, 1fr)",
|
|
313
|
+
"--diffmap-areas": '"article diff"',
|
|
195
314
|
},
|
|
196
315
|
"@media (max-width: 900px)": {
|
|
197
316
|
gridTemplateColumns: "minmax(0, 1fr)",
|
|
198
317
|
gridTemplateRows: "minmax(0, 1fr) auto",
|
|
318
|
+
gridTemplateAreas: '"article" "diff"',
|
|
199
319
|
},
|
|
200
320
|
}),
|
|
201
321
|
article: style(spacing.padding({ x: 12, y: 12 }), {
|
|
202
|
-
|
|
322
|
+
gridArea: "article",
|
|
203
323
|
flex: "1 1 auto",
|
|
204
324
|
minWidth: 0,
|
|
205
325
|
minHeight: 0,
|
|
206
326
|
overflowY: "auto",
|
|
207
327
|
backgroundColor: backgroundColor.app,
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
},
|
|
328
|
+
// Scrollport size for the sticky gutter rail (`100cqh`).
|
|
329
|
+
containerType: "size",
|
|
211
330
|
}),
|
|
212
331
|
prose: style({
|
|
213
332
|
display: "grid",
|
|
@@ -215,9 +334,13 @@ const styles = {
|
|
|
215
334
|
width: "100%",
|
|
216
335
|
maxWidth: "none",
|
|
217
336
|
minWidth: 0,
|
|
337
|
+
"&[data-has-toc='true']": {
|
|
338
|
+
gridTemplateColumns: `minmax(${String(TOC_SIDE_MARGIN_PX)}px, 1fr) minmax(0, ${proseMaxWidth}) minmax(${String(TOC_SIDE_MARGIN_PX)}px, 1fr)`,
|
|
339
|
+
},
|
|
218
340
|
}),
|
|
219
341
|
content: style({
|
|
220
342
|
gridColumn: "2 / 3",
|
|
343
|
+
gridRow: "1",
|
|
221
344
|
width: "100%",
|
|
222
345
|
maxWidth: "none",
|
|
223
346
|
minWidth: 0,
|
|
@@ -247,11 +370,23 @@ const styles = {
|
|
|
247
370
|
},
|
|
248
371
|
}),
|
|
249
372
|
closed: style(
|
|
250
|
-
|
|
251
|
-
|
|
373
|
+
flex({
|
|
374
|
+
direction: "column",
|
|
375
|
+
alignItems: "center",
|
|
376
|
+
justifyContent: "center",
|
|
377
|
+
}),
|
|
378
|
+
spacing.padding({ x: 12, y: 12 }),
|
|
252
379
|
{
|
|
253
380
|
minHeight: "100vh",
|
|
254
381
|
backgroundColor: backgroundColor.app,
|
|
255
382
|
},
|
|
256
383
|
),
|
|
384
|
+
closedCopy: style(
|
|
385
|
+
flex({ direction: "column", alignItems: "center", gap: 3 }),
|
|
386
|
+
{
|
|
387
|
+
width: "100%",
|
|
388
|
+
maxWidth: proseMaxWidth,
|
|
389
|
+
textAlign: "center",
|
|
390
|
+
},
|
|
391
|
+
),
|
|
257
392
|
};
|
package/src/viewer/main.tsx
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { StrictMode } from "react";
|
|
2
2
|
import { createRoot } from "react-dom/client";
|
|
3
|
-
import { MauiProvider } from "maui";
|
|
4
|
-
import { viewerDocument } from "virtual:diffmap";
|
|
3
|
+
import { MauiProvider, P } from "maui";
|
|
4
|
+
import { parseError, viewerDocument } from "virtual:diffmap";
|
|
5
5
|
import { ViewerApp } from "./ViewerApp.tsx";
|
|
6
|
+
import { GistStatus } from "../gist/GistStatus.tsx";
|
|
6
7
|
import "./styles.css";
|
|
7
8
|
|
|
8
9
|
const root = document.getElementById("root");
|
|
@@ -11,7 +12,13 @@ if (root === null) throw new Error("diffmap root element is missing");
|
|
|
11
12
|
createRoot(root).render(
|
|
12
13
|
<StrictMode>
|
|
13
14
|
<MauiProvider>
|
|
14
|
-
|
|
15
|
+
{parseError !== null || viewerDocument === null ? (
|
|
16
|
+
<GistStatus title="Could not parse this spec">
|
|
17
|
+
<P>{parseError ?? "Unknown parse error."}</P>
|
|
18
|
+
</GistStatus>
|
|
19
|
+
) : (
|
|
20
|
+
<ViewerApp document={viewerDocument} mode="local" />
|
|
21
|
+
)}
|
|
15
22
|
</MauiProvider>
|
|
16
23
|
</StrictMode>,
|
|
17
24
|
);
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import type { CodeViewDiffItem } from "@pierre/diffs";
|
|
4
|
+
import {
|
|
5
|
+
sourcePanelOrigin,
|
|
6
|
+
sourcePanelPath,
|
|
7
|
+
sourcePanelPin,
|
|
8
|
+
} from "./sourceHeader.ts";
|
|
9
|
+
|
|
10
|
+
const sha = "ef78bf4678d2e76a7c8bcc4087fcadf347a1fcdf";
|
|
11
|
+
const github = {
|
|
12
|
+
owner: "tanishqkancharla",
|
|
13
|
+
repo: "diffmap",
|
|
14
|
+
sha,
|
|
15
|
+
};
|
|
16
|
+
const gist = {
|
|
17
|
+
gistId: "a3d2fe2b2e0b1422a4fd5378baced900",
|
|
18
|
+
file: "walkthrough.md",
|
|
19
|
+
htmlUrl: "https://gist.github.com/a3d2fe2b2e0b1422a4fd5378baced900",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function diffItem(id: string, name: string): CodeViewDiffItem {
|
|
23
|
+
return {
|
|
24
|
+
id,
|
|
25
|
+
type: "diff",
|
|
26
|
+
fileDiff: { name } as CodeViewDiffItem["fileDiff"],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
test("github file pin is owner/repo @ shortsha linking to the blob at the pin SHA", () => {
|
|
31
|
+
const path = "src/cli.ts";
|
|
32
|
+
const pin = sourcePanelPin({
|
|
33
|
+
origin: "file",
|
|
34
|
+
path,
|
|
35
|
+
github,
|
|
36
|
+
});
|
|
37
|
+
assert.deepEqual(pin, {
|
|
38
|
+
kind: "github",
|
|
39
|
+
label: "tanishqkancharla/diffmap @ ef78bf4",
|
|
40
|
+
href: `https://github.com/tanishqkancharla/diffmap/blob/${sha}/src/cli.ts`,
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("embedded source-diff says from this spec, not a GitHub blob", () => {
|
|
45
|
+
const items = [diffItem("cli", "src/cli.ts")];
|
|
46
|
+
const origin = sourcePanelOrigin({
|
|
47
|
+
showingFile: false,
|
|
48
|
+
reference: { kind: "file", path: "src/cli.ts" },
|
|
49
|
+
items,
|
|
50
|
+
linkedDiffId: "cli",
|
|
51
|
+
});
|
|
52
|
+
assert.equal(origin, "spec-diff");
|
|
53
|
+
assert.deepEqual(sourcePanelPin({ origin, path: "src/cli.ts", github }), {
|
|
54
|
+
kind: "spec",
|
|
55
|
+
label: "from this spec",
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("PR file diffs pin to the repo blob, not from this spec", () => {
|
|
60
|
+
const items = [diffItem("pr:src/cli.ts", "src/cli.ts")];
|
|
61
|
+
const origin = sourcePanelOrigin({
|
|
62
|
+
showingFile: false,
|
|
63
|
+
reference: { kind: "file", path: "src/cli.ts" },
|
|
64
|
+
items,
|
|
65
|
+
});
|
|
66
|
+
assert.equal(origin, "repo-diff");
|
|
67
|
+
const pin = sourcePanelPin({ origin, path: "src/cli.ts", github });
|
|
68
|
+
assert.equal(pin.kind, "github");
|
|
69
|
+
if (pin.kind !== "github") return;
|
|
70
|
+
assert.equal(pin.label, "tanishqkancharla/diffmap @ ef78bf4");
|
|
71
|
+
assert.match(
|
|
72
|
+
pin.href,
|
|
73
|
+
/\/blob\/ef78bf4678d2e76a7c8bcc4087fcadf347a1fcdf\/src\/cli.ts$/,
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("gist pin is gist id / filename, not a fake repo", () => {
|
|
78
|
+
const pin = sourcePanelPin({
|
|
79
|
+
origin: "file",
|
|
80
|
+
path: "src/cli.ts",
|
|
81
|
+
gist,
|
|
82
|
+
});
|
|
83
|
+
assert.deepEqual(pin, {
|
|
84
|
+
kind: "gist",
|
|
85
|
+
label: "a3d2fe2b2e0b1422a4fd5378baced900 / walkthrough.md",
|
|
86
|
+
href: "https://gist.github.com/a3d2fe2b2e0b1422a4fd5378baced900",
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("gist embedded source-diff says from this spec", () => {
|
|
91
|
+
const origin = sourcePanelOrigin({
|
|
92
|
+
showingFile: false,
|
|
93
|
+
items: [diffItem("serve", "src/serve.ts")],
|
|
94
|
+
reference: { kind: "diff", id: "serve", side: "new", start: 1, end: 3 },
|
|
95
|
+
});
|
|
96
|
+
assert.equal(origin, "spec-diff");
|
|
97
|
+
assert.deepEqual(sourcePanelPin({ origin, path: "src/serve.ts", gist }), {
|
|
98
|
+
kind: "spec",
|
|
99
|
+
label: "from this spec",
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("local file has no pin line", () => {
|
|
104
|
+
assert.deepEqual(sourcePanelPin({ origin: "file", path: "src/cli.ts" }), {
|
|
105
|
+
kind: "none",
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("sourcePanelPath prefers the open file, then the linked diff, then the reference", () => {
|
|
110
|
+
const items = [diffItem("cli", "src/cli.ts")];
|
|
111
|
+
assert.equal(
|
|
112
|
+
sourcePanelPath({ filePath: "src/registry.ts", items }),
|
|
113
|
+
"src/registry.ts",
|
|
114
|
+
);
|
|
115
|
+
assert.equal(
|
|
116
|
+
sourcePanelPath({
|
|
117
|
+
items,
|
|
118
|
+
linkedDiffId: "cli",
|
|
119
|
+
reference: { kind: "file", path: "src/other.ts" },
|
|
120
|
+
}),
|
|
121
|
+
"src/cli.ts",
|
|
122
|
+
);
|
|
123
|
+
assert.equal(
|
|
124
|
+
sourcePanelPath({
|
|
125
|
+
items,
|
|
126
|
+
reference: { kind: "file", path: "src/cli.ts" },
|
|
127
|
+
}),
|
|
128
|
+
"src/cli.ts",
|
|
129
|
+
);
|
|
130
|
+
assert.equal(sourcePanelPath({ items: [] }), undefined);
|
|
131
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { CodeViewDiffItem } from "@pierre/diffs";
|
|
2
|
+
import type { SourceReference } from "../annotations.js";
|
|
3
|
+
import type { GistPin } from "../gist/pin.ts";
|
|
4
|
+
import { githubGistUrl } from "../gist/route.ts";
|
|
5
|
+
import type { GitHubPin } from "../github/pin.ts";
|
|
6
|
+
import { githubBlobUrl } from "../github/route.ts";
|
|
7
|
+
|
|
8
|
+
const PR_DIFF_PREFIX = "pr:";
|
|
9
|
+
|
|
10
|
+
export type SourcePanelOrigin = "file" | "spec-diff" | "repo-diff";
|
|
11
|
+
|
|
12
|
+
export type SourcePanelPin =
|
|
13
|
+
| { kind: "github"; label: string; href: string }
|
|
14
|
+
| { kind: "gist"; label: string; href: string }
|
|
15
|
+
| { kind: "spec"; label: "from this spec" }
|
|
16
|
+
| { kind: "none" };
|
|
17
|
+
|
|
18
|
+
/** Pull-files diffs are repo patches at the pin SHA; everything else is embedded in the spec. */
|
|
19
|
+
export function isSpecEmbeddedDiff(item: CodeViewDiffItem) {
|
|
20
|
+
return !item.id.startsWith(PR_DIFF_PREFIX);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function sourcePanelPath(input: {
|
|
24
|
+
filePath?: string;
|
|
25
|
+
reference?: SourceReference;
|
|
26
|
+
items: CodeViewDiffItem[];
|
|
27
|
+
linkedDiffId?: string;
|
|
28
|
+
}): string | undefined {
|
|
29
|
+
if (input.filePath !== undefined && input.filePath.length > 0) {
|
|
30
|
+
return input.filePath;
|
|
31
|
+
}
|
|
32
|
+
if (input.linkedDiffId !== undefined) {
|
|
33
|
+
const linked = input.items.find((item) => item.id === input.linkedDiffId);
|
|
34
|
+
if (linked !== undefined) return linked.fileDiff.name;
|
|
35
|
+
}
|
|
36
|
+
const ref = input.reference;
|
|
37
|
+
if (ref === undefined) return undefined;
|
|
38
|
+
if (ref.kind === "file") return ref.path;
|
|
39
|
+
return input.items.find((item) => item.id === ref.id)?.fileDiff.name;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function sourcePanelOrigin(input: {
|
|
43
|
+
showingFile: boolean;
|
|
44
|
+
reference?: SourceReference;
|
|
45
|
+
items: CodeViewDiffItem[];
|
|
46
|
+
linkedDiffId?: string;
|
|
47
|
+
}): SourcePanelOrigin {
|
|
48
|
+
if (input.showingFile) return "file";
|
|
49
|
+
const ref = input.reference;
|
|
50
|
+
const focused =
|
|
51
|
+
input.linkedDiffId !== undefined
|
|
52
|
+
? input.items.find((item) => item.id === input.linkedDiffId)
|
|
53
|
+
: ref?.kind === "diff"
|
|
54
|
+
? input.items.find((item) => item.id === ref.id)
|
|
55
|
+
: ref?.kind === "file"
|
|
56
|
+
? input.items.find((item) => item.fileDiff.name === ref.path)
|
|
57
|
+
: undefined;
|
|
58
|
+
if (focused !== undefined) {
|
|
59
|
+
return isSpecEmbeddedDiff(focused) ? "spec-diff" : "repo-diff";
|
|
60
|
+
}
|
|
61
|
+
if (input.items.some(isSpecEmbeddedDiff)) return "spec-diff";
|
|
62
|
+
if (input.items.length > 0) return "repo-diff";
|
|
63
|
+
return "file";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Pin line for the source panel. A hosted spec is one SHA; blob links use that
|
|
68
|
+
* SHA for the open path. Embedded `source-diff` is from the spec, not a blob.
|
|
69
|
+
*/
|
|
70
|
+
export function sourcePanelPin(input: {
|
|
71
|
+
origin: SourcePanelOrigin;
|
|
72
|
+
path: string | undefined;
|
|
73
|
+
github?: GitHubPin;
|
|
74
|
+
gist?: GistPin;
|
|
75
|
+
}): SourcePanelPin {
|
|
76
|
+
if (input.origin === "spec-diff") {
|
|
77
|
+
return { kind: "spec", label: "from this spec" };
|
|
78
|
+
}
|
|
79
|
+
if (input.github !== undefined && input.path !== undefined) {
|
|
80
|
+
const shortSha = input.github.sha.slice(0, 7);
|
|
81
|
+
return {
|
|
82
|
+
kind: "github",
|
|
83
|
+
label: `${input.github.owner}/${input.github.repo} @ ${shortSha}`,
|
|
84
|
+
href: githubBlobUrl(
|
|
85
|
+
input.github.owner,
|
|
86
|
+
input.github.repo,
|
|
87
|
+
input.github.sha,
|
|
88
|
+
input.path,
|
|
89
|
+
),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (input.gist !== undefined) {
|
|
93
|
+
return {
|
|
94
|
+
kind: "gist",
|
|
95
|
+
label: `${input.gist.gistId} / ${input.gist.file}`,
|
|
96
|
+
href:
|
|
97
|
+
input.gist.htmlUrl.length > 0
|
|
98
|
+
? input.gist.htmlUrl
|
|
99
|
+
: githubGistUrl(input.gist.gistId),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return { kind: "none" };
|
|
103
|
+
}
|