1import type { ReviewHighlightedToken } from "../review/shikiReviewHighlighter";
2import { buildNativeSourceRows } from "./nativeSourceFileAdapter";
3
4const MAX_CACHED_DOCUMENTS = 8;
5const MAX_CACHED_CHARACTERS = 4 * 1024 * 1024;
6
7export interface SourceFileDocument {
8 readonly contents: string;
9 readonly lines: ReadonlyArray<string>;
10 readonly rowsJson: string;
11}
12
13const documentCache = new Map<string, SourceFileDocument>();
14let cachedCharacterCount = 0;
15
16function removeOldestCachedDocument(): void {
17 const oldestKey = documentCache.keys().next().value;
18 if (typeof oldestKey !== "string") {
19 return;
20 }
21 const document = documentCache.get(oldestKey);
22 documentCache.delete(oldestKey);
23 cachedCharacterCount -= (document?.contents.length ?? 0) + (document?.rowsJson.length ?? 0);
24}
25
26export function prepareSourceFileDocument(contents: string): SourceFileDocument {
27 const cached = documentCache.get(contents);
28 if (cached !== undefined) {
29 documentCache.delete(contents);
30 documentCache.set(contents, cached);
31 return cached;
32 }
33
34 const normalizedContents = contents.replace(/\r\n?/g, "\n");
35 const lines = normalizedContents.split("\n");
36 const document = {
37 contents: normalizedContents,
38 lines,
39 rowsJson: JSON.stringify(buildNativeSourceRows(lines)),
40 } satisfies SourceFileDocument;
41 const characterCount = document.contents.length + document.rowsJson.length;
42
43 if (characterCount <= MAX_CACHED_CHARACTERS) {
44 while (
45 documentCache.size >= MAX_CACHED_DOCUMENTS ||
46 cachedCharacterCount + characterCount > MAX_CACHED_CHARACTERS
47 ) {
48 removeOldestCachedDocument();
49 }
50 documentCache.set(contents, document);
51 cachedCharacterCount += characterCount;
52 }
53
54 return document;
55}
56
57// A selectable document cannot virtualize its rows. Cap React spans instead; large
58// attachments remain fully selectable as one plain string, including every newline.
59export function boundedSelectableSourceTokens(
60 tokens: ReadonlyArray<ReadonlyArray<ReviewHighlightedToken>> | null,
61): typeof tokens {
62 if (!tokens) return null;
63 let spans = tokens.length;
64 for (const line of tokens) {
65 spans += line.length;
66 if (spans > 2_000) return null;
67 }
68 return tokens;
69}