From 1885d8602f6b3b13ed54abf25146a141dd6ecafa Mon Sep 17 00:00:00 2001 From: Aaron Florey Date: Thu, 16 Apr 2026 12:33:49 +1000 Subject: [PATCH] feat(app): Render markdown files in file pane (#427) Detect .md and .markdown files in the shared file pane and render them as markdown instead of syntax highlighted code. This makes README-style files easier to read without changing the existing preview flow for other text files or .mdx. Keep the change focused to the app renderer by reusing the existing markdown stack and adding a narrow extension check with targeted coverage. --- .../components/file-pane-render-mode.test.ts | 23 ++++++++++++ .../src/components/file-pane-render-mode.ts | 4 +++ packages/app/src/components/file-pane.tsx | 35 +++++++++++++++++-- 3 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 packages/app/src/components/file-pane-render-mode.test.ts create mode 100644 packages/app/src/components/file-pane-render-mode.ts diff --git a/packages/app/src/components/file-pane-render-mode.test.ts b/packages/app/src/components/file-pane-render-mode.test.ts new file mode 100644 index 000000000..b57b81711 --- /dev/null +++ b/packages/app/src/components/file-pane-render-mode.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from "vitest"; +import { isRenderedMarkdownFile } from "@/components/file-pane-render-mode"; + +describe("isRenderedMarkdownFile", () => { + it("detects .md files", () => { + expect(isRenderedMarkdownFile("README.md")).toBe(true); + expect(isRenderedMarkdownFile("docs/guide.MD")).toBe(true); + }); + + it("detects .markdown files", () => { + expect(isRenderedMarkdownFile("notes.markdown")).toBe(true); + expect(isRenderedMarkdownFile("docs/CHANGELOG.MARKDOWN")).toBe(true); + }); + + it("does not treat .mdx files as rendered markdown", () => { + expect(isRenderedMarkdownFile("page.mdx")).toBe(false); + }); + + it("does not treat other text files as rendered markdown", () => { + expect(isRenderedMarkdownFile("src/index.ts")).toBe(false); + expect(isRenderedMarkdownFile("README.md.txt")).toBe(false); + }); +}); diff --git a/packages/app/src/components/file-pane-render-mode.ts b/packages/app/src/components/file-pane-render-mode.ts new file mode 100644 index 000000000..d6f507f9e --- /dev/null +++ b/packages/app/src/components/file-pane-render-mode.ts @@ -0,0 +1,4 @@ +export function isRenderedMarkdownFile(filePath: string): boolean { + const normalizedPath = filePath.trim().toLowerCase(); + return normalizedPath.endsWith(".md") || normalizedPath.endsWith(".markdown"); +} diff --git a/packages/app/src/components/file-pane.tsx b/packages/app/src/components/file-pane.tsx index e11419e57..7341bf888 100644 --- a/packages/app/src/components/file-pane.tsx +++ b/packages/app/src/components/file-pane.tsx @@ -1,5 +1,6 @@ import React, { useMemo, useRef } from "react"; import { useQuery } from "@tanstack/react-query"; +import Markdown, { MarkdownIt } from "react-native-markdown-display"; import { ActivityIndicator, Image as RNImage, @@ -20,7 +21,9 @@ import { type HighlightStyle, } from "@getpaseo/highlight"; import { lineNumberGutterWidth } from "@/components/code-insets"; +import { isRenderedMarkdownFile } from "@/components/file-pane-render-mode"; import { isWeb } from "@/constants/platform"; +import { createMarkdownStyles } from "@/styles/markdown-styles"; interface CodeLineProps { tokens: HighlightToken[]; @@ -117,6 +120,9 @@ function FilePreviewBody({ const isDark = theme.colorScheme === "dark"; const colorMap = isDark ? darkHighlightColors : lightHighlightColors; const baseColor = isDark ? "#c9d1d9" : "#24292f"; + const markdownStyles = useMemo(() => createMarkdownStyles(theme), [theme]); + const markdownParser = useMemo(() => MarkdownIt({ typographer: true, linkify: true }), []); + const isMarkdownFile = preview?.kind === "text" && isRenderedMarkdownFile(filePath); const previewScrollRef = useRef(null); const scrollbar = useWebScrollViewScrollbar(previewScrollRef, { @@ -124,12 +130,12 @@ function FilePreviewBody({ }); const highlightedLines = useMemo(() => { - if (!preview || preview.kind !== "text") { + if (!preview || preview.kind !== "text" || isMarkdownFile) { return null; } return highlightCode(preview.content ?? "", filePath); - }, [preview?.kind, preview?.content, filePath]); + }, [isMarkdownFile, preview?.kind, preview?.content, filePath]); const gutterWidth = useMemo(() => { if (!highlightedLines) return 0; @@ -154,6 +160,28 @@ function FilePreviewBody({ } if (preview.kind === "text") { + if (isMarkdownFile) { + return ( + + + + {preview.content ?? ""} + + + {scrollbar.overlay} + + ); + } + const lines = highlightedLines ?? [[{ text: preview.content ?? "", style: null }]]; const codeLines = ( @@ -329,6 +357,9 @@ const styles = StyleSheet.create((theme) => ({ previewCodeScrollContent: { padding: theme.spacing[4], }, + previewMarkdownScrollContent: { + padding: theme.spacing[4], + }, previewImageScrollContent: { flexGrow: 1, padding: theme.spacing[4],