1import { useAtomValue } from "@effect/atom-react";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import { AsyncResult, Atom } from "effect/reactivity";
5import { useMemo } from "react";
6
7import {
8 highlightCodeSnippet,
9 type ReviewDiffTheme,
10 type ReviewHighlightedToken,
11} from "../review/shikiReviewHighlighter";
12
13const MARKDOWN_CODE_HIGHLIGHT_IDLE_TTL_MS = 5 * 60_000;
14
15export type MarkdownHighlightedCode = ReadonlyArray<ReadonlyArray<ReviewHighlightedToken>>;
16
17export interface MarkdownCodeHighlightInput {
18 readonly code: string;
19 readonly enabled: boolean;
20 readonly language: string;
21 readonly theme: ReviewDiffTheme;
22}
23
24type MarkdownCodeHighlighter = (
25 input: MarkdownCodeHighlightInput,
26) => Promise<MarkdownHighlightedCode | null>;
27
28class MarkdownCodeHighlightCacheKey extends Data.Class<MarkdownCodeHighlightInput> {}
29
30class MarkdownCodeHighlightError extends Data.TaggedError("MarkdownCodeHighlightError")<{
31 readonly cause: unknown;
32}> {}
33
34export function createMarkdownCodeHighlightAtomFamily(options?: {
35 readonly highlight?: MarkdownCodeHighlighter;
36 readonly idleTtlMs?: number;
37}) {
38 const highlight =
39 options?.highlight ??
40 ((input: MarkdownCodeHighlightInput) =>
41 input.enabled
42 ? highlightCodeSnippet({
43 code: input.code,
44 language: input.language,
45 theme: input.theme,
46 })
47 : Promise.resolve(null));
48 const idleTtlMs = options?.idleTtlMs ?? MARKDOWN_CODE_HIGHLIGHT_IDLE_TTL_MS;
49 const family = Atom.family((request: MarkdownCodeHighlightCacheKey) =>
50 Atom.make(
51 Effect.tryPromise({
52 try: () => highlight(request),
53 catch: (cause) => new MarkdownCodeHighlightError({ cause }),
54 }),
55 ).pipe(
56 Atom.setIdleTTL(idleTtlMs),
57 Atom.withLabel(`mobile:thread-markdown-code-highlight:${request.theme}:${request.language}`),
58 ),
59 );
60
61 return (input: MarkdownCodeHighlightInput) => family(new MarkdownCodeHighlightCacheKey(input));
62}
63
64export const markdownCodeHighlightAtom = createMarkdownCodeHighlightAtomFamily();
65
66export function useMarkdownCodeHighlight(input: {
67 readonly code: string;
68 readonly enabled: boolean;
69 readonly language: string | null | undefined;
70 readonly theme: ReviewDiffTheme;
71}): MarkdownHighlightedCode | null {
72 const normalizedLanguage = input.language?.trim() || "text";
73 const enabled = input.enabled && Boolean(input.language?.trim());
74 const atomLanguage = enabled ? normalizedLanguage : "text";
75 const highlightAtom = useMemo(
76 () =>
77 markdownCodeHighlightAtom({
78 code: enabled ? input.code : "",
79 enabled,
80 language: atomLanguage,
81 theme: input.theme,
82 }),
83 [atomLanguage, enabled, input.code, input.theme],
84 );
85 const result = useAtomValue(highlightAtom);
86 return AsyncResult.isSuccess(result) ? result.value : null;
87}