mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 08:52:12 +00:00
fix(code-insight): keep usage highlights readable (#855)
Usage previews rendered the matched occurrence through tokenSpans, so the token colour rules outranked the inherited --fg-search-match and left syntax colours on the search-match background (about 1.1:1 in Islands Light). Render the match as plain text so it takes the search-match foreground; syntax colours stay on the surrounding code. Covered by unit tests and a Playwright contrast check across light, dark, high-contrast and mixed editor schemes. Fixes #851 Co-authored-by: kkMihai <[email protected]>
This commit is contained in:
1 parent
e3f8932e21
commit
64b8b5cea9
3 files changed
+87
-3
No files matched your search
@@ -0,0 +1,81 @@
|
||||
import { expect, test } from "./fixtures";
|
||||
|
||||
const themes = [
|
||||
{ theme: "light", scheme: "theme" },
|
||||
{ theme: "dark", scheme: "theme" },
|
||||
{ theme: "dark-hc", scheme: "theme" },
|
||||
{ theme: "light", scheme: "dark" },
|
||||
{ theme: "dark", scheme: "light" },
|
||||
] as const;
|
||||
|
||||
for (const { theme, scheme } of themes) {
|
||||
test(`usage highlights have readable text with ${theme} theme and ${scheme} scheme`, async ({
|
||||
workbench: page,
|
||||
}, info) => {
|
||||
await page.evaluate(
|
||||
async ({ theme, scheme }) => {
|
||||
const themePath = "/src/workbench/theme.ts";
|
||||
const schemePath = "/src/platform/colorScheme.ts";
|
||||
const usagesPath = "/src/features/code-insight/usages.ts";
|
||||
const { applyThemeAttributes } = await import(themePath);
|
||||
const { schemeCss } = await import(schemePath);
|
||||
const { useUsagesPopup } = await import(usagesPath);
|
||||
applyThemeAttributes(theme);
|
||||
const style = document.createElement("style");
|
||||
style.textContent = schemeCss(scheme);
|
||||
document.head.append(style);
|
||||
const symbol = "trainingSessionFetch";
|
||||
const texts = [
|
||||
`export function ${symbol}() {}`,
|
||||
`import { ${symbol} } from './api';`,
|
||||
`// Call ${symbol} here`,
|
||||
`const name = '${symbol}';`,
|
||||
];
|
||||
useUsagesPopup.setState({
|
||||
open: true,
|
||||
anchor: { x: 100, y: 100 },
|
||||
symbol,
|
||||
groups: texts.map((text, index) => ({
|
||||
path: `/contrast/example-${index}.ts`,
|
||||
usages: [
|
||||
{
|
||||
path: `/contrast/example-${index}.ts`,
|
||||
line: 0,
|
||||
col: text.indexOf(symbol),
|
||||
endCol: text.indexOf(symbol) + symbol.length,
|
||||
text,
|
||||
},
|
||||
],
|
||||
})),
|
||||
note: null,
|
||||
});
|
||||
},
|
||||
{ theme, scheme },
|
||||
);
|
||||
const popup = page.getByRole("dialog", { name: "Usages", exact: true });
|
||||
await expect(popup.locator(".tok-comment").first()).toBeVisible();
|
||||
await expect(popup.locator("mark")).toHaveCount(4);
|
||||
const ratios = await popup.locator("mark").evaluateAll((marks) => {
|
||||
const luminance = (color: string) => {
|
||||
const channels = color
|
||||
.match(/[\d.]+/g)!
|
||||
.slice(0, 3)
|
||||
.map(Number)
|
||||
.map((value) => {
|
||||
const channel = value / 255;
|
||||
return channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4;
|
||||
});
|
||||
return channels[0]! * 0.2126 + channels[1]! * 0.7152 + channels[2]! * 0.0722;
|
||||
};
|
||||
return marks.flatMap((mark) => {
|
||||
const background = luminance(getComputedStyle(mark).backgroundColor);
|
||||
return [mark, ...mark.querySelectorAll("*")].map((node) => {
|
||||
const foreground = luminance(getComputedStyle(node).color);
|
||||
return (Math.max(background, foreground) + 0.05) / (Math.min(background, foreground) + 0.05);
|
||||
});
|
||||
});
|
||||
});
|
||||
for (const ratio of ratios) expect(ratio).toBeGreaterThanOrEqual(4.5);
|
||||
await popup.screenshot({ path: info.outputPath("usage-highlights.png") });
|
||||
});
|
||||
}
|
||||
@@ -54,7 +54,7 @@ export function UsagePreview({ usage }: { usage: Usage }) {
|
||||
return (
|
||||
<span className={styles.preview}>
|
||||
{tokenSpans(text, 0, from, tokens, "b:")}
|
||||
<mark className={styles.match}>{tokenSpans(text, from, to, tokens, "m:")}</mark>
|
||||
<mark className={styles.match}>{text.slice(from, to)}</mark>
|
||||
{tokenSpans(text, to, text.length, tokens, "a:")}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -17,14 +17,17 @@ describe("UsagePreview", () => {
|
||||
);
|
||||
await waitFor(() => expect(container.querySelector(".tok-keyword")?.textContent).toBe("const"));
|
||||
expect(container.querySelector("mark")?.textContent).toBe("foo");
|
||||
expect(container.querySelector("mark")?.childElementCount).toBe(0);
|
||||
expect(container.textContent).toBe("const x = foo();");
|
||||
});
|
||||
|
||||
it("colours a usage inside a doc comment as a comment", async () => {
|
||||
it("keeps comment colours outside the match and the search foreground inside it", async () => {
|
||||
const { container } = render(
|
||||
<UsagePreview usage={{ path: "/p/a.ts", line: 1, col: 8, endCol: 11, text: " * Uses foo()" }} />,
|
||||
);
|
||||
await waitFor(() => expect(container.querySelector("mark .tok-comment")?.textContent).toBe("foo"));
|
||||
await waitFor(() => expect(container.querySelector(".tok-comment")?.textContent).toBe("* Uses "));
|
||||
expect(container.querySelector("mark")?.textContent).toBe("foo");
|
||||
expect(container.querySelector("mark")?.childElementCount).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user