fix(go): enable semantic colors and Go scheme settings (#2017)

Go files showed no semantic colours because gopls semantic tokens were not requested, and Color Scheme had no Go page.

gopls is now initialised with semantic tokens, constants, package variables and package references get Go-specific roles, and Color Scheme has a Go page with a preview.

Siblings covered: Go-specific attributes inherit from Language Defaults, Reset restores them, Chinese labels show in the Color Scheme tree, and a user's own gopls settings are kept.

Co-authored-by: shohruh-genius <[email protected]>
This commit is contained in:
Shohruh Ismoilovandshohruh-genius authored and GitHub committed 2026-11-08 22:28:20 +08:00
1 parent e8cf744463
commit fff2c35181
15 files changed
+330 -5

No files matched your search

+14
View File
@@ -984,6 +984,20 @@ mod tests {
assert_eq!(res.line_comment.as_deref(), Some(";"));
}
#[test]
fn go_enables_semantic_tokens_when_the_server_initializes() {
let reg = reg();
let go = reg.get(&LanguageId::new("go")).unwrap();
assert_eq!(
go.language_servers[0]
.initialization_options
.as_ref()
.and_then(|options| options.get("semanticTokens"))
.and_then(toml::Value::as_bool),
Some(true)
);
}
#[test]
fn astro_server_reads_the_project_typescript() {
let reg = reg();
+43
View File
@@ -0,0 +1,43 @@
import { EditorView } from "@codemirror/view";
import { syntaxHighlighting } from "@codemirror/language";
import { commands } from "../src/platform/ipc";
import { setInvokeRoute } from "../src/platform/tauriCore";
import { schemeCss } from "../src/platform/colorScheme";
import { openideaHighlightStyle } from "../src/editor/core/cm/highlight";
import { findLanguageDescription, loadSupport } from "../src/editor/core/cm/languages";
import { lspDoc } from "../src/editor/lsp/lsp";
import { semanticExtension } from "../src/editor/lsp/semantic";
import { lspTheme } from "../src/editor/lsp/theme";
setInvokeRoute(() => null);
commands.lspSemanticTokens = async () => ({
status: "ok",
data: {
tokenTypes: ["variable", "namespace", "function"],
tokenModifiers: ["readonly", "static", "declaration", "deprecated"],
data: [0, 6, 5, 0, 1, 1, 4, 6, 0, 2, 2, 0, 3, 1, 0, 3, 5, 5, 2, 14],
},
});
const style = document.createElement("style");
style.textContent = schemeCss("dark", {
dark: {
"go.constant": { foreground: "#224466" },
"go.globalVariable": { foreground: "#335577" },
},
});
document.head.append(style);
const host = document.createElement("section");
host.dataset.language = "Go";
document.body.append(host);
const support = await loadSupport(findLanguageDescription("Go", "demo.go")!);
new EditorView({
parent: host,
doc: "const Limit = 3\nvar Global = 1\nfmt.Println(Global)\nfunc Greet() {}",
extensions: [
support!,
syntaxHighlighting(openideaHighlightStyle),
lspTheme,
lspDoc.of({ doc: 1, path: "demo.go", language: "go", uiMode: "Go", readOnly: false }),
semanticExtension(),
],
});
+9
View File
@@ -0,0 +1,9 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
</head>
<body>
<script type="module" src="./goHighlight.fixture.ts"></script>
</body>
</html>
+10
View File
@@ -0,0 +1,10 @@
import { expect, test } from "@playwright/test";
test("gopls constants and globals use the Go scheme attributes", async ({ page }) => {
await page.goto("/e2e/goHighlight.html");
await expect(page.locator(".cm-sem-constant")).toHaveText("Limit");
await expect(page.locator(".cm-sem-constant")).toHaveCSS("color", "rgb(34, 68, 102)");
await expect(page.locator(".cm-sem-global")).toHaveText("Global");
await expect(page.locator(".cm-sem-global")).toHaveCSS("color", "rgb(51, 85, 119)");
await expect(page.locator(".cm-sem-namespace")).toHaveText("fmt");
});
+1
View File
@@ -17,6 +17,7 @@ func main() {
[[language-servers]]
name = "gopls"
command = "gopls"
initialization-options = { semanticTokens = true }
root-markers = ["go.work", "go.mod"]
install-hint = "go install golang.org/x/tools/gopls@latest"
+65
View File
@@ -0,0 +1,65 @@
import { describe, expect, it } from "vitest";
import { Text } from "@codemirror/state";
import { semanticRanges } from "../index";
const doc = Text.of(["name"]);
describe("Go semantic tokens", () => {
it.each([
[["readonly"], ["cm-sem-constant"]],
[["static"], ["cm-sem-global", "cm-sem-static"]],
[
["readonly", "static"],
["cm-sem-constant", "cm-sem-static"],
],
[
["constant", "static"],
["cm-sem-constant", "cm-sem-static"],
],
])("classifies variable modifiers %j", (modifiers, classes) => {
const ranges = semanticRanges(
doc,
{
tokenTypes: ["variable"],
tokenModifiers: modifiers,
data: [0, 0, 4, 0, (1 << modifiers.length) - 1],
},
false,
"go",
);
expect(ranges[0]?.className.split(" ").sort()).toEqual(classes);
});
it("keeps local constants out of rainbow coloring and scopes readonly to Go", () => {
const tokens = { tokenTypes: ["variable"], tokenModifiers: ["readonly"], data: [0, 0, 4, 0, 1] };
expect(semanticRanges(doc, tokens, true, "go")[0]?.className).toBe("cm-sem-constant");
expect(semanticRanges(doc, tokens, false, "typescript")).toEqual([]);
expect(semanticRanges(doc, { ...tokens, data: [0, 0, 4, 0, 0] }, true, "go")[0]?.className).toContain(
"cm-sem-rainbow-",
);
expect(semanticRanges(doc, { ...tokens, tokenTypes: ["parameter"] }, true, "go")[0]?.className).toContain(
"cm-sem-rainbow-",
);
});
it("retains package, field and function roles from the common decoder", () => {
for (const [type, expected] of [
["namespace", "cm-sem-namespace"],
["property", "cm-sem-property"],
["function", "cm-sem-function"],
["type", "cm-sem-type"],
["typeParameter", "cm-sem-typeParameter"],
]) {
expect(
semanticRanges(doc, { tokenTypes: [type!], tokenModifiers: [], data: [0, 0, 4, 0, 0] }, false, "go")[0]
?.className,
).toBe(expected);
}
});
it("leaves branch labels and missing server tokens to syntax highlighting", () => {
expect(
semanticRanges(doc, { tokenTypes: ["label"], tokenModifiers: [], data: [0, 0, 4, 0, 0] }, false, "go"),
).toEqual([]);
expect(semanticRanges(doc, { tokenTypes: [], tokenModifiers: [], data: [] }, true, "go")).toEqual([]);
});
});
+13
View File
@@ -0,0 +1,13 @@
import { registerLanguageSemantics, type LanguageSemantics } from "../languages";
const goSemantics: LanguageSemantics = {
tokenClasses({ type, modifiers, classes }) {
if (type === "variable") {
if (modifiers.includes("readonly") && !classes.includes("cm-sem-constant")) classes.push("cm-sem-constant");
else if (modifiers.includes("static") && !classes.includes("cm-sem-constant")) classes.push("cm-sem-global");
}
},
isLocal: ({ type, modifiers }) => type === "parameter" || (type === "variable" && !modifiers.includes("readonly")),
};
registerLanguageSemantics("go", goSemantics);
+1
View File
@@ -29,6 +29,7 @@ export { languageSemantics } from "./languages";
import "./kotlin";
import "./python";
import "./php";
import "./go";
const semanticBackoff = featureBackoff();
@@ -1,8 +1,9 @@
import { readFileSync } from "node:fs";
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { act, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { executeAction } from "@/platform/actions";
import { CLEAR_COLOR } from "@/platform/colorScheme";
import { setLocale } from "@/platform/i18n";
import { useProject } from "@/platform/project";
import { DEFAULT_SETTINGS } from "../model/schema";
import { openSettings, stage, stagedValue, useSettingsDialog } from "../model/staging";
@@ -120,6 +121,49 @@ describe("scheme actions", () => {
});
});
describe("Go scheme attributes", () => {
it("translates the attribute tree and preview selection in Chinese", async () => {
await act(async () => {
await setLocale("zh");
});
try {
schemePage("editor.colorScheme.go");
expect(screen.getByRole("treeitem", { name: "标识符" })).toBeInTheDocument();
const preview = screen.getByLabelText("配色方案预览");
act(() => fireEvent.click(within(preview).getByText("fmt")));
expect(screen.getByRole("treeitem", { name: "包引用" })).toHaveAttribute("aria-selected", "true");
} finally {
await act(async () => {
await setLocale("en");
});
}
});
it("selects package references from the preview, inherits defaults and restores overrides", () => {
act(() => {
stage("editor.colorScheme", "global", "editor.colorScheme", "dark");
stage("editor.colorScheme", "global", "editor.colorOverrides", {
dark: { packageReference: { foreground: "#139839" } },
});
});
schemePage("editor.colorScheme.go");
const preview = screen.getByLabelText("Color scheme preview");
act(() => fireEvent.click(within(preview).getByText("fmt")));
expect(screen.getByRole("treeitem", { name: "Package reference" })).toHaveAttribute("aria-selected", "true");
expect(preview.style.getPropertyValue("--syn-namespace")).toBe("#139839");
act(() => fireEvent.click(screen.getByRole("checkbox", { name: "Bold" })));
expect(value("colorOverrides")).toEqual({
dark: { packageReference: { foreground: "#139839" }, "go.packageReference": { bold: true } },
});
expect(preview.style.getPropertyValue("--syn-namespace-bold")).toBe("var(--fw-code-bold, 700)");
act(() => fireEvent.click(screen.getByRole("button", { name: "Restore Defaults" })));
expect(value("colorOverrides")).toEqual({});
expect(screen.getByRole("checkbox", { name: "Bold" })).not.toBeChecked();
});
});
describe("scheme attribute state", () => {
beforeEach(() => schemePage("editor.colorScheme.language-defaults"));
it("shows checked attributes and inherited colours from the selected scheme", () => {
@@ -29,7 +29,7 @@ import { executeAction, registerActions } from "@/platform/actions";
import { call, commands } from "@/platform/ipc";
import { ask, confirm, showError } from "@/workbench/messages";
import { parseIcls, importScheme, deleteScheme, exportIcls } from "@/platform/icls";
import { tx } from "@/platform/i18n";
import { tx, useTx } from "@/platform/i18n";
import { selectPage } from "../../model/staging";
type Overrides = Record<string, Record<string, TextAttributesOverride>>;
@@ -689,6 +689,74 @@ const SAMPLES: Record<string, CodeSample> = {
[["}", "java.operator"]],
],
},
Go: {
code: [
[["package ", "go.keyword"], ["palette"]],
[
["import ", "go.keyword"],
['"fmt"', "go.string"],
],
[["// Retry a greeting", "go.lineComment"]],
[
["const ", "go.keyword"],
["MaxRetries", "go.constant"],
[" = ", "go.operator"],
["3", "go.number"],
],
[
["var ", "go.keyword"],
["DefaultName", "go.globalVariable"],
[" = ", "go.operator"],
['"Go"', "go.string"],
],
[
["type ", "go.keyword"],
["Greeter", "go.className"],
[" struct ", "go.keyword"],
["{", "go.operator"],
],
[
[" Name", "go.field"],
[" string", "go.predefined"],
],
[["}", "go.operator"]],
[
["func ", "go.keyword"],
["(", "go.operator"],
["g", "go.parameter"],
[" Greeter", "go.className"],
[") ", "go.operator"],
["Greet", "go.functionDeclaration"],
["(", "go.operator"],
["count", "go.parameter"],
[" int", "go.predefined"],
[") ", "go.operator"],
["string", "go.predefined"],
[" {", "go.operator"],
],
[
[" message", "go.identifier"],
[" := ", "go.operator"],
["fmt", "go.packageReference"],
[".", "go.operator"],
["Sprintf", "go.functionCall"],
["(", "go.operator"],
['"Hello, %s', "go.string"],
["\\n", "go.escape"],
['"', "go.string"],
[", ", "go.operator"],
["g", "go.parameter"],
[".", "go.operator"],
["Name", "go.field"],
[")", "go.operator"],
],
[
[" return ", "go.keyword"],
["message", "go.identifier"],
],
[["}", "go.operator"]],
],
},
Kotlin: {
code: [
[["// Kotlin", "kotlin.lineComment"]],
@@ -979,6 +1047,7 @@ function ColorRow({
}
function ColorSchemePage({ scopes, landing }: { scopes: readonly string[]; landing?: boolean }) {
const translate = useTx();
const [scheme, setScheme] = useSetting<string>("editor.colorScheme", "global");
const [overrides, setOverrides] = useSetting<Overrides>("editor.colorOverrides", "global");
const [weight] = useSetting<number>("editor.fontWeight");
@@ -1219,7 +1288,7 @@ function ColorSchemePage({ scopes, landing }: { scopes: readonly string[]; landi
aria-label="Color scheme attributes"
roots={roots}
getKey={(n) => n.key}
getLabel={(n) => n.label}
getLabel={(n) => translate(n.label)}
getChildren={(n) => n.children}
isExpandable={(n) => !!n.children?.length}
expanded={expanded}
@@ -1232,7 +1301,7 @@ function ColorSchemePage({ scopes, landing }: { scopes: readonly string[]; landi
<span
className={`${styles.treeLabel} ${(n.attr ? mine?.[n.attr.id] : modifiedKeys.has(n.key)) ? styles.attrModified : ""}`}
>
{n.label}
{translate(n.label)}
</span>
)}
speedSearch
+8
View File
@@ -1,6 +1,7 @@
import { readFileSync } from "node:fs";
import { beforeAll, describe, expect, it } from "vitest";
import {
ATTRIBUTES,
CLEAR_COLOR,
_setSchemeSource,
resolvedAttribute,
@@ -44,6 +45,13 @@ describe("color schemes", () => {
expect(imported).toMatch(/\.cm-editor\[data-language="PHP"\] \{\n {2}--syn-variable: #48d8ee !important;/);
});
it("offers a Go scheme page and scopes its overrides to Go editors", () => {
expect(ATTRIBUTES.find((a) => a.id === "go.constant")?.path).toEqual(["Go"]);
const css = schemeCss("dark", { dark: { "go.constant": { foreground: "#224466" } } });
expect(css).toContain('.cm-editor[data-language="Go"]');
expect(css).toContain("--syn-constant: #224466 !important");
});
it("resolves scheme attribute state and editor defaults", () => {
expect(resolvedAttribute("light", "lineComment")).toMatchObject({
foreground: "#8c8c8c",
+1 -1
View File
@@ -182,7 +182,7 @@ const BASE_ATTRIBUTES: readonly AttributeDef[] = [
syn("regexp", "Regular expression", ["RegExp"], "regexp"),
];
const SCHEME_LANGUAGES = { java: "Java", kotlin: "Kotlin", php: "PHP" } as const;
const SCHEME_LANGUAGES = { go: "Go", java: "Java", kotlin: "Kotlin", php: "PHP" } as const;
export const ATTRIBUTES: readonly AttributeDef[] = [
...BASE_ATTRIBUTES,
+26
View File
@@ -1,4 +1,30 @@
{
"Keyword": "Keyword",
"Identifiers": "Identifiers",
"Local variable": "Local variable",
"Parameter": "Parameter",
"Global variable": "Global variable",
"Package reference": "Package reference",
"Constant": "Constant",
"Instance field": "Instance field",
"Static symbol": "Static symbol",
"Function declaration": "Function declaration",
"Function call": "Function call",
"Predefined symbol": "Predefined symbol",
"Classes": "Classes",
"Class name": "Class name",
"Number": "Number",
"String": "String",
"String text": "String text",
"Valid escape sequence": "Valid escape sequence",
"Comments": "Comments",
"Line comment": "Line comment",
"Doc comment": "Doc comment",
"Metadata": "Metadata",
"Braces and Operators": "Braces and Operators",
"Markup": "Markup",
"Tag": "Tag",
"Attribute": "Attribute",
"Contents differ only in line separators": "Contents differ only in line separators",
"Cannot remove a worktree containing the open project.": "Cannot remove a worktree containing the open project.",
"Delete Remote Branches": "Delete Remote Branches",
+20
View File
@@ -1,4 +1,24 @@
{
"Identifiers": "标识符",
"Parameter": "参数",
"Package reference": "包引用",
"Instance field": "实例字段",
"Static symbol": "静态符号",
"Function declaration": "函数声明",
"Function call": "函数调用",
"Predefined symbol": "预定义符号",
"Class name": "类名",
"Number": "数字",
"String": "字符串",
"String text": "字符串文本",
"Valid escape sequence": "有效转义序列",
"Comments": "注释",
"Line comment": "行注释",
"Doc comment": "文档注释",
"Metadata": "元数据",
"Braces and Operators": "括号与运算符",
"Markup": "标记",
"Attribute": "属性",
"Identifiers › Package reference": "标识符 › 包引用",
"Cannot remove a worktree containing the open project.": "无法移除包含当前打开项目的工作树。",
"Select Python Environment Folder": "选择 Python 环境文件夹",
+2
View File
@@ -534,6 +534,8 @@ function scanTypeScript(): Scan {
for (const text of texts) {
literals.add(shape(text));
literals.add(shape(text.trim()));
// Color scheme trees display the individual segments of grouped attribute names.
for (const part of text.split(" › ")) literals.add(shape(part));
}
const sink = checked ? sinkOf(node, exact, template) : undefined;
if (sink) {