diff --git a/e2e/icls-php.spec.ts b/e2e/icls-php.spec.ts new file mode 100644 index 00000000..6a199e61 --- /dev/null +++ b/e2e/icls-php.spec.ts @@ -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) => { + const result = await open(...args); + return args[0].endsWith(".php") ? { ...result, language: "php", uiMode: "PHP" } : result; + }; + backend.files.set( + "/dev/acme-api/palette.php", + "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" }); +}); diff --git a/src/editor/core/cm/highlight.ts b/src/editor/core/cm/highlight.ts index d46306da..4518a087 100644 --- a/src/editor/core/cm/highlight.ts +++ b/src/editor/core/cm/highlight.ts @@ -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" }, diff --git a/src/editor/core/cm/languages.test.ts b/src/editor/core/cm/languages.test.ts index 72d674ea..c6e71b62 100644 --- a/src/editor/core/cm/languages.test.ts +++ b/src/editor/core/cm/languages.test.ts @@ -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 = " = []; + 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); diff --git a/src/editor/core/cm/languages.ts b/src/editor/core/cm/languages.ts index ddff2757..2c61f901 100644 --- a/src/editor/core/cm/languages.ts +++ b/src/editor/core/cm/languages.ts @@ -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[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[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; diff --git a/src/editor/lsp/theme.ts b/src/editor/lsp/theme.ts index aff186e7..8ea732ac 100644 --- a/src/editor/lsp/theme.ts +++ b/src/editor/lsp/theme.ts @@ -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", diff --git a/src/features/settings/pages/editor/colorScheme.tsx b/src/features/settings/pages/editor/colorScheme.tsx index 16024a1e..6831c494 100644 --- a/src/features/settings/pages/editor/colorScheme.tsx +++ b/src/features/settings/pages/editor/colorScheme.tsx @@ -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 | undefined, customs: CustomColorSchemes, + language?: string, ): CSSProperties { const vars: Record = {}; 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 ?? {}) }; diff --git a/src/platform/__fixtures__/colorSchemes/monokai-php.icls b/src/platform/__fixtures__/colorSchemes/monokai-php.icls new file mode 100644 index 00000000..03d90ddd --- /dev/null +++ b/src/platform/__fixtures__/colorSchemes/monokai-php.icls @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/platform/colorScheme.ts b/src/platform/colorScheme.ts index 93564613..4e0dbc5e 100644 --- a/src/platform/colorScheme.ts +++ b/src/platform/colorScheme.ts @@ -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> = { /** Declarations for the overrides of one scheme. */ export function overrideDeclarations( overrides: Record | 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; } diff --git a/src/platform/icls.test.ts b/src/platform/icls.test.ts index 9e914a56..4f72ec1d 100644 --- a/src/platform/icls.test.ts +++ b/src/platform/icls.test.ts @@ -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(` + + `); + 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" }); diff --git a/src/platform/icls.ts b/src/platform/icls.ts index 7062162e..1e17f8ba 100644 --- a/src/platform/icls.ts +++ b/src/platform/icls.ts @@ -96,17 +96,40 @@ const LANGUAGE_KEYS: Record = { 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 = { @@ -147,6 +170,22 @@ export function parseIcls(xml: string): { scheme: CustomColorScheme; applied: nu ) throw new Error("Invalid .icls color scheme"); const attributes: Record = {}; + 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(); + 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(`