From 566b4fdeb10087ce549c1444d1e97fa6612f60f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=83=A1=E7=BF=80?= <1020327414@qq.com> Date: Tue, 27 Oct 2026 14:27:24 +0800 Subject: [PATCH] Make static symbol italics configurable (#70) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Python language servers mark class attributes `static`, so their declarations and `self.` references rendered italic, unlike PyCharm. Adds Color Scheme attributes for static symbols: Language Defaults › Identifiers › Static symbol (italic by default, as IDEA's static members) and Python › Static symbol (upright by default), applied to semantic tokens with the `static` modifier. Verified in the app with basedpyright: a class attribute renders upright by default and italic when Python › Static symbol › Italic is on, applied live. Maintainer follow-up: formatted colorScheme.tsx. https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn --- src/editor/lsp/features.test.ts | 6 +++++ src/editor/lsp/semantic.ts | 5 ++-- src/editor/lsp/theme.ts | 3 ++- .../settings/pages/editor/colorScheme.tsx | 27 ++++++++++++++----- src/platform/colorScheme.test.ts | 4 +++ src/platform/colorScheme.ts | 5 ++++ 6 files changed, 41 insertions(+), 9 deletions(-) diff --git a/src/editor/lsp/features.test.ts b/src/editor/lsp/features.test.ts index 1f23db661..cd0dac840 100644 --- a/src/editor/lsp/features.test.ts +++ b/src/editor/lsp/features.test.ts @@ -26,6 +26,12 @@ describe("semanticRanges", () => { { from: 34, to: 37, className: "cm-sem-constant" }, ]); }); + + it("applies the Python-specific static style without changing other languages", () => { + const input = { tokenTypes: ["property"], tokenModifiers: ["static"], data: [0, 0, 5, 0, 1] }; + expect(semanticRanges(doc, input, false, "python")[0]?.className).toBe("cm-sem-property cm-sem-static cm-sem-python-static"); + expect(semanticRanges(doc, input, false, "java")[0]?.className).toBe("cm-sem-property cm-sem-static"); + }); }); describe("semantic highlighting (rainbow identifiers)", () => { diff --git a/src/editor/lsp/semantic.ts b/src/editor/lsp/semantic.ts index 5ab2a19fb..18aa60af2 100644 --- a/src/editor/lsp/semantic.ts +++ b/src/editor/lsp/semantic.ts @@ -80,7 +80,7 @@ export interface SemanticRange { } /** Decodes absolute `[line, col, len, type, modifiers]` quintuples into styled ranges. */ -export function semanticRanges(doc: Text, tokens: SemanticTokens, rainbow = false): SemanticRange[] { +export function semanticRanges(doc: Text, tokens: SemanticTokens, rainbow = false, language?: string): SemanticRange[] { const out: SemanticRange[] = []; const { data, tokenTypes, tokenModifiers } = tokens; // Servers with a `local` modifier (TypeScript) tell module-level variables apart. @@ -102,6 +102,7 @@ export function semanticRanges(doc: Text, tokens: SemanticTokens, rainbow = fals if (constant && (type === "variable" || type === "property")) base = "constant"; if (base && base !== "variable") classes.push(`cm-sem-${base}`); for (const n of names) if (MODIFIER_CLASS[n]) classes.push(`cm-sem-${MODIFIER_CLASS[n]}`); + if (language === "python" && names.includes("static")) classes.push("cm-sem-python-static"); const local = rainbow && !constant && @@ -163,7 +164,7 @@ const semanticPlugin = ViewPlugin.fromClass( } decorate(doc: Text, tokens: SemanticTokens | null) { this.last = { doc, tokens }; - const ranges = tokens ? semanticRanges(doc, tokens, this.rainbow) : []; + const ranges = tokens ? semanticRanges(doc, tokens, this.rainbow, docOf(this.view)?.language) : []; this.view.dispatch({ effects: setSemantic.of(Decoration.set(ranges.map((r) => markFor(r.className).range(r.from, r.to)), true)), }); diff --git a/src/editor/lsp/theme.ts b/src/editor/lsp/theme.ts index b04640910..0246c835e 100644 --- a/src/editor/lsp/theme.ts +++ b/src/editor/lsp/theme.ts @@ -348,7 +348,8 @@ export const lspTheme = EditorView.theme({ ".cm-sem-self": { color: "var(--syn-self, var(--syn-keyword))" }, ".cm-sem-keyword": { color: "var(--syn-keyword)", fontWeight: "var(--syn-keyword-bold, inherit)" }, ".cm-sem-builtin:not(.cm-sem-function)": { color: "var(--syn-builtin, inherit)" }, - ".cm-sem-static": { fontStyle: "italic" }, + ".cm-sem-static": { fontStyle: "var(--syn-static-italic, italic)" }, + ".cm-sem-python-static": { fontStyle: "var(--syn-python-static-italic, normal)" }, ".cm-sem-mutable": { textDecoration: "underline", textDecorationColor: "var(--fg-faint)", textUnderlineOffset: "3px" }, ".cm-sem-deprecated": { textDecoration: "line-through" }, // Semantic highlighting: after the base classes so a parameter's colour gives way, and over the diff --git a/src/features/settings/pages/editor/colorScheme.tsx b/src/features/settings/pages/editor/colorScheme.tsx index 05bc9fe91..128ab419a 100644 --- a/src/features/settings/pages/editor/colorScheme.tsx +++ b/src/features/settings/pages/editor/colorScheme.tsx @@ -87,6 +87,8 @@ function previewVars(id: SchemeId, o: Record | u /** Text styled like an attribute in the preview. */ function tokenStyle(attr: AttributeDef | undefined): CSSProperties { if (!attr?.token) return attr?.fg ? { color: `var(${attr.fg})` } : {}; + if (attr.italicOnly) + return { fontStyle: `var(--syn-${attr.token}-italic, ${attr.defaultItalic === false ? "normal" : "italic"})` }; const t = attr.token; return { color: `var(--syn-${t})`, @@ -111,6 +113,8 @@ const SAMPLE: Seg[][] = [ [" = ", "operator"], ["3", "number"], ], + [[" static_field", "staticSymbol"], [" = 3"]], + [[" python_field", "pyStaticSymbol"], [" = 3"]], [ [" def ", "keyword"], ["greet", "functionDeclaration"], @@ -150,7 +154,16 @@ function AttributeEditor({ }; return (
- {attr.token && ( + {attr.italicOnly && ( +
+ set({ italic: b === (attr.defaultItalic ?? true) ? undefined : b })} + label="Italic" + /> +
+ )} + {attr.token && !attr.italicOnly && ( <>
set({ bold: b || undefined })} label="Bold" /> @@ -159,11 +172,11 @@ function AttributeEditor({ )} {attr.fg && set({ foreground: c })} />} - {(attr.bg || attr.token) && ( + {(attr.bg || (attr.token && !attr.italicOnly)) && ( set({ background: c })} /> )} {attr.effect && set({ effectColor: c })} />} - {attr.token && ( + {attr.token && !attr.italicOnly && (
)} -
- Unchecked values are inherited from the scheme. -
+ {!attr.italicOnly && ( +
+ Unchecked values are inherited from the scheme. +
+ )}
); } diff --git a/src/platform/colorScheme.test.ts b/src/platform/colorScheme.test.ts index 9a3cde952..d41867cc6 100644 --- a/src/platform/colorScheme.test.ts +++ b/src/platform/colorScheme.test.ts @@ -26,6 +26,10 @@ 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 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); + expect(pythonItalic).toContain("--syn-python-static-italic: italic !important"); }); it("keeps editor weights out of the schemes", () => { diff --git a/src/platform/colorScheme.ts b/src/platform/colorScheme.ts index a0f829489..b89c30e47 100644 --- a/src/platform/colorScheme.ts +++ b/src/platform/colorScheme.ts @@ -29,6 +29,9 @@ export interface AttributeDef { effect?: string; /** Syntax token: bold / italic / background / effect come from `--syn--*`. */ token?: string; + /** A semantic modifier whose only configurable attribute is italic. */ + italicOnly?: boolean; + defaultItalic?: boolean; } const syn = (id: string, name: string, path: string[], token: string): AttributeDef => ({ @@ -81,6 +84,7 @@ export const ATTRIBUTES: readonly AttributeDef[] = [ syn("parameter", "Identifiers › Parameter", [LD], "param"), syn("constant", "Identifiers › Constant", [LD], "constant"), 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"), syn("functionCall", "Identifiers › Function call", [LD], "function-call"), syn("predefined", "Identifiers › Predefined symbol", [LD], "builtin"), @@ -98,6 +102,7 @@ export const ATTRIBUTES: readonly AttributeDef[] = [ ...[1, 2, 3].map((n) => syn(`bracket${n}`, `Rainbow brackets › Level ${n}`, [LD], `bracket-${n}`)), syn("pySelf", "Self parameter", ["Python"], "self"), + { id: "pyStaticSymbol", name: "Static symbol", path: ["Python"], token: "python-static", italicOnly: true, defaultItalic: false }, syn("pyKeywordArg", "Keyword argument", ["Python"], "keyword-arg"), syn("pyDocstring", "Docstring", ["Python"], "docstring"), syn("pyBuiltin", "Built-in name", ["Python"], "predefined"),