Files
tolaria/src/hooks/useDiffMode.ts
2026-05-05 12:29:46 +02:00

289 lines
8.7 KiB
TypeScript

import {
useState,
useEffect,
useCallback,
useRef,
useMemo,
type Dispatch,
type SetStateAction,
} from 'react'
export interface CommitDiffRequest {
requestId: number
path: string
commitHash?: string | null
}
interface UseDiffModeParams {
activeTabPath: string | null
onLoadDiff?: (path: string) => Promise<string>
onLoadDiffAtCommit?: (path: string, commitHash: string) => Promise<string>
pendingCommitDiffRequest?: CommitDiffRequest | null
onPendingCommitDiffHandled?: (requestId: number) => void
}
interface DiffStateSetters {
setDiffMode: Dispatch<SetStateAction<boolean>>
setDiffContent: Dispatch<SetStateAction<string | null>>
setDiffLoading: Dispatch<SetStateAction<boolean>>
setDiffPath: Dispatch<SetStateAction<string | null>>
}
type DiffLoadCancellation = { current: boolean }
function useDiffLoadCancellation() {
const activeDiffLoadRef = useRef<DiffLoadCancellation | null>(null)
const cancelActiveDiffLoad = useCallback(() => {
if (!activeDiffLoadRef.current) return
activeDiffLoadRef.current.current = true
activeDiffLoadRef.current = null
}, [])
const createDiffLoadCancellation = useCallback(() => {
cancelActiveDiffLoad()
const cancellation = { current: false }
activeDiffLoadRef.current = cancellation
return cancellation
}, [cancelActiveDiffLoad])
const clearDiffLoadCancellation = useCallback((cancellation: DiffLoadCancellation) => {
if (activeDiffLoadRef.current === cancellation) {
activeDiffLoadRef.current = null
}
}, [])
return {
cancelActiveDiffLoad,
createDiffLoadCancellation,
clearDiffLoadCancellation,
}
}
async function loadDiffForPath(
path: string,
onLoadDiff: ((path: string) => Promise<string>) | undefined,
{ setDiffMode, setDiffContent, setDiffLoading, setDiffPath }: DiffStateSetters,
) {
if (!onLoadDiff) return
setDiffLoading(true)
try {
const diff = await onLoadDiff(path)
setDiffContent(diff)
setDiffPath(path)
setDiffMode(true)
} catch (err) {
console.warn('Failed to load diff:', err)
} finally {
setDiffLoading(false)
}
}
async function loadCommitDiffForPath(
path: string,
commitHash: string,
onLoadDiffAtCommit: ((path: string, commitHash: string) => Promise<string>) | undefined,
{ setDiffMode, setDiffContent, setDiffLoading, setDiffPath }: DiffStateSetters,
) {
if (!onLoadDiffAtCommit) return
setDiffLoading(true)
try {
const diff = await onLoadDiffAtCommit(path, commitHash)
setDiffContent(diff)
setDiffPath(path)
setDiffMode(true)
} catch (err) {
console.warn('Failed to load commit diff:', err)
} finally {
setDiffLoading(false)
}
}
function shouldHandlePendingCommitDiffRequest(
activeTabPath: string | null,
pendingCommitDiffRequest: CommitDiffRequest | null | undefined,
): pendingCommitDiffRequest is CommitDiffRequest {
return !!pendingCommitDiffRequest && pendingCommitDiffRequest.path === activeTabPath
}
function hasCommitHash(pendingCommitDiffRequest: CommitDiffRequest): pendingCommitDiffRequest is CommitDiffRequest & { commitHash: string } {
return typeof pendingCommitDiffRequest.commitHash === 'string' && pendingCommitDiffRequest.commitHash.length > 0
}
function buildGuardedDiffStateSetters(
cancelledRef: DiffLoadCancellation,
{ setDiffMode, setDiffContent, setDiffLoading, setDiffPath }: DiffStateSetters,
): DiffStateSetters {
return {
setDiffMode: (value) => { if (!cancelledRef.current) setDiffMode(value) },
setDiffContent: (value) => { if (!cancelledRef.current) setDiffContent(value) },
setDiffLoading: (value) => { if (!cancelledRef.current) setDiffLoading(value) },
setDiffPath: (value) => { if (!cancelledRef.current) setDiffPath(value) },
}
}
function closeDiffMode(activeTabPath: string | null, {
setDiffMode,
setDiffContent,
setDiffLoading,
setDiffPath,
}: DiffStateSetters) {
setDiffPath(activeTabPath)
setDiffMode(false)
setDiffContent(null)
setDiffLoading(false)
}
function runPendingCommitDiffRequest(
pendingCommitDiffRequest: CommitDiffRequest,
onLoadDiff: ((path: string) => Promise<string>) | undefined,
onLoadDiffAtCommit: ((path: string, commitHash: string) => Promise<string>) | undefined,
onPendingCommitDiffHandled: ((requestId: number) => void) | undefined,
diffState: DiffStateSetters,
) {
const cancelledRef = { current: false }
const loadDiffPromise = hasCommitHash(pendingCommitDiffRequest)
? loadCommitDiffForPath(
pendingCommitDiffRequest.path,
pendingCommitDiffRequest.commitHash,
onLoadDiffAtCommit,
buildGuardedDiffStateSetters(cancelledRef, diffState),
)
: loadDiffForPath(
pendingCommitDiffRequest.path,
onLoadDiff,
buildGuardedDiffStateSetters(cancelledRef, diffState),
)
void loadDiffPromise.finally(() => {
if (cancelledRef.current) return
onPendingCommitDiffHandled?.(pendingCommitDiffRequest.requestId)
})
return () => {
cancelledRef.current = true
}
}
function usePendingCommitDiffRequest({
activeTabPath,
onLoadDiff,
onLoadDiffAtCommit,
pendingCommitDiffRequest,
onPendingCommitDiffHandled,
setDiffMode,
setDiffContent,
setDiffLoading,
setDiffPath,
}: UseDiffModeParams & DiffStateSetters) {
useEffect(() => {
if (!shouldHandlePendingCommitDiffRequest(activeTabPath, pendingCommitDiffRequest)) return
if (hasCommitHash(pendingCommitDiffRequest) && !onLoadDiffAtCommit) {
onPendingCommitDiffHandled?.(pendingCommitDiffRequest.requestId)
return
}
if (!hasCommitHash(pendingCommitDiffRequest) && !onLoadDiff) {
onPendingCommitDiffHandled?.(pendingCommitDiffRequest.requestId)
return
}
return runPendingCommitDiffRequest(
pendingCommitDiffRequest,
onLoadDiff,
onLoadDiffAtCommit,
onPendingCommitDiffHandled,
{ setDiffMode, setDiffContent, setDiffLoading, setDiffPath },
)
}, [activeTabPath, onLoadDiff, onLoadDiffAtCommit, onPendingCommitDiffHandled, pendingCommitDiffRequest, setDiffContent, setDiffLoading, setDiffMode, setDiffPath])
}
function useDiffPathReset(
activeTabPath: string | null,
cancelActiveDiffLoad: () => void,
diffState: DiffStateSetters,
) {
useEffect(() => {
cancelActiveDiffLoad()
closeDiffMode(activeTabPath, diffState)
}, [activeTabPath, cancelActiveDiffLoad, diffState])
}
function useCancelDiffLoadOnUnmount(cancelActiveDiffLoad: () => void) {
useEffect(() => () => cancelActiveDiffLoad(), [cancelActiveDiffLoad])
}
export function useDiffMode({
activeTabPath,
onLoadDiff,
onLoadDiffAtCommit,
pendingCommitDiffRequest,
onPendingCommitDiffHandled,
}: UseDiffModeParams) {
const [diffMode, setDiffMode] = useState(false)
const [diffContent, setDiffContent] = useState<string | null>(null)
const [diffLoading, setDiffLoading] = useState(false)
const [diffPath, setDiffPath] = useState<string | null>(activeTabPath)
const diffState = useMemo(() => ({
setDiffMode,
setDiffContent,
setDiffLoading,
setDiffPath,
}), [setDiffMode, setDiffContent, setDiffLoading, setDiffPath])
const {
cancelActiveDiffLoad,
createDiffLoadCancellation,
clearDiffLoadCancellation,
} = useDiffLoadCancellation()
useDiffPathReset(activeTabPath, cancelActiveDiffLoad, diffState)
useCancelDiffLoadOnUnmount(cancelActiveDiffLoad)
usePendingCommitDiffRequest({
activeTabPath,
onLoadDiff,
onLoadDiffAtCommit,
pendingCommitDiffRequest,
onPendingCommitDiffHandled,
setDiffMode,
setDiffContent,
setDiffLoading,
setDiffPath,
})
const isDiffVisible = diffMode && diffPath === activeTabPath
const handleToggleDiff = useCallback(async () => {
if (isDiffVisible) {
cancelActiveDiffLoad()
closeDiffMode(activeTabPath, diffState)
return
}
if (!activeTabPath || !onLoadDiff) return
const cancellation = createDiffLoadCancellation()
try {
await loadDiffForPath(activeTabPath, onLoadDiff, buildGuardedDiffStateSetters(cancellation, diffState))
} finally {
clearDiffLoadCancellation(cancellation)
}
}, [activeTabPath, cancelActiveDiffLoad, clearDiffLoadCancellation, createDiffLoadCancellation, diffState, isDiffVisible, onLoadDiff])
const handleViewCommitDiff = useCallback(async (commitHash: string) => {
if (!activeTabPath) return
const cancellation = createDiffLoadCancellation()
try {
await loadCommitDiffForPath(activeTabPath, commitHash, onLoadDiffAtCommit, buildGuardedDiffStateSetters(cancellation, diffState))
} finally {
clearDiffLoadCancellation(cancellation)
}
}, [activeTabPath, clearDiffLoadCancellation, createDiffLoadCancellation, diffState, onLoadDiffAtCommit])
return {
diffMode: isDiffVisible,
diffContent: isDiffVisible ? diffContent : null,
diffLoading,
handleToggleDiff,
handleViewCommitDiff,
}
}