@reviewflow/ui 6.24.0 → 6.25.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/package.json +17 -2
- package/src/components/branch-diff/diff-font.ts +26 -0
- package/src/components/branch-diff/diff-themes.ts +53 -1
- package/src/components/branch-diff/reviewflow-diff-selection.ts +11 -0
- package/src/components/branch-diff/reviewflow-diff-themes.ts +2218 -0
- package/src/components/branch-diff/use-diff-settings.tsx +15 -3
- package/src/components/branch-diff/whitespace-button.tsx +2 -2
- package/src/components/diff-settings-section.tsx +189 -0
- package/src/components/diff-theme-preview.tsx +63 -0
- package/src/components/file-kind.ts +91 -0
- package/src/components/file-type-icon.tsx +70 -0
- package/src/components/pull-request-diff-annotations.tsx +5 -2
- package/src/components/pull-request-diff-jump.ts +59 -1
- package/src/components/pull-request-diff-overview-tree.ts +57 -0
- package/src/components/pull-request-diff-overview.tsx +488 -88
- package/src/components/pull-request-diff-tree-files.ts +64 -0
- package/src/components/pull-request-diff-tree.tsx +232 -0
- package/src/components/pull-request-diff-view.tsx +111 -27
- package/src/components/pull-request-diff-viewer.tsx +23 -2
- package/src/components/pull-request-files.tsx +4 -4
- package/src/components/pull-request-panel.tsx +4 -2
- package/src/components/pull-request-screen.tsx +16 -4
- package/src/components/scroll-parent.ts +20 -0
- package/src/components/tooltip-icon-button.tsx +27 -0
- package/src/editor/code-editor.tsx +1 -0
- package/src/editor/code-token-colors.css +39 -0
- package/src/editor/codeHighlight.ts +2 -1
- package/src/editor/markdown-editor.css +1 -34
- package/src/editor/markdown-editor.tsx +1 -0
- package/src/index.ts +6 -6
- package/src/palette.ts +22 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { PullRequestDiffResponse } from "@reviewflow/sdk";
|
|
2
|
+
|
|
3
|
+
export type DiffTreeFileStatus = "added" | "deleted" | "modified" | "renamed";
|
|
4
|
+
|
|
5
|
+
export interface DiffTreeFile {
|
|
6
|
+
path: string;
|
|
7
|
+
/** null for an omitted file, whose header the patch does not carry */
|
|
8
|
+
status: DiffTreeFileStatus | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const fileHeader = /^diff --git /mu;
|
|
12
|
+
const samePathHeader = /^a\/(.+) b\/\1$/u;
|
|
13
|
+
const renameTo = /^rename to (.+)$/mu;
|
|
14
|
+
const newFile = /^new file mode /mu;
|
|
15
|
+
const deletedFile = /^deleted file mode /mu;
|
|
16
|
+
|
|
17
|
+
const statusOfBlock = (block: string): DiffTreeFileStatus => {
|
|
18
|
+
if (newFile.test(block)) return "added";
|
|
19
|
+
if (deletedFile.test(block)) return "deleted";
|
|
20
|
+
if (renameTo.test(block)) return "renamed";
|
|
21
|
+
return "modified";
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const pathOfBlock = (block: string): string | undefined => {
|
|
25
|
+
const renamed = renameTo.exec(block)?.[1];
|
|
26
|
+
if (renamed !== undefined) return renamed;
|
|
27
|
+
const firstLine = block.slice(0, block.indexOf("\n"));
|
|
28
|
+
return samePathHeader.exec(firstLine)?.[1];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Read from the patch's git headers, keyed by the path after any rename. */
|
|
32
|
+
export const fileStatusesOf = (
|
|
33
|
+
patch: string,
|
|
34
|
+
): ReadonlyMap<string, DiffTreeFileStatus> =>
|
|
35
|
+
new Map(
|
|
36
|
+
patch
|
|
37
|
+
.split(fileHeader)
|
|
38
|
+
.slice(1)
|
|
39
|
+
.flatMap((block) => {
|
|
40
|
+
const path = pathOfBlock(block);
|
|
41
|
+
return path === undefined
|
|
42
|
+
? []
|
|
43
|
+
: [[path, statusOfBlock(block)] as const];
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Every file of the range: the patch's, with their status, then the omitted
|
|
49
|
+
* ones. A patch file whose header cannot be read counts as modified.
|
|
50
|
+
*/
|
|
51
|
+
export const diffTreeFilesOf = (
|
|
52
|
+
diff: Pick<PullRequestDiffResponse, "groups" | "omitted" | "patch">,
|
|
53
|
+
): DiffTreeFile[] => {
|
|
54
|
+
const statuses = fileStatusesOf(diff.patch);
|
|
55
|
+
return [
|
|
56
|
+
...diff.groups.flatMap(({ paths }) =>
|
|
57
|
+
paths.map((path) => ({
|
|
58
|
+
path,
|
|
59
|
+
status: statuses.get(path) ?? "modified",
|
|
60
|
+
})),
|
|
61
|
+
),
|
|
62
|
+
...diff.omitted.map(({ path }) => ({ path, status: null })),
|
|
63
|
+
];
|
|
64
|
+
};
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { getResolvedOrResolveTheme } from "@pierre/diffs";
|
|
2
|
+
import type {
|
|
3
|
+
FileTree as FileTreeModel,
|
|
4
|
+
FileTreeRowDecoration,
|
|
5
|
+
GitStatusEntry,
|
|
6
|
+
} from "@pierre/trees";
|
|
7
|
+
import { themeToTreeStyles } from "@pierre/trees";
|
|
8
|
+
import { FileTree, useFileTree } from "@pierre/trees/react";
|
|
9
|
+
import type { AccentOrNeutral } from "alouette";
|
|
10
|
+
import { AccentScope, useCurrentMode } from "alouette";
|
|
11
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
12
|
+
import { useEffect, useRef, useState } from "react";
|
|
13
|
+
import type { DiffTheme } from "./branch-diff/diff-themes.ts";
|
|
14
|
+
import { diffThemeNames } from "./branch-diff/diff-themes.ts";
|
|
15
|
+
import type { DiffTreeFile } from "./pull-request-diff-tree-files.ts";
|
|
16
|
+
|
|
17
|
+
export interface PullRequestDiffTreeProps {
|
|
18
|
+
files: DiffTreeFile[];
|
|
19
|
+
viewedPaths: readonly string[];
|
|
20
|
+
/** the diff's highlighting theme, whose git colors the statuses take */
|
|
21
|
+
theme: DiffTheme;
|
|
22
|
+
/** brings the file into view in the diff */
|
|
23
|
+
onFilePress: (path: string) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface DecorationData {
|
|
27
|
+
viewedPaths: ReadonlySet<string>;
|
|
28
|
+
omittedPaths: ReadonlySet<string>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type CustomProperties = CSSProperties & Record<`--${string}`, string>;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Custom properties cross the shadow root, and a `var()` in one resolves where
|
|
35
|
+
* it is declared, inside alouette's theme: the tree takes the screen's tokens.
|
|
36
|
+
* A status takes the diff theme's git color, as the diff's file headers do, and
|
|
37
|
+
* the color of its accent when the theme has none (`CapturedAccent`).
|
|
38
|
+
*/
|
|
39
|
+
const hostStyle = (
|
|
40
|
+
mode: "dark" | "light",
|
|
41
|
+
gitColors: CustomProperties,
|
|
42
|
+
): CustomProperties => ({
|
|
43
|
+
...gitColors,
|
|
44
|
+
height: "100%",
|
|
45
|
+
colorScheme: mode,
|
|
46
|
+
"--trees-bg-override": "transparent",
|
|
47
|
+
"--trees-fg-override": "var(--color-sharp)",
|
|
48
|
+
"--trees-fg-muted-override": "var(--color-muted)",
|
|
49
|
+
"--trees-bg-muted-override": "var(--color-interactive-soft-hover)",
|
|
50
|
+
"--trees-selected-bg-override": "var(--color-interactive-soft-active)",
|
|
51
|
+
"--trees-selected-fg-override": "var(--color-sharp)",
|
|
52
|
+
"--trees-border-color-override": "var(--color-border-muted)",
|
|
53
|
+
"--trees-indent-guide-bg-override": "var(--color-border-muted)",
|
|
54
|
+
"--trees-input-bg-override": "var(--color-lowered)",
|
|
55
|
+
"--trees-search-bg-override": "var(--color-lowered)",
|
|
56
|
+
"--trees-search-fg-override": "var(--color-sharp)",
|
|
57
|
+
"--trees-accent-override": "var(--tree-brand)",
|
|
58
|
+
"--trees-focus-ring-color-override": "var(--tree-brand)",
|
|
59
|
+
"--trees-status-added-override":
|
|
60
|
+
"var(--trees-theme-git-added-fg, var(--tree-success))",
|
|
61
|
+
"--trees-status-untracked-override":
|
|
62
|
+
"var(--trees-theme-git-untracked-fg, var(--tree-success))",
|
|
63
|
+
"--trees-status-modified-override":
|
|
64
|
+
"var(--trees-theme-git-modified-fg, var(--tree-info))",
|
|
65
|
+
"--trees-status-renamed-override":
|
|
66
|
+
"var(--trees-theme-git-renamed-fg, var(--tree-warning))",
|
|
67
|
+
"--trees-status-deleted-override":
|
|
68
|
+
"var(--trees-theme-git-deleted-fg, var(--tree-danger))",
|
|
69
|
+
"--trees-font-family-override": "var(--font-body)",
|
|
70
|
+
"--trees-font-size-override": "var(--text-sm, 13px)",
|
|
71
|
+
"--trees-padding-inline-override": "0px",
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
/** Holds `accent`'s color as `--tree-<accent>` for the subtree, whatever accent it is under. */
|
|
75
|
+
function CapturedAccent({
|
|
76
|
+
accent,
|
|
77
|
+
children,
|
|
78
|
+
}: {
|
|
79
|
+
accent: Exclude<AccentOrNeutral, "neutral">;
|
|
80
|
+
children: ReactNode;
|
|
81
|
+
}): ReactNode {
|
|
82
|
+
const style: CustomProperties = {
|
|
83
|
+
display: "contents",
|
|
84
|
+
[`--tree-${accent}`]: "var(--color-accent)",
|
|
85
|
+
};
|
|
86
|
+
return (
|
|
87
|
+
<AccentScope accent={accent}>
|
|
88
|
+
<div style={style}>{children}</div>
|
|
89
|
+
</AccentScope>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const gitColorsOf = (styles: Record<string, string>): CustomProperties =>
|
|
94
|
+
Object.fromEntries(
|
|
95
|
+
Object.entries(styles).filter(([key]) =>
|
|
96
|
+
key.startsWith("--trees-theme-git-"),
|
|
97
|
+
),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
interface ThemeGitColors {
|
|
101
|
+
themeName: string;
|
|
102
|
+
colors: CustomProperties;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Empty until the theme resolves, or when it cannot: the accents stand in. */
|
|
106
|
+
const useThemeGitColors = (themeName: string): CustomProperties => {
|
|
107
|
+
const [resolved, setResolved] = useState<ThemeGitColors | null>(null);
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
let current = true;
|
|
110
|
+
Promise.resolve(getResolvedOrResolveTheme(themeName)).then(
|
|
111
|
+
(theme) => {
|
|
112
|
+
if (current) {
|
|
113
|
+
setResolved({
|
|
114
|
+
themeName,
|
|
115
|
+
colors: gitColorsOf(themeToTreeStyles(theme)),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
() => {
|
|
120
|
+
if (current) setResolved({ themeName, colors: {} });
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
return () => {
|
|
124
|
+
current = false;
|
|
125
|
+
};
|
|
126
|
+
}, [themeName]);
|
|
127
|
+
return resolved?.themeName === themeName ? resolved.colors : {};
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const gitStatusOf = (files: readonly DiffTreeFile[]): GitStatusEntry[] =>
|
|
131
|
+
files.flatMap(({ path, status }) =>
|
|
132
|
+
status === null ? [] : [{ path, status }],
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const decorationOf = (
|
|
136
|
+
path: string,
|
|
137
|
+
{ viewedPaths, omittedPaths }: DecorationData,
|
|
138
|
+
): FileTreeRowDecoration | null => {
|
|
139
|
+
if (omittedPaths.has(path)) {
|
|
140
|
+
return { text: "left out", title: "Not in the diff: listed above it" };
|
|
141
|
+
}
|
|
142
|
+
if (viewedPaths.has(path)) return { text: "viewed", title: "Marked viewed" };
|
|
143
|
+
return null;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The range's files by folder, in `@pierre/trees`: searchable, virtualized,
|
|
148
|
+
* each file's git status and viewed mark on its row. Web only (DOM in a shadow
|
|
149
|
+
* root), loaded lazily as the diff viewer is.
|
|
150
|
+
*
|
|
151
|
+
* The model is created once and fed through its methods. Selecting a file
|
|
152
|
+
* scrolls the diff to it and is undone at once, so pressing the same file
|
|
153
|
+
* again scrolls again. The row decorations have no setter: they read the
|
|
154
|
+
* latest data from a ref, and resetting the git status repaints them.
|
|
155
|
+
*/
|
|
156
|
+
function PullRequestDiffTree({
|
|
157
|
+
files,
|
|
158
|
+
viewedPaths: viewedList,
|
|
159
|
+
theme,
|
|
160
|
+
onFilePress,
|
|
161
|
+
}: PullRequestDiffTreeProps): ReactNode {
|
|
162
|
+
const mode = useCurrentMode();
|
|
163
|
+
const gitColors = useThemeGitColors(diffThemeNames(theme)[mode]);
|
|
164
|
+
const paths = files.map(({ path }) => path);
|
|
165
|
+
const pathsKey = paths.join("\n");
|
|
166
|
+
const repaintKey = [
|
|
167
|
+
...files.map(({ path, status }) => `${status ?? "omitted"} ${path}`),
|
|
168
|
+
...viewedList,
|
|
169
|
+
].join("\n");
|
|
170
|
+
const viewedPaths = new Set(viewedList);
|
|
171
|
+
const omittedPaths = new Set(
|
|
172
|
+
files.filter(({ status }) => status === null).map(({ path }) => path),
|
|
173
|
+
);
|
|
174
|
+
const latest = useRef({ files, onFilePress, viewedPaths, omittedPaths });
|
|
175
|
+
const filePaths = useRef(new Set(paths));
|
|
176
|
+
const modelRef = useRef<FileTreeModel | null>(null);
|
|
177
|
+
const appliedPathsKey = useRef(pathsKey);
|
|
178
|
+
|
|
179
|
+
const { model } = useFileTree({
|
|
180
|
+
paths,
|
|
181
|
+
flattenEmptyDirectories: true,
|
|
182
|
+
initialExpansion: "open",
|
|
183
|
+
density: "compact",
|
|
184
|
+
icons: { set: "standard", colored: false },
|
|
185
|
+
search: true,
|
|
186
|
+
fileTreeSearchMode: "hide-non-matches",
|
|
187
|
+
gitStatus: gitStatusOf(files),
|
|
188
|
+
renderRowDecoration: ({ item }) =>
|
|
189
|
+
item.kind === "file" ? decorationOf(item.path, latest.current) : null,
|
|
190
|
+
onSelectionChange: (selected) => {
|
|
191
|
+
const path = selected.at(-1);
|
|
192
|
+
if (path === undefined || !filePaths.current.has(path)) return;
|
|
193
|
+
latest.current.onFilePress(path);
|
|
194
|
+
queueMicrotask(() => {
|
|
195
|
+
modelRef.current?.getItem(path)?.deselect();
|
|
196
|
+
});
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
modelRef.current = model;
|
|
202
|
+
latest.current = { files, onFilePress, viewedPaths, omittedPaths };
|
|
203
|
+
filePaths.current = new Set(paths);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
useEffect(() => {
|
|
207
|
+
const current = latest.current.files;
|
|
208
|
+
if (appliedPathsKey.current !== pathsKey) {
|
|
209
|
+
appliedPathsKey.current = pathsKey;
|
|
210
|
+
model.resetPaths(current.map(({ path }) => path));
|
|
211
|
+
}
|
|
212
|
+
model.setGitStatus(gitStatusOf(current));
|
|
213
|
+
}, [model, pathsKey, repaintKey]);
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<CapturedAccent accent="brand">
|
|
217
|
+
<CapturedAccent accent="success">
|
|
218
|
+
<CapturedAccent accent="info">
|
|
219
|
+
<CapturedAccent accent="warning">
|
|
220
|
+
<CapturedAccent accent="danger">
|
|
221
|
+
<AccentScope accent="neutral">
|
|
222
|
+
<FileTree model={model} style={hostStyle(mode, gitColors)} />
|
|
223
|
+
</AccentScope>
|
|
224
|
+
</CapturedAccent>
|
|
225
|
+
</CapturedAccent>
|
|
226
|
+
</CapturedAccent>
|
|
227
|
+
</CapturedAccent>
|
|
228
|
+
</CapturedAccent>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export default PullRequestDiffTree;
|
|
@@ -7,10 +7,11 @@ import type {
|
|
|
7
7
|
import {
|
|
8
8
|
AccentScope,
|
|
9
9
|
Code,
|
|
10
|
-
IconButton,
|
|
11
10
|
IndeterminateCircularProgress,
|
|
12
11
|
IndeterminateLinearProgress,
|
|
13
12
|
InfoMessage,
|
|
13
|
+
Tab,
|
|
14
|
+
Tabs,
|
|
14
15
|
Text,
|
|
15
16
|
View,
|
|
16
17
|
WarningMessage,
|
|
@@ -31,6 +32,7 @@ import {
|
|
|
31
32
|
useRef,
|
|
32
33
|
useState,
|
|
33
34
|
} from "react";
|
|
35
|
+
import { Platform } from "react-native";
|
|
34
36
|
import type { PullRequestDiffRefusal } from "../dataSource.ts";
|
|
35
37
|
import { usePullRequestReviewDraft } from "../services/PullRequestReviewDraftProvider.tsx";
|
|
36
38
|
import { usePullRequestSlots } from "../slots.tsx";
|
|
@@ -51,20 +53,30 @@ import type { ImpactsStatus } from "./pull-request-diff-impacts.ts";
|
|
|
51
53
|
import {
|
|
52
54
|
DiffFileElementsContext,
|
|
53
55
|
createDiffFileElements,
|
|
56
|
+
useFileInView,
|
|
54
57
|
} from "./pull-request-diff-jump.ts";
|
|
55
58
|
import type { Composer } from "./pull-request-diff-lines.ts";
|
|
56
59
|
import type { OverviewReadingOrderStatus } from "./pull-request-diff-overview.tsx";
|
|
57
60
|
import { PullRequestDiffOverview } from "./pull-request-diff-overview.tsx";
|
|
61
|
+
import { diffTreeFilesOf } from "./pull-request-diff-tree-files.ts";
|
|
58
62
|
import type {
|
|
59
63
|
DiffStyle,
|
|
60
64
|
PullRequestDiffReview,
|
|
61
65
|
} from "./pull-request-diff-viewer.tsx";
|
|
62
66
|
import { cssVariableOf, scrollToTopOf } from "./scroll-parent.ts";
|
|
67
|
+
import { TooltipIconButton } from "./tooltip-icon-button.tsx";
|
|
63
68
|
|
|
64
69
|
const PullRequestDiffViewer = lazy(
|
|
65
70
|
() => import("./pull-request-diff-viewer.tsx"),
|
|
66
71
|
);
|
|
67
72
|
|
|
73
|
+
const PullRequestDiffTree = lazy(() => import("./pull-request-diff-tree.tsx"));
|
|
74
|
+
|
|
75
|
+
/** the tree renders DOM in a shadow root */
|
|
76
|
+
const diffTreeAvailable = Platform.OS === "web";
|
|
77
|
+
|
|
78
|
+
type AsideView = "list" | "tree";
|
|
79
|
+
|
|
68
80
|
const omissionLabel = (reason: DiffOmittedFile["reason"]): string => {
|
|
69
81
|
if (reason === "binary") return "binary";
|
|
70
82
|
if (reason === "tooLarge") return "too large";
|
|
@@ -209,6 +221,7 @@ export function PullRequestDiffView({
|
|
|
209
221
|
const [toolbarHeight, setToolbarHeight] = useState(0);
|
|
210
222
|
const fileElements = useMemo(createDiffFileElements, []);
|
|
211
223
|
const highlightProbe = useRef<ComponentRef<typeof View>>(null);
|
|
224
|
+
const toolbar = useRef<ComponentRef<typeof View>>(null);
|
|
212
225
|
// a JS breakpoint rather than `xl:` classes: the sticky offset is the measured toolbar's
|
|
213
226
|
const aside = useCurrentBreakpointNameFiltered(["base", "wide"]) === "wide";
|
|
214
227
|
const whitespace = useWhitespaceToggle();
|
|
@@ -244,7 +257,8 @@ export function PullRequestDiffView({
|
|
|
244
257
|
(shownCommitComposer !== null ||
|
|
245
258
|
(review.commitOid === undefined && draft.composer !== null));
|
|
246
259
|
const mode = useCurrentMode();
|
|
247
|
-
const { wordWrap, theme, fileOrder, setFileOrder } =
|
|
260
|
+
const { wordWrap, theme, fontFamily, fileOrder, setFileOrder } =
|
|
261
|
+
useDiffSettings();
|
|
248
262
|
const missingCheckout = useMissingCheckoutReason(range);
|
|
249
263
|
const readingOrder = usePullRequestReadingOrder(
|
|
250
264
|
range,
|
|
@@ -267,6 +281,48 @@ export function PullRequestDiffView({
|
|
|
267
281
|
if (readingOrder.error !== null) return "failed";
|
|
268
282
|
return "none";
|
|
269
283
|
})();
|
|
284
|
+
const [requestedAsideView, setRequestedAsideView] =
|
|
285
|
+
useState<AsideView | null>(null);
|
|
286
|
+
const asideView = ((): AsideView => {
|
|
287
|
+
if (!diffTreeAvailable) return "list";
|
|
288
|
+
if (requestedAsideView !== null) return requestedAsideView;
|
|
289
|
+
return ordered ? "list" : "tree";
|
|
290
|
+
})();
|
|
291
|
+
const listTab = (
|
|
292
|
+
<Tab
|
|
293
|
+
value="list"
|
|
294
|
+
label={readingOrder.available ? "Reading order" : "Workspaces"}
|
|
295
|
+
/>
|
|
296
|
+
);
|
|
297
|
+
const treeFiles = useMemo(
|
|
298
|
+
() => (shownDiff === null ? [] : diffTreeFilesOf(shownDiff)),
|
|
299
|
+
[shownDiff],
|
|
300
|
+
);
|
|
301
|
+
const statuses = useMemo(
|
|
302
|
+
() =>
|
|
303
|
+
new Map(
|
|
304
|
+
treeFiles.flatMap(({ path, status }) =>
|
|
305
|
+
status === null ? [] : [[path, status] as const],
|
|
306
|
+
),
|
|
307
|
+
),
|
|
308
|
+
[treeFiles],
|
|
309
|
+
);
|
|
310
|
+
const viewedPaths = (review?.files ?? [])
|
|
311
|
+
.filter(({ viewed }) => viewed === "viewed")
|
|
312
|
+
.map(({ path }) => path);
|
|
313
|
+
const fileInView = useFileInView(
|
|
314
|
+
fileElements,
|
|
315
|
+
toolbar,
|
|
316
|
+
aside && asideView === "list",
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
const goToFile = (path: string): void => {
|
|
320
|
+
const element = fileElements.elementOf(path);
|
|
321
|
+
if (element === undefined) return;
|
|
322
|
+
scrollToTopOf(element, toolbarHeight);
|
|
323
|
+
const color = cssVariableOf(highlightProbe.current, "--color-enabled");
|
|
324
|
+
if (color !== "") fileElements.highlight(path, color);
|
|
325
|
+
};
|
|
270
326
|
|
|
271
327
|
return (
|
|
272
328
|
<View
|
|
@@ -277,6 +333,7 @@ export function PullRequestDiffView({
|
|
|
277
333
|
}
|
|
278
334
|
>
|
|
279
335
|
<View
|
|
336
|
+
ref={toolbar}
|
|
280
337
|
className="-mx-l flex-row flex-wrap items-center justify-between gap-xs bg-screen px-l py-sm web:sticky web:top-0 web:z-10"
|
|
281
338
|
onLayout={(event) => {
|
|
282
339
|
setToolbarHeight(event.nativeEvent.layout.height);
|
|
@@ -296,7 +353,7 @@ export function PullRequestDiffView({
|
|
|
296
353
|
{toolbarStart}
|
|
297
354
|
</View>
|
|
298
355
|
<View className="flex-row items-center gap-xs">
|
|
299
|
-
<
|
|
356
|
+
<TooltipIconButton
|
|
300
357
|
icon={
|
|
301
358
|
diffStyle === "split" ? (
|
|
302
359
|
<RowsRegularIcon />
|
|
@@ -313,7 +370,7 @@ export function PullRequestDiffView({
|
|
|
313
370
|
}}
|
|
314
371
|
/>
|
|
315
372
|
{readingOrder.available ? (
|
|
316
|
-
<
|
|
373
|
+
<TooltipIconButton
|
|
317
374
|
icon={<TreeStructureRegularIcon />}
|
|
318
375
|
size="sm"
|
|
319
376
|
accent={ordered ? "brand" : "neutral"}
|
|
@@ -325,6 +382,7 @@ export function PullRequestDiffView({
|
|
|
325
382
|
}
|
|
326
383
|
onPress={() => {
|
|
327
384
|
setFileOrder(ordered ? "packages" : "reading");
|
|
385
|
+
setRequestedAsideView(null);
|
|
328
386
|
}}
|
|
329
387
|
/>
|
|
330
388
|
) : null}
|
|
@@ -335,13 +393,13 @@ export function PullRequestDiffView({
|
|
|
335
393
|
}
|
|
336
394
|
onToggle={whitespace.toggle}
|
|
337
395
|
/>
|
|
338
|
-
<
|
|
396
|
+
<TooltipIconButton
|
|
339
397
|
icon={<ArrowsOutLineVerticalRegularIcon />}
|
|
340
398
|
size="sm"
|
|
341
399
|
aria-label="Open every file"
|
|
342
400
|
onPress={collapse.expandAll}
|
|
343
401
|
/>
|
|
344
|
-
<
|
|
402
|
+
<TooltipIconButton
|
|
345
403
|
icon={<ArrowsInLineVerticalRegularIcon />}
|
|
346
404
|
size="sm"
|
|
347
405
|
aria-label="Close every file"
|
|
@@ -369,31 +427,56 @@ export function PullRequestDiffView({
|
|
|
369
427
|
<View
|
|
370
428
|
className={
|
|
371
429
|
aside
|
|
372
|
-
? "w-[280px] shrink-0 web:sticky web:max-h-[calc(100vh-8rem)] web:overflow-y-auto"
|
|
430
|
+
? "w-[280px] shrink-0 gap-sm web:sticky web:max-h-[calc(100vh-8rem)] web:overflow-y-auto"
|
|
373
431
|
: ""
|
|
374
432
|
}
|
|
375
433
|
style={aside ? { top: toolbarHeight } : undefined}
|
|
376
434
|
>
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
435
|
+
{aside && diffTreeAvailable ? (
|
|
436
|
+
<Tabs
|
|
437
|
+
aria-label="Files listed"
|
|
438
|
+
value={asideView}
|
|
439
|
+
onValueChange={(value) => {
|
|
440
|
+
setRequestedAsideView(value as AsideView);
|
|
441
|
+
}}
|
|
442
|
+
>
|
|
443
|
+
{ordered ? listTab : null}
|
|
444
|
+
<Tab value="tree" label="Tree" />
|
|
445
|
+
{ordered ? null : listTab}
|
|
446
|
+
</Tabs>
|
|
447
|
+
) : null}
|
|
448
|
+
{aside && asideView === "tree" ? (
|
|
449
|
+
// a height of its own: the tree virtualizes, and grows to every row without one
|
|
450
|
+
<View className="web:h-[calc(100vh-12rem)]">
|
|
451
|
+
<Suspense fallback={<DiffLoading />}>
|
|
452
|
+
<PullRequestDiffTree
|
|
453
|
+
files={treeFiles}
|
|
454
|
+
viewedPaths={viewedPaths}
|
|
455
|
+
theme={theme}
|
|
456
|
+
onFilePress={goToFile}
|
|
457
|
+
/>
|
|
458
|
+
</Suspense>
|
|
459
|
+
</View>
|
|
460
|
+
) : (
|
|
461
|
+
<PullRequestDiffOverview
|
|
462
|
+
groups={shownDiff.groups}
|
|
463
|
+
omitted={shownDiff.omitted}
|
|
464
|
+
readingOrder={readingOrder.value}
|
|
465
|
+
readingOrderStatus={overviewStatus}
|
|
466
|
+
impacts={impacts.value}
|
|
467
|
+
impactsStatus={impactsStatus}
|
|
468
|
+
displayOrder={
|
|
469
|
+
(aside && diffTreeAvailable) || ordered
|
|
470
|
+
? readingOrder.value
|
|
471
|
+
: null
|
|
472
|
+
}
|
|
473
|
+
statuses={statuses}
|
|
474
|
+
viewedPaths={viewedPaths}
|
|
475
|
+
currentPath={fileInView}
|
|
476
|
+
aside={aside}
|
|
477
|
+
onFilePress={goToFile}
|
|
478
|
+
/>
|
|
479
|
+
)}
|
|
397
480
|
{/* the file header is pierre's shadow DOM, which no accent
|
|
398
481
|
class reaches: the color is read here and handed to it */}
|
|
399
482
|
<AccentScope accent="info">
|
|
@@ -418,6 +501,7 @@ export function PullRequestDiffView({
|
|
|
418
501
|
diffStyle={diffStyle}
|
|
419
502
|
theme={theme}
|
|
420
503
|
themeType={mode}
|
|
504
|
+
fontFamily={fontFamily}
|
|
421
505
|
wordWrap={wordWrap}
|
|
422
506
|
stickyTop={toolbarHeight}
|
|
423
507
|
collapse={collapse}
|
|
@@ -49,11 +49,16 @@ import type { PullRequestDataSource } from "../dataSource.ts";
|
|
|
49
49
|
import { usePullRequestDataSource } from "../dataSource.ts";
|
|
50
50
|
import type { PendingComment } from "../services/PullRequestReviewDraftProvider.tsx";
|
|
51
51
|
import { usePullRequestReviewDraft } from "../services/PullRequestReviewDraftProvider.tsx";
|
|
52
|
+
import { diffFontCSS } from "./branch-diff/diff-font.ts";
|
|
52
53
|
import type { DiffTheme } from "./branch-diff/diff-themes.ts";
|
|
53
|
-
import {
|
|
54
|
+
import {
|
|
55
|
+
changeBackgroundCSS,
|
|
56
|
+
diffThemeNames,
|
|
57
|
+
} from "./branch-diff/diff-themes.ts";
|
|
54
58
|
// metro picks the .web.tsx variant only for a specifier without extension
|
|
55
59
|
/* eslint-disable-next-line import-x/no-unresolved */ // oxlint-disable-next-line import/extensions
|
|
56
60
|
import { DiffWorkerPool } from "./branch-diff/diff-worker-pool";
|
|
61
|
+
import { reviewflowSelectionColors } from "./branch-diff/reviewflow-diff-selection.ts";
|
|
57
62
|
import type { CommitFileAnchoring } from "./pull-request-commit-files.ts";
|
|
58
63
|
import type {
|
|
59
64
|
AnnotationGroup,
|
|
@@ -428,6 +433,13 @@ const fileRadius = "1rem";
|
|
|
428
433
|
*/
|
|
429
434
|
const roundedFile = `:host { border-radius: ${fileRadius}; overflow: clip; box-shadow: ${surfaceShadow}; }`;
|
|
430
435
|
|
|
436
|
+
/** The reviewflow theme's selected lines mix with a lighter blue than its modified color. */
|
|
437
|
+
const selectionCSS = (theme: DiffTheme, mode: "dark" | "light"): string => {
|
|
438
|
+
if (theme !== "reviewflow") return "";
|
|
439
|
+
const color = reviewflowSelectionColors[mode];
|
|
440
|
+
return `:host { --diffs-bg-selection-override: ${color}; --diffs-bg-selection-number-override: ${color}; }`;
|
|
441
|
+
};
|
|
442
|
+
|
|
431
443
|
const pierreScrollbars =
|
|
432
444
|
"@supports not (-moz-appearance: none) { [data-code] { scrollbar-width: auto; scrollbar-color: auto; } }";
|
|
433
445
|
|
|
@@ -446,6 +458,7 @@ interface DiffFileProps {
|
|
|
446
458
|
diffStyle: DiffStyle;
|
|
447
459
|
theme: DiffTheme;
|
|
448
460
|
themeType: "dark" | "light";
|
|
461
|
+
fontFamily: string | null;
|
|
449
462
|
wordWrap: boolean;
|
|
450
463
|
stickyTop: number;
|
|
451
464
|
collapsed: boolean;
|
|
@@ -634,6 +647,7 @@ function DiffFile({
|
|
|
634
647
|
diffStyle,
|
|
635
648
|
theme,
|
|
636
649
|
themeType,
|
|
650
|
+
fontFamily,
|
|
637
651
|
wordWrap,
|
|
638
652
|
stickyTop,
|
|
639
653
|
collapsed,
|
|
@@ -722,7 +736,7 @@ function DiffFile({
|
|
|
722
736
|
overflow: wordWrap ? "wrap" : "scroll",
|
|
723
737
|
collapsed,
|
|
724
738
|
stickyHeader: true,
|
|
725
|
-
unsafeCSS: `${stickyHeaderBelow(stickyTop)} ${headerInset} ${roundedFile} ${pierreScrollbars} ${headerHighlight(highlight, collapsed)}`,
|
|
739
|
+
unsafeCSS: `${stickyHeaderBelow(stickyTop)} ${headerInset} ${roundedFile} ${pierreScrollbars} ${diffFontCSS(fontFamily)} ${selectionCSS(theme, themeType)} ${changeBackgroundCSS(theme)} ${headerHighlight(highlight, collapsed)}`,
|
|
726
740
|
loadDiffFiles: loadFiles,
|
|
727
741
|
enableGutterUtility: commentable,
|
|
728
742
|
onGutterUtilityClick: commentable ? onGutterUtilityClick : undefined,
|
|
@@ -1018,6 +1032,7 @@ interface ReviewedFilesProps {
|
|
|
1018
1032
|
diffStyle: DiffStyle;
|
|
1019
1033
|
theme: DiffTheme;
|
|
1020
1034
|
themeType: "dark" | "light";
|
|
1035
|
+
fontFamily: string | null;
|
|
1021
1036
|
wordWrap: boolean;
|
|
1022
1037
|
stickyTop: number;
|
|
1023
1038
|
collapse: PullRequestCollapse;
|
|
@@ -1034,6 +1049,7 @@ function ReviewedFiles({
|
|
|
1034
1049
|
diffStyle,
|
|
1035
1050
|
theme,
|
|
1036
1051
|
themeType,
|
|
1052
|
+
fontFamily,
|
|
1037
1053
|
wordWrap,
|
|
1038
1054
|
stickyTop,
|
|
1039
1055
|
collapse,
|
|
@@ -1174,6 +1190,7 @@ function ReviewedFiles({
|
|
|
1174
1190
|
diffStyle={diffStyle}
|
|
1175
1191
|
theme={theme}
|
|
1176
1192
|
themeType={themeType}
|
|
1193
|
+
fontFamily={fontFamily}
|
|
1177
1194
|
wordWrap={wordWrap}
|
|
1178
1195
|
stickyTop={stickyTop}
|
|
1179
1196
|
loadFiles={loadFiles}
|
|
@@ -1249,6 +1266,7 @@ interface PullRequestDiffViewerProps {
|
|
|
1249
1266
|
diffStyle: DiffStyle;
|
|
1250
1267
|
theme: DiffTheme;
|
|
1251
1268
|
themeType: "dark" | "light";
|
|
1269
|
+
fontFamily: string | null;
|
|
1252
1270
|
wordWrap: boolean;
|
|
1253
1271
|
/** where a file header sticks: the height of the toolbar stuck above it */
|
|
1254
1272
|
stickyTop: number;
|
|
@@ -1282,6 +1300,7 @@ function PullRequestDiffViewer({
|
|
|
1282
1300
|
diffStyle,
|
|
1283
1301
|
theme,
|
|
1284
1302
|
themeType,
|
|
1303
|
+
fontFamily,
|
|
1285
1304
|
wordWrap,
|
|
1286
1305
|
stickyTop,
|
|
1287
1306
|
collapse,
|
|
@@ -1334,6 +1353,7 @@ function PullRequestDiffViewer({
|
|
|
1334
1353
|
diffStyle={diffStyle}
|
|
1335
1354
|
theme={theme}
|
|
1336
1355
|
themeType={themeType}
|
|
1356
|
+
fontFamily={fontFamily}
|
|
1337
1357
|
wordWrap={wordWrap}
|
|
1338
1358
|
stickyTop={stickyTop}
|
|
1339
1359
|
loadFiles={loadFiles}
|
|
@@ -1357,6 +1377,7 @@ function PullRequestDiffViewer({
|
|
|
1357
1377
|
diffStyle={diffStyle}
|
|
1358
1378
|
theme={theme}
|
|
1359
1379
|
themeType={themeType}
|
|
1380
|
+
fontFamily={fontFamily}
|
|
1360
1381
|
wordWrap={wordWrap}
|
|
1361
1382
|
stickyTop={stickyTop}
|
|
1362
1383
|
collapse={collapse}
|