|
1 | 1 | import { PanelMessage } from "@components/ui/PanelMessage"; |
| 2 | +import { Tooltip } from "@components/ui/Tooltip"; |
2 | 3 | import { CodeMirrorEditor } from "@features/code-editor/components/CodeMirrorEditor"; |
| 4 | +import { useMarkdownViewerStore } from "@features/code-editor/stores/markdownViewerStore"; |
| 5 | +import { isMarkdownFile } from "@features/code-editor/utils/markdownUtils"; |
3 | 6 | import { getRelativePath } from "@features/code-editor/utils/pathUtils"; |
4 | 7 | import { isImageFile } from "@features/message-editor/utils/imageUtils"; |
| 8 | +import { usePanelLayoutStore } from "@features/panels"; |
| 9 | +import { useFileTreeStore } from "@features/right-sidebar/stores/fileTreeStore"; |
5 | 10 | import { useCwd } from "@features/sidebar/hooks/useCwd"; |
6 | | -import { Box, Flex } from "@radix-ui/themes"; |
7 | | -import { useTRPC } from "@renderer/trpc/client"; |
| 11 | +import { Code, Eye } from "@phosphor-icons/react"; |
| 12 | +import { Box, Flex, IconButton, Text } from "@radix-ui/themes"; |
| 13 | +import { trpcClient, useTRPC } from "@renderer/trpc/client"; |
8 | 14 | import type { Task } from "@shared/types"; |
9 | 15 |
|
10 | 16 | import { useQuery } from "@tanstack/react-query"; |
| 17 | +import { useCallback, useMemo } from "react"; |
| 18 | +import type { Components } from "react-markdown"; |
| 19 | +import ReactMarkdown from "react-markdown"; |
| 20 | +import remarkGfm from "remark-gfm"; |
11 | 21 |
|
12 | 22 | const IMAGE_MIME_TYPES: Record<string, string> = { |
13 | 23 | png: "image/png", |
@@ -43,6 +53,51 @@ export function CodeEditorPanel({ |
43 | 53 | const isInsideRepo = !!repoPath && absolutePath.startsWith(repoPath); |
44 | 54 | const filePath = getRelativePath(absolutePath, repoPath); |
45 | 55 | const isImage = isImageFile(absolutePath); |
| 56 | + const isMarkdown = isMarkdownFile(absolutePath); |
| 57 | + const preferRendered = useMarkdownViewerStore((s) => s.preferRendered); |
| 58 | + const togglePreferRendered = useMarkdownViewerStore( |
| 59 | + (s) => s.togglePreferRendered, |
| 60 | + ); |
| 61 | + const openFileInSplit = usePanelLayoutStore((s) => s.openFileInSplit); |
| 62 | + const expandToFile = useFileTreeStore((s) => s.expandToFile); |
| 63 | + |
| 64 | + const handleMarkdownLinkClick = useCallback( |
| 65 | + (e: React.MouseEvent<HTMLAnchorElement>, href: string) => { |
| 66 | + e.preventDefault(); |
| 67 | + if (href.startsWith("http://") || href.startsWith("https://")) { |
| 68 | + trpcClient.os.openExternal.mutate({ url: href }); |
| 69 | + return; |
| 70 | + } |
| 71 | + const cleanHref = href.replace(/^\.\//, ""); |
| 72 | + const dir = filePath.includes("/") |
| 73 | + ? filePath.slice(0, filePath.lastIndexOf("/")) |
| 74 | + : ""; |
| 75 | + const resolved = dir ? `${dir}/${cleanHref}` : cleanHref; |
| 76 | + if (repoPath) { |
| 77 | + expandToFile(taskId, `${repoPath}/${resolved}`); |
| 78 | + } |
| 79 | + openFileInSplit(taskId, resolved); |
| 80 | + }, |
| 81 | + [filePath, taskId, repoPath, openFileInSplit, expandToFile], |
| 82 | + ); |
| 83 | + |
| 84 | + const markdownComponents: Components = useMemo( |
| 85 | + () => ({ |
| 86 | + a: ({ href, children }) => ( |
| 87 | + <Tooltip content={href ?? ""}> |
| 88 | + <a |
| 89 | + href={href ?? "#"} |
| 90 | + onClick={(e) => handleMarkdownLinkClick(e, href ?? "")} |
| 91 | + className="cursor-pointer" |
| 92 | + style={{ color: "var(--accent-11)", textDecoration: "underline" }} |
| 93 | + > |
| 94 | + {children} |
| 95 | + </a> |
| 96 | + </Tooltip> |
| 97 | + ), |
| 98 | + }), |
| 99 | + [handleMarkdownLinkClick], |
| 100 | + ); |
46 | 101 |
|
47 | 102 | const repoQuery = useQuery( |
48 | 103 | trpcReact.fs.readRepoFile.queryOptions( |
@@ -112,6 +167,57 @@ export function CodeEditorPanel({ |
112 | 167 | return <PanelMessage>File is empty</PanelMessage>; |
113 | 168 | } |
114 | 169 |
|
| 170 | + if (isMarkdown) { |
| 171 | + return ( |
| 172 | + <Flex direction="column" height="100%" style={{ overflow: "hidden" }}> |
| 173 | + <Flex |
| 174 | + px="3" |
| 175 | + py="2" |
| 176 | + align="center" |
| 177 | + justify="between" |
| 178 | + style={{ borderBottom: "1px solid var(--gray-6)", flexShrink: 0 }} |
| 179 | + > |
| 180 | + <Text |
| 181 | + size="1" |
| 182 | + color="gray" |
| 183 | + style={{ fontFamily: "var(--code-font-family)" }} |
| 184 | + > |
| 185 | + {filePath} |
| 186 | + </Text> |
| 187 | + <Tooltip content={preferRendered ? "View source" : "View rendered"}> |
| 188 | + <IconButton |
| 189 | + size="1" |
| 190 | + variant="ghost" |
| 191 | + color="gray" |
| 192 | + className="cursor-pointer" |
| 193 | + onClick={togglePreferRendered} |
| 194 | + > |
| 195 | + {preferRendered ? <Code size={14} /> : <Eye size={14} />} |
| 196 | + </IconButton> |
| 197 | + </Tooltip> |
| 198 | + </Flex> |
| 199 | + <Box style={{ flex: 1, overflow: "auto" }}> |
| 200 | + {preferRendered ? ( |
| 201 | + <Box className="plan-markdown" p="5" style={{ maxWidth: 750 }}> |
| 202 | + <ReactMarkdown |
| 203 | + remarkPlugins={[remarkGfm]} |
| 204 | + components={markdownComponents} |
| 205 | + > |
| 206 | + {fileContent} |
| 207 | + </ReactMarkdown> |
| 208 | + </Box> |
| 209 | + ) : ( |
| 210 | + <CodeMirrorEditor |
| 211 | + content={fileContent} |
| 212 | + filePath={absolutePath} |
| 213 | + readOnly |
| 214 | + /> |
| 215 | + )} |
| 216 | + </Box> |
| 217 | + </Flex> |
| 218 | + ); |
| 219 | + } |
| 220 | + |
115 | 221 | return ( |
116 | 222 | <Box height="100%" style={{ overflow: "hidden" }}> |
117 | 223 | <CodeMirrorEditor |
|
0 commit comments