import { ArrowsPointingOutIcon } from "@heroicons/react/20/solid"; import { Clipboard, ClipboardCheck } from "lucide-react"; import type { Language, PrismTheme } from "prism-react-renderer"; import { Highlight, Prism } from "prism-react-renderer"; import type { ReactNode } from "react"; import { forwardRef, useCallback, useEffect, useState } from "react"; import { TextWrapIcon } from "~/assets/icons/TextWrapIcon"; import { cn } from "~/utils/cn"; import { highlightSearchText } from "~/utils/logUtils"; import { Button } from "../primitives/Buttons"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../primitives/Dialog"; import { Paragraph } from "../primitives/Paragraph"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../primitives/Tooltip"; import { TextInlineIcon } from "~/assets/icons/TextInlineIcon"; //This is a fork of https://github.com/mantinedev/mantine/blob/master/src/mantine-prism/src/Prism/Prism.tsx //it didn't support highlighting lines by dimming the rest of the code, or animations on the highlighting async function setup() { (typeof global !== "undefined" ? global : window).Prism = Prism; //@ts-ignore await import("prismjs/components/prism-json"); //@ts-ignore await import("prismjs/components/prism-typescript"); //@ts-ignore await import("prismjs/components/prism-sql.js"); } setup(); type CodeBlockProps = { /** Code which will be highlighted */ code: string; /** Programming language that should be highlighted */ language?: Language; /** Show copy to clipboard button */ showCopyButton?: boolean; /** Show text wrapping button */ showTextWrapping?: boolean; /** Display line numbers */ showLineNumbers?: boolean; /** Highlight line at given line number with color from theme.colors */ highlightedRanges?: [number, number][]; /** Add/override classes on the overall element */ className?: string; /** Add/override code theme */ theme?: PrismTheme; /** Max lines */ maxLines?: number; /** Whether to show the chrome, if you provide a string it will be used as the title, */ showChrome?: boolean; /** filename */ fileName?: string; /** title text for the Title row */ rowTitle?: ReactNode; /** Whether to show the open in modal button */ showOpenInModal?: boolean; /** Search term to highlight in the code */ searchTerm?: string; /** Whether to wrap the code */ wrap?: boolean; }; const dimAmount = 0.5; const extraLinesWhenClipping = 0.35; const defaultTheme: PrismTheme = { plain: { color: "var(--color-code-constant)", backgroundColor: "rgba(0, 0, 0, 0)", }, styles: [ { types: ["comment", "prolog", "doctype", "cdata"], style: { color: "var(--color-code-muted)", }, }, { types: ["punctuation"], style: { color: "var(--color-code-foreground)", }, }, { types: ["property", "tag", "constant", "symbol", "deleted"], style: { color: "var(--color-code-language)", }, }, { types: ["boolean", "number"], style: { color: "var(--color-code-builtin)", }, }, { types: ["selector", "attr-name", "string", "char", "builtin", "inserted"], style: { color: "var(--color-code-string)", }, }, { types: ["operator", "entity", "url"], style: { color: "var(--color-code-plain)", }, }, { types: ["variable"], style: { color: "var(--color-code-variable)", }, }, { types: ["atrule", "attr-value", "keyword"], style: { color: "var(--color-code-keyword)", }, }, { types: ["function", "class-name"], style: { color: "var(--color-code-function)", }, }, { types: ["regex"], style: { color: "var(--color-code-regexp)", }, }, { types: ["important", "bold"], style: { fontWeight: "bold", }, }, { types: ["italic"], style: { fontStyle: "italic", }, }, { types: ["namespace"], style: { opacity: 0.7, }, }, { types: ["deleted"], style: { color: "var(--color-code-deleted)", }, }, { types: ["char"], style: { color: "var(--color-code-number)", }, }, { types: ["tag"], style: { color: "var(--color-code-escape)", }, }, { types: ["keyword.operator"], style: { color: "var(--color-code-storage)", }, }, { types: ["meta.template.expression"], style: { color: "var(--color-code-plain)", }, }, ], }; export const CodeBlock = forwardRef( ( { showCopyButton = true, showTextWrapping = false, showLineNumbers = true, showOpenInModal = true, highlightedRanges, code, className, language = "typescript", theme = defaultTheme, maxLines, showChrome = false, fileName, rowTitle, searchTerm, wrap = false, ...props }: CodeBlockProps, ref ) => { const [mouseOver, setMouseOver] = useState(false); const [copied, setCopied] = useState(false); const [modalCopied, setModalCopied] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false); const [isWrapped, setIsWrapped] = useState(wrap); const onCopied = useCallback( (event: React.MouseEvent) => { event.preventDefault(); event.stopPropagation(); navigator.clipboard.writeText(code); setCopied(true); setTimeout(() => { setCopied(false); }, 1500); }, [code] ); const onModalCopied = useCallback( (event: React.MouseEvent) => { event.preventDefault(); event.stopPropagation(); navigator.clipboard.writeText(code); setModalCopied(true); setTimeout(() => { setModalCopied(false); }, 1500); }, [code] ); code = code?.trim() ?? ""; const lineCount = code.split("\n").length; const maxLineWidth = lineCount.toString().length; let maxHeight: string | undefined = undefined; if (maxLines && lineCount > maxLines) { maxHeight = `calc(${(maxLines + extraLinesWhenClipping) * 0.75 * 1.625}rem + 1.5rem )`; } const highlightLines = highlightedRanges?.flatMap(([start, end]) => Array.from({ length: end - start + 1 }, (_, i) => start + i) ); // if there are more than 1000 lines, don't highlight const shouldHighlight = lineCount <= 1000; return ( <>
{showChrome && } {rowTitle && }
{showTextWrapping && ( setIsWrapped(!isWrapped)} className="transition-colors focus-custom hover:cursor-pointer hover:text-text-bright" > {isWrapped ? ( ) : ( )} {isWrapped ? "Unwrap" : "Wrap"} )} {showCopyButton && ( setMouseOver(true)} onMouseLeave={() => setMouseOver(false)} className={cn( "transition-colors duration-100 focus-custom hover:cursor-pointer", copied ? "text-success" : "text-text-dimmed hover:text-text-bright" )} > {copied ? ( ) : ( )} {copied ? "Copied" : "Copy"} )} {showOpenInModal && ( setIsModalOpen(true)}> Expand )}
{shouldHighlight ? ( ) : (
                {highlightSearchText(code, searchTerm)}
              
)}
{fileName && fileName} {rowTitle && rowTitle} {shouldHighlight ? ( ) : (
                  {highlightSearchText(code, searchTerm)}
                
)}
); } ); CodeBlock.displayName = "CodeBlock"; function Chrome({ title }: { title?: string }) { return (
{title}
); } export function TitleRow({ title }: { title: ReactNode }) { return (
{title}
); } type HighlightCodeProps = { theme: PrismTheme; code: string; language: Language; showLineNumbers: boolean; highlightLines?: number[]; maxLineWidth?: number; className?: string; preClassName?: string; isWrapped: boolean; searchTerm?: string; }; function HighlightCode({ theme, code, language, showLineNumbers, highlightLines, maxLineWidth, className, preClassName, isWrapped, searchTerm, }: HighlightCodeProps) { const [isLoaded, setIsLoaded] = useState(false); useEffect(() => { Promise.all([ //@ts-ignore import("prismjs/components/prism-json"), //@ts-ignore import("prismjs/components/prism-typescript"), //@ts-ignore import("prismjs/components/prism-sql.js"), ]).then(() => setIsLoaded(true)); }, []); const containerClasses = cn( "min-h-0 flex-1 px-3 py-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control", !isWrapped && "overflow-auto", isWrapped && "overflow-auto", className ); const preClasses = cn( "relative mr-2 font-mono leading-relaxed", preClassName, isWrapped && "[&_span]:whitespace-pre-wrap [&_span]:wrap-break-word" ); if (!isLoaded) { return (
{code}
); } return ( {({ className: inheritedClassName, style: inheritedStyle, tokens, getLineProps, getTokenProps, }) => (
            {tokens
              .map((line, index) => {
                if (index === tokens.length - 1 && line.length === 1 && line[0].content === "\n") {
                  return null;
                }

                const lineNumber = index + 1;
                const lineProps = getLineProps({ line });

                let hasAnyHighlights = highlightLines ? highlightLines.length > 0 : false;

                let shouldDim = hasAnyHighlights;
                if (hasAnyHighlights && highlightLines?.includes(lineNumber)) {
                  shouldDim = false;
                }

                return (
                  
{showLineNumbers && (
{lineNumber}
)}
{line.map((token, key) => { const tokenProps = getTokenProps({ token }); // Highlight search term matches in token const content = highlightSearchText(token.content, searchTerm); return ( {content} ); })}
); }) .filter(Boolean)}
)}
); }