@tanishqkancharla/diffmap 0.1.3 → 0.1.4
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 +13 -10
- package/package.json +2 -2
- package/src/cli.ts +94 -54
- package/src/contentPlugin.ts +5 -3
- package/src/errors.ts +1 -1
- package/src/gist/GistApp.tsx +31 -19
- package/src/gist/GistStatus.tsx +22 -8
- 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/index.ts +1 -0
- package/src/mermaid.ts +29 -1
- package/src/parseViewer.test.ts +185 -0
- package/src/parseViewer.ts +27 -3
- package/src/serve.ts +9 -1
- package/src/share.ts +5 -1
- package/src/viewer/CallStackDiff.tsx +26 -6
- package/src/viewer/FileExcerpt.tsx +28 -1
- package/src/viewer/SourceDiffPanel.tsx +131 -54
- package/src/viewer/TableOfContents.tsx +1 -0
- package/src/viewer/ViewerApp.tsx +4 -1
- 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 +46 -2
- package/src/viewer/viewerMode.ts +1 -1
- package/src/viewer/vite-env.d.ts +2 -1
|
@@ -50,7 +50,12 @@ export function CallStackDiff(
|
|
|
50
50
|
);
|
|
51
51
|
if (line.references.length === 0) {
|
|
52
52
|
return (
|
|
53
|
-
<div
|
|
53
|
+
<div
|
|
54
|
+
key={index}
|
|
55
|
+
className={row}
|
|
56
|
+
data-change={sign}
|
|
57
|
+
title={description}
|
|
58
|
+
>
|
|
54
59
|
{content}
|
|
55
60
|
</div>
|
|
56
61
|
);
|
|
@@ -63,7 +68,7 @@ export function CallStackDiff(
|
|
|
63
68
|
data-change={sign}
|
|
64
69
|
aria-pressed={props.selectedAnnotation === line}
|
|
65
70
|
aria-controls="source-diff-panel"
|
|
66
|
-
title=
|
|
71
|
+
title={description}
|
|
67
72
|
aria-label={description}
|
|
68
73
|
onClick={() => props.onSelectAnnotation(line)}
|
|
69
74
|
>
|
|
@@ -81,20 +86,24 @@ const styles = {
|
|
|
81
86
|
spacing.padding({ y: 2 }),
|
|
82
87
|
{
|
|
83
88
|
backgroundColor: backgroundColor.app,
|
|
84
|
-
overflowX: "
|
|
89
|
+
overflowX: "hidden",
|
|
90
|
+
maxWidth: "100%",
|
|
85
91
|
},
|
|
86
92
|
),
|
|
87
93
|
row: style(
|
|
88
94
|
spacing.padding({ x: 3 }),
|
|
89
95
|
{
|
|
90
96
|
display: "flex",
|
|
97
|
+
alignItems: "center",
|
|
91
98
|
gap: "12px",
|
|
99
|
+
boxSizing: "border-box",
|
|
92
100
|
width: "100%",
|
|
93
|
-
|
|
101
|
+
maxWidth: "100%",
|
|
102
|
+
minWidth: 0,
|
|
103
|
+
overflow: "hidden",
|
|
94
104
|
textAlign: "left",
|
|
95
105
|
font: "inherit",
|
|
96
106
|
lineHeight: "1.6",
|
|
97
|
-
whiteSpace: "pre",
|
|
98
107
|
color: "inherit",
|
|
99
108
|
background: "transparent",
|
|
100
109
|
border: 0,
|
|
@@ -109,9 +118,19 @@ const styles = {
|
|
|
109
118
|
"& .stack-content": {
|
|
110
119
|
display: "flex",
|
|
111
120
|
alignItems: "stretch",
|
|
121
|
+
flex: "1 1 auto",
|
|
122
|
+
minWidth: 0,
|
|
112
123
|
minHeight: "28px",
|
|
124
|
+
overflow: "hidden",
|
|
125
|
+
},
|
|
126
|
+
"& .stack-label": {
|
|
127
|
+
flex: "1 1 auto",
|
|
128
|
+
minWidth: 0,
|
|
129
|
+
paddingBlock: "3px",
|
|
130
|
+
overflow: "hidden",
|
|
131
|
+
textOverflow: "ellipsis",
|
|
132
|
+
whiteSpace: "nowrap",
|
|
113
133
|
},
|
|
114
|
-
"& .stack-label": { paddingBlock: "3px" },
|
|
115
134
|
"& .stack-comment": { color: colors.gray[11], fontWeight: 400 },
|
|
116
135
|
"& .tree-prefix": { display: "inline-flex", alignSelf: "stretch" },
|
|
117
136
|
"& [data-branch]": {
|
|
@@ -147,6 +166,7 @@ const styles = {
|
|
|
147
166
|
"& > .diff-sign": { alignSelf: "center" },
|
|
148
167
|
"& > .source-indicator": {
|
|
149
168
|
alignSelf: "center",
|
|
169
|
+
flexShrink: 0,
|
|
150
170
|
marginLeft: "auto",
|
|
151
171
|
color: colors.blue[11],
|
|
152
172
|
},
|
|
@@ -4,6 +4,8 @@ import { useTheme } from "maui";
|
|
|
4
4
|
import { useStyles } from "purse-styles";
|
|
5
5
|
import { pierreFileOptions, pierreShell } from "./pierre.ts";
|
|
6
6
|
import { useViewerMode } from "./viewerMode.ts";
|
|
7
|
+
import { useGitHubPin } from "../github/pin.ts";
|
|
8
|
+
import { fetchPinnedBlob } from "../github/fetchRepo.ts";
|
|
7
9
|
|
|
8
10
|
type FileExcerptPayload = {
|
|
9
11
|
path: string;
|
|
@@ -40,8 +42,33 @@ export function FileExcerpt(props: {
|
|
|
40
42
|
function useFileExcerpt(input: { path: string; start: number; end: number }) {
|
|
41
43
|
const [excerpt, setExcerpt] = useState<FileExcerptPayload>();
|
|
42
44
|
const mode = useViewerMode();
|
|
45
|
+
const pin = useGitHubPin();
|
|
43
46
|
useEffect(() => {
|
|
44
47
|
if (mode === "gist") return;
|
|
48
|
+
if (mode === "github") {
|
|
49
|
+
if (pin === undefined) return;
|
|
50
|
+
// oxlint-disable-next-line typescript/no-floating-promises -- React effects cannot await; this request owns the excerpt update.
|
|
51
|
+
void fetchPinnedBlob(pin, input.path)
|
|
52
|
+
.then((contents) => {
|
|
53
|
+
if (contents instanceof Error) return undefined;
|
|
54
|
+
const lines = contents.split(/\r?\n/);
|
|
55
|
+
const start = input.start;
|
|
56
|
+
const end = Math.min(input.end, lines.length);
|
|
57
|
+
return {
|
|
58
|
+
path: input.path,
|
|
59
|
+
start,
|
|
60
|
+
end,
|
|
61
|
+
contents: lines.slice(start - 1, end).join("\n"),
|
|
62
|
+
} satisfies FileExcerptPayload;
|
|
63
|
+
})
|
|
64
|
+
.then((value) => {
|
|
65
|
+
if (value !== undefined) setExcerpt(value);
|
|
66
|
+
})
|
|
67
|
+
.catch((cause) => {
|
|
68
|
+
console.warn("diffmap file excerpt failed", cause);
|
|
69
|
+
});
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
45
72
|
const params = new URLSearchParams({
|
|
46
73
|
path: input.path,
|
|
47
74
|
start: String(input.start),
|
|
@@ -60,6 +87,6 @@ function useFileExcerpt(input: { path: string; start: number; end: number }) {
|
|
|
60
87
|
.catch((cause) => {
|
|
61
88
|
console.warn("diffmap file excerpt failed", cause);
|
|
62
89
|
});
|
|
63
|
-
}, [input.path, input.start, input.end, mode]);
|
|
90
|
+
}, [input.path, input.start, input.end, mode, pin]);
|
|
64
91
|
return excerpt;
|
|
65
92
|
}
|
|
@@ -22,12 +22,19 @@ import {
|
|
|
22
22
|
type SourceReference,
|
|
23
23
|
} from "../annotations.js";
|
|
24
24
|
import type { SourceDefinition } from "../definitions.js";
|
|
25
|
+
import { useGistPin } from "../gist/pin.ts";
|
|
26
|
+
import { useGitHubPin } from "../github/pin.ts";
|
|
25
27
|
import {
|
|
26
28
|
matchingSourceDiff,
|
|
27
29
|
requestSource,
|
|
28
30
|
useSourceReference,
|
|
29
31
|
} from "./sourceNavigation.js";
|
|
30
32
|
import { pierreDiffOptions, sourceSelectionCss } from "./pierre.js";
|
|
33
|
+
import {
|
|
34
|
+
sourcePanelOrigin,
|
|
35
|
+
sourcePanelPath,
|
|
36
|
+
sourcePanelPin,
|
|
37
|
+
} from "./sourceHeader.ts";
|
|
31
38
|
import { useViewerMode } from "./viewerMode.ts";
|
|
32
39
|
|
|
33
40
|
export type SourceSelection = {
|
|
@@ -66,12 +73,13 @@ export function SourceDiffPanel(props: {
|
|
|
66
73
|
const definition = history.at(-1);
|
|
67
74
|
const panel = useStyles(styles.panel);
|
|
68
75
|
const header = useStyles(styles.header);
|
|
76
|
+
const titleRow = useStyles(styles.titleRow);
|
|
77
|
+
const titleClass = useStyles(styles.title);
|
|
78
|
+
const pinClass = useStyles(styles.pin);
|
|
69
79
|
const links = useStyles(styles.links);
|
|
70
80
|
const code = useStyles(styles.code);
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
disableFileHeader: false,
|
|
74
|
-
});
|
|
81
|
+
const github = useGitHubPin();
|
|
82
|
+
const gist = useGistPin();
|
|
75
83
|
const selection = props.selection;
|
|
76
84
|
const source = useSourceReference(selection?.reference);
|
|
77
85
|
const resolvedFile = source instanceof Error ? undefined : source;
|
|
@@ -93,7 +101,9 @@ export function SourceDiffPanel(props: {
|
|
|
93
101
|
? navigation.status
|
|
94
102
|
: source instanceof Error
|
|
95
103
|
? source.message
|
|
96
|
-
: selection?.reference.kind === "file" &&
|
|
104
|
+
: selection?.reference.kind === "file" &&
|
|
105
|
+
source === undefined &&
|
|
106
|
+
mode !== "gist"
|
|
97
107
|
? "Opening source…"
|
|
98
108
|
: undefined;
|
|
99
109
|
const selectedLines = useMemo<CodeViewLineSelection | null>(() => {
|
|
@@ -113,6 +123,32 @@ export function SourceDiffPanel(props: {
|
|
|
113
123
|
};
|
|
114
124
|
}, [selection, file, linkedDiff]);
|
|
115
125
|
|
|
126
|
+
const openPath = sourcePanelPath({
|
|
127
|
+
filePath: file?.path,
|
|
128
|
+
reference: selection?.reference,
|
|
129
|
+
items: props.items,
|
|
130
|
+
linkedDiffId: linkedDiff?.id,
|
|
131
|
+
});
|
|
132
|
+
const origin = sourcePanelOrigin({
|
|
133
|
+
showingFile: file !== undefined,
|
|
134
|
+
reference: selection?.reference,
|
|
135
|
+
items: props.items,
|
|
136
|
+
linkedDiffId: linkedDiff?.id,
|
|
137
|
+
});
|
|
138
|
+
const pin =
|
|
139
|
+
openPath === undefined
|
|
140
|
+
? { kind: "none" as const }
|
|
141
|
+
: sourcePanelPin({
|
|
142
|
+
origin,
|
|
143
|
+
path: openPath,
|
|
144
|
+
github,
|
|
145
|
+
gist,
|
|
146
|
+
});
|
|
147
|
+
const options = pierreDiffOptions({
|
|
148
|
+
themeType: resolvedTheme,
|
|
149
|
+
disableFileHeader: openPath !== undefined,
|
|
150
|
+
});
|
|
151
|
+
|
|
116
152
|
useEffect(() => {
|
|
117
153
|
if (selectedLines === null) return;
|
|
118
154
|
viewer.current?.scrollTo({
|
|
@@ -128,7 +164,7 @@ export function SourceDiffPanel(props: {
|
|
|
128
164
|
event,
|
|
129
165
|
context,
|
|
130
166
|
) => {
|
|
131
|
-
if (mode
|
|
167
|
+
if (mode !== "local") return;
|
|
132
168
|
if (!event.metaKey && !event.ctrlKey) return;
|
|
133
169
|
event.preventDefault();
|
|
134
170
|
const item = context.item;
|
|
@@ -178,28 +214,53 @@ export function SourceDiffPanel(props: {
|
|
|
178
214
|
|
|
179
215
|
return (
|
|
180
216
|
<aside id="source-diff-panel" aria-label="Source changes" className={panel}>
|
|
181
|
-
<div className={header}>
|
|
182
|
-
|
|
183
|
-
{
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
217
|
+
<div className={header} data-diffmap-kind="source-header">
|
|
218
|
+
{openPath !== undefined && (
|
|
219
|
+
<div className={titleRow}>
|
|
220
|
+
<div className={titleClass} title={openPath}>
|
|
221
|
+
{openPath}
|
|
222
|
+
</div>
|
|
223
|
+
{definition !== undefined && (
|
|
224
|
+
<Button
|
|
225
|
+
variant="quiet"
|
|
226
|
+
onClick={() => {
|
|
227
|
+
request.current++;
|
|
228
|
+
setNavigation({
|
|
229
|
+
selection,
|
|
230
|
+
history: history.slice(0, -1),
|
|
231
|
+
status: undefined,
|
|
232
|
+
});
|
|
233
|
+
}}
|
|
234
|
+
>
|
|
235
|
+
{history.length === 1 ? "Back to reference" : "Back"}
|
|
236
|
+
</Button>
|
|
237
|
+
)}
|
|
238
|
+
</div>
|
|
239
|
+
)}
|
|
240
|
+
{pin.kind === "github" && (
|
|
241
|
+
<a
|
|
242
|
+
className={pinClass}
|
|
243
|
+
href={pin.href}
|
|
244
|
+
data-diffmap-pin="github"
|
|
245
|
+
title={pin.label}
|
|
246
|
+
>
|
|
247
|
+
{pin.label}
|
|
248
|
+
</a>
|
|
249
|
+
)}
|
|
250
|
+
{pin.kind === "gist" && (
|
|
251
|
+
<a
|
|
252
|
+
className={pinClass}
|
|
253
|
+
href={pin.href}
|
|
254
|
+
data-diffmap-pin="gist"
|
|
255
|
+
title={pin.label}
|
|
200
256
|
>
|
|
201
|
-
{
|
|
202
|
-
</
|
|
257
|
+
{pin.label}
|
|
258
|
+
</a>
|
|
259
|
+
)}
|
|
260
|
+
{pin.kind === "spec" && (
|
|
261
|
+
<span className={pinClass} data-diffmap-pin="spec">
|
|
262
|
+
{pin.label}
|
|
263
|
+
</span>
|
|
203
264
|
)}
|
|
204
265
|
{mode === "local" && (
|
|
205
266
|
<span>⌘-click or Ctrl-click a symbol to go to its definition.</span>
|
|
@@ -210,32 +271,27 @@ export function SourceDiffPanel(props: {
|
|
|
210
271
|
Select a linked stack row or diagram element to open its source.
|
|
211
272
|
</span>
|
|
212
273
|
) : (
|
|
213
|
-
|
|
214
|
-
<
|
|
215
|
-
{selection.annotation.
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
</Button>
|
|
235
|
-
))}
|
|
236
|
-
</div>
|
|
237
|
-
)}
|
|
238
|
-
</>
|
|
274
|
+
selection.annotation.references.length > 1 && (
|
|
275
|
+
<div className={links} aria-label="Linked changes">
|
|
276
|
+
{selection.annotation.references.map((reference, index) => (
|
|
277
|
+
<Button
|
|
278
|
+
key={index}
|
|
279
|
+
variantColor="blue"
|
|
280
|
+
variant={
|
|
281
|
+
reference === selection.reference ? "primary" : "quiet"
|
|
282
|
+
}
|
|
283
|
+
onClick={() =>
|
|
284
|
+
props.onSelect({
|
|
285
|
+
annotation: selection.annotation,
|
|
286
|
+
reference,
|
|
287
|
+
})
|
|
288
|
+
}
|
|
289
|
+
>
|
|
290
|
+
{referenceLabel(reference)}
|
|
291
|
+
</Button>
|
|
292
|
+
))}
|
|
293
|
+
</div>
|
|
294
|
+
)
|
|
239
295
|
)}
|
|
240
296
|
</div>
|
|
241
297
|
<CodeView
|
|
@@ -289,12 +345,33 @@ const styles = {
|
|
|
289
345
|
text({ size: "sm", color: "lowContrast" }),
|
|
290
346
|
{
|
|
291
347
|
gap: "6px",
|
|
292
|
-
|
|
348
|
+
minWidth: 0,
|
|
293
349
|
maxHeight: "35%",
|
|
294
350
|
overflowY: "auto",
|
|
295
351
|
flexShrink: 0,
|
|
296
352
|
},
|
|
297
353
|
),
|
|
354
|
+
titleRow: style(flex({ direction: "row", align: "center" }), {
|
|
355
|
+
gap: "6px",
|
|
356
|
+
minWidth: 0,
|
|
357
|
+
}),
|
|
358
|
+
title: style(text({ size: "sm", fontWeight: 600, color: "highContrast" }), {
|
|
359
|
+
minWidth: 0,
|
|
360
|
+
flex: "1 1 auto",
|
|
361
|
+
overflow: "hidden",
|
|
362
|
+
textOverflow: "ellipsis",
|
|
363
|
+
whiteSpace: "nowrap",
|
|
364
|
+
}),
|
|
365
|
+
pin: style(text({ size: "sm", color: "lowContrast" }), {
|
|
366
|
+
minWidth: 0,
|
|
367
|
+
overflow: "hidden",
|
|
368
|
+
textOverflow: "ellipsis",
|
|
369
|
+
whiteSpace: "nowrap",
|
|
370
|
+
textDecoration: "none",
|
|
371
|
+
"&:is(a):hover": {
|
|
372
|
+
textDecoration: "underline",
|
|
373
|
+
},
|
|
374
|
+
}),
|
|
298
375
|
links: style(flex({ direction: "row" }), { gap: "6px", flexWrap: "wrap" }),
|
|
299
376
|
code: style({
|
|
300
377
|
flex: "1 1 auto",
|
package/src/viewer/ViewerApp.tsx
CHANGED
|
@@ -32,7 +32,9 @@ export function ViewerApp(props: {
|
|
|
32
32
|
const viewerDocument = props.document;
|
|
33
33
|
const meta = useViewerMeta(props.mode === "local");
|
|
34
34
|
const [selection, setSelection] = useState<SourceSelection>();
|
|
35
|
-
const [showDiffPanel, setShowDiffPanel] = useState(
|
|
35
|
+
const [showDiffPanel, setShowDiffPanel] = useState(
|
|
36
|
+
viewerDocument.sourceDiffs.length > 0,
|
|
37
|
+
);
|
|
36
38
|
const hasSourceDiffs =
|
|
37
39
|
viewerDocument.sourceDiffs.length > 0 || viewerDocument.hasReferences;
|
|
38
40
|
const diffPanelOpen = hasSourceDiffs && showDiffPanel;
|
|
@@ -189,6 +191,7 @@ const styles = {
|
|
|
189
191
|
minHeight: 0,
|
|
190
192
|
minWidth: 0,
|
|
191
193
|
overflow: "hidden",
|
|
194
|
+
backgroundColor: backgroundColor.app,
|
|
192
195
|
"&[data-has-source-diffs='true']": {
|
|
193
196
|
"--diffmap-columns":
|
|
194
197
|
"minmax(0, max-content) minmax(0, 1fr) minmax(0, 1fr)",
|
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
|
+
}
|