From 55f42d27714c363595042bff4dcad784ef07cba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=83=A1=E7=BF=80?= <1020327414@qq.com> Date: Tue, 27 Oct 2026 10:41:56 +0800 Subject: [PATCH] fix(editor): match IDEA font weights in the light scheme (#35) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: the editor had no font weight settings, and the WebView rendering of JetBrains Mono looks lighter than IDEA (#32). Fix (IDEA's Settings › Editor › Font › Typography Settings): configurable Main font weight and Bold font weight (defaults Regular 400 / Bold 700) via --fw-code / --fw-code-bold; colour-scheme bold/regular overrides still win; settings preview updates live. Replaces the Light-only 450/550 hard-coding. Siblings covered: sticky lines, diff/merge, run console (ANSI bold), Find in Files, completion/parameter info/docs, database editor, debugger value popup, matched braces, terminal with "Use editor font". Verified: scoped vitest, ide-core tests, in-app on macOS (settings UI, editor weight follows the setting end to end). Claude-Session: https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn --- crates/ide-core/src/settings.rs | 16 ++++++ src/bindings.ts | 8 +++ src/editor/core/cm/highlight.ts | 4 +- src/editor/core/cm/prefs.ts | 4 +- src/editor/core/cm/theme.ts | 4 +- src/editor/core/colorSchemeRuntime.ts | 16 ++++-- src/editor/lsp/Popups.module.css | 1 + src/editor/lsp/theme.ts | 14 +++-- src/features/database/Database.module.css | 4 ++ .../debugger/editor/editor.module.css | 1 + src/features/run/ConsoleView.tsx | 2 +- src/features/run/Run.module.css | 1 + .../search/find/FindInFiles.module.css | 1 + src/features/search/find/parts.module.css | 1 + .../settings/__tests__/dialog.test.tsx | 23 ++++++++- .../settings/pages/editor/Editor.module.css | 2 + .../settings/pages/editor/colorScheme.tsx | 12 +++-- src/features/settings/pages/editor/font.tsx | 51 +++++++++++++++++-- src/features/terminal/embedded.ts | 2 + src/features/terminal/session.ts | 8 ++- src/features/terminal/terminal.test.ts | 12 +++++ src/features/terminal/theme.ts | 14 ++++- src/features/todo/highlight.ts | 2 +- src/features/vcs/cm.ts | 4 +- src/platform/colorScheme.test.ts | 43 +++++++++++++++- src/platform/colorScheme.ts | 7 ++- src/platform/editorSettings.ts | 35 ++++++++++++- src/styles/tokens.css | 13 ++++- src/ui/SourcePreview.module.css | 1 + 29 files changed, 274 insertions(+), 32 deletions(-) diff --git a/crates/ide-core/src/settings.rs b/crates/ide-core/src/settings.rs index 240562300..d3f31d929 100644 --- a/crates/ide-core/src/settings.rs +++ b/crates/ide-core/src/settings.rs @@ -292,6 +292,10 @@ pub struct EditorSettings { pub ligatures: bool, /// Fallback font for glyphs the main font lacks; empty = none. pub fallback_font: String, + /// Typography Settings › Main font weight (CSS 100–900), as IDEA. + pub font_weight: u16, + /// Typography Settings › Bold font weight: wherever the editor renders bold. + pub bold_font_weight: u16, pub general: EditorGeneralSettings, pub appearance: EditorAppearanceSettings, pub breadcrumbs: BreadcrumbsSettings, @@ -327,6 +331,8 @@ impl Default for EditorSettings { color_overrides: BTreeMap::new(), ligatures: false, fallback_font: String::new(), + font_weight: 400, + bold_font_weight: 700, general: EditorGeneralSettings::default(), appearance: EditorAppearanceSettings::default(), breadcrumbs: BreadcrumbsSettings::default(), @@ -564,6 +570,16 @@ impl Settings { mod tests { use super::*; + #[test] + fn editor_font_weights_default_to_regular_and_bold() { + let json = serde_json::to_value(Settings::default()).unwrap(); + assert_eq!(json["editor"]["fontWeight"], 400); + assert_eq!(json["editor"]["boldFontWeight"], 700); + let s: Settings = serde_json::from_str(r#"{"editor":{"fontWeight":300,"boldFontWeight":600}}"#).unwrap(); + assert_eq!((s.editor.font_weight, s.editor.bold_font_weight), (300, 600)); + assert_eq!(s.editor.font_size, 13.0); + } + #[test] fn keymap_overrides_are_camel_case_and_default_empty() { let s: Settings = diff --git a/src/bindings.ts b/src/bindings.ts index e2af9937e..853cb8822 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -2019,6 +2019,10 @@ export type EditorSettings_Deserialize = { ligatures?: boolean, /** Fallback font for glyphs the main font lacks; empty = none. */ fallbackFont?: string, + /** Typography Settings › Main font weight (CSS 100–900), as IDEA. */ + fontWeight?: number, + /** Typography Settings › Bold font weight: wherever the editor renders bold. */ + boldFontWeight?: number, general?: EditorGeneralSettings, appearance?: EditorAppearanceSettings, breadcrumbs?: BreadcrumbsSettings, @@ -2055,6 +2059,10 @@ export type EditorSettings_Serialize = { ligatures: boolean, /** Fallback font for glyphs the main font lacks; empty = none. */ fallbackFont: string, + /** Typography Settings › Main font weight (CSS 100–900), as IDEA. */ + fontWeight: number, + /** Typography Settings › Bold font weight: wherever the editor renders bold. */ + boldFontWeight: number, general: EditorGeneralSettings, appearance: EditorAppearanceSettings, breadcrumbs: BreadcrumbsSettings, diff --git a/src/editor/core/cm/highlight.ts b/src/editor/core/cm/highlight.ts index 0d9b01dea..7300a79f8 100644 --- a/src/editor/core/cm/highlight.ts +++ b/src/editor/core/cm/highlight.ts @@ -46,10 +46,10 @@ export const openideaHighlightStyle = HighlightStyle.define([ token([t.punctuation, t.bracket, t.separator, t.operator, t.derefOperator], "punct"), token(t.invalid, "invalid"), // Markup (Markdown, docs) - token(t.heading, "markup-heading", { bold: "600" }), + token(t.heading, "markup-heading", { bold: "var(--fw-code-bold, 700)" }), token(t.processingInstruction, "keyword"), { tag: t.emphasis, fontStyle: "italic" }, - { tag: t.strong, fontWeight: "600" }, + { tag: t.strong, fontWeight: "var(--fw-code-bold, 700)" }, { tag: t.strikethrough, textDecoration: "line-through" }, // Markdown, as IntelliJ: link destination like a static method, rules like markers. token(t.url, "function", { italic: "italic" }), diff --git a/src/editor/core/cm/prefs.ts b/src/editor/core/cm/prefs.ts index d39d133c7..bf9a93444 100644 --- a/src/editor/core/cm/prefs.ts +++ b/src/editor/core/cm/prefs.ts @@ -280,12 +280,14 @@ export function richTextHtml(view: EditorView, from: number, to: number): string const styleOf = new Map(); const probe = document.createElement("span"); view.contentDOM.appendChild(probe); + // Bold = heavier than the editor's main weight (Typography Settings). + const regular = Number(getComputedStyle(view.contentDOM).fontWeight) || 400; const resolve = (cls: string) => { let s = styleOf.get(cls); if (s === undefined) { probe.className = cls; const cs = getComputedStyle(probe); - s = `color:${cs.color};${cs.fontStyle !== "normal" ? `font-style:${cs.fontStyle};` : ""}${Number(cs.fontWeight) >= 600 ? "font-weight:bold;" : ""}`; + s = `color:${cs.color};${cs.fontStyle !== "normal" ? `font-style:${cs.fontStyle};` : ""}${Number(cs.fontWeight) > regular ? "font-weight:bold;" : ""}`; styleOf.set(cls, s); } return s; diff --git a/src/editor/core/cm/theme.ts b/src/editor/core/cm/theme.ts index 57cfb92e3..a0d46311c 100644 --- a/src/editor/core/cm/theme.ts +++ b/src/editor/core/cm/theme.ts @@ -29,6 +29,7 @@ export const openideaTheme = EditorView.theme({ // The workbench body uses the arrow cursor; the text area gets the I-beam (also past the last line). cursor: "text", fontFamily: "var(--openidea-editor-font, var(--font-mono))", + fontWeight: "var(--fw-code, 400)", fontSize: "var(--openidea-editor-font-size, var(--fs-code))", lineHeight: "var(--openidea-editor-line-height, var(--lh-code))", fontVariantLigatures: "none", @@ -71,7 +72,7 @@ export const openideaTheme = EditorView.theme({ ".cm-matchingBracket, &.cm-focused .cm-matchingBracket": { backgroundColor: "var(--editor-brace)", color: "inherit", - fontWeight: "var(--matched-brace-weight, 700)", + fontWeight: "var(--matched-brace-weight, var(--fw-code-bold, 700))", borderRadius: "1px", }, ".cm-matchingTag, &.cm-focused .cm-matchingTag": { @@ -241,6 +242,7 @@ export const openideaTheme = EditorView.theme({ borderBottom: "1px solid var(--editor-gutter-border)", boxShadow: "0 3px 6px -2px color-mix(in srgb, var(--fg-strong) 12%, transparent)", fontFamily: "var(--openidea-editor-font, var(--font-mono))", + fontWeight: "var(--fw-code, 400)", fontSize: "var(--openidea-editor-font-size, var(--fs-code))", lineHeight: "var(--openidea-editor-line-height, var(--lh-code))", overflow: "hidden", diff --git a/src/editor/core/colorSchemeRuntime.ts b/src/editor/core/colorSchemeRuntime.ts index e2ecd8cc1..fda59c012 100644 --- a/src/editor/core/colorSchemeRuntime.ts +++ b/src/editor/core/colorSchemeRuntime.ts @@ -1,6 +1,10 @@ -/** Applies Settings › Editor › Color Scheme (scheme + attribute overrides) as one stylesheet. */ +/** + * Applies Settings › Editor › Color Scheme (scheme + attribute overrides) as one + * stylesheet, and Editor › Font › Typography Settings as the root's + * `--fw-code` / `--fw-code-bold`. + */ import { useProject } from "@/platform/project"; -import { editorPrefs } from "@/platform/editorSettings"; +import { editorPrefs, fontWeightVars } from "@/platform/editorSettings"; import { schemeCss } from "@/platform/colorScheme"; export function installColorScheme(): () => void { @@ -8,10 +12,14 @@ export function installColorScheme(): () => void { const el = document.createElement("style"); el.id = "openidea-editor-color-scheme"; document.head.appendChild(el); + const root = document.documentElement; const apply = () => { const settings = useProject.getState().settings; - const css = schemeCss(settings?.editor?.colorScheme, editorPrefs(settings).colorOverrides); + const prefs = editorPrefs(settings); + const css = schemeCss(settings?.editor?.colorScheme, prefs.colorOverrides); if (el.textContent !== css) el.textContent = css; + for (const [name, value] of Object.entries(fontWeightVars(prefs.fontWeight, prefs.boldFontWeight))) + if (root.style.getPropertyValue(name) !== value) root.style.setProperty(name, value); }; apply(); const unsubscribe = useProject.subscribe((s, prev) => { @@ -23,5 +31,7 @@ export function installColorScheme(): () => void { unsubscribe(); observer?.disconnect(); el.remove(); + root.style.removeProperty("--fw-code"); + root.style.removeProperty("--fw-code-bold"); }; } diff --git a/src/editor/lsp/Popups.module.css b/src/editor/lsp/Popups.module.css index 9931c3de6..c2fd3d95e 100644 --- a/src/editor/lsp/Popups.module.css +++ b/src/editor/lsp/Popups.module.css @@ -65,6 +65,7 @@ margin: 0; padding: 6px 0; font-family: var(--openidea-editor-font, var(--font-mono)); + font-weight: var(--fw-code); font-size: var(--fs-small); color: var(--fg-editor); } diff --git a/src/editor/lsp/theme.ts b/src/editor/lsp/theme.ts index de719a2e8..1fb6d96d3 100644 --- a/src/editor/lsp/theme.ts +++ b/src/editor/lsp/theme.ts @@ -33,11 +33,13 @@ const markdown = { ".cm-lsp-markdown a:hover": { color: "var(--link-hover)", textDecoration: "underline" }, ".cm-lsp-markdown code": { fontFamily: "var(--openidea-editor-font, var(--font-mono))", + fontWeight: "var(--fw-code, 400)", fontSize: "var(--fs-small)", color: "var(--fg-emphasis)", }, ".cm-lsp-markdown pre, .cm-lsp-code": { fontFamily: "var(--openidea-editor-font, var(--font-mono))", + fontWeight: "var(--fw-code, 400)", fontSize: "var(--fs-ui)", lineHeight: "1.5", margin: "6px 0", @@ -71,11 +73,13 @@ export const lspTheme = EditorView.theme({ ".cm-tooltip.cm-tooltip-autocomplete.cm-lsp-completion": { ...tooltipSurface, fontFamily: "var(--openidea-editor-font, var(--font-mono))", + fontWeight: "var(--fw-code, 400)", fontSize: "var(--fs-code)", overflow: "hidden", }, ".cm-tooltip.cm-tooltip-autocomplete.cm-lsp-completion > ul": { fontFamily: "var(--openidea-editor-font, var(--font-mono))", + fontWeight: "var(--fw-code, 400)", minWidth: "420px", maxWidth: "min(720px, 80vw)", maxHeight: "calc(var(--h-list-row) * 11 + 8px)", @@ -123,7 +127,7 @@ export const lspTheme = EditorView.theme({ }, ".cm-lsp-completion .cm-completionMatchedText": { textDecoration: "none", - fontWeight: "700", + fontWeight: "var(--fw-code-bold, 700)", color: "var(--fg-completion-match, var(--link))", }, // A server's label detail (`(a: string)`) hugs the name; template descriptions keep the gap. @@ -138,7 +142,7 @@ export const lspTheme = EditorView.theme({ overflow: "hidden", textOverflow: "ellipsis", }, - ".cm-lsp-completion .cm-lsp-keyword-option .cm-completionLabel": { fontWeight: "700" }, + ".cm-lsp-completion .cm-lsp-keyword-option .cm-completionLabel": { fontWeight: "var(--fw-code-bold, 700)" }, ".cm-lsp-completion .cm-lsp-deprecated-option .cm-completionLabel": { textDecoration: "line-through", color: "var(--fg-muted)", @@ -211,6 +215,7 @@ export const lspTheme = EditorView.theme({ ".cm-lsp-link-button:hover": { color: "var(--link-hover)", textDecoration: "underline" }, ".cm-lsp-signature": { fontFamily: "var(--openidea-editor-font, var(--font-mono))", + fontWeight: "var(--fw-code, 400)", fontSize: "var(--fs-code)", padding: "6px 10px", maxWidth: "640px", @@ -223,7 +228,7 @@ export const lspTheme = EditorView.theme({ padding: "0 10px", }, ".cm-lsp-signature-disabled": { opacity: "0.55" }, - ".cm-lsp-signature b": { fontWeight: "700", color: "var(--fg-strong)" }, + ".cm-lsp-signature b": { fontWeight: "var(--fw-code-bold, 700)", color: "var(--fg-strong)" }, ".cm-lsp-signature-doc": { borderTop: "1px solid var(--border-popup)", marginTop: "6px", @@ -286,6 +291,7 @@ export const lspTheme = EditorView.theme({ margin: "0 -8px 4px", padding: "0", fontFamily: "var(--openidea-editor-font, var(--font-mono))", + fontWeight: "var(--fw-code, 400)", fontSize: "var(--fs-ui)", color: "var(--fg)", }, @@ -339,7 +345,7 @@ export const lspTheme = EditorView.theme({ ".cm-sem-property": { color: "var(--syn-property)" }, ".cm-sem-parameter": { color: "var(--syn-param, var(--syn-variable))" }, ".cm-sem-self": { color: "var(--syn-self, var(--syn-keyword))" }, - ".cm-sem-keyword": { color: "var(--syn-keyword)" }, + ".cm-sem-keyword": { color: "var(--syn-keyword)", fontWeight: "var(--syn-keyword-bold, inherit)" }, ".cm-sem-builtin:not(.cm-sem-function)": { color: "var(--syn-builtin, inherit)" }, ".cm-sem-static": { fontStyle: "italic" }, ".cm-sem-mutable": { textDecoration: "underline", textDecorationColor: "var(--fg-faint)", textUnderlineOffset: "3px" }, diff --git a/src/features/database/Database.module.css b/src/features/database/Database.module.css index f99baed31..043d4fb16 100644 --- a/src/features/database/Database.module.css +++ b/src/features/database/Database.module.css @@ -335,6 +335,7 @@ } .paramInput { font-family: var(--openidea-editor-font, var(--font-mono)); + font-weight: var(--fw-code); } .formRow { display: flex; @@ -479,6 +480,7 @@ :global(.cm-openidea-db-doc) { margin: 0; font-family: var(--openidea-editor-font, var(--font-mono)); + font-weight: var(--fw-code); font-size: var(--fs-code); color: var(--fg-editor, var(--fg-strong)); white-space: pre; @@ -498,6 +500,7 @@ overflow: auto; padding: 4px 8px; font-family: var(--openidea-editor-font, var(--font-mono)); + font-weight: var(--fw-code); font-size: var(--fs-code); line-height: 1.5; color: var(--fg-editor, var(--fg-strong)); @@ -648,6 +651,7 @@ max-height: 220px; overflow: auto; font-family: var(--openidea-editor-font, var(--font-mono)); + font-weight: var(--fw-code); font-size: var(--fs-small); line-height: 1.5; white-space: pre; diff --git a/src/features/debugger/editor/editor.module.css b/src/features/debugger/editor/editor.module.css index 2a89c9dfd..668254455 100644 --- a/src/features/debugger/editor/editor.module.css +++ b/src/features/debugger/editor/editor.module.css @@ -139,6 +139,7 @@ max-width: 520px; padding: 4px 8px; font-family: var(--font-mono); + font-weight: var(--fw-code); font-size: var(--fs-code); white-space: pre-wrap; word-break: break-word; diff --git a/src/features/run/ConsoleView.tsx b/src/features/run/ConsoleView.tsx index fd152beae..358428bfd 100644 --- a/src/features/run/ConsoleView.tsx +++ b/src/features/run/ConsoleView.tsx @@ -28,7 +28,7 @@ function spanStyle(s: SpanStyle): CSSProperties | undefined { return { color: s.fg, background: s.bg, - fontWeight: s.bold ? 600 : undefined, + fontWeight: s.bold ? "var(--fw-code-bold, 700)" : undefined, opacity: s.dim ? 0.6 : undefined, fontStyle: s.italic ? "italic" : undefined, textDecoration: deco || undefined, diff --git a/src/features/run/Run.module.css b/src/features/run/Run.module.css index 106a0953b..64b5f2a0a 100644 --- a/src/features/run/Run.module.css +++ b/src/features/run/Run.module.css @@ -109,6 +109,7 @@ position: relative; padding: 4px 0; font-family: var(--font-mono); + font-weight: var(--fw-code); font-size: var(--fs-code); color: var(--fg); background: var(--bg-app); diff --git a/src/features/search/find/FindInFiles.module.css b/src/features/search/find/FindInFiles.module.css index ac1be6d53..9737ec6b4 100644 --- a/src/features/search/find/FindInFiles.module.css +++ b/src/features/search/find/FindInFiles.module.css @@ -108,6 +108,7 @@ flex-direction: column; padding: 2px 0; font-family: var(--font-mono); + font-weight: var(--fw-code); } .results > * { flex: 1; diff --git a/src/features/search/find/parts.module.css b/src/features/search/find/parts.module.css index 1e2051cb2..cc0ce7b5f 100644 --- a/src/features/search/find/parts.module.css +++ b/src/features/search/find/parts.module.css @@ -27,6 +27,7 @@ overflow: auto; background: var(--bg-app); font-family: var(--font-mono); + font-weight: var(--fw-code); font-size: var(--fs-ui); line-height: 19px; color: var(--fg); diff --git a/src/features/settings/__tests__/dialog.test.tsx b/src/features/settings/__tests__/dialog.test.tsx index 8f894c254..42591fcdc 100644 --- a/src/features/settings/__tests__/dialog.test.tsx +++ b/src/features/settings/__tests__/dialog.test.tsx @@ -25,7 +25,7 @@ import { useProject } from "@/platform/project"; import { useKeymap } from "@/platform/keymap"; import { ContextMenuHost } from "@/ui"; import { DEFAULT_SETTINGS, setAt } from "../model/schema"; -import { useSettingsDialog } from "../model/staging"; +import { stage, useSettingsDialog } from "../model/staging"; import { SettingsDialog } from "../dialog/SettingsDialog"; import { activate } from ".."; import { MessagesHost } from "@/workbench/messages"; @@ -107,6 +107,27 @@ describe("Settings dialog", () => { expect(within(dialog).getByRole("button", { name: "Apply" })).toBeDisabled(); }); + it("Typography Settings weights preview live and apply as editor settings", async () => { + const dialog = await openDialog("editor.font"); + expect(within(dialog).getByRole("button", { name: "Main font weight" })).toHaveTextContent("Regular"); + expect(within(dialog).getByRole("button", { name: "Bold font weight" })).toHaveTextContent("Bold"); + act(() => { + stage("editor.font", "global", "editor.fontWeight", 300); + stage("editor.font", "global", "editor.boldFontWeight", 600); + }); + expect(within(dialog).getByRole("button", { name: "Main font weight" })).toHaveTextContent("Light"); + const preview = within(dialog).getByLabelText("Font preview"); + expect(preview.style.getPropertyValue("--fw-code")).toBe("300"); + expect(preview.style.getPropertyValue("--fw-code-bold")).toBe("600"); + await act(async () => { + fireEvent.click(within(dialog).getByRole("button", { name: "Apply" })); + }); + expect(settingsSet.mock.calls).toEqual([ + ["global", "editor.boldFontWeight", "600"], + ["global", "editor.fontWeight", "300"], + ]); + }); + it("Cancel discards staged edits", async () => { const dialog = await openDialog("editor.codeStyle.other"); fireEvent.change(within(dialog).getByLabelText("Tab size"), { target: { value: "2" } }); diff --git a/src/features/settings/pages/editor/Editor.module.css b/src/features/settings/pages/editor/Editor.module.css index b2c45f951..1c44fda50 100644 --- a/src/features/settings/pages/editor/Editor.module.css +++ b/src/features/settings/pages/editor/Editor.module.css @@ -130,6 +130,7 @@ color: var(--fg-strong); } .previewArea { + font-weight: var(--fw-code); width: 100%; min-height: 240px; resize: vertical; @@ -213,6 +214,7 @@ background: var(--bg-editor, var(--bg-island)); color: var(--fg-editor, var(--fg)); font-family: var(--openidea-editor-font, var(--font-mono)); + font-weight: var(--fw-code); font-size: var(--fs-code); line-height: var(--lh-code); overflow: auto; diff --git a/src/features/settings/pages/editor/colorScheme.tsx b/src/features/settings/pages/editor/colorScheme.tsx index 62f677598..8eecc45dd 100644 --- a/src/features/settings/pages/editor/colorScheme.tsx +++ b/src/features/settings/pages/editor/colorScheme.tsx @@ -6,10 +6,11 @@ */ import { useMemo, useState, type CSSProperties } from "react"; import { Button, Checkbox, Input, Tree, type SelectOption, T } from "@/ui"; -import type { TextAttributesOverride } from "@/platform/editorSettings"; +import { fontWeightVars, type TextAttributesOverride } from "@/platform/editorSettings"; import { ATTRIBUTES, SCHEMES, + boldWeight, effectiveScheme, schemeTokens, type AttributeDef, @@ -72,7 +73,7 @@ function previewVars(id: SchemeId, o: Record | u if (ov.foreground && attr.fg) vars[attr.fg] = ov.foreground; if (ov.background && attr.bg) vars[attr.bg] = ov.background; if (attr.token) { - if (ov.bold !== undefined) vars[`--syn-${attr.token}-bold`] = ov.bold ? "700" : "400"; + if (ov.bold !== undefined) vars[`--syn-${attr.token}-bold`] = boldWeight(ov.bold); if (ov.italic !== undefined) vars[`--syn-${attr.token}-italic`] = ov.italic ? "italic" : "normal"; if (ov.background) vars[`--syn-${attr.token}-bg`] = ov.background; if (ov.effect && ov.effect !== "none") @@ -255,11 +256,16 @@ function ColorRow({ function ColorSchemePage() { const [scheme, setScheme] = useSetting("editor.colorScheme"); const [overrides, setOverrides] = useSetting("editor.colorOverrides"); + const [weight] = useSetting("editor.fontWeight"); + const [boldFontWeight] = useSetting("editor.boldFontWeight"); const [selected, setSelected] = useState("keyword"); const id = effectiveScheme(scheme); const mine = overrides?.[id]; const attr = ATTRIBUTES.find((a) => a.id === selected); - const vars = useMemo(() => previewVars(id, mine), [id, mine]); + const vars = useMemo( + () => ({ ...previewVars(id, mine), ...fontWeightVars(weight, boldFontWeight) }) as CSSProperties, + [id, mine, weight, boldFontWeight], + ); const update = (attrId: string, v: TextAttributesOverride | undefined) => { const next: Overrides = { ...(overrides ?? {}) }; const forScheme = { ...(next[id] ?? {}) }; diff --git a/src/features/settings/pages/editor/font.tsx b/src/features/settings/pages/editor/font.tsx index b648bf5f4..3c422d86f 100644 --- a/src/features/settings/pages/editor/font.tsx +++ b/src/features/settings/pages/editor/font.tsx @@ -1,9 +1,13 @@ -/** Editor › Font: family (installed fonts, monospaced filter), size, line height, ligatures, fallback, editable preview. */ +/** + * Editor › Font: family (installed fonts, monospaced filter), size, line height, + * ligatures, Typography Settings (main / bold weight, fallback font), editable preview. + */ import { useMemo, useState } from "react"; import { Checkbox, type SelectOption, T } from "@/ui"; import { monoFontFamily } from "@/platform/fonts"; import { NumberInput, Row, SearchLabel, Select } from "../../components/Form"; import { useLabelMatch, useSetting } from "../../components/context"; +import { FONT_WEIGHTS, fontWeightOf, fontWeightVars } from "@/platform/editorSettings"; import { LIMITS } from "../../model/schema"; import { editorPage } from "./kit"; import styles from "./Editor.module.css"; @@ -131,6 +135,38 @@ function FallbackRow() { ); } +const WEIGHT_OPTIONS: SelectOption[] = FONT_WEIGHTS.map(([w, name]) => ({ value: String(w), label: name, detail: String(w) })); + +/** + * IDEA's Main / Bold font weight. IDEA lists the weights the family ships; the + * bundled JetBrains Mono is variable (100–900), and other families get the + * standard names (the WebView picks the nearest face or synthesises bold). + */ +function WeightRows() { + const [main, setMain] = useSetting("editor.fontWeight"); + const [bold, setBold] = useSetting("editor.boldFontWeight"); + return ( + <> + + setBold(Number(v))} + aria-label="Bold font weight" + /> + + + ); +} + const SAMPLE = `OpenIDEA is a full-featured IDE with a high level of usability and outstanding advanced code editing and refactoring support. @@ -149,6 +185,8 @@ function Preview() { const [size] = useSetting("editor.fontSize"); const [line] = useSetting("editor.lineHeight"); const [ligatures] = useSetting("editor.ligatures"); + const [weight] = useSetting("editor.fontWeight"); + const [boldWeight] = useSetting("editor.boldFontWeight"); const [text, setText] = useState(SAMPLE); const fam = monoFontFamily(family, fallback, "var(--font-mono)"); return ( @@ -163,6 +201,7 @@ function Preview() { value={text} onChange={(e) => setText(e.target.value)} style={{ + ...fontWeightVars(weight, boldWeight), fontFamily: fam, fontSize: `${size}px`, lineHeight: `${line}px`, @@ -178,7 +217,7 @@ export const FONT_PAGE = editorPage({ title: "Font", order: 22, projectScope: true, - keywords: ["typeface", "monospace", "ligatures", "font size", "line spacing"], + keywords: ["typeface", "monospace", "ligatures", "font size", "line spacing", "font weight", "bold"], sections: [ { items: [ @@ -187,7 +226,13 @@ export const FONT_PAGE = editorPage({ { t: "check", key: "editor.ligatures", label: "Enable ligatures" }, ], }, - { title: "Typography Settings", items: [{ t: "custom", label: "Fallback font", component: FallbackRow }] }, + { + title: "Typography Settings", + items: [ + { t: "custom", label: "Main font weight", keywords: ["Bold font weight"], component: WeightRows }, + { t: "custom", label: "Fallback font", component: FallbackRow }, + ], + }, ], footer: Preview, }); diff --git a/src/features/terminal/embedded.ts b/src/features/terminal/embedded.ts index d8133fc72..ae64676ab 100644 --- a/src/features/terminal/embedded.ts +++ b/src/features/terminal/embedded.ts @@ -30,6 +30,8 @@ export function createEmbeddedTerminal(host: HTMLElement, opts: EmbeddedTerminal fontSize: font.size, lineHeight: font.lineHeight, letterSpacing: font.letterSpacing, + fontWeight: font.weight, + fontWeightBold: font.boldWeight, cursorBlink: settings.cursorBlink, cursorWidth: 2, cursorInactiveStyle: "outline", diff --git a/src/features/terminal/session.ts b/src/features/terminal/session.ts index 140bb5526..01aca4e16 100644 --- a/src/features/terminal/session.ts +++ b/src/features/terminal/session.ts @@ -15,7 +15,7 @@ import { splitCommandLine } from "@/platform/commandLine"; import { useProject } from "@/platform/project"; import { workflowSettings, type TerminalSettingsValid } from "@/platform/workflowSettings"; import { editorPrefs } from "@/platform/editorSettings"; -import { readSearchDecorations, readTerminalTheme, remeasureWhenFontsLoad, terminalFont, type TerminalFontSettings } from "./theme"; +import { readSearchDecorations, readTerminalTheme, remeasureWhenFontsLoad, terminalFont, type EditorFont, type TerminalFontSettings } from "./theme"; export interface TerminalSearchOptions { caseSensitive: boolean; @@ -80,7 +80,7 @@ function beep() { /** The terminal font for `t`, resolved against the current editor font (also used by embedded terminals). */ export function fontFor(t: TerminalFontSettings) { - const e = useProject.getState().settings?.editor as { fontFamily?: string; fontSize?: number; lineHeight?: number } | undefined; + const e = useProject.getState().settings?.editor as EditorFont | undefined; return terminalFont(t, { ...e, fallbackFont: editorPrefs().fallbackFont }); } @@ -171,6 +171,8 @@ export class TerminalSession { fontSize: font.size, lineHeight: font.lineHeight, letterSpacing: font.letterSpacing, + fontWeight: font.weight, + fontWeightBold: font.boldWeight, cursorBlink: settings.cursorBlink, cursorStyle: CURSOR[settings.cursorShape], cursorWidth: 2, @@ -246,6 +248,8 @@ export class TerminalSession { if (o.fontSize !== font.size) o.fontSize = font.size; if (o.lineHeight !== font.lineHeight) o.lineHeight = font.lineHeight; if (o.letterSpacing !== font.letterSpacing) o.letterSpacing = font.letterSpacing; + if (o.fontWeight !== font.weight) o.fontWeight = font.weight; + if (o.fontWeightBold !== font.boldWeight) o.fontWeightBold = font.boldWeight; o.cursorStyle = CURSOR[t.cursorShape]; o.cursorBlink = t.cursorBlink; o.macOptionIsMeta = t.optionAsMeta; diff --git a/src/features/terminal/terminal.test.ts b/src/features/terminal/terminal.test.ts index 8b89f0942..8f88524cd 100644 --- a/src/features/terminal/terminal.test.ts +++ b/src/features/terminal/terminal.test.ts @@ -235,6 +235,18 @@ describe("terminalFont", () => { expect(font.family).not.toContain("PingFang SC"); }); + it("follows the editor's Typography Settings weights with Use editor font", () => { + const f = terminalFont(t, { ...editor, fontWeight: 300, boldFontWeight: 600 }, BASE); + expect([f.weight, f.boldWeight]).toEqual([300, 600]); + const d = terminalFont(t, editor, BASE); + expect([d.weight, d.boldWeight]).toEqual([400, 700]); + }); + + it("keeps regular / bold for the terminal's own font", () => { + const f = terminalFont({ ...t, useEditorFont: false }, { ...editor, fontWeight: 300, boldFontWeight: 600 }, BASE); + expect([f.weight, f.boldWeight]).toEqual([400, 700]); + }); + it("turns the column width into letter spacing", () => { expect(terminalFont({ ...t, useEditorFont: false }, editor, BASE).letterSpacing).toBe(0); expect(terminalFont({ ...t, useEditorFont: false, columnWidth: 1.5 }, editor, BASE).letterSpacing).toBe(3); diff --git a/src/features/terminal/theme.ts b/src/features/terminal/theme.ts index 944f4274a..7d6972662 100644 --- a/src/features/terminal/theme.ts +++ b/src/features/terminal/theme.ts @@ -5,6 +5,7 @@ * derived from the status + syntax tokens. */ import type { ITheme, Terminal } from "@xterm/xterm"; +import { fontWeightOf } from "@/platform/editorSettings"; import { BUNDLED_MONO_FAMILY, fontsLoaded, monoFontFamily, monoStack } from "@/platform/fonts"; export type Rgba = [number, number, number, number]; @@ -192,6 +193,8 @@ export interface EditorFont { fontSize?: number; lineHeight?: number; fallbackFont?: string; + fontWeight?: number; + boldFontWeight?: number; } export interface TerminalFont { @@ -199,6 +202,9 @@ export interface TerminalFont { size: number; lineHeight: number; letterSpacing: number; + /** xterm `fontWeight` / `fontWeightBold`. */ + weight: number; + boldWeight: number; } /** @@ -216,7 +222,7 @@ function cssNumber(name: string): number { /** * Font from Settings › Tools › Terminal: the editor font (Editor › Font, with its - * fallback) or the terminal's own family / size / line height (a multiple of the + * fallback and Typography Settings weights) or the terminal's own family / size / line height (a multiple of the * cell height). The family resolves like the editor's (`monoFontFamily`), so the * bundled JetBrains Mono is used where it isn't installed; `base` is the resolved * `--font-mono` stack, since xterm measures on a canvas and can't use `var()`. @@ -225,6 +231,8 @@ function cssNumber(name: string): number { */ export function terminalFont(t: TerminalFontSettings, editor: EditorFont | undefined, base = monoStack()): TerminalFont { let family: string, fallback: string, size: number, lineHeight: number; + let weight = 400, + boldWeight = 700; if (!t.useEditorFont) { family = t.fontFamily; fallback = t.fallbackFontFamily; @@ -237,9 +245,11 @@ export function terminalFont(t: TerminalFontSettings, editor: EditorFont | undef const lh = editor?.lineHeight ?? (editor?.fontSize ? size * 1.6 : cssNumber("--lh-code") || 21); // xterm's lineHeight is a multiplier of the cell height (≈ 1.2 × font size); aim for the editor's line height. lineHeight = Math.max(1, Math.round((lh / (size * 1.2)) * 100) / 100); + weight = fontWeightOf(editor?.fontWeight, weight); + boldWeight = fontWeightOf(editor?.boldFontWeight, boldWeight); } const letterSpacing = Math.round((t.columnWidth - 1) * size * 0.6 * 10) / 10; - return { family: withNerdFonts(monoFontFamily(family, fallback, base)), size, lineHeight, letterSpacing }; + return { family: withNerdFonts(monoFontFamily(family, fallback, base)), size, lineHeight, letterSpacing, weight, boldWeight }; } /** diff --git a/src/features/todo/highlight.ts b/src/features/todo/highlight.ts index b44044b93..d2992f90c 100644 --- a/src/features/todo/highlight.ts +++ b/src/features/todo/highlight.ts @@ -83,7 +83,7 @@ export function todoDecorations(view: EditorView, settings: TodoSettings): Decor const theme = EditorView.baseTheme({ ".cm-openidea-todo, .cm-openidea-todo *": { color: "var(--todo-color) !important", fontStyle: "italic" }, - ".cm-openidea-todo-keyword, .cm-openidea-todo-keyword *": { fontWeight: "600", fontStyle: "normal" }, + ".cm-openidea-todo-keyword, .cm-openidea-todo-keyword *": { fontWeight: "var(--fw-code-bold, 700)", fontStyle: "normal" }, }); export function todoEditorExtension(ctx: EditorDocContext): Extension { diff --git a/src/features/vcs/cm.ts b/src/features/vcs/cm.ts index 1f8200ac1..a8b83d722 100644 --- a/src/features/vcs/cm.ts +++ b/src/features/vcs/cm.ts @@ -22,7 +22,7 @@ export const viewerHighlight = HighlightStyle.define([ { tag: [t.propertyName], color: "var(--syn-property)" }, { tag: [t.variableName], color: "var(--syn-variable)" }, { tag: t.invalid, color: "var(--syn-invalid)" }, - { tag: t.heading, fontWeight: "600", color: "var(--fg-strong)" }, + { tag: t.heading, fontWeight: "var(--fw-code-bold, 700)", color: "var(--fg-strong)" }, { tag: t.link, color: "var(--link)", textDecoration: "underline" }, ]); @@ -34,7 +34,7 @@ export const viewerTheme = EditorView.theme( color: "var(--fg)", fontSize: "var(--fs-code)", }, - ".cm-scroller": { fontFamily: "var(--font-mono)", lineHeight: "var(--lh-code)" }, + ".cm-scroller": { fontFamily: "var(--font-mono)", fontWeight: "var(--fw-code, 400)", lineHeight: "var(--lh-code)" }, ".cm-content": { caretColor: "var(--accent)" }, ".cm-cursor, .cm-dropCursor": { borderLeftColor: "var(--accent)" }, "&.cm-focused .cm-selectionBackground, .cm-selectionBackground, ::selection": { diff --git a/src/platform/colorScheme.test.ts b/src/platform/colorScheme.test.ts index b6ac2d926..9a3cde952 100644 --- a/src/platform/colorScheme.test.ts +++ b/src/platform/colorScheme.test.ts @@ -1,7 +1,10 @@ import { readFileSync } from "node:fs"; import { beforeAll, describe, expect, it } from "vitest"; import { _setSchemeSource, schemeCss, schemeTokens } from "./colorScheme"; -import { validateEditorExtras } from "./editorSettings"; +import { fontWeightVars, validateEditorExtras } from "./editorSettings"; +import type { Settings } from "./ipc"; +import { useProject } from "./project"; +import { installColorScheme } from "@/editor/core/colorSchemeRuntime"; describe("color schemes", () => { beforeAll(() => _setSchemeSource(readFileSync("src/styles/tokens.css", "utf8"))); @@ -19,7 +22,43 @@ describe("color schemes", () => { expect(dark).toContain("--syn-keyword: #cf8e6d !important"); const over = schemeCss("theme-default", { light: { keyword: { foreground: "#ff0000", bold: true } } }, root); expect(over).toContain("--syn-keyword: #ff0000 !important"); - expect(over).toContain("--syn-keyword-bold: 700 !important"); + // Explicit Bold / regular follow Typography Settings' bold / main weight. + expect(over).toContain("--syn-keyword-bold: var(--fw-code-bold, 700) !important"); + const regular = schemeCss("theme-default", { light: { keyword: { bold: false } } }, root); + expect(regular).toContain("--syn-keyword-bold: var(--fw-code, 400) !important"); + }); + + it("keeps editor weights out of the schemes", () => { + for (const id of ["light", "dark", "dark-hc"] as const) { + expect(schemeTokens(id).has("--fw-code")).toBe(false); + expect(schemeTokens(id).has("--syn-keyword-bold")).toBe(false); + } + // Dark schemes bold matched braces with the configured bold weight, even under the light theme. + expect(schemeTokens("dark").get("--matched-brace-weight")).toBe("var(--fw-code-bold)"); + expect(schemeTokens("light").get("--matched-brace-weight")).toBe("inherit"); + const root = document.documentElement; + root.dataset.theme = "light"; + expect(schemeCss("dark", undefined, root)).toContain("--matched-brace-weight: var(--fw-code-bold) !important"); + }); + + it("sets the configured weights on the root and follows settings changes", () => { + const root = document.documentElement; + useProject.setState({ settings: { editor: { fontWeight: 300, boldFontWeight: 800 } } as unknown as Settings }); + const dispose = installColorScheme(); + expect([root.style.getPropertyValue("--fw-code"), root.style.getPropertyValue("--fw-code-bold")]).toEqual(["300", "800"]); + useProject.setState({ settings: { editor: {} } as unknown as Settings }); + expect([root.style.getPropertyValue("--fw-code"), root.style.getPropertyValue("--fw-code-bold")]).toEqual(["400", "700"]); + dispose(); + expect(root.style.getPropertyValue("--fw-code")).toBe(""); + }); + + it("validates Typography Settings weights", () => { + const d = validateEditorExtras({}); + expect([d.fontWeight, d.boldFontWeight]).toEqual([400, 700]); + const v = validateEditorExtras({ fontWeight: 349, boldFontWeight: 1200 }); + expect([v.fontWeight, v.boldFontWeight]).toEqual([300, 900]); + expect(validateEditorExtras({ fontWeight: "bold" }).fontWeight).toBe(400); + expect(fontWeightVars(500, undefined)).toEqual({ "--fw-code": "500", "--fw-code-bold": "700" }); }); it("coerces editor settings to their defaults' shape", () => { diff --git a/src/platform/colorScheme.ts b/src/platform/colorScheme.ts index d4ab24572..5204fd523 100644 --- a/src/platform/colorScheme.ts +++ b/src/platform/colorScheme.ts @@ -214,6 +214,11 @@ export function schemeValue(id: SchemeId, cssVar: string): string | undefined { return schemeTokens(id).get(cssVar); } +/** An explicit Bold / regular attribute: the editor's configured bold or main weight. */ +export function boldWeight(bold: boolean): string { + return bold ? "var(--fw-code-bold, 700)" : "var(--fw-code, 400)"; +} + const EFFECT_CSS: Record string> = { underline: (c) => `underline ${c}`, wave: (c) => `underline wavy ${c}`, @@ -237,7 +242,7 @@ export function overrideDeclarations(overrides: Record>, ligatures: false, fallbackFont: "", + /** Typography Settings › Main / Bold font weight (CSS weights, IDEA's Regular / Bold). */ + fontWeight: 400, + boldFontWeight: 700, general: { wheelZoom: false, dragAndDrop: true, @@ -185,8 +188,38 @@ export function coerceLike(raw: unknown, def: T): T { return def; } +/** IDEA's named font weights (Typography Settings). */ +export const FONT_WEIGHTS: ReadonlyArray = [ + [100, "Thin"], + [200, "ExtraLight"], + [300, "Light"], + [400, "Regular"], + [500, "Medium"], + [600, "SemiBold"], + [700, "Bold"], + [800, "ExtraBold"], + [900, "Black"], +]; + +/** Nearest CSS weight step (100–900); `def` for anything that is not a number. */ +export function fontWeightOf(raw: unknown, def: number): number { + if (typeof raw !== "number" || !Number.isFinite(raw)) return def; + return Math.min(900, Math.max(100, Math.round(raw / 100) * 100)); +} + +/** `--fw-code` / `--fw-code-bold`: what editor-font text and its bold runs render with. */ +export function fontWeightVars(main: unknown, bold: unknown): Record<"--fw-code" | "--fw-code-bold", string> { + return { + "--fw-code": String(fontWeightOf(main, EDITOR_EXTRA_DEFAULTS.fontWeight)), + "--fw-code-bold": String(fontWeightOf(bold, EDITOR_EXTRA_DEFAULTS.boldFontWeight)), + }; +} + export function validateEditorExtras(raw: unknown): EditorExtras { - return coerceLike(raw, EDITOR_EXTRA_DEFAULTS); + const out = coerceLike(raw, EDITOR_EXTRA_DEFAULTS); + out.fontWeight = fontWeightOf(out.fontWeight, EDITOR_EXTRA_DEFAULTS.fontWeight); + out.boldFontWeight = fontWeightOf(out.boldFontWeight, EDITOR_EXTRA_DEFAULTS.boldFontWeight); + return out; } let cache: { raw: unknown; powerSave: boolean; value: EditorExtras } | null = null; diff --git a/src/styles/tokens.css b/src/styles/tokens.css index b97e00043..6bc00f49d 100644 --- a/src/styles/tokens.css +++ b/src/styles/tokens.css @@ -5,6 +5,15 @@ * (+ expUI parents) and the Islands Dark / Light editor schemes; metrics are the * theme JSON values at 100% zoom. Components use only these variables. */ +/* + * Editor font weights (Settings › Editor › Font › Typography Settings; set on + * the root at runtime). Outside `:root` so scheme tokens such as + * `--matched-brace-weight` keep referring to them instead of a resolved value. + */ +html { + --fw-code: 400; + --fw-code-bold: 700; +} :root { /* surfaces */ --bg-app: #191a1c; @@ -121,7 +130,7 @@ --bg-text-search-result: #114957; --bg-text-search-result-current: #165e70; --bg-matched-brace: #43454a; - --matched-brace-weight: 700; + --matched-brace-weight: var(--fw-code-bold); --bg-matched-tag: #3f4045; --bg-fold-placeholder: #393b40; --fg-fold-placeholder: #868991; @@ -455,7 +464,7 @@ --bg-text-search-result: #fcd47e; --bg-text-search-result-current: #f2bf57; --bg-matched-brace: #93d9d9; - --matched-brace-weight: normal; + --matched-brace-weight: inherit; --bg-matched-tag: #c9ecec; --bg-fold-placeholder: #e9f5e6; --fg-fold-placeholder: #414d41; diff --git a/src/ui/SourcePreview.module.css b/src/ui/SourcePreview.module.css index dfb99aab4..53e45f39f 100644 --- a/src/ui/SourcePreview.module.css +++ b/src/ui/SourcePreview.module.css @@ -7,6 +7,7 @@ overflow: auto; background: var(--bg-app); font-family: var(--font-mono); + font-weight: var(--fw-code); font-size: var(--fs-ui); line-height: 19px; color: var(--fg);