diff --git a/crates/ide-core/src/settings.rs b/crates/ide-core/src/settings.rs index 38b676ca..f93bf1d6 100644 --- a/crates/ide-core/src/settings.rs +++ b/crates/ide-core/src/settings.rs @@ -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, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)] diff --git a/src/bindings.ts b/src/bindings.ts index 31aa20b4..58ac9d10 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -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, diff --git a/src/features/notebook/Notebook.module.css b/src/features/notebook/Notebook.module.css index c5bb59f5..cf9d9c26 100644 --- a/src/features/notebook/Notebook.module.css +++ b/src/features/notebook/Notebook.module.css @@ -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; diff --git a/src/features/notebook/NotebookEditor.tsx b/src/features/notebook/NotebookEditor.tsx index 339fa9d6..da3b049d 100644 --- a/src/features/notebook/NotebookEditor.tsx +++ b/src/features/notebook/NotebookEditor.tsx @@ -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 }) {
{ if (event.nativeEvent.isComposing || event.defaultPrevented) return; diff --git a/src/features/settings/__tests__/languages.test.ts b/src/features/settings/__tests__/languages.test.ts index 0f78d295..04070caa 100644 --- a/src/features/settings/__tests__/languages.test.ts +++ b/src/features/settings/__tests__/languages.test.ts @@ -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", diff --git a/src/features/settings/languages/MarkdownPage.tsx b/src/features/settings/languages/MarkdownPage.tsx new file mode 100644 index 00000000..50b45400 --- /dev/null +++ b/src/features/settings/languages/MarkdownPage.tsx @@ -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("languages.markdown.previewFontSize"); + const [editorSize] = useSetting("editor.fontSize"); + return ( +
+ + + +
+ ); +} diff --git a/src/features/settings/pages/languages.tsx b/src/features/settings/pages/languages.tsx index 8f217c37..60655f4b 100644 --- a/src/features/settings/pages/languages.tsx +++ b/src/features/settings/pages/languages.tsx @@ -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, diff --git a/src/features/viewers/markdown/Markdown.module.css b/src/features/viewers/markdown/Markdown.module.css index 86bbf294..38cc5e58 100644 --- a/src/features/viewers/markdown/Markdown.module.css +++ b/src/features/viewers/markdown/Markdown.module.css @@ -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; diff --git a/src/features/viewers/markdown/MarkdownEditor.tsx b/src/features/viewers/markdown/MarkdownEditor.tsx index 917a3e55..18b0895a 100644 --- a/src/features/viewers/markdown/MarkdownEditor.tsx +++ b/src/features/viewers/markdown/MarkdownEditor.tsx @@ -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 }) {
| 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( + +
+ , + ); + 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); + }); +}); diff --git a/src/platform/languageSettings.ts b/src/platform/languageSettings.ts index 01032a2a..f6df5986 100644 --- a/src/platform/languageSettings.ts +++ b/src/platform/languageSettings.ts @@ -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 { diff --git a/src/platform/locales/text/zh.json b/src/platform/locales/text/zh.json index 7760826c..12b2c59d 100644 --- a/src/platform/locales/text/zh.json +++ b/src/platform/locales/text/zh.json @@ -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": "图片名称格式",