| 1 | import type { ReviewHighlightedToken } from "../review/shikiReviewHighlighter"; |
| 2 | import { buildNativeSourceRows } from "./nativeSourceFileAdapter"; |
| 3 | |
| 4 | const MAX_CACHED_DOCUMENTS = 8; |
| 5 | const MAX_CACHED_CHARACTERS = 4 * 1024 * 1024; |
| 6 | |
| 7 | export interface SourceFileDocument { |
| 8 | readonly contents: string; |
| 9 | readonly lines: ReadonlyArray<string>; |
| 10 | readonly rowsJson: string; |
| 11 | } |
| 12 | |
| 13 | const documentCache = new Map<string, SourceFileDocument>(); |
| 14 | let cachedCharacterCount = 0; |
| 15 | |
| 16 | function 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 | |
| 26 | export 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. |
| 59 | export 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 | } |