| ... | ... | @@ -21,7 +21,7 @@ import { |
| 21 | 21 | import { ChevronDown, ChevronRight, ChevronsDownUp, ChevronsUpDown } from "lucide"; |
| 22 | 22 | import * as Schema from "effect/Schema"; |
| 23 | 23 | import * as DateTime from "effect/DateTime"; |
| 24 | | import { useCallback, useEffect, useMemo, useRef, useState } from "react"; |
| 24 | import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; |
| 25 | 25 | import { useCodeViewFileReveal } from "./diffs/useCodeViewFileReveal"; |
| 26 | 26 | import { useOpenInPreferredEditor } from "../editorPreferences"; |
| 27 | 27 | import { useFileContextMenuHandler } from "../fileContextMenu"; |
| ... | ... | @@ -118,6 +118,74 @@ interface CollapsedDiffFilesState { |
| 118 | 118 | |
| 119 | 119 | const EMPTY_COLLAPSED_DIFF_FILE_KEYS: ReadonlySet<string> = new Set(); |
| 120 | 120 | |
| 121 | /** Collapse control for one file header; re-renders only when its own file changes. */ |
| 122 | function DiffFileCollapseToggle({ |
| 123 | filePath, |
| 124 | fileKey, |
| 125 | collapsed, |
| 126 | unavailable, |
| 127 | iconClassName, |
| 128 | onToggle, |
| 129 | }: { |
| 130 | filePath: string; |
| 131 | fileKey: string; |
| 132 | collapsed: boolean; |
| 133 | unavailable: boolean; |
| 134 | iconClassName: string; |
| 135 | onToggle: (fileKey: string) => void; |
| 136 | }) { |
| 137 | return ( |
| 138 | <Tooltip> |
| 139 | <TooltipTrigger |
| 140 | render={ |
| 141 | <Button |
| 142 | size="icon-micro" |
| 143 | variant="ghost" |
| 144 | className="-ms-0.5" |
| 145 | aria-label={collapsed ? `Expand ${filePath}` : `Collapse ${filePath}`} |
| 146 | aria-expanded={!collapsed} |
| 147 | disabled={unavailable} |
| 148 | onClick={(event) => { |
| 149 | event.stopPropagation(); |
| 150 | onToggle(fileKey); |
| 151 | }} |
| 152 | /> |
| 153 | } |
| 154 | > |
| 155 | <MorphIcon |
| 156 | className={cn("size-4", iconClassName)} |
| 157 | icon={collapsed ? ChevronRight : ChevronDown} |
| 158 | /> |
| 159 | </TooltipTrigger> |
| 160 | <TooltipPopup side="top">{collapsed ? "Expand diff" : "Collapse diff"}</TooltipPopup> |
| 161 | </Tooltip> |
| 162 | ); |
| 163 | } |
| 164 | |
| 165 | /** Copy and status controls for one file header; re-renders only when its own file changes. */ |
| 166 | function DiffFileHeaderSuffix({ |
| 167 | filePath, |
| 168 | hasStat, |
| 169 | error, |
| 170 | truncated, |
| 171 | onRetry, |
| 172 | }: { |
| 173 | filePath: string; |
| 174 | hasStat: boolean; |
| 175 | error: boolean; |
| 176 | truncated: boolean; |
| 177 | onRetry: (path: string) => void; |
| 178 | }) { |
| 179 | return ( |
| 180 | <> |
| 181 | <DiffFilePathCopyButton filePath={filePath} /> |
| 182 | {hasStat ? ( |
| 183 | <DiffFileStatus error={error} truncated={truncated} retry={() => onRetry(filePath)} /> |
| 184 | ) : null} |
| 185 | </> |
| 186 | ); |
| 187 | } |
| 188 | |
| 121 | 189 | interface DiffPanelProps { |
| 122 | 190 | mode?: DiffPanelMode; |
| 123 | 191 | composerDraftTarget: ScopedThreadRef | DraftId; |
| ... | ... | @@ -598,22 +666,24 @@ export default function DiffPanel({ |
| 598 | 666 | }, |
| 599 | 667 | [activeCwd, activeRepositoryRoot, openInPreferredEditor, routeThreadRef], |
| 600 | 668 | ); |
| 601 | | const toggleDiffFileCollapsed = useCallback( |
| 602 | | (fileKey: string) => { |
| 603 | | setCollapsedDiffFiles((current) => { |
| 604 | | const next = new Set( |
| 605 | | current.scopeKey === collapseScopeKey ? current.fileKeys : defaultCollapsedDiffFileKeys, |
| 606 | | ); |
| 607 | | if (next.has(fileKey)) { |
| 608 | | next.delete(fileKey); |
| 609 | | } else { |
| 610 | | next.add(fileKey); |
| 611 | | } |
| 612 | | return { scopeKey: collapseScopeKey, fileKeys: next }; |
| 613 | | }); |
| 614 | | }, |
| 615 | | [collapseScopeKey, defaultCollapsedDiffFileKeys], |
| 616 | | ); |
| 669 | const collapseDefaultsRef = useRef({ collapseScopeKey, defaultCollapsedDiffFileKeys }); |
| 670 | useLayoutEffect(() => { |
| 671 | collapseDefaultsRef.current = { collapseScopeKey, defaultCollapsedDiffFileKeys }; |
| 672 | }, [collapseScopeKey, defaultCollapsedDiffFileKeys]); |
| 673 | const toggleDiffFileCollapsed = useCallback((fileKey: string) => { |
| 674 | const { collapseScopeKey, defaultCollapsedDiffFileKeys } = collapseDefaultsRef.current; |
| 675 | setCollapsedDiffFiles((current) => { |
| 676 | const next = new Set( |
| 677 | current.scopeKey === collapseScopeKey ? current.fileKeys : defaultCollapsedDiffFileKeys, |
| 678 | ); |
| 679 | if (next.has(fileKey)) { |
| 680 | next.delete(fileKey); |
| 681 | } else { |
| 682 | next.add(fileKey); |
| 683 | } |
| 684 | return { scopeKey: collapseScopeKey, fileKeys: next }; |
| 685 | }); |
| 686 | }, []); |
| 617 | 687 | |
| 618 | 688 | const toggleDiffFileCollapse = useCallback(() => { |
| 619 | 689 | setCodeViewRevision((current) => current + 1); |
| ... | ... | @@ -1082,14 +1152,15 @@ export default function DiffPanel({ |
| 1082 | 1152 | composerDraftTarget={composerDraftTarget} |
| 1083 | 1153 | renderHeaderFilenameSuffix={(fileDiff) => { |
| 1084 | 1154 | const path = resolveFileDiffPath(fileDiff); |
| 1085 | | const stat = fileStats.get(path); |
| 1155 | const state = fileStates.get(path); |
| 1086 | 1156 | return ( |
| 1087 | | <> |
| 1088 | | <DiffFilePathCopyButton filePath={path} /> |
| 1089 | | {stat ? ( |
| 1090 | | <DiffFileStatus {...fileStates.get(path)} retry={() => retry(path)} /> |
| 1091 | | ) : null} |
| 1092 | | </> |
| 1157 | <DiffFileHeaderSuffix |
| 1158 | filePath={path} |
| 1159 | hasStat={fileStats.has(path)} |
| 1160 | error={state?.error ?? false} |
| 1161 | truncated={state?.truncated ?? false} |
| 1162 | onRetry={retry} |
| 1163 | /> |
| 1093 | 1164 | ); |
| 1094 | 1165 | }} |
| 1095 | 1166 | {...(lazySource |
| ... | ... | @@ -1109,37 +1180,15 @@ export default function DiffPanel({ |
| 1109 | 1180 | : {})} |
| 1110 | 1181 | renderHeaderPrefix={(fileDiff, fileKey) => { |
| 1111 | 1182 | const unavailable = fileDiff.cacheKey?.endsWith(":pending") === true; |
| 1112 | | const collapsed = unavailable || collapsedDiffFileKeys.has(fileKey); |
| 1113 | | const filePath = resolveFileDiffPath(fileDiff); |
| 1114 | 1183 | return ( |
| 1115 | | <Tooltip> |
| 1116 | | <TooltipTrigger |
| 1117 | | render={ |
| 1118 | | <Button |
| 1119 | | size="icon-micro" |
| 1120 | | variant="ghost" |
| 1121 | | className="-ms-0.5" |
| 1122 | | aria-label={ |
| 1123 | | collapsed ? `Expand ${filePath}` : `Collapse ${filePath}` |
| 1124 | | } |
| 1125 | | aria-expanded={!collapsed} |
| 1126 | | disabled={unavailable} |
| 1127 | | onClick={(event) => { |
| 1128 | | event.stopPropagation(); |
| 1129 | | toggleDiffFileCollapsed(fileKey); |
| 1130 | | }} |
| 1131 | | /> |
| 1132 | | } |
| 1133 | | > |
| 1134 | | <MorphIcon |
| 1135 | | className={cn("size-4", getDiffCollapseIconClassName(fileDiff))} |
| 1136 | | icon={collapsed ? ChevronRight : ChevronDown} |
| 1137 | | /> |
| 1138 | | </TooltipTrigger> |
| 1139 | | <TooltipPopup side="top"> |
| 1140 | | {collapsed ? "Expand diff" : "Collapse diff"} |
| 1141 | | </TooltipPopup> |
| 1142 | | </Tooltip> |
| 1184 | <DiffFileCollapseToggle |
| 1185 | filePath={resolveFileDiffPath(fileDiff)} |
| 1186 | fileKey={fileKey} |
| 1187 | collapsed={unavailable || collapsedDiffFileKeys.has(fileKey)} |
| 1188 | unavailable={unavailable} |
| 1189 | iconClassName={getDiffCollapseIconClassName(fileDiff)} |
| 1190 | onToggle={toggleDiffFileCollapsed} |
| 1191 | /> |
| 1143 | 1192 | ); |
| 1144 | 1193 | }} |
| 1145 | 1194 | options={{ |