import { useAtomValue } from "@effect/atom-react"; import { AsyncResult } from "effect/unstable/reactivity"; import type { ComponentType } from "react"; import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { FlatList, ScrollView, Text as NativeText, useWindowDimensions, View } from "react-native"; import { AppText as Text } from "../../components/AppText"; import { LoadingStrip } from "../../components/LoadingStrip"; import { type NativeReviewDiffViewProps, resolveNativeReviewDiffView, } from "../diffs/nativeReviewDiffSurface"; import { createNativeReviewDiffTheme } from "../review/nativeReviewDiffAdapter"; import { REVIEW_MONO_FONT_FAMILY, renderVisibleWhitespace } from "../review/reviewDiffRendering"; import type { ReviewHighlightedToken } from "../review/shikiReviewHighlighter"; import { cn } from "../../lib/cn"; import type { ResolvedMobileCodeSurface } from "../../lib/appearancePreferences"; import { useAppearanceCodeSurface } from "../settings/appearance/useAppearanceCodeSurface"; import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; import { buildNativeSourceTokens, NATIVE_SOURCE_CONTENT_WIDTH, nativeSourceRowId, } from "./nativeSourceFileAdapter"; import { prepareSourceFileDocument } from "./source-file-document"; import { sourceHighlightAtom } from "./sourceHighlightingState"; interface SourceFileSurfaceProps { readonly contents: string; readonly path: string; readonly initialLine?: number | null; /** Enables native pull-to-refresh on the source surface. */ readonly onRefresh?: () => Promise | void; } type SourceHighlightStatus = "highlighting" | "ready" | "error"; const HighlightedSourceLine = memo(function HighlightedSourceLine(props: { readonly codeSurface: ResolvedMobileCodeSurface; readonly index: number; readonly line: string; readonly tokens: ReadonlyArray | null; readonly highlighted: boolean; readonly wordBreak: boolean; }) { return ( {props.index + 1} {props.tokens && props.tokens.length > 0 ? (() => { let offset = 0; return props.tokens.map((token) => { const start = offset; offset += token.content.length; const fontWeight = token.fontStyle !== null && (token.fontStyle & 2) === 2 ? ("700" as const) : ("400" as const); const fontStyle = token.fontStyle !== null && (token.fontStyle & 1) === 1 ? ("italic" as const) : ("normal" as const); return ( {token.content.length > 0 ? renderVisibleWhitespace(token.content) : " "} ); }); })() : renderVisibleWhitespace(props.line || " ")} ); }); function useSourceFileModel(props: SourceFileSurfaceProps) { const { themeAppearance: theme } = useAppearancePreferences(); const document = useMemo(() => prepareSourceFileDocument(props.contents), [props.contents]); const { contents: normalizedContents, lines, rowsJson } = document; const targetIndex = props.initialLine !== null && props.initialLine !== undefined && props.initialLine > 0 ? Math.min(Math.floor(props.initialLine) - 1, Math.max(0, lines.length - 1)) : null; const highlightAtom = useMemo( () => sourceHighlightAtom({ path: props.path, contents: normalizedContents, theme }), [normalizedContents, props.path, theme], ); const highlightResult = useAtomValue(highlightAtom); const tokens = AsyncResult.isSuccess(highlightResult) ? highlightResult.value : null; const status: SourceHighlightStatus = AsyncResult.isFailure(highlightResult) ? "error" : AsyncResult.isSuccess(highlightResult) ? "ready" : "highlighting"; return { lines, rowsJson, status, targetIndex, theme, tokens }; } function SourceHighlightStatusView(props: { readonly status: SourceHighlightStatus }) { if (props.status === "highlighting") { return ; } if (props.status === "error") { return ( Plain text ); } return null; } function NativeSourceFileSurface( props: SourceFileSurfaceProps & { readonly NativeView: ComponentType; }, ) { const { NativeView, onRefresh } = props; const { codeSurface, codeWordBreak, nativeSourceStyle } = useAppearanceCodeSurface(); const { themeAppearance, themeId } = useAppearancePreferences(); const { width: viewportWidth } = useWindowDimensions(); const { rowsJson, status, targetIndex, tokens } = useSourceFileModel(props); const [isPullRefreshing, setIsPullRefreshing] = useState(false); const handlePullToRefresh = useCallback(async () => { if (!onRefresh) { return; } setIsPullRefreshing(true); try { await onRefresh(); } finally { setIsPullRefreshing(false); } }, [onRefresh]); const tokensJson = useMemo(() => JSON.stringify(buildNativeSourceTokens(tokens)), [tokens]); const selectedRowIdsJson = useMemo( () => JSON.stringify(targetIndex === null ? [] : [nativeSourceRowId(targetIndex)]), [targetIndex], ); const themeJson = useMemo( () => JSON.stringify(createNativeReviewDiffTheme(themeAppearance, themeId)), [themeAppearance, themeId], ); const styleJson = useMemo(() => JSON.stringify(nativeSourceStyle), [nativeSourceStyle]); const contentWidth = codeWordBreak ? Math.max(240, viewportWidth - codeSurface.gutterWidth - 24) : NATIVE_SOURCE_CONTENT_WIDTH; return ( void handlePullToRefresh(), } : {})} /> ); } function JavaScriptSourceFileSurface(props: SourceFileSurfaceProps) { const { codeSurface, codeWordBreak } = useAppearanceCodeSurface(); const { lines, status, targetIndex, tokens } = useSourceFileModel(props); const listRef = useRef>(null); useEffect(() => { if (targetIndex === null) { return; } const frame = requestAnimationFrame(() => { listRef.current?.scrollToIndex({ index: targetIndex, animated: false, viewPosition: 0.3 }); }); return () => cancelAnimationFrame(frame); }, [props.path, targetIndex]); const renderLine = useCallback( ({ item, index }: { item: string; index: number }) => ( ), [codeSurface, codeWordBreak, targetIndex, tokens], ); const list = ( String(index)} initialNumToRender={80} maxToRenderPerBatch={80} windowSize={12} {...(codeWordBreak ? {} : { getItemLayout: (_data, index) => ({ length: codeSurface.rowHeight, offset: codeSurface.rowHeight * index, index, }), })} contentContainerStyle={{ minWidth: codeWordBreak ? undefined : "100%", paddingBottom: codeSurface.rowHeight, paddingTop: 8, }} renderItem={renderLine} /> ); return ( {codeWordBreak ? ( list ) : ( {list} )} ); } export function SourceFileSurface(props: SourceFileSurfaceProps) { const NativeView = resolveNativeReviewDiffView(); return NativeView ? ( ) : ( ); }