Files
eleith dcf4d9c348
ci/woodpecker/push/web-explorer Pipeline was successful
ci/woodpecker/push/web-lsp Pipeline was successful
ci/woodpecker/push/web-pieces Pipeline was successful
ci/woodpecker/push/web-proxy Pipeline was successful
ci/woodpecker/push/web-worker Pipeline was successful
[web] rename editor route to admin
2026-06-01 10:15:51 -07:00

200 lines
5.1 KiB
JavaScript

import { createServer as httpServer } from "http";
import { fileURLToPath } from "url";
import { WebSocketServer } from "ws";
import { toSocket } from "vscode-ws-jsonrpc";
import {
createWebSocketConnection,
createServerProcess,
} from "vscode-ws-jsonrpc/server";
import { route as frontmatterRoute } from "./luzzle-lsp.js";
import { route as markdownRoute } from "./markdownlint-lsp.js";
const PORT = 9001;
const ROUTES = [frontmatterRoute, markdownRoute];
function createMultiplex(wsConn, routes) {
const servers = [];
for (const route of routes) {
const proc = createServerProcess(
route.name,
route.command,
route.args,
route.spawnOptions,
);
if (proc) {
servers.push({ route, proc });
} else {
console.error(`Failed to start ${route.name}`);
}
}
if (servers.length === 0) return null;
const diagnosticStore = new Map();
const documents = new Map();
const pendingRequests = new Map();
function getMergedDiagnostics(uri) {
const merged = [];
for (let i = 0; i < servers.length; i++) {
const stored = diagnosticStore.get(`${i}:${uri}`);
if (stored) merged.push(...stored);
}
return merged;
}
function isNotification(message) {
return message.method !== undefined && message.id === undefined;
}
function trackDocument(message) {
if (message.method === "textDocument/didOpen") {
const { uri, text } = message.params.textDocument;
documents.set(uri, text);
} else if (message.method === "textDocument/didChange") {
const uri = message.params.textDocument.uri;
const text = message.params.contentChanges?.[0]?.text;
if (text !== undefined) {
documents.set(uri, text);
}
} else if (message.method === "textDocument/didClose") {
documents.delete(message.params.textDocument.uri);
}
}
function trackPositionRequest(message) {
if (
message.id !== undefined &&
message.params?.position &&
message.params?.textDocument?.uri
) {
pendingRequests.set(message.id, {
uri: message.params.textDocument.uri,
position: message.params.position,
});
}
}
function handleClientMessage(message) {
trackDocument(message);
trackPositionRequest(message);
for (const { proc } of servers) {
proc.writer.write(JSON.parse(JSON.stringify(message)));
}
}
function handleDiagnostics(message, index) {
const { uri, version } = message.params;
diagnosticStore.set(`${index}:${uri}`, message.params.diagnostics || []);
wsConn.writer.write({
jsonrpc: "2.0",
method: "textDocument/publishDiagnostics",
params: { uri, diagnostics: getMergedDiagnostics(uri), version },
});
}
function handlePrimaryResponse(message, index) {
const req = pendingRequests.get(message.id);
if (req) {
pendingRequests.delete(message.id);
const text = documents.get(req.uri);
if (!servers[index].route.shouldRespond(text, req.position)) {
wsConn.writer.write({ jsonrpc: "2.0", id: message.id, result: null });
return;
}
}
wsConn.writer.write(message);
}
function handleServerMessage(message, index) {
if (message.method === "textDocument/publishDiagnostics") {
handleDiagnostics(message, index);
return;
}
if (isNotification(message)) {
wsConn.writer.write(message);
return;
}
if (index === 0) {
handlePrimaryResponse(message, index);
}
}
wsConn.reader.listen(handleClientMessage);
servers.forEach(({ proc }, index) => {
proc.reader.listen((message) => handleServerMessage(message, index));
});
wsConn.onClose(() => {
for (const { proc } of servers) proc.dispose();
});
servers[0].proc.onClose(() => wsConn.dispose());
return { servers, diagnosticStore, documents, pendingRequests };
}
function createServer(routes = ROUTES) {
const wss = new WebSocketServer({ noServer: true });
const server = httpServer((req, res) => {
if (req.url === "/health") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ status: "ok" }));
return;
}
res.writeHead(404, { "Content-Type": "text/plain" });
res.end("Not Found");
});
server.on("upgrade", (req, socket, head) => {
const url = new URL(req.url || "", "http://localhost");
if (url.pathname !== "/admin/lsp") {
socket.write("HTTP/1.1 404 Not Found\r\n\r\n");
socket.destroy();
return;
}
wss.handleUpgrade(req, socket, head, (ws) => {
wss.emit("connection", ws);
});
});
wss.on("connection", (ws) => {
console.log(`[${new Date().toISOString()}] LSP client connected`);
const socket = toSocket(ws);
const wsConn = createWebSocketConnection(socket);
const multiplex = createMultiplex(wsConn, routes);
if (!multiplex) {
console.error(
`[${new Date().toISOString()}] Failed to start any LSP process`,
);
ws.close();
return;
}
ws.on("close", () => {
console.log(`[${new Date().toISOString()}] LSP client disconnected`);
});
});
return server;
}
const server = createServer();
/* c8 ignore start */
if (process.argv[1] === fileURLToPath(import.meta.url)) {
server.listen(PORT, "0.0.0.0", () => {
console.log(`Luzzle Web LSP listening on port ${PORT}`);
});
}
/* c8 ignore stop */
export { server, createServer, createMultiplex, ROUTES };