diff --git a/frontend/src/components/Diff/Diff.tsx b/frontend/src/components/Diff/Diff.tsx index 9d180bbb4..909ab2b86 100644 --- a/frontend/src/components/Diff/Diff.tsx +++ b/frontend/src/components/Diff/Diff.tsx @@ -4,12 +4,12 @@ import { forwardRef, type HTMLAttributes, type RefObject, + useEffect, useMemo, - useRef, useState, } from "react"; -import { VersionsIcon, FoldIcon, UnfoldIcon } from "@primer/octicons-react"; +import { FoldIcon, UnfoldIcon } from "@primer/octicons-react"; import type { EditorView } from "codemirror"; import AutoSizer from "react-virtualized-auto-sizer"; import { FixedSizeList } from "react-window"; @@ -29,22 +29,20 @@ import * as AsmDiffer from "./DiffRowAsmDiffer"; import DragBar from "./DragBar"; import { useHighlighers } from "./Highlighter"; import CopyButton from "../CopyButton"; - -const diffContentsToString = (diff: api.DiffOutput, kind: string): string => { - // kind is either "base", "current", or "previous" - const contents = diff.rows.map((row) => { - let text = ""; - if (kind === "base" && row.base) { - text = row.base.text.map((t) => t.text).join(""); - } else if (kind === "current" && row.current) { - text = row.current.text.map((t) => t.text).join(""); - } else if (kind === "previous" && row.previous) { - text = row.previous.text.map((t) => t.text).join(""); - } - return text; - }); - - return contents.join("\n"); +import ToggleButton from "./ToggleButton"; +import { useResizableColumns } from "./hooks"; + +type ColumnKey = "base" | "current" | "previous"; +type ColumnState = Record; +const ALL_COLUMNS: ColumnKey[] = ["base", "current", "previous"]; + +const diffContentsToString = ( + diff: api.DiffOutput, + kind: ColumnKey, +): string => { + return diff.rows + .map((row) => row[kind]?.text?.map((t) => t.text).join("") ?? "") + .join("\n"); }; // https://github.com/bvaughn/react-window#can-i-add-padding-to-the-top-and-bottom-of-a-list @@ -65,16 +63,24 @@ const innerElementType = forwardRef< }); innerElementType.displayName = "innerElementType"; +export type VisibleRow = { + key: string; + cells: Array; + isPlaceholder?: boolean; +}; + function DiffBody({ diff, diffLabel, fontSize, compressionEnabled, + columns, }: { diff: api.DiffOutput | null; diffLabel: string | null; fontSize: number | undefined; compressionEnabled: boolean; + columns: Array; }) { const { highlighters, setHighlightAll } = useHighlighers(3); const [compressionContext] = diffCompressionContext(); @@ -91,19 +97,39 @@ function DiffBody({ Record >({}); + useEffect(() => { + setExpandedGroups({}); + }, [groups]); + const flattened = useMemo( () => flattenGroups(groups, expandedGroups), [groups, expandedGroups], ); - const itemData: AsmDiffer.DiffListData = useMemo( + const visibleRows: VisibleRow[] = useMemo(() => { + const getCells = (row: api.DiffRow) => columns.map((col) => row[col]); + + return flattened.map((row) => { + const isPlaceholder = row.base?.text?.[0]?.format === "diff_skip"; + + return { + key: row.key, + isPlaceholder, + cells: getCells(row), + }; + }); + }, [flattened, columns]); + + const handleToggle = (key: string) => { + setExpandedGroups((prev) => ({ ...prev, [key]: !prev[key] })); + }; + const itemData = useMemo( () => ({ - rows: flattened, + rows: visibleRows, highlighters, - onToggle: (key: string) => - setExpandedGroups((prev) => ({ ...prev, [key]: !prev[key] })), + onToggle: handleToggle, }), - [flattened, highlighters], + [visibleRows, highlighters], ); if (!diff) { @@ -144,30 +170,6 @@ function DiffBody({ ); } -function ThreeWayToggleButton({ - enabled, - setEnabled, -}: { enabled: boolean; setEnabled: (enabled: boolean) => void }) { - return ( - - ); -} - function CompressToggleButton({ enabled, setEnabled, @@ -178,9 +180,7 @@ function CompressToggleButton({ return ( + ); +} diff --git a/frontend/src/components/Diff/hooks.ts b/frontend/src/components/Diff/hooks.ts new file mode 100644 index 000000000..613364032 --- /dev/null +++ b/frontend/src/components/Diff/hooks.ts @@ -0,0 +1,78 @@ +import { useState } from "react"; + +type ResizableColumns = { + bar1Px: number; + bar2Px: number; + setBar1Px: (px: number) => void; + setBar2Px: (px: number) => void; +}; + +export function useResizableColumns({ + width, + columnCount, + minColumnWidth = 80, +}: { + width: number; + columnCount: number; + minColumnWidth?: number; +}): ResizableColumns { + const [bar1Ratio, setBar1Ratio] = useState(null); + const [bar2Ratio, setBar2Ratio] = useState(null); + + const defaultRatios = (() => { + if (columnCount <= 1) return [1, 1]; + if (columnCount === 2) return [0.5, 1]; + return [1 / 3, 2 / 3]; + })(); + + const r1 = bar1Ratio ?? defaultRatios[0]; + const r2 = bar2Ratio ?? defaultRatios[1]; + + const rawBar1 = r1 * width; + const rawBar2 = r2 * width; + + function clamp() { + if (!width || columnCount <= 1) { + return { bar1: width, bar2: width }; + } + + if (columnCount === 2) { + const bar1 = Math.max( + minColumnWidth, + Math.min(width - minColumnWidth, rawBar1), + ); + return { bar1, bar2: width }; + } + + const bar1 = Math.max( + minColumnWidth, + Math.min(width - minColumnWidth * 2, rawBar1), + ); + + const bar2 = Math.max( + bar1 + minColumnWidth, + Math.min(width - minColumnWidth, rawBar2), + ); + + return { bar1, bar2 }; + } + + const { bar1, bar2 } = clamp(); + + const setBar1Px = (px: number) => { + if (!width) return; + setBar1Ratio(px / width); + }; + + const setBar2Px = (px: number) => { + if (!width) return; + setBar2Ratio(px / width); + }; + + return { + bar1Px: bar1, + bar2Px: bar2, + setBar1Px, + setBar2Px, + }; +}