fix(editor): match IDEA font weights in the light scheme (#35)

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
Цей коміт міститься в:
胡翀 authored and GitHub committed 2026-10-27 10:41:56 +08:00
1 джерело 30310810e6
коміт 55f42d2771
29 files changed
+274 -32

No files matched your search

+16
Переглянути файл
@@ -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 =
+8
Переглянути файл
@@ -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,
+2 -2
Переглянути файл
@@ -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" }),
+3 -1
Переглянути файл
@@ -280,12 +280,14 @@ export function richTextHtml(view: EditorView, from: number, to: number): string
const styleOf = new Map<string, string>();
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;
+3 -1
Переглянути файл
@@ -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",
+13 -3
Переглянути файл
@@ -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");
};
}
+1
Переглянути файл
@@ -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);
}
+10 -4
Переглянути файл
@@ -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" },
+4
Переглянути файл
@@ -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;
+1
Переглянути файл
@@ -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;
+1 -1
Переглянути файл
@@ -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,
+1
Переглянути файл
@@ -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);
+1
Переглянути файл
@@ -108,6 +108,7 @@
flex-direction: column;
padding: 2px 0;
font-family: var(--font-mono);
font-weight: var(--fw-code);
}
.results > * {
flex: 1;
+1
Переглянути файл
@@ -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);
+22 -1
Переглянути файл
@@ -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" } });
+2
Переглянути файл
@@ -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;
+9 -3
Переглянути файл
@@ -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<string, TextAttributesOverride> | 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<string>("editor.colorScheme");
const [overrides, setOverrides] = useSetting<Overrides>("editor.colorOverrides");
const [weight] = useSetting<number>("editor.fontWeight");
const [boldFontWeight] = useSetting<number>("editor.boldFontWeight");
const [selected, setSelected] = useState<string>("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] ?? {}) };
+48 -3
Переглянути файл
@@ -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<number>("editor.fontWeight");
const [bold, setBold] = useSetting<number>("editor.boldFontWeight");
return (
<>
<Row label="Main font weight">
<Select
value={String(fontWeightOf(main, 400))}
options={WEIGHT_OPTIONS}
onChange={(v) => setMain(Number(v))}
aria-label="Main font weight"
/>
</Row>
<Row label="Bold font weight" hint="Used wherever the color scheme shows text in bold">
<Select
value={String(fontWeightOf(bold, 700))}
options={WEIGHT_OPTIONS}
onChange={(v) => setBold(Number(v))}
aria-label="Bold font weight"
/>
</Row>
</>
);
}
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<number>("editor.fontSize");
const [line] = useSetting<number>("editor.lineHeight");
const [ligatures] = useSetting<boolean>("editor.ligatures");
const [weight] = useSetting<number>("editor.fontWeight");
const [boldWeight] = useSetting<number>("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,
});
+2
Переглянути файл
@@ -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",
+6 -2
Переглянути файл
@@ -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;
+12
Переглянути файл
@@ -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);
+12 -2
Переглянути файл
@@ -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 };
}
/**
+1 -1
Переглянути файл
@@ -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 {
+2 -2
Переглянути файл
@@ -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": {
+41 -2
Переглянути файл
@@ -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", () => {
+6 -1
Переглянути файл
@@ -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, (c: string) => string> = {
underline: (c) => `underline ${c}`,
wave: (c) => `underline wavy ${c}`,
@@ -237,7 +242,7 @@ export function overrideDeclarations(overrides: Record<string, TextAttributesOve
}
if (attr.token) {
const t = attr.token;
if (o.bold !== undefined) out.push([`--syn-${t}-bold`, o.bold ? "700" : "400"]);
if (o.bold !== undefined) out.push([`--syn-${t}-bold`, boldWeight(o.bold)]);
if (o.italic !== undefined) {
out.push([`--syn-${t}-italic`, o.italic ? "italic" : "normal"]);
if (t === "comment") out.push(["--syn-comment-style", o.italic ? "italic" : "normal"]);
+34 -1
Переглянути файл
@@ -54,6 +54,9 @@ export const EDITOR_EXTRA_DEFAULTS = {
colorOverrides: {} as Record<string, Record<string, TextAttributesOverride>>,
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<T>(raw: unknown, def: T): T {
return def;
}
/** IDEA's named font weights (Typography Settings). */
export const FONT_WEIGHTS: ReadonlyArray<readonly [number, string]> = [
[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;
+11 -2
Переглянути файл
@@ -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;
+1
Переглянути файл
@@ -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);