@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.
Files changed (53) hide show
  1. package/README.md +69 -61
  2. package/index.html +18 -0
  3. package/package.json +7 -5
  4. package/src/annotations.ts +36 -29
  5. package/src/changes.ts +229 -0
  6. package/src/cli.test.ts +50 -0
  7. package/src/cli.ts +41 -5
  8. package/src/errors.ts +10 -0
  9. package/src/gist/GistApp.tsx +39 -20
  10. package/src/gist/GistStatus.tsx +2 -51
  11. package/src/gist/fetchGist.ts +23 -0
  12. package/src/git.test.ts +402 -0
  13. package/src/git.ts +498 -0
  14. package/src/github/GitHubApp.tsx +39 -22
  15. package/src/github/fetchRepo.test.ts +59 -1
  16. package/src/github/fetchRepo.ts +99 -43
  17. package/src/github/patch.test.ts +18 -9
  18. package/src/github/patch.ts +30 -19
  19. package/src/host/HostHome.tsx +86 -0
  20. package/src/host/files.ts +10 -0
  21. package/src/host/home.md +156 -0
  22. package/src/host/home.test.ts +83 -0
  23. package/src/index.ts +1 -0
  24. package/src/packageVersion.ts +7 -0
  25. package/src/parseFence.ts +6 -6
  26. package/src/parseViewer.test.ts +87 -0
  27. package/src/parseViewer.ts +103 -77
  28. package/src/serve.test.ts +306 -0
  29. package/src/serve.ts +48 -5
  30. package/src/share.ts +18 -0
  31. package/src/update.test.ts +19 -0
  32. package/src/update.ts +285 -0
  33. package/src/viewer/CallStackDiff.tsx +15 -3
  34. package/src/viewer/ChangesCard.tsx +267 -0
  35. package/src/viewer/CloseServerButton.tsx +16 -0
  36. package/src/viewer/ComarkView.tsx +83 -15
  37. package/src/viewer/Fence.tsx +9 -1
  38. package/src/viewer/FileExcerpt.tsx +25 -3
  39. package/src/viewer/HtmlPlaceholder.tsx +6 -1
  40. package/src/viewer/MermaidBlock.tsx +99 -11
  41. package/src/viewer/RangePicker.tsx +526 -0
  42. package/src/viewer/SourceDiffPanel.tsx +277 -121
  43. package/src/viewer/TableOfContents.tsx +313 -39
  44. package/src/viewer/ViewerApp.tsx +120 -68
  45. package/src/viewer/changesSource.ts +91 -0
  46. package/src/viewer/sourceHeader.test.ts +2 -87
  47. package/src/viewer/sourceHeader.ts +4 -63
  48. package/src/viewer/sourceNavigation.ts +33 -42
  49. package/src/viewer/viewerMode.ts +1 -1
  50. package/vite.config.ts +2 -1
  51. package/vite.gist.config.ts +57 -0
  52. package/src/viewer/Check.tsx +0 -24
  53. package/src/viewer/DoneButton.tsx +0 -32
@@ -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 { DoneButton } from "./DoneButton.tsx";
20
+ import { CloseServerButton } from "./CloseServerButton.tsx";
18
21
  import { DiffButton } from "./DiffButton.tsx";
19
- import { TableOfContents } from "./TableOfContents.tsx";
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
- viewerDocument.sourceDiffs.length > 0,
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
- viewerDocument.sourceDiffs.length > 0 || viewerDocument.hasReferences;
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 <main className={closed}>Closed.</main>;
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
- <DoneButton
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={body} data-has-source-diffs={diffPanelOpen}>
101
- <TableOfContents
102
- headings={viewerDocument.headings}
103
- articleRef={articleRef}
104
- />
105
- <article ref={articleRef} className={article}>
106
- <div className={prose}>
107
- <div className={content} data-diffmap-kind="page">
108
- <ComarkView
109
- document={viewerDocument}
110
- selectedAnnotation={selection?.annotation}
111
- onSelectAnnotation={(line) => {
112
- setShowDiffPanel(true);
113
- setSelection({
114
- annotation: line,
115
- reference: line.references[0]!,
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
- </div>
121
- </article>
122
- {diffPanelOpen && (
123
- <SourceDiffPanel
124
- items={viewerDocument.sourceDiffs}
125
- selection={selection}
126
- onSelect={setSelection}
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", align: "center", justify: "between" }),
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: "column" }), {
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", align: "center", gap: 3 }), {
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
- "minmax(0, max-content) minmax(0, 1fr) minmax(0, 1fr)",
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
- gridColumn: "2",
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
- "@media (max-width: 900px)": {
212
- gridColumn: "1",
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
- text({ size: "md", fontWeight: 500, color: "highContrast" }),
254
- spacing.padding({ all: 12 }),
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 type { CodeViewDiffItem } from "@pierre/diffs";
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({ origin: "file", path: "src/cli.ts" }), {
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 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.
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
- origin: SourcePanelOrigin;
72
- path: string | undefined;
16
+ path: string;
73
17
  github?: GitHubPin;
74
18
  gist?: GistPin;
75
19
  }): SourcePanelPin {
76
- if (input.origin === "spec-diff") {
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",