Skip to content

Commit d0a7f80

Browse files
authored
feat(code): add markdown file rendering (#1276)
- adds markdown rendering to md files with global preference toggle - handles links in md files by opening files in posthog code, or urls in external browser - auto-expands the file tree to reveal the file when clicking a link closes #1198
1 parent ca09532 commit d0a7f80

4 files changed

Lines changed: 156 additions & 2 deletions

File tree

apps/code/src/renderer/features/code-editor/components/CodeEditorPanel.tsx

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
import { PanelMessage } from "@components/ui/PanelMessage";
2+
import { Tooltip } from "@components/ui/Tooltip";
23
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";
36
import { getRelativePath } from "@features/code-editor/utils/pathUtils";
47
import { isImageFile } from "@features/message-editor/utils/imageUtils";
8+
import { usePanelLayoutStore } from "@features/panels";
9+
import { useFileTreeStore } from "@features/right-sidebar/stores/fileTreeStore";
510
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";
814
import type { Task } from "@shared/types";
915

1016
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";
1121

1222
const IMAGE_MIME_TYPES: Record<string, string> = {
1323
png: "image/png",
@@ -43,6 +53,51 @@ export function CodeEditorPanel({
4353
const isInsideRepo = !!repoPath && absolutePath.startsWith(repoPath);
4454
const filePath = getRelativePath(absolutePath, repoPath);
4555
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+
);
46101

47102
const repoQuery = useQuery(
48103
trpcReact.fs.readRepoFile.queryOptions(
@@ -112,6 +167,57 @@ export function CodeEditorPanel({
112167
return <PanelMessage>File is empty</PanelMessage>;
113168
}
114169

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+
115221
return (
116222
<Box height="100%" style={{ overflow: "hidden" }}>
117223
<CodeMirrorEditor
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { create } from "zustand";
2+
import { persist } from "zustand/middleware";
3+
4+
interface MarkdownViewerStoreState {
5+
preferRendered: boolean;
6+
}
7+
8+
interface MarkdownViewerStoreActions {
9+
togglePreferRendered: () => void;
10+
}
11+
12+
type MarkdownViewerStore = MarkdownViewerStoreState &
13+
MarkdownViewerStoreActions;
14+
15+
export const useMarkdownViewerStore = create<MarkdownViewerStore>()(
16+
persist(
17+
(set) => ({
18+
preferRendered: true,
19+
togglePreferRendered: () =>
20+
set((s) => ({ preferRendered: !s.preferRendered })),
21+
}),
22+
{
23+
name: "markdown-viewer-storage",
24+
},
25+
),
26+
);
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
const MARKDOWN_EXTENSIONS = new Set(["md", "markdown"]);
2+
3+
export function isMarkdownFile(filename: string): boolean {
4+
const ext = filename.split(".").pop()?.toLowerCase();
5+
return !!ext && MARKDOWN_EXTENSIONS.has(ext);
6+
}

apps/code/src/renderer/features/right-sidebar/stores/fileTreeStore.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ interface FileTreeStoreState {
77

88
interface FileTreeStoreActions {
99
togglePath: (taskId: string, path: string) => void;
10+
expandToFile: (taskId: string, filePath: string) => void;
1011
collapseAll: (taskId: string) => void;
1112
}
1213

@@ -30,6 +31,21 @@ export const useFileTreeStore = create<FileTreeStore>()((set) => ({
3031
},
3132
};
3233
}),
34+
expandToFile: (taskId, filePath) =>
35+
set((state) => {
36+
const taskPaths = state.expandedPaths[taskId] ?? new Set<string>();
37+
const newPaths = new Set(taskPaths);
38+
const parts = filePath.split("/");
39+
for (let i = 1; i < parts.length; i++) {
40+
newPaths.add(parts.slice(0, i).join("/"));
41+
}
42+
return {
43+
expandedPaths: {
44+
...state.expandedPaths,
45+
[taskId]: newPaths,
46+
},
47+
};
48+
}),
3349
collapseAll: (taskId) =>
3450
set((state) => ({
3551
expandedPaths: {

0 commit comments

Comments
 (0)