mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-14 16:07:52 +00:00
feat(color scheme): edit text style for errors and warnings (#3154)
Errors and Warnings attributes (Error, Warning, Typo, Unused symbol, Deprecated symbol) now offer bold, italic and effect choices like syntax tokens, written to --wave-*/--fg-* variables the editor reads; Typo and Deprecated symbol are listed and previewed. Deprecated symbol keeps the text colour and draws only its effect; an effect set to none draws none. Custom and imported schemes keep the font style and effect of token-less attributes (Errors and Warnings, Git), and the preview shows them. Co-authored-by: muutot <92625353+muutot@users.noreply.github.com>
This commit is contained in:
@@ -235,12 +235,16 @@ const lintTheme = EditorView.theme({
|
||||
".cm-lintRange-error": {
|
||||
textDecoration: "var(--editor-error-decoration, underline wavy var(--danger))",
|
||||
WebkitTextDecoration: "var(--editor-error-decoration, underline wavy var(--danger))",
|
||||
fontWeight: "var(--wave-error-bold, inherit)",
|
||||
fontStyle: "var(--wave-error-italic, inherit)",
|
||||
textDecorationSkipInk: "none",
|
||||
textUnderlineOffset: "3px",
|
||||
},
|
||||
".cm-lintRange-warning": {
|
||||
textDecoration: "var(--editor-warning-decoration, underline wavy var(--warning))",
|
||||
WebkitTextDecoration: "var(--editor-warning-decoration, underline wavy var(--warning))",
|
||||
fontWeight: "var(--wave-warning-bold, inherit)",
|
||||
fontStyle: "var(--wave-warning-italic, inherit)",
|
||||
backgroundColor: "var(--editor-warning-background, transparent)",
|
||||
textDecorationSkipInk: "none",
|
||||
textUnderlineOffset: "3px",
|
||||
@@ -254,8 +258,10 @@ const lintTheme = EditorView.theme({
|
||||
".cm-lintRange-hint": { textDecoration: "none" },
|
||||
// Typo level: IDEA's green wavy underline.
|
||||
".cm-lintRange.cm-lsp-typo": {
|
||||
textDecoration: "underline wavy color-mix(in srgb, var(--success) 85%, transparent)",
|
||||
WebkitTextDecoration: "underline wavy color-mix(in srgb, var(--success) 85%, transparent)",
|
||||
textDecoration: "var(--wave-typo-effect, underline wavy var(--wave-typo, var(--success)))",
|
||||
WebkitTextDecoration: "var(--wave-typo-effect, underline wavy var(--wave-typo, var(--success)))",
|
||||
fontWeight: "var(--wave-typo-bold, inherit)",
|
||||
fontStyle: "var(--wave-typo-italic, inherit)",
|
||||
textDecorationSkipInk: "none",
|
||||
textUnderlineOffset: "3px",
|
||||
},
|
||||
@@ -263,9 +269,16 @@ const lintTheme = EditorView.theme({
|
||||
textDecoration: "none",
|
||||
backgroundColor: "transparent",
|
||||
color: "var(--fg-unused, var(--fg-faint))",
|
||||
fontWeight: "var(--fg-unused-bold, inherit)",
|
||||
fontStyle: "var(--fg-unused-italic, inherit)",
|
||||
},
|
||||
".cm-lintRange.cm-lsp-unnecessary *": { color: "var(--fg-unused, var(--fg-faint))" },
|
||||
".cm-lintRange.cm-lsp-deprecated": { textDecoration: "line-through", textDecorationColor: "var(--fg-muted)" },
|
||||
".cm-lintRange.cm-lsp-deprecated": {
|
||||
textDecoration: "var(--fg-deprecated-effect, line-through var(--fg-deprecated, var(--fg-muted)))",
|
||||
WebkitTextDecoration: "var(--fg-deprecated-effect, line-through var(--fg-deprecated, var(--fg-muted)))",
|
||||
fontWeight: "var(--fg-deprecated-bold, inherit)",
|
||||
fontStyle: "var(--fg-deprecated-italic, inherit)",
|
||||
},
|
||||
// An error or warning on the same range (merged into one mark) keeps its underline.
|
||||
".cm-lintRange-error.cm-lsp-typo, .cm-lintRange-error.cm-lsp-unnecessary": {
|
||||
textDecoration: "var(--editor-error-decoration, underline wavy var(--danger))",
|
||||
|
||||
@@ -10,6 +10,7 @@ import { fontWeightVars, type CustomColorSchemes, type TextAttributesOverride }
|
||||
import {
|
||||
ATTRIBUTES,
|
||||
SCHEMES,
|
||||
EFFECT_CSS,
|
||||
overrideDeclarations,
|
||||
effectiveScheme,
|
||||
resolvedSchemeTokens,
|
||||
@@ -123,6 +124,8 @@ function previewVars(
|
||||
k.startsWith("--border-") ||
|
||||
k.startsWith("--ansi-") ||
|
||||
k.startsWith("--git-") ||
|
||||
k.startsWith("--wave-") ||
|
||||
k.startsWith("--editor-") ||
|
||||
k === "--caret"
|
||||
)
|
||||
vars[k] = v;
|
||||
@@ -137,20 +140,32 @@ function previewVars(
|
||||
|
||||
/** Text styled like an attribute in the preview. */
|
||||
function tokenStyle(attr: AttributeDef | undefined): CSSProperties {
|
||||
if (!attr?.token)
|
||||
return {
|
||||
const base = attr?.fg ?? attr?.effect;
|
||||
if (!attr?.token) {
|
||||
const colour: CSSProperties = {
|
||||
...(attr?.fg ? { color: `var(${attr.fg})` } : null),
|
||||
...(attr?.bg ? { backgroundColor: `var(${attr.bg})` } : null),
|
||||
// Token-less attributes with a font-style renderer carry it on `--<fg>-*`, the same
|
||||
// declarations `overrideDeclarations` emits for their overrides.
|
||||
...(attr?.fontStyle && (attr.fg ?? attr.effect)
|
||||
? {
|
||||
fontWeight: `var(${attr.fg ?? attr.effect}-bold, inherit)`,
|
||||
fontStyle: `var(${attr.fg ?? attr.effect}-italic, inherit)`,
|
||||
textDecoration: `var(${attr.effect ?? attr.fg}-effect, none)`,
|
||||
}
|
||||
};
|
||||
// Token-less attributes with a font-style renderer carry it on `--<fg|effect>-*`, the same
|
||||
// declarations `overrideDeclarations` emits for their overrides.
|
||||
if (!attr?.fontStyle || !base) return colour;
|
||||
const effectColor = `var(${attr.effect ?? attr.fg ?? "--fg-muted"})`;
|
||||
const effect: TextAttributesOverride["effect"] = attr.defaultEffect ?? (attr.effect ? "wave" : undefined);
|
||||
const decoration =
|
||||
effect && effect !== "none"
|
||||
? `var(${attr.effect ?? base}-effect, ${EFFECT_CSS[effect]?.(effectColor) ?? "none"})`
|
||||
: undefined;
|
||||
return {
|
||||
...colour,
|
||||
fontWeight: `var(${base}-bold, ${
|
||||
attr.defaultBold ? "var(--fw-code-bold, 700)" : "inherit"
|
||||
})` as CSSProperties["fontWeight"],
|
||||
fontStyle: `var(${base}-italic, ${attr.defaultItalic ? "italic" : "inherit"})`,
|
||||
...(decoration
|
||||
? { textDecoration: decoration, ...({ WebkitTextDecoration: decoration } as CSSProperties) }
|
||||
: null),
|
||||
};
|
||||
}
|
||||
if (attr.italicOnly)
|
||||
return { fontStyle: `var(--syn-${attr.token}-italic, ${attr.defaultItalic === false ? "normal" : "italic"})` };
|
||||
const t = attr.token;
|
||||
@@ -206,6 +221,11 @@ const SAMPLES: Record<string, CodeSample> = {
|
||||
[" ", "text"],
|
||||
["legacyCall", "unused"],
|
||||
],
|
||||
[
|
||||
["typo", "typo"],
|
||||
[" ", "text"],
|
||||
["deprecatedCall", "deprecated"],
|
||||
],
|
||||
[
|
||||
["bad\u203dchar", "invalid"],
|
||||
[" \u00b7 ", "whitespaces"],
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
ATTRIBUTES,
|
||||
CLEAR_COLOR,
|
||||
_setSchemeSource,
|
||||
overrideDeclarations,
|
||||
resolvedAttribute,
|
||||
schemeAfterThemeChange,
|
||||
schemeCss,
|
||||
@@ -341,4 +342,43 @@ describe("color schemes", () => {
|
||||
expect(v.codeStyle.languages.go).toEqual({ useTab: true });
|
||||
expect(v.general.softWrapFiles).toContain("*.md");
|
||||
});
|
||||
|
||||
it("styles Errors and Warnings attributes beyond colour", () => {
|
||||
const decls = overrideDeclarations({
|
||||
error: { bold: true, italic: true, effect: "strikeout", effectColor: "#ff0000" },
|
||||
unused: { italic: true },
|
||||
});
|
||||
const map = new Map(decls);
|
||||
expect(map.get("--wave-error-bold")).toBe("var(--fw-code-bold, 700)");
|
||||
expect(map.get("--wave-error-italic")).toBe("italic");
|
||||
expect(map.get("--wave-error-effect")).toBe("line-through #ff0000");
|
||||
expect(map.get("--editor-error-decoration")).toBe("line-through #ff0000");
|
||||
expect(map.get("--wave-error")).toBe("#ff0000");
|
||||
expect(map.get("--fg-unused-italic")).toBe("italic");
|
||||
const none = new Map(overrideDeclarations({ warning: { effect: "none" }, deprecated: { effect: "underline" } }));
|
||||
expect(none.get("--editor-warning-decoration")).toBe("none");
|
||||
expect(none.get("--fg-deprecated-effect")).toBe("underline var(--fg-deprecated)");
|
||||
});
|
||||
|
||||
it("keeps the font style of Errors and Warnings in custom schemes", () => {
|
||||
const customs = {
|
||||
"custom-x": {
|
||||
name: "X",
|
||||
parent: "dark" as const,
|
||||
attributes: { error: { bold: true }, deprecated: { effect: "underline" as const } },
|
||||
},
|
||||
};
|
||||
const css = schemeCss("custom-x", undefined, null, customs);
|
||||
expect(css).toContain("--wave-error-bold: var(--fw-code-bold, 700)");
|
||||
expect(css).toContain("--fg-deprecated-effect: underline var(--fg-deprecated)");
|
||||
});
|
||||
|
||||
it("lists typo and deprecated symbol under Errors and Warnings", () => {
|
||||
const ids = ATTRIBUTES.filter((a) => a.path.join("/") === "General/Errors and Warnings").map((a) => a.id);
|
||||
expect(ids).toEqual(["error", "warning", "weakWarning", "typo", "unused", "deprecated", "invalid"]);
|
||||
expect(resolvedAttribute("dark", "typo")).toMatchObject({ effect: "wave" });
|
||||
expect(resolvedAttribute("dark", "deprecated")).toMatchObject({ effect: "strikeout", effectColor: "#8c8f95" });
|
||||
expect(resolvedAttribute("dark", "deprecated").foreground).toBeUndefined();
|
||||
expect(resolvedAttribute("dark", "error")).toMatchObject({ effect: "wave", bold: false, italic: false });
|
||||
});
|
||||
});
|
||||
|
||||
+70
-17
@@ -31,7 +31,8 @@ export interface AttributeDef {
|
||||
token?: string;
|
||||
/** A semantic modifier whose only configurable attribute is italic. */
|
||||
italicOnly?: boolean;
|
||||
/** The attribute's renderer applies font style and effects (tokens, the Git surfaces). */
|
||||
/** The attribute's renderer applies font style and effects (tokens, the Git surfaces,
|
||||
* Errors and Warnings): overrides land on `--<fg|effect>-*`. */
|
||||
fontStyle?: true;
|
||||
defaultBold?: boolean;
|
||||
defaultItalic?: boolean;
|
||||
@@ -139,10 +140,46 @@ const BASE_ATTRIBUTES: readonly AttributeDef[] = [
|
||||
path: ["General", "Search Results"],
|
||||
bg: "--bg-search-match-current",
|
||||
},
|
||||
{ id: "error", name: "Error", path: ["General", "Errors and Warnings"], effect: "--wave-error" },
|
||||
{ id: "warning", name: "Warning", path: ["General", "Errors and Warnings"], effect: "--wave-warning" },
|
||||
{
|
||||
id: "error",
|
||||
name: "Error",
|
||||
path: ["General", "Errors and Warnings"],
|
||||
effect: "--wave-error",
|
||||
defaultEffect: "wave",
|
||||
fontStyle: true,
|
||||
},
|
||||
{
|
||||
id: "warning",
|
||||
name: "Warning",
|
||||
path: ["General", "Errors and Warnings"],
|
||||
effect: "--wave-warning",
|
||||
defaultEffect: "wave",
|
||||
fontStyle: true,
|
||||
},
|
||||
{ id: "weakWarning", name: "Weak warning", path: ["General", "Errors and Warnings"], fg: "--fg-weak-warning" },
|
||||
{ id: "unused", name: "Unused symbol", path: ["General", "Errors and Warnings"], fg: "--fg-unused" },
|
||||
{
|
||||
id: "typo",
|
||||
name: "Typo",
|
||||
path: ["General", "Errors and Warnings"],
|
||||
effect: "--wave-typo",
|
||||
defaultEffect: "wave",
|
||||
fontStyle: true,
|
||||
},
|
||||
{
|
||||
id: "unused",
|
||||
name: "Unused symbol",
|
||||
path: ["General", "Errors and Warnings"],
|
||||
fg: "--fg-unused",
|
||||
fontStyle: true,
|
||||
},
|
||||
{
|
||||
id: "deprecated",
|
||||
name: "Deprecated symbol",
|
||||
path: ["General", "Errors and Warnings"],
|
||||
effect: "--fg-deprecated",
|
||||
defaultEffect: "strikeout",
|
||||
fontStyle: true,
|
||||
},
|
||||
syn("invalid", "Bad character", ["General", "Errors and Warnings"], "invalid"),
|
||||
syn("todo", "TODO defaults", ["General", "Code"], "todo"),
|
||||
{ id: "inlineValue", name: "Inline debugger values", path: ["Debugger"], fg: "--syn-inline-value" },
|
||||
@@ -339,6 +376,11 @@ export const ATTRIBUTES: readonly AttributeDef[] = [
|
||||
/** Variables a scheme sets: every attribute variable plus the editor tokens derived from them. */
|
||||
const SCHEME_VARS = new Set<string>([
|
||||
...ATTRIBUTES.flatMap((a) => [a.fg, a.bg, a.effect].filter((v): v is string => !!v)),
|
||||
// Font style of token-less attributes (`overrideDeclarations` writes them for custom schemes too).
|
||||
...ATTRIBUTES.flatMap((a) => {
|
||||
const base = a.fg ?? a.effect;
|
||||
return !a.token && a.fontStyle && base ? [`${base}-bold`, `${base}-italic`, `${a.effect ?? base}-effect`] : [];
|
||||
}),
|
||||
"--syn-comment-style",
|
||||
"--syn-doc-comment-tag-effect",
|
||||
"--editor-error-decoration",
|
||||
@@ -477,7 +519,7 @@ export function boldWeight(bold: boolean): string {
|
||||
return bold ? "var(--fw-code-bold, 700)" : "var(--fw-code, 400)";
|
||||
}
|
||||
|
||||
const EFFECT_CSS: Record<string, (c: string) => string> = {
|
||||
export const EFFECT_CSS: Record<string, (c: string) => string> = {
|
||||
underline: (c) => `underline ${c}`,
|
||||
wave: (c) => `underline wavy ${c}`,
|
||||
strikeout: (c) => `line-through ${c}`,
|
||||
@@ -507,9 +549,12 @@ export function overrideDeclarations(
|
||||
}
|
||||
if (setOrCleared(o.background) && attr.bg) out.push([attr.bg, o.background]);
|
||||
if (setOrCleared(o.effectColor) && attr.effect) out.push([attr.effect, o.effectColor]);
|
||||
if (attr.effect && setOrCleared(o.effectColor)) {
|
||||
if (attr.id === "error") out.push(["--editor-error-decoration", `underline wavy ${o.effectColor}`]);
|
||||
if (attr.id === "warning") out.push(["--editor-warning-decoration", `underline wavy ${o.effectColor}`]);
|
||||
if (attr.effect && (setOrCleared(o.effectColor) || o.effect)) {
|
||||
const style = o.effect ?? attr.defaultEffect ?? "wave";
|
||||
const color = setOrCleared(o.effectColor) ? o.effectColor! : `var(${attr.effect})`;
|
||||
const decoration = EFFECT_CSS[style]?.(color) ?? "none";
|
||||
if (attr.id === "error") out.push(["--editor-error-decoration", decoration]);
|
||||
if (attr.id === "warning") out.push(["--editor-warning-decoration", decoration]);
|
||||
}
|
||||
if (attr.token) {
|
||||
const t = attr.token;
|
||||
@@ -534,7 +579,9 @@ export function overrideDeclarations(
|
||||
if (o.effect)
|
||||
out.push([
|
||||
`${attr.effect ?? base}-effect`,
|
||||
EFFECT_CSS[o.effect]?.(setOrCleared(o.effectColor) ? o.effectColor! : "currentColor") ?? "none",
|
||||
EFFECT_CSS[o.effect]?.(
|
||||
setOrCleared(o.effectColor) ? o.effectColor! : attr.effect ? `var(${attr.effect})` : "currentColor",
|
||||
) ?? "none",
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -646,14 +693,20 @@ export function resolvedAttribute(
|
||||
const italicCss =
|
||||
vars.get(`--syn-${token}-italic`) ?? (token === "comment" ? vars.get("--syn-comment-style") : undefined);
|
||||
out.italic = own?.italic ?? italicFromCss(italicCss) ?? attr.defaultItalic ?? false;
|
||||
} else if (attr.fontStyle) {
|
||||
const base = attr.fg ?? attr.effect;
|
||||
const bold = base ? vars.get(`${base}-bold`) : undefined;
|
||||
const italic = base ? vars.get(`${base}-italic`) : undefined;
|
||||
if (bold !== undefined) out.bold = boldFromCss(bold) ?? false;
|
||||
if (italic !== undefined) out.italic = italicFromCss(italic) ?? false;
|
||||
} else if (effect) out.effect = effect;
|
||||
if (token && effect) out.effect = effect;
|
||||
if (effect) out.effect = effect;
|
||||
} else {
|
||||
// Token-less attributes whose renderer applies font style (the Git segments, Errors and
|
||||
// Warnings) carry their overrides on `--<fg|effect>-*`, the same declarations
|
||||
// `overrideDeclarations` emits.
|
||||
if (attr.fontStyle) {
|
||||
const base = attr.fg ?? attr.effect;
|
||||
const bold = base ? vars.get(`${base}-bold`) : undefined;
|
||||
const italic = base ? vars.get(`${base}-italic`) : undefined;
|
||||
out.bold = boldFromCss(bold) ?? attr.defaultBold ?? false;
|
||||
out.italic = italicFromCss(italic) ?? attr.defaultItalic ?? false;
|
||||
}
|
||||
if (effect) out.effect = effect;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@@ -204,6 +204,8 @@ html {
|
||||
--fg-fold-marker: #6f737a;
|
||||
--fg-whitespace: #6f737a;
|
||||
--fg-unused: #6f737a;
|
||||
--wave-typo: #59a869;
|
||||
--fg-deprecated: #8c8f95;
|
||||
--bg-identifier-under-caret: #373b39;
|
||||
--bg-identifier-under-caret-write: #402f33;
|
||||
--fg-inlay: #868a91;
|
||||
@@ -643,6 +645,8 @@ html {
|
||||
--fg-fold-marker: #8b8e94;
|
||||
--fg-whitespace: #b5b7bd;
|
||||
--fg-unused: #8c8c8c;
|
||||
--wave-typo: #3d9950;
|
||||
--fg-deprecated: #6c707e;
|
||||
--bg-identifier-under-caret: #edebfc;
|
||||
--bg-identifier-under-caret-write: #fce8f4;
|
||||
--fg-inlay: #73767c;
|
||||
@@ -935,6 +939,8 @@ html {
|
||||
--fg-fold-marker: #9fa2a9;
|
||||
--fg-whitespace: #8b8e95;
|
||||
--fg-unused: #8b8e95;
|
||||
--wave-typo: #6cbb7d;
|
||||
--fg-deprecated: #b6b8be;
|
||||
--bg-identifier-under-caret: #3f4a44;
|
||||
--bg-identifier-under-caret-write: #553840;
|
||||
--fg-inlay: #ccced3;
|
||||
@@ -1147,6 +1153,8 @@ html {
|
||||
--fg-fold-marker: var(--palette-muted);
|
||||
--fg-whitespace: var(--palette-muted);
|
||||
--fg-unused: var(--palette-muted);
|
||||
--wave-typo: var(--palette-green);
|
||||
--fg-deprecated: var(--palette-muted);
|
||||
--fg-inlay: var(--palette-muted);
|
||||
--fg-weak-warning: var(--palette-muted);
|
||||
--syn-inline-value: var(--palette-muted);
|
||||
|
||||
Reference in New Issue
Block a user