1import { useEffect, useRef, useState } from "react";
2
3import {
4 highlightNativeReviewDiffVisibleRows,
5 type NativeReviewDiffHighlightEngine,
6 type NativeReviewDiffHighlightScheme,
7} from "../diffs/nativeReviewDiffHighlighter";
8import type { NativeReviewDiffRow } from "../diffs/nativeReviewDiffSurface";
9import type { NativeReviewDiffFile } from "../diffs/nativeReviewDiffTypes";
10
11import {
12 createReviewDiffHighlightScheduler,
13 type NativeReviewVisibleRange,
14} from "./reviewDiffHighlightScheduler";
15
16function createEmptyTokenPatch(resetKey: string): string {
17 return JSON.stringify({ resetKey, tokensByRowId: {} });
18}
19
20function isReviewDiffDebugLoggingEnabled(): boolean {
21 return typeof __DEV__ !== "undefined" ? __DEV__ : false;
22}
23
24function logReviewDiffDiagnostic(message: string, details?: Record<string, unknown>): void {
25 if (!isReviewDiffDebugLoggingEnabled()) {
26 return;
27 }
28
29 if (details) {
30 console.log(`[review-sheet] ${message}`, details);
31 return;
32 }
33
34 console.log(`[review-sheet] ${message}`);
35}
36
37export function useNativeReviewDiffHighlighting(input: {
38 readonly files: ReadonlyArray<NativeReviewDiffFile>;
39 readonly rows: ReadonlyArray<NativeReviewDiffRow>;
40 readonly scheme: NativeReviewDiffHighlightScheme;
41 readonly resetKey: string;
42 readonly enabled: boolean;
43}) {
44 const { enabled, files, resetKey, rows, scheme } = input;
45 const highlightedRowIdsRef = useRef<Set<string>>(new Set());
46 const [visibleRange, setVisibleRange] = useState<NativeReviewVisibleRange>({
47 firstRowIndex: 0,
48 lastRowIndex: 80,
49 });
50 const visibleChunkIndexRef = useRef(0);
51 const [tokensPatchJson, setTokensPatchJson] = useState(() => createEmptyTokenPatch(resetKey));
52 const [scheduler] = useState(() => createReviewDiffHighlightScheduler(setVisibleRange));
53
54 useEffect(() => {
55 scheduler.reset();
56 highlightedRowIdsRef.current = new Set();
57 visibleChunkIndexRef.current = 0;
58 setVisibleRange({ firstRowIndex: 0, lastRowIndex: 80 });
59 setTokensPatchJson(createEmptyTokenPatch(resetKey));
60 return () => scheduler.cancel();
61 }, [enabled, resetKey, rows.length, scheduler]);
62
63 useEffect(() => {
64 if (!enabled || rows.length === 0) {
65 return;
66 }
67
68 const abortController = new AbortController();
69 const requestRange = visibleRange;
70 const engine: NativeReviewDiffHighlightEngine = "native";
71
72 void (async () => {
73 try {
74 const result = await highlightNativeReviewDiffVisibleRows({
75 files,
76 rows,
77 scheme,
78 engine,
79 firstRowIndex: requestRange.firstRowIndex,
80 lastRowIndex: requestRange.lastRowIndex,
81 alreadyHighlightedRowIds: highlightedRowIdsRef.current,
82 signal: abortController.signal,
83 });
84
85 if (abortController.signal.aborted || result.rowCount === 0) {
86 return;
87 }
88
89 for (const rowId of Object.keys(result.tokensByRowId)) {
90 highlightedRowIdsRef.current.add(rowId);
91 }
92
93 const chunkIndex = visibleChunkIndexRef.current;
94 visibleChunkIndexRef.current += 1;
95 setTokensPatchJson(
96 JSON.stringify({
97 resetKey,
98 chunkIndex,
99 fileId: "visible",
100 filePath: "visible rows",
101 language: "diff",
102 lineCount: result.rowCount,
103 durationMs: result.durationMs,
104 tokensByRowId: result.tokensByRowId,
105 }),
106 );
107 } catch (error) {
108 if (!abortController.signal.aborted) {
109 logReviewDiffDiagnostic("native visible highlight failed", {
110 error,
111 resetKey,
112 scheme,
113 firstRowIndex: requestRange.firstRowIndex,
114 lastRowIndex: requestRange.lastRowIndex,
115 });
116 }
117 }
118 })();
119
120 return () => abortController.abort();
121 }, [enabled, files, resetKey, rows, scheme, visibleRange]);
122
123 return {
124 tokensPatchJson,
125 updateVisibleRange: scheduler.update,
126 };
127}