mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-07 10:16:03 -10:00
feat(settings): restructure the color scheme page into JetBrains-style sub-pages (#1734)
* feat(settings): JetBrains-style color scheme sub-pages Move General, Debugger, Language Defaults and each language onto its own page under Color Scheme, drop the redundant scope root from the attribute tree, and turn the Color Scheme root page into a landing with links to the sub-pages. * feat(settings): per-scope preview samples with click-to-reveal Give every scope page a code sample covering as much of its token set as possible, render bg-only attributes in the preview, and let a click on a preview token select its attribute and expand its tree ancestors. * refactor(settings): fold attribute name groups into collapsible tree levels Split the "›" groups of an attribute display name (Identifiers, String, Comments, ...) into collapsible intermediate nodes so the tree matches IDEA instead of repeating the group in every leaf label. * feat(editor): preview the scheme's actual attribute state in settings Attribute controls now mirror the scheme's effective appearance instead of only showing user overrides: checkboxes and colours resolve imported scheme values, theme tokens and built-in defaults. Removing an inherited colour stores an explicit transparent override instead of falling back silently, and redundant overrides matching the inherited value are dropped. Also drops the stale "Incoming commits" catalog key. * feat(settings): move the effect style onto its own right-aligned row Put the effect checkbox and its colour on one line and the style select on a narrower row below, right-aligned with the hex fields, which now stretch to the panel edge. * refactor(settings): render attribute tree labels in the plain font Drop the per-token styling of the tree labels; IDEA lists them as normal text, the preview keeps the colours. * feat(settings): scroll the attribute tree to a preview-clicked token Preview clicks now also reveal the attribute: after the ancestors expand, the tree selects the row and centres it in the viewport. * feat(settings): mark overridden attributes in blue up to the root Show overridden attributes and every group containing one in the accent colour, like IDEA, instead of a dot on the leaf alone. * feat(settings): use the JetBrains demo as the Python preview and reveal cross-page tokens Replace the Python sample with the classic syntax-highlighting demo (semantic spectrum, f-strings, byte strings, escapes) while keeping coverage for every Python attribute. Preview tokens outside the current sub-page now navigate to their owning page and reveal the attribute there. * fix(settings): keep locale entries, English preview text and default styles in the colour scheme preview - Restore the "Incoming commits" translations that were dropped from other locales - Replace the Chinese docstring lines in the Python preview with English text - Use the existing Java operator attribute for separators in the Java preview - Render default bold, italic and underline in the preview, matching the editor --------- Co-authored-by: l0ng-ai <[email protected]>
This commit is contained in:
1 parent
e367f6757e
commit
bbbdcb465e
13 files changed
+1338
-173
No files matched your search
@@ -1,12 +1,13 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { act, render, screen, waitFor } from "@testing-library/react";
|
||||
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { executeAction } from "@/platform/actions";
|
||||
import { CLEAR_COLOR } from "@/platform/colorScheme";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { DEFAULT_SETTINGS } from "../model/schema";
|
||||
import { openSettings, stage, stagedValue, useSettingsDialog } from "../model/staging";
|
||||
import { PageContext } from "../components/context";
|
||||
import { COLOR_SCHEME_PAGE } from "../pages/editor/colorScheme";
|
||||
import { COLOR_SCHEME_PAGES } from "../pages/editor/colorScheme";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
open: vi.fn(),
|
||||
@@ -28,6 +29,15 @@ const run = (name: string) =>
|
||||
act(async () => {
|
||||
await executeAction(`editor.colorScheme.${name}`);
|
||||
});
|
||||
const schemePage = (id: string) => {
|
||||
const page = COLOR_SCHEME_PAGES.find((p) => p.id === id)!;
|
||||
const Page = page.component!;
|
||||
render(
|
||||
<PageContext.Provider value={{ pageId: page.id, scope: "project" }}>
|
||||
<Page />
|
||||
</PageContext.Provider>,
|
||||
);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
@@ -40,15 +50,10 @@ beforeEach(() => {
|
||||
mocks.write.mockResolvedValue({ status: "ok", data: null });
|
||||
useProject.setState({ settings: structuredClone(DEFAULT_SETTINGS), project: null });
|
||||
openSettings("editor.colorScheme");
|
||||
const Page = COLOR_SCHEME_PAGE.component!;
|
||||
render(
|
||||
<PageContext.Provider value={{ pageId: "editor.colorScheme", scope: "project" }}>
|
||||
<Page />
|
||||
</PageContext.Provider>,
|
||||
);
|
||||
});
|
||||
|
||||
describe("scheme actions", () => {
|
||||
beforeEach(() => schemePage("editor.colorScheme"));
|
||||
it("imports and selects in global scope, exports and confirms deletion", async () => {
|
||||
await run("import");
|
||||
const id = value("colorScheme") as string;
|
||||
@@ -114,3 +119,115 @@ describe("scheme actions", () => {
|
||||
expect(useSettingsDialog.getState().staged).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe("scheme attribute state", () => {
|
||||
beforeEach(() => schemePage("editor.colorScheme.language-defaults"));
|
||||
it("shows checked attributes and inherited colours from the selected scheme", () => {
|
||||
act(() => {
|
||||
stage("editor.colorScheme", "global", "editor.customSchemes", {
|
||||
"custom-test": {
|
||||
name: "Test",
|
||||
parent: "dark",
|
||||
attributes: {
|
||||
keyword: {
|
||||
foreground: "#139839",
|
||||
bold: true,
|
||||
italic: false,
|
||||
effect: "wave",
|
||||
effectColor: "#abcdef",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
stage("editor.colorScheme", "global", "editor.colorScheme", "custom-test");
|
||||
});
|
||||
|
||||
expect(screen.getByRole("checkbox", { name: "Bold" })).toBeChecked();
|
||||
expect(screen.getByRole("checkbox", { name: "Italic" })).not.toBeChecked();
|
||||
expect(screen.getByRole("textbox", { name: "Foreground hex" })).toHaveValue("#139839");
|
||||
expect(screen.queryByRole("textbox", { name: "Background hex" })).not.toBeInTheDocument();
|
||||
expect(screen.getByRole("checkbox", { name: "Effects" })).toBeChecked();
|
||||
expect(screen.getByRole("button", { name: "Effect" })).toHaveTextContent("Underwaved");
|
||||
expect(screen.getByRole("textbox", { name: "Effect color hex" })).toHaveValue("#abcdef");
|
||||
});
|
||||
|
||||
it("selects an empty background and clears it back to the scheme state", () => {
|
||||
const background = screen.getByRole("checkbox", { name: "Background" });
|
||||
act(() => fireEvent.click(background));
|
||||
|
||||
expect(background).toBeChecked();
|
||||
expect(screen.getByRole("textbox", { name: "Background hex" })).toHaveValue("#808080");
|
||||
expect(Object.values(value("colorOverrides") as object)).toEqual([{ keyword: { background: "#808080" } }]);
|
||||
|
||||
act(() => fireEvent.click(background));
|
||||
|
||||
expect(background).not.toBeChecked();
|
||||
expect(screen.queryByRole("textbox", { name: "Background hex" })).not.toBeInTheDocument();
|
||||
expect(value("colorOverrides")).toEqual({});
|
||||
expect(useSettingsDialog.getState().staged["global|editor.colorOverrides"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("clears a background inherited from the selected scheme", () => {
|
||||
act(() => {
|
||||
stage("editor.colorScheme", "global", "editor.customSchemes", {
|
||||
"custom-test": {
|
||||
name: "Test",
|
||||
parent: "dark",
|
||||
attributes: { keyword: { background: "#2b2d30" } },
|
||||
},
|
||||
});
|
||||
stage("editor.colorScheme", "global", "editor.colorScheme", "custom-test");
|
||||
});
|
||||
|
||||
const background = screen.getByRole("checkbox", { name: "Background" });
|
||||
expect(background).toBeChecked();
|
||||
expect(background).toBeEnabled();
|
||||
expect(screen.getByRole("textbox", { name: "Background hex" })).toHaveValue("#2b2d30");
|
||||
|
||||
act(() => fireEvent.click(background));
|
||||
expect(background).not.toBeChecked();
|
||||
expect(screen.queryByRole("textbox", { name: "Background hex" })).not.toBeInTheDocument();
|
||||
expect(Object.values(value("colorOverrides") as object)).toEqual([{ keyword: { background: CLEAR_COLOR } }]);
|
||||
|
||||
act(() => fireEvent.click(background));
|
||||
expect(background).toBeChecked();
|
||||
expect(screen.getByRole("textbox", { name: "Background hex" })).toHaveValue("#2b2d30");
|
||||
expect(value("colorOverrides")).toEqual({});
|
||||
});
|
||||
|
||||
it("removes an effect override when the scheme value is selected again", () => {
|
||||
act(() => {
|
||||
stage("editor.colorScheme", "global", "editor.customSchemes", {
|
||||
"custom-test": {
|
||||
name: "Test",
|
||||
parent: "dark",
|
||||
attributes: { keyword: { effect: "wave", effectColor: "#abcdef" } },
|
||||
},
|
||||
});
|
||||
stage("editor.colorScheme", "global", "editor.colorScheme", "custom-test");
|
||||
});
|
||||
|
||||
const effects = screen.getByRole("checkbox", { name: "Effects" });
|
||||
act(() => fireEvent.click(effects));
|
||||
expect(value("colorOverrides")).not.toEqual({});
|
||||
|
||||
act(() => fireEvent.click(effects));
|
||||
expect(effects).toBeChecked();
|
||||
expect(value("colorOverrides")).toEqual({});
|
||||
expect(useSettingsDialog.getState().staged["global|editor.colorOverrides"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("clears a newly selected effect back to an unmodified state", () => {
|
||||
const effects = screen.getByRole("checkbox", { name: "Effects" });
|
||||
expect(effects).not.toBeChecked();
|
||||
|
||||
act(() => fireEvent.click(effects));
|
||||
expect(effects).toBeChecked();
|
||||
expect(value("colorOverrides")).not.toEqual({});
|
||||
|
||||
act(() => fireEvent.click(effects));
|
||||
expect(effects).not.toBeChecked();
|
||||
expect(value("colorOverrides")).toEqual({});
|
||||
expect(useSettingsDialog.getState().staged["global|editor.colorOverrides"]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -172,10 +172,19 @@ export interface SelectProps<V extends string> {
|
||||
onChange: (value: V) => void;
|
||||
disabled?: boolean;
|
||||
onPreview?: (value: V | null) => void;
|
||||
className?: string;
|
||||
"aria-label"?: string;
|
||||
}
|
||||
|
||||
export function Select<V extends string>({ value, options, onChange, disabled, onPreview, ...aria }: SelectProps<V>) {
|
||||
export function Select<V extends string>({
|
||||
value,
|
||||
options,
|
||||
onChange,
|
||||
disabled,
|
||||
onPreview,
|
||||
className,
|
||||
...aria
|
||||
}: SelectProps<V>) {
|
||||
return (
|
||||
<UiSelect<V>
|
||||
value={value}
|
||||
@@ -183,7 +192,7 @@ export function Select<V extends string>({ value, options, onChange, disabled, o
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
onPreview={onPreview}
|
||||
className={styles.select}
|
||||
className={clsx(styles.select, className)}
|
||||
aria-label={aria["aria-label"]}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -153,19 +153,57 @@
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
/* Landing links of the Color Scheme root page (JetBrains-style sub-page list). */
|
||||
.schemeLinks {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 2px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.schemeLink {
|
||||
border: 0;
|
||||
background: none;
|
||||
padding: 2px 4px;
|
||||
border-radius: var(--radius-small);
|
||||
color: var(--accent);
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
.schemeLink:hover {
|
||||
background: var(--bg-hover);
|
||||
text-decoration: underline;
|
||||
}
|
||||
.schemeBody {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 300px) minmax(0, 1fr);
|
||||
gap: 16px;
|
||||
min-height: 300px;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(220px, 260px);
|
||||
gap: 14px;
|
||||
min-height: 320px;
|
||||
}
|
||||
.attrTree {
|
||||
max-height: 340px;
|
||||
max-height: 360px;
|
||||
overflow: auto;
|
||||
border-radius: var(--radius-small);
|
||||
box-shadow: inset 0 0 0 1px var(--border-strong);
|
||||
padding: 2px 0;
|
||||
}
|
||||
.treeLabel {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
/* Overridden attributes and their ancestor groups show in accent, up to the root. */
|
||||
.attrModified {
|
||||
color: var(--accent);
|
||||
}
|
||||
.attrPanel {
|
||||
box-shadow: inset 0 0 0 1px var(--border-strong);
|
||||
border-radius: var(--radius-small);
|
||||
padding: 10px;
|
||||
align-self: start;
|
||||
overflow: auto;
|
||||
max-height: 360px;
|
||||
}
|
||||
.attrEditor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -180,8 +218,16 @@
|
||||
.attrRow > :first-child {
|
||||
min-width: 110px;
|
||||
}
|
||||
/* Effect style select on its own row, right-aligned with the colour hex fields. */
|
||||
.effectSelect {
|
||||
align-self: flex-end;
|
||||
min-width: 0;
|
||||
width: 150px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.swatchInput {
|
||||
width: 90px;
|
||||
flex: 1 1 90px;
|
||||
min-width: 90px;
|
||||
}
|
||||
.colorBox {
|
||||
position: relative;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -2,7 +2,7 @@
|
||||
import type { PageSpec } from "../spec";
|
||||
import { GENERAL_PAGES } from "./general";
|
||||
import { FONT_PAGE } from "./font";
|
||||
import { COLOR_SCHEME_PAGE } from "./colorScheme";
|
||||
import { COLOR_SCHEME_PAGES } from "./colorScheme";
|
||||
import { CODE_STYLE_PAGES } from "./codeStyle";
|
||||
import { INSPECTIONS_PAGE } from "./inspections";
|
||||
import { NATURAL_LANGUAGES_PAGE, SPELLING_PAGE } from "./spelling";
|
||||
@@ -12,7 +12,7 @@ export { codeStyleLanguagePages } from "./codeStyle";
|
||||
export const EDITOR_PAGES: PageSpec[] = [
|
||||
...GENERAL_PAGES,
|
||||
FONT_PAGE,
|
||||
COLOR_SCHEME_PAGE,
|
||||
...COLOR_SCHEME_PAGES,
|
||||
...CODE_STYLE_PAGES,
|
||||
INSPECTIONS_PAGE,
|
||||
NATURAL_LANGUAGES_PAGE,
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
import { _setSchemeSource, schemeAfterThemeChange, schemeCss, schemeTokens } from "./colorScheme";
|
||||
import {
|
||||
CLEAR_COLOR,
|
||||
_setSchemeSource,
|
||||
resolvedAttribute,
|
||||
schemeAfterThemeChange,
|
||||
schemeCss,
|
||||
schemeTokens,
|
||||
} from "./colorScheme";
|
||||
import { fontWeightVars, validateEditorExtras } from "./editorSettings";
|
||||
import type { Settings } from "./ipc";
|
||||
import { useProject } from "./project";
|
||||
@@ -37,6 +44,35 @@ describe("color schemes", () => {
|
||||
expect(imported).toMatch(/\.cm-editor\[data-language="PHP"\] \{\n {2}--syn-variable: #48d8ee !important;/);
|
||||
});
|
||||
|
||||
it("resolves scheme attribute state and editor defaults", () => {
|
||||
expect(resolvedAttribute("light", "lineComment")).toMatchObject({
|
||||
foreground: "#8c8c8c",
|
||||
italic: true,
|
||||
});
|
||||
expect(resolvedAttribute("dark", "lineComment")).toMatchObject({
|
||||
foreground: "#7a7e85",
|
||||
italic: false,
|
||||
});
|
||||
expect(resolvedAttribute("dark", "mdHeading")).toMatchObject({ bold: true });
|
||||
|
||||
const customs = {
|
||||
"custom-test": {
|
||||
name: "Test",
|
||||
parent: "dark" as const,
|
||||
attributes: {
|
||||
todo: { foreground: "#139839", background: "#101010", bold: true, italic: false, effect: "wave" as const },
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(resolvedAttribute("custom-test", "todo", customs)).toMatchObject({
|
||||
foreground: "#139839",
|
||||
background: "#101010",
|
||||
bold: true,
|
||||
italic: false,
|
||||
effect: "wave",
|
||||
});
|
||||
});
|
||||
|
||||
it("emits scheme and override declarations", () => {
|
||||
const root = document.documentElement;
|
||||
root.dataset.theme = "light";
|
||||
@@ -49,6 +85,8 @@ describe("color schemes", () => {
|
||||
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");
|
||||
const cleared = schemeCss("theme-default", { light: { keyword: { background: CLEAR_COLOR } } }, root);
|
||||
expect(cleared).toContain("--syn-keyword-bg: transparent !important");
|
||||
const upright = schemeCss("theme-default", { light: { staticSymbol: { italic: false } } }, root);
|
||||
expect(upright).toContain("--syn-static-italic: normal !important");
|
||||
const pythonItalic = schemeCss("theme-default", { light: { pyStaticSymbol: { italic: true } } }, root);
|
||||
|
||||
+94
-12
@@ -31,17 +31,26 @@ export interface AttributeDef {
|
||||
token?: string;
|
||||
/** A semantic modifier whose only configurable attribute is italic. */
|
||||
italicOnly?: boolean;
|
||||
defaultBold?: boolean;
|
||||
defaultItalic?: boolean;
|
||||
defaultEffect?: TextAttributesOverride["effect"];
|
||||
/** CodeMirror language name; unset attributes are language defaults. */
|
||||
language?: string;
|
||||
}
|
||||
|
||||
const syn = (id: string, name: string, path: string[], token: string): AttributeDef => ({
|
||||
const syn = (
|
||||
id: string,
|
||||
name: string,
|
||||
path: string[],
|
||||
token: string,
|
||||
defaults: Pick<AttributeDef, "defaultBold" | "defaultItalic" | "defaultEffect"> = {},
|
||||
): AttributeDef => ({
|
||||
id,
|
||||
name,
|
||||
path,
|
||||
token,
|
||||
fg: `--syn-${token}`,
|
||||
...defaults,
|
||||
});
|
||||
|
||||
const LD = "Language Defaults";
|
||||
@@ -116,7 +125,7 @@ const BASE_ATTRIBUTES: readonly AttributeDef[] = [
|
||||
syn("identifier", "Identifiers › Local variable", [LD], "variable"),
|
||||
syn("parameter", "Identifiers › Parameter", [LD], "param"),
|
||||
syn("globalVariable", "Identifiers › Global variable", [LD], "global"),
|
||||
syn("constant", "Identifiers › Constant", [LD], "constant"),
|
||||
syn("constant", "Identifiers › Constant", [LD], "constant", { defaultItalic: true }),
|
||||
syn("field", "Identifiers › Instance field", [LD], "property"),
|
||||
{ id: "staticSymbol", name: "Identifiers › Static symbol", path: [LD], token: "static", italicOnly: true },
|
||||
syn("functionDeclaration", "Identifiers › Function declaration", [LD], "function"),
|
||||
@@ -127,7 +136,7 @@ const BASE_ATTRIBUTES: readonly AttributeDef[] = [
|
||||
syn("string", "String › String text", [LD], "string"),
|
||||
syn("escape", "String › Valid escape sequence", [LD], "escape"),
|
||||
syn("lineComment", "Comments › Line comment", [LD], "comment"),
|
||||
syn("docComment", "Comments › Doc comment", [LD], "doc-comment"),
|
||||
syn("docComment", "Comments › Doc comment", [LD], "doc-comment", { defaultItalic: true }),
|
||||
syn("metadata", "Metadata", [LD], "decorator"),
|
||||
syn("operator", "Braces and Operators", [LD], "punct"),
|
||||
syn("markupTag", "Markup › Tag", [LD], "tag"),
|
||||
@@ -156,7 +165,7 @@ const BASE_ATTRIBUTES: readonly AttributeDef[] = [
|
||||
defaultItalic: false,
|
||||
},
|
||||
syn("pyKeywordArg", "Keyword argument", ["Python"], "keyword-arg"),
|
||||
syn("pyDocstring", "Docstring", ["Python"], "docstring"),
|
||||
syn("pyDocstring", "Docstring", ["Python"], "docstring", { defaultItalic: true }),
|
||||
syn("pyBuiltin", "Built-in name", ["Python"], "python-builtin"),
|
||||
syn("pyDecorator", "Decorator", ["Python"], "annotation"),
|
||||
syn("pyLocalVariable", "Local variable", ["Python"], "python-local-variable"),
|
||||
@@ -166,9 +175,9 @@ const BASE_ATTRIBUTES: readonly AttributeDef[] = [
|
||||
syn("pyConstant", "Constant", ["Python"], "python-constant"),
|
||||
syn("pyProperty", "Property", ["Python"], "python-property"),
|
||||
syn("pyTypeParameter", "Type parameter", ["Python"], "python-type-parameter"),
|
||||
syn("mdHeading", "Heading", ["Markdown"], "markup-heading"),
|
||||
syn("mdHeading", "Heading", ["Markdown"], "markup-heading", { defaultBold: true }),
|
||||
syn("mdCode", "Code span", ["Markdown"], "markup-code"),
|
||||
syn("mdLink", "Link", ["Markdown"], "link"),
|
||||
syn("mdLink", "Link", ["Markdown"], "link", { defaultEffect: "underline" }),
|
||||
syn("regexp", "Regular expression", ["RegExp"], "regexp"),
|
||||
];
|
||||
|
||||
@@ -341,6 +350,11 @@ const EFFECT_CSS: Record<string, (c: string) => string> = {
|
||||
none: () => "none",
|
||||
};
|
||||
|
||||
/** Explicitly removes a scheme colour; unlike an unset field, this must survive as an override. */
|
||||
export const CLEAR_COLOR = "transparent";
|
||||
|
||||
const setOrCleared = (value: string | undefined) => isHexColor(value) || value === CLEAR_COLOR;
|
||||
|
||||
/** Declarations for the overrides of one scheme. */
|
||||
export function overrideDeclarations(
|
||||
overrides: Record<string, TextAttributesOverride> | undefined,
|
||||
@@ -352,10 +366,10 @@ export function overrideDeclarations(
|
||||
if (attr.language !== language) continue;
|
||||
const o = overrides[attr.id];
|
||||
if (!o) continue;
|
||||
if (isHexColor(o.foreground) && attr.fg) out.push([attr.fg, o.foreground]);
|
||||
if (isHexColor(o.background) && attr.bg) out.push([attr.bg, o.background]);
|
||||
if (isHexColor(o.effectColor) && attr.effect) out.push([attr.effect, o.effectColor]);
|
||||
if (attr.effect && isHexColor(o.effectColor)) {
|
||||
if (setOrCleared(o.foreground) && attr.fg) out.push([attr.fg, o.foreground]);
|
||||
if (setOrCleared(o.background) && attr.bg) out.push([attr.bg, o.background]);
|
||||
if (setOrCleared(o.effectColor) && attr.effect) out.push([attr.effect, o.effectColor]);
|
||||
if (attr.effect && setOrCleared(o.effectColor)) {
|
||||
if (attr.id === "error") out.push(["--editor-error-decoration", `underline wavy ${o.effectColor}`]);
|
||||
if (attr.id === "warning") out.push(["--editor-warning-decoration", `underline wavy ${o.effectColor}`]);
|
||||
}
|
||||
@@ -366,11 +380,11 @@ export function overrideDeclarations(
|
||||
out.push([`--syn-${t}-italic`, o.italic ? "italic" : "normal"]);
|
||||
if (t === "comment") out.push(["--syn-comment-style", o.italic ? "italic" : "normal"]);
|
||||
}
|
||||
if (isHexColor(o.background)) out.push([`--syn-${t}-bg`, o.background]);
|
||||
if (setOrCleared(o.background)) out.push([`--syn-${t}-bg`, o.background]);
|
||||
if (o.effect)
|
||||
out.push([
|
||||
`--syn-${t}-effect`,
|
||||
EFFECT_CSS[o.effect]?.(isHexColor(o.effectColor) ? o.effectColor : "currentColor") ?? "none",
|
||||
EFFECT_CSS[o.effect]?.(setOrCleared(o.effectColor) ? o.effectColor! : "currentColor") ?? "none",
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -418,6 +432,74 @@ export function resolvedSchemeTokens(id: string, customs: CustomColorSchemes = {
|
||||
return tokens;
|
||||
}
|
||||
|
||||
const hexIn = (value: string | undefined): string | undefined =>
|
||||
value?.match(/#(?:[0-9a-f]{3}|[0-9a-f]{6})\b/i)?.[0]?.toLowerCase();
|
||||
|
||||
const effectFromCss = (value: string | undefined): TextAttributesOverride["effect"] => {
|
||||
if (!value) return undefined;
|
||||
if (/line-through/i.test(value)) return "strikeout";
|
||||
if (/wavy/i.test(value)) return "wave";
|
||||
if (/underline/i.test(value)) return "underline";
|
||||
if (/none/i.test(value)) return "none";
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const boldFromCss = (value: string | undefined): boolean | undefined => {
|
||||
if (!value) return undefined;
|
||||
if (/bold|[6-9]00/i.test(value)) return true;
|
||||
if (/normal|[1-5]00/i.test(value)) return false;
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const italicFromCss = (value: string | undefined): boolean | undefined => {
|
||||
if (!value) return undefined;
|
||||
if (/italic|oblique/i.test(value)) return true;
|
||||
if (/normal/i.test(value)) return false;
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* The attributes an editor shows for a scheme before user overrides: imported
|
||||
* values where the scheme sets them, otherwise the theme token and the editor's
|
||||
* built-in defaults.
|
||||
*/
|
||||
export function resolvedAttribute(
|
||||
id: string,
|
||||
attrId: string,
|
||||
customs: CustomColorSchemes = {},
|
||||
): TextAttributesOverride {
|
||||
const attr = ATTRIBUTES.find((a) => a.id === attrId);
|
||||
if (!attr) return {};
|
||||
const custom = customSchemes(customs)[id];
|
||||
const own = custom?.attributes[attr.id];
|
||||
const vars = resolvedSchemeTokens(id, customs);
|
||||
if (attr.language)
|
||||
for (const [key, value] of overrideDeclarations(custom?.attributes, attr.language)) vars.set(key, value);
|
||||
|
||||
const token = attr.token;
|
||||
const foreground = attr.fg ? vars.get(attr.fg) : undefined;
|
||||
const background = attr.bg ? vars.get(attr.bg) : token ? vars.get(`--syn-${token}-bg`) : undefined;
|
||||
const effectCss = attr.effect ? vars.get(attr.effect) : token ? vars.get(`--syn-${token}-effect`) : undefined;
|
||||
const effect = own?.effect ?? effectFromCss(effectCss) ?? attr.defaultEffect;
|
||||
const out: TextAttributesOverride = {};
|
||||
if (isHexColor(foreground)) out.foreground = foreground.toLowerCase();
|
||||
if (isHexColor(background)) out.background = background.toLowerCase();
|
||||
if (own?.effectColor && isHexColor(own.effectColor)) out.effectColor = own.effectColor.toLowerCase();
|
||||
else {
|
||||
const effectColor = hexIn(effectCss);
|
||||
if (effectColor) out.effectColor = effectColor;
|
||||
}
|
||||
if (token) {
|
||||
if (!attr.italicOnly)
|
||||
out.bold = own?.bold ?? boldFromCss(vars.get(`--syn-${token}-bold`)) ?? attr.defaultBold ?? false;
|
||||
const italicCss =
|
||||
vars.get(`--syn-${token}-italic`) ?? (token === "comment" ? vars.get("--syn-comment-style") : undefined);
|
||||
out.italic = own?.italic ?? italicFromCss(italicCss) ?? attr.defaultItalic ?? false;
|
||||
} else if (effect) out.effect = effect;
|
||||
if (token && effect) out.effect = effect;
|
||||
return out;
|
||||
}
|
||||
|
||||
const SAFE = /^[^;{}<>]*$/;
|
||||
|
||||
/** Stylesheet text for the chosen scheme and its overrides ("" when the theme's own is used unchanged). */
|
||||
|
||||
@@ -2340,7 +2340,6 @@
|
||||
"UI font size": "Tamaño de fuente de la interfaz",
|
||||
"Uninstalled {name}": "Se desinstaló {name}",
|
||||
"URL:": "URL:",
|
||||
"Unchecked values are inherited from the scheme.": "Los valores desmarcados se heredan del esquema.",
|
||||
"Uncommitted changes are stashed before the update and restored afterwards.": "Los cambios sin confirmar se guardan en el stash antes de la actualización y se restauran después.",
|
||||
"Underline": "Subrayado",
|
||||
"Underscored": "Subrayado simple",
|
||||
|
||||
@@ -2237,7 +2237,6 @@
|
||||
"UI font size": "Taille de police de l'interface",
|
||||
"Uninstalled {name}": "{name} désinstallé",
|
||||
"URL:": "URL :",
|
||||
"Unchecked values are inherited from the scheme.": "Les valeurs non cochées sont héritées du schéma.",
|
||||
"Uncommitted changes are stashed before the update and restored afterwards.": "Les modifications non validées sont remisées avant la mise à jour et restaurées ensuite.",
|
||||
"Underline": "Souligné",
|
||||
"Underscored": "Souligné d'un trait",
|
||||
|
||||
@@ -2396,7 +2396,6 @@
|
||||
"UI font": "UI 글꼴",
|
||||
"UI font size": "UI 글꼴 크기",
|
||||
"Uninstalled {name}": "{name} 제거됨",
|
||||
"Unchecked values are inherited from the scheme.": "선택 해제된 값은 구성표에서 상속됩니다.",
|
||||
"Uncommitted changes are stashed before the update and restored afterwards.": "커밋되지 않은 변경 사항은 업데이트 전에 스태시되었다가 이후에 복원됩니다.",
|
||||
"Underline": "밑줄",
|
||||
"Underscored": "밑줄 표시",
|
||||
|
||||
@@ -2319,7 +2319,6 @@
|
||||
"UI font size": "Rozmiar czcionki interfejsu użytkownika",
|
||||
"Uninstalled {name}": "Odinstalowano {name}",
|
||||
"URL:": "Adres URL:",
|
||||
"Unchecked values are inherited from the scheme.": "Niezaznaczone wartości są dziedziczone ze schematu.",
|
||||
"Uncommitted changes are stashed before the update and restored afterwards.": "Niezatwierdzone zmiany są odkładane do schowka przed aktualizacją i przywracane po jej zakończeniu.",
|
||||
"Underline": "Podkreślenie",
|
||||
"Underscored": "Podkreślony równo",
|
||||
|
||||
@@ -2320,7 +2320,6 @@
|
||||
"UI font size": "Tamanho da fonte da interface",
|
||||
"Uninstalled {name}": "{name} desinstalado",
|
||||
"URL:": "URL:",
|
||||
"Unchecked values are inherited from the scheme.": "Os valores desmarcados são herdados do esquema.",
|
||||
"Uncommitted changes are stashed before the update and restored afterwards.": "As alterações não commitadas são guardadas em stash antes da atualização e restauradas depois.",
|
||||
"Underline": "Sublinhado",
|
||||
"Underscored": "Sublinhado simples",
|
||||
|
||||
@@ -516,6 +516,7 @@
|
||||
"Collapse Unchanged Fragments": "折叠未更改的片段",
|
||||
"Color Scheme": "配色方案",
|
||||
"Color scheme attributes": "配色方案属性",
|
||||
"Color scheme pages": "配色方案页面",
|
||||
"Color scheme preview": "配色方案预览",
|
||||
"Color theme": "颜色主题",
|
||||
"Column": "列",
|
||||
@@ -2425,7 +2426,7 @@
|
||||
"UI font size": "UI 字体大小",
|
||||
"Uninstalled {name}": "已卸载 {name}",
|
||||
"URL:": "URL:",
|
||||
"Unchecked values are inherited from the scheme.": "未勾选的值继承自方案。",
|
||||
"The controls show the scheme's actual appearance; changes override it.": "控件显示方案的实际外观;修改即覆盖方案。",
|
||||
"Uncommitted changes are stashed before the update and restored afterwards.": "更新前会储藏未提交的更改,并在之后恢复。",
|
||||
"Underline": "下划线",
|
||||
"Underscored": "下划线",
|
||||
|
||||
Reference in new issue
Block a user