authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-05 01:53:42-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-05 01:53:42-07:00
log4db2c542a8c5e6b6756fac08615e30b220b71a2f
treed285488450cbae4837f137fe4cc4eff5b3319731
parentfedf84ac7922e11be0971ac50817d7c4199b8e89
signature Signed by PGP key B5690EEEBB952194

refactor(mobile): remove obsolete whole-file review highlighters (#10067)


2 files changed, 86 insertions(+), 528 deletions(-)

apps/mobile/src/features/review/shikiReviewHighlighter.test.ts+85-121
......@@ -1,131 +1,60 @@
11import { describe, expect, it, vi } from "vite-plus/test";
22
3import type { ReviewRenderableFile } from "./reviewModel";
4import { highlightCodeSnippet, highlightReviewFile } from "./shikiReviewHighlighter";
3import type { ReviewRenderableLineRow } from "./reviewModel";
4import {
5 highlightCodeSnippet,
6 highlightReviewSelectedLines,
7 highlightSourceFile,
8} from "./shikiReviewHighlighter";
59
6function makeRenderableFile(
7 input: Partial<ReviewRenderableFile> & Pick<ReviewRenderableFile, "path">,
8): ReviewRenderableFile {
9 return {
10 id: input.path,
11 cacheKey: input.path,
12 previousPath: null,
13 changeType: "new",
14 additions: 0,
15 deletions: 0,
16 languageHint: null,
17 additionLines: [],
18 deletionLines: [],
19 rows: [],
20 ...input,
21 };
22}
23
24describe("highlightReviewFile", () => {
25 it("preserves one highlighted token row per diff line even without trailing newlines", async () => {
26 const file = makeRenderableFile({
27 path: "apps/mobile/src/example.txt",
28 additionLines: [
29 'const items = ["a"];',
30 'expect(items).toEqual(["a"]);',
31 "const next = items.map((item) => item.toUpperCase());",
32 'expect(next).toContain("A");',
33 ],
10describe("highlightSourceFile", () => {
11 it("preserves one highlighted token row per source line without trailing newlines", async () => {
12 const lines = [
13 'const items = ["a"];',
14 'expect(items).toEqual(["a"]);',
15 "const next = items.map((item) => item.toUpperCase());",
16 'expect(next).toContain("A");',
17 ];
18
19 const highlighted = await highlightSourceFile({
20 path: "apps/mobile/src/example.ts",
21 contents: lines.join("\n"),
22 theme: "light",
3423 });
3524
36 const highlighted = await highlightReviewFile(file, "light");
37
38 expect(highlighted.additionLines).toHaveLength(file.additionLines.length);
39 expect(highlighted.additionLines[0]?.map((token) => token.content).join("")).toBe(
40 file.additionLines[0],
41 );
42 expect(highlighted.additionLines[1]?.map((token) => token.content).join("")).toBe(
43 file.additionLines[1],
25 expect(highlighted.map((tokens) => tokens.map((token) => token.content).join(""))).toEqual(
26 lines,
4427 );
45 expect(highlighted.additionLines[2]?.map((token) => token.content).join("")).toBe(
46 file.additionLines[2],
47 );
48 expect(highlighted.additionLines[3]?.map((token) => token.content).join("")).toBe(
49 file.additionLines[3],
50 );
51 });
52
53 it("adds word-alt diff emphasis for paired deletion and addition lines", async () => {
54 const file = makeRenderableFile({
55 path: "apps/mobile/src/example-inline-diff.txt",
56 additionLines: ["const after = 2;"],
57 deletionLines: ["const before = 1;"],
58 rows: [
59 {
60 kind: "line",
61 id: "delete-1",
62 change: "delete",
63 oldLineNumber: 1,
64 newLineNumber: null,
65 content: "const before = 1;",
66 additionTokenIndex: null,
67 deletionTokenIndex: 0,
68 comparison: { change: "add", tokenIndex: 0 },
69 },
70 {
71 kind: "line",
72 id: "add-1",
73 change: "add",
74 oldLineNumber: null,
75 newLineNumber: 1,
76 content: "const after = 2;",
77 additionTokenIndex: 0,
78 deletionTokenIndex: null,
79 comparison: { change: "delete", tokenIndex: 0 },
80 },
81 ],
82 });
83
84 const highlighted = await highlightReviewFile(file, "light");
85
86 expect(highlighted.deletionLines[0]?.some((token) => token.diffHighlight === true)).toBe(true);
87 expect(highlighted.additionLines[0]?.some((token) => token.diffHighlight === true)).toBe(true);
8828 });
8929
9030 it("falls back to plain tokens for very long lines", async () => {
9131 const longLine = `const value = "${"a".repeat(1_100)}";`;
92 const file = makeRenderableFile({
93 path: "apps/mobile/src/example-long-line.txt",
94 additionLines: [longLine],
95 rows: [
32
33 const highlighted = await highlightSourceFile({
34 path: "apps/mobile/src/example-long-line.ts",
35 contents: longLine,
36 theme: "light",
37 });
38
39 expect(highlighted).toEqual([
40 [
9641 {
97 kind: "line",
98 id: "add-1",
99 change: "add",
100 oldLineNumber: null,
101 newLineNumber: 1,
10242 content: longLine,
103 additionTokenIndex: 0,
104 deletionTokenIndex: null,
105 comparison: null,
43 color: null,
44 fontStyle: null,
10645 },
10746 ],
108 });
109
110 const highlighted = await highlightReviewFile(file, "light");
111
112 expect(highlighted.additionLines).toHaveLength(1);
113 expect(highlighted.additionLines[0]).toEqual([
114 {
115 content: longLine,
116 color: null,
117 fontStyle: null,
118 },
11947 ]);
12048 });
121});
12249
123describe("highlightCodeSnippet", () => {
124 it("resolves language aliases and returns syntax-colored tokens", async () => {
50 it("initializes source and snippet highlighting without a warmup", async () => {
51 vi.resetModules();
52 const highlighter = await import("./shikiReviewHighlighter");
12553 const source = "const answer: number = 42;";
126 const highlighted = await highlightCodeSnippet({
127 code: source,
128 language: "ts",
54
55 const highlighted = await highlighter.highlightSourceFile({
56 path: "example.ts",
57 contents: source,
12958 theme: "dark",
13059 });
13160
......@@ -136,18 +65,56 @@ describe("highlightCodeSnippet", () => {
13665 .join(""),
13766 ).toBe(source);
13867 expect(highlighted.flat().some((token) => token.color !== null)).toBe(true);
68 expect(
69 await highlighter.highlightCodeSnippet({ code: source, language: "ts", theme: "dark" }),
70 ).toEqual(highlighted);
13971 });
14072});
14173
142describe("highlightSourceFile", () => {
143 it("initializes source and snippet highlighting without a warmup", async () => {
144 vi.resetModules();
145 const highlighter = await import("./shikiReviewHighlighter");
146 const source = "const answer: number = 42;";
74describe("highlightReviewSelectedLines", () => {
75 it("adds word-alt diff emphasis for paired deletion and addition lines", async () => {
76 const lines: ReviewRenderableLineRow[] = [
77 {
78 kind: "line",
79 id: "delete-1",
80 change: "delete",
81 oldLineNumber: 1,
82 newLineNumber: null,
83 content: "const before = 1;",
84 additionTokenIndex: null,
85 deletionTokenIndex: 0,
86 comparison: { change: "add", tokenIndex: 0 },
87 },
88 {
89 kind: "line",
90 id: "add-1",
91 change: "add",
92 oldLineNumber: null,
93 newLineNumber: 1,
94 content: "const after = 2;",
95 additionTokenIndex: 0,
96 deletionTokenIndex: null,
97 comparison: { change: "delete", tokenIndex: 0 },
98 },
99 ];
147100
148 const highlighted = await highlighter.highlightSourceFile({
149 path: "example.ts",
150 contents: source,
101 const highlighted = await highlightReviewSelectedLines({
102 filePath: "apps/mobile/src/example-inline-diff.txt",
103 lines,
104 theme: "light",
105 });
106
107 expect(highlighted["delete-1"]?.some((token) => token.diffHighlight === true)).toBe(true);
108 expect(highlighted["add-1"]?.some((token) => token.diffHighlight === true)).toBe(true);
109 });
110});
111
112describe("highlightCodeSnippet", () => {
113 it("resolves language aliases and returns syntax-colored tokens", async () => {
114 const source = "const answer: number = 42;";
115 const highlighted = await highlightCodeSnippet({
116 code: source,
117 language: "ts",
151118 theme: "dark",
152119 });
153120
......@@ -158,8 +125,5 @@ describe("highlightSourceFile", () => {
158125 .join(""),
159126 ).toBe(source);
160127 expect(highlighted.flat().some((token) => token.color !== null)).toBe(true);
161 expect(
162 await highlighter.highlightCodeSnippet({ code: source, language: "ts", theme: "dark" }),
163 ).toEqual(highlighted);
164128 });
165129});
apps/mobile/src/features/review/shikiReviewHighlighter.ts+1-407
......@@ -17,7 +17,7 @@ import {
1717 resolveReviewHighlighterEnginePreference,
1818 type ReviewHighlighterEngine,
1919} from "./reviewHighlighterEngine";
20import type { ReviewRenderableFile, ReviewRenderableLineRow } from "./reviewModel";
20import type { ReviewRenderableLineRow } from "./reviewModel";
2121import { applyDiffRangesToTokens, computeWordAltDiffRanges } from "./reviewWordDiffs";
2222
2323export type ReviewDiffTheme = "light" | "dark";
......@@ -43,17 +43,6 @@ export interface ReviewHighlightedToken {
4343 readonly diffHighlight?: boolean;
4444}
4545
46export interface ReviewHighlightedFile {
47 readonly additionLines: ReadonlyArray<ReadonlyArray<ReviewHighlightedToken>>;
48 readonly deletionLines: ReadonlyArray<ReadonlyArray<ReviewHighlightedToken>>;
49}
50
51export interface ReviewHighlightFileProgress {
52 readonly highlightedFile: ReviewHighlightedFile;
53 readonly complete: boolean;
54 readonly highlightedLineCount: number;
55}
56
5746const SHIKI_THEME_NAME_BY_SCHEME = {
5847 light: "github-light-default",
5948 dark: "github-dark-default",
......@@ -64,16 +53,9 @@ const REVIEW_HIGHLIGHTER_ENGINE_ENV_VALUE =
6453const REVIEW_HIGHLIGHTER_ENGINE_PREFERENCE = resolveReviewHighlighterEnginePreference(
6554 REVIEW_HIGHLIGHTER_ENGINE_ENV_VALUE,
6655);
67const REVIEW_HIGHLIGHTER_DISABLE_RESULT_CACHE = resolveReviewHighlighterBooleanFlag(
68 process.env.EXPO_PUBLIC_REVIEW_HIGHLIGHTER_DISABLE_CACHE,
69 false,
70);
71const REVIEW_HIGHLIGHT_RESULT_CACHE_LIMIT = 8;
7256const REVIEW_HIGHLIGHT_CHUNK_LINE_THRESHOLD = 8;
7357const REVIEW_HIGHLIGHT_CHUNK_SIZE = 200;
7458const REVIEW_TOKENIZE_MAX_LINE_LENGTH = 1_000;
75const highlightCache = new Map<string, Promise<ReviewHighlightedFile>>();
76const resolvedHighlightCache = new Map<string, ReviewHighlightedFile>();
7759const REVIEW_INITIAL_LANGUAGE_MODULES = [
7860 bashLanguage,
7961 javascriptLanguage,
......@@ -204,22 +186,6 @@ type LoadedLanguageModule = {
204186 default: Parameters<HighlighterCore["loadLanguage"]>[0];
205187};
206188
207function resolveReviewHighlighterBooleanFlag(
208 value: string | undefined,
209 defaultValue: boolean,
210): boolean {
211 switch (value) {
212 case "1":
213 case "true":
214 return true;
215 case "0":
216 case "false":
217 return false;
218 default:
219 return defaultValue;
220 }
221}
222
223189function isReviewHighlighterDebugLoggingEnabled(): boolean {
224190 return typeof __DEV__ !== "undefined" ? __DEV__ : false;
225191}
......@@ -267,7 +233,6 @@ async function getHighlighter(): Promise<HighlighterCore> {
267233 logReviewHighlighterDiagnostic("initializing", {
268234 configuredPreference: REVIEW_HIGHLIGHTER_ENGINE_ENV_VALUE,
269235 preference: REVIEW_HIGHLIGHTER_ENGINE_PREFERENCE,
270 resultCacheDisabled: REVIEW_HIGHLIGHTER_DISABLE_RESULT_CACHE,
271236 });
272237
273238 const themes = [githubLightDefault, githubDarkDefault];
......@@ -488,13 +453,6 @@ async function resolveLanguageFromPath(
488453 return candidate;
489454}
490455
491async function resolveLanguage(file: ReviewRenderableFile): Promise<string> {
492 return (
493 resolveLoadedLanguageFromPath(file.path, file.languageHint) ??
494 (await resolveLanguageFromPath(file.path, file.languageHint))
495 );
496}
497
498456function normalizeHighlightedLines(
499457 tokenLines: ReadonlyArray<ReadonlyArray<{ content: string; color?: string; fontStyle?: number }>>,
500458): ReadonlyArray<ReadonlyArray<ReviewHighlightedToken>> {
......@@ -507,84 +465,6 @@ function normalizeHighlightedLines(
507465 );
508466}
509467
510function makePlainHighlightedLines(
511 lines: ReadonlyArray<string>,
512): ReadonlyArray<ReadonlyArray<ReviewHighlightedToken>> {
513 return lines.map((line) => [
514 {
515 content: stripTrailingNewline(line),
516 color: null,
517 fontStyle: null,
518 },
519 ]);
520}
521
522function applyWordAltDiffHighlightsToFile(
523 file: ReviewRenderableFile,
524 highlighted: ReviewHighlightedFile,
525): ReviewHighlightedFile {
526 const nextAdditionLines = [...highlighted.additionLines];
527 const nextDeletionLines = [...highlighted.deletionLines];
528 const processedPairs = new Set<string>();
529 let changed = false;
530
531 file.rows.forEach((row) => {
532 if (row.kind !== "line" || row.change === "context" || !row.comparison) {
533 return;
534 }
535
536 const deletionTokenIndex =
537 row.change === "delete"
538 ? row.deletionTokenIndex
539 : row.comparison.change === "delete"
540 ? row.comparison.tokenIndex
541 : null;
542 const additionTokenIndex =
543 row.change === "add"
544 ? row.additionTokenIndex
545 : row.comparison.change === "add"
546 ? row.comparison.tokenIndex
547 : null;
548
549 if (deletionTokenIndex === null || additionTokenIndex === null) {
550 return;
551 }
552
553 const pairKey = `${deletionTokenIndex}:${additionTokenIndex}`;
554 if (processedPairs.has(pairKey)) {
555 return;
556 }
557 processedPairs.add(pairKey);
558
559 const deletionLine = stripTrailingNewline(file.deletionLines[deletionTokenIndex] ?? "");
560 const additionLine = stripTrailingNewline(file.additionLines[additionTokenIndex] ?? "");
561 const ranges = computeWordAltDiffRanges({ deletionLine, additionLine });
562
563 if (ranges.deletion.length > 0) {
564 nextDeletionLines[deletionTokenIndex] = applyDiffRangesToTokens(
565 nextDeletionLines[deletionTokenIndex] ?? [],
566 ranges.deletion,
567 );
568 changed = true;
569 }
570
571 if (ranges.addition.length > 0) {
572 nextAdditionLines[additionTokenIndex] = applyDiffRangesToTokens(
573 nextAdditionLines[additionTokenIndex] ?? [],
574 ranges.addition,
575 );
576 changed = true;
577 }
578 });
579
580 return changed
581 ? {
582 additionLines: nextAdditionLines,
583 deletionLines: nextDeletionLines,
584 }
585 : highlighted;
586}
587
588468function applyWordAltDiffHighlightsToSelectedLines(input: {
589469 readonly lines: ReadonlyArray<ReviewRenderableLineRow>;
590470 readonly tokenMap: Record<string, ReadonlyArray<ReviewHighlightedToken>>;
......@@ -733,292 +613,6 @@ export async function highlightSourceFile(input: {
733613 return highlightLines(input.contents, language, SHIKI_THEME_NAME_BY_SCHEME[input.theme]);
734614}
735615
736async function highlightPatchLinesInChunks(input: {
737 readonly lines: ReadonlyArray<string>;
738 readonly language: string;
739 readonly theme: string;
740 readonly onChunk: (
741 startIndex: number,
742 tokens: ReadonlyArray<ReadonlyArray<ReviewHighlightedToken>>,
743 ) => void;
744}): Promise<ReadonlyArray<ReadonlyArray<ReviewHighlightedToken>>> {
745 if (input.lines.length === 0) {
746 return [];
747 }
748
749 const highlighter = await getHighlighter();
750 const highlightedLines: Array<ReadonlyArray<ReviewHighlightedToken>> = [];
751
752 for (
753 let startIndex = 0;
754 startIndex < input.lines.length;
755 startIndex += REVIEW_HIGHLIGHT_CHUNK_SIZE
756 ) {
757 const lineChunk = input.lines.slice(startIndex, startIndex + REVIEW_HIGHLIGHT_CHUNK_SIZE);
758 const chunkTokens: Array<ReadonlyArray<ReviewHighlightedToken>> = [];
759 const tokenizableLines: string[] = [];
760 const tokenizableIndexes: number[] = [];
761
762 lineChunk.forEach((line, index) => {
763 const strippedLine = stripTrailingNewline(line);
764 if (strippedLine.length > REVIEW_TOKENIZE_MAX_LINE_LENGTH) {
765 chunkTokens[index] = [{ content: strippedLine, color: null, fontStyle: null }];
766 return;
767 }
768
769 tokenizableIndexes.push(index);
770 tokenizableLines.push(strippedLine);
771 });
772
773 if (tokenizableLines.length > 0) {
774 const tokenLines = highlighter.codeToTokensBase(tokenizableLines.join("\n"), {
775 lang: input.language,
776 theme: input.theme,
777 });
778 const normalizedTokenLines = normalizeHighlightedLines(tokenLines);
779
780 tokenizableIndexes.forEach((chunkIndex, tokenIndex) => {
781 chunkTokens[chunkIndex] = normalizedTokenLines[tokenIndex] ?? [];
782 });
783 }
784
785 const completedChunk = lineChunk.map((_, index) => chunkTokens[index] ?? []);
786 highlightedLines.push(...completedChunk);
787 input.onChunk(startIndex, completedChunk);
788
789 if (startIndex + REVIEW_HIGHLIGHT_CHUNK_SIZE < input.lines.length) {
790 await waitForNextFrame();
791 }
792 }
793
794 return highlightedLines;
795}
796
797function getHighlightCacheKey(file: ReviewRenderableFile, theme: ReviewDiffTheme): string {
798 return `${SHIKI_THEME_NAME_BY_SCHEME[theme]}:${file.cacheKey}`;
799}
800
801function storeResolvedHighlightedFile(cacheKey: string, highlighted: ReviewHighlightedFile): void {
802 if (resolvedHighlightCache.has(cacheKey)) {
803 resolvedHighlightCache.delete(cacheKey);
804 }
805
806 resolvedHighlightCache.set(cacheKey, highlighted);
807
808 while (resolvedHighlightCache.size > REVIEW_HIGHLIGHT_RESULT_CACHE_LIMIT) {
809 const oldestKey = resolvedHighlightCache.keys().next().value;
810 if (oldestKey === undefined) {
811 break;
812 }
813 resolvedHighlightCache.delete(oldestKey);
814 }
815}
816
817export async function highlightReviewFile(
818 file: ReviewRenderableFile,
819 theme: ReviewDiffTheme,
820): Promise<ReviewHighlightedFile> {
821 const shikiTheme = SHIKI_THEME_NAME_BY_SCHEME[theme];
822 const cacheKey = getHighlightCacheKey(file, theme);
823 if (!REVIEW_HIGHLIGHTER_DISABLE_RESULT_CACHE) {
824 const resolved = resolvedHighlightCache.get(cacheKey);
825 if (resolved) {
826 logReviewHighlighterDiagnostic("file highlight cache hit (resolved)", {
827 fileId: file.id,
828 filePath: file.path,
829 theme,
830 });
831 return resolved;
832 }
833 const cached = highlightCache.get(cacheKey);
834 if (cached) {
835 logReviewHighlighterDiagnostic("file highlight cache hit (pending)", {
836 fileId: file.id,
837 filePath: file.path,
838 theme,
839 });
840 return cached;
841 }
842 }
843
844 const promise = (async () => {
845 const startedAt = Date.now();
846 logReviewHighlighterDiagnostic("file highlight start", {
847 fileId: file.id,
848 filePath: file.path,
849 theme,
850 additionLineCount: file.additionLines.length,
851 deletionLineCount: file.deletionLines.length,
852 rowCount: file.rows.length,
853 });
854 const loadedLanguage = resolveLoadedLanguageFromPath(file.path, file.languageHint);
855 const language = loadedLanguage ?? (await resolveLanguage(file));
856 if (language === "text") {
857 const highlighted = applyWordAltDiffHighlightsToFile(file, {
858 additionLines: makePlainHighlightedLines(file.additionLines),
859 deletionLines: makePlainHighlightedLines(file.deletionLines),
860 });
861 if (!REVIEW_HIGHLIGHTER_DISABLE_RESULT_CACHE) {
862 storeResolvedHighlightedFile(cacheKey, highlighted);
863 }
864 logReviewHighlighterDiagnostic("file highlight complete", {
865 fileId: file.id,
866 filePath: file.path,
867 theme,
868 language,
869 highlightedAdditionLineCount: highlighted.additionLines.length,
870 highlightedDeletionLineCount: highlighted.deletionLines.length,
871 durationMs: Date.now() - startedAt,
872 });
873 return highlighted;
874 }
875
876 const additionLines = await highlightLines(
877 joinPatchLines(file.additionLines),
878 language,
879 shikiTheme,
880 );
881 await waitForNextFrame();
882 const deletionLines = await highlightLines(
883 joinPatchLines(file.deletionLines),
884 language,
885 shikiTheme,
886 );
887 await waitForNextFrame();
888
889 const highlighted = applyWordAltDiffHighlightsToFile(file, { additionLines, deletionLines });
890 if (!REVIEW_HIGHLIGHTER_DISABLE_RESULT_CACHE) {
891 storeResolvedHighlightedFile(cacheKey, highlighted);
892 }
893 logReviewHighlighterDiagnostic("file highlight complete", {
894 fileId: file.id,
895 filePath: file.path,
896 theme,
897 language,
898 highlightedAdditionLineCount: highlighted.additionLines.length,
899 highlightedDeletionLineCount: highlighted.deletionLines.length,
900 durationMs: Date.now() - startedAt,
901 });
902 return highlighted;
903 })();
904
905 if (!REVIEW_HIGHLIGHTER_DISABLE_RESULT_CACHE) {
906 highlightCache.set(cacheKey, promise);
907 }
908 return promise.finally(() => {
909 if (!REVIEW_HIGHLIGHTER_DISABLE_RESULT_CACHE) {
910 highlightCache.delete(cacheKey);
911 }
912 });
913}
914
915export async function streamHighlightReviewFile(
916 file: ReviewRenderableFile,
917 theme: ReviewDiffTheme,
918 onProgress: (progress: ReviewHighlightFileProgress) => void,
919): Promise<ReviewHighlightedFile> {
920 const shikiTheme = SHIKI_THEME_NAME_BY_SCHEME[theme];
921 const cacheKey = getHighlightCacheKey(file, theme);
922 if (!REVIEW_HIGHLIGHTER_DISABLE_RESULT_CACHE) {
923 const resolved = resolvedHighlightCache.get(cacheKey);
924 if (resolved) {
925 onProgress({
926 highlightedFile: resolved,
927 complete: true,
928 highlightedLineCount: resolved.additionLines.length + resolved.deletionLines.length,
929 });
930 return resolved;
931 }
932 }
933
934 const startedAt = Date.now();
935 logReviewHighlighterDiagnostic("file stream highlight start", {
936 fileId: file.id,
937 filePath: file.path,
938 theme,
939 additionLineCount: file.additionLines.length,
940 deletionLineCount: file.deletionLines.length,
941 rowCount: file.rows.length,
942 });
943
944 const loadedLanguage = resolveLoadedLanguageFromPath(file.path, file.languageHint);
945 const language = loadedLanguage ?? (await resolveLanguage(file));
946 if (language === "text") {
947 const highlighted = applyWordAltDiffHighlightsToFile(file, {
948 additionLines: makePlainHighlightedLines(file.additionLines),
949 deletionLines: makePlainHighlightedLines(file.deletionLines),
950 });
951 if (!REVIEW_HIGHLIGHTER_DISABLE_RESULT_CACHE) {
952 storeResolvedHighlightedFile(cacheKey, highlighted);
953 }
954 onProgress({
955 highlightedFile: highlighted,
956 complete: true,
957 highlightedLineCount: highlighted.additionLines.length + highlighted.deletionLines.length,
958 });
959 logReviewHighlighterDiagnostic("file stream highlight complete", {
960 fileId: file.id,
961 filePath: file.path,
962 theme,
963 language,
964 highlightedAdditionLineCount: highlighted.additionLines.length,
965 highlightedDeletionLineCount: highlighted.deletionLines.length,
966 highlightedLineCount: highlighted.additionLines.length + highlighted.deletionLines.length,
967 durationMs: Date.now() - startedAt,
968 });
969 return highlighted;
970 }
971
972 const additionLines: Array<ReadonlyArray<ReviewHighlightedToken>> = [];
973 const deletionLines: Array<ReadonlyArray<ReviewHighlightedToken>> = [];
974 let highlightedLineCount = 0;
975
976 await highlightPatchLinesInChunks({
977 lines: file.additionLines,
978 language,
979 theme: shikiTheme,
980 onChunk: (startIndex, tokens) => {
981 tokens.forEach((lineTokens, index) => {
982 additionLines[startIndex + index] = lineTokens;
983 });
984 highlightedLineCount += tokens.length;
985 },
986 });
987 await waitForNextFrame();
988 await highlightPatchLinesInChunks({
989 lines: file.deletionLines,
990 language,
991 theme: shikiTheme,
992 onChunk: (startIndex, tokens) => {
993 tokens.forEach((lineTokens, index) => {
994 deletionLines[startIndex + index] = lineTokens;
995 });
996 highlightedLineCount += tokens.length;
997 },
998 });
999
1000 const highlighted = applyWordAltDiffHighlightsToFile(file, { additionLines, deletionLines });
1001 if (!REVIEW_HIGHLIGHTER_DISABLE_RESULT_CACHE) {
1002 storeResolvedHighlightedFile(cacheKey, highlighted);
1003 }
1004 onProgress({
1005 highlightedFile: highlighted,
1006 complete: true,
1007 highlightedLineCount,
1008 });
1009 logReviewHighlighterDiagnostic("file stream highlight complete", {
1010 fileId: file.id,
1011 filePath: file.path,
1012 theme,
1013 language,
1014 highlightedAdditionLineCount: highlighted.additionLines.length,
1015 highlightedDeletionLineCount: highlighted.deletionLines.length,
1016 highlightedLineCount,
1017 durationMs: Date.now() - startedAt,
1018 });
1019 return highlighted;
1020}
1021
1022616export async function highlightReviewSelectedLines(input: {
1023617 readonly filePath: string;
1024618 readonly lines: ReadonlyArray<ReviewRenderableLineRow>;