дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 03:05:40 +02:00
fix(settings): use IntelliJ's codes for .icls effect types (#2203)
The `.icls` effect table used the wrong `EFFECT_TYPE` codes, so imports lost underlines and wavy underlines, and exports gave IntelliJ a box where the IDE showed an underline. Codes now match IntelliJ: 1 underline, 2 wave, 3 strikeout, 4 and 5 become underline, 0 and 6 are skipped. As in IntelliJ, an effect without an effect colour is not drawn on import, and export writes a colour for every effect, leaving a cleared error or warning effect off. Export also keeps effects defined by theme tokens, such as the doc tag underline in Islands Light. Co-authored-by: lleviathe <[email protected]>
Цей коміт міститься в:
1 джерело
02ff61e6a7
коміт
9a76d1766d
3 files changed
+133
-8
No files matched your search
@@ -15,7 +15,7 @@
|
||||
<option name="KOTLIN_FUNCTION_DECLARATION"><value><option name="FOREGROUND" value="e0c080"/><option name="FONT_TYPE" value="2"/></value></option>
|
||||
<option name="DEFAULT_STRING"><value><option name="FOREGROUND" value="88ddbb"/></value></option>
|
||||
<option name="SEARCH_RESULT_ATTRIBUTES"><value><option name="BACKGROUND" value="6a5530"/></value></option>
|
||||
<option name="ERRORS_ATTRIBUTES"><value><option name="EFFECT_COLOR" value="ff6655"/><option name="EFFECT_TYPE" value="4"/></value></option>
|
||||
<option name="ERRORS_ATTRIBUTES"><value><option name="EFFECT_COLOR" value="ff6655"/><option name="EFFECT_TYPE" value="2"/></value></option>
|
||||
<option name="UNKNOWN_LANGUAGE_TOKEN"><value><option name="FOREGROUND" value="123456"/></value></option>
|
||||
</attributes>
|
||||
</scheme>
|
||||
@@ -1,6 +1,14 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
import { _setSchemeSource, customSchemes, effectiveScheme, listSchemes, SCHEMES, schemeCss } from "./colorScheme";
|
||||
import {
|
||||
_setSchemeSource,
|
||||
CLEAR_COLOR,
|
||||
customSchemes,
|
||||
effectiveScheme,
|
||||
listSchemes,
|
||||
SCHEMES,
|
||||
schemeCss,
|
||||
} from "./colorScheme";
|
||||
import { deleteScheme, exportIcls, importScheme, parseIcls } from "./icls";
|
||||
import { validateSettings } from "@/features/settings/model/schema";
|
||||
import { colorSchemeItems } from "@/workbench/quickSwitch";
|
||||
@@ -114,6 +122,89 @@ describe(".icls schemes", () => {
|
||||
expect(customs).toEqual(before);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["0", undefined],
|
||||
["1", "underline"],
|
||||
["2", "wave"],
|
||||
["3", "strikeout"],
|
||||
["4", "underline"],
|
||||
["5", "underline"],
|
||||
["6", undefined],
|
||||
] as const)("imports IntelliJ effect type %s as %s", (code, effect) => {
|
||||
const { scheme } = parseIcls(`<scheme name="Effects"><attributes>
|
||||
<option name="DEFAULT_KEYWORD"><value><option name="EFFECT_COLOR" value="aa1122"/><option name="EFFECT_TYPE" value="${code}"/></value></option>
|
||||
</attributes></scheme>`);
|
||||
expect(scheme.attributes.keyword?.effect).toBe(effect);
|
||||
});
|
||||
|
||||
it("imports no effect without an effect colour, which IntelliJ does not draw", () => {
|
||||
const { scheme } = parseIcls(`<scheme name="Effects"><attributes>
|
||||
<option name="DEFAULT_KEYWORD"><value><option name="FOREGROUND" value="112233"/><option name="EFFECT_TYPE" value="3"/></value></option>
|
||||
</attributes></scheme>`);
|
||||
expect(scheme.attributes.keyword).toEqual({ foreground: "#112233", bold: false, italic: false });
|
||||
});
|
||||
|
||||
it("exports effects with IntelliJ's codes and a colour IntelliJ draws them in", () => {
|
||||
const { id, customs } = importScheme({}, parseIcls(fixture("forest")).scheme);
|
||||
const xml = exportIcls(id, customs, {
|
||||
keyword: { effect: "underline" },
|
||||
number: { effect: "strikeout", effectColor: "#aa1122" },
|
||||
});
|
||||
const effect = (name: string) => {
|
||||
const value = new DOMParser().parseFromString(xml, "application/xml").querySelector(`option[name="${name}"]`);
|
||||
const option = (key: string) => value?.querySelector(`option[name="${key}"]`)?.getAttribute("value");
|
||||
return [option("EFFECT_TYPE"), option("EFFECT_COLOR")];
|
||||
};
|
||||
expect(effect("DEFAULT_KEYWORD")).toEqual(["1", "aadd88"]);
|
||||
expect(effect("ERRORS_ATTRIBUTES")).toEqual(["2", "ff6655"]);
|
||||
expect(effect("DEFAULT_NUMBER")).toEqual(["3", "aa1122"]);
|
||||
const attrs = parseIcls(xml).scheme.attributes;
|
||||
expect(attrs.keyword).toMatchObject({ effect: "underline", effectColor: "#aadd88" });
|
||||
expect(attrs.error).toEqual({ effect: "wave", effectColor: "#ff6655" });
|
||||
});
|
||||
|
||||
it("exports no effect whose colour was cleared", () => {
|
||||
const { id, customs } = importScheme({}, parseIcls(fixture("forest")).scheme);
|
||||
const xml = exportIcls(id, customs, { error: { effectColor: CLEAR_COLOR } });
|
||||
expect(parseIcls(xml).scheme.attributes.error).toBeUndefined();
|
||||
});
|
||||
|
||||
it("round trips the Islands Light doc comment tag underline", () => {
|
||||
const xml = exportIcls("light", {});
|
||||
const doc = new DOMParser().parseFromString(xml, "application/xml");
|
||||
const tag = doc.querySelector('option[name="DEFAULT_DOC_COMMENT_TAG"]');
|
||||
expect(tag?.querySelector('option[name="EFFECT_TYPE"]')?.getAttribute("value")).toBe("1");
|
||||
expect(tag?.querySelector('option[name="EFFECT_COLOR"]')?.getAttribute("value")).toBe("999999");
|
||||
const scheme = parseIcls(xml).scheme;
|
||||
expect(scheme.attributes.docCommentTag).toMatchObject({ effect: "underline", effectColor: "#999999" });
|
||||
const imported = parseIcls(exportIcls("custom-docs", { "custom-docs": scheme })).scheme;
|
||||
expect(imported.attributes.docCommentTag).toEqual(scheme.attributes.docCommentTag);
|
||||
});
|
||||
|
||||
it("round trips language effects and exports none without changing shared state", () => {
|
||||
const scheme = parseIcls(`<scheme name="Effects"><attributes>
|
||||
<option name="JAVA_DOC_TAG"><value><option name="FOREGROUND" value="112233"/><option name="EFFECT_COLOR" value="abcdef"/><option name="EFFECT_TYPE" value="2"/></value></option>
|
||||
</attributes></scheme>`).scheme;
|
||||
const customs = { "custom-effects": scheme };
|
||||
const overrides = {
|
||||
keyword: { effect: "underline" as const },
|
||||
number: { effect: "none" as const, effectColor: "#ff0000" },
|
||||
"java.string": { effect: "strikeout" as const },
|
||||
};
|
||||
const before = structuredClone({ customs, overrides });
|
||||
const xml = exportIcls("custom-effects", customs, overrides);
|
||||
expect({ customs, overrides }).toEqual(before);
|
||||
const doc = new DOMParser().parseFromString(xml, "application/xml");
|
||||
expect(doc.querySelector('option[name="DEFAULT_NUMBER"] option[name="EFFECT_TYPE"]')).toBeNull();
|
||||
const imported = parseIcls(xml).scheme;
|
||||
expect(imported.attributes["java.docCommentTag"]).toMatchObject({ effect: "wave", effectColor: "#abcdef" });
|
||||
expect(imported.attributes["java.string"]).toMatchObject({ effect: "strikeout" });
|
||||
expect(imported.attributes.number?.effect).toBeUndefined();
|
||||
const roundTrip = parseIcls(exportIcls("custom-effects", { "custom-effects": imported })).scheme;
|
||||
expect(roundTrip.attributes["java.docCommentTag"]).toEqual(imported.attributes["java.docCommentTag"]);
|
||||
expect(roundTrip.attributes["java.string"]).toEqual(imported.attributes["java.string"]);
|
||||
});
|
||||
|
||||
it("imports PyCharm dot keys after shared defaults without inherited font effects", () => {
|
||||
const xml = `<scheme name="Python"><attributes>
|
||||
<option name="PY.KEYWORD"><value><option name="FOREGROUND" value="112233"/></value></option>
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
/** IntelliJ editor schemes. Unsupported keys are counted, never copied into CSS. */
|
||||
import { ATTRIBUTES, customSchemes, isHexColor, listSchemes, resolvedSchemeTokens } from "./colorScheme";
|
||||
import {
|
||||
ATTRIBUTES,
|
||||
CLEAR_COLOR,
|
||||
customSchemes,
|
||||
isHexColor,
|
||||
listSchemes,
|
||||
resolvedAttribute,
|
||||
resolvedSchemeTokens,
|
||||
} from "./colorScheme";
|
||||
import { isBuiltinTheme, themeKind } from "./themes";
|
||||
import type { CustomColorScheme, CustomColorSchemes, TextAttributesOverride } from "./editorSettings";
|
||||
|
||||
@@ -182,12 +190,19 @@ const COLOR_NAMES: Record<string, [string, "foreground" | "background"]> = {
|
||||
for (const attr of ATTRIBUTES)
|
||||
if (!Object.values(ATTRIBUTE_NAMES).includes(attr.id)) ATTRIBUTE_NAMES[`OPENIDEA_${attr.id}`] = attr.id;
|
||||
|
||||
// IntelliJ's EFFECT_TYPE codes. The box (0) and faded text (6) have no counterpart here; the bold and the
|
||||
// bold dotted line (4, 5) become an underline.
|
||||
const EFFECTS: Record<string, TextAttributesOverride["effect"]> = {
|
||||
"0": "underline",
|
||||
"1": "underline",
|
||||
"2": "wave",
|
||||
"3": "strikeout",
|
||||
"4": "wave",
|
||||
"4": "underline",
|
||||
"5": "underline",
|
||||
"6": "underline",
|
||||
};
|
||||
const EFFECT_CODES: Partial<Record<NonNullable<TextAttributesOverride["effect"]>, string>> = {
|
||||
underline: "1",
|
||||
wave: "2",
|
||||
strikeout: "3",
|
||||
};
|
||||
const children = (el: Element, tag: string) => Array.from(el.children).filter((e) => e.tagName === tag);
|
||||
const color = (v: string | null): string | undefined =>
|
||||
@@ -263,6 +278,10 @@ export function parseIcls(xml: string): { scheme: CustomColorScheme; applied: nu
|
||||
// An explicit TextAttributes value defaults to Font.PLAIN, not the last alias's style.
|
||||
if (attr.token) merge(id, { bold: false, italic: false });
|
||||
if (name.startsWith("PY.")) merge(id, { effect: "none" });
|
||||
// IntelliJ draws no effect without an effect colour.
|
||||
const drawn = children(value, "option").some(
|
||||
(o) => o.getAttribute("name") === "EFFECT_COLOR" && color(o.getAttribute("value")),
|
||||
);
|
||||
for (const option of children(value, "option")) {
|
||||
const name = option.getAttribute("name");
|
||||
const raw = option.getAttribute("value");
|
||||
@@ -287,7 +306,13 @@ export function parseIcls(xml: string): { scheme: CustomColorScheme; applied: nu
|
||||
} else if (name === "FONT_TYPE" && attr.token && raw && /^[0-3]$/.test(raw)) {
|
||||
merge(id, { bold: !!(Number(raw) & 1), italic: !!(Number(raw) & 2) });
|
||||
applied++;
|
||||
} else if (name === "EFFECT_TYPE" && (attr.token || attr.effect) && raw && Object.hasOwn(EFFECTS, raw)) {
|
||||
} else if (
|
||||
name === "EFFECT_TYPE" &&
|
||||
drawn &&
|
||||
(attr.token || attr.effect) &&
|
||||
raw &&
|
||||
Object.hasOwn(EFFECTS, raw)
|
||||
) {
|
||||
merge(id, { effect: EFFECTS[raw] });
|
||||
applied++;
|
||||
} else skipped++;
|
||||
@@ -353,6 +378,11 @@ export function exportIcls(
|
||||
const custom = customSchemes(customs)[id];
|
||||
const name = listSchemes(customs).find((s) => s.id === id)?.name ?? id;
|
||||
const tokens = resolvedSchemeTokens(id, customs);
|
||||
const tokenEffect = (attr: (typeof ATTRIBUTES)[number]): TextAttributesOverride => {
|
||||
if (!attr.token) return {};
|
||||
const { effect, effectColor } = resolvedAttribute(id, attr.id, customs);
|
||||
return { effect, effectColor };
|
||||
};
|
||||
const values = (attr: (typeof ATTRIBUTES)[number]): TextAttributesOverride => ({
|
||||
...(attr.token
|
||||
? {
|
||||
@@ -369,6 +399,7 @@ export function exportIcls(
|
||||
...(attr.fg && isHexColor(tokens.get(attr.fg)) ? { foreground: tokens.get(attr.fg) } : {}),
|
||||
...(attr.bg && isHexColor(tokens.get(attr.bg)) ? { background: tokens.get(attr.bg) } : {}),
|
||||
...(attr.effect && isHexColor(tokens.get(attr.effect)) ? { effectColor: tokens.get(attr.effect) } : {}),
|
||||
...tokenEffect(attr),
|
||||
...custom?.attributes[attr.id],
|
||||
...overrides[attr.id],
|
||||
});
|
||||
@@ -379,11 +410,15 @@ export function exportIcls(
|
||||
const c = attr && values(attr)[field];
|
||||
if (isHexColor(c)) colors.push(` <option name="${key}" value="${exportColor(c)}" />`);
|
||||
}
|
||||
const text = values(ATTRIBUTES.find((a) => a.id === "text")!);
|
||||
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);
|
||||
// A cleared effect colour hides the effect. One left unset takes the text's here, and IntelliJ would draw none.
|
||||
const effect = v.effect && v.effectColor !== CLEAR_COLOR ? EFFECT_CODES[v.effect] : undefined;
|
||||
if (effect && !isHexColor(v.effectColor)) v.effectColor = v.foreground ?? text.foreground;
|
||||
const opts: string[] = [];
|
||||
for (const [key, field] of [
|
||||
["FOREGROUND", "foreground"],
|
||||
@@ -392,7 +427,6 @@ export function exportIcls(
|
||||
] as const)
|
||||
if (isHexColor(v[field])) opts.push(`<option name="${key}" value="${exportColor(v[field]!)}" />`);
|
||||
if (attr.token) opts.push(`<option name="FONT_TYPE" value="${(v.bold ? 1 : 0) + (v.italic ? 2 : 0)}" />`);
|
||||
const effect = Object.entries(EFFECTS).find(([, value]) => value === v.effect)?.[0];
|
||||
if (effect) opts.push(`<option name="EFFECT_TYPE" value="${effect}" />`);
|
||||
if (opts.length) attributes.push(` <option name="${key}"><value>${opts.join("")}</value></option>`);
|
||||
}
|
||||
|
||||
Reference in new issue
Заблокувати користувача