From fff2c351811037605d81d53eff2db3d38b7dd260 Mon Sep 17 00:00:00 2001 From: Shohruh Ismoilov Date: Sun, 8 Nov 2026 19:28:20 +0500 Subject: [PATCH] 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 <7658451+shohruh-genius@users.noreply.github.com> --- crates/ide-language/src/registry.rs | 14 ++++ e2e/goHighlight.fixture.ts | 43 +++++++++++ e2e/goHighlight.html | 9 +++ e2e/goHighlight.spec.ts | 10 +++ languages/go.toml | 1 + src/editor/lsp/semantic/go/index.test.ts | 65 ++++++++++++++++ src/editor/lsp/semantic/go/index.ts | 13 ++++ src/editor/lsp/semantic/index.ts | 1 + .../settings/__tests__/colorScheme.test.tsx | 46 +++++++++++- .../settings/pages/editor/colorScheme.tsx | 75 ++++++++++++++++++- src/platform/colorScheme.test.ts | 8 ++ src/platform/colorScheme.ts | 2 +- src/platform/locales/text/en.json | 26 +++++++ src/platform/locales/text/zh.json | 20 +++++ src/platform/locales/textCatalog.test.ts | 2 + 15 files changed, 330 insertions(+), 5 deletions(-) create mode 100644 e2e/goHighlight.fixture.ts create mode 100644 e2e/goHighlight.html create mode 100644 e2e/goHighlight.spec.ts create mode 100644 src/editor/lsp/semantic/go/index.test.ts create mode 100644 src/editor/lsp/semantic/go/index.ts diff --git a/crates/ide-language/src/registry.rs b/crates/ide-language/src/registry.rs index 39bc2b092..99b35d7e3 100644 --- a/crates/ide-language/src/registry.rs +++ b/crates/ide-language/src/registry.rs @@ -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(); diff --git a/e2e/goHighlight.fixture.ts b/e2e/goHighlight.fixture.ts new file mode 100644 index 000000000..f3ac195f3 --- /dev/null +++ b/e2e/goHighlight.fixture.ts @@ -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(), + ], +}); diff --git a/e2e/goHighlight.html b/e2e/goHighlight.html new file mode 100644 index 000000000..ca5b1b2fc --- /dev/null +++ b/e2e/goHighlight.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/e2e/goHighlight.spec.ts b/e2e/goHighlight.spec.ts new file mode 100644 index 000000000..81a000a24 --- /dev/null +++ b/e2e/goHighlight.spec.ts @@ -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"); +}); diff --git a/languages/go.toml b/languages/go.toml index 30dbabb6c..59cd7662e 100644 --- a/languages/go.toml +++ b/languages/go.toml @@ -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" diff --git a/src/editor/lsp/semantic/go/index.test.ts b/src/editor/lsp/semantic/go/index.test.ts new file mode 100644 index 000000000..eec56a47a --- /dev/null +++ b/src/editor/lsp/semantic/go/index.test.ts @@ -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([]); + }); +}); diff --git a/src/editor/lsp/semantic/go/index.ts b/src/editor/lsp/semantic/go/index.ts new file mode 100644 index 000000000..39a8d05c9 --- /dev/null +++ b/src/editor/lsp/semantic/go/index.ts @@ -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); diff --git a/src/editor/lsp/semantic/index.ts b/src/editor/lsp/semantic/index.ts index 7bb309c47..523612693 100644 --- a/src/editor/lsp/semantic/index.ts +++ b/src/editor/lsp/semantic/index.ts @@ -29,6 +29,7 @@ export { languageSemantics } from "./languages"; import "./kotlin"; import "./python"; import "./php"; +import "./go"; const semanticBackoff = featureBackoff(); diff --git a/src/features/settings/__tests__/colorScheme.test.tsx b/src/features/settings/__tests__/colorScheme.test.tsx index 3f7b51f2a..2a961f287 100644 --- a/src/features/settings/__tests__/colorScheme.test.tsx +++ b/src/features/settings/__tests__/colorScheme.test.tsx @@ -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", () => { diff --git a/src/features/settings/pages/editor/colorScheme.tsx b/src/features/settings/pages/editor/colorScheme.tsx index 24b7cf875..9f3359009 100644 --- a/src/features/settings/pages/editor/colorScheme.tsx +++ b/src/features/settings/pages/editor/colorScheme.tsx @@ -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>; @@ -689,6 +689,74 @@ const SAMPLES: Record = { [["}", "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("editor.colorScheme", "global"); const [overrides, setOverrides] = useSetting("editor.colorOverrides", "global"); const [weight] = useSetting("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 - {n.label} + {translate(n.label)} )} speedSearch diff --git a/src/platform/colorScheme.test.ts b/src/platform/colorScheme.test.ts index 47987ce7c..a3082dc06 100644 --- a/src/platform/colorScheme.test.ts +++ b/src/platform/colorScheme.test.ts @@ -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", diff --git a/src/platform/colorScheme.ts b/src/platform/colorScheme.ts index c9d622bc6..8a7ef88c9 100644 --- a/src/platform/colorScheme.ts +++ b/src/platform/colorScheme.ts @@ -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, diff --git a/src/platform/locales/text/en.json b/src/platform/locales/text/en.json index 70262d01a..f784b9136 100644 --- a/src/platform/locales/text/en.json +++ b/src/platform/locales/text/en.json @@ -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", diff --git a/src/platform/locales/text/zh.json b/src/platform/locales/text/zh.json index e529fb272..b5fa5068a 100644 --- a/src/platform/locales/text/zh.json +++ b/src/platform/locales/text/zh.json @@ -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 环境文件夹", diff --git a/src/platform/locales/textCatalog.test.ts b/src/platform/locales/textCatalog.test.ts index 194f56066..f7b04bbf3 100644 --- a/src/platform/locales/textCatalog.test.ts +++ b/src/platform/locales/textCatalog.test.ts @@ -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) {