From 0333befb554e86061e75cfa2f93bd6bee7e4d499 Mon Sep 17 00:00:00 2001 From: Techatrix Date: Wed, 22 Apr 2026 23:12:33 +0200 Subject: [PATCH 1/2] various refactors --- src/editor.ts | 4 ++-- src/theme.ts | 3 +-- src/utils.ts | 2 +- src/workers/runner.ts | 12 ++++++------ src/workers/zig.ts | 8 ++++---- src/workers/zls.ts | 12 ++++++------ 6 files changed, 20 insertions(+), 21 deletions(-) diff --git a/src/editor.ts b/src/editor.ts index ecb223a..dd518bb 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -101,7 +101,7 @@ function revealOutputWindow() { let zigWorker = new ZigWorker(); -zigWorker.onmessage = ev => { +zigWorker.onmessage = (ev: MessageEvent) => { if (ev.data.stderr) { document.querySelector(".zig-output:last-child")!.textContent += ev.data.stderr; revealOutputWindow(); @@ -120,7 +120,7 @@ zigWorker.onmessage = ev => { runnerWorker.postMessage({ run: ev.data.compiled }); - runnerWorker.onmessage = rev => { + runnerWorker.onmessage = (rev: MessageEvent) => { if (rev.data.stderr) { document.querySelector(".runner-output:last-child")!.textContent += rev.data.stderr; revealOutputWindow(); diff --git a/src/theme.ts b/src/theme.ts index 0be93db..15d8ab0 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -21,7 +21,6 @@ export const editorTheme = EditorView.theme( backgroundColor: "var(--tooltip-background)", color: "var(--tooltip-text)", } - }, - {} + } ); diff --git a/src/utils.ts b/src/utils.ts index 2b9b1ac..0df53e3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -5,7 +5,7 @@ export async function getLatestZigArchive() { const response = await fetch(new URL("../zig-out/zig.tar.gz", import.meta.url)); let arrayBuffer = await response.arrayBuffer(); const magicNumber = new Uint8Array(arrayBuffer).slice(0, 2); - if (magicNumber[0] == 0x1F && magicNumber[1] == 0x8B) { // gzip + if (magicNumber[0] == 0x1F && magicNumber[1] == 0x8B) { const ds = new DecompressionStream("gzip"); const response = new Response(new Response(arrayBuffer).body!.pipeThrough(ds)); arrayBuffer = await response.arrayBuffer(); diff --git a/src/workers/runner.ts b/src/workers/runner.ts index 52973c3..559283f 100644 --- a/src/workers/runner.ts +++ b/src/workers/runner.ts @@ -3,18 +3,18 @@ import { WASI, PreopenDirectory, OpenFile, File } from "@bjorn3/browser_wasi_shim"; import { stderrOutput } from "../utils"; -async function run(wasmData: Uint8Array) { - let args = ["main.wasm"]; - let env = []; - let fds = [ +async function run(wasmData: BufferSource) { + const args = ["main.wasm"]; + const env: string[] = []; + const fds = [ new OpenFile(new File([])), // stdin stderrOutput(), // stdout stderrOutput(), // stderr new PreopenDirectory(".", new Map([])), ]; - let wasi = new WASI(args, env, fds); + const wasi = new WASI(args, env, fds); - let { instance } = await WebAssembly.instantiate(wasmData, { + const { instance } = await WebAssembly.instantiate(wasmData, { "wasi_snapshot_preview1": wasi.wasiImport, });; diff --git a/src/workers/zig.ts b/src/workers/zig.ts index b95f20d..58327bf 100644 --- a/src/workers/zig.ts +++ b/src/workers/zig.ts @@ -10,7 +10,7 @@ async function run(source: string) { const libDirectory = await getLatestZigArchive(); const libCompilerRt = await fetch(new URL("../../zig-out/libcompiler_rt.a", import.meta.url)); - let args = [ + const args = [ "zig.wasm", "build-exe", "main.zig", @@ -18,8 +18,8 @@ async function run(source: string) { "-fno-compiler-rt", // manually linked because the self hosted webassembly backend cannot compile it by itself "-fno-entry", // prevent the native webassembly backend from adding a start function to the module ]; - let env = []; - let fds = [ + const env: string[] = []; + const fds = [ new OpenFile(new File([])), // stdin stderrOutput(), // stdout stderrOutput(), // stderr @@ -30,7 +30,7 @@ async function run(source: string) { new PreopenDirectory("/lib", libDirectory.contents), new PreopenDirectory("/cache", new Map()), ] satisfies Fd[]; - let wasi = new WASI(args, env, fds, { debug: false }); + const wasi = new WASI(args, env, fds, { debug: false }); const { instance } = await WebAssembly.instantiateStreaming(fetch(new URL("../../zig-out/bin/zig.wasm", import.meta.url)), { "wasi_snapshot_preview1": wasi.wasiImport, diff --git a/src/workers/zls.ts b/src/workers/zls.ts index 7a6bcc5..eb02f6c 100644 --- a/src/workers/zls.ts +++ b/src/workers/zls.ts @@ -15,7 +15,7 @@ class Stdio extends Fd { } } -let instance: any; +let instance: WebAssembly.Instance; let bufferedMessages: string[] = []; function sendMessage(message: string) { @@ -42,11 +42,11 @@ onmessage = (event) => { }; (async () => { - let libDirectory = await getLatestZigArchive(); + const libDirectory = await getLatestZigArchive(); - let args = ["zls.wasm"]; - let env = []; - let fds = [ + const args = ["zls.wasm"]; + const env: string[] = []; + const fds = [ new Stdio(), // stdin new Stdio(), // stdout ConsoleStdout.lineBuffered((line) => postMessage(JSON.stringify({ stderr: line }))), // stderr @@ -54,7 +54,7 @@ onmessage = (event) => { new PreopenDirectory("/lib", libDirectory.contents), new PreopenDirectory("/cache", new Map()), ]; - let wasi = new WASI(args, env, fds, { debug: false }); + const wasi = new WASI(args, env, fds, { debug: false }); const { instance: localInstance } = await WebAssembly.instantiateStreaming(fetch(new URL("../../zig-out/bin/zls.wasm", import.meta.url)), { "wasi_snapshot_preview1": wasi.wasiImport, From a9545cef7cbd225cafa1d450f0d9b0b7d678c071 Mon Sep 17 00:00:00 2001 From: Techatrix Date: Thu, 23 Apr 2026 16:27:41 +0200 Subject: [PATCH 2/2] adopt `@codemirror/lsp-client` --- package-lock.json | 83 ++++- package.json | 7 +- src/editor.ts | 115 ++---- src/lsp.ts | 273 ++++++++++++++ src/lsp/LICENSE | 27 -- src/lsp/README.md | 1 - src/lsp/index.ts | 746 --------------------------------------- src/theme.ts | 28 ++ style/style.css | 29 ++ style/zig-theme.css | 5 + style/zigtools-theme.css | 5 + 11 files changed, 446 insertions(+), 873 deletions(-) create mode 100644 src/lsp.ts delete mode 100644 src/lsp/LICENSE delete mode 100644 src/lsp/README.md delete mode 100644 src/lsp/index.ts diff --git a/package-lock.json b/package-lock.json index 9531e1c..a8ea27a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,9 +11,11 @@ "@codemirror/autocomplete": "^6.4.2", "@codemirror/commands": "^6.2.2", "@codemirror/language": "^6.6.0", - "@codemirror/lint": "^6.2.0", + "@codemirror/lsp-client": "^6.2.3", "@codemirror/state": "^6.2.0", "@codemirror/view": "^6.9.3", + "@ndim/codemirror-lang-zig": "^0.1.0", + "@ndim/lezer-zig": "^0.1.0", "codemirror": "^6.0.1", "vscode-languageserver-protocol": "^3.17.3" }, @@ -34,9 +36,9 @@ "license": "MIT OR Apache-2.0" }, "node_modules/@codemirror/autocomplete": { - "version": "6.18.6", - "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz", - "integrity": "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==", + "version": "6.20.1", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.1.tgz", + "integrity": "sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A==", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -82,6 +84,22 @@ "crelt": "^1.0.5" } }, + "node_modules/@codemirror/lsp-client": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/@codemirror/lsp-client/-/lsp-client-6.2.3.tgz", + "integrity": "sha512-tDGoLzOU8npz5TadVFtZEU0oRbJXEEM+HpRBfW4w/lWvIjQDkDAmqUtAlP04io1HMmWcGnTBoSwXVOTH+80otw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.20.0", + "@codemirror/language": "^6.11.0", + "@codemirror/lint": "^6.8.5", + "@codemirror/state": "^6.5.2", + "@codemirror/view": "^6.37.0", + "@lezer/highlight": "^1.2.1", + "marked": "^15.0.12", + "vscode-languageserver-protocol": "^3.17.5" + } + }, "node_modules/@codemirror/search": { "version": "6.5.11", "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz", @@ -540,24 +558,24 @@ } }, "node_modules/@lezer/common": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", - "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz", + "integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==", "license": "MIT" }, "node_modules/@lezer/highlight": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", - "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", "license": "MIT", "dependencies": { - "@lezer/common": "^1.0.0" + "@lezer/common": "^1.3.0" } }, "node_modules/@lezer/lr": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", - "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz", + "integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==", "license": "MIT", "dependencies": { "@lezer/common": "^1.0.0" @@ -569,6 +587,27 @@ "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", "license": "MIT" }, + "node_modules/@ndim/codemirror-lang-zig": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@ndim/codemirror-lang-zig/-/codemirror-lang-zig-0.1.0.tgz", + "integrity": "sha512-491O1ccottJ9iTSaHcLjf0Mgs/8gjqhOwBYCWNUHVQLMYxs/zrrrw5DMQXSN6I17ZcRszQstWY5YRLwE+LHfRw==", + "license": "MIT", + "dependencies": { + "lezer-zig": "^0.1.0" + } + }, + "node_modules/@ndim/codemirror-lang-zig/@ndim/lezer-zig": {}, + "node_modules/@ndim/lezer-zig": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@ndim/lezer-zig/-/lezer-zig-0.1.0.tgz", + "integrity": "sha512-yZvcEQlHmT5kq12gEEAlCTV2IOUzOKnEMi5g365uQiXv6Esnqy0lFG+bh39tKkojbNgMrKS9gwXkw3X8kltohg==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.4.0", + "@lezer/highlight": "^1.2.3", + "@lezer/lr": "^1.4.4" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.41.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.1.tgz", @@ -948,6 +987,22 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/lezer-zig": { + "resolved": "node_modules/@ndim/codemirror-lang-zig/@ndim/lezer-zig", + "link": true + }, + "node_modules/marked": { + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", diff --git a/package.json b/package.json index 8897a7e..bee905c 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,11 @@ "@codemirror/autocomplete": "^6.4.2", "@codemirror/commands": "^6.2.2", "@codemirror/language": "^6.6.0", - "@codemirror/lint": "^6.2.0", + "@codemirror/lsp-client": "^6.2.3", "@codemirror/state": "^6.2.0", "@codemirror/view": "^6.9.3", + "@ndim/codemirror-lang-zig": "^0.1.0", + "@ndim/lezer-zig": "^0.1.0", "codemirror": "^6.0.1", "vscode-languageserver-protocol": "^3.17.3" }, @@ -22,6 +24,9 @@ "typescript": "~5.7.2", "vite": "^6.3.1" }, + "overrides": { + "lezer-zig": "@ndim/lezer-zig" + }, "browserslist": [ "since 2017-06" ] diff --git a/src/editor.ts b/src/editor.ts index dd518bb..b18fb09 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -1,12 +1,12 @@ -import { EditorState } from "@codemirror/state" -import { keymap } from "@codemirror/view" -import { EditorView, basicSetup } from "codemirror" -import { JsonRpcMessage, LspClient } from "./lsp"; +import { EditorState } from "@codemirror/state"; +import { keymap } from "@codemirror/view"; +import { EditorView, basicSetup } from "codemirror"; +import { formatDocument } from "@codemirror/lsp-client"; import { indentWithTab } from "@codemirror/commands"; -import { indentUnit } from "@codemirror/language"; -import { editorTheme } from "./theme.ts"; -// @ts-ignore -import ZLSWorker from './workers/zls.ts?worker'; +import { indentUnit, syntaxHighlighting } from "@codemirror/language"; +import { zigLanguage } from "@ndim/codemirror-lang-zig"; +import { editorTheme, highlightStyle } from "./theme.ts"; +import { lspClient } from "./lsp.ts"; // @ts-ignore import ZigWorker from './workers/zig.ts?worker'; // @ts-ignore @@ -14,81 +14,28 @@ import RunnerWorker from './workers/runner.ts?worker'; // @ts-ignore import zigMainSource from './main.zig?raw'; -export default class ZlsClient extends LspClient { - public worker: Worker; - - constructor(worker: Worker) { - super("file:///", []); - this.worker = worker; - - this.worker.addEventListener("message", this.messageHandler); - } - - private messageHandler = (ev: MessageEvent) => { - const data = JSON.parse(ev.data); - - if (data.method == "window/logMessage") { - if (!data.stderr) { - switch (data.params.type) { - case 5: - console.debug("ZLS --- ", data.params.message); - break; - case 4: - console.log("ZLS --- ", data.params.message); - break; - case 3: - console.info("ZLS --- ", data.params.message); - break; - case 2: - console.warn("ZLS --- ", data.params.message); - break; - case 1: - console.error("ZLS --- ", data.params.message); - break; - default: - console.error(data.params.message); - break; - } - } - } else { - console.debug("LSP <<-", data); - } - this.handleMessage(data); - }; - - public async sendMessage(message: JsonRpcMessage): Promise { - console.debug("LSP ->>", message); - if (this.worker) { - this.worker.postMessage(JSON.stringify(message)); - } - } - - public async close(): Promise { - super.close(); - this.worker.terminate(); - } -} - -let client = new ZlsClient(new ZLSWorker()); - -let editor = (async () => { - await client.initialize(); - - let editor = new EditorView({ - extensions: [], - parent: document.getElementById("editor")!, - state: EditorState.create({ - doc: zigMainSource, - extensions: [basicSetup, editorTheme, indentUnit.of(" "), client.createPlugin("file:///main.zig", "zig", true), keymap.of([indentWithTab]),], - }), - }); - - await client.plugins[0].updateDecorations(); - await client.plugins[0].updateFoldingRanges(); - editor.update([]); - - return editor; -})(); +const editor = new EditorView({ + extensions: [], + parent: document.getElementById("editor")!, + state: EditorState.create({ + doc: zigMainSource, + extensions: [ + basicSetup, + editorTheme, + indentUnit.of(" "), + keymap.of([ + indentWithTab, + { + key: "Mod-s", + run: formatDocument, + }, + ]), + zigLanguage, + syntaxHighlighting(highlightStyle), + lspClient.plugin("file:///main.zig"), + ], + }), +}); function revealOutputWindow() { const outputs = document.getElementById("output")!; @@ -190,6 +137,6 @@ outputsRun.addEventListener("click", async () => { revealOutputWindow(); zigWorker.postMessage({ - run: (await editor).state.doc.toString(), + run: editor.state.doc.toString(), }); }); diff --git a/src/lsp.ts b/src/lsp.ts new file mode 100644 index 0000000..c5b91c8 --- /dev/null +++ b/src/lsp.ts @@ -0,0 +1,273 @@ +import * as lsp from "vscode-languageserver-protocol"; +import { StateField, StateEffect, RangeSetBuilder } from "@codemirror/state"; +import { + EditorView, + ViewPlugin, + ViewUpdate, + DecorationSet, + Decoration, +} from "@codemirror/view"; +import { + LSPPlugin, + Transport, + LSPClient, + languageServerExtensions, + LSPClientExtension, +} from "@codemirror/lsp-client"; +import { zigLanguage } from "@ndim/codemirror-lang-zig"; +// @ts-ignore +import ZLSWorker from "./workers/zls.ts?worker"; + +class ZlsTransport implements Transport { + public worker: Worker; + handlers: ((value: string) => void)[] = []; + + constructor(worker: Worker) { + this.worker = worker; + this.worker.addEventListener("message", this.messageHandler); + } + + subscribe(handler: (value: string) => void) { + this.handlers.push(handler); + } + unsubscribe(handler: (value: string) => void) { + this.handlers = this.handlers.filter((h) => h != handler); + } + + private messageHandler = (ev: MessageEvent) => { + const data = JSON.parse(ev.data); + + if (data.method == "window/logMessage") { + if (!data.stderr) { + switch (data.params.type) { + case 5: + console.debug("ZLS --- ", data.params.message); + break; + case 4: + console.log("ZLS --- ", data.params.message); + break; + case 3: + console.info("ZLS --- ", data.params.message); + break; + case 2: + console.warn("ZLS --- ", data.params.message); + break; + case 1: + console.error("ZLS --- ", data.params.message); + break; + default: + console.error(data.params.message); + break; + } + } + } else { + console.debug("LSP <<-", data); + } + + const stringified = JSON.stringify(data); + for (const handler of this.handlers) { + handler(stringified); + } + }; + + send(message: string) { + console.debug("LSP ->>", JSON.parse(message)); + if (this.worker) { + this.worker.postMessage(message); + } + } +} + +const semanticTokensDebounceTimeMS: number = 100; + +const semanticTokensPlugin = ViewPlugin.fromClass( + class { + debounceTimer: number = 0; + pendingRequest: Promise | null = null; + + update(update: ViewUpdate): void { + const plugin = LSPPlugin.get(update.view); + if (!plugin) return; + + const semanticTokensProvider = + plugin.client.serverCapabilities?.semanticTokensProvider; + if (!semanticTokensProvider) return; + if (!semanticTokensProvider.full && !semanticTokensProvider.range) return; + + const state = update.view.state.field(semanticTokensState); + if (state == null) { + this.startRequest(plugin, update.view); + return; + } + + if (!update.docChanged) return; + + if (this.debounceTimer) clearTimeout(this.debounceTimer); + this.debounceTimer = setTimeout(() => { + if (this.debounceTimer) clearTimeout(this.debounceTimer); + this.startRequest(plugin, update.view); + }, semanticTokensDebounceTimeMS); + } + + startRequest(plugin: LSPPlugin, view: EditorView): void { + if (this.pendingRequest != null) { + // There is a pending request on an older document state that should + // be cancelled here. + } + + plugin.client.sync(); + + const supportRangeRequest = + !!plugin.client.serverCapabilities?.semanticTokensProvider?.range; + const promise = supportRangeRequest + ? plugin.client.request< + lsp.SemanticTokensRangeParams, + lsp.SemanticTokens | null + >("textDocument/semanticTokens/range", { + textDocument: { uri: plugin.uri }, + range: { + start: { line: 0, character: 0 }, + end: plugin.toPosition(view.state.doc.length, view.state.doc), + }, + }) + : plugin.client.request< + lsp.SemanticTokensParams, + lsp.SemanticTokens | null + >("textDocument/semanticTokens/full", { + textDocument: { uri: plugin.uri }, + }); + this.pendingRequest = promise; + promise + .then((data) => { + if (this.pendingRequest == promise) { + this.pendingRequest = null; + this.handleResponse(data, plugin, view); + } + }) + .catch((err) => { + if (this.pendingRequest == promise) { + this.pendingRequest = null; + } + if ( + "code" in err && + (err as lsp.ResponseError).code == -32800 /* RequestCancelled */ + ) + return; + throw err; + }); + } + + handleResponse( + semanticTokens: lsp.SemanticTokens | null, + plugin: LSPPlugin, + view: EditorView, + ): void { + if (!semanticTokens) return; + if (semanticTokens.data.length % 5) return; + + const semanticTokensProvider = + plugin.client.serverCapabilities?.semanticTokensProvider!; + const tokenTypeLegend = semanticTokensProvider.legend.tokenTypes; + const tokenModifierLegend = semanticTokensProvider.legend.tokenModifiers; + + const builder = new RangeSetBuilder(); + + let lineStart = 0; + let line = 0; + let character = 0; + + const data = semanticTokens.data; + for (let i = 0; i < data.length; i += 5) { + const deltaLine = data[i]; + const deltaStartChar = data[i + 1]; + const length = data[i + 2]; + const tokenType = data[i + 3]; + const tokenModifierBitSet = data[i + 4]; + + line += deltaLine; + if (deltaLine != 0) { + lineStart = view.state.doc.line(line + 1).from; + character = 0; + } + character += deltaStartChar; + + let modifiers = []; + let value = tokenModifierBitSet; + let index = 0; + while (value != 0) { + if (value & 1) { + modifiers.push(tokenModifierLegend[index]); + } + value = value >> 1; + index += 1; + } + + let className = `st-${tokenTypeLegend[tokenType]}`; + for (const modifier of modifiers) { + className += ` sm-${modifier}`; + } + + const from = lineStart + character; + const to = from + length; + const decoration = Decoration.mark({ + inclusive: true, + class: className, + }); + builder.add(from, to, decoration); + } + view.dispatch({ effects: [semanticTokensEffect.of(builder.finish())] }); + } + + destroy() { + if (this.debounceTimer) clearTimeout(this.debounceTimer); + } + }, +); + +const semanticTokensState = StateField.define({ + create() { + return null; + }, + update(decorations, tr) { + for (let e of tr.effects) { + if (e.is(semanticTokensEffect)) { + decorations = e.value; + } + } + if (decorations && tr.docChanged) { + return decorations.map(tr.changes); + } + return decorations; + }, + provide: (f) => + EditorView.decorations.from(f, (set) => set ?? Decoration.none), +}); + +const semanticTokensEffect = StateEffect.define({}); + +const transport = new ZlsTransport(new ZLSWorker()); +const lspClient = new LSPClient({ + highlightLanguage(name) { + if (name == "zig") return zigLanguage; + return null; + }, + extensions: [ + ...languageServerExtensions(), + { + clientCapabilities: { + semanticTokens: { + requests: { + full: true, + range: true, + }, + tokenTypes: Object.values(lsp.SemanticTokenTypes), + tokenModifiers: Object.values(lsp.SemanticTokenModifiers), + formats: ["relative"], + overlappingTokenSupport: true, + }, + }, + editorExtension: [semanticTokensState, semanticTokensPlugin], + } satisfies LSPClientExtension, + ], +}).connect(transport); +export { lspClient }; diff --git a/src/lsp/LICENSE b/src/lsp/LICENSE deleted file mode 100644 index ecee515..0000000 --- a/src/lsp/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -Copyright (c) 2021, Mahmud Ridwan -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the library nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/lsp/README.md b/src/lsp/README.md deleted file mode 100644 index bcb3c53..0000000 --- a/src/lsp/README.md +++ /dev/null @@ -1 +0,0 @@ -Rewritten version of https://github.com/FurqanSoftware/codemirror-languageserver diff --git a/src/lsp/index.ts b/src/lsp/index.ts deleted file mode 100644 index 6f30d8a..0000000 --- a/src/lsp/index.ts +++ /dev/null @@ -1,746 +0,0 @@ -import { autocompletion } from "@codemirror/autocomplete"; -import { setDiagnostics } from "@codemirror/lint"; -import { ChangeSpec, Facet, Prec, RangeSetBuilder, StateEffect, StateField } from "@codemirror/state"; -import { EditorView, ViewPlugin, Tooltip, hoverTooltip, keymap, DecorationSet, Decoration } from '@codemirror/view'; -import { - DiagnosticSeverity, - CompletionItemKind, - CompletionTriggerKind, -} from "vscode-languageserver-protocol"; - -import type { - Completion, - CompletionContext, - CompletionResult, -} from '@codemirror/autocomplete'; -import type { PublishDiagnosticsParams } from 'vscode-languageserver-protocol'; -import type { ViewUpdate, PluginValue } from '@codemirror/view'; -import { Text } from '@codemirror/state'; -import type * as LSP from 'vscode-languageserver-protocol'; -import { SemanticTokenTypes } from 'vscode-languageserver-protocol'; -import { foldService } from "@codemirror/language"; - -const CompletionItemKindMap = Object.fromEntries( - Object.entries(CompletionItemKind).map(([key, value]) => [value, key]) -) as Record; - -const useLast = (values: readonly any[]) => values.reduce((_, v) => v, ''); - -const client = Facet.define({ combine: useLast }); -const documentUri = Facet.define({ combine: useLast }); -const languageId = Facet.define({ combine: useLast }); - -export type JsonRpcId = string | number; -type OutboundRequest = { - promise: Promise; - resolve: Function; - reject: Function; -}; - -export type JsonRpcMessage = { - jsonrpc: "2.0", - id?: JsonRpcId, - method?: string, - params?: any, - result?: any, - error?: any, -}; - -const setDecorations = StateEffect.define({}); - -export abstract class LspClient { - public id: number; - public outboundRequests: Map; - - public rootUri: string; - public workspaceFolders: LSP.WorkspaceFolder[]; - - public autoClose?: boolean; - public plugins: LspPlugin[]; - - public isOpen: boolean; - /** - * Await initialization cycle completion - */ - public initializePromise: Promise; - /** - * Relies on initializePromise - */ - public capabilities: LSP.ServerCapabilities; - - constructor(rootUri: string, workspaceFolders: LSP.WorkspaceFolder[]) { - this.id = 0; - this.outboundRequests = new Map(); - - this.rootUri = rootUri; - this.workspaceFolders = workspaceFolders; - - this.autoClose = true; - this.plugins = []; - - this.isOpen = false; - } - - abstract sendMessage(data: JsonRpcMessage): Promise; - - public async initialize() { - const { capabilities } = await this.request("initialize", { - capabilities: { - textDocument: { - publishDiagnostics: {}, - semanticTokens: { - requests: {}, - tokenTypes: Object.values(SemanticTokenTypes), - tokenModifiers: [], - formats: ["relative"], - overlappingTokenSupport: true, - }, - hover: { - dynamicRegistration: true, - contentFormat: ['plaintext', 'markdown'], - }, - moniker: {}, - synchronization: { - dynamicRegistration: true, - willSave: false, - didSave: false, - willSaveWaitUntil: false, - }, - completion: { - dynamicRegistration: true, - completionItem: { - snippetSupport: false, - commitCharactersSupport: true, - documentationFormat: ['plaintext', 'markdown'], - deprecatedSupport: false, - preselectSupport: false, - }, - contextSupport: false, - }, - signatureHelp: { - dynamicRegistration: true, - signatureInformation: { - documentationFormat: ['plaintext', 'markdown'], - }, - }, - declaration: { - dynamicRegistration: true, - linkSupport: true, - }, - definition: { - dynamicRegistration: true, - linkSupport: true, - }, - typeDefinition: { - dynamicRegistration: true, - linkSupport: true, - }, - implementation: { - dynamicRegistration: true, - linkSupport: true, - }, - }, - workspace: { - configuration: true, - }, - }, - initializationOptions: null, - processId: null, - rootUri: this.rootUri, - workspaceFolders: this.workspaceFolders, - }); - this.capabilities = capabilities; - this.notify("initialized", {}); - this.isOpen = true; - } - - async close() { - await this.request("shutdown", void{}); - await this.notify("exit", void{}); - } - - textDocumentDidOpen(params: LSP.DidOpenTextDocumentParams) { - return this.notify("textDocument/didOpen", params); - } - - textDocumentDidChange(params: LSP.DidChangeTextDocumentParams) { - return this.notify("textDocument/didChange", params); - } - - textDocumentHover(params: LSP.HoverParams) { - return this.request("textDocument/hover", params); - } - - textDocumentCompletion(params: LSP.CompletionParams) { - return this.request("textDocument/completion", params); - } - - textDocumentFoldingRange(params: LSP.FoldingRangeParams) { - return this.request("textDocument/foldingRange", params); - } - - textDocumentSemanticTokensFull(params: LSP.SemanticTokensParams) { - return this.request("textDocument/semanticTokens/full", params); - } - - attachPlugin(plugin: LspPlugin) { - this.plugins.push(plugin); - } - - detachPlugin(plugin: LspPlugin) { - const i = this.plugins.indexOf(plugin); - if (i === -1) return; - this.plugins.splice(i, 1); - if (this.autoClose) this.close(); - } - - public async request(method: string, params: P): Promise { - const id = this.id++; - - let resolve; - let reject; - let promise = new Promise((a, b) => { - resolve = a; - reject = b; - }); - - this.outboundRequests.set(id, {promise, resolve, reject}) - this.sendMessage({ - jsonrpc: "2.0", - id, - method, - params, - }); - - const result = await promise; - this.outboundRequests.delete(id); - return result as R; - } - - public async notify

(method: string, params: P) { - await this.sendMessage({ - jsonrpc: "2.0", - method, - params, - }); - } - - public handleMessage(message: JsonRpcMessage) { - if (message.method === "workspace/configuration") { - const configParams = message.params as LSP.ConfigurationParams; - let resp: unknown[] = []; - - for (const item of configParams.items) { - if (item.section === "zls.prefer_ast_check_as_child_process") { - resp.push(false); - } else { - resp.push(null); - } - } - - this.sendMessage({ - jsonrpc: "2.0", - id: message.id, - result: resp, - }) - - return; - } - - if (message.id !== undefined && message.method === undefined) { - const req = this.outboundRequests.get(message.id); - if (req) { - if (message.error) req.reject(message.error); - else req.resolve(message.result); - } else { - console.error("Got non-answer"); - } - } - - for (const plugin of this.plugins) - plugin.handleMessage(message); - } - - public createPlugin(docUri: string, langId: string, allowHtmlContent: boolean) { - let plugin: LspPlugin | null = null; - - const decorations = StateField.define({ - create() { - return Decoration.none; - }, - update(decorations, tr) { - for (let e of tr.effects) if (e.is(setDecorations)) { - decorations = e.value; - } - return decorations; - }, - provide: f => EditorView.decorations.from(f) - }); - - return [ - client.of(this), - documentUri.of(docUri), - languageId.of(langId), - decorations.extension, - ViewPlugin.define((view) => (plugin = new LspPlugin(view, allowHtmlContent)), {}), - hoverTooltip( - (view, pos) => plugin?.requestHoverTooltip( - view, - offsetToPos(view.state.doc, pos) - ) ?? null - ), - foldService.of((state, lineStart, lineEnd) => { - const startLine = state.doc.lineAt(lineStart); - const range = plugin?.foldingRangeMap.get(startLine.number - 1); - if (range) { - if (range.endLine > state.doc.lines) return null; - const endLine = state.doc.line(range.endLine + 1); - return {from: range.startCharacter != undefined ? lineStart + range.startCharacter : startLine.to, to: range.endCharacter != undefined ? endLine.from + range.endCharacter : endLine.to}; - } - - return null; - }), - - autocompletion({ - override: [ - async (context) => { - if (plugin == null) return null; - - const { state, pos, explicit } = context; - const line = state.doc.lineAt(pos); - let trigKind: CompletionTriggerKind = - CompletionTriggerKind.Invoked; - let trigChar: string | undefined; - if ( - !explicit && - plugin.client.capabilities?.completionProvider?.triggerCharacters?.includes( - line.text[pos - line.from - 1] - ) - ) { - trigKind = CompletionTriggerKind.TriggerCharacter; - trigChar = line.text[pos - line.from - 1]; - } - if ( - trigKind === CompletionTriggerKind.Invoked && - !context.matchBefore(/\w+$/) - ) { - return null; - } - return await plugin.requestCompletion( - context, - offsetToPos(state.doc, pos), - { - triggerKind: trigKind, - triggerCharacter: trigChar, - } - ); - }, - ], - }), - Prec.highest( - keymap.of([{ - key: "Mod-s", - run(view) { - plugin!.requestFormat(view); - return true; - } - }]) - ) - ]; - } -} - -class LspPlugin implements PluginValue { - public client: LspClient; - - private documentUri: string; - private languageId: string; - private documentVersion: number; - - public decorations: DecorationSet; - public foldingRangeMap: Map; - - constructor(private view: EditorView, private allowHtmlContent: boolean) { - this.client = this.view.state.facet(client); - this.documentUri = this.view.state.facet(documentUri); - this.languageId = this.view.state.facet(languageId); - this.documentVersion = 0; - - this.decorations = Decoration.none; - this.foldingRangeMap = new Map(); - - this.client.attachPlugin(this); - - this.initialize({ - documentText: this.view.state.doc.toString(), - }); - } - - update(update: ViewUpdate) { - if (!update.docChanged) return; - this.foldingRangeMap.clear(); - (async () => { - await this.sendChange({ - documentText: this.view.state.doc.toString(), - }); - await this.updateDecorations(); - await this.updateFoldingRanges(); - })(); - } - - destroy() { - this.client.detachPlugin(this); - } - - async initialize({ documentText }: { documentText: string }) { - if (this.client.initializePromise) { - await this.client.initializePromise; - } - this.client.textDocumentDidOpen({ - textDocument: { - uri: this.documentUri, - languageId: this.languageId, - text: documentText, - version: this.documentVersion, - } - }); - await this.updateDecorations(); - await this.updateFoldingRanges(); - } - - async sendChange({ documentText }: { documentText: string }) { - if (!this.client.isOpen) return; - try { - await this.client.textDocumentDidChange({ - textDocument: { - uri: this.documentUri, - version: this.documentVersion++, - }, - contentChanges: [{ text: documentText }], - }); - } catch (e) { - console.error(e); - } - } - - public async updateDecorations(): Promise { - // TODO: Look into using incremental semantic - // tokens using view.visibleRanges - - const semanticTokens = await this.client.textDocumentSemanticTokensFull({ - textDocument: { - uri: this.documentUri, - } - }); - - if (!semanticTokens) return console.log("No semantic tokens!"); - - const tokenTypes = this.client.capabilities.semanticTokensProvider!.legend.tokenTypes; - const tokenModifiers = this.client.capabilities.semanticTokensProvider!.legend.tokenModifiers; - - let builder = new RangeSetBuilder(); - - let line = 0; - let col = 0; - - const data = semanticTokens.data; - for (let i = 0; i < data.length; i += 5) { - const deltaLine = data[i]; - const deltaStartChar = data[i + 1]; - const length = data[i + 2]; - const tokenType = data[i + 3]; - const tokenModifierBitSet = data[i + 4]; - - line += deltaLine; - if (deltaLine == 0) { // same line - col += deltaStartChar; - } else { - col = deltaStartChar; - } - - let className = `st-${tokenTypes[tokenType]}`; - - { - let value = tokenModifierBitSet; - let index = 0; - while (value != 0) { - if (value & 1) { - className += ` sm-${tokenModifiers[index]}`; - } - value = value >> 1; - index += 1; - } - } - - const l = this.view.state.doc.line(line + 1).from; - builder.add(l + col, l + col + length, Decoration.mark({ - class: className, - })); - } - this.decorations = builder.finish() - this.view.dispatch({effects: [setDecorations.of(this.decorations)]}); - } - - public async updateFoldingRanges(): Promise { - const ranges = await this.client.textDocumentFoldingRange({ - textDocument: { - uri: this.documentUri, - } - }); - - this.foldingRangeMap.clear(); - if (ranges) { - for (const range of ranges) { - this.foldingRangeMap.set(range.startLine, range); - } - } - } - - async requestFormat(view: EditorView): Promise { - const formattingResult = await this.client.request("textDocument/formatting", { - options: { - insertSpaces: true, - tabSize: 4, - }, - textDocument: { - uri: this.documentUri, - } - }); - - this.foldingRangeMap.clear(); - - if (formattingResult) { - const text = this.view.state.doc; - - let changes: ChangeSpec[] = []; - for (const n of formattingResult) { - changes.push({ from: posToOffset(text, n.range.start)!, to: posToOffset(text, n.range.end)!, insert: n.newText }); - } - if (changes.length > 0) { - this.view.dispatch({ - changes, - }); - } - } - - await this.updateFoldingRanges(); - } - - async requestHoverTooltip( - view: EditorView, - { line, character }: { line: number; character: number } - ): Promise { - if (!this.client.isOpen || !this.client.capabilities!.hoverProvider) return null; - - const result = await this.client.textDocumentHover({ - textDocument: { uri: this.documentUri }, - position: { line, character }, - }); - if (!result) return null; - - const { contents, range } = result; - let pos = posToOffset(view.state.doc, { line, character })!; - let end: number = pos; - if (range) { - pos = posToOffset(view.state.doc, range.start)!; - end = posToOffset(view.state.doc, range.end) ?? end; - } - if (pos === null) return null; - return { pos, end, create () { - const dom = document.createElement("div"); - dom.textContent = formatContents(contents); - return {dom}; - }, above: true }; - } - - async requestCompletion( - context: CompletionContext, - { line, character }: { line: number; character: number }, - { - triggerKind, - triggerCharacter, - }: { - triggerKind: CompletionTriggerKind; - triggerCharacter: string | undefined; - } - ): Promise { - if (!this.client.isOpen || !this.client.capabilities!.completionProvider) return null; - this.sendChange({ - documentText: context.state.doc.toString(), - }); - - const result = await this.client.textDocumentCompletion({ - textDocument: { uri: this.documentUri }, - position: { line, character }, - context: { - triggerKind, - triggerCharacter, - } - }); - - if (!result) return null; - - const items = 'items' in result ? result.items : result; - - let options = items.map( - ({ - detail, - label, - kind, - textEdit, - documentation, - sortText, - filterText, - }) => { - const completion: Completion & { - filterText: string; - sortText?: string; - apply: string; - } = { - label, - detail, - apply: textEdit?.newText ?? label, - type: kind && CompletionItemKindMap[kind].toLowerCase(), - sortText: sortText ?? label, - filterText: filterText ?? label, - }; - if (documentation) { - completion.info = formatContents(documentation); - } - return completion; - } - ); - - const [span, match] = prefixMatch(options); - const token = context.matchBefore(match); - let { pos } = context; - - if (token) { - pos = token.from; - const word = token.text.toLowerCase(); - if (/^\w+$/.test(word)) { - options = options - .filter(({ filterText }) => - filterText.toLowerCase().startsWith(word) - ) - .sort(({ apply: a }, { apply: b }) => { - switch (true) { - case a.startsWith(token.text) && - !b.startsWith(token.text): - return -1; - case !a.startsWith(token.text) && - b.startsWith(token.text): - return 1; - } - return 0; - }); - } - } - return { - from: pos, - options, - }; - } - - handleMessage(message: JsonRpcMessage) { - try { - switch (message.method) { - case "textDocument/publishDiagnostics": - this.handleDiagnostics(message.params); - break; - } - } catch (error) { - console.error(error); - } - } - - handleDiagnostics(params: PublishDiagnosticsParams) { - if (params.uri !== this.documentUri) return; - - const diagnostics = params.diagnostics - .map(({ range, message, severity }) => ({ - from: posToOffset(this.view.state.doc, range.start)!, - to: posToOffset(this.view.state.doc, range.end)!, - severity: ({ - [DiagnosticSeverity.Error]: 'error', - [DiagnosticSeverity.Warning]: 'warning', - [DiagnosticSeverity.Information]: 'info', - [DiagnosticSeverity.Hint]: 'info', - } as const)[severity!], - message, - })) - .filter(({ from, to }) => from !== null && to !== null && from !== undefined && to !== undefined) - .sort((a, b) => { - switch (true) { - case a.from < b.from: - return -1; - case a.from > b.from: - return 1; - } - return 0; - }); - - this.view.dispatch(setDiagnostics(this.view.state, diagnostics)); - } -} - -interface LanguageServerBaseOptions { - rootUri: string | null; - workspaceFolders: LSP.WorkspaceFolder[] | null; - documentUri: string; - languageId: string; -} - -function posToOffset(doc: Text, pos: { line: number; character: number }) { - if (pos.line >= doc.lines) return; - const offset = doc.line(pos.line + 1).from + pos.character; - if (offset > doc.length) return; - return offset; -} - -function offsetToPos(doc: Text, offset: number) { - const line = doc.lineAt(offset); - return { - line: line.number - 1, - character: offset - line.from, - }; -} - -function formatContents( - contents: LSP.MarkupContent | LSP.MarkedString | LSP.MarkedString[] -): string { - if (Array.isArray(contents)) { - return contents.map((c) => formatContents(c) + '\n\n').join(''); - } else if (typeof contents === 'string') { - return contents; - } else { - return contents.value; - } -} - -function toSet(chars: Set) { - let preamble = ''; - let flat = Array.from(chars).join(''); - const words = /\w/.test(flat); - if (words) { - preamble += '\\w'; - flat = flat.replace(/\w/g, ''); - } - return `[${preamble}${flat.replace(/[^\w\s]/g, '\\$&')}]`; -} - -function prefixMatch(options: Completion[]) { - const first = new Set(); - const rest = new Set(); - - for (const { apply } of options) { - const [initial, ...restStr] = apply as string; - first.add(initial); - for (const char of restStr) { - rest.add(char); - } - } - - const source = toSet(first) + toSet(rest) + '*$'; - return [new RegExp('^' + source), new RegExp(source)]; -} diff --git a/src/theme.ts b/src/theme.ts index 15d8ab0..09156d1 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -1,4 +1,6 @@ +import { HighlightStyle } from "@codemirror/language"; import { EditorView } from "@codemirror/view"; +import { tags } from "@lezer/highlight"; export const editorTheme = EditorView.theme( { @@ -20,7 +22,33 @@ export const editorTheme = EditorView.theme( ".cm-tooltip": { backgroundColor: "var(--tooltip-background)", color: "var(--tooltip-text)", + }, + ".cm-completionIcon": { + display: "none", + }, + ".cm-completionLabel": { + fontFamily: "monospace", + }, + ".cm-tooltip-autocomplete": { + "& > ul > li > .cm-completionDetail": { + color: "#aaaaaa", + } } } ); +export const highlightStyle = HighlightStyle.define([ + { tag: tags.definitionKeyword, class: "st-keyword" }, + { tag: tags.modifier, class: "st-keyword" }, + { tag: tags.controlKeyword, class: "st-keyword" }, + { tag: tags.labelName, class: "st-label" }, + { tag: tags.keyword, class: "st-builtin" }, + { + tag: tags.function(tags.definition(tags.variableName)), + class: "st-function", + }, + // { tag: tags.typeName, class: "st-type" }, + { tag: tags.lineComment, class: "st-comment" }, + { tag: tags.number, class: "st-number" }, + { tag: tags.string, class: "st-string" }, +]); diff --git a/style/style.css b/style/style.css index 45b336c..0dae148 100644 --- a/style/style.css +++ b/style/style.css @@ -185,6 +185,35 @@ code { padding: 6px; } +.cm-lsp-documentation { + p, + pre { + line-height: 1.2; + } + + :not(pre) code { + background: #f8f8f8; + border: 1px dotted silver; + } + + @media (prefers-color-scheme: dark) { + :not(pre) code { + background: #222; + border-color: #444; + border: none; + } + } +} + +.cm-gutters:hover > .cm-foldGutter > .cm-gutterElement { + opacity: 1; +} + +.cm-foldGutter > .cm-gutterElement { + opacity: 0; + transition: opacity 0.3s; +} + ul { list-style: circle; margin-left: 2rem; diff --git a/style/zig-theme.css b/style/zig-theme.css index a52952e..30a5f8b 100644 --- a/style/zig-theme.css +++ b/style/zig-theme.css @@ -45,6 +45,11 @@ font-style: italic; } +/* Avoid the string class overriding escape sequences when dealing with nested highlighting */ +.st-escapeSequence .st-string { + color: inherit; +} + @media (prefers-color-scheme: dark) { :root { --code-background-color: #1e1e1e; diff --git a/style/zigtools-theme.css b/style/zigtools-theme.css index 3b97692..23b1ee0 100644 --- a/style/zigtools-theme.css +++ b/style/zigtools-theme.css @@ -50,6 +50,11 @@ font-style: italic; } +/* Avoid the string class overriding escape sequences when dealing with nested highlighting */ +.st-escapeSequence .st-string { + color: inherit; +} + @media (prefers-color-scheme: dark) { :root { --code-background-color: #121212;