447 lines
14 KiB
JavaScript
447 lines
14 KiB
JavaScript
import * as monaco from "monaco-editor";
|
|
import {
|
|
createConnection,
|
|
MonacoLanguageClient,
|
|
MonacoServices,
|
|
Services,
|
|
} from "monaco-languageclient";
|
|
import { createMessageConnection } from "vscode-jsonrpc";
|
|
import { AbstractMessageReader } from "vscode-jsonrpc/lib/messageReader.js";
|
|
import { AbstractMessageWriter } from "vscode-jsonrpc/lib/messageWriter.js";
|
|
import { Terminal } from "xterm";
|
|
import { FitAddon } from "xterm-addon-fit";
|
|
|
|
import "xterm/css/xterm.css";
|
|
|
|
const DEBUG = window.location.hash === "#debug";
|
|
const config = window.rijuConfig;
|
|
|
|
const formatButton = document.getElementById("formatButton");
|
|
const lspButton = document.getElementById("lspButton");
|
|
const lspButtonState = document.getElementById("lspButtonState");
|
|
const connectionStatus = document.getElementById("connectionStatus");
|
|
|
|
function closeModal() {
|
|
document.querySelector("html").classList.remove("is-clipped");
|
|
document.getElementById("modal").classList.remove("is-active");
|
|
}
|
|
|
|
function showError({ message, data }) {
|
|
document.getElementById("modal-title").innerText = message;
|
|
document.getElementById("modal-data").innerText =
|
|
data || "(no output on stderr)";
|
|
document.getElementById("modal").classList.add("is-active");
|
|
document.querySelector("html").classList.add("is-clipped");
|
|
}
|
|
|
|
class RijuMessageReader extends AbstractMessageReader {
|
|
constructor(socket) {
|
|
super();
|
|
this.state = "initial";
|
|
this.callback = null;
|
|
this.messageQueue = [];
|
|
this.socket = socket;
|
|
this.socket.addEventListener("message", (event) => {
|
|
this.readMessage(event.data);
|
|
});
|
|
}
|
|
|
|
listen(callback) {
|
|
if (this.state === "initial") {
|
|
this.state = "listening";
|
|
this.callback = callback;
|
|
while (this.messageQueue.length > 0) {
|
|
this.readMessage(this.messageQueue.pop());
|
|
}
|
|
}
|
|
}
|
|
|
|
readMessage(rawMessage) {
|
|
if (this.state === "initial") {
|
|
this.messageQueue.splice(0, 0, rawMessage);
|
|
} else if (this.state === "listening") {
|
|
let message;
|
|
try {
|
|
message = JSON.parse(rawMessage);
|
|
} catch (err) {
|
|
return;
|
|
}
|
|
switch (message && message.event) {
|
|
case "lspOutput":
|
|
if (DEBUG) {
|
|
console.log("RECEIVE LSP:", message.output);
|
|
}
|
|
this.callback(message.output);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class RijuMessageWriter extends AbstractMessageWriter {
|
|
constructor(socket) {
|
|
super();
|
|
this.socket = socket;
|
|
}
|
|
|
|
write(msg) {
|
|
switch (msg.method) {
|
|
case "initialize":
|
|
msg.params.processId = null;
|
|
if (config.lsp.disableDynamicRegistration) {
|
|
this.disableDynamicRegistration(msg);
|
|
}
|
|
break;
|
|
case "textDocument/didOpen":
|
|
if (config.lsp.lang) {
|
|
msg.params.textDocument.languageId = config.lsp.lang;
|
|
}
|
|
}
|
|
if (DEBUG) {
|
|
console.log("SEND LSP:", msg);
|
|
}
|
|
this.socket.send(JSON.stringify({ event: "lspInput", input: msg }));
|
|
}
|
|
|
|
disableDynamicRegistration(msg) {
|
|
if (!msg || typeof msg !== "object") return;
|
|
for (const [key, val] of Object.entries(msg)) {
|
|
if (key === "dynamicRegistration" && val === true)
|
|
msg.dynamicRegistration = false;
|
|
this.disableDynamicRegistration(val);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
let serviceLogBuffers = {};
|
|
let serviceLogLines = {};
|
|
|
|
let lastActivityTimestamp = new Date();
|
|
let idleDueToInactivity = false;
|
|
|
|
function recordActivity() {
|
|
lastActivityTimestamp = new Date();
|
|
if (idleDueToInactivity) {
|
|
scheduleConnect();
|
|
}
|
|
}
|
|
|
|
const term = new Terminal();
|
|
const fitAddon = new FitAddon();
|
|
term.loadAddon(fitAddon);
|
|
term.open(document.getElementById("terminal"));
|
|
|
|
fitAddon.fit();
|
|
window.addEventListener("resize", () => fitAddon.fit());
|
|
|
|
await new Promise((resolve) =>
|
|
term.write("Connecting to server...", resolve)
|
|
);
|
|
|
|
const initialRetryDelayMs = 200;
|
|
let retryDelayMs = initialRetryDelayMs;
|
|
|
|
function sendMessage(message) {
|
|
if (DEBUG) {
|
|
console.log("SEND:", message);
|
|
}
|
|
if (socket) {
|
|
socket.send(JSON.stringify(message));
|
|
}
|
|
}
|
|
|
|
function tryConnect() {
|
|
serviceLogBuffers = {};
|
|
serviceLogLines = {};
|
|
let clientDisposable = null;
|
|
let servicesDisposable = null;
|
|
connectionStatus.innerText = "connecting...";
|
|
console.log("Connecting to server...");
|
|
socket = new WebSocket(
|
|
(document.location.protocol === "http:" ? "ws://" : "wss://") +
|
|
document.location.host +
|
|
`/api/v1/ws?lang=${encodeURIComponent(config.id)}`
|
|
);
|
|
socket.addEventListener("open", () => {
|
|
connectionStatus.innerText = "connected";
|
|
console.log("Successfully connected to server");
|
|
});
|
|
socket.addEventListener("message", (event) => {
|
|
let message;
|
|
try {
|
|
message = JSON.parse(event.data);
|
|
} catch (err) {
|
|
console.error("Malformed message from server:", event.data);
|
|
return;
|
|
}
|
|
if (
|
|
DEBUG &&
|
|
message &&
|
|
message.event !== "lspOutput" &&
|
|
message.event !== "serviceLog"
|
|
) {
|
|
console.log("RECEIVE:", message);
|
|
}
|
|
if (message && message.event && message.event !== "error") {
|
|
retryDelayMs = initialRetryDelayMs;
|
|
}
|
|
switch (message && message.event) {
|
|
case "terminalClear":
|
|
term.reset();
|
|
return;
|
|
case "terminalOutput":
|
|
if (typeof message.output !== "string") {
|
|
console.error("Unexpected message from server:", message);
|
|
return;
|
|
}
|
|
term.write(message.output);
|
|
return;
|
|
case "formattedCode":
|
|
formatButton.disabled = false;
|
|
formatButton.classList.remove("is-loading");
|
|
if (
|
|
typeof message.code !== "string" ||
|
|
typeof message.originalCode !== "string"
|
|
) {
|
|
console.error("Unexpected message from server:", message);
|
|
return;
|
|
}
|
|
if (editor.getValue() === message.originalCode) {
|
|
editor.setValue(message.code);
|
|
}
|
|
return;
|
|
case "lspStopped":
|
|
lspButton.disabled = false;
|
|
lspButton.classList.remove("is-loading");
|
|
lspButton.classList.add("is-light");
|
|
lspButtonState.innerText = "OFF";
|
|
if (clientDisposable) {
|
|
clientDisposable.dispose();
|
|
clientDisposable = null;
|
|
}
|
|
if (servicesDisposable) {
|
|
servicesDisposable.dispose();
|
|
servicesDisposable = null;
|
|
}
|
|
break;
|
|
case "lspStarted":
|
|
lspButton.disabled = false;
|
|
lspButton.classList.remove("is-loading");
|
|
lspButton.classList.remove("is-light");
|
|
lspButtonState.innerText = "ON";
|
|
if (typeof message.root !== "string") {
|
|
console.error("Unexpected message from server:", message);
|
|
return;
|
|
}
|
|
const services = MonacoServices.create(editor, {
|
|
rootUri: `file://${message.root}`,
|
|
});
|
|
servicesDisposable = Services.install(services);
|
|
const newURI = `file://${message.root}/${config.main}`;
|
|
const oldModel = editor.getModel();
|
|
if (oldModel.uri.toString() !== newURI) {
|
|
// This code is likely to be buggy as it will probably
|
|
// never run and has thus never been tested.
|
|
editor.setModel(
|
|
monaco.editor.createModel(
|
|
oldModel.getValue(),
|
|
undefined,
|
|
monaco.Uri.parse(newURI)
|
|
)
|
|
);
|
|
oldModel.dispose();
|
|
}
|
|
const connection = createMessageConnection(
|
|
new RijuMessageReader(socket),
|
|
new RijuMessageWriter(socket)
|
|
);
|
|
const client = new MonacoLanguageClient({
|
|
name: "Riju",
|
|
clientOptions: {
|
|
documentSelector: [{ pattern: "**" }],
|
|
middleware: {
|
|
workspace: {
|
|
configuration: (params, token, configuration) => {
|
|
return Array(configuration(params, token).length).fill(
|
|
config.lsp.config !== undefined ? config.lsp.config : {}
|
|
);
|
|
},
|
|
},
|
|
},
|
|
initializationOptions: config.lsp.init || {},
|
|
},
|
|
connectionProvider: {
|
|
get: (errorHandler, closeHandler) =>
|
|
Promise.resolve(
|
|
createConnection(connection, errorHandler, closeHandler)
|
|
),
|
|
},
|
|
});
|
|
clientDisposable = client.start();
|
|
return;
|
|
case "lspOutput":
|
|
// Should be handled by RijuMessageReader
|
|
return;
|
|
case "serviceLog":
|
|
if (
|
|
typeof message.service !== "string" ||
|
|
typeof message.output !== "string"
|
|
) {
|
|
console.error("Unexpected message from server:", message);
|
|
return;
|
|
}
|
|
let buffer = serviceLogBuffers[message.service] || "";
|
|
let lines = serviceLogLines[message.service] || [];
|
|
buffer += message.output;
|
|
while (buffer.includes("\n")) {
|
|
const idx = buffer.indexOf("\n");
|
|
const line = buffer.slice(0, idx);
|
|
buffer = buffer.slice(idx + 1);
|
|
lines.push(line);
|
|
if (DEBUG) {
|
|
console.log(`${message.service.toUpperCase()} || ${line}`);
|
|
}
|
|
}
|
|
serviceLogBuffers[message.service] = buffer;
|
|
serviceLogLines[message.service] = lines;
|
|
return;
|
|
case "serviceFailed":
|
|
if (
|
|
typeof message.service !== "string" ||
|
|
typeof message.error !== "string"
|
|
) {
|
|
console.error("Unexpected message from server:", message);
|
|
return;
|
|
}
|
|
switch (message.service) {
|
|
case "formatter":
|
|
formatButton.disabled = false;
|
|
formatButton.classList.remove("is-loading");
|
|
showError({
|
|
message: "Could not prettify code!",
|
|
data: serviceLogLines["formatter"].join("\n"),
|
|
});
|
|
break;
|
|
case "lsp":
|
|
lspButton.disabled = false;
|
|
lspButton.classList.remove("is-loading");
|
|
lspButton.classList.add("is-light");
|
|
lspButtonState.innerText = "CRASHED";
|
|
break;
|
|
case "terminal":
|
|
term.write(`\r\n[${message.error}]`);
|
|
break;
|
|
}
|
|
return;
|
|
case "langConfig":
|
|
// We could use this message instead of hardcoding the
|
|
// language config into the HTML page returned from the
|
|
// server, but for now we just ignore it.
|
|
return;
|
|
default:
|
|
console.error("Unexpected message from server:", message);
|
|
return;
|
|
}
|
|
});
|
|
socket.addEventListener("close", (event) => {
|
|
if (event.wasClean) {
|
|
console.log("Connection closed cleanly");
|
|
} else {
|
|
console.error("Connection died");
|
|
}
|
|
if (clientDisposable) {
|
|
clientDisposable.dispose();
|
|
clientDisposable = null;
|
|
}
|
|
if (servicesDisposable) {
|
|
servicesDisposable.dispose();
|
|
servicesDisposable = null;
|
|
}
|
|
if (lspButtonState.innerText === "ON") {
|
|
lspButton.disabled = false;
|
|
lspButton.classList.remove("is-loading");
|
|
lspButton.classList.add("is-light");
|
|
lspButtonState.innerText = "DISCONNECTED";
|
|
}
|
|
scheduleConnect();
|
|
});
|
|
}
|
|
|
|
function scheduleConnect() {
|
|
idleDueToInactivity = new Date() - lastActivityTimestamp > 10 * 60 * 1000;
|
|
if (idleDueToInactivity) {
|
|
connectionStatus.innerText = "idle";
|
|
return;
|
|
}
|
|
const delay = retryDelayMs * Math.random();
|
|
console.log(`Trying to reconnect in ${Math.floor(delay)}ms`);
|
|
setTimeout(tryConnect, delay);
|
|
retryDelayMs *= 2;
|
|
}
|
|
|
|
let socket = null;
|
|
tryConnect();
|
|
|
|
term.onData((data) => {
|
|
sendMessage({ event: "terminalInput", input: data });
|
|
recordActivity();
|
|
});
|
|
|
|
const editor = monaco.editor.create(document.getElementById("editor"), {
|
|
minimap: { enabled: false },
|
|
scrollbar: { verticalScrollbarSize: 0 },
|
|
});
|
|
editor.addAction({
|
|
id: "runCode",
|
|
label: "Run",
|
|
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter],
|
|
contextMenuGroupId: "2_execution",
|
|
run: () => {
|
|
sendMessage({ event: "runCode", code: editor.getValue() });
|
|
},
|
|
});
|
|
editor.getModel().onDidChangeContent(() => recordActivity());
|
|
window.addEventListener("resize", () => editor.layout());
|
|
editor.getModel().setValue(config.template + "\n");
|
|
monaco.editor.setModelLanguage(
|
|
editor.getModel(),
|
|
config.monacoLang || "plaintext"
|
|
);
|
|
|
|
document.getElementById("runButton").addEventListener("click", () => {
|
|
sendMessage({ event: "runCode", code: editor.getValue() });
|
|
});
|
|
if (config.format) {
|
|
formatButton.classList.remove("is-hidden");
|
|
formatButton.addEventListener("click", () => {
|
|
formatButton.classList.add("is-loading");
|
|
formatButton.disabled = true;
|
|
serviceLogBuffers["formatter"] = "";
|
|
serviceLogLines["formatter"] = [];
|
|
sendMessage({ event: "formatCode", code: editor.getValue() });
|
|
});
|
|
}
|
|
if (config.lsp) {
|
|
lspButton.classList.remove("is-hidden");
|
|
lspButton.addEventListener("click", () => {
|
|
lspButton.classList.add("is-loading");
|
|
lspButton.disabled = true;
|
|
lspButton.classList.remove("is-light");
|
|
if (lspButtonState.innerText === "ON") {
|
|
sendMessage({ event: "lspStop" });
|
|
} else {
|
|
serviceLogBuffers["lsp"] = "";
|
|
serviceLogLines["lsp"] = [];
|
|
sendMessage({ event: "lspStart" });
|
|
}
|
|
});
|
|
}
|
|
|
|
for (const elt of document.querySelectorAll(".will-close-modal")) {
|
|
elt.addEventListener("click", closeModal);
|
|
}
|
|
}
|
|
|
|
main().catch(console.error);
|