Skip to content

Commit

Permalink
add a progress bar
Browse files Browse the repository at this point in the history
  • Loading branch information
guyutongxue committed Jan 30, 2024
1 parent ac19e12 commit b4b1e6c
Show file tree
Hide file tree
Showing 8 changed files with 206 additions and 34 deletions.
2 changes: 1 addition & 1 deletion build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ cp -r build/lib/clang/$LLVM_VER_MAJOR/include/* $ROOT_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)' --preload-file=$ROOT_DIR/wasi-sysroot/include@/usr/include" \
-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=$ROOT_DIR/wasi-sysroot/include@/usr/include" \
-DCMAKE_BUILD_TYPE=MinSizeRel \
-DLLVM_TARGET_ARCH=wasm32-emscripten \
-DLLVM_DEFAULT_TARGET_TRIPLE=wasm32-wasi \
Expand Down
37 changes: 27 additions & 10 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
Expand All @@ -15,28 +15,45 @@
<div id="buildPanelResize"></div>
<div id="buildPanelBody">
<div id="buildPanelHeader">
<button class="success" id="run"><i class="icon icon-run"></i>Run code</button>
<button class="success" id="run">
<i class="icon icon-run"></i>Run code
</button>
<div class="button-group" id="buildPanelTab">
<button>Input</button>
<button active>Output</button>
<button>Compiler Diagnostics</button>
</div>
<div class="spacer"></div>
<button class="danger" id="closeBuildPanel"><i class="icon icon-close"></i></button>
<button class="danger" id="closeBuildPanel">
<i class="icon icon-close"></i>
</button>
</div>
<textarea class="io display-none" id="buildPanelInput" placeholder="Type input here..."></textarea>
<textarea
class="io display-none"
id="buildPanelInput"
placeholder="Type input here..."
></textarea>
<pre class="io" id="buildPanelOutput"></pre>
</div>
</div>
<div id="footer">
<div>
<div id="status"></div>
<div id="progress"></div>
<div id="status" data-tooltip data-tooltip-left>
<i class="icon icon-loading"></i>
<div id="progress">
<div class="progress-bar">
<!-- <div class="progress-value"></div> -->
</div>
</div>
</div>
<div>
<button id="showBuildPanel"><i class="icon icon-run"></i></button>
<button id="toggleTheme"><i class="icon icon-toggle-theme"></i></button>
<span> <a href="https://guyutongxue.site" target="_blank">Guyutongxue</a> &copy; 2024</span>
<button id="showBuildPanel" data-tooltip="Run code"><i class="icon icon-run"></i></button>
<button id="toggleTheme" data-tooltip="Toggle theme">
<i class="icon icon-toggle-theme"></i>
</button>
<span>
<a href="https://guyutongxue.site" target="_blank">Guyutongxue</a>
&copy; 2024</span
>
</div>
</div>
</div>
Expand Down
3 changes: 3 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { MonacoLanguageClient } from "monaco-languageclient";

import { LANGUAGE_ID } from "./config";
import { createServer } from "./server";
import { setClangdStatus } from "./ui";

let clientRunning = false;
let retry = 0;
Expand All @@ -26,6 +27,7 @@ export async function createClient(serverWorker: Worker) {
const readerOnClose = reader.onClose(() => restart);
const successCallback = reader.listen(() => {
succeeded = true;
setClangdStatus("ready");
successCallback.dispose();
});

Expand All @@ -47,6 +49,7 @@ export async function createClient(serverWorker: Worker) {
if (clientRunning) {
try {
clientRunning = false;
setClangdStatus("indeterminate");
await client.stop();
await client.dispose();
readerOnError.dispose();
Expand Down
44 changes: 29 additions & 15 deletions src/main.worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,34 @@ import {
declare var self: DedicatedWorkerGlobalScope;

const wasmBase = `${import.meta.env.BASE_URL}wasm/`;
const prefetched: Record<string, string> = Object.fromEntries(
await Promise.all(
["clangd.wasm", "clangd.data"].map(async (name) => {
const url = `${wasmBase}${name}`;
const blob = await fetch(url).then((r) => r.blob());
const dataUrl = URL.createObjectURL(blob);
return [name, dataUrl];
})
)
);
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 = wasmResponse.headers.get("Content-Length");
const wasmReader = wasmResponse.body!.getReader();
let receivedLength = 0;
let chunks: Uint8Array[] = [];
while (true) {
const { done, value } = await wasmReader.read();
if (done) {
break;
}
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 import(
/* @vite-ignore */ `${wasmBase}clangd.js`
);
const { default: Clangd } = await jsModule;

