mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 08:52:12 +00:00
Merge remote-tracking branch 'origin/main' into feat/search-everywhere-multi-select
This commit is contained in:
commit
cf6e640ac0
11 files changed
+142
-38
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(", ");
|
||||
}
|
||||
@@ -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}",
|
||||
|
||||
Reference in new issue
Block a user