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'