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:
Mohamed Boudra
2026-07-08 21:20:51 +00:00
committed by GitHub
parent c7406a415d
commit 29a3261dd6
14 changed files with 348 additions and 303 deletions
+44 -1
View File
@@ -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]
+9
View File
@@ -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
View File
@@ -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,
}; };
+6
View File
@@ -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: "طي كافة الملفات",
+6
View File
@@ -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",
+6
View File
@@ -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",
+6
View File
@@ -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",
+6
View File
@@ -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: "すべて折りたたむ",
+6
View File
@@ -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",
+6
View File
@@ -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: "Свернуть все файлы",
+6
View File
@@ -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: "折叠所有文件",