feat: connect NWScript language server to Monaco via WebSocket
Add the forked nwscript-ee-language-server as a git submodule and wire it up to the editor through a WebSocket-based LSP bridge: - Backend: lsp.service.ts spawns the language server in --stdio mode and bridges JSON-RPC messages between WebSocket and stdin/stdout - Backend: /ws/lsp upgrade handler in index.ts - Frontend: LspClient class using vscode-ws-jsonrpc for JSON-RPC over WebSocket, with Monaco providers for completions, hover, and diagnostics - Frontend: useLspClient/useLspDocument hooks integrated into MonacoEditor component
This commit is contained in:
@@ -10,6 +10,7 @@ import dockerRouter from "./routes/docker.js";
|
||||
import editorRouter from "./routes/editor.js";
|
||||
import terminalRouter from "./routes/terminal.js";
|
||||
import { attachWebSocket, createTerminalSession } from "./services/terminal.service.js";
|
||||
import { attachLspWebSocket } from "./services/lsp.service.js";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const app = express();
|
||||
@@ -41,6 +42,15 @@ app.get("*path", (_req, res) => {
|
||||
|
||||
server.on("upgrade", (request, socket, head) => {
|
||||
const url = new URL(request.url || "", `http://${request.headers.host}`);
|
||||
|
||||
if (url.pathname === "/ws/lsp") {
|
||||
const lspWss = new WebSocketServer({ noServer: true });
|
||||
lspWss.handleUpgrade(request, socket, head, (ws) => {
|
||||
attachLspWebSocket(ws);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const termMatch = url.pathname.match(/^\/ws\/terminal\/(.+)$/);
|
||||
if (termMatch) {
|
||||
const sessionId = termMatch[1];
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { spawn, ChildProcess } from "child_process";
|
||||
import { WebSocket } from "ws";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
let lspProcess: ChildProcess | null = null;
|
||||
let lspClient: WebSocket | null = null;
|
||||
|
||||
function getLspServerPath(): string {
|
||||
return path.resolve(
|
||||
__dirname,
|
||||
"../../../../lsp/nwscript-language-server/server/out/server.js",
|
||||
);
|
||||
}
|
||||
|
||||
export function startLspServer(): ChildProcess {
|
||||
if (lspProcess && !lspProcess.killed) {
|
||||
return lspProcess;
|
||||
}
|
||||
|
||||
const serverPath = getLspServerPath();
|
||||
lspProcess = spawn("node", [serverPath, "--stdio"], {
|
||||
env: { ...process.env },
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
lspProcess.stderr?.on("data", (data: Buffer) => {
|
||||
console.error(`[LSP stderr] ${data.toString()}`);
|
||||
});
|
||||
|
||||
lspProcess.on("exit", (code) => {
|
||||
console.log(`[LSP] Language server exited with code ${code}`);
|
||||
lspProcess = null;
|
||||
});
|
||||
|
||||
return lspProcess;
|
||||
}
|
||||
|
||||
export function attachLspWebSocket(ws: WebSocket): void {
|
||||
const proc = startLspServer();
|
||||
lspClient = ws;
|
||||
|
||||
ws.on("message", (data) => {
|
||||
const message = data.toString();
|
||||
const header = `Content-Length: ${Buffer.byteLength(message)}\r\n\r\n`;
|
||||
proc.stdin?.write(header + message);
|
||||
});
|
||||
|
||||
let buffer = "";
|
||||
proc.stdout?.on("data", (chunk: Buffer) => {
|
||||
buffer += chunk.toString();
|
||||
|
||||
while (true) {
|
||||
const headerEnd = buffer.indexOf("\r\n\r\n");
|
||||
if (headerEnd === -1) break;
|
||||
|
||||
const header = buffer.substring(0, headerEnd);
|
||||
const contentLengthMatch = header.match(/Content-Length:\s*(\d+)/i);
|
||||
if (!contentLengthMatch) {
|
||||
buffer = buffer.substring(headerEnd + 4);
|
||||
continue;
|
||||
}
|
||||
|
||||
const contentLength = parseInt(contentLengthMatch[1], 10);
|
||||
const bodyStart = headerEnd + 4;
|
||||
if (buffer.length < bodyStart + contentLength) break;
|
||||
|
||||
const body = buffer.substring(bodyStart, bodyStart + contentLength);
|
||||
buffer = buffer.substring(bodyStart + contentLength);
|
||||
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(body);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ws.on("close", () => {
|
||||
lspClient = null;
|
||||
});
|
||||
}
|
||||
|
||||
export function stopLspServer(): void {
|
||||
if (lspProcess) {
|
||||
lspProcess.kill();
|
||||
lspProcess = null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user