Merge remote-tracking branch 'origin/main' into feat/search-everywhere-multi-select

This commit is contained in:
l0ng-ai committed 2026-11-01 16:04:31 +08:00
commit cf6e640ac0
11 files changed
+142 -38

No files matched your search

+17
View File
@@ -276,6 +276,8 @@ pub struct AppearanceSettings {
/// Apply `ui_font_family` / `ui_font_size` (otherwise the theme's).
pub use_custom_font: bool,
pub ui_font_family: String,
/// Font tried when `ui_font_family` lacks a glyph; empty = none.
pub ui_font_fallback: String,
pub contrast_scrollbars: bool,
/// Adjust colours for red-green vision deficiency.
pub red_green_vision: bool,
@@ -326,6 +328,7 @@ impl Default for AppearanceSettings {
zoom: 100,
use_custom_font: false,
ui_font_family: "Inter".into(),
ui_font_fallback: String::new(),
contrast_scrollbars: false,
red_green_vision: false,
different_tool_window_background: false,
@@ -721,6 +724,20 @@ impl Settings {
mod tests {
use super::*;
#[test]
fn ui_font_fallback_defaults_empty_and_round_trips() {
let old: Settings = serde_json::from_str(r#"{"appearance":{"useCustomFont":true}}"#).unwrap();
assert!(old.appearance.ui_font_fallback.is_empty());
for family in ["", "Songti SC", "Maple Mono NF CN"] {
let mut settings = Settings::default();
settings.appearance.ui_font_fallback = family.into();
let json = serde_json::to_value(&settings).unwrap();
assert_eq!(json["appearance"]["uiFontFallback"], family);
let restored: Settings = serde_json::from_value(json).unwrap();
assert_eq!(restored.appearance.ui_font_fallback, family);
}
}
#[test]
fn tool_window_badges_default_on_and_round_trip() {
let old: Settings = serde_json::from_str(r#"{"appearance":{"showToolWindowNames":true}}"#).unwrap();
+2
View File
@@ -1422,6 +1422,8 @@ export type AppearanceSettings = {
/** Apply `ui_font_family` / `ui_font_size` (otherwise the theme's). */
useCustomFont?: boolean,
uiFontFamily?: string,
/** Font tried when `ui_font_family` lacks a glyph; empty = none. */
uiFontFallback?: string,
contrastScrollbars?: boolean,
/** Adjust colours for red-green vision deficiency. */
redGreenVision?: boolean,
@@ -1,7 +1,8 @@
import { describe, expect, it } from "vitest";
import { formatPattern, formatTimestamp } from "@/platform/dateFormat";
import { resolveThemeId } from "@/workbench/theme";
import { nextZoom } from "../appearanceRuntime";
import { applyAppearanceOptions, nextZoom } from "../appearanceRuntime";
import { validateSettings } from "../model/schema";
import { validateAppearanceExtras, validateNotifications } from "../model/behaviorSchema";
describe("appearance & behavior settings", () => {
@@ -27,6 +28,27 @@ describe("appearance & behavior settings", () => {
expect(validateAppearanceExtras({ showToolWindowBadges: 1 }).showToolWindowBadges).toBe(true);
});
it("keeps the UI fallback font empty by default and trims a stored one", () => {
expect(validateAppearanceExtras({}).uiFontFallback).toBe("");
expect(validateAppearanceExtras({ uiFontFallback: " 思源黑体 " }).uiFontFallback).toBe("思源黑体");
expect(validateAppearanceExtras({ uiFontFallback: 5 }).uiFontFallback).toBe("");
});
it("applies and clears the UI fallback, and removes custom fonts when disabled", () => {
const root = document.createElement("div");
const settings = validateSettings({
appearance: { useCustomFont: true, uiFontFamily: "Menlo", uiFontFallback: "Songti SC" },
});
applyAppearanceOptions(settings, root);
expect(root.style.getPropertyValue("--font-ui")).toBe('"Menlo", "Songti SC", system-ui, sans-serif');
settings.appearance.uiFontFallback = "";
applyAppearanceOptions(settings, root);
expect(root.style.getPropertyValue("--font-ui")).toBe('"Menlo", system-ui, sans-serif');
settings.appearance.useCustomFont = false;
applyAppearanceOptions(settings, root);
expect(root.style.getPropertyValue("--font-ui")).toBe("");
});
it("formats dates with an override pattern and pretty dates", () => {
const d = new Date(2026, 0, 5, 14, 7);
expect(formatPattern(d, "dd.MM.yyyy")).toBe("05.01.2026");
@@ -222,6 +222,24 @@ describe("Settings dialog", () => {
]);
});
it("enables, saves and clears the custom UI fallback font", async () => {
const dialog = await openDialog("appearance");
const fallback = within(dialog).getByRole("combobox", { name: "UI fallback font" });
const custom = within(dialog).getByRole("checkbox", { name: "Use custom font:" });
expect(fallback).toBeDisabled();
fireEvent.click(custom);
expect(fallback).toBeEnabled();
fireEvent.change(fallback, { target: { value: "Songti SC" } });
expect(settingsSet).not.toHaveBeenCalled();
await act(async () => fireEvent.click(within(dialog).getByRole("button", { name: "Apply" })));
expect(settingsSet).toHaveBeenCalledWith("global", "appearance.uiFontFallback", JSON.stringify("Songti SC"));
fireEvent.change(fallback, { target: { value: "" } });
await act(async () => fireEvent.click(within(dialog).getByRole("button", { name: "Apply" })));
expect(settingsSet).toHaveBeenCalledWith("global", "appearance.uiFontFallback", JSON.stringify(""));
fireEvent.click(custom);
expect(fallback).toBeDisabled();
});
it("edits and saves independent custom font weights", async () => {
const dialog = await openDialog("editor.font");
for (const [label, value] of [
+1 -1
View File
@@ -33,7 +33,7 @@ export function applyAppearanceOptions(s: ValidSettings, root: HTMLElement = doc
if (size === DEFAULT_SETTINGS.appearance.uiFontSize) root.style.removeProperty(name);
else root.style.setProperty(name, `${size + delta}px`);
}
if (a.useCustomFont) root.style.setProperty("--font-ui", uiFontFamily(a.uiFontFamily));
if (a.useCustomFont) root.style.setProperty("--font-ui", uiFontFamily(a.uiFontFamily, a.uiFontFallback));
else root.style.removeProperty("--font-ui");
flag(root, "compact", a.compactMode);
@@ -20,6 +20,7 @@ export interface AppearanceExtras {
zoom: number;
useCustomFont: boolean;
uiFontFamily: string;
uiFontFallback: string;
contrastScrollbars: boolean;
redGreenVision: boolean;
differentToolWindowBackground: boolean;
@@ -72,6 +73,7 @@ export const DEFAULT_APPEARANCE_EXTRAS: AppearanceExtras = {
zoom: 100,
useCustomFont: false,
uiFontFamily: "Inter",
uiFontFallback: "",
contrastScrollbars: false,
redGreenVision: false,
differentToolWindowBackground: false,
@@ -123,6 +125,7 @@ export function validateAppearanceExtras(raw: unknown): AppearanceExtras {
zoom: int(a.zoom, d.zoom, ZOOM_LIMITS),
useCustomFont: b("useCustomFont"),
uiFontFamily: typeof a.uiFontFamily === "string" && a.uiFontFamily.trim() ? a.uiFontFamily.trim() : d.uiFontFamily,
uiFontFallback: typeof a.uiFontFallback === "string" ? a.uiFontFallback.trim() : d.uiFontFallback,
contrastScrollbars: b("contrastScrollbars"),
redGreenVision: b("redGreenVision"),
differentToolWindowBackground: b("differentToolWindowBackground"),
@@ -24,7 +24,7 @@
padding: 8px 14px;
}
/* "[x] Use custom font: [Inter] Size: [13]" */
/* Inline option groups, with custom font controls wrapping together. */
.fontRow {
display: flex;
align-items: center;
@@ -37,8 +37,17 @@
.fontInput {
width: 220px;
}
.sizeLabel {
margin-left: 8px;
.customFontRow {
flex-wrap: wrap;
}
.fontOption {
display: inline-flex;
align-items: center;
gap: 8px;
max-width: 100%;
}
.fontOption > label {
flex-shrink: 0;
}
.sizeInput {
width: 64px;
+43 -25
View File
@@ -181,35 +181,53 @@ function ZoomRow() {
function CustomFontRow() {
const [on, setOn] = useSetting<boolean>("appearance.useCustomFont");
const [family, setFamily] = useSetting<string>("appearance.uiFontFamily");
const [fallback, setFallback] = useSetting<string>("appearance.uiFontFallback");
const [size, setSize] = useSetting<number>("appearance.uiFontSize");
const id = useId();
const matched = useLabelMatch("Use custom font");
return (
<div className={clsx(styles.fontRow, matched && styles.matched)}>
<Checkbox checked={on} onChange={setOn} label={<SearchLabel text="Use custom font:" />} />
<FontPicker label="UI font" disabled={!on} value={family} onChange={setFamily} />
<label className={clsx(styles.sizeLabel, !on && styles.disabled)} htmlFor={`${id}-size`}>
<SearchLabel text="Size:" />
</label>
{on ? (
<NumberInput
id={`${id}-size`}
value={size}
onChange={setSize}
{...LIMITS.uiFontSize}
integer
aria-label="UI font size"
<div className={clsx(styles.fontRow, styles.customFontRow, matched && styles.matched)}>
<span className={styles.fontOption}>
<Checkbox checked={on} onChange={setOn} label={<SearchLabel text="Use custom font:" />} />
<FontPicker label="UI font" disabled={!on} value={family} onChange={setFamily} />
</span>
<span className={styles.fontOption}>
<label className={clsx(!on && styles.disabled)} htmlFor={`${id}-fallback`}>
<SearchLabel text="Fallback font:" />
</label>
<FontPicker
id={`${id}-fallback`}
label="UI fallback font"
disabled={!on}
value={fallback}
onChange={setFallback}
allowEmpty
/>
) : (
<Input
id={`${id}-size`}
className={styles.sizeInput}
value={String(size)}
disabled
aria-label="UI font size"
readOnly
/>
)}
</span>
<span className={styles.fontOption}>
<label className={clsx(!on && styles.disabled)} htmlFor={`${id}-size`}>
<SearchLabel text="Size:" />
</label>
{on ? (
<NumberInput
id={`${id}-size`}
value={size}
onChange={setSize}
{...LIMITS.uiFontSize}
integer
aria-label="UI font size"
/>
) : (
<Input
id={`${id}-size`}
className={styles.sizeInput}
value={String(size)}
disabled
aria-label="UI font size"
readOnly
/>
)}
</span>
</div>
);
}
@@ -531,7 +549,7 @@ export const BEHAVIOR_PAGES: PageSpec[] = [
{
kind: "custom",
label: "Use custom font",
keywords: ["UI font size", "Size", "font family"],
keywords: ["UI font size", "Size", "font family", "fallback font"],
component: CustomFontRow,
},
{ kind: "toggle", key: "appearance.contrastScrollbars", label: "Use contrast scrollbars" },
+17 -5
View File
@@ -36,11 +36,13 @@ it("escapes CSS special characters without turning a family into a list", () =>
describe("uiFontFamily", () => {
it.each([
["Inter", '"Inter Variable", "Inter", system-ui, sans-serif'],
["JetBrains Mono", '"JetBrains Mono Variable", "JetBrains Mono", system-ui, sans-serif'],
[' "PingFang SC" ', '"PingFang SC", system-ui, sans-serif'],
])("%j", (family, expected) => {
expect(uiFontFamily(family)).toBe(expected);
["Inter", "", '"Inter Variable", "Inter", system-ui, sans-serif'],
["JetBrains Mono", "", '"JetBrains Mono Variable", "JetBrains Mono", system-ui, sans-serif'],
[' "PingFang SC" ', "", '"PingFang SC", system-ui, sans-serif'],
["Inter", "思源黑体", '"Inter Variable", "Inter", "思源黑体", system-ui, sans-serif'],
["Inter", " ", '"Inter Variable", "Inter", system-ui, sans-serif'],
])("%j + %j", (family, fallback, expected) => {
expect(uiFontFamily(family, fallback)).toBe(expected);
});
});
@@ -126,6 +128,16 @@ describe("macOS user font resolver", () => {
expect(invoke).not.toHaveBeenCalled();
});
it("loads a UI fallback through the shared macOS user font resolver", async () => {
const { fonts, invoke, registered } = await resolver(false);
const family = fonts.uiFontFamily("Inter", "Maple Mono NF CN");
expect(family).toBe('"Inter Variable", "Inter", "OpenIDEA User Font 0", "Maple Mono NF CN", system-ui, sans-serif');
expect(fonts.monoFontFamily("Inter", "Maple Mono NF CN")).toContain('"OpenIDEA User Font 0"');
await fonts.fontsLoaded(family, 13);
expect(invoke).toHaveBeenCalledExactlyOnceWith("user_font_data", { family: "Maple Mono NF CN" });
expect(registered).toHaveLength(3);
});
it("detects user fonts that WKWebView exposes only to connected canvases without loading bytes", async () => {
const { fonts, invoke, canvases } = await resolver("connected");
expect(fonts.fontAvailable("Maple Mono NF CN")).toBe(true);
+4 -3
View File
@@ -140,7 +140,8 @@ export async function fontsLoaded(family: string, size: number): Promise<void> {
}
}
/** Bundled aliases also apply when selected for the interface. */
export function uiFontFamily(family: string): string {
return [...familyFaces(family), "system-ui", "sans-serif"].join(", ");
/** Bundled aliases also apply when selected for the interface; `fallback` covers glyphs `family` lacks (empty = none). */
export function uiFontFamily(family: string, fallback = ""): string {
const fb = familyName(fallback);
return [...familyFaces(family), ...(fb ? familyFaces(fb) : []), "system-ui", "sans-serif"].join(", ");
}
+2
View File
@@ -760,6 +760,7 @@
"Failed": "失败",
"Failed to restart language server": "重启语言服务器失败",
"Fallback font": "备用字体",
"Fallback font:": "备用字体:",
"Fetch": "获取",
"Fetches remotes in the background so incoming commits show in the Branches popup. Auto stops after an authentication failure.": "在后台获取远程,以便传入的提交显示在“分支”弹出窗口中。“自动”在身份验证失败后停止。",
"Field access": "字段访问",
@@ -2223,6 +2224,7 @@
"Merged with main toolbar": "与主工具栏合并",
"UI Options": "UI 选项",
"UI elements take up less screen space": "UI 元素占用更少的屏幕空间",
"UI fallback font": "UI 备用字体",
"UI font": "UI 字体",
"UI font size": "UI 字体大小",
"Uninstalled {name}": "已卸载 {name}",