Restore compact Changes diff controls (#1945)
* feat(app): restore compact Changes diff controls Keep the folder tree behind a view toggle while preserving the flat file list as the default. Move secondary diff options into the overflow menu so the Changes toolbar stays compact. * fix(app): mirror whitespace diff option copy * fix(app): polish diff controls review follow-up
This commit is contained in:
@@ -218,6 +218,38 @@ test("changes diff keeps code rows aligned with the gutter", async ({ page }) =>
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("changes diff switches between flat and tree file lists", async ({ page }) => {
|
||||||
|
const workspace = await createWorkspaceWithMountedTabDiff();
|
||||||
|
await useUnwrappedDiffLines(page);
|
||||||
|
await openWorkspaceChanges(page, workspace);
|
||||||
|
|
||||||
|
await expectFlatFileList(page);
|
||||||
|
await expect(page.getByTestId("changes-toggle-layout")).toBeVisible();
|
||||||
|
await expect(page.getByTestId("changes-layout-unified")).toHaveCount(0);
|
||||||
|
await expect(page.getByTestId("changes-layout-split")).toHaveCount(0);
|
||||||
|
|
||||||
|
await page.getByTestId("changes-options-menu").click();
|
||||||
|
await expect(page.getByTestId("changes-options-menu-content")).toBeVisible();
|
||||||
|
await expect(page.getByTestId("changes-toggle-whitespace")).toContainText("Hide whitespace");
|
||||||
|
await expect(page.getByTestId("changes-toggle-wrap-lines")).toContainText("Wrap long lines");
|
||||||
|
await expect(page.getByTestId("changes-refresh")).toContainText("Refresh");
|
||||||
|
await page.getByTestId("changes-toggle-whitespace").click();
|
||||||
|
await page.getByTestId("changes-options-menu").click();
|
||||||
|
await expect(page.getByTestId("changes-toggle-whitespace")).toContainText("Show whitespace");
|
||||||
|
await page.keyboard.press("Escape");
|
||||||
|
|
||||||
|
await scrollToLowerUnwrappedDiffRows(page);
|
||||||
|
await page.getByTestId("changes-toggle-view-mode").click();
|
||||||
|
await expect(page.getByTestId("diff-folder-src")).toBeVisible();
|
||||||
|
await expect(page.getByTestId("diff-file-0")).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByTestId("diff-folder-src-toggle").click();
|
||||||
|
await expect(page.getByTestId("diff-file-0")).toHaveCount(0);
|
||||||
|
|
||||||
|
await page.getByTestId("changes-toggle-view-mode").click();
|
||||||
|
await expectFlatFileList(page);
|
||||||
|
});
|
||||||
|
|
||||||
test("changes diff keeps unwrapped gutter and code rows aligned after code size changes", async ({
|
test("changes diff keeps unwrapped gutter and code rows aligned after code size changes", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -266,13 +298,24 @@ async function useUnwrappedDiffLines(page: Page): Promise<void> {
|
|||||||
({ preferencesKey }) => {
|
({ preferencesKey }) => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
preferencesKey,
|
preferencesKey,
|
||||||
JSON.stringify({ layout: "unified", wrapLines: false, hideWhitespace: false }),
|
JSON.stringify({
|
||||||
|
layout: "unified",
|
||||||
|
viewMode: "flat",
|
||||||
|
wrapLines: false,
|
||||||
|
hideWhitespace: false,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
{ preferencesKey: CHANGES_PREFERENCES_KEY },
|
{ preferencesKey: CHANGES_PREFERENCES_KEY },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function expectFlatFileList(page: Page): Promise<void> {
|
||||||
|
await expect(page.locator('[data-testid^="diff-folder-"]')).toHaveCount(0);
|
||||||
|
await expect(page.getByTestId("diff-file-0")).toContainText("use-mounted-tab-set.ts");
|
||||||
|
await expect(page.getByTestId("diff-file-0")).toContainText("src");
|
||||||
|
}
|
||||||
|
|
||||||
async function expectDiffCodeFontSize(page: Page, fontSize: number): Promise<void> {
|
async function expectDiffCodeFontSize(page: Page, fontSize: number): Promise<void> {
|
||||||
await expect
|
await expect
|
||||||
.poll(async () => {
|
.poll(async () => {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ describe("buildDiffFlatItems", () => {
|
|||||||
it("emits a body and a sticky index for each expanded file", () => {
|
it("emits a body and a sticky index for each expanded file", () => {
|
||||||
const { items, stickyHeaderIndices } = buildDiffFlatItems({
|
const { items, stickyHeaderIndices } = buildDiffFlatItems({
|
||||||
files: [createFile("a.ts"), createFile("b.ts")],
|
files: [createFile("a.ts"), createFile("b.ts")],
|
||||||
|
viewMode: "tree",
|
||||||
collapsedFolders: new Set(),
|
collapsedFolders: new Set(),
|
||||||
expandedPaths: new Set(["a.ts"]),
|
expandedPaths: new Set(["a.ts"]),
|
||||||
});
|
});
|
||||||
@@ -32,9 +33,23 @@ describe("buildDiffFlatItems", () => {
|
|||||||
expect(items[0].type).toBe("header");
|
expect(items[0].type).toBe("header");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("emits the old flat list without folder rows or indentation", () => {
|
||||||
|
const { items, stickyHeaderIndices } = buildDiffFlatItems({
|
||||||
|
files,
|
||||||
|
viewMode: "flat",
|
||||||
|
collapsedFolders: new Set(),
|
||||||
|
expandedPaths: new Set(["src/app/a.ts"]),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(summarize(items)).toEqual(["a.ts", "body:a.ts", "b.ts"]);
|
||||||
|
expect(stickyHeaderIndices).toEqual([0]);
|
||||||
|
expect(items.every((item) => item.type !== "folder")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("groups files under compressed folder rows, all expanded by default", () => {
|
it("groups files under compressed folder rows, all expanded by default", () => {
|
||||||
const { items } = buildDiffFlatItems({
|
const { items } = buildDiffFlatItems({
|
||||||
files,
|
files,
|
||||||
|
viewMode: "tree",
|
||||||
collapsedFolders: new Set(),
|
collapsedFolders: new Set(),
|
||||||
expandedPaths: new Set(),
|
expandedPaths: new Set(),
|
||||||
});
|
});
|
||||||
@@ -45,6 +60,7 @@ describe("buildDiffFlatItems", () => {
|
|||||||
it("collapsing a folder hides its descendants but keeps the row", () => {
|
it("collapsing a folder hides its descendants but keeps the row", () => {
|
||||||
const { items } = buildDiffFlatItems({
|
const { items } = buildDiffFlatItems({
|
||||||
files,
|
files,
|
||||||
|
viewMode: "tree",
|
||||||
collapsedFolders: new Set(["src/app/nested"]),
|
collapsedFolders: new Set(["src/app/nested"]),
|
||||||
expandedPaths: new Set(),
|
expandedPaths: new Set(),
|
||||||
});
|
});
|
||||||
@@ -54,6 +70,7 @@ describe("buildDiffFlatItems", () => {
|
|||||||
it("collapsing an ancestor hides everything below it", () => {
|
it("collapsing an ancestor hides everything below it", () => {
|
||||||
const { items } = buildDiffFlatItems({
|
const { items } = buildDiffFlatItems({
|
||||||
files,
|
files,
|
||||||
|
viewMode: "tree",
|
||||||
collapsedFolders: new Set(["src/app"]),
|
collapsedFolders: new Set(["src/app"]),
|
||||||
expandedPaths: new Set(),
|
expandedPaths: new Set(),
|
||||||
});
|
});
|
||||||
@@ -65,6 +82,7 @@ describe("buildDiffFlatItems", () => {
|
|||||||
// header at index 3, with the body right after.
|
// header at index 3, with the body right after.
|
||||||
const { items, stickyHeaderIndices } = buildDiffFlatItems({
|
const { items, stickyHeaderIndices } = buildDiffFlatItems({
|
||||||
files,
|
files,
|
||||||
|
viewMode: "tree",
|
||||||
collapsedFolders: new Set(),
|
collapsedFolders: new Set(),
|
||||||
expandedPaths: new Set(["src/app/a.ts"]),
|
expandedPaths: new Set(["src/app/a.ts"]),
|
||||||
});
|
});
|
||||||
@@ -84,6 +102,7 @@ describe("buildDiffFlatItems", () => {
|
|||||||
it("maps tree file rows back to their original files array index", () => {
|
it("maps tree file rows back to their original files array index", () => {
|
||||||
const { items } = buildDiffFlatItems({
|
const { items } = buildDiffFlatItems({
|
||||||
files,
|
files,
|
||||||
|
viewMode: "tree",
|
||||||
collapsedFolders: new Set(),
|
collapsedFolders: new Set(),
|
||||||
expandedPaths: new Set(),
|
expandedPaths: new Set(),
|
||||||
});
|
});
|
||||||
@@ -96,6 +115,7 @@ describe("buildDiffFlatItems", () => {
|
|||||||
describe("sumHeightsBefore", () => {
|
describe("sumHeightsBefore", () => {
|
||||||
const items = buildDiffFlatItems({
|
const items = buildDiffFlatItems({
|
||||||
files: [createFile("src/a.ts"), createFile("src/b.ts")],
|
files: [createFile("src/a.ts"), createFile("src/b.ts")],
|
||||||
|
viewMode: "tree",
|
||||||
collapsedFolders: new Set(),
|
collapsedFolders: new Set(),
|
||||||
expandedPaths: new Set(),
|
expandedPaths: new Set(),
|
||||||
}).items; // [folder, a.ts, b.ts]
|
}).items; // [folder, a.ts, b.ts]
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export interface DiffFlatItemsResult {
|
|||||||
|
|
||||||
export interface BuildDiffFlatItemsInput {
|
export interface BuildDiffFlatItemsInput {
|
||||||
files: ParsedDiffFile[];
|
files: ParsedDiffFile[];
|
||||||
|
viewMode: "flat" | "tree";
|
||||||
/** Full uncompressed directory paths that are collapsed (empty = all expanded). */
|
/** Full uncompressed directory paths that are collapsed (empty = all expanded). */
|
||||||
collapsedFolders: ReadonlySet<string>;
|
collapsedFolders: ReadonlySet<string>;
|
||||||
/** File paths whose diff body is expanded. */
|
/** File paths whose diff body is expanded. */
|
||||||
@@ -52,6 +53,7 @@ export interface BuildDiffFlatItemsInput {
|
|||||||
*/
|
*/
|
||||||
export function buildDiffFlatItems({
|
export function buildDiffFlatItems({
|
||||||
files,
|
files,
|
||||||
|
viewMode,
|
||||||
collapsedFolders,
|
collapsedFolders,
|
||||||
expandedPaths,
|
expandedPaths,
|
||||||
tree,
|
tree,
|
||||||
@@ -68,6 +70,13 @@ export function buildDiffFlatItems({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (viewMode === "flat") {
|
||||||
|
for (const [fileIndex, file] of files.entries()) {
|
||||||
|
pushFile(file, fileIndex, 0);
|
||||||
|
}
|
||||||
|
return { items, stickyHeaderIndices };
|
||||||
|
}
|
||||||
|
|
||||||
const indexByPath = new Map(files.map((file, index) => [file.path, index]));
|
const indexByPath = new Map(files.map((file, index) => [file.path, index]));
|
||||||
const compressedTree = tree ?? compressSingleChildChains(buildDiffTree(files));
|
const compressedTree = tree ?? compressSingleChildChains(buildDiffTree(files));
|
||||||
const rows = flattenDiffTree(compressedTree, collapsedFolders);
|
const rows = flattenDiffTree(compressedTree, collapsedFolders);
|
||||||
|
|||||||
+214
-300
@@ -39,6 +39,7 @@ import {
|
|||||||
FolderTree,
|
FolderTree,
|
||||||
GitCommitHorizontal,
|
GitCommitHorizontal,
|
||||||
GitMerge,
|
GitMerge,
|
||||||
|
List,
|
||||||
ListChevronsDownUp,
|
ListChevronsDownUp,
|
||||||
ListChevronsUpDown,
|
ListChevronsUpDown,
|
||||||
Pilcrow,
|
Pilcrow,
|
||||||
@@ -1219,7 +1220,6 @@ type PressableStyleFn = (
|
|||||||
state: PressableStateCallbackType & { hovered?: boolean; open?: boolean },
|
state: PressableStateCallbackType & { hovered?: boolean; open?: boolean },
|
||||||
) => StyleProp<ViewStyle>;
|
) => StyleProp<ViewStyle>;
|
||||||
|
|
||||||
const foregroundIconColorMapping = (theme: Theme) => ({ color: theme.colors.foreground });
|
|
||||||
const foregroundMutedIconColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
|
const foregroundMutedIconColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
|
||||||
|
|
||||||
const ThemedActivityIndicator = withUnistyles(ActivityIndicator);
|
const ThemedActivityIndicator = withUnistyles(ActivityIndicator);
|
||||||
@@ -1230,6 +1230,7 @@ const ThemedWrapText = withUnistyles(WrapText);
|
|||||||
const ThemedListChevronsDownUp = withUnistyles(ListChevronsDownUp);
|
const ThemedListChevronsDownUp = withUnistyles(ListChevronsDownUp);
|
||||||
const ThemedListChevronsUpDown = withUnistyles(ListChevronsUpDown);
|
const ThemedListChevronsUpDown = withUnistyles(ListChevronsUpDown);
|
||||||
const ThemedFolderTree = withUnistyles(FolderTree);
|
const ThemedFolderTree = withUnistyles(FolderTree);
|
||||||
|
const ThemedList = withUnistyles(List);
|
||||||
const ThemedGitCommitHorizontal = withUnistyles(GitCommitHorizontal);
|
const ThemedGitCommitHorizontal = withUnistyles(GitCommitHorizontal);
|
||||||
const ThemedDownload = withUnistyles(Download);
|
const ThemedDownload = withUnistyles(Download);
|
||||||
const ThemedUpload = withUnistyles(Upload);
|
const ThemedUpload = withUnistyles(Upload);
|
||||||
@@ -1240,101 +1241,89 @@ const ThemedRefreshCcw = withUnistyles(RefreshCcw);
|
|||||||
const ThemedArchive = withUnistyles(Archive);
|
const ThemedArchive = withUnistyles(Archive);
|
||||||
const ThemedChevronDown = withUnistyles(ChevronDown);
|
const ThemedChevronDown = withUnistyles(ChevronDown);
|
||||||
|
|
||||||
interface DiffLayoutToggleGroupProps {
|
const DIFF_OPTIONS_WHITESPACE_ICON = (
|
||||||
|
<ThemedPilcrow size={14} uniProps={foregroundMutedIconColorMapping} />
|
||||||
|
);
|
||||||
|
const DIFF_OPTIONS_WRAP_ICON = (
|
||||||
|
<ThemedWrapText size={14} uniProps={foregroundMutedIconColorMapping} />
|
||||||
|
);
|
||||||
|
|
||||||
|
interface DiffLayoutToggleProps {
|
||||||
layout: "unified" | "split";
|
layout: "unified" | "split";
|
||||||
unifiedToggleStyle: PressableStyleFn;
|
|
||||||
splitToggleStyle: PressableStyleFn;
|
|
||||||
onUnified: () => void;
|
|
||||||
onSplit: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function DiffLayoutToggleGroup({
|
|
||||||
layout,
|
|
||||||
unifiedToggleStyle,
|
|
||||||
splitToggleStyle,
|
|
||||||
onUnified,
|
|
||||||
onSplit,
|
|
||||||
}: DiffLayoutToggleGroupProps) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const unifiedLabel = t("workspace.git.diff.unified");
|
|
||||||
const splitLabel = t("workspace.git.diff.split");
|
|
||||||
return (
|
|
||||||
<View style={styles.toggleButtonGroup}>
|
|
||||||
<Tooltip delayDuration={300}>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Pressable
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={unifiedLabel}
|
|
||||||
testID="changes-layout-unified"
|
|
||||||
onPress={onUnified}
|
|
||||||
style={unifiedToggleStyle}
|
|
||||||
>
|
|
||||||
<ThemedAlignJustify
|
|
||||||
size={14}
|
|
||||||
uniProps={
|
|
||||||
layout === "unified" ? foregroundIconColorMapping : foregroundMutedIconColorMapping
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Pressable>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="bottom">
|
|
||||||
<Text style={styles.tooltipText}>{unifiedLabel}</Text>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip delayDuration={300}>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Pressable
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={splitLabel}
|
|
||||||
testID="changes-layout-split"
|
|
||||||
onPress={onSplit}
|
|
||||||
style={splitToggleStyle}
|
|
||||||
>
|
|
||||||
<ThemedColumns2
|
|
||||||
size={14}
|
|
||||||
uniProps={
|
|
||||||
layout === "split" ? foregroundIconColorMapping : foregroundMutedIconColorMapping
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Pressable>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="bottom">
|
|
||||||
<Text style={styles.tooltipText}>{splitLabel}</Text>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DiffWhitespaceToggleProps {
|
|
||||||
hideWhitespace: boolean;
|
|
||||||
isMobile: boolean;
|
isMobile: boolean;
|
||||||
toggleStyle: PressableStyleFn;
|
toggleStyle: PressableStyleFn;
|
||||||
onToggle: () => void;
|
onToggle: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DiffWhitespaceToggle({
|
function DiffLayoutToggle({ layout, isMobile, toggleStyle, onToggle }: DiffLayoutToggleProps) {
|
||||||
hideWhitespace,
|
|
||||||
isMobile,
|
|
||||||
toggleStyle,
|
|
||||||
onToggle,
|
|
||||||
}: DiffWhitespaceToggleProps) {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const label = t("workspace.git.diff.hideWhitespace");
|
const label =
|
||||||
|
layout === "unified"
|
||||||
|
? t("workspace.git.diff.switchToSplit")
|
||||||
|
: t("workspace.git.diff.switchToUnified");
|
||||||
return (
|
return (
|
||||||
<Tooltip delayDuration={300}>
|
<Tooltip delayDuration={300}>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel={label}
|
accessibilityLabel={label}
|
||||||
testID="changes-toggle-whitespace"
|
testID="changes-toggle-layout"
|
||||||
|
onPress={onToggle}
|
||||||
|
style={toggleStyle}
|
||||||
|
>
|
||||||
|
{layout === "unified" ? (
|
||||||
|
<ThemedColumns2 size={isMobile ? 18 : 14} uniProps={foregroundMutedIconColorMapping} />
|
||||||
|
) : (
|
||||||
|
<ThemedAlignJustify
|
||||||
|
size={isMobile ? 18 : 14}
|
||||||
|
uniProps={foregroundMutedIconColorMapping}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Pressable>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="bottom">
|
||||||
|
<Text style={styles.tooltipText}>{label}</Text>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DiffViewModeToggleProps {
|
||||||
|
viewMode: "flat" | "tree";
|
||||||
|
isMobile: boolean;
|
||||||
|
toggleStyle: PressableStyleFn;
|
||||||
|
onToggle: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DiffViewModeToggle({
|
||||||
|
viewMode,
|
||||||
|
isMobile,
|
||||||
|
toggleStyle,
|
||||||
|
onToggle,
|
||||||
|
}: DiffViewModeToggleProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const label =
|
||||||
|
viewMode === "flat"
|
||||||
|
? t("workspace.git.diff.showTreeView")
|
||||||
|
: t("workspace.git.diff.showFlatView");
|
||||||
|
return (
|
||||||
|
<Tooltip delayDuration={300}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Pressable
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={label}
|
||||||
|
testID="changes-toggle-view-mode"
|
||||||
style={toggleStyle}
|
style={toggleStyle}
|
||||||
onPress={onToggle}
|
onPress={onToggle}
|
||||||
>
|
>
|
||||||
<ThemedPilcrow
|
{viewMode === "flat" ? (
|
||||||
size={isMobile ? 18 : 14}
|
<ThemedFolderTree
|
||||||
uniProps={hideWhitespace ? foregroundIconColorMapping : foregroundMutedIconColorMapping}
|
size={isMobile ? 18 : 14}
|
||||||
/>
|
uniProps={foregroundMutedIconColorMapping}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ThemedList size={isMobile ? 18 : 14} uniProps={foregroundMutedIconColorMapping} />
|
||||||
|
)}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="bottom">
|
<TooltipContent side="bottom">
|
||||||
@@ -1345,82 +1334,24 @@ function DiffWhitespaceToggle({
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface DiffFilesToolbarProps {
|
interface DiffFilesToolbarProps {
|
||||||
wrapLines: boolean;
|
|
||||||
// Controls file-DIFF bodies only (not folders). Named explicitly so the
|
|
||||||
// folder control below stays unambiguous (Codex item 4).
|
|
||||||
allFileDiffsExpanded: boolean;
|
allFileDiffsExpanded: boolean;
|
||||||
showFolderControl: boolean;
|
|
||||||
allFoldersCollapsed: boolean;
|
|
||||||
isMobile: boolean;
|
isMobile: boolean;
|
||||||
wrapLinesToggleStyle: PressableStyleFn;
|
|
||||||
expandAllToggleStyle: PressableStyleFn;
|
expandAllToggleStyle: PressableStyleFn;
|
||||||
folderToggleStyle: PressableStyleFn;
|
|
||||||
onToggleWrapLines: () => void;
|
|
||||||
onToggleExpandAll: () => void;
|
onToggleExpandAll: () => void;
|
||||||
onToggleCollapseAllFolders: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function DiffFilesToolbar({
|
function DiffFilesToolbar({
|
||||||
wrapLines,
|
|
||||||
allFileDiffsExpanded,
|
allFileDiffsExpanded,
|
||||||
showFolderControl,
|
|
||||||
allFoldersCollapsed,
|
|
||||||
isMobile,
|
isMobile,
|
||||||
wrapLinesToggleStyle,
|
|
||||||
expandAllToggleStyle,
|
expandAllToggleStyle,
|
||||||
folderToggleStyle,
|
|
||||||
onToggleWrapLines,
|
|
||||||
onToggleExpandAll,
|
onToggleExpandAll,
|
||||||
onToggleCollapseAllFolders,
|
|
||||||
}: DiffFilesToolbarProps) {
|
}: DiffFilesToolbarProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const wrapLinesLabel = wrapLines
|
|
||||||
? t("workspace.git.diff.scrollLongLines")
|
|
||||||
: t("workspace.git.diff.wrapLongLines");
|
|
||||||
const expandAllLabel = allFileDiffsExpanded
|
const expandAllLabel = allFileDiffsExpanded
|
||||||
? t("workspace.git.diff.collapseAll")
|
? t("workspace.git.diff.collapseAll")
|
||||||
: t("workspace.git.diff.expandAll");
|
: t("workspace.git.diff.expandAll");
|
||||||
const folderLabel = allFoldersCollapsed
|
|
||||||
? t("workspace.git.diff.expandAllFolders")
|
|
||||||
: t("workspace.git.diff.collapseAllFolders");
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.diffStatusButtons}>
|
<View style={styles.diffStatusButtons}>
|
||||||
{showFolderControl ? (
|
|
||||||
<Tooltip delayDuration={300}>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Pressable
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={folderLabel}
|
|
||||||
testID="changes-toggle-folders"
|
|
||||||
style={folderToggleStyle}
|
|
||||||
onPress={onToggleCollapseAllFolders}
|
|
||||||
>
|
|
||||||
<ThemedFolderTree
|
|
||||||
size={isMobile ? 18 : 14}
|
|
||||||
uniProps={
|
|
||||||
allFoldersCollapsed ? foregroundIconColorMapping : foregroundMutedIconColorMapping
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Pressable>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="bottom">
|
|
||||||
<Text style={styles.tooltipText}>{folderLabel}</Text>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
) : null}
|
|
||||||
<Tooltip delayDuration={300}>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Pressable style={wrapLinesToggleStyle} onPress={onToggleWrapLines}>
|
|
||||||
<ThemedWrapText
|
|
||||||
size={isMobile ? 18 : 14}
|
|
||||||
uniProps={wrapLines ? foregroundIconColorMapping : foregroundMutedIconColorMapping}
|
|
||||||
/>
|
|
||||||
</Pressable>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="bottom">
|
|
||||||
<Text style={styles.tooltipText}>{wrapLinesLabel}</Text>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip delayDuration={300}>
|
<Tooltip delayDuration={300}>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Pressable
|
<Pressable
|
||||||
@@ -1450,50 +1381,105 @@ function DiffFilesToolbar({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DiffRefreshButtonProps {
|
interface DiffOptionsMenuProps {
|
||||||
|
hideWhitespace: boolean;
|
||||||
|
isMobile: boolean;
|
||||||
isRefreshing: boolean;
|
isRefreshing: boolean;
|
||||||
toggleStyle: PressableStyleFn;
|
overflowToggleStyle: PressableStyleFn;
|
||||||
onPress: () => void;
|
refreshSupported: boolean;
|
||||||
|
wrapLines: boolean;
|
||||||
|
onRefresh: () => void;
|
||||||
|
onToggleHideWhitespace: () => void;
|
||||||
|
onToggleWrapLines: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DiffOptionsMenu({
|
||||||
|
hideWhitespace,
|
||||||
|
isMobile,
|
||||||
|
isRefreshing,
|
||||||
|
overflowToggleStyle,
|
||||||
|
refreshSupported,
|
||||||
|
wrapLines,
|
||||||
|
onRefresh,
|
||||||
|
onToggleHideWhitespace,
|
||||||
|
onToggleWrapLines,
|
||||||
|
}: DiffOptionsMenuProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const whitespaceLabel = hideWhitespace
|
||||||
|
? t("workspace.git.diff.showWhitespace")
|
||||||
|
: t("workspace.git.diff.hideWhitespace");
|
||||||
|
const wrapLinesLabel = wrapLines
|
||||||
|
? t("workspace.git.diff.scrollLongLines")
|
||||||
|
: t("workspace.git.diff.wrapLongLines");
|
||||||
|
const optionsLabel = t("workspace.git.diff.options");
|
||||||
|
const refreshIcon = useMemo(
|
||||||
|
() =>
|
||||||
|
isRefreshing ? (
|
||||||
|
<ThemedLoadingSpinner size={ICON_SIZE.sm} uniProps={foregroundMutedIconColorMapping} />
|
||||||
|
) : (
|
||||||
|
<ThemedRotateCw size={ICON_SIZE.sm} uniProps={foregroundMutedIconColorMapping} />
|
||||||
|
),
|
||||||
|
[isRefreshing],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<Tooltip delayDuration={300}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<DropdownMenuTrigger
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={optionsLabel}
|
||||||
|
testID="changes-options-menu"
|
||||||
|
style={overflowToggleStyle}
|
||||||
|
>
|
||||||
|
<ThemedChevronDown
|
||||||
|
size={isMobile ? 18 : 14}
|
||||||
|
uniProps={foregroundMutedIconColorMapping}
|
||||||
|
/>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="bottom">
|
||||||
|
<Text style={styles.tooltipText}>{optionsLabel}</Text>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<DropdownMenuContent align="end" width={240} testID="changes-options-menu-content">
|
||||||
|
<DropdownMenuItem
|
||||||
|
leading={DIFF_OPTIONS_WHITESPACE_ICON}
|
||||||
|
selected={hideWhitespace}
|
||||||
|
testID="changes-toggle-whitespace"
|
||||||
|
onSelect={onToggleHideWhitespace}
|
||||||
|
>
|
||||||
|
{whitespaceLabel}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
leading={DIFF_OPTIONS_WRAP_ICON}
|
||||||
|
selected={wrapLines}
|
||||||
|
testID="changes-toggle-wrap-lines"
|
||||||
|
onSelect={onToggleWrapLines}
|
||||||
|
>
|
||||||
|
{wrapLinesLabel}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{refreshSupported ? (
|
||||||
|
<>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem
|
||||||
|
leading={refreshIcon}
|
||||||
|
disabled={isRefreshing}
|
||||||
|
testID="changes-refresh"
|
||||||
|
onSelect={onRefresh}
|
||||||
|
>
|
||||||
|
{isRefreshing ? t("workspace.git.diff.refreshing") : t("workspace.git.diff.refresh")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ThemedRotateCw = withUnistyles(RotateCw);
|
const ThemedRotateCw = withUnistyles(RotateCw);
|
||||||
const ThemedLoadingSpinner = withUnistyles(LoadingSpinner);
|
const ThemedLoadingSpinner = withUnistyles(LoadingSpinner);
|
||||||
|
|
||||||
function DiffRefreshButton({ isRefreshing, toggleStyle, onPress }: DiffRefreshButtonProps) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const refreshLabel = t("workspace.git.diff.refresh");
|
|
||||||
return (
|
|
||||||
<Tooltip delayDuration={300}>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Pressable
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={
|
|
||||||
isRefreshing ? t("workspace.git.diff.refreshing") : t("workspace.git.diff.refreshState")
|
|
||||||
}
|
|
||||||
testID="changes-refresh"
|
|
||||||
style={toggleStyle}
|
|
||||||
onPress={onPress}
|
|
||||||
disabled={isRefreshing}
|
|
||||||
>
|
|
||||||
<View style={styles.refreshIcon}>
|
|
||||||
{isRefreshing ? (
|
|
||||||
<ThemedLoadingSpinner
|
|
||||||
size={ICON_SIZE.sm}
|
|
||||||
uniProps={foregroundMutedIconColorMapping}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ThemedRotateCw size={ICON_SIZE.sm} uniProps={foregroundMutedIconColorMapping} />
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</Pressable>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="bottom">
|
|
||||||
<Text style={styles.tooltipText}>{refreshLabel}</Text>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
type DiffFlatItemLayoutGetter = NonNullable<FlatListProps<DiffFlatItem>["getItemLayout"]>;
|
type DiffFlatItemLayoutGetter = NonNullable<FlatListProps<DiffFlatItem>["getItemLayout"]>;
|
||||||
|
|
||||||
function getUnifiedDiffLineCount(file: ParsedDiffFile): number {
|
function getUnifiedDiffLineCount(file: ParsedDiffFile): number {
|
||||||
@@ -1756,30 +1742,22 @@ export function GitDiffPane({ serverId, workspaceId, cwd, enabled }: GitDiffPane
|
|||||||
const { preferences: changesPreferences, updatePreferences: updateChangesPreferences } =
|
const { preferences: changesPreferences, updatePreferences: updateChangesPreferences } =
|
||||||
useChangesPreferences();
|
useChangesPreferences();
|
||||||
const wrapLines = changesPreferences.wrapLines;
|
const wrapLines = changesPreferences.wrapLines;
|
||||||
|
const viewMode = changesPreferences.viewMode;
|
||||||
const effectiveLayout = canUseSplitLayout ? changesPreferences.layout : "unified";
|
const effectiveLayout = canUseSplitLayout ? changesPreferences.layout : "unified";
|
||||||
|
|
||||||
const handleToggleWrapLines = useCallback(() => {
|
const handleToggleWrapLines = useCallback(() => {
|
||||||
void updateChangesPreferences({ wrapLines: !wrapLines });
|
void updateChangesPreferences({ wrapLines: !wrapLines });
|
||||||
}, [updateChangesPreferences, wrapLines]);
|
}, [updateChangesPreferences, wrapLines]);
|
||||||
|
|
||||||
const handleLayoutChange = useCallback(
|
|
||||||
(nextLayout: "unified" | "split") => {
|
|
||||||
void updateChangesPreferences({ layout: nextLayout });
|
|
||||||
},
|
|
||||||
[updateChangesPreferences],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleToggleHideWhitespace = useCallback(() => {
|
const handleToggleHideWhitespace = useCallback(() => {
|
||||||
void updateChangesPreferences({ hideWhitespace: !changesPreferences.hideWhitespace });
|
void updateChangesPreferences({ hideWhitespace: !changesPreferences.hideWhitespace });
|
||||||
}, [changesPreferences.hideWhitespace, updateChangesPreferences]);
|
}, [changesPreferences.hideWhitespace, updateChangesPreferences]);
|
||||||
|
|
||||||
const handleLayoutUnified = useCallback(() => {
|
const handleToggleLayout = useCallback(() => {
|
||||||
handleLayoutChange("unified");
|
void updateChangesPreferences({
|
||||||
}, [handleLayoutChange]);
|
layout: changesPreferences.layout === "unified" ? "split" : "unified",
|
||||||
|
});
|
||||||
const handleLayoutSplit = useCallback(() => {
|
}, [changesPreferences.layout, updateChangesPreferences]);
|
||||||
handleLayoutChange("split");
|
|
||||||
}, [handleLayoutChange]);
|
|
||||||
|
|
||||||
const codeFontSize = appSettings.codeFontSize;
|
const codeFontSize = appSettings.codeFontSize;
|
||||||
const diffBodyLineHeight = Math.round(codeFontSize * 1.5);
|
const diffBodyLineHeight = Math.round(codeFontSize * 1.5);
|
||||||
@@ -1796,39 +1774,19 @@ export function GitDiffPane({ serverId, workspaceId, cwd, enabled }: GitDiffPane
|
|||||||
}, [appSettings.monoFontFamily, codeFontSize, diffBodyLineHeight]);
|
}, [appSettings.monoFontFamily, codeFontSize, diffBodyLineHeight]);
|
||||||
const diffModeTriggerStyle = useMemo(() => buildDiffModeTriggerStyle(), []);
|
const diffModeTriggerStyle = useMemo(() => buildDiffModeTriggerStyle(), []);
|
||||||
|
|
||||||
const unifiedToggleStyle = useMemo(
|
const layoutToggleStyle = useMemo(
|
||||||
() =>
|
() => buildToggleButtonStyle(false, styles.expandAllButton),
|
||||||
buildToggleButtonStyle(changesPreferences.layout === "unified", [
|
[],
|
||||||
styles.toggleButton,
|
|
||||||
styles.toggleButtonGroupStart,
|
|
||||||
]),
|
|
||||||
[changesPreferences.layout],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const splitToggleStyle = useMemo(
|
const viewModeToggleStyle = useMemo(
|
||||||
() =>
|
() => buildToggleButtonStyle(viewMode === "tree", styles.expandAllButton),
|
||||||
buildToggleButtonStyle(changesPreferences.layout === "split", [
|
[viewMode],
|
||||||
styles.toggleButton,
|
|
||||||
styles.toggleButtonGroupEnd,
|
|
||||||
]),
|
|
||||||
[changesPreferences.layout],
|
|
||||||
);
|
|
||||||
|
|
||||||
const hideWhitespaceToggleStyle = useMemo(
|
|
||||||
() => buildToggleButtonStyle(changesPreferences.hideWhitespace, styles.expandAllButton),
|
|
||||||
[changesPreferences.hideWhitespace],
|
|
||||||
);
|
|
||||||
|
|
||||||
const wrapLinesToggleStyle = useMemo(
|
|
||||||
() => buildToggleButtonStyle(wrapLines, styles.expandAllButton),
|
|
||||||
[wrapLines],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const expandAllToggleStyle = useMemo(() => buildExpandAllButtonStyle(), []);
|
const expandAllToggleStyle = useMemo(() => buildExpandAllButtonStyle(), []);
|
||||||
|
|
||||||
const folderToggleStyle = useMemo(() => buildExpandAllButtonStyle(), []);
|
const overflowToggleStyle = useMemo(() => buildExpandAllButtonStyle(), []);
|
||||||
|
|
||||||
const refreshToggleStyle = useMemo(() => buildExpandAllButtonStyle(), []);
|
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const refreshSupported = useSessionStore(
|
const refreshSupported = useSessionStore(
|
||||||
@@ -1995,6 +1953,16 @@ export function GitDiffPane({ serverId, workspaceId, cwd, enabled }: GitDiffPane
|
|||||||
[collapsedFoldersArray, allFolderPathSet],
|
[collapsedFoldersArray, allFolderPathSet],
|
||||||
);
|
);
|
||||||
const diffListRef = useRef<FlatList<DiffFlatItem>>(null);
|
const diffListRef = useRef<FlatList<DiffFlatItem>>(null);
|
||||||
|
const handleToggleViewMode = useCallback(() => {
|
||||||
|
const nextViewMode = viewMode === "flat" ? "tree" : "flat";
|
||||||
|
if (nextViewMode === "tree") {
|
||||||
|
diffListRef.current?.scrollToOffset({ offset: 0, animated: false });
|
||||||
|
if (workspaceStateKey) {
|
||||||
|
setDiffCollapsedFoldersForWorkspace(workspaceStateKey, []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void updateChangesPreferences({ viewMode: nextViewMode });
|
||||||
|
}, [setDiffCollapsedFoldersForWorkspace, updateChangesPreferences, viewMode, workspaceStateKey]);
|
||||||
const scrollbar = useWebScrollViewScrollbar(diffListRef, {
|
const scrollbar = useWebScrollViewScrollbar(diffListRef, {
|
||||||
enabled: showDesktopWebScrollbar,
|
enabled: showDesktopWebScrollbar,
|
||||||
});
|
});
|
||||||
@@ -2012,12 +1980,13 @@ export function GitDiffPane({ serverId, workspaceId, cwd, enabled }: GitDiffPane
|
|||||||
const { flatItems, stickyHeaderIndices } = useMemo(() => {
|
const { flatItems, stickyHeaderIndices } = useMemo(() => {
|
||||||
const { items, stickyHeaderIndices: stickyIndices } = buildDiffFlatItems({
|
const { items, stickyHeaderIndices: stickyIndices } = buildDiffFlatItems({
|
||||||
files,
|
files,
|
||||||
|
viewMode,
|
||||||
tree: compressedTree,
|
tree: compressedTree,
|
||||||
collapsedFolders,
|
collapsedFolders,
|
||||||
expandedPaths,
|
expandedPaths,
|
||||||
});
|
});
|
||||||
return { flatItems: items, stickyHeaderIndices: stickyIndices };
|
return { flatItems: items, stickyHeaderIndices: stickyIndices };
|
||||||
}, [compressedTree, collapsedFolders, expandedPaths, files]);
|
}, [compressedTree, collapsedFolders, expandedPaths, files, viewMode]);
|
||||||
|
|
||||||
const getBodyHeightKey = useCallback(
|
const getBodyHeightKey = useCallback(
|
||||||
(file: ParsedDiffFile): string => {
|
(file: ParsedDiffFile): string => {
|
||||||
@@ -2248,25 +2217,6 @@ export function GitDiffPane({ serverId, workspaceId, cwd, enabled }: GitDiffPane
|
|||||||
}
|
}
|
||||||
}, [allFileDiffsExpanded, files, setDiffExpandedPathsForWorkspace, workspaceStateKey]);
|
}, [allFileDiffsExpanded, files, setDiffExpandedPathsForWorkspace, workspaceStateKey]);
|
||||||
|
|
||||||
// Membership, not count: collapsedFolders is already filtered to present folders, so
|
|
||||||
// "all collapsed" means every present folder path is in it.
|
|
||||||
const allFoldersCollapsed =
|
|
||||||
allFolderPaths.length > 0 && allFolderPaths.every((path) => collapsedFolders.has(path));
|
|
||||||
|
|
||||||
const handleToggleCollapseAllFolders = useCallback(() => {
|
|
||||||
if (!workspaceStateKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Anchor to the top before collapsing everything so the viewport isn't stranded.
|
|
||||||
if (!allFoldersCollapsed) {
|
|
||||||
diffListRef.current?.scrollToOffset({ offset: 0, animated: false });
|
|
||||||
}
|
|
||||||
setDiffCollapsedFoldersForWorkspace(
|
|
||||||
workspaceStateKey,
|
|
||||||
allFoldersCollapsed ? [] : allFolderPaths,
|
|
||||||
);
|
|
||||||
}, [allFolderPaths, allFoldersCollapsed, setDiffCollapsedFoldersForWorkspace, workspaceStateKey]);
|
|
||||||
|
|
||||||
const renderFlatItem = useCallback(
|
const renderFlatItem = useCallback(
|
||||||
({ item }: { item: DiffFlatItem }) => {
|
({ item }: { item: DiffFlatItem }) => {
|
||||||
if (item.type === "folder") {
|
if (item.type === "folder") {
|
||||||
@@ -2290,7 +2240,7 @@ export function GitDiffPane({ serverId, workspaceId, cwd, enabled }: GitDiffPane
|
|||||||
file={item.file}
|
file={item.file}
|
||||||
isExpanded={item.isExpanded}
|
isExpanded={item.isExpanded}
|
||||||
depth={item.depth}
|
depth={item.depth}
|
||||||
showDir={false}
|
showDir={viewMode === "flat"}
|
||||||
onToggle={handleToggleExpanded}
|
onToggle={handleToggleExpanded}
|
||||||
onHeaderHeightChange={handleHeaderHeightChange}
|
onHeaderHeightChange={handleHeaderHeightChange}
|
||||||
testID={`diff-file-${item.fileIndex}`}
|
testID={`diff-file-${item.fileIndex}`}
|
||||||
@@ -2320,6 +2270,7 @@ export function GitDiffPane({ serverId, workspaceId, cwd, enabled }: GitDiffPane
|
|||||||
handleToggleExpanded,
|
handleToggleExpanded,
|
||||||
handleToggleFolder,
|
handleToggleFolder,
|
||||||
reviewActions,
|
reviewActions,
|
||||||
|
viewMode,
|
||||||
wrapLines,
|
wrapLines,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -2347,6 +2298,7 @@ export function GitDiffPane({ serverId, workspaceId, cwd, enabled }: GitDiffPane
|
|||||||
effectiveLayout,
|
effectiveLayout,
|
||||||
diffBodyTypographyKey,
|
diffBodyTypographyKey,
|
||||||
heightVersion,
|
heightVersion,
|
||||||
|
viewMode,
|
||||||
wrapLines,
|
wrapLines,
|
||||||
reviewActions,
|
reviewActions,
|
||||||
}),
|
}),
|
||||||
@@ -2356,6 +2308,7 @@ export function GitDiffPane({ serverId, workspaceId, cwd, enabled }: GitDiffPane
|
|||||||
effectiveLayout,
|
effectiveLayout,
|
||||||
diffBodyTypographyKey,
|
diffBodyTypographyKey,
|
||||||
heightVersion,
|
heightVersion,
|
||||||
|
viewMode,
|
||||||
wrapLines,
|
wrapLines,
|
||||||
reviewActions,
|
reviewActions,
|
||||||
],
|
],
|
||||||
@@ -2485,42 +2438,40 @@ export function GitDiffPane({ serverId, workspaceId, cwd, enabled }: GitDiffPane
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
<View style={styles.diffStatusButtons}>
|
<View style={styles.diffStatusButtons}>
|
||||||
{canUseSplitLayout ? (
|
{canUseSplitLayout ? (
|
||||||
<DiffLayoutToggleGroup
|
<DiffLayoutToggle
|
||||||
layout={changesPreferences.layout}
|
layout={changesPreferences.layout}
|
||||||
unifiedToggleStyle={unifiedToggleStyle}
|
isMobile={isMobile}
|
||||||
splitToggleStyle={splitToggleStyle}
|
toggleStyle={layoutToggleStyle}
|
||||||
onUnified={handleLayoutUnified}
|
onToggle={handleToggleLayout}
|
||||||
onSplit={handleLayoutSplit}
|
/>
|
||||||
|
) : null}
|
||||||
|
{files.length > 0 ? (
|
||||||
|
<DiffViewModeToggle
|
||||||
|
viewMode={viewMode}
|
||||||
|
isMobile={isMobile}
|
||||||
|
toggleStyle={viewModeToggleStyle}
|
||||||
|
onToggle={handleToggleViewMode}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<DiffWhitespaceToggle
|
|
||||||
hideWhitespace={changesPreferences.hideWhitespace}
|
|
||||||
isMobile={isMobile}
|
|
||||||
toggleStyle={hideWhitespaceToggleStyle}
|
|
||||||
onToggle={handleToggleHideWhitespace}
|
|
||||||
/>
|
|
||||||
{files.length > 0 ? (
|
{files.length > 0 ? (
|
||||||
<DiffFilesToolbar
|
<DiffFilesToolbar
|
||||||
wrapLines={wrapLines}
|
|
||||||
allFileDiffsExpanded={allFileDiffsExpanded}
|
allFileDiffsExpanded={allFileDiffsExpanded}
|
||||||
showFolderControl={allFolderPaths.length > 0}
|
|
||||||
allFoldersCollapsed={allFoldersCollapsed}
|
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
wrapLinesToggleStyle={wrapLinesToggleStyle}
|
|
||||||
expandAllToggleStyle={expandAllToggleStyle}
|
expandAllToggleStyle={expandAllToggleStyle}
|
||||||
folderToggleStyle={folderToggleStyle}
|
|
||||||
onToggleWrapLines={handleToggleWrapLines}
|
|
||||||
onToggleExpandAll={handleToggleExpandAll}
|
onToggleExpandAll={handleToggleExpandAll}
|
||||||
onToggleCollapseAllFolders={handleToggleCollapseAllFolders}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
{refreshSupported ? (
|
|
||||||
<DiffRefreshButton
|
|
||||||
isRefreshing={isRefreshing}
|
|
||||||
toggleStyle={refreshToggleStyle}
|
|
||||||
onPress={handleRefresh}
|
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
<DiffOptionsMenu
|
||||||
|
hideWhitespace={changesPreferences.hideWhitespace}
|
||||||
|
isMobile={isMobile}
|
||||||
|
isRefreshing={isRefreshing}
|
||||||
|
overflowToggleStyle={overflowToggleStyle}
|
||||||
|
refreshSupported={refreshSupported}
|
||||||
|
wrapLines={wrapLines}
|
||||||
|
onRefresh={handleRefresh}
|
||||||
|
onToggleHideWhitespace={handleToggleHideWhitespace}
|
||||||
|
onToggleWrapLines={handleToggleWrapLines}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -2602,46 +2553,9 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
gap: theme.spacing[1],
|
gap: theme.spacing[1],
|
||||||
flexWrap: "wrap",
|
flexWrap: "wrap",
|
||||||
},
|
},
|
||||||
toggleButtonGroup: {
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
},
|
|
||||||
toggleButton: {
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
minWidth: {
|
|
||||||
xs: 32,
|
|
||||||
sm: 32,
|
|
||||||
md: 24,
|
|
||||||
},
|
|
||||||
height: {
|
|
||||||
xs: 32,
|
|
||||||
sm: 32,
|
|
||||||
md: 24,
|
|
||||||
},
|
|
||||||
paddingHorizontal: {
|
|
||||||
xs: theme.spacing[2],
|
|
||||||
sm: theme.spacing[2],
|
|
||||||
md: theme.spacing[1],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
toggleButtonGroupStart: {
|
|
||||||
borderTopLeftRadius: theme.borderRadius.base,
|
|
||||||
borderBottomLeftRadius: theme.borderRadius.base,
|
|
||||||
},
|
|
||||||
toggleButtonGroupEnd: {
|
|
||||||
borderTopRightRadius: theme.borderRadius.base,
|
|
||||||
borderBottomRightRadius: theme.borderRadius.base,
|
|
||||||
},
|
|
||||||
toggleButtonSelected: {
|
toggleButtonSelected: {
|
||||||
backgroundColor: theme.colors.surface2,
|
backgroundColor: theme.colors.surface2,
|
||||||
},
|
},
|
||||||
refreshIcon: {
|
|
||||||
width: ICON_SIZE.md,
|
|
||||||
height: ICON_SIZE.md,
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
},
|
|
||||||
expandAllButton: {
|
expandAllButton: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ describe("loadChangesPreferencesFromStorage", () => {
|
|||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
layout: "unified",
|
layout: "unified",
|
||||||
|
viewMode: "flat",
|
||||||
wrapLines: true,
|
wrapLines: true,
|
||||||
hideWhitespace: false,
|
hideWhitespace: false,
|
||||||
});
|
});
|
||||||
@@ -37,6 +38,7 @@ describe("loadChangesPreferencesFromStorage", () => {
|
|||||||
it("loads persisted layout and whitespace preferences without rewriting storage", async () => {
|
it("loads persisted layout and whitespace preferences without rewriting storage", async () => {
|
||||||
const persisted = JSON.stringify({
|
const persisted = JSON.stringify({
|
||||||
layout: "split",
|
layout: "split",
|
||||||
|
viewMode: "tree",
|
||||||
hideWhitespace: true,
|
hideWhitespace: true,
|
||||||
wrapLines: false,
|
wrapLines: false,
|
||||||
});
|
});
|
||||||
@@ -48,6 +50,7 @@ describe("loadChangesPreferencesFromStorage", () => {
|
|||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
layout: "split",
|
layout: "split",
|
||||||
|
viewMode: "tree",
|
||||||
hideWhitespace: true,
|
hideWhitespace: true,
|
||||||
wrapLines: false,
|
wrapLines: false,
|
||||||
});
|
});
|
||||||
@@ -64,11 +67,16 @@ describe("saveChangesPreferences", () => {
|
|||||||
|
|
||||||
await saveChangesPreferences({
|
await saveChangesPreferences({
|
||||||
queryClient,
|
queryClient,
|
||||||
updates: { layout: "split", hideWhitespace: true },
|
updates: { layout: "split", viewMode: "tree", hideWhitespace: true },
|
||||||
storage,
|
storage,
|
||||||
});
|
});
|
||||||
|
|
||||||
const expected = { ...DEFAULT_CHANGES_PREFERENCES, layout: "split", hideWhitespace: true };
|
const expected = {
|
||||||
|
...DEFAULT_CHANGES_PREFERENCES,
|
||||||
|
layout: "split",
|
||||||
|
viewMode: "tree",
|
||||||
|
hideWhitespace: true,
|
||||||
|
};
|
||||||
expect(queryClient.getQueryData(CHANGES_PREFERENCES_QUERY_KEY)).toEqual(expected);
|
expect(queryClient.getQueryData(CHANGES_PREFERENCES_QUERY_KEY)).toEqual(expected);
|
||||||
expect(storage.entries.get(CHANGES_PREFERENCES_STORAGE_KEY)).toBe(JSON.stringify(expected));
|
expect(storage.entries.get(CHANGES_PREFERENCES_STORAGE_KEY)).toBe(JSON.stringify(expected));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,18 +7,21 @@ export const CHANGES_PREFERENCES_QUERY_KEY = ["changes-preferences"];
|
|||||||
|
|
||||||
const changesPreferencesSchema = z.object({
|
const changesPreferencesSchema = z.object({
|
||||||
layout: z.enum(["unified", "split"]).optional(),
|
layout: z.enum(["unified", "split"]).optional(),
|
||||||
|
viewMode: z.enum(["flat", "tree"]).optional(),
|
||||||
wrapLines: z.boolean().optional(),
|
wrapLines: z.boolean().optional(),
|
||||||
hideWhitespace: z.boolean().optional(),
|
hideWhitespace: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export interface ChangesPreferences {
|
export interface ChangesPreferences {
|
||||||
layout: "unified" | "split";
|
layout: "unified" | "split";
|
||||||
|
viewMode: "flat" | "tree";
|
||||||
wrapLines: boolean;
|
wrapLines: boolean;
|
||||||
hideWhitespace: boolean;
|
hideWhitespace: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_CHANGES_PREFERENCES: ChangesPreferences = {
|
export const DEFAULT_CHANGES_PREFERENCES: ChangesPreferences = {
|
||||||
layout: "unified",
|
layout: "unified",
|
||||||
|
viewMode: "flat",
|
||||||
wrapLines: false,
|
wrapLines: false,
|
||||||
hideWhitespace: false,
|
hideWhitespace: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -707,7 +707,13 @@ export const ar: TranslationResources = {
|
|||||||
tooLarge: "الفرق كبير جدًا بحيث لا يمكن عرضه",
|
tooLarge: "الفرق كبير جدًا بحيث لا يمكن عرضه",
|
||||||
unified: "الفرق الموحدة",
|
unified: "الفرق الموحدة",
|
||||||
split: "فرق جنبًا إلى جنب",
|
split: "فرق جنبًا إلى جنب",
|
||||||
|
switchToUnified: "التبديل إلى الفرق الموحد",
|
||||||
|
switchToSplit: "التبديل إلى الفرق جنبًا إلى جنب",
|
||||||
|
showTreeView: "إظهار شجرة المجلدات",
|
||||||
|
showFlatView: "إظهار قائمة الملفات المسطحة",
|
||||||
|
options: "خيارات الفرق",
|
||||||
hideWhitespace: "إخفاء المسافة البيضاء",
|
hideWhitespace: "إخفاء المسافة البيضاء",
|
||||||
|
showWhitespace: "إظهار المسافة البيضاء",
|
||||||
scrollLongLines: "قم بتمرير الخطوط الطويلة",
|
scrollLongLines: "قم بتمرير الخطوط الطويلة",
|
||||||
wrapLongLines: "لف الخطوط الطويلة",
|
wrapLongLines: "لف الخطوط الطويلة",
|
||||||
collapseAll: "طي كافة الملفات",
|
collapseAll: "طي كافة الملفات",
|
||||||
|
|||||||
@@ -714,7 +714,13 @@ export const en = {
|
|||||||
tooLarge: "Diff too large to display",
|
tooLarge: "Diff too large to display",
|
||||||
unified: "Unified diff",
|
unified: "Unified diff",
|
||||||
split: "Side-by-side diff",
|
split: "Side-by-side diff",
|
||||||
|
switchToUnified: "Switch to unified diff",
|
||||||
|
switchToSplit: "Switch to side-by-side diff",
|
||||||
|
showTreeView: "Show folder tree",
|
||||||
|
showFlatView: "Show flat file list",
|
||||||
|
options: "Diff options",
|
||||||
hideWhitespace: "Hide whitespace",
|
hideWhitespace: "Hide whitespace",
|
||||||
|
showWhitespace: "Show whitespace",
|
||||||
scrollLongLines: "Scroll long lines",
|
scrollLongLines: "Scroll long lines",
|
||||||
wrapLongLines: "Wrap long lines",
|
wrapLongLines: "Wrap long lines",
|
||||||
collapseAll: "Collapse all files",
|
collapseAll: "Collapse all files",
|
||||||
|
|||||||
@@ -734,7 +734,13 @@ export const es: TranslationResources = {
|
|||||||
tooLarge: "La diferencia es demasiado grande para mostrarse",
|
tooLarge: "La diferencia es demasiado grande para mostrarse",
|
||||||
unified: "Diferencia unificada",
|
unified: "Diferencia unificada",
|
||||||
split: "Diferencia de lado a lado",
|
split: "Diferencia de lado a lado",
|
||||||
|
switchToUnified: "Cambiar a diferencia unificada",
|
||||||
|
switchToSplit: "Cambiar a diferencia lado a lado",
|
||||||
|
showTreeView: "Mostrar árbol de carpetas",
|
||||||
|
showFlatView: "Mostrar lista plana de archivos",
|
||||||
|
options: "Opciones de diferencia",
|
||||||
hideWhitespace: "Ocultar espacios en blanco",
|
hideWhitespace: "Ocultar espacios en blanco",
|
||||||
|
showWhitespace: "Mostrar espacios en blanco",
|
||||||
scrollLongLines: "Desplazarse por largas filas",
|
scrollLongLines: "Desplazarse por largas filas",
|
||||||
wrapLongLines: "Envolver largas filas",
|
wrapLongLines: "Envolver largas filas",
|
||||||
collapseAll: "Contraer todos los archivos",
|
collapseAll: "Contraer todos los archivos",
|
||||||
|
|||||||
@@ -733,7 +733,13 @@ export const fr: TranslationResources = {
|
|||||||
tooLarge: "Diff trop grand pour être affiché",
|
tooLarge: "Diff trop grand pour être affiché",
|
||||||
unified: "Différentiel unifié",
|
unified: "Différentiel unifié",
|
||||||
split: "Différent côte à côte",
|
split: "Différent côte à côte",
|
||||||
|
switchToUnified: "Passer au diff unifié",
|
||||||
|
switchToSplit: "Passer au diff côte à côte",
|
||||||
|
showTreeView: "Afficher l'arborescence des dossiers",
|
||||||
|
showFlatView: "Afficher la liste de fichiers à plat",
|
||||||
|
options: "Options du diff",
|
||||||
hideWhitespace: "Masquer les espaces",
|
hideWhitespace: "Masquer les espaces",
|
||||||
|
showWhitespace: "Afficher les espaces",
|
||||||
scrollLongLines: "Faire défiler les longues lignes",
|
scrollLongLines: "Faire défiler les longues lignes",
|
||||||
wrapLongLines: "Enroulez les longues lignes",
|
wrapLongLines: "Enroulez les longues lignes",
|
||||||
collapseAll: "Réduire tous les fichiers",
|
collapseAll: "Réduire tous les fichiers",
|
||||||
|
|||||||
@@ -719,7 +719,13 @@ export const ja: TranslationResources = {
|
|||||||
tooLarge: "差分が大きすぎて表示できません",
|
tooLarge: "差分が大きすぎて表示できません",
|
||||||
unified: "ユニファイド差分",
|
unified: "ユニファイド差分",
|
||||||
split: "左右比較",
|
split: "左右比較",
|
||||||
|
switchToUnified: "ユニファイド差分に切り替え",
|
||||||
|
switchToSplit: "左右比較差分に切り替え",
|
||||||
|
showTreeView: "フォルダツリーを表示",
|
||||||
|
showFlatView: "フラットなファイル一覧を表示",
|
||||||
|
options: "差分オプション",
|
||||||
hideWhitespace: "空白を非表示",
|
hideWhitespace: "空白を非表示",
|
||||||
|
showWhitespace: "空白を表示",
|
||||||
scrollLongLines: "長い行をスクロール",
|
scrollLongLines: "長い行をスクロール",
|
||||||
wrapLongLines: "長い行を折り返す",
|
wrapLongLines: "長い行を折り返す",
|
||||||
collapseAll: "すべて折りたたむ",
|
collapseAll: "すべて折りたたむ",
|
||||||
|
|||||||
@@ -725,7 +725,13 @@ export const ptBR: TranslationResources = {
|
|||||||
tooLarge: "Diff grande demais para exibir",
|
tooLarge: "Diff grande demais para exibir",
|
||||||
unified: "Diff unificado",
|
unified: "Diff unificado",
|
||||||
split: "Diff lado a lado",
|
split: "Diff lado a lado",
|
||||||
|
switchToUnified: "Alternar para diff unificado",
|
||||||
|
switchToSplit: "Alternar para diff lado a lado",
|
||||||
|
showTreeView: "Mostrar árvore de pastas",
|
||||||
|
showFlatView: "Mostrar lista plana de arquivos",
|
||||||
|
options: "Opções do diff",
|
||||||
hideWhitespace: "Ocultar espaços em branco",
|
hideWhitespace: "Ocultar espaços em branco",
|
||||||
|
showWhitespace: "Mostrar espaços em branco",
|
||||||
scrollLongLines: "Rolar linhas longas",
|
scrollLongLines: "Rolar linhas longas",
|
||||||
wrapLongLines: "Quebrar linhas longas",
|
wrapLongLines: "Quebrar linhas longas",
|
||||||
collapseAll: "Recolher todos os arquivos",
|
collapseAll: "Recolher todos os arquivos",
|
||||||
|
|||||||
@@ -726,7 +726,13 @@ export const ru: TranslationResources = {
|
|||||||
tooLarge: "Разница слишком велика для отображения",
|
tooLarge: "Разница слишком велика для отображения",
|
||||||
unified: "Единый дифференциал",
|
unified: "Единый дифференциал",
|
||||||
split: "Параллельная разница",
|
split: "Параллельная разница",
|
||||||
|
switchToUnified: "Переключить на объединенный diff",
|
||||||
|
switchToSplit: "Переключить на diff рядом",
|
||||||
|
showTreeView: "Показать дерево папок",
|
||||||
|
showFlatView: "Показать плоский список файлов",
|
||||||
|
options: "Параметры diff",
|
||||||
hideWhitespace: "Скрыть пробелы",
|
hideWhitespace: "Скрыть пробелы",
|
||||||
|
showWhitespace: "Показать пробелы",
|
||||||
scrollLongLines: "Прокручивать длинные строки",
|
scrollLongLines: "Прокручивать длинные строки",
|
||||||
wrapLongLines: "Перенос длинных строк",
|
wrapLongLines: "Перенос длинных строк",
|
||||||
collapseAll: "Свернуть все файлы",
|
collapseAll: "Свернуть все файлы",
|
||||||
|
|||||||
@@ -701,7 +701,13 @@ export const zhCN: TranslationResources = {
|
|||||||
tooLarge: "Diff 过大,无法显示",
|
tooLarge: "Diff 过大,无法显示",
|
||||||
unified: "Unified diff",
|
unified: "Unified diff",
|
||||||
split: "Side-by-side diff",
|
split: "Side-by-side diff",
|
||||||
|
switchToUnified: "切换到统一差异视图",
|
||||||
|
switchToSplit: "切换到并排差异视图",
|
||||||
|
showTreeView: "显示文件夹树",
|
||||||
|
showFlatView: "显示平铺文件列表",
|
||||||
|
options: "Diff 选项",
|
||||||
hideWhitespace: "隐藏空白差异",
|
hideWhitespace: "隐藏空白差异",
|
||||||
|
showWhitespace: "显示空白差异",
|
||||||
scrollLongLines: "滚动长行",
|
scrollLongLines: "滚动长行",
|
||||||
wrapLongLines: "自动换行长行",
|
wrapLongLines: "自动换行长行",
|
||||||
collapseAll: "折叠所有文件",
|
collapseAll: "折叠所有文件",
|
||||||
|
|||||||
Reference in New Issue
Block a user