const textEncoder = new TextEncoder();
let resolveStdinReady = () => {};
Expand Down Expand Up @@ -79,7 +93,7 @@ const onAbort = () => {
const clangd = await Clangd({
thisProgram: "/usr/bin/clangd",
locateFile: (path: string, prefix: string) => {
return prefetched[path] ?? `${prefix}${path}`;
return path.endsWith(".wasm") ? wasmDataUrl : `${prefix}${path}`;
},
stdinReady,
stdin,
Expand Down Expand Up @@ -109,7 +123,7 @@ function startServer() {
}
startServer();

self.postMessage("ready");
self.postMessage({ type: "ready" });

const reader = new BrowserMessageReader(self);
const writer = new BrowserMessageWriter(self);
Expand Down
15 changes: 12 additions & 3 deletions src/server.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import ClangdWorker from "./main.worker?worker";
import { setClangdStatus } from "./ui";

export async function createServer() {
let clangdResolve = () => {};
const clangdReady = new Promise<void>((r) => (clangdResolve = r));
const worker = new ClangdWorker();
const readyListener = (e: MessageEvent) => {
if (e.data === "ready") {
clangdResolve();
worker.removeEventListener("message", readyListener);
switch (e.data?.type) {
case "ready": {
clangdResolve();
worker.removeEventListener("message", readyListener);
setClangdStatus("indeterminate");
break;
}
case "progress": {
setClangdStatus(e.data.value, e.data.progress);
break;
}
}
};
worker.addEventListener("message", readyListener);
Expand Down
117 changes: 115 additions & 2 deletions src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ body.dark {
min-width: 0;
}

button, textarea {
button,
textarea {
all: unset;
}

Expand Down Expand Up @@ -101,6 +102,7 @@ body {
}
&:has(.loader)::after {
content: "Loading Monaco Editor";
color: var(--c-text-light);
}
}

Expand Down Expand Up @@ -208,7 +210,6 @@ body {
}
}


.io {
margin: 0;
flex-grow: 1;
Expand Down Expand Up @@ -259,6 +260,64 @@ body {
}
}

#status {
display: flex;
flex-direction: row;
align-items: stretch;
gap: 0.5em;
--progress-value: 0;
--progress-max: 1;
--tooltip-text: "Clangd is loading";

&[data-ready] {
--tooltip-text: "Clangd is ready";
}
}

#progress {
width: 8em;
padding: 0 0.5em;
margin: auto;
}
[data-ready] #progress {
display: none;
}

.progress-bar {
height: 0.5em;
background-color: var(--c-bg);
border-radius: 0.25em;
position: relative;
}
.progress-bar::before {
content: "";
position: absolute;
top: 0;
left: 0;
height: 100%;
display: block;
background-color: var(--c-accent);
border-radius: 0.25em;
--width: calc(var(--progress-value, 0) / var(--progress-max, 1) * 100%);
width: var(--width, 0);
transition: left width 0.3s ease-in-out;
}
[data-indeterminate] .progress-bar::before {
--width: 33%;
animation: progress-bar-indeterminate 2s ease-in-out infinite;
}
@keyframes progress-bar-indeterminate {
0% {
left: 0%;
}
50% {
left: calc(100% - var(--width, 0));
}
100% {
left: 0%;
}
}

a {
color: var(--c-accent);
text-decoration: none;
Expand All @@ -269,6 +328,28 @@ a {
vertical-align: middle;
}

.icon-loading {
--un-icon: url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 24 24' width='1.2em' height='1.2em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='none' stroke='currentColor' stroke-linecap='round' stroke-width='2'%3E%3Cpath stroke-dasharray='60' stroke-dashoffset='60' stroke-opacity='.3' d='M12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3Z'%3E%3Canimate fill='freeze' attributeName='stroke-dashoffset' dur='1.3s' values='60;0'/%3E%3C/path%3E%3Cpath stroke-dasharray='15' stroke-dashoffset='15' d='M12 3C16.9706 3 21 7.02944 21 12'%3E%3Canimate fill='freeze' attributeName='stroke-dashoffset' dur='0.3s' values='15;0'/%3E%3CanimateTransform attributeName='transform' dur='1.5s' repeatCount='indefinite' type='rotate' values='0 12 12;360 12 12'/%3E%3C/path%3E%3C/g%3E%3C/svg%3E");
-webkit-mask: var(--un-icon) no-repeat;
mask: var(--un-icon) no-repeat;
-webkit-mask-size: 100% 100%;
mask-size: 100% 100%;
background-color: currentColor;
color: inherit;
width: 1.2em;
height: 1.2em;
}
[data-ready] .icon-loading {
--un-icon: url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 24 24' width='1.2em' height='1.2em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='m9.55 18l-5.7-5.7l1.425-1.425L9.55 15.15l9.175-9.175L20.15 7.4z'/%3E%3C/svg%3E");
-webkit-mask: var(--un-icon) no-repeat;
mask: var(--un-icon) no-repeat;
-webkit-mask-size: 100% 100%;
mask-size: 100% 100%;
background-color: currentColor;
color: inherit;
width: 1.2em;
height: 1.2em;
}
.icon-run {
--un-icon: url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 24 24' width='1.2em' height='1.2em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M8 19V5l11 7z'/%3E%3C/svg%3E");
-webkit-mask: var(--un-icon) no-repeat;
Expand Down Expand Up @@ -317,3 +398,35 @@ a {
.display-none {
display: none !important;
}

[data-tooltip] {
position: relative;
}

[data-tooltip]::after {
display: none;
content: var(--tooltip-text, attr(data-tooltip));
position: absolute;
top: -0.5em;
left: 50%;
transform: translate(-50%, -100%);
padding: 0.5em 1em;
background-color: var(--c-bg-dark);
color: var(--c-text);
border-radius: 0.5em;
font-size: 0.8em;
white-space: nowrap;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s ease-in-out;
}

[data-tooltip]:hover::after {
display: block;
opacity: 1;
}

[data-tooltip][data-tooltip-left]::after {
left: 0%;
transform: translate(0%, -100%);
}
18 changes: 17 additions & 1 deletion src/ui.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { editorValueGetter } from "./config";
import { runCode } from "./runner";

const status = document.querySelector<HTMLElement>("#status")!;
const toggleThemeBtn = document.querySelector<HTMLButtonElement>("#toggleTheme")!;
const buildPanel = document.querySelector<HTMLElement>("#buildPanel")!;
const showBuildPanel = document.querySelector<HTMLElement>("#showBuildPanel")!;
Expand Down Expand Up @@ -125,4 +126,19 @@ function toggleTheme() {
}
toggleThemeBtn.addEventListener("click", toggleTheme);

export {};
export function setClangdStatus(status: "ready" | "indeterminate"): void;
export function setClangdStatus(value: number, max: number): void;
export function setClangdStatus(strOrVal: string | number, max?: number) {
if (typeof strOrVal === "number") {
status.removeAttribute("data-ready");
status.removeAttribute("data-indeterminate");
status.style.setProperty("--progress-value", `${strOrVal}`);
status.style.setProperty("--progress-max", `${max}`);
} else if (strOrVal === "ready") {
status.setAttribute("data-ready", "");
status.removeAttribute("data-indeterminate");
} else if (strOrVal === "indeterminate") {
status.removeAttribute("data-ready");
status.setAttribute("data-indeterminate", "");
}
}
4 changes: 2 additions & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"compilerOptions": {
"target": "ES2020",
"target": "ES2022",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"skipLibCheck": true,

/* Bundler mode */
Expand Down

0 comments on commit b4b1e6c

Please sign in to comment.