1import * as Data from "effect/Data";
2import * as Effect from "effect/Effect";
3import { Atom } from "effect/reactivity";
4
5import {
6 highlightSourceFile,
7 type ReviewDiffTheme,
8 type ReviewHighlightedToken,
9} from "../review/shikiReviewHighlighter";
10
11const SOURCE_HIGHLIGHT_IDLE_TTL_MS = 5 * 60_000;
12
13export interface SourceHighlightInput {
14 readonly path: string;
15 readonly contents: string;
16 readonly theme: ReviewDiffTheme;
17}
18
19export type SourceHighlightTokens = ReadonlyArray<ReadonlyArray<ReviewHighlightedToken>>;
20
21type SourceHighlighter = (input: SourceHighlightInput) => Promise<SourceHighlightTokens>;
22
23class SourceHighlightCacheKey extends Data.Class<SourceHighlightInput> {}
24
25class SourceHighlightError extends Data.TaggedError("SourceHighlightError")<{
26 readonly cause: unknown;
27}> {}
28
29export function createSourceHighlightAtomFamily(options?: {
30 readonly highlight?: SourceHighlighter;
31 readonly idleTtlMs?: number;
32}) {
33 const highlight = options?.highlight ?? highlightSourceFile;
34 const idleTtlMs = options?.idleTtlMs ?? SOURCE_HIGHLIGHT_IDLE_TTL_MS;
35 const family = Atom.family((request: SourceHighlightCacheKey) =>
36 Atom.make(
37 Effect.tryPromise({
38 try: () => highlight(request),
39 catch: (cause) => new SourceHighlightError({ cause }),
40 }),
41 ).pipe(
42 Atom.setIdleTTL(idleTtlMs),
43 Atom.withLabel(`mobile:source-highlight:${request.theme}:${request.path}`),
44 ),
45 );
46
47 return (input: SourceHighlightInput) => family(new SourceHighlightCacheKey(input));
48}
49
50export const sourceHighlightAtom = createSourceHighlightAtomFamily();