fix(themes): preserve language-specific ICLS styles (#1457)

Importing a PhpStorm .icls applied Java/Kotlin keys and PHP-only colours to every language, so PHP variables, strings and punctuation came out wrong. Java, Kotlin and PHP attributes are now kept per language (Color Scheme lists them under their own nodes, and editors scope them by data-language), explicit attribute inheritance is resolved, an explicit TextAttributes value resets omitted bold/italic, the light/dark parent follows the scheme's background, and PHP classes, method declarations, fields, brackets and separators get their own tags. Selecting a language attribute in Settings previews that language's colours.

Co-authored-by: marianomd <274989+marianomd@users.noreply.github.com>
This commit is contained in:
Mariano Dupont
2026-11-05 12:59:38 +08:00
committed by GitHub
co-authored by marianomd
parent 75c4ca0be8
commit 8dedf5e73b
12 changed files with 318 additions and 37 deletions
+77
View File
@@ -0,0 +1,77 @@
import { readFileSync } from "node:fs";
import { expect, test } from "./fixtures";
const xml = readFileSync(
process.env.OPENIDEA_ICLS_FIXTURE ?? "src/platform/__fixtures__/colorSchemes/monokai-php.icls",
"utf8",
);
test("imported PHP colours stay separate from Java and preserve regular text on a dark background", async ({
workbench: page,
}, info) => {
await page.evaluate(async (xml) => {
const iclsPath = "/src/platform/icls.ts";
const projectPath = "/src/platform/project.ts";
const backendPath = "/src/editor/core/backend.ts";
const contributionsPath = "/src/workbench/contributions.ts";
const { parseIcls } = await import(iclsPath);
const { useProject } = await import(projectPath);
const { docBackend } = await import(backendPath);
const { editorService } = await import(contributionsPath);
const previous = useProject.getState().settings ?? {};
useProject.setState({
settings: {
...previous,
editor: {
...previous.editor,
colorScheme: "custom-php",
customSchemes: { "custom-php": parseIcls(xml).scheme },
semanticHighlighting: false,
},
},
});
const backend = docBackend();
const open = backend.open.bind(backend);
backend.open = async (...args: Parameters<typeof open>) => {
const result = await open(...args);
return args[0].endsWith(".php") ? { ...result, language: "php", uiMode: "PHP" } : result;
};
backend.files.set(
"/dev/acme-api/palette.php",
"<?php\n/** Plain PHP documentation */\nclass Palette extends BasePalette {\n var $name = 'Palette';\n function example($value) {\n return $this->name == 10;\n }\n}\n",
);
await editorService().openFile("/dev/acme-api/palette.php");
}, xml);
const php = page.locator('.cm-editor[data-language="PHP"]');
await expect(php).toBeVisible();
await expect(php).toHaveCSS("background-color", "rgb(0, 0, 0)");
const token = (text: string) =>
php
.locator(".cm-content span")
.filter({ hasText: text })
.filter({ hasNot: php.locator("span span") });
await expect(token("$name").first()).toHaveCSS("color", "rgb(72, 216, 238)");
await expect(token("'Palette'")).toHaveCSS("color", "rgb(255, 231, 146)");
await expect(token("'Palette'")).toHaveCSS("font-weight", "400");
await expect(token("class").first()).toHaveCSS("color", "rgb(249, 38, 114)");
await expect(token("Plain PHP documentation")).toHaveCSS("font-style", "normal");
await expect(token("BasePalette")).toHaveCSS("color", "rgb(174, 129, 255)");
await expect(token(";").first()).toHaveCSS("color", "rgb(255, 255, 255)");
await expect(token("example")).toHaveCSS("color", "rgb(141, 225, 45)");
await expect(token("$name").first()).toHaveCSS("font-weight", "700");
await page.evaluate(async () => {
const backendPath = "/src/editor/core/backend.ts";
const contributionsPath = "/src/workbench/contributions.ts";
const { docBackend } = await import(backendPath);
const { editorService } = await import(contributionsPath);
docBackend().files.set("/dev/acme-api/Palette.java", 'class Palette { String name = "Palette"; }');
await editorService().openFile("/dev/acme-api/Palette.java");
});
const java = page.locator('.cm-editor[data-language="Java"]');
await expect(java).toBeVisible();
await expect(java.locator(".cm-content span").filter({ hasText: '"Palette"' })).toHaveCSS("font-weight", "700");
await page.getByRole("tab", { name: "palette.php", exact: true }).click();
await expect(php).toBeVisible();
await expect(token("'Palette'")).toHaveCSS("font-weight", "400");
await php.screenshot({ path: info.outputPath("php-import.png"), animations: "disabled" });
});
+12 -8
View File
@@ -9,17 +9,19 @@ import { tags as t, type Tag } from "@lezer/highlight";
function token(
tag: Tag | Tag[],
name: string,
defaults: { italic?: string; bold?: string; effect?: string } = {},
defaults: { italic?: string; bold?: string; effect?: string; color?: string } = {},
): TagStyle {
const variable = (suffix: string, fallback: string) =>
`var(--syn-${name}${suffix}, ${defaults.color ? `var(--syn-${defaults.color}${suffix}, ${fallback})` : fallback})`;
return {
tag,
color: `var(--syn-${name})`,
fontStyle: `var(--syn-${name}-italic, ${defaults.italic ?? "inherit"})`,
fontWeight: `var(--syn-${name}-bold, ${defaults.bold ?? "inherit"})`,
backgroundColor: `var(--syn-${name}-bg, transparent)`,
textDecoration: `var(--syn-${name}-effect, ${defaults.effect ?? "none"})`,
color: defaults.color ? variable("", "inherit") : `var(--syn-${name})`,
fontStyle: variable("-italic", defaults.italic ?? "inherit"),
fontWeight: variable("-bold", defaults.bold ?? "inherit"),
backgroundColor: variable("-bg", "transparent"),
textDecoration: variable("-effect", defaults.effect ?? "none"),
// WebKit takes a decoration with a style or colour only through the prefixed property.
WebkitTextDecoration: `var(--syn-${name}-effect, ${defaults.effect ?? "none"})`,
WebkitTextDecoration: variable("-effect", defaults.effect ?? "none"),
};
}
@@ -71,10 +73,12 @@ export const openideaHighlightStyle = HighlightStyle.define([
token(t.special(t.brace), "decorator"),
token([t.tagName, t.angleBracket], "tag"),
token([t.punctuation, t.bracket, t.separator, t.operator, t.derefOperator], "punct"),
token(t.bracket, "bracket", { color: "punct" }),
token(t.separator, "separator", { color: "punct" }),
token(t.invalid, "invalid"),
// Markup (Markdown, docs)
token(t.heading, "markup-heading", { bold: "var(--fw-code-bold, 700)" }),
token(t.processingInstruction, "keyword"),
token(t.processingInstruction, "processing-instruction", { color: "keyword" }),
{ tag: t.emphasis, fontStyle: "italic" },
{ tag: t.strong, fontWeight: "var(--fw-code-bold, 700)" },
{ tag: t.strikethrough, textDecoration: "line-through" },
+21
View File
@@ -3,6 +3,27 @@ import { highlightTree, tagHighlighter, tags as t } from "@lezer/highlight";
import { findLanguageDescription, loadSupport } from "./languages";
describe("language modes", () => {
it("distinguishes PHP classes, method declarations and fields for imported schemes", async () => {
const support = await loadSupport(findLanguageDescription("PHP", "/tmp/palette.php")!);
const source = "<?php class Palette extends BasePalette { var $name; function example() {} }";
const found: Array<[string, string]> = [];
highlightTree(
support!.language.parser.parse(source),
tagHighlighter([
{ tag: t.className, class: "class" },
{ tag: t.function(t.definition(t.variableName)), class: "function" },
{ tag: t.definition(t.propertyName), class: "field" },
]),
(from, to, style) => found.push([source.slice(from, to), style]),
);
expect(found).toEqual([
["Palette", "class"],
["BasePalette", "class"],
["$name", "field"],
["example", "function"],
]);
});
it("recognizes AsciiDoc files and loads their lightweight syntax mode", async () => {
for (const path of ["/docs/guide.adoc", "/docs/guide.asciidoc"]) {
const description = findLanguageDescription("AsciiDoc", path);
+15 -2
View File
@@ -5,6 +5,7 @@
* and indentation fall back to the backend definition when the mode lacks them.
*/
import { EditorState, Prec, type Extension } from "@codemirror/state";
import { EditorView } from "@codemirror/view";
import {
LRLanguage,
LanguageDescription,
@@ -246,6 +247,12 @@ const EXTRA_TAGS: Record<string, Parameters<typeof styleTags>[0]> = {
JSX: JS_TAGS,
TSX: JS_TAGS,
Java: JAVA_TAGS,
PHP: {
"; ,": t.separator,
"ClassDeclaration/Name BaseClause/Name InterfaceClause/Name": t.className,
"MethodDeclaration/Name FunctionDefinition/Name": t.function(t.definition(t.variableName)),
"PropertyDeclaration/VariableDeclarator/VariableName": t.definition(t.propertyName),
},
CSS: CSS_TAGS,
SCSS: CSS_TAGS,
Sass: CSS_TAGS,
@@ -255,8 +262,14 @@ const EXTRA_TAGS: Record<string, Parameters<typeof styleTags>[0]> = {
function patchSupport(desc: LanguageDescription, support: LanguageSupport): LanguageSupport {
const extra = EXTRA_TAGS[desc.name];
if (!extra || !(support.language instanceof LRLanguage)) return support;
const patched = new LanguageSupport(support.language.configure({ props: [styleTags(extra)] }), support.support);
const language =
extra && support.language instanceof LRLanguage
? support.language.configure({ props: [styleTags(extra)] })
: support.language;
const patched = new LanguageSupport(language, [
support.support,
EditorView.editorAttributes.of({ "data-language": desc.name }),
]);
// `loadedSupport` reads `desc.support` synchronously; keep it on the patched instance.
(desc as { support: LanguageSupport | undefined }).support = patched;
return patched;
+8
View File
@@ -365,6 +365,14 @@ export const lspTheme = EditorView.theme({
".cm-sem-global:not(.cm-sem-function)": { color: "var(--syn-global, inherit)" },
".cm-sem-builtin:not(.cm-sem-function)": { color: "var(--syn-builtin, inherit)" },
".cm-sem-static": { fontStyle: "var(--syn-static-italic, italic)" },
".cm-sem-property.cm-sem-static, .cm-sem-property.cm-sem-static *": {
color: "var(--syn-static-property, var(--syn-property))",
fontStyle: "var(--syn-static-property-italic, var(--syn-static-italic, italic))",
fontWeight: "var(--syn-static-property-bold, var(--syn-property-bold, inherit))",
backgroundColor: "var(--syn-static-property-bg, transparent)",
textDecoration: "var(--syn-static-property-effect, none)",
WebkitTextDecoration: "var(--syn-static-property-effect, none)",
},
".cm-sem-python-static": { fontStyle: "var(--syn-python-static-italic, normal)" },
".cm-sem-mutable": {
textDecoration: "underline",
@@ -55,11 +55,15 @@ function buildTree(): Node[] {
const TREE = buildTree();
/** CSS custom properties of a scheme plus pending overrides (for the preview). */
/**
* CSS custom properties of a scheme plus pending overrides (for the preview); with
* `language`, that language's own colours on top, as its editors show them.
*/
function previewVars(
id: string,
o: Record<string, TextAttributesOverride> | undefined,
customs: CustomColorSchemes,
language?: string,
): CSSProperties {
const vars: Record<string, string> = {};
const tokens = resolvedSchemeTokens(id, customs);
@@ -74,6 +78,10 @@ function previewVars(
vars[k] = v;
vars["--bg-editor"] = tokens.get("--bg-editor") ?? tokens.get("--bg-island") ?? "";
for (const [key, value] of overrideDeclarations(o)) vars[key] = value;
if (language) {
for (const [key, value] of overrideDeclarations(customs[id]?.attributes, language)) vars[key] = value;
for (const [key, value] of overrideDeclarations(o, language)) vars[key] = value;
}
return vars as CSSProperties;
}
@@ -276,9 +284,10 @@ function ColorSchemePage() {
const id = effectiveScheme(scheme, undefined, customs);
const mine = overrides?.[id];
const attr = ATTRIBUTES.find((a) => a.id === selected);
const language = attr?.language;
const vars = useMemo(
() => ({ ...previewVars(id, mine, customs), ...fontWeightVars(weight, boldFontWeight) }) as CSSProperties,
[id, mine, customs, weight, boldFontWeight],
() => ({ ...previewVars(id, mine, customs, language), ...fontWeightVars(weight, boldFontWeight) }) as CSSProperties,
[id, mine, customs, language, weight, boldFontWeight],
);
const update = (attrId: string, v: TextAttributesOverride | undefined) => {
const next: Overrides = { ...(overrides ?? {}) };
@@ -0,0 +1,24 @@
<scheme name="Monokai PHP" version="142" parent_scheme="Default">
<attributes>
<option name="TEXT"><value><option name="FOREGROUND" value="f8f8f2"/><option name="BACKGROUND" value="0"/></value></option>
<option name="DEFAULT_IDENTIFIER"><value><option name="FOREGROUND" value="e162d1"/></value></option>
<option name="DEFAULT_KEYWORD"><value><option name="FOREGROUND" value="41abff"/><option name="FONT_TYPE" value="1"/></value></option>
<option name="JAVA_KEYWORD"><value><option name="FOREGROUND" value="f72671"/><option name="FONT_TYPE" value="1"/></value></option>
<option name="JAVA_STRING"><value><option name="FOREGROUND" value="ffe792"/><option name="FONT_TYPE" value="1"/></value></option>
<option name="PHP_CLASS"><value><option name="FOREGROUND" value="ae81ff"/></value></option>
<option name="PHP_COMMENT"><value><option name="FOREGROUND" value="75715e"/></value></option>
<option name="PHP_DOC_COMMENT_ID"><value><option name="FOREGROUND" value="75715e"/></value></option>
<option name="PHP_FUNCTION"><value><option name="FOREGROUND" value="8de12d"/></value></option>
<option name="PHP_FUNCTION_CALL"><value><option name="FOREGROUND" value="8de12d"/></value></option>
<option name="PHP_INSTANCE_FIELD"><value><option name="FOREGROUND" value="48d8ee"/><option name="FONT_TYPE" value="1"/></value></option>
<option name="PHP_KEYWORD"><value><option name="FOREGROUND" value="f92672"/></value></option>
<option name="PHP_NUMBER"><value><option name="FOREGROUND" value="ae81ff"/></value></option>
<option name="PHP_OPERATION_SIGN"><value><option name="FOREGROUND" value="f72671"/></value></option>
<option name="PHP_BRACKETS"><value><option name="FOREGROUND" value="ffffff"/></value></option>
<option name="PHP_COMMA"><value><option name="FOREGROUND" value="ffffff"/></value></option>
<option name="PHP_SEMICOLON"><value><option name="FOREGROUND" value="ffffff"/></value></option>
<option name="PHP_PARAMETER"><value><option name="FOREGROUND" value="48d8ee"/></value></option>
<option name="PHP_STRING"><value><option name="FOREGROUND" value="ffe792"/></value></option>
<option name="PHP_VAR"><value><option name="FOREGROUND" value="48d8ee"/></value></option>
</attributes>
</scheme>
+36 -2
View File
@@ -32,6 +32,8 @@ export interface AttributeDef {
/** A semantic modifier whose only configurable attribute is italic. */
italicOnly?: boolean;
defaultItalic?: boolean;
/** CodeMirror language name; unset attributes are language defaults. */
language?: string;
}
const syn = (id: string, name: string, path: string[], token: string): AttributeDef => ({
@@ -45,7 +47,7 @@ const syn = (id: string, name: string, path: string[], token: string): Attribute
const LD = "Language Defaults";
/** IDEA-style attribute tree: General, Language Defaults, then languages. */
export const ATTRIBUTES: readonly AttributeDef[] = [
const BASE_ATTRIBUTES: readonly AttributeDef[] = [
{ id: "text", name: "Default text", path: ["General", "Text"], fg: "--fg-editor", bg: "--bg-editor" },
{ id: "caret", name: "Caret", path: ["General", "Editor"], fg: "--caret" },
{ id: "caretRow", name: "Caret row", path: ["General", "Editor"], bg: "--bg-current-line" },
@@ -152,6 +154,26 @@ export const ATTRIBUTES: readonly AttributeDef[] = [
syn("regexp", "Regular expression", ["RegExp"], "regexp"),
];
const SCHEME_LANGUAGES = { java: "Java", kotlin: "Kotlin", php: "PHP" } as const;
export const ATTRIBUTES: readonly AttributeDef[] = [
...BASE_ATTRIBUTES,
...Object.entries(SCHEME_LANGUAGES).flatMap(([id, language]) =>
BASE_ATTRIBUTES.filter((a) => a.path[0] === LD && !/^(semantic|bracket)\d$/.test(a.id)).map((a) => ({
...a,
id: `${id}.${a.id}`,
path: [language, ...a.path.slice(1)],
language,
})),
),
...[
["brackets", "Brackets", "bracket"],
["separator", "Comma and semicolon", "separator"],
["scriptTag", "PHP tags", "processing-instruction"],
["staticField", "Static field", "static-property"],
].map(([id, name, token]) => ({ ...syn(`php.${id}`, name!, ["PHP"], token!), language: "PHP" })),
];
// --- scheme base values ------------------------------------------------------
/** Variables a scheme sets: every attribute variable plus the editor tokens derived from them. */
@@ -304,10 +326,12 @@ const EFFECT_CSS: Record<string, (c: string) => string> = {
/** Declarations for the overrides of one scheme. */
export function overrideDeclarations(
overrides: Record<string, TextAttributesOverride> | undefined,
language?: string,
): Array<[string, string]> {
const out: Array<[string, string]> = [];
if (!overrides) return out;
for (const attr of ATTRIBUTES) {
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]);
@@ -402,5 +426,15 @@ export function schemeCss(
const body = decls
.filter(([k, v]) => /^--[\w-]+$/.test(k) && SAFE.test(v))
.map(([k, v]) => ` ${k}: ${v} !important;`);
return body.length ? `:root {\n${body.join("\n")}\n}\n` : "";
let css = body.length ? `:root {\n${body.join("\n")}\n}\n` : "";
for (const language of Object.values(SCHEME_LANGUAGES)) {
const languageBody = [
...overrideDeclarations(valid[id]?.attributes, language),
...overrideDeclarations(overrides?.[id], language),
]
.filter(([k, v]) => /^--[\w-]+$/.test(k) && SAFE.test(v))
.map(([k, v]) => ` ${k}: ${v} !important;`);
if (languageBody.length) css += `.cm-editor[data-language="${language}"] {\n${languageBody.join("\n")}\n}\n`;
}
return css;
}
+42 -5
View File
@@ -12,12 +12,48 @@ const fixture = (name: string) => readFileSync(`src/platform/__fixtures__/colorS
beforeAll(() => _setSchemeSource(readFileSync("src/styles/tokens.css", "utf8")));
describe(".icls schemes", () => {
it("resolves explicit attribute inheritance independent of XML order and stops cycles", () => {
const { scheme } = parseIcls(`<scheme name="Inherited"><attributes>
<option name="PHP_PARAMETER" baseAttributes="PHP_VAR"/>
<option name="PHP_VAR" baseAttributes="DEFAULT_IDENTIFIER"/>
<option name="DEFAULT_IDENTIFIER"><value><option name="FOREGROUND" value="48d8ee"/></value></option>
<option name="PHP_STRING" baseAttributes="PHP_NUMBER"/>
<option name="PHP_NUMBER" baseAttributes="PHP_STRING"/>
</attributes></scheme>`);
expect(scheme.attributes["php.parameter"]).toEqual({ foreground: "#48d8ee", bold: false, italic: false });
expect(scheme.attributes["php.identifier"]).toEqual(scheme.attributes["php.parameter"]);
expect(scheme.attributes["php.string"]).toBeUndefined();
});
it("preserves PHP colours and regular styles separately from Java and language defaults", () => {
const { scheme } = parseIcls(fixture("monokai-php"));
expect(scheme.parent).toBe("dark");
expect(scheme.attributes["php.identifier"]).toEqual({ foreground: "#48d8ee", bold: false, italic: false });
expect(scheme.attributes["php.keyword"]).toEqual({ foreground: "#f92672", bold: false, italic: false });
expect(scheme.attributes["php.string"]).toEqual({ foreground: "#ffe792", bold: false, italic: false });
expect(scheme.attributes["php.docComment"]).toEqual({ foreground: "#75715e", bold: false, italic: false });
expect(scheme.attributes["java.string"]).toEqual({ foreground: "#ffe792", bold: true, italic: false });
expect(scheme.attributes.keyword?.foreground).toBe("#41abff");
const customs = { "custom-php": scheme };
const css = schemeCss("custom-php", undefined, null, customs);
expect(css).toContain('.cm-editor[data-language="PHP"]');
expect(css).toContain("--syn-variable: #48d8ee !important");
const roundTrip = parseIcls(exportIcls("custom-php", customs)).scheme;
expect(roundTrip.attributes["php.string"]).toEqual(scheme.attributes["php.string"]);
expect(roundTrip.attributes["java.string"]).toEqual(scheme.attributes["java.string"]);
});
it("maps defaults, Java/Kotlin, editor surfaces, search and diagnostics with counts", () => {
const { scheme, applied, skipped } = parseIcls(fixture("forest"));
expect(scheme.name).toBe("Forest & Moss");
expect(scheme.parent).toBe("dark");
expect(scheme.attributes.keyword).toEqual({ foreground: "#99dd77", bold: true, italic: false });
expect(scheme.attributes.functionDeclaration).toEqual({ foreground: "#e0c080", bold: false, italic: true });
expect(scheme.attributes.keyword).toEqual({ foreground: "#aadd88", bold: false, italic: false });
expect(scheme.attributes["java.keyword"]).toEqual({ foreground: "#99dd77", bold: true, italic: false });
expect(scheme.attributes["kotlin.functionDeclaration"]).toEqual({
foreground: "#e0c080",
bold: false,
italic: true,
});
expect(scheme.attributes.text).toEqual({ foreground: "#d8ebd8", background: "#18281e" });
expect(scheme.attributes.caret?.foreground).toBe("#f0e080");
expect(scheme.attributes.selection?.background).toBe("#406040");
@@ -53,8 +89,8 @@ describe(".icls schemes", () => {
});
it.each([
["Darcula", "ffffff", "dark"],
["Default", "000000", "light"],
["Darcula", "ffffff", "light"],
["Default", "000000", "dark"],
["Other", "101820", "dark"],
["Other", "fefefe", "light"],
["", "0", "dark"],
@@ -111,7 +147,8 @@ describe(".icls schemes", () => {
root.dataset.theme = theme;
const css = schemeCss(id, overrides, root, customs);
expect(css).toContain("--bg-editor: #18281e !important");
expect(css.lastIndexOf("--syn-keyword: #abcdef")).toBeGreaterThan(css.indexOf("--syn-keyword: #99dd77"));
expect(css.indexOf("--syn-keyword: #abcdef")).toBeGreaterThan(css.indexOf("--syn-keyword: #aadd88"));
expect(css).toContain('.cm-editor[data-language="Java"]');
expect(effectiveScheme("theme-default", root, customs)).toBe(theme);
const deleted = deleteScheme(customs, overrides, id, id);
expect(deleted).toEqual({ customs: {}, overrides: {}, selected: "theme-default" });
+63 -17
View File
@@ -96,17 +96,40 @@ const LANGUAGE_KEYS: Record<string, string> = {
PARENTHS: "operator",
};
for (const [name, id] of Object.entries(LANGUAGE_KEYS)) {
ATTRIBUTE_NAMES[`JAVA_${name}`] = id;
ATTRIBUTE_NAMES[`KOTLIN_${name}`] = id;
ATTRIBUTE_NAMES[`JAVA_${name}`] = `java.${id}`;
ATTRIBUTE_NAMES[`KOTLIN_${name}`] = `kotlin.${id}`;
ATTRIBUTE_NAMES[name] = id;
}
Object.assign(ATTRIBUTE_NAMES, {
KOTLIN_MUTABLE_VARIABLE: "identifier",
KOTLIN_PACKAGE_PROPERTY: "field",
KOTLIN_SYNTHETIC_EXTENSION_PROPERTY: "field",
KOTLIN_EXTENSION_FUNCTION_CALL: "functionCall",
KOTLIN_NAMED_ARGUMENT: "parameter",
KOTLIN_OBJECT: "className",
KOTLIN_MUTABLE_VARIABLE: "kotlin.identifier",
KOTLIN_PACKAGE_PROPERTY: "kotlin.field",
KOTLIN_SYNTHETIC_EXTENSION_PROPERTY: "kotlin.field",
KOTLIN_EXTENSION_FUNCTION_CALL: "kotlin.functionCall",
KOTLIN_NAMED_ARGUMENT: "kotlin.parameter",
KOTLIN_OBJECT: "kotlin.className",
CLASS_NAME_ATTRIBUTES: "className",
PHP_KEYWORD: "php.keyword",
PHP_VAR: "php.identifier",
PHP_PARAMETER: "php.parameter",
PHP_CLASS: "php.className",
PHP_CONSTANT: "php.constant",
PHP_INSTANCE_FIELD: "php.field",
PHP_STATIC_FIELD: "php.staticField",
PHP_FUNCTION: "php.functionDeclaration",
PHP_FUNCTION_CALL: "php.functionCall",
PHP_INSTANCE_METHOD: "php.functionCall",
"PHP_PREDEFINED SYMBOL": "php.predefined",
PHP_NUMBER: "php.number",
PHP_STRING: "php.string",
PHP_ESCAPE_SEQUENCE: "php.escape",
PHP_COMMENT: "php.lineComment",
PHP_DOC_COMMENT_ID: "php.docComment",
PHP_DOC_TAG: "php.metadata",
PHP_OPERATION_SIGN: "php.operator",
PHP_BRACKETS: "php.brackets",
PHP_COMMA: "php.separator",
PHP_SEMICOLON: "php.separator",
PHP_TAG: "php.scriptTag",
});
const COLOR_NAMES: Record<string, [string, "foreground" | "background"]> = {
@@ -147,6 +170,22 @@ export function parseIcls(xml: string): { scheme: CustomColorScheme; applied: nu
)
throw new Error("Invalid .icls color scheme");
const attributes: Record<string, TextAttributesOverride> = {};
const entries = new Map(
children(root, "attributes").flatMap((section) =>
children(section, "option").map((entry) => [entry.getAttribute("name"), entry] as const),
),
);
const inheritedValue = (entry: Element): Element | undefined => {
const seen = new Set<Element>();
let current: Element | undefined = entry;
while (current && !seen.has(current)) {
seen.add(current);
const value = children(current, "value")[0];
if (value) return value;
current = entries.get(current.getAttribute("baseAttributes"));
}
return undefined;
};
let applied = 0;
let skipped = 0;
const merge = (id: string, value: TextAttributesOverride) => {
@@ -171,11 +210,14 @@ export function parseIcls(xml: string): { scheme: CustomColorScheme; applied: nu
skipped++;
continue;
}
const value = children(entry, "value")[0];
const value = inheritedValue(entry);
if (!value) {
skipped++;
continue;
}
const attr = ATTRIBUTES.find((a) => a.id === id)!;
// An explicit TextAttributes value defaults to Font.PLAIN, not the last alias's style.
if (attr.token) merge(id, { bold: false, italic: false });
for (const option of children(value, "option")) {
const name = option.getAttribute("name");
const raw = option.getAttribute("value");
@@ -188,7 +230,6 @@ export function parseIcls(xml: string): { scheme: CustomColorScheme; applied: nu
: name === "EFFECT_COLOR"
? "effectColor"
: undefined;
const attr = ATTRIBUTES.find((a) => a.id === id)!;
const supported =
field === "foreground"
? !!attr.fg
@@ -212,13 +253,17 @@ export function parseIcls(xml: string): { scheme: CustomColorScheme; applied: nu
const bg = attributes.text?.background;
const rgb = bg ? [1, 3, 5].map((i) => parseInt(bg.slice(i, i + 2), 16)) : [255, 255, 255];
const dark = rgb[0]! * 0.299 + rgb[1]! * 0.587 + rgb[2]! * 0.114 < 128;
const parent = /darcula|dark/i.test(parentName)
? "dark"
: /default|light/i.test(parentName)
? "light"
: dark
? "dark"
: "light";
const parent = bg
? dark
? "dark"
: "light"
: /darcula|dark/i.test(parentName)
? "dark"
: /default|light/i.test(parentName)
? "light"
: dark
? "dark"
: "light";
return { scheme: { name: root.getAttribute("name")!.trim(), parent, attributes }, applied, skipped };
}
@@ -290,6 +335,7 @@ export function exportIcls(
if (isHexColor(c)) colors.push(` <option name="${key}" value="${exportColor(c)}" />`);
}
for (const attr of ATTRIBUTES) {
if (attr.language && !custom?.attributes[attr.id] && !overrides[attr.id]) continue;
const key = Object.keys(ATTRIBUTE_NAMES).find((key) => ATTRIBUTE_NAMES[key] === attr.id);
if (!key) continue;
const v = values(attr);
+4
View File
@@ -1,4 +1,8 @@
{
"Brackets": "Corchetes y paréntesis",
"Comma and semicolon": "Coma y punto y coma",
"PHP tags": "Etiquetas PHP",
"Static field": "Campo estático",
"Unity": "Unity",
"Show Meta Files": "Mostrar archivos meta",
"Show Generated Files": "Mostrar archivos generados",
+4
View File
@@ -4,6 +4,10 @@
"Unlink Gradle project?": "取消关联 Gradle 项目?",
"Remove this build from Gradle? Files will remain in the Project view; link its build file again to restore it.": "从 Gradle 中移除此构建?文件将保留在项目视图中;重新关联其构建文件即可恢复。",
"Gradle build files": "Gradle 构建文件",
"Brackets": "括号",
"Comma and semicolon": "逗号和分号",
"PHP tags": "PHP 标签",
"Static field": "静态字段",
"Unity": "Unity 项目",
"Show Meta Files": "显示 Meta 文件",
"Show Generated Files": "显示生成的文件",