diff --git a/src/renderer/src/components/pull-request-page/files/active-section.ts b/src/renderer/src/components/pull-request-page/files/active-section.ts new file mode 100644 index 00000000000..cc234d7a012 --- /dev/null +++ b/src/renderer/src/components/pull-request-page/files/active-section.ts @@ -0,0 +1,12 @@ +import { useCallback, useState } from 'react' + +export function usePRFileActiveSection( + entrySignature: string +): [string | null, (key: string | null) => void] { + const [state, setState] = useState({ entrySignature, key: null as string | null }) + const setActiveSection = useCallback( + (key: string | null) => setState({ entrySignature, key }), + [entrySignature] + ) + return [state.entrySignature === entrySignature ? state.key : null, setActiveSection] +} diff --git a/src/renderer/src/components/pull-request-page/files/combined-diff-viewer.tsx b/src/renderer/src/components/pull-request-page/files/combined-diff-viewer.tsx index 8a50aabcf26..3cee0fee2aa 100644 --- a/src/renderer/src/components/pull-request-page/files/combined-diff-viewer.tsx +++ b/src/renderer/src/components/pull-request-page/files/combined-diff-viewer.tsx @@ -1,4 +1,3 @@ -/* oxlint-disable react-doctor/no-adjust-state-on-prop-change -- Why: switching PRs re-seeds the whole diff view (sections, heights, scroll target, cached loaded set) from the persisted cache, which cannot be derived during render. */ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import { useVirtualizer } from '@tanstack/react-virtual' import type { editor as monacoEditor } from 'monaco-editor' @@ -34,6 +33,8 @@ import { addPullRequestLineComment } from './line-comment' import { usePRFileSectionLoader } from './section-loader' import { usePRFilesDiffViewPersistence } from './view-restore' import { buildInlineReviewComments } from './inline-comments' +import { usePRFileSectionHeights } from './section-heights' +import { usePRFileActiveSection } from './active-section' export function PRFilesCombinedDiffViewer({ files, @@ -106,8 +107,8 @@ export function PRFilesCombinedDiffViewer({ const [sections, setSections] = useState([]) const [sideBySide, setSideBySide] = useState(false) const [fileTreeCollapsed, setFileTreeCollapsed] = useState(false) - const [sectionHeights, setSectionHeights] = useState>({}) - const [activeTreeSectionKey, setActiveTreeSectionKey] = useState(null) + const [sectionHeights, setSectionHeights] = usePRFileSectionHeights(entrySignature) + const [activeTreeSectionKey, setActiveTreeSectionKey] = usePRFileActiveSection(entrySignature) // Why: short virtualizer-key token; entrySignature serializes every file and would be copied into every item key. const [entryRevision, setEntryRevision] = useState(0) const scrollContainerRef = useRef(null) @@ -147,8 +148,6 @@ export function PRFilesCombinedDiffViewer({ loadedIndicesRef.current.clear() loadingIndicesRef.current.clear() pendingRestoreScrollTopRef.current = prFilesDiffScrollTopCache.get(viewStateKey) ?? null - setSectionHeights({}) - setActiveTreeSectionKey(null) setSections( entries.map((entry) => ({ key: getPRFileSectionKey(entry.path), @@ -167,7 +166,7 @@ export function PRFilesCombinedDiffViewer({ largeDiffRenderLimit: null })) ) - }, [entries, entrySignature, viewStateKey]) + }, [entries, entrySignature, setActiveTreeSectionKey, setSectionHeights, viewStateKey]) const { loadSection, retrySection, toggleSection, setAllSectionsCollapsed } = usePRFileSectionLoader({ @@ -189,6 +188,10 @@ export function PRFilesCombinedDiffViewer({ const allSectionsCollapsed = sections.length > 0 && sections.every((section) => section.collapsed) const sectionIndexByKey = useMemo(() => createCombinedDiffSectionIndexMap(sections), [sections]) + const visibleActiveTreeSectionKey = + activeTreeSectionKey && sectionIndexByKey.has(activeTreeSectionKey) + ? activeTreeSectionKey + : null const viewedSectionKeys = useMemo( () => new Set(files.filter(isPRFileViewed).map((file) => getPRFileSectionKey(file.path))), [files] @@ -237,7 +240,7 @@ export function PRFilesCombinedDiffViewer({ sectionHeights, sideBySide, fileTreeCollapsed, - activeTreeSectionKey, + activeTreeSectionKey: visibleActiveTreeSectionKey, loadedIndicesRef, scrollContainerRef, pendingRestoreScrollTopRef @@ -257,7 +260,7 @@ export function PRFilesCombinedDiffViewer({ setActiveTreeSectionKey(sectionsRef.current[navigatedIndex]?.key ?? null) } }, - [sectionIndexByKey, toggleSection, virtualizer] + [sectionIndexByKey, setActiveTreeSectionKey, toggleSection, virtualizer] ) const openFilesOnGitHub = useCallback(() => { @@ -334,7 +337,7 @@ export function PRFilesCombinedDiffViewer({ worktreePath={repoPath} entries={entries} sectionIndexByKey={sectionIndexByKey} - activeSectionKey={activeTreeSectionKey} + activeSectionKey={visibleActiveTreeSectionKey} viewedSectionKeys={viewedSectionKeys} collapsed={fileTreeCollapsed} onCollapsedChange={setFileTreeCollapsed} diff --git a/src/renderer/src/components/pull-request-page/files/inline-comments.ts b/src/renderer/src/components/pull-request-page/files/inline-comments.ts index f2370f05296..a23189f25a9 100644 --- a/src/renderer/src/components/pull-request-page/files/inline-comments.ts +++ b/src/renderer/src/components/pull-request-page/files/inline-comments.ts @@ -1,4 +1,4 @@ -import type { DecoratedDiffComment } from '@/components/diff-comments/useDiffCommentDecorator' +import type { DecoratedDiffComment } from '@/components/diff-comments/decorated-diff-comment' import { formatRelativeTime } from '@/components/github/work-item-state-presentation' import type { PRComment } from '../../../../../shared/github/comment-types' diff --git a/src/renderer/src/components/pull-request-page/files/section-heights.ts b/src/renderer/src/components/pull-request-page/files/section-heights.ts new file mode 100644 index 00000000000..0caba64fb80 --- /dev/null +++ b/src/renderer/src/components/pull-request-page/files/section-heights.ts @@ -0,0 +1,23 @@ +import { useCallback, useState, type SetStateAction } from 'react' + +const EMPTY_SECTION_HEIGHTS: Record = {} + +export function usePRFileSectionHeights( + entrySignature: string +): [Record, (updater: SetStateAction>) => void] { + const [state, setState] = useState(() => ({ entrySignature, values: EMPTY_SECTION_HEIGHTS })) + const heights = state.entrySignature === entrySignature ? state.values : EMPTY_SECTION_HEIGHTS + const setHeights = useCallback( + (updater: SetStateAction>) => { + setState((prev) => { + const current = prev.entrySignature === entrySignature ? prev.values : EMPTY_SECTION_HEIGHTS + return { + entrySignature, + values: typeof updater === 'function' ? updater(current) : updater + } + }) + }, + [entrySignature] + ) + return [heights, setHeights] +}