@tanishqkancharla/diffmap 0.1.4 → 0.2.0
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 +69 -61
- package/index.html +18 -0
- package/package.json +7 -5
- package/src/annotations.ts +36 -29
- package/src/changes.ts +229 -0
- package/src/cli.test.ts +50 -0
- package/src/cli.ts +41 -5
- package/src/errors.ts +10 -0
- package/src/gist/GistApp.tsx +39 -20
- package/src/gist/GistStatus.tsx +2 -51
- package/src/gist/fetchGist.ts +23 -0
- package/src/git.test.ts +402 -0
- package/src/git.ts +498 -0
- package/src/github/GitHubApp.tsx +39 -22
- package/src/github/fetchRepo.test.ts +59 -1
- package/src/github/fetchRepo.ts +99 -43
- package/src/github/patch.test.ts +18 -9
- package/src/github/patch.ts +30 -19
- package/src/host/HostHome.tsx +86 -0
- package/src/host/files.ts +10 -0
- package/src/host/home.md +156 -0
- package/src/host/home.test.ts +83 -0
- package/src/index.ts +1 -0
- package/src/packageVersion.ts +7 -0
- package/src/parseFence.ts +6 -6
- package/src/parseViewer.test.ts +87 -0
- package/src/parseViewer.ts +103 -77
- package/src/serve.test.ts +306 -0
- package/src/serve.ts +48 -5
- package/src/share.ts +18 -0
- package/src/update.test.ts +19 -0
- package/src/update.ts +285 -0
- package/src/viewer/CallStackDiff.tsx +15 -3
- package/src/viewer/ChangesCard.tsx +267 -0
- package/src/viewer/CloseServerButton.tsx +16 -0
- package/src/viewer/ComarkView.tsx +83 -15
- package/src/viewer/Fence.tsx +9 -1
- package/src/viewer/FileExcerpt.tsx +25 -3
- package/src/viewer/HtmlPlaceholder.tsx +6 -1
- package/src/viewer/MermaidBlock.tsx +99 -11
- package/src/viewer/RangePicker.tsx +526 -0
- package/src/viewer/SourceDiffPanel.tsx +277 -121
- package/src/viewer/TableOfContents.tsx +313 -39
- package/src/viewer/ViewerApp.tsx +120 -68
- package/src/viewer/changesSource.ts +91 -0
- package/src/viewer/sourceHeader.test.ts +2 -87
- package/src/viewer/sourceHeader.ts +4 -63
- package/src/viewer/sourceNavigation.ts +33 -42
- package/src/viewer/viewerMode.ts +1 -1
- package/vite.config.ts +2 -1
- package/vite.gist.config.ts +57 -0
- package/src/viewer/Check.tsx +0 -24
- package/src/viewer/DoneButton.tsx +0 -32
package/src/viewer/ViewerApp.tsx
CHANGED
|
@@ -5,18 +5,25 @@ import {
|
|
|
5
5
|
colors,
|
|
6
6
|
flex,
|
|
7
7
|
flexItem,
|
|
8
|
+
H1,
|
|
9
|
+
P,
|
|
8
10
|
proseHtml,
|
|
9
11
|
proseMaxWidth,
|
|
10
12
|
spacing,
|
|
11
13
|
text,
|
|
12
14
|
} from "maui";
|
|
13
15
|
import { style, useStyles } from "purse-styles";
|
|
16
|
+
import { ALL, WORKING, type Rev } from "../changes.js";
|
|
14
17
|
import type { ViewerDocument } from "../parseViewer.js";
|
|
15
18
|
import { ComarkView } from "./ComarkView.tsx";
|
|
16
19
|
import { SourceDiffPanel, type SourceSelection } from "./SourceDiffPanel.js";
|
|
17
|
-
import {
|
|
20
|
+
import { CloseServerButton } from "./CloseServerButton.tsx";
|
|
18
21
|
import { DiffButton } from "./DiffButton.tsx";
|
|
19
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
hasTableOfContents,
|
|
24
|
+
TableOfContents,
|
|
25
|
+
TOC_SIDE_MARGIN_PX,
|
|
26
|
+
} from "./TableOfContents.tsx";
|
|
20
27
|
import { ViewerModeContext, type ViewerMode } from "./viewerMode.ts";
|
|
21
28
|
|
|
22
29
|
type ViewerMeta = {
|
|
@@ -28,15 +35,31 @@ export function ViewerApp(props: {
|
|
|
28
35
|
mode: ViewerMode;
|
|
29
36
|
title?: string;
|
|
30
37
|
headerActions?: ReactNode;
|
|
38
|
+
/** Changes the Diff button opens before any reference is selected, such as a pull request. */
|
|
39
|
+
defaultRev?: Rev;
|
|
31
40
|
}) {
|
|
32
41
|
const viewerDocument = props.document;
|
|
33
42
|
const meta = useViewerMeta(props.mode === "local");
|
|
34
43
|
const [selection, setSelection] = useState<SourceSelection>();
|
|
35
|
-
const [showDiffPanel, setShowDiffPanel] = useState(
|
|
36
|
-
|
|
37
|
-
|
|
44
|
+
const [showDiffPanel, setShowDiffPanel] = useState(false);
|
|
45
|
+
const local = props.mode === "local";
|
|
46
|
+
const documentRev =
|
|
47
|
+
props.defaultRev ??
|
|
48
|
+
viewerDocument.changeRevs[0] ??
|
|
49
|
+
viewerDocument.references.find((ref) => ref.rev !== undefined)?.rev ??
|
|
50
|
+
(local ? ALL : WORKING);
|
|
51
|
+
const [panelRev, setPanelRev] = useState(documentRev);
|
|
52
|
+
// A reference opens its own rev; a rev-less one opens uncommitted changes locally.
|
|
53
|
+
const select = (next: SourceSelection) => {
|
|
54
|
+
setSelection(next);
|
|
55
|
+
setPanelRev(next.reference.rev ?? (local ? WORKING : documentRev));
|
|
56
|
+
};
|
|
57
|
+
// A local viewer always has git changes to show.
|
|
38
58
|
const hasSourceDiffs =
|
|
39
|
-
|
|
59
|
+
local ||
|
|
60
|
+
viewerDocument.references.length > 0 ||
|
|
61
|
+
viewerDocument.changeRevs.length > 0 ||
|
|
62
|
+
props.defaultRev !== undefined;
|
|
40
63
|
const diffPanelOpen = hasSourceDiffs && showDiffPanel;
|
|
41
64
|
const body = useStyles(styles.body);
|
|
42
65
|
const [shutDown, setShutDown] = useState(false);
|
|
@@ -55,8 +78,10 @@ export function ViewerApp(props: {
|
|
|
55
78
|
const prose = useStyles(styles.prose);
|
|
56
79
|
const content = useStyles(proseHtml("md"), styles.content);
|
|
57
80
|
const closed = useStyles(styles.closed);
|
|
81
|
+
const closedCopy = useStyles(styles.closedCopy);
|
|
82
|
+
const stage = useStyles(styles.stage);
|
|
58
83
|
const articleRef = useRef<HTMLElement>(null);
|
|
59
|
-
|
|
84
|
+
const hasToc = hasTableOfContents(viewerDocument.headings);
|
|
60
85
|
useEffect(() => {
|
|
61
86
|
document.title = title;
|
|
62
87
|
}, [title]);
|
|
@@ -68,7 +93,14 @@ export function ViewerApp(props: {
|
|
|
68
93
|
}, []);
|
|
69
94
|
|
|
70
95
|
if (shutDown) {
|
|
71
|
-
return
|
|
96
|
+
return (
|
|
97
|
+
<main className={closed}>
|
|
98
|
+
<div className={closedCopy}>
|
|
99
|
+
<H1>Closed spec</H1>
|
|
100
|
+
<P>The local server stopped.</P>
|
|
101
|
+
</div>
|
|
102
|
+
</main>
|
|
103
|
+
);
|
|
72
104
|
}
|
|
73
105
|
|
|
74
106
|
return (
|
|
@@ -87,7 +119,7 @@ export function ViewerApp(props: {
|
|
|
87
119
|
)}
|
|
88
120
|
{props.headerActions}
|
|
89
121
|
{props.mode === "local" && (
|
|
90
|
-
<
|
|
122
|
+
<CloseServerButton
|
|
91
123
|
onClick={() => {
|
|
92
124
|
setShutDown(true);
|
|
93
125
|
// oxlint-disable-next-line typescript/no-floating-promises -- React click callbacks cannot await the server shutdown request.
|
|
@@ -97,35 +129,48 @@ export function ViewerApp(props: {
|
|
|
97
129
|
)}
|
|
98
130
|
</div>
|
|
99
131
|
</header>
|
|
100
|
-
<div className={
|
|
101
|
-
<
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
132
|
+
<div className={stage}>
|
|
133
|
+
<div className={body} data-has-source-diffs={diffPanelOpen}>
|
|
134
|
+
<article ref={articleRef} className={article}>
|
|
135
|
+
<div className={prose} data-has-toc={hasToc ? "true" : "false"}>
|
|
136
|
+
<div className={content} data-diffmap-kind="page">
|
|
137
|
+
<ComarkView
|
|
138
|
+
document={viewerDocument}
|
|
139
|
+
selectedAnnotation={selection?.annotation}
|
|
140
|
+
onSelectAnnotation={(line) => {
|
|
141
|
+
setShowDiffPanel(true);
|
|
142
|
+
select({
|
|
143
|
+
annotation: line,
|
|
144
|
+
reference: line.references[0]!,
|
|
145
|
+
});
|
|
146
|
+
}}
|
|
147
|
+
/>
|
|
148
|
+
</div>
|
|
149
|
+
{hasToc && (
|
|
150
|
+
<TableOfContents
|
|
151
|
+
headings={viewerDocument.headings}
|
|
152
|
+
articleRef={articleRef}
|
|
153
|
+
/>
|
|
154
|
+
)}
|
|
119
155
|
</div>
|
|
120
|
-
</
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
156
|
+
</article>
|
|
157
|
+
{diffPanelOpen && (
|
|
158
|
+
<SourceDiffPanel
|
|
159
|
+
rev={panelRev}
|
|
160
|
+
onRevChange={
|
|
161
|
+
local
|
|
162
|
+
? (rev) => {
|
|
163
|
+
// A focused reference's lines belong to its own rev.
|
|
164
|
+
setSelection(undefined);
|
|
165
|
+
setPanelRev(rev);
|
|
166
|
+
}
|
|
167
|
+
: undefined
|
|
168
|
+
}
|
|
169
|
+
selection={selection}
|
|
170
|
+
onSelect={select}
|
|
171
|
+
/>
|
|
172
|
+
)}
|
|
173
|
+
</div>
|
|
129
174
|
</div>
|
|
130
175
|
</div>
|
|
131
176
|
</ViewerModeContext.Provider>
|
|
@@ -161,7 +206,7 @@ const styles = {
|
|
|
161
206
|
backgroundColor: colors.gray[4],
|
|
162
207
|
}),
|
|
163
208
|
header: style(
|
|
164
|
-
flex({ direction: "row",
|
|
209
|
+
flex({ direction: "row", alignItems: "center", justifyContent: "between" }),
|
|
165
210
|
spacing.padding({ x: 6, y: 3 }),
|
|
166
211
|
flexItem({ size: "hug" }),
|
|
167
212
|
border(["bottom"], "border"),
|
|
@@ -170,8 +215,9 @@ const styles = {
|
|
|
170
215
|
backgroundColor: backgroundColor.app,
|
|
171
216
|
},
|
|
172
217
|
),
|
|
173
|
-
heading: style(flex({ direction: "
|
|
218
|
+
heading: style(flex({ direction: "row", alignItems: "center", gap: 3 }), {
|
|
174
219
|
minWidth: 0,
|
|
220
|
+
flex: "1 1 auto",
|
|
175
221
|
}),
|
|
176
222
|
title: style(text({ size: "md", fontWeight: 600, color: "highContrast" }), {
|
|
177
223
|
minWidth: 0,
|
|
@@ -179,38 +225,46 @@ const styles = {
|
|
|
179
225
|
textOverflow: "ellipsis",
|
|
180
226
|
whiteSpace: "nowrap",
|
|
181
227
|
}),
|
|
182
|
-
actions: style(flex({ direction: "row",
|
|
228
|
+
actions: style(flex({ direction: "row", alignItems: "center", gap: 3 }), {
|
|
183
229
|
flexShrink: 0,
|
|
184
230
|
}),
|
|
231
|
+
stage: style(flex({ direction: "column" }), {
|
|
232
|
+
position: "relative",
|
|
233
|
+
flex: "1 1 auto",
|
|
234
|
+
minWidth: 0,
|
|
235
|
+
minHeight: 0,
|
|
236
|
+
}),
|
|
185
237
|
body: style({
|
|
186
238
|
display: "grid",
|
|
187
|
-
gridTemplateColumns: "var(--diffmap-columns)",
|
|
188
239
|
gridTemplateRows: "minmax(0, 1fr)",
|
|
189
|
-
"--diffmap-columns": "minmax(0, max-content) minmax(0, 1fr)",
|
|
190
240
|
flex: "1 1 auto",
|
|
191
241
|
minHeight: 0,
|
|
192
242
|
minWidth: 0,
|
|
193
243
|
overflow: "hidden",
|
|
194
244
|
backgroundColor: backgroundColor.app,
|
|
245
|
+
"--diffmap-columns": "minmax(0, 1fr)",
|
|
246
|
+
"--diffmap-areas": '"article"',
|
|
247
|
+
gridTemplateColumns: "var(--diffmap-columns)",
|
|
248
|
+
gridTemplateAreas: "var(--diffmap-areas)",
|
|
195
249
|
"&[data-has-source-diffs='true']": {
|
|
196
|
-
"--diffmap-columns":
|
|
197
|
-
|
|
250
|
+
"--diffmap-columns": "minmax(0, 1fr) minmax(0, 1fr)",
|
|
251
|
+
"--diffmap-areas": '"article diff"',
|
|
198
252
|
},
|
|
199
253
|
"@media (max-width: 900px)": {
|
|
200
254
|
gridTemplateColumns: "minmax(0, 1fr)",
|
|
201
255
|
gridTemplateRows: "minmax(0, 1fr) auto",
|
|
256
|
+
gridTemplateAreas: '"article" "diff"',
|
|
202
257
|
},
|
|
203
258
|
}),
|
|
204
259
|
article: style(spacing.padding({ x: 12, y: 12 }), {
|
|
205
|
-
|
|
260
|
+
gridArea: "article",
|
|
206
261
|
flex: "1 1 auto",
|
|
207
262
|
minWidth: 0,
|
|
208
263
|
minHeight: 0,
|
|
209
264
|
overflowY: "auto",
|
|
210
265
|
backgroundColor: backgroundColor.app,
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
},
|
|
266
|
+
// Scrollport size for the sticky gutter rail (`100cqh`).
|
|
267
|
+
containerType: "size",
|
|
214
268
|
}),
|
|
215
269
|
prose: style({
|
|
216
270
|
display: "grid",
|
|
@@ -218,9 +272,13 @@ const styles = {
|
|
|
218
272
|
width: "100%",
|
|
219
273
|
maxWidth: "none",
|
|
220
274
|
minWidth: 0,
|
|
275
|
+
"&[data-has-toc='true']": {
|
|
276
|
+
gridTemplateColumns: `minmax(${String(TOC_SIDE_MARGIN_PX)}px, 1fr) minmax(0, ${proseMaxWidth}) minmax(${String(TOC_SIDE_MARGIN_PX)}px, 1fr)`,
|
|
277
|
+
},
|
|
221
278
|
}),
|
|
222
279
|
content: style({
|
|
223
280
|
gridColumn: "2 / 3",
|
|
281
|
+
gridRow: "1",
|
|
224
282
|
width: "100%",
|
|
225
283
|
maxWidth: "none",
|
|
226
284
|
minWidth: 0,
|
|
@@ -230,31 +288,25 @@ const styles = {
|
|
|
230
288
|
"& ul > li[data-task]::before, & ol > li[data-task]::before": {
|
|
231
289
|
content: "none",
|
|
232
290
|
},
|
|
233
|
-
"& ul > li[data-task] > .diffmap-task-checkbox, & ol > li[data-task] > .diffmap-task-checkbox":
|
|
234
|
-
{
|
|
235
|
-
// Maui proseHtml md listPadding.
|
|
236
|
-
position: "absolute",
|
|
237
|
-
left: "-20px",
|
|
238
|
-
top: "6px",
|
|
239
|
-
},
|
|
240
|
-
"& .diffmap-task-checkbox label > span:last-child": {
|
|
241
|
-
position: "absolute",
|
|
242
|
-
width: "1px",
|
|
243
|
-
height: "1px",
|
|
244
|
-
padding: 0,
|
|
245
|
-
margin: "-1px",
|
|
246
|
-
overflow: "hidden",
|
|
247
|
-
clip: "rect(0, 0, 0, 0)",
|
|
248
|
-
whiteSpace: "nowrap",
|
|
249
|
-
border: 0,
|
|
250
|
-
},
|
|
251
291
|
}),
|
|
252
292
|
closed: style(
|
|
253
|
-
|
|
254
|
-
|
|
293
|
+
flex({
|
|
294
|
+
direction: "column",
|
|
295
|
+
alignItems: "center",
|
|
296
|
+
justifyContent: "center",
|
|
297
|
+
}),
|
|
298
|
+
spacing.padding({ x: 12, y: 12 }),
|
|
255
299
|
{
|
|
256
300
|
minHeight: "100vh",
|
|
257
301
|
backgroundColor: backgroundColor.app,
|
|
258
302
|
},
|
|
259
303
|
),
|
|
304
|
+
closedCopy: style(
|
|
305
|
+
flex({ direction: "column", alignItems: "center", gap: 3 }),
|
|
306
|
+
{
|
|
307
|
+
width: "100%",
|
|
308
|
+
maxWidth: proseMaxWidth,
|
|
309
|
+
textAlign: "center",
|
|
310
|
+
},
|
|
311
|
+
),
|
|
260
312
|
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { createContext, useContext, useEffect, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
isCommitRev,
|
|
4
|
+
type Delta,
|
|
5
|
+
type DeltasResponse,
|
|
6
|
+
type Changes,
|
|
7
|
+
type ChangesResponse,
|
|
8
|
+
type Rev,
|
|
9
|
+
} from "../changes.js";
|
|
10
|
+
import { DiffmapDefinitionError } from "../errors.js";
|
|
11
|
+
|
|
12
|
+
/** Where a viewer reads changes: the local server, GitHub, or a gist snapshot. */
|
|
13
|
+
export type ChangesLoader = (rev: Rev) => Promise<Changes | Error>;
|
|
14
|
+
|
|
15
|
+
const localCache = new Map<Rev, Promise<Changes | Error>>();
|
|
16
|
+
|
|
17
|
+
/** Commits never change, so they are cached; anything uncommitted is re-read. */
|
|
18
|
+
export const loadLocalChanges: ChangesLoader = (rev) => {
|
|
19
|
+
const cached = localCache.get(rev);
|
|
20
|
+
if (cached !== undefined) return cached;
|
|
21
|
+
const request = fetch(`/__diffmap/changes?${new URLSearchParams({ rev })}`)
|
|
22
|
+
.then((response) => response.json())
|
|
23
|
+
.then((value) => {
|
|
24
|
+
// SAFETY: the local diffmap server owns this response shape.
|
|
25
|
+
const result = value as ChangesResponse;
|
|
26
|
+
if (result.changes !== undefined) return result.changes;
|
|
27
|
+
return new DiffmapDefinitionError({
|
|
28
|
+
reason: result.error ?? "Could not load changes.",
|
|
29
|
+
});
|
|
30
|
+
})
|
|
31
|
+
.catch(
|
|
32
|
+
(cause) =>
|
|
33
|
+
new DiffmapDefinitionError({
|
|
34
|
+
reason: "Could not reach the source service.",
|
|
35
|
+
cause,
|
|
36
|
+
}),
|
|
37
|
+
);
|
|
38
|
+
if (isCommitRev(rev)) localCache.set(rev, request);
|
|
39
|
+
return request;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** The range picker's rows. Always re-read: uncommitted changes and HEAD move. */
|
|
43
|
+
export async function loadLocalDeltas(rev: Rev): Promise<Delta[] | Error> {
|
|
44
|
+
const result = await fetch(
|
|
45
|
+
`/__diffmap/deltas?${new URLSearchParams({ rev })}`,
|
|
46
|
+
)
|
|
47
|
+
.then((response) => response.json())
|
|
48
|
+
.then((value) => {
|
|
49
|
+
// SAFETY: the local diffmap server owns this response shape.
|
|
50
|
+
return value as DeltasResponse;
|
|
51
|
+
})
|
|
52
|
+
.catch(
|
|
53
|
+
(cause) =>
|
|
54
|
+
new DiffmapDefinitionError({
|
|
55
|
+
reason: "Could not reach the source service.",
|
|
56
|
+
cause,
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
if (result instanceof Error) return result;
|
|
60
|
+
return (
|
|
61
|
+
result.deltas ??
|
|
62
|
+
new DiffmapDefinitionError({
|
|
63
|
+
reason: result.error ?? "Could not list changes.",
|
|
64
|
+
})
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const ChangesLoaderContext =
|
|
69
|
+
createContext<ChangesLoader>(loadLocalChanges);
|
|
70
|
+
|
|
71
|
+
export function useChanges(rev: Rev) {
|
|
72
|
+
const load = useContext(ChangesLoaderContext);
|
|
73
|
+
const [loaded, setLoaded] = useState<{
|
|
74
|
+
rev: Rev;
|
|
75
|
+
load: ChangesLoader;
|
|
76
|
+
result: Changes | Error;
|
|
77
|
+
}>();
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
let active = true;
|
|
80
|
+
// oxlint-disable-next-line typescript/no-floating-promises -- The effect owns this request and ignores it after cleanup.
|
|
81
|
+
void load(rev).then((result) => {
|
|
82
|
+
if (active) setLoaded({ rev, load, result });
|
|
83
|
+
});
|
|
84
|
+
return () => {
|
|
85
|
+
active = false;
|
|
86
|
+
};
|
|
87
|
+
}, [rev, load]);
|
|
88
|
+
return loaded?.rev === rev && loaded.load === load
|
|
89
|
+
? loaded.result
|
|
90
|
+
: undefined;
|
|
91
|
+
}
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
sourcePanelOrigin,
|
|
6
|
-
sourcePanelPath,
|
|
7
|
-
sourcePanelPin,
|
|
8
|
-
} from "./sourceHeader.ts";
|
|
3
|
+
import { sourcePanelPin } from "./sourceHeader.ts";
|
|
9
4
|
|
|
10
5
|
const sha = "ef78bf4678d2e76a7c8bcc4087fcadf347a1fcdf";
|
|
11
6
|
const github = {
|
|
@@ -19,18 +14,9 @@ const gist = {
|
|
|
19
14
|
htmlUrl: "https://gist.github.com/a3d2fe2b2e0b1422a4fd5378baced900",
|
|
20
15
|
};
|
|
21
16
|
|
|
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
17
|
test("github file pin is owner/repo @ shortsha linking to the blob at the pin SHA", () => {
|
|
31
18
|
const path = "src/cli.ts";
|
|
32
19
|
const pin = sourcePanelPin({
|
|
33
|
-
origin: "file",
|
|
34
20
|
path,
|
|
35
21
|
github,
|
|
36
22
|
});
|
|
@@ -41,42 +27,8 @@ test("github file pin is owner/repo @ shortsha linking to the blob at the pin SH
|
|
|
41
27
|
});
|
|
42
28
|
});
|
|
43
29
|
|
|
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
30
|
test("gist pin is gist id / filename, not a fake repo", () => {
|
|
78
31
|
const pin = sourcePanelPin({
|
|
79
|
-
origin: "file",
|
|
80
32
|
path: "src/cli.ts",
|
|
81
33
|
gist,
|
|
82
34
|
});
|
|
@@ -87,45 +39,8 @@ test("gist pin is gist id / filename, not a fake repo", () => {
|
|
|
87
39
|
});
|
|
88
40
|
});
|
|
89
41
|
|
|
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
42
|
test("local file has no pin line", () => {
|
|
104
|
-
assert.deepEqual(sourcePanelPin({
|
|
43
|
+
assert.deepEqual(sourcePanelPin({ path: "src/cli.ts" }), {
|
|
105
44
|
kind: "none",
|
|
106
45
|
});
|
|
107
46
|
});
|
|
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
|
-
});
|
|
@@ -1,82 +1,23 @@
|
|
|
1
|
-
import type { CodeViewDiffItem } from "@pierre/diffs";
|
|
2
|
-
import type { SourceReference } from "../annotations.js";
|
|
3
1
|
import type { GistPin } from "../gist/pin.ts";
|
|
4
2
|
import { githubGistUrl } from "../gist/route.ts";
|
|
5
3
|
import type { GitHubPin } from "../github/pin.ts";
|
|
6
4
|
import { githubBlobUrl } from "../github/route.ts";
|
|
7
5
|
|
|
8
|
-
const PR_DIFF_PREFIX = "pr:";
|
|
9
|
-
|
|
10
|
-
export type SourcePanelOrigin = "file" | "spec-diff" | "repo-diff";
|
|
11
|
-
|
|
12
6
|
export type SourcePanelPin =
|
|
13
7
|
| { kind: "github"; label: string; href: string }
|
|
14
8
|
| { kind: "gist"; label: string; href: string }
|
|
15
|
-
| { kind: "spec"; label: "from this spec" }
|
|
16
9
|
| { kind: "none" };
|
|
17
10
|
|
|
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
11
|
/**
|
|
67
|
-
* Pin line
|
|
68
|
-
* SHA for the open path.
|
|
12
|
+
* Pin line under a single open file. A hosted spec is one SHA; blob links use
|
|
13
|
+
* that SHA for the open path.
|
|
69
14
|
*/
|
|
70
15
|
export function sourcePanelPin(input: {
|
|
71
|
-
|
|
72
|
-
path: string | undefined;
|
|
16
|
+
path: string;
|
|
73
17
|
github?: GitHubPin;
|
|
74
18
|
gist?: GistPin;
|
|
75
19
|
}): SourcePanelPin {
|
|
76
|
-
if (input.
|
|
77
|
-
return { kind: "spec", label: "from this spec" };
|
|
78
|
-
}
|
|
79
|
-
if (input.github !== undefined && input.path !== undefined) {
|
|
20
|
+
if (input.github !== undefined) {
|
|
80
21
|
const shortSha = input.github.sha.slice(0, 7);
|
|
81
22
|
return {
|
|
82
23
|
kind: "github",
|