diff --git a/index.html b/index.html index b55d8fef..ab7fff45 100644 --- a/index.html +++ b/index.html @@ -54,6 +54,11 @@

Groovy

Groovy Language Client & Language Server (Web Socket)
+

Cpp / Clangd

+ Cpp Language Client & Clangd Language Server (Worker/Wasm) +
+ +

Monaco Editor React

React: Langium Statemachine Language Client & Language Server (Worker)
diff --git a/package-lock.json b/package-lock.json index 93f23627..213755a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -579,6 +579,15 @@ "vscode": "npm:@codingame/monaco-vscode-api@8.0.1" } }, + "node_modules/@codingame/monaco-vscode-cpp-default-extension": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-cpp-default-extension/-/monaco-vscode-cpp-default-extension-8.0.1.tgz", + "integrity": "sha512-hqLzB7O8sZDT+iLIW+5/w1mCbouzMtAuReMt6aKrT8gX81GWgabztOjOHxeK8qgXyq2imsMM0dVURZO44oolqQ==", + "license": "MIT", + "dependencies": { + "vscode": "npm:@codingame/monaco-vscode-api@8.0.1" + } + }, "node_modules/@codingame/monaco-vscode-editor-service-override": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-editor-service-override/-/monaco-vscode-editor-service-override-8.0.1.tgz", @@ -11181,6 +11190,7 @@ "license": "MIT", "dependencies": { "@codingame/monaco-vscode-configuration-service-override": "~8.0.1", + "@codingame/monaco-vscode-cpp-default-extension": "~8.0.1", "@codingame/monaco-vscode-files-service-override": "~8.0.1", "@codingame/monaco-vscode-groovy-default-extension": "~8.0.1", "@codingame/monaco-vscode-java-default-extension": "~8.0.1", diff --git a/packages/examples/.gitignore b/packages/examples/.gitignore index 39cdeec5..338f6cb8 100644 --- a/packages/examples/.gitignore +++ b/packages/examples/.gitignore @@ -1,3 +1,4 @@ +resources/clangd/wasm resources/groovy/external resources/eclipse.jdt.ls/ls resources/eclipse.jdt.ls/*.tar.gz diff --git a/packages/examples/clangd.html b/packages/examples/clangd.html new file mode 100644 index 00000000..bf101855 --- /dev/null +++ b/packages/examples/clangd.html @@ -0,0 +1,24 @@ + + + + + Cpp Language Client & Clangd Language Server (Worker/Wasm) + + + + + + +

Cpp Language Client & Clangd Language Server (Worker/Wasm)

+ + +
+ + + + diff --git a/packages/examples/package.json b/packages/examples/package.json index 728760b4..2b4e0e90 100644 --- a/packages/examples/package.json +++ b/packages/examples/package.json @@ -55,6 +55,7 @@ }, "dependencies": { "@codingame/monaco-vscode-configuration-service-override": "~8.0.1", + "@codingame/monaco-vscode-cpp-default-extension": "~8.0.1", "@codingame/monaco-vscode-files-service-override": "~8.0.1", "@codingame/monaco-vscode-groovy-default-extension": "~8.0.1", "@codingame/monaco-vscode-keybindings-service-override": "~8.0.1", @@ -121,6 +122,7 @@ "start:server:python": "vite-node src/python/server/direct.ts", "start:server:groovy": "vite-node src/groovy/server/direct.ts", "start:server:jdtls": "vite-node src/eclipse.jdt.ls/server/direct.ts", - "langium:generate": "langium generate --file ./src/langium/statemachine/config/langium-config.json" + "langium:generate": "langium generate --file ./src/langium/statemachine/config/langium-config.json", + "extract:docker": "vite-node ./resources/clangd/scripts/extractDockerFiles.ts" } } diff --git a/packages/examples/resources/clangd/build-docker.sh b/packages/examples/resources/clangd/build-docker.sh new file mode 100644 index 00000000..e64b0757 --- /dev/null +++ b/packages/examples/resources/clangd/build-docker.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash + +# 3b. Build LLVM + +WORKSPACE_DIR=$PWD +# otherwise emcmake is not found +source $WORKSPACE_DIR/emsdk/emsdk_env.sh + +cd llvm-project + +## Build native tools first +cmake -G Ninja -S llvm -B build-native \ + -DCMAKE_BUILD_TYPE=Release \ + -DLLVM_ENABLE_PROJECTS=clang +cmake --build build-native --target llvm-tblgen clang-tblgen + +## Apply a patch for blocking stdin read +git apply $WORKSPACE_DIR/wait_stdin.patch + +## Build clangd (1st time, just for compiler headers) +emcmake cmake -G Ninja -S llvm -B build \ + -DCMAKE_CXX_FLAGS="-pthread -Dwait4=__syscall_wait4" \ + -DCMAKE_EXE_LINKER_FLAGS="-pthread -s ENVIRONMENT=worker -s NO_INVOKE_RUN" \ + -DCMAKE_BUILD_TYPE=MinSizeRel \ + -DLLVM_TARGET_ARCH=wasm32-emscripten \ + -DLLVM_DEFAULT_TARGET_TRIPLE=wasm32-wasi \ + -DLLVM_TARGETS_TO_BUILD=WebAssembly \ + -DLLVM_ENABLE_PROJECTS="clang;clang-tools-extra" \ + -DLLVM_TABLEGEN=$PWD/build-native/bin/llvm-tblgen \ + -DCLANG_TABLEGEN=$PWD/build-native/bin/clang-tblgen \ + -DLLVM_BUILD_STATIC=ON \ + -DLLVM_INCLUDE_EXAMPLES=OFF \ + -DLLVM_INCLUDE_TESTS=OFF \ + -DLLVM_ENABLE_BACKTRACES=OFF \ + -DLLVM_ENABLE_UNWIND_TABLES=OFF \ + -DLLVM_ENABLE_CRASH_OVERRIDES=OFF \ + -DCLANG_ENABLE_STATIC_ANALYZER=OFF \ + -DLLVM_ENABLE_TERMINFO=OFF \ + -DLLVM_ENABLE_PIC=OFF \ + -DLLVM_ENABLE_ZLIB=OFF \ + -DCLANG_ENABLE_ARCMT=OFF +cmake --build build --target clangd + +## Copy installed headers to WASI sysroot +cp -r build/lib/clang/$LLVM_VER_MAJOR/include/* $WORKSPACE_DIR/wasi-sysroot/include/ + +## Build clangd (2nd time, for the real thing) +emcmake cmake -G Ninja -S llvm -B build \ + -DCMAKE_CXX_FLAGS="-pthread -Dwait4=__syscall_wait4" \ + -DCMAKE_EXE_LINKER_FLAGS="-pthread -s ENVIRONMENT=worker -s NO_INVOKE_RUN -s EXIT_RUNTIME -s INITIAL_MEMORY=2GB -s ALLOW_MEMORY_GROWTH -s MAXIMUM_MEMORY=4GB -s STACK_SIZE=256kB -s EXPORTED_RUNTIME_METHODS=FS,callMain -s MODULARIZE -s EXPORT_ES6 -s WASM_BIGINT -s ASSERTIONS -s ASYNCIFY -s PTHREAD_POOL_SIZE='Math.max(navigator.hardwareConcurrency, 8)' --embed-file=$WORKSPACE_DIR/wasi-sysroot/include@/usr/include" \ + -DCMAKE_BUILD_TYPE=MinSizeRel \ + -DLLVM_TARGET_ARCH=wasm32-emscripten \ + -DLLVM_DEFAULT_TARGET_TRIPLE=wasm32-wasi \ + -DLLVM_TARGETS_TO_BUILD=WebAssembly \ + -DLLVM_ENABLE_PROJECTS="clang;clang-tools-extra" \ + -DLLVM_TABLEGEN=$PWD/build-native/bin/llvm-tblgen \ + -DCLANG_TABLEGEN=$PWD/build-native/bin/clang-tblgen \ + -DLLVM_BUILD_STATIC=ON \ + -DLLVM_INCLUDE_EXAMPLES=OFF \ + -DLLVM_INCLUDE_TESTS=OFF \ + -DLLVM_ENABLE_BACKTRACES=OFF \ + -DLLVM_ENABLE_UNWIND_TABLES=OFF \ + -DLLVM_ENABLE_CRASH_OVERRIDES=OFF \ + -DCLANG_ENABLE_STATIC_ANALYZER=OFF \ + -DLLVM_ENABLE_TERMINFO=OFF \ + -DLLVM_ENABLE_PIC=OFF \ + -DLLVM_ENABLE_ZLIB=OFF \ + -DCLANG_ENABLE_ARCMT=OFF +cmake --build build --target clangd diff --git a/packages/examples/resources/clangd/build.Dockerfile b/packages/examples/resources/clangd/build.Dockerfile new file mode 100644 index 00000000..e736c135 --- /dev/null +++ b/packages/examples/resources/clangd/build.Dockerfile @@ -0,0 +1,6 @@ +FROM clangd-clangd-configure + +COPY build-docker.sh /builder/build-docker.sh +COPY wait_stdin.patch /builder/wait_stdin.patch + +RUN (cd /builder; ./build-docker.sh) diff --git a/packages/examples/resources/clangd/build.docker-compose.yml b/packages/examples/resources/clangd/build.docker-compose.yml new file mode 100644 index 00000000..9c362617 --- /dev/null +++ b/packages/examples/resources/clangd/build.docker-compose.yml @@ -0,0 +1,10 @@ +services: + clangd-build: + build: + dockerfile: ./build.Dockerfile + context: . + # only linux/amd64 for now + platforms: + - "linux/amd64" + platform: linux/amd64 + container_name: clangd-build diff --git a/packages/examples/resources/clangd/configure-docker.sh b/packages/examples/resources/clangd/configure-docker.sh new file mode 100644 index 00000000..69a04884 --- /dev/null +++ b/packages/examples/resources/clangd/configure-docker.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# It's not recommend for you to run this script directly, +# (because I'm not good at writing this sorry) +# but you can use it as a reference for building. + +# 0. Configs + +# sudo apt install vim git build-essential cmake ninja-build python3 + +## Note: Better to make sure WASI SDK version matches the LLVM version +EMSDK_VER=3.1.52 +WASI_SDK_VER=22.0 +WASI_SDK_VER_MAJOR=22 +LLVM_VER=18.1.2 +LLVM_VER_MAJOR=18 + +# 1. Get Emscripten + +git clone --branch $EMSDK_VER --depth 1 https://github.com/emscripten-core/emsdk +pushd emsdk +./emsdk install $EMSDK_VER +./emsdk activate $EMSDK_VER +source ./emsdk_env.sh +popd + +# 2. Prepare WASI sysroot + +wget -O- https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-$WASI_SDK_VER_MAJOR/wasi-sysroot-$WASI_SDK_VER.tar.gz | tar -xz + +# 3a. Build LLVM + +git clone --branch llvmorg-$LLVM_VER --depth 1 https://github.com/llvm/llvm-project diff --git a/packages/examples/resources/clangd/configure.Dockerfile b/packages/examples/resources/clangd/configure.Dockerfile new file mode 100644 index 00000000..a4d5a5e5 --- /dev/null +++ b/packages/examples/resources/clangd/configure.Dockerfile @@ -0,0 +1,18 @@ +FROM ubuntu + +RUN apt update \ + && apt upgrade -y \ + && apt install -y curl git wget build-essential cmake ninja-build python3 + +RUN curl https://get.volta.sh | bash +ENV VOLTA_FEATURE_PNPM=1 +ENV VOLTA_HOME "/root/.volta" +ENV PATH "$VOLTA_HOME/bin:$PATH" + +RUN volta install node \ + && volta install pnpm + +RUN mkdir /builder + +COPY configure-docker.sh /builder/configure-docker.sh +RUN (cd /builder; ./configure-docker.sh) diff --git a/packages/examples/resources/clangd/configure.docker-compose.yml b/packages/examples/resources/clangd/configure.docker-compose.yml new file mode 100644 index 00000000..0c6299ad --- /dev/null +++ b/packages/examples/resources/clangd/configure.docker-compose.yml @@ -0,0 +1,10 @@ +services: + clangd-configure: + build: + dockerfile: ./configure.Dockerfile + context: . + # only linux/amd64 for now + platforms: + - "linux/amd64" + platform: linux/amd64 + container_name: clangd-configure diff --git a/packages/examples/resources/clangd/scripts/extractDockerFiles.ts b/packages/examples/resources/clangd/scripts/extractDockerFiles.ts new file mode 100644 index 00000000..a4c44a03 --- /dev/null +++ b/packages/examples/resources/clangd/scripts/extractDockerFiles.ts @@ -0,0 +1,18 @@ +import * as fs from "node:fs"; +import child_process from "node:child_process"; + +const outputDir = './resources/clangd/wasm'; + +// clean always +fs.rmSync(outputDir, { + force: true, + recursive: true +}); +fs.mkdirSync(outputDir); + +child_process.execFileSync('docker', ['create', '--name', 'extract-clangd', 'clangd-clangd-build']); +child_process.execFileSync('docker', ['cp', 'extract-clangd:/builder/llvm-project/build/bin/clangd.js', outputDir]); +child_process.execFileSync('docker', ['cp', 'extract-clangd:/builder/llvm-project/build/bin/clangd.wasm', outputDir]); +child_process.execFileSync('docker', ['cp', 'extract-clangd:/builder/llvm-project/build/bin/clangd.worker.js', outputDir]); +child_process.execFileSync('docker', ['cp', 'extract-clangd:/builder/llvm-project/build/bin/clangd.worker.mjs', outputDir]); +child_process.execFileSync('docker', ['rm', 'extract-clangd']); diff --git a/packages/examples/resources/clangd/wait_stdin.patch b/packages/examples/resources/clangd/wait_stdin.patch new file mode 100644 index 00000000..bf82578f --- /dev/null +++ b/packages/examples/resources/clangd/wait_stdin.patch @@ -0,0 +1,31 @@ +diff --git a/clang-tools-extra/clangd/JSONTransport.cpp b/clang-tools-extra/clangd/JSONTransport.cpp +index 9dc0df807..b1a4e9bd1 100644 +--- a/clang-tools-extra/clangd/JSONTransport.cpp ++++ b/clang-tools-extra/clangd/JSONTransport.cpp +@@ -1,3 +1,26 @@ ++#ifdef __EMSCRIPTEN__ ++ ++#include ++ ++#include "support/Shutdown.h" ++#include ++ ++EM_ASYNC_JS(void, waitForStdin, (), { ++ await Module.stdinReady(); ++}) ++ ++template ()())> ++Ret doUntilStdinAvailable( ++ const std::enable_if_t& fail, ++ const Fun& f) { ++ waitForStdin(); ++ return clang::clangd::retryAfterSignalUnlessShutdown(fail, f); ++} ++ ++#define retryAfterSignalUnlessShutdown doUntilStdinAvailable ++ ++#endif ++ + //===--- JSONTransport.cpp - sending and receiving LSP messages over JSON -===// + // + // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. diff --git a/packages/examples/src/clangd/README.md b/packages/examples/src/clangd/README.md new file mode 100644 index 00000000..6b4e4e10 --- /dev/null +++ b/packages/examples/src/clangd/README.md @@ -0,0 +1,5 @@ +# Cpp Language Client & Clangd Language Server (Worker/Wasm) + +This example is based on the wonderful [work](https://github.com/guyutongxue/clangd-in-browser) from [Guyutongxue](https://github.com/guyutongxue). + +This example has less features and therefore less code and will be used to test and integrate new features under development. diff --git a/packages/examples/src/clangd/client/config.ts b/packages/examples/src/clangd/client/config.ts new file mode 100644 index 00000000..05bb8592 --- /dev/null +++ b/packages/examples/src/clangd/client/config.ts @@ -0,0 +1,138 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) 2024 TypeFox and others. + * Licensed under the MIT License. See LICENSE in the package root for license information. + * ------------------------------------------------------------------------------------------ */ + +import * as vscode from 'vscode'; +import getConfigurationServiceOverride from '@codingame/monaco-vscode-configuration-service-override'; +import getTextmateServiceOverride from '@codingame/monaco-vscode-textmate-service-override'; +import getThemeServiceOverride from '@codingame/monaco-vscode-theme-service-override'; +import '@codingame/monaco-vscode-theme-defaults-default-extension'; +// this is required syntax highlighting +import '@codingame/monaco-vscode-cpp-default-extension'; +import { BrowserMessageReader, BrowserMessageWriter } from 'vscode-languageclient/browser.js'; +import { LanguageClientConfig, MonacoEditorLanguageClientWrapper, UserConfig } from 'monaco-editor-wrapper'; +import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'; +import { FILE_PATH, LANGUAGE_ID, WORKSPACE_PATH } from '../definitions.js'; +import { createServer } from '../worker/server.js'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const self = globalThis as any; +self.MonacoEnvironment = { getWorker: () => new EditorWorker() }; + +let clientRunning = false; +let retry = 0; +let succeeded = false; +const wrapper = new MonacoEditorLanguageClientWrapper(); + +export const createUserConfig = async (code: string): Promise => { + const serverWorker = await createServer(); + const recreateLsp = async () => { + console.log('reloading lsp...'); + wrapper.getLanguageClientWrapper()?.restartLanguageClient(serverWorker, false); + }; + + const restart = async () => { + if (clientRunning) { + try { + clientRunning = false; + console.log('indeterminate'); + readerOnError.dispose(); + readerOnClose.dispose(); + wrapper + .getLanguageClientWrapper() + ?.restartLanguageClient(serverWorker, false); + } finally { + retry++; + if (retry > 5 && !succeeded) { + console.log('disabled'); + console.error('Failed to start clangd after 5 retries'); + } else { + setTimeout(recreateLsp, 1000); + } + } + } + }; + + const reader = new BrowserMessageReader(serverWorker); + const writer = new BrowserMessageWriter(serverWorker); + const readerOnError = reader.onError(() => restart); + const readerOnClose = reader.onClose(() => restart); + const successCallback = reader.listen(() => { + succeeded = true; + console.log('ready'); + successCallback.dispose(); + }); + + const languageClientConfig: LanguageClientConfig = { + languageId: LANGUAGE_ID, + name: 'Clangd WASM Language Server', + options: { + $type: 'WorkerDirect', + worker: serverWorker, + }, + clientOptions: { + documentSelector: [LANGUAGE_ID], + workspaceFolder: { + index: 0, + name: 'workspace', + uri: vscode.Uri.file(WORKSPACE_PATH), + }, + }, + connectionProvider: { + get: async () => ({ reader, writer }), + }, + }; + + return { + languageClientConfig, + wrapperConfig: { + serviceConfig: { + workspaceConfig: { + workspaceProvider: { + trusted: true, + workspace: { + workspaceUri: vscode.Uri.file(WORKSPACE_PATH), + }, + async open(p) { + console.log(`Editor open request: ${p}`); + return false; + }, + }, + }, + userServices: { + ...getConfigurationServiceOverride(), + ...getTextmateServiceOverride(), + ...getThemeServiceOverride(), + }, + debugLogging: true, + }, + editorAppConfig: { + $type: 'extended', + codeResources: { + main: { + text: code, + uri: FILE_PATH, + }, + }, + userConfiguration: { + json: getUserConfigurationJson(), + }, + useDiffEditor: false, + }, + }, + loggerConfig: { + enabled: true, + debugEnabled: true, + }, + }; +}; + +const getUserConfigurationJson = () => { + return JSON.stringify({ + 'workbench.colorTheme': 'Default Dark Modern', + 'editor.wordBasedSuggestions': 'off', + 'editor.inlayHints.enabled': 'offUnlessPressed', + 'editor.quickSuggestionsDelay': 200, + }); +}; diff --git a/packages/examples/src/clangd/client/hello.cpp b/packages/examples/src/clangd/client/hello.cpp new file mode 100644 index 00000000..ef7cc7b5 --- /dev/null +++ b/packages/examples/src/clangd/client/hello.cpp @@ -0,0 +1,6 @@ +#include +#include "tester.h" + +int main() { + std::println("Hello, {}!", "world"); +} diff --git a/packages/examples/src/clangd/client/main.ts b/packages/examples/src/clangd/client/main.ts new file mode 100644 index 00000000..256bb3c1 --- /dev/null +++ b/packages/examples/src/clangd/client/main.ts @@ -0,0 +1,50 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) 2024 TypeFox and others. + * Licensed under the MIT License. See LICENSE in the package root for license information. + * ------------------------------------------------------------------------------------------ */ + +import * as vscode from 'vscode'; +import { RegisteredFileSystemProvider, registerFileSystemOverlay, RegisteredMemoryFile } from '@codingame/monaco-vscode-files-service-override'; +import { useWorkerFactory } from 'monaco-editor-wrapper/workerFactory'; +import { MonacoEditorLanguageClientWrapper } from 'monaco-editor-wrapper'; +import { createUserConfig } from './config.js'; +import helloCppCode from './hello.cpp?raw'; +import testerHCode from './tester.h?raw'; + +export const configureMonacoWorkers = () => { + useWorkerFactory({ + ignoreMapping: true, + workerLoaders: { + editorWorkerService: () => new Worker(new URL('monaco-editor/esm/vs/editor/editor.worker.js', import.meta.url), { type: 'module' }), + } + }); +}; + +export const runClangdWrapper = () => { + const wrapper = new MonacoEditorLanguageClientWrapper(); + const htmlElement = document.getElementById('monaco-editor-root'); + + try { + document.querySelector('#button-start')?.addEventListener('click', async () => { + const userConfig = await createUserConfig(helloCppCode); + await wrapper.initAndStart(userConfig, htmlElement); + + const helloCppUri = vscode.Uri.file('/home/web_user/hello.cpp'); + const testerHUri = vscode.Uri.file('/home/web_user/tester.h'); + + const fileSystemProvider = new RegisteredFileSystemProvider(false); + fileSystemProvider.registerFile(new RegisteredMemoryFile(helloCppUri, helloCppCode)); + fileSystemProvider.registerFile(new RegisteredMemoryFile(testerHUri, testerHCode)); + + registerFileSystemOverlay(1, fileSystemProvider); + + await vscode.workspace.openTextDocument(helloCppUri); + await vscode.workspace.openTextDocument(testerHUri); + }); + document.querySelector('#button-dispose')?.addEventListener('click', async () => { + await wrapper.dispose(); + }); + } catch (e) { + console.error(e); + } +}; diff --git a/packages/examples/src/clangd/client/tester.h b/packages/examples/src/clangd/client/tester.h new file mode 100644 index 00000000..e1c2e124 --- /dev/null +++ b/packages/examples/src/clangd/client/tester.h @@ -0,0 +1,6 @@ +namespace Tester { + class MyClass { + public: + void printHelloWorld(); + }; +} diff --git a/packages/examples/src/clangd/definitions.ts b/packages/examples/src/clangd/definitions.ts new file mode 100644 index 00000000..6fd078e1 --- /dev/null +++ b/packages/examples/src/clangd/definitions.ts @@ -0,0 +1,15 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) 2024 TypeFox and others. + * Licensed under the MIT License. See LICENSE in the package root for license information. + * ------------------------------------------------------------------------------------------ */ + +export const LANGUAGE_ID = 'cpp'; +export const WORKSPACE_PATH = '/home/web_user'; +export const FILE_PATH = '/home/web_user/main.cpp'; + +export const COMPILE_ARGS = [ + '-xc++', + '-std=c++2b', + '-pedantic-errors', + '-Wall', +]; diff --git a/packages/examples/src/clangd/worker/clangd-server.ts b/packages/examples/src/clangd/worker/clangd-server.ts new file mode 100644 index 00000000..a24a4030 --- /dev/null +++ b/packages/examples/src/clangd/worker/clangd-server.ts @@ -0,0 +1,157 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) 2024 TypeFox and others. + * Licensed under the MIT License. See LICENSE in the package root for license information. + * ------------------------------------------------------------------------------------------ */ + +/// + +import { COMPILE_ARGS, FILE_PATH, WORKSPACE_PATH } from '../definitions.js'; +import { JsonStream } from './json_stream.js'; +import { BrowserMessageReader, BrowserMessageWriter } from 'vscode-languageserver/browser.js'; + +declare const self: DedicatedWorkerGlobalScope; + +const wasmBase = `${import.meta.env.BASE_URL}packages/examples/resources/clangd/wasm/`; +const wasmUrl = `${wasmBase}clangd.wasm`; +const jsModule = import( /* @vite-ignore */ `${wasmBase}clangd.js`); + +// Pre-fetch wasm, and report progress to main +const wasmResponse = await fetch(wasmUrl); +const wasmSize = __WASM_SIZE__; +const wasmReader = wasmResponse.body!.getReader(); +let receivedLength = 0; +const chunks: Uint8Array[] = []; +let loadingComplete = false; +while (!loadingComplete) { + const { done, value } = await wasmReader.read(); + loadingComplete = done; + if (value) { + chunks.push(value); + receivedLength += value.length; + self.postMessage({ + type: 'progress', + value: receivedLength, + max: Number(wasmSize), + }); + } +} +const wasmBlob = new Blob(chunks, { type: 'application/wasm' }); +const wasmDataUrl = URL.createObjectURL(wasmBlob); + +const { default: Clangd } = await jsModule; + +const textEncoder = new TextEncoder(); +let resolveStdinReady = () => { }; +const stdinChunks: string[] = []; +const currentStdinChunk: Array = []; + +const stdin = (): number | null => { + if (currentStdinChunk.length === 0) { + if (stdinChunks.length === 0) { + // Should not reach here + // stdinChunks.push("Content-Length: 0\r\n", "\r\n"); + console.error('Try to fetch exhausted stdin'); + return null; + } + const nextChunk = stdinChunks.shift()!; + currentStdinChunk.push(...textEncoder.encode(nextChunk), null); + } + return currentStdinChunk.shift()!; +}; + +const jsonStream = new JsonStream(); + +const stdout = (charCode: number) => { + const jsonOrNull = jsonStream.insert(charCode); + if (jsonOrNull !== null) { + console.log('%c%s', 'color: green', jsonOrNull); + writer.write(JSON.parse(jsonOrNull)); + } +}; + +const LF = 10; +let stderrLine = ''; +const stderr = (charCode: number) => { + if (charCode === LF) { + console.log('%c%s', 'color: darkorange', stderrLine); + stderrLine = ''; + } else { + stderrLine += String.fromCharCode(charCode); + } +}; + +const stdinReady = async () => { + if (stdinChunks.length === 0) { + return new Promise((r) => (resolveStdinReady = r)); + } +}; + +const onAbort = () => { + writer.end(); + self.reportError('clangd aborted'); +}; + +const clangd = await Clangd({ + thisProgram: '/usr/bin/clangd', + locateFile: (path: string, prefix: string) => { + return path.endsWith('.wasm') ? wasmDataUrl : `${prefix}${path}`; + }, + stdinReady, + stdin, + stdout, + stderr, + onExit: onAbort, + onAbort, +}); +console.log(clangd); + +const flags = [ + ...COMPILE_ARGS, + '--target=wasm32-wasi', + '-isystem/usr/include/c++/v1', + '-isystem/usr/include/wasm32-wasi/c++/v1', + '-isystem/usr/include', + '-isystem/usr/include/wasm32-wasi', +]; + +clangd.FS.writeFile(FILE_PATH, ''); +clangd.FS.writeFile( + `${WORKSPACE_PATH}/.clangd`, + JSON.stringify({ CompileFlags: { Add: flags } }) +); + +clangd.FS.writeFile( + `${WORKSPACE_PATH}/tester.h`, + 'struct Tester {}' +); +// const test2 = clangd.FS.readFile('/usr/include/wasm32-wasi/stdio.h'); +// console.log(String.fromCharCode.apply(null, test2)); + +function startServer() { + console.log('%c%s', 'font-size: 2em; color: green', 'clangd started'); + clangd.callMain([]); +} +startServer(); + +self.postMessage({ type: 'ready' }); + +const reader = new BrowserMessageReader(self); +const writer = new BrowserMessageWriter(self); + +reader.listen((data) => { + // non-ASCII characters cause bad Content-Length. Just escape them. + const body = JSON.stringify(data).replace(/[\u007F-\uFFFF]/g, (ch) => { + return '\\u' + ch.codePointAt(0)!.toString(16).padStart(4, '0'); + }); + const header = `Content-Length: ${body.length}\r\n`; + const delimiter = '\r\n'; + stdinChunks.push(header, delimiter, body); + resolveStdinReady(); + // console.log("%c%s", "color: red", `${header}${delimiter}${body}`); +}); + +// setTimeout(() => { +// // test read back +// const test1 = clangd.FS.readFile(`${WORKSPACE_PATH}/tester.h`) as number[]; +// console.log(String.fromCharCode.apply(null, test1)); +// }, 5000); diff --git a/packages/examples/src/clangd/worker/json_stream.ts b/packages/examples/src/clangd/worker/json_stream.ts new file mode 100644 index 00000000..80b6d578 --- /dev/null +++ b/packages/examples/src/clangd/worker/json_stream.ts @@ -0,0 +1,64 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) 2024 TypeFox and others. + * Licensed under the MIT License. See LICENSE in the package root for license information. + * ------------------------------------------------------------------------------------------ */ + +const QUOT = 34; +const LBRACE = 123; +const RBRACE = 125; +const BACKSLASH = 92; + +export class JsonStream { + #inJson = false; + #rawText: number[] = []; + #unbalancedBraces = 0; + #inString = false; + #inEscape = 0; + readonly #textDecoder = new TextDecoder(); + + constructor() { } + + /** + * Insert a char into current partial JSON + * @param charCode + * @returns Complete JSON string if the inserted char makes the JSON complete, otherwise null + */ + insert(charCode: number): string | null { + if (!this.#inJson && charCode === LBRACE) { + this.#inJson = true; + this.#rawText = []; + } + if (!this.#inJson) { + return null; + } + this.#rawText.push(charCode); + if (this.#inString) { + if (this.#inEscape) { + if (charCode === 75) { + // \uxxxx + this.#inEscape += 4; + } + this.#inEscape--; + } else { + if (charCode === BACKSLASH) { + this.#inEscape = 1; + } else if (charCode === QUOT) { + this.#inString = false; + } + } + } else { + if (charCode === LBRACE) { + this.#unbalancedBraces++; + } else if (charCode === RBRACE) { + this.#unbalancedBraces--; + if (this.#unbalancedBraces === 0) { + this.#inJson = false; + return this.#textDecoder.decode(new Uint8Array(this.#rawText)); + } + } else if (charCode === QUOT) { + this.#inString = true; + } + } + return null; + } +} diff --git a/packages/examples/src/clangd/worker/server.ts b/packages/examples/src/clangd/worker/server.ts new file mode 100644 index 00000000..90f93415 --- /dev/null +++ b/packages/examples/src/clangd/worker/server.ts @@ -0,0 +1,29 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) 2024 TypeFox and others. + * Licensed under the MIT License. See LICENSE in the package root for license information. + * ------------------------------------------------------------------------------------------ */ + +import clangdWorkerUrl from './clangd-server?worker&url'; + +export async function createServer() { + let clangdResolve = () => { }; + const clangdReady = new Promise((r) => (clangdResolve = r)); + const worker = new Worker(clangdWorkerUrl, { + type: 'module', + name: 'Clangd Server Worker', + }); + const readyListener = (e: MessageEvent) => { + switch (e.data?.type) { + case 'ready': { + clangdResolve(); + break; + } + case 'progress': { + break; + } + } + }; + worker.addEventListener('message', readyListener); + await clangdReady; + return worker; +} diff --git a/packages/examples/src/vite-env.d.ts b/packages/examples/src/vite-env.d.ts index a9f5ee94..3fef0115 100644 --- a/packages/examples/src/vite-env.d.ts +++ b/packages/examples/src/vite-env.d.ts @@ -3,6 +3,10 @@ * Licensed under the MIT License. See LICENSE in the package root for license information. * ------------------------------------------------------------------------------------------ */ +/// + +declare const __WASM_SIZE__: number; + declare module '*?raw' { const content: string; export default content; diff --git a/vite.config.ts b/vite.config.ts index 1c6f5956..3a0e78ce 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See LICENSE in the package root for license information. * ------------------------------------------------------------------------------------------ */ +import fs from 'node:fs'; import { defineConfig as defineViteConfig, mergeConfig } from 'vite'; import { defineConfig as defineVitestConfig } from 'vitest/config'; import * as path from 'path'; @@ -11,6 +12,7 @@ import importMetaUrlPlugin from '@codingame/esbuild-import-meta-url-plugin'; import vsixPlugin from '@codingame/monaco-vscode-rollup-vsix-plugin'; import react from '@vitejs/plugin-react'; +const clangdWasmLocation = 'packages/examples/resources/clangd/wasm/clangd.wasm'; const viteConfig = defineViteConfig({ build: { target: 'esnext', @@ -29,8 +31,10 @@ const viteConfig = defineViteConfig({ wrapperLangium: path.resolve(__dirname, 'packages/examples/wrapper_langium.html'), // python python: path.resolve(__dirname, 'packages/examples/python.html'), - // grrovy + // groovy groovy: path.resolve(__dirname, 'packages/examples/groovy.html'), + // clangd + clangd: path.resolve(__dirname, 'packages/examples/clangd.html'), // monaco-editor-react // langium @@ -50,7 +54,11 @@ const viteConfig = defineViteConfig({ }, server: { origin: 'http://localhost:20001', - port: 20001 + port: 20001, + headers: { + 'Cross-Origin-Opener-Policy': 'same-origin', + 'Cross-Origin-Embedder-Policy': 'require-corp', + } }, optimizeDeps: { esbuildOptions: { @@ -64,7 +72,9 @@ const viteConfig = defineViteConfig({ react(), ], define: { - rootDirectory: JSON.stringify(__dirname) + rootDirectory: JSON.stringify(__dirname), + // Server-provided Content-Length header may be gzipped, get the real size in build time + __WASM_SIZE__: fs.existsSync(clangdWasmLocation) ? fs.statSync(clangdWasmLocation).size : 0 }, worker: { format: 'es'