Файли
openidea/src/platform/icls.ts
T
875e049dcd
CI / Changes (push) Successful in 2s
Language smoke / Language smoke (kotlin) (push) Failing after 5s
Language smoke / Language smoke (go) (push) Failing after 7s
Language smoke / Language smoke (java) (push) Failing after 6s
Language smoke / Language smoke (python) (push) Failing after 4s
Language smoke / Language smoke (rust) (push) Failing after 5s
Language smoke / Language smoke (typescript) (push) Failing after 4s
CI / Plugins (SDK, official, fixtures) (push) Successful in 3m8s
CI / Cargo deny (push) Successful in 56s
CI / Remote server (linux musl) (push) Successful in 5m57s
CI / E2E (push) Failing after 15m56s
CI / Frontend (push) Failing after 24m3s
CI / Rust (ubuntu-22.04) (push) Failing after 46m51s
CI / Rust (macos-latest) (push) Canceled after 0s
CI / Rust (windows-latest) (push) Canceled after 0s
fix(editor): align Python identifier colors and unused-parameter styling (#1572)
Python defaults diverged in several places: built-ins used the predefined magenta, __init__/__doc__ used function/self colours, instance fields the property colour, f-strings and byte strings the generic string colour, and the PEP 695 type keyword was uncoloured. Python now has its own tokens for built-ins, predefined definitions/usages, f-strings and byte strings; instance fields follow identifiers; type is a keyword. Unused parameters (not reported by Pyrefly) are greyed by a scope-aware local check that skips self/cls/_ names and signatures dictated elsewhere: stubs, @abstractmethod/@overload/@override, dunder methods and methods of classes with bases.

Co-authored-by: muutot <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 23:38:03 +08:00

391 рядки
15 KiB
TypeScript

/** IntelliJ editor schemes. Unsupported keys are counted, never copied into CSS. */
import { ATTRIBUTES, customSchemes, isHexColor, listSchemes, resolvedSchemeTokens } from "./colorScheme";
import { isBuiltinTheme, themeKind } from "./themes";
import type { CustomColorScheme, CustomColorSchemes, TextAttributesOverride } from "./editorSettings";
// Many language keys share the editor's language-default tokens.
const ATTRIBUTE_NAMES: Record<string, string> = {
TEXT: "text",
DEFAULT_TEXT: "text",
DEFAULT_KEYWORD: "keyword",
DEFAULT_IDENTIFIER: "identifier",
DEFAULT_LOCAL_VARIABLE: "identifier",
DEFAULT_PARAMETER: "parameter",
DEFAULT_GLOBAL_VARIABLE: "globalVariable",
DEFAULT_CONSTANT: "constant",
DEFAULT_INSTANCE_FIELD: "field",
DEFAULT_STATIC_FIELD: "constant",
DEFAULT_FUNCTION_DECLARATION: "functionDeclaration",
DEFAULT_FUNCTION_CALL: "functionCall",
DEFAULT_PREDEFINED_SYMBOL: "predefined",
DEFAULT_CLASS_NAME: "className",
DEFAULT_INTERFACE_NAME: "className",
DEFAULT_NUMBER: "number",
DEFAULT_STRING: "string",
DEFAULT_VALID_STRING_ESCAPE: "escape",
DEFAULT_INVALID_STRING_ESCAPE: "invalid",
DEFAULT_LINE_COMMENT: "lineComment",
DEFAULT_BLOCK_COMMENT: "lineComment",
DEFAULT_DOC_COMMENT: "docComment",
DEFAULT_METADATA: "metadata",
DEFAULT_OPERATION_SIGN: "operator",
DEFAULT_BRACES: "operator",
DEFAULT_BRACKETS: "operator",
DEFAULT_PARENTHS: "operator",
DEFAULT_DOT: "operator",
DEFAULT_COMMA: "operator",
DEFAULT_SEMICOLON: "operator",
DEFAULT_MARKUP_TAG: "markupTag",
DEFAULT_MARKUP_ATTRIBUTE: "markupAttribute",
BAD_CHARACTER: "invalid",
TODO_DEFAULT_ATTRIBUTES: "todo",
SEARCH_RESULT_ATTRIBUTES: "searchResult",
TEXT_SEARCH_RESULT_ATTRIBUTES: "searchResultCurrent",
WRITE_SEARCH_RESULT_ATTRIBUTES: "identifierUnderCaretWrite",
IDENTIFIER_UNDER_CARET_ATTRIBUTES: "identifierUnderCaret",
MATCHED_BRACE_ATTRIBUTES: "matchedBrace",
ERRORS_ATTRIBUTES: "error",
WARNING_ATTRIBUTES: "warning",
WEAK_WARNING_ATTRIBUTES: "weakWarning",
NOT_USED_ELEMENT_ATTRIBUTES: "unused",
FOLDED_TEXT_ATTRIBUTES: "folded",
INLAY_DEFAULT: "inlay",
PY_SELF_PARAMETER: "pySelf",
PY_KEYWORD_ARGUMENT: "pyKeywordArg",
PY_DOC_STRING: "pyDocstring",
PY_BUILTIN_NAME: "pyBuiltin",
PY_DECORATOR: "pyDecorator",
MARKDOWN_HEADER_LEVEL_1: "mdHeading",
MARKDOWN_CODE_SPAN: "mdCode",
MARKDOWN_LINK_TEXT: "mdLink",
REGEXP: "regexp",
};
const LANGUAGE_KEYS: Record<string, string> = {
KEYWORD: "keyword",
NUMBER: "number",
STRING: "string",
STRING_TEXT: "string",
VALID_STRING_ESCAPE: "escape",
LINE_COMMENT: "lineComment",
BLOCK_COMMENT: "lineComment",
DOC_COMMENT: "docComment",
CLASS: "className",
CLASS_NAME: "className",
INTERFACE: "className",
ENUM: "className",
TYPE_PARAMETER: "className",
ANNOTATION_NAME: "metadata",
ANNOTATION: "metadata",
LOCAL_VARIABLE: "identifier",
PARAMETER: "parameter",
INSTANCE_FIELD: "field",
STATIC_FIELD: "constant",
STATIC_FINAL_FIELD: "constant",
PROPERTY: "field",
BACKING_FIELD_VARIABLE: "field",
FUNCTION_DECLARATION: "functionDeclaration",
METHOD_DECLARATION: "functionDeclaration",
FUNCTION_CALL: "functionCall",
METHOD_CALL: "functionCall",
STATIC_METHOD: "functionCall",
CONSTRUCTOR_CALL: "className",
OPERATION_SIGN: "operator",
BRACES: "operator",
BRACKETS: "operator",
PARENTH: "operator",
PARENTHS: "operator",
};
for (const [name, id] of Object.entries(LANGUAGE_KEYS)) {
ATTRIBUTE_NAMES[`JAVA_${name}`] = `java.${id}`;
ATTRIBUTE_NAMES[`KOTLIN_${name}`] = `kotlin.${id}`;
ATTRIBUTE_NAMES[name] = id;
}
Object.assign(ATTRIBUTE_NAMES, {
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",
});
Object.assign(ATTRIBUTE_NAMES, {
"PY.ANNOTATION": "pyAnnotation",
"PY.PARENTHS": "pyParentheses",
"PY.BRACKETS": "pyBrackets",
"PY.BRACES": "pyBraces",
"PY.KEYWORD": "keyword",
"PY.FUNC_DEFINITION": "functionDeclaration",
"PY.NESTED_FUNC_DEFINITION": "pyNestedFunction",
"PY.METHOD_CALL": "pyMethodCall",
"PY.DOC_COMMENT": "pyDocstring",
"PY.BUILTIN_NAME": "pyBuiltin",
"PY.DECORATOR": "pyDecorator",
"PY.SELF_PARAMETER": "pySelf",
"PY.KEYWORD_ARGUMENT": "pyKeywordArg",
"PY.PREDEFINED_DEFINITION": "pyPredefinedDefinition",
"PY.PREDEFINED_USAGE": "pyPredefinedUsage",
"PY.STRING.B": "pyBytes",
"PY.STRING.F": "pyFString",
"PY.VALID_STRING_ESCAPE": "escape",
"PY.OPERATION_SIGN": "operator",
});
const COLOR_NAMES: Record<string, [string, "foreground" | "background"]> = {
CARET_COLOR: ["caret", "foreground"],
CARET_ROW_COLOR: ["caretRow", "background"],
SELECTION_BACKGROUND: ["selection", "background"],
INACTIVE_SELECTION_BACKGROUND: ["selectionInactive", "background"],
GUTTER_BACKGROUND: ["gutter", "background"],
LINE_NUMBERS_COLOR: ["lineNumber", "foreground"],
LINE_NUMBER_ON_CARET_ROW_COLOR: ["lineNumberCaret", "foreground"],
WHITESPACES: ["whitespaces", "foreground"],
INDENT_GUIDE: ["indentGuide", "foreground"],
RIGHT_MARGIN_COLOR: ["rightMargin", "foreground"],
METHOD_SEPARATORS_COLOR: ["methodSeparator", "foreground"],
};
// Preserve editor attributes without a standard exchange name on our own round trip.
for (const attr of ATTRIBUTES)
if (!Object.values(ATTRIBUTE_NAMES).includes(attr.id)) ATTRIBUTE_NAMES[`OPENIDEA_${attr.id}`] = attr.id;
const EFFECTS: Record<string, TextAttributesOverride["effect"]> = {
"0": "underline",
"3": "strikeout",
"4": "wave",
"5": "underline",
"6": "underline",
};
const children = (el: Element, tag: string) => Array.from(el.children).filter((e) => e.tagName === tag);
const color = (v: string | null): string | undefined =>
v && /^[0-9a-f]{1,6}$/i.test(v) ? `#${v.padStart(6, "0").toLowerCase()}` : undefined;
function orderedAttributes(section: Element): Element[] {
const entries = children(section, "option");
if (!entries.some((e) => e.getAttribute("name")?.startsWith("PY."))) return entries;
const priority = (e: Element) =>
e.getAttribute("name")?.startsWith("PY.") ? 2 : e.getAttribute("name")?.startsWith("DEFAULT_") ? 1 : 0;
return entries
.filter((e) => e.getAttribute("name") !== "DEFAULT_BLOCK_COMMENT")
.sort((a, b) => priority(a) - priority(b));
}
export function parseIcls(xml: string): { scheme: CustomColorScheme; applied: number; skipped: number } {
const doc = new DOMParser().parseFromString(xml, "application/xml");
const root = doc.documentElement;
if (
doc.querySelector("parsererror") ||
doc.doctype ||
root.tagName !== "scheme" ||
!root.getAttribute("name")?.trim()
)
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) => {
attributes[id] = { ...attributes[id], ...value };
};
for (const section of Array.from(root.children)) {
if (section.tagName === "colors")
for (const entry of children(section, "option")) {
const name = entry.getAttribute("name") ?? "";
const target = Object.hasOwn(COLOR_NAMES, name) ? COLOR_NAMES[name] : undefined;
const c = color(entry.getAttribute("value"));
if (target && c) {
merge(target[0], { [target[1]]: c });
applied++;
} else skipped++;
}
if (section.tagName === "attributes")
for (const entry of orderedAttributes(section)) {
const name = entry.getAttribute("name") ?? "";
const id = Object.hasOwn(ATTRIBUTE_NAMES, name) ? ATTRIBUTE_NAMES[name] : undefined;
if (!id) {
skipped++;
continue;
}
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 });
if (name.startsWith("PY.")) merge(id, { effect: "none" });
for (const option of children(value, "option")) {
const name = option.getAttribute("name");
const raw = option.getAttribute("value");
const c = color(raw);
const field =
name === "FOREGROUND"
? "foreground"
: name === "BACKGROUND"
? "background"
: name === "EFFECT_COLOR"
? "effectColor"
: undefined;
const supported =
field === "foreground"
? !!attr.fg
: field === "background"
? !!attr.bg || !!attr.token
: !!attr.effect || !!attr.token;
if (field && c && supported) {
merge(id, { [field]: c });
applied++;
} 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)) {
merge(id, { effect: EFFECTS[raw] });
applied++;
} else skipped++;
}
}
}
const parentName = root.getAttribute("parent_scheme") ?? "";
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 = 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 };
}
/** Name collisions include built-ins; imports never replace a built-in. */
export function importScheme(customs: CustomColorSchemes, scheme: CustomColorScheme, overwrite = false) {
const existing = Object.entries(customs).find(([, s]) => s.name === scheme.name);
const names = new Set(listSchemes(customs).map((s) => s.name));
let name = scheme.name;
if (!(overwrite && existing)) for (let i = 2; names.has(name); i++) name = `${scheme.name} (${i})`;
const id = overwrite && existing ? existing[0] : `custom-${crypto.randomUUID()}`;
return { id, customs: { ...customs, [id]: { ...scheme, name } } };
}
export function deleteScheme(
customs: CustomColorSchemes,
overrides: Record<string, Record<string, TextAttributesOverride>>,
selected: string,
id: string,
) {
return {
customs: Object.fromEntries(Object.entries(customs).filter(([key]) => key !== id)),
overrides: Object.fromEntries(Object.entries(overrides).filter(([key]) => key !== id)),
selected: selected === id ? "theme-default" : selected,
};
}
const exportColor = (c: string) =>
c.length === 4
? c
.slice(1)
.split("")
.map((ch) => ch + ch)
.join("")
: c.slice(1);
const escapeXml = (v: string) =>
v.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/"/g, "&quot;").replace(/>/g, "&gt;");
export function exportIcls(
id: string,
customs: CustomColorSchemes,
overrides: Record<string, TextAttributesOverride> = {},
): string {
const custom = customSchemes(customs)[id];
const name = listSchemes(customs).find((s) => s.id === id)?.name ?? id;
const tokens = resolvedSchemeTokens(id, customs);
const values = (attr: (typeof ATTRIBUTES)[number]): TextAttributesOverride => ({
...(attr.token
? {
bold: /bold|[6-9]00/.test(tokens.get(`--syn-${attr.token}-bold`) ?? ""),
italic:
(tokens.get(`--syn-${attr.token}-italic`) ??
(attr.token === "comment"
? tokens.get("--syn-comment-style")
: (attr.italicOnly && attr.defaultItalic !== false) || ["doc-comment", "docstring"].includes(attr.token)
? "italic"
: "normal")) === "italic",
}
: {}),
...(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) } : {}),
...custom?.attributes[attr.id],
...overrides[attr.id],
});
const colors: string[] = [];
const attributes: string[] = [];
for (const [key, [id, field]] of Object.entries(COLOR_NAMES)) {
const attr = ATTRIBUTES.find((a) => a.id === id);
const c = attr && values(attr)[field];
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);
const opts: string[] = [];
for (const [key, field] of [
["FOREGROUND", "foreground"],
["BACKGROUND", "background"],
["EFFECT_COLOR", "effectColor"],
] 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>`);
}
const parent = custom?.parent ?? (isBuiltinTheme(id) ? themeKind(id) : "dark");
return `<scheme name="${escapeXml(name)}" version="142" parent_scheme="${parent === "dark" ? "Darcula" : "Default"}">\n <colors>\n${colors.join("\n")}\n </colors>\n <attributes>\n${attributes.join("\n")}\n </attributes>\n</scheme>\n`;
}