@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.
@@ -50,7 +50,12 @@ export function CallStackDiff(
50
50
  );
51
51
  if (line.references.length === 0) {
52
52
  return (
53
- <div key={index} className={row} data-change={sign}>
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="Show source changes"
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: "auto",
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
- minWidth: "max-content",
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 options = pierreDiffOptions({
72
- themeType: resolvedTheme,
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" && source === undefined
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 === "gist") return;
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
- <strong>
183
- {definition !== undefined
184
- ? "Symbol definition"
185
- : file !== undefined
186
- ? "Source file"
187
- : "Source changes"}
188
- </strong>
189
- {definition !== undefined && (
190
- <Button
191
- variant="quiet"
192
- onClick={() => {
193
- request.current++;
194
- setNavigation({
195
- selection,
196
- history: history.slice(0, -1),
197
- status: undefined,
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
- {history.length === 1 ? "Back to reference" : "Back"}
202
- </Button>
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
- <span>
215
- {selection.annotation.text.replace(/^[+ -]/, "").trim()}
216
- </span>
217
- {selection.annotation.references.length > 1 && (
218
- <div className={links} aria-label="Linked changes">
219
- {selection.annotation.references.map((reference, index) => (
220
- <Button
221
- key={index}
222
- variantColor="blue"
223
- variant={
224
- reference === selection.reference ? "primary" : "quiet"
225
- }
226
- onClick={() =>
227
- props.onSelect({
228
- annotation: selection.annotation,
229
- reference,
230
- })
231
- }
232
- >
233
- {referenceLabel(reference)}
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
- overflowWrap: "anywhere",
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",
@@ -164,6 +164,7 @@ const styles = {
164
164
  marginTop: "auto",
165
165
  marginBottom: "auto",
166
166
  overflowY: "auto",
167
+ backgroundColor: "transparent",
167
168
  "@media (max-width: 1100px)": {
168
169
  display: "none",
169
170
  },
@@ -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(false);
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)",
@@ -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
- <ViewerApp document={viewerDocument} mode="local" />
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
+ }