feat(markdown): set the preview font size apart from the UI font (#1045)

The Markdown preview had a fixed 14px body and notebook Markdown cells inherited the UI font size, so enlarging them meant scaling the whole UI. Languages & Frameworks › Markdown › Preview font size (application-level, 8–72, following the editor font size until set) now drives the preview and notebook Markdown cells; fenced code scales with it, and notebook code cells and outputs follow the editor font.

Fixes #768

Co-authored-by: jaw52 <53564781+jaw52@users.noreply.github.com>
This commit is contained in:
Jaw
2026-11-03 23:16:53 +08:00
committed by GitHub
parent 71dd9d120b
commit d6efea8dac
12 changed files with 176 additions and 6 deletions
+9
View File
@@ -549,6 +549,15 @@ pub struct LanguagesSettings {
pub prettier: PrettierSettings,
pub java: JavaSettings,
pub ruby: RubySettings,
pub markdown: MarkdownSettings,
}
/// Languages & Frameworks › Markdown. Like IDEA's, it is application-level only.
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Type)]
#[serde(default, rename_all = "camelCase")]
pub struct MarkdownSettings {
/// Preview (and notebook Markdown cell) font size in px; unset follows the editor font size.
pub preview_font_size: Option<u32>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
+7
View File
@@ -4122,6 +4122,7 @@ export type LanguagesSettings = {
prettier?: PrettierSettings,
java?: JavaSettings,
ruby?: RubySettings,
markdown?: MarkdownSettings,
};
/** What a freshly opened window should do once its frontend is up. */
@@ -4352,6 +4353,12 @@ export type ManifestDependency = {
to: number,
};
/** Languages & Frameworks › Markdown. Like IDEA's, it is application-level only. */
export type MarkdownSettings = {
/** Preview (and notebook Markdown cell) font size in px; unset follows the editor font size. */
previewFontSize?: number | null,
};
export type Markup = {
/** `markdown` or `plaintext`. */
kind: string,
+3 -1
View File
@@ -121,7 +121,8 @@
.rawCell pre {
white-space: pre-wrap;
overflow-wrap: anywhere;
font: var(--fs-code) / var(--lh-code, 1.5) var(--font-mono);
font: var(--openidea-editor-font-size, var(--fs-code)) / var(--openidea-editor-line-height, var(--lh-code, 1.5))
var(--font-mono);
margin: 0;
user-select: text;
}
@@ -160,6 +161,7 @@
.markdown {
padding: 8px 14px;
min-height: 32px;
font-size: var(--markdown-preview-font-size, var(--fs-ui));
overflow-wrap: anywhere;
line-height: 1.6;
user-select: text;
+20 -1
View File
@@ -1,7 +1,17 @@
import { useCallback, useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
import {
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
useSyncExternalStore,
type CSSProperties,
} from "react";
import { useVirtualizer } from "@tanstack/react-virtual";
import type { NotebookCell } from "@/bindings";
import { tx } from "@/platform/i18n";
import { useMarkdownPreviewFontSize } from "@/platform/languageSettings";
import { useProject } from "@/platform/project";
import { Button, Dialog, EmptyState, IconButton, Select, Spinner } from "@/ui";
import type { EditorInput } from "@/workbench/contributions";
import { CellEditor } from "./CellEditor";
@@ -56,6 +66,14 @@ function NotebookDocument({ session }: { session: NotebookSession }) {
return () => element.removeEventListener("beforeinput", history, true);
}, [session, invoke]);
// Markdown cells follow the Markdown preview size, code cells and outputs the editor font, not the UI font.
const markdownFontSize = useMarkdownPreviewFontSize();
const editor = useProject((s) => s.settings?.editor as { fontSize?: number; lineHeight?: number } | undefined);
const fontVars = {
"--markdown-preview-font-size": `${markdownFontSize}px`,
...(editor?.fontSize ? { "--openidea-editor-font-size": `${editor.fontSize}px` } : {}),
...(editor?.lineHeight ? { "--openidea-editor-line-height": `${editor.lineHeight}px` } : {}),
} as CSSProperties;
const disabled = state.frozen || !state.snapshot;
const historyDisabled = disabled || state.running;
const options = state.environments.map((sdk) => ({ value: sdk.path, label: sdk.name, detail: sdk.path }));
@@ -69,6 +87,7 @@ function NotebookDocument({ session }: { session: NotebookSession }) {
<div
ref={root}
className={styles.root}
style={fontVars}
tabIndex={-1}
onKeyDownCapture={(event) => {
if (event.nativeEvent.isComposing || event.defaultPrevented) return;
@@ -53,6 +53,16 @@ describe("languages settings", () => {
expect(validateLanguages({ prettier: { languages: [] } }).prettier.languages).toEqual([]);
});
it("leaves the Markdown preview font size unset (editor font) and keeps a set one in 8..72", () => {
expect(validateLanguages({}).markdown).toEqual({ previewFontSize: null });
expect(validateLanguages({ markdown: { previewFontSize: 90 } }).markdown).toEqual({ previewFontSize: 72 });
expect(validateLanguages({ markdown: { previewFontSize: "x" } }).markdown).toEqual({ previewFontSize: null });
});
it("keeps the Markdown page global, like IDEA's application-level setting", () => {
expect(LANGUAGE_PAGES.find((p) => p.id === "languages.markdown")).not.toHaveProperty("projectScope");
});
it("marks folders with one role each", () => {
expect(normalizeFolder("./src/main/")).toBe("src/main");
let ps = validateProjectStructure({ sourceRoots: ["src/", "src", 3], excluded: ["/dist"] });
@@ -74,6 +84,7 @@ describe("language pages", () => {
expect(langs.children.map((n) => n.title)).toEqual([
"Go",
"Java",
"Markdown",
"Node.js",
"Python Interpreter",
"Prettier",
@@ -0,0 +1,30 @@
import { useId } from "react";
import { NumberInput, Row, Section } from "../components/Form";
import { useSetting } from "../components/context";
/** Languages & Frameworks › Markdown, after IDEA's: application-level, preview font size follows the editor font until set. */
export function MarkdownPage() {
const id = useId();
const [size, setSize] = useSetting<number | null>("languages.markdown.previewFontSize");
const [editorSize] = useSetting<number>("editor.fontSize");
return (
<Section title="Preview">
<Row
label="Preview font size"
htmlFor={id}
hint="Also used by Markdown cells in Jupyter notebooks. Defaults to the editor font size, independent of the UI font size."
>
<NumberInput
id={id}
value={size ?? editorSize}
onChange={setSize}
min={8}
max={72}
integer
unit="px"
aria-label="Preview font size"
/>
</Row>
</Section>
);
}
+11 -1
View File
@@ -1,9 +1,10 @@
/**
* PyCharm's top-level "Python" group (Interpreter), "Project Structure", and
* Languages & Frameworks › Go / Java / Node.js / Rust / TypeScript. Pages live in
* Languages & Frameworks › Go / Java / Markdown / Node.js / Rust / TypeScript. Pages live in
* `../languages/`; settings are `languages.*` / `projectStructure.*`.
*/
import { GoPage, JavaPage, NodePage, RubyPage, RustPage, TypeScriptPage } from "../languages/LanguagePages";
import { MarkdownPage } from "../languages/MarkdownPage";
import { PrettierPage } from "../languages/PrettierPage";
import { ProjectStructurePage } from "../languages/ProjectStructurePage";
import { PythonInterpreterPage } from "../languages/PythonInterpreterPage";
@@ -108,6 +109,15 @@ export const LANGUAGE_PAGES: PageSpec[] = [
keywords: ["node interpreter", "package manager", "npm", "pnpm", "yarn", "bun", "nvm"],
component: NodePage,
},
{
// Global only, like IDEA's application-level Markdown settings.
id: "languages.markdown",
title: "Markdown",
group: LANGUAGES_GROUP,
order: 400,
keywords: ["markdown", "preview", "font size", "jupyter", "notebook"],
component: MarkdownPage,
},
{
id: "languages.prettier",
projectScope: true,
@@ -40,7 +40,7 @@
background: var(--bg-editor, var(--bg-app));
color: var(--fg-editor, var(--fg));
font-family: var(--font-ui);
font-size: calc(14px + var(--markdown-font-zoom, 0px));
font-size: calc(var(--markdown-preview-font-size, 14px) + var(--markdown-font-zoom, 0px));
line-height: 1.6;
overflow-wrap: break-word;
}
@@ -105,7 +105,7 @@
.preview :global(pre code) {
padding: 0;
background: none;
font-size: calc(var(--fs-code) + var(--markdown-font-zoom, 0px));
font-size: 0.93em;
}
.preview :global(blockquote) {
padding: 0 1em;
@@ -19,6 +19,7 @@ import {
} from "react";
import clsx from "clsx";
import { call, commands, isTauri } from "@/platform/ipc";
import { useMarkdownPreviewFontSize } from "@/platform/languageSettings";
import { maybeEditorService } from "@/workbench/contributions";
import { setShownLine } from "@/editor/api";
import { highlightInto } from "@/editor/lsp/markdown";
@@ -93,6 +94,7 @@ const blocksOf = (root: HTMLElement) =>
function MarkdownPreview({ path, sync }: { path: string; sync: boolean }) {
const text = useLiveMarkdown((s) => s[path]?.text);
const fontZoom = useLiveMarkdown((s) => s[path]?.fontZoom ?? 0);
const fontSize = useMarkdownPreviewFontSize();
const topLine = useLiveMarkdown((s) => s[path]?.topLine ?? 0);
const scrollSeq = useLiveMarkdown((s) => s[path]?.scrollSeq ?? 0);
const caretLine = useLiveMarkdown((s) => s[path]?.caretLine ?? 0);
@@ -236,7 +238,12 @@ function MarkdownPreview({ path, sync }: { path: string; sync: boolean }) {
<div
ref={ref}
className={clsx(styles.preview, selectableText)}
style={{ "--markdown-font-zoom": `${fontZoom}px` } as CSSProperties}
style={
{
"--markdown-preview-font-size": `${fontSize}px`,
"--markdown-font-zoom": `${fontZoom}px`,
} as CSSProperties
}
tabIndex={-1}
role="document"
aria-label={tx("Markdown preview")}
@@ -11,6 +11,8 @@ import { useContextKeys } from "@/platform/context";
import { intellijMacos } from "@/platform/keymaps/intellij-macos";
import { intellijWindows } from "@/platform/keymaps/intellij-windows";
import { vscodeMacos } from "@/platform/keymaps/vscode-macos";
import { markdownPreviewFontSize } from "@/platform/languageSettings";
import { useProject } from "@/platform/project";
let activeSlot: Partial<EditorSlot> | null = null;
vi.mock("@/editor/core/controller", async (importOriginal) => ({
@@ -128,3 +130,47 @@ describe("editor font size actions", () => {
}
});
});
describe("Markdown preview font size", () => {
const preview = () => {
act(() => useMarkdownModes.setState((s) => ({ byPath: { ...s.byPath, [mdCtx.path]: "preview" } })));
render(
<MarkdownEditor path={mdCtx.path} tabId="t">
<div />
</MarkdownEditor>,
);
return screen.getByRole("document", { name: "Markdown preview" });
};
afterEach(() => {
cleanup();
useProject.setState({ settings: null });
});
it("follows the editor font size until Markdown › Preview font size is set, never the UI font", () => {
expect(preview().style.getPropertyValue("--markdown-preview-font-size")).toBe("13px");
cleanup();
act(() => useProject.setState({ settings: { appearance: { uiFontSize: 11 }, editor: { fontSize: 15 } } } as never));
expect(preview().style.getPropertyValue("--markdown-preview-font-size")).toBe("15px");
cleanup();
act(() =>
useProject.setState({
settings: {
appearance: { uiFontSize: 11 },
editor: { fontSize: 15 },
languages: { markdown: { previewFontSize: 20 } },
},
} as never),
);
expect(preview().style.getPropertyValue("--markdown-preview-font-size")).toBe("20px");
});
it("keeps a set size within IDEA's 8..72", () => {
const size = (v: unknown) =>
markdownPreviewFontSize({ editor: { fontSize: 14 }, languages: { markdown: { previewFontSize: v } } });
expect(size(3)).toBe(8);
expect(size(200)).toBe(72);
expect(size(15.6)).toBe(16);
expect(size(null)).toBe(14);
expect(size("big")).toBe(14);
});
});
+27
View File
@@ -51,6 +51,8 @@ export interface LanguagesValid {
gradle: { jvm: string; distribution: "wrapper" | "local"; home: string; userHome: string };
maven: { home: string; userSettings: string; localRepository: string; unlinkedProjects: string[] };
};
/** Preview font size in px; `null` follows the editor font size. */
markdown: { previewFontSize: number | null };
}
export interface ProjectStructureValid {
@@ -111,6 +113,7 @@ export const DEFAULT_LANGUAGES: LanguagesValid = {
gradle: { jvm: "", distribution: "wrapper", home: "", userHome: "" },
maven: { home: "", userSettings: "", localRepository: "", unlinkedProjects: [] },
},
markdown: { previewFontSize: null },
};
export const DEFAULT_PROJECT_STRUCTURE: ProjectStructureValid = {
@@ -140,6 +143,9 @@ export function normalizeFolder(path: string): string {
.replace(/^\/+|\/+$/g, "");
}
/** A stored preview font size within IDEA's 8..72, or `null` (follow the editor font). */
const previewSize = (v: unknown): number | null =>
typeof v === "number" && Number.isFinite(v) ? Math.min(72, Math.max(8, Math.round(v))) : null;
const folders = (v: unknown): string[] =>
Array.isArray(v)
? [
@@ -171,6 +177,7 @@ export function validateLanguages(raw: unknown): LanguagesValid {
const java = obj(l.java);
const maven = obj(java.maven);
const gradle = obj(java.gradle);
const markdown = obj(l.markdown);
return {
python: {
interpreter: text(py.interpreter, d.python.interpreter),
@@ -229,9 +236,29 @@ export function validateLanguages(raw: unknown): LanguagesValid {
unlinkedProjects: folders(maven.unlinkedProjects),
},
},
markdown: { previewFontSize: previewSize(markdown.previewFontSize) },
};
}
/**
* Markdown preview (and notebook Markdown cell) font size in px: Languages &
* Frameworks › Markdown, else the editor font size, as IDEA defaults it.
*/
export function markdownPreviewFontSize(settings: unknown): number {
const s = obj(settings);
const editorSize = obj(s.editor).fontSize;
return (
previewSize(obj(obj(s.languages).markdown).previewFontSize) ??
// 13: the editor's default font size.
(typeof editorSize === "number" && editorSize > 0 ? editorSize : 13)
);
}
/** Reactive `markdownPreviewFontSize` of the open project. */
export function useMarkdownPreviewFontSize(): number {
return useProject((s) => markdownPreviewFontSize(s.settings));
}
export function validateProjectStructure(raw: unknown): ProjectStructureValid {
const p = obj(raw);
return {
+2
View File
@@ -5373,6 +5373,8 @@
"Pull Request branch update failed": "拉取请求分支更新失败",
"#{n} on {branch}": "#{n} 位于 {branch}",
"Markdown Images": "Markdown 图片",
"Preview font size": "预览字号",
"Also used by Markdown cells in Jupyter notebooks. Defaults to the editor font size, independent of the UI font size.": "同时用于 Jupyter 笔记本中的 Markdown 单元格。默认跟随编辑器字号,与 UI 字号相互独立。",
"Image folder": "图片文件夹",
"Relative to the Markdown file.": "相对于 Markdown 文件。",
"Image name pattern": "图片名称格式",