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