#!/usr/bin/env node import fs from "node:fs"; import os from "node:os"; import path from "node:path"; const logPath = process.env.KANDEV_LSP_E2E_LOG ?? path.join(os.homedir(), "lsp-e2e-events.jsonl"); const crashOnOpenPath = path.join(os.homedir(), "lsp-e2e-crash-on-open"); const initializeModePath = path.join(os.homedir(), "lsp-e2e-initialize-mode.json"); const initializeReleasePath = path.join(os.homedir(), "lsp-e2e-initialize-release"); let input = Buffer.alloc(0); let nextServerRequestId = 10_000; const openDocumentUris = new Set(); const definitionTargetFile = "nested/references/Definition Target # query? 100%.kt"; function log(event, details = {}) { fs.appendFileSync( logPath, `${JSON.stringify({ event, pid: process.pid, timestamp: Date.now(), ...details })}\n`, ); } function send(message) { const payload = Buffer.from(JSON.stringify({ jsonrpc: "2.0", ...message }), "utf8"); process.stdout.write(`Content-Length: ${payload.length}\r\n\r\n`); process.stdout.write(payload); } function diagnostic(uri, message, version) { send({ method: "textDocument/publishDiagnostics", params: { uri, ...(Number.isSafeInteger(version) ? { version } : {}), diagnostics: [ { range: { start: { line: 0, character: 0 }, end: { line: 0, character: 8 }, }, severity: 2, source: "kandev-e2e", code: "FAKE_KOTLIN", message, }, ], }, }); } function location(uri, line = 2) { return { uri, range: { start: { line, character: 4 }, end: { line, character: 13 }, }, }; } function siblingFileUri(uri, fileName) { const directoryEnd = uri.lastIndexOf("/") + 1; const encodedPath = fileName .split("/") .map((segment) => encodeURIComponent(segment)) .join("/"); return `${uri.slice(0, directoryEnd)}${encodedPath}`; } function requestDocumentUri(message) { const uri = message.params?.textDocument?.uri; if (uri) return uri; if (openDocumentUris.size !== 1) return null; return openDocumentUris.values().next().value ?? null; } function initializeResult(id) { return { id, result: { capabilities: { textDocumentSync: { openClose: true, change: 2, save: { includeText: true } }, completionProvider: { triggerCharacters: ["."] }, hoverProvider: true, definitionProvider: true, referencesProvider: true, signatureHelpProvider: { triggerCharacters: ["(", ","] }, semanticTokensProvider: { legend: { tokenTypes: ["function", "variable"], tokenModifiers: [] }, full: true, }, }, }, }; } function progressToken(message) { const token = message.params?.workDoneToken; return typeof token === "string" || typeof token === "number" ? token : null; } function sendInitializeProgress(message, progress) { const token = progressToken(message); if (token === null || !progress) return token; send({ method: "$/progress", params: { token, value: { kind: "begin", title: progress.title, percentage: progress.beginPercentage, }, }, }); send({ method: "$/progress", params: { token, value: { kind: "report", message: progress.message, percentage: progress.percentage, }, }, }); log("initialize progress reported", { token, progress }); return token; } function handleInitialize(message) { if (!fs.existsSync(initializeModePath)) { send(initializeResult(message.id)); return; } const mode = JSON.parse(fs.readFileSync(initializeModePath, "utf8")); const token = sendInitializeProgress(message, mode.progress); if (mode.keepProgress) { send(initializeResult(message.id)); log("initialize completed with active progress", { id: message.id, token }); return; } log("initialize held", { id: message.id, token }); const finish = () => { if (!fs.existsSync(initializeReleasePath)) return false; if (token !== null && mode.progress) { send({ method: "$/progress", params: { token, value: { kind: "end", message: mode.progress.endMessage }, }, }); } send(initializeResult(message.id)); log("initialize released", { id: message.id, token }); return true; }; if (finish()) return; const timer = setInterval(() => { if (finish()) clearInterval(timer); }, 25); } function handleRequest(message) { const uri = requestDocumentUri(message); switch (message.method) { case "initialize": handleInitialize(message); return; case "textDocument/completion": send({ id: message.id, result: { isIncomplete: true, items: [ { label: "fakeGreeting", kind: 3, detail: "Kandev E2E completion", insertText: "fakeGreeting", }, ], }, }); return; case "textDocument/hover": send({ id: message.id, result: { contents: { kind: "markdown", value: "**Fake Kotlin hover**" }, range: location(uri).range, }, }); return; case "textDocument/definition": send({ id: message.id, result: uri ? location(siblingFileUri(uri, definitionTargetFile)) : null, }); return; case "textDocument/references": send({ id: message.id, result: uri ? [location(uri), location(uri, 3)] : [] }); return; case "textDocument/signatureHelp": send({ id: message.id, result: { signatures: [ { label: "greeting(name: String): String", documentation: "Kandev E2E signature", parameters: [{ label: "name: String" }], }, ], activeSignature: 0, activeParameter: 0, }, }); return; case "textDocument/semanticTokens/full": send({ id: message.id, result: { resultId: "e2e-1", data: [2, 4, 8, 0, 0] } }); return; case "shutdown": send({ id: message.id, result: null }); return; default: send({ id: message.id, result: null }); } } function handleDidOpen(message) { const uri = message.params?.textDocument?.uri; if (uri) { openDocumentUris.add(uri); diagnostic(uri, "Fake Kotlin diagnostic", message.params?.textDocument?.version); } if (fs.existsSync(crashOnOpenPath)) { log("crashing", { reason: "didOpen" }); process.exit(23); } } function handleDidChange(message) { const uri = message.params?.textDocument?.uri; if (uri && openDocumentUris.has(uri)) { diagnostic(uri, "Fake Kotlin diagnostic after edit", message.params?.textDocument?.version); } } function handleDidClose(message) { const uri = message.params?.textDocument?.uri; if (!uri) return; send({ method: "textDocument/publishDiagnostics", params: { uri, diagnostics: [] }, }); openDocumentUris.delete(uri); } function handleNotification(message) { switch (message.method) { case "initialized": { const id = nextServerRequestId++; log("workspace/configuration requested", { id }); send({ id, method: "workspace/configuration", params: { items: [{ section: "kotlin" }] }, }); return; } case "textDocument/didOpen": handleDidOpen(message); return; case "textDocument/didChange": handleDidChange(message); return; case "textDocument/didClose": handleDidClose(message); return; case "exit": log("exit"); process.exit(0); } } function handleMessage(message) { if (message.method) { log("message", { id: message.id, method: message.method, params: message.params }); if (message.id !== undefined) handleRequest(message); else handleNotification(message); return; } log("response", { id: message.id, result: message.result, error: message.error }); } function parseInput() { while (input.length > 0) { const headerEnd = input.indexOf("\r\n\r\n"); if (headerEnd < 0) return; const header = input.subarray(0, headerEnd).toString("ascii"); const lengthMatch = /^Content-Length:\s*(\d+)$/im.exec(header); if (!lengthMatch) { log("protocol error", { header }); process.exit(2); } const contentLength = Number(lengthMatch[1]); const payloadStart = headerEnd + 4; const payloadEnd = payloadStart + contentLength; if (input.length < payloadEnd) return; const payload = input.subarray(payloadStart, payloadEnd).toString("utf8"); input = input.subarray(payloadEnd); handleMessage(JSON.parse(payload)); } } process.stdin.on("data", (chunk) => { input = Buffer.concat([input, chunk]); parseInput(); }); process.stdin.on("end", () => { log("stdin ended"); }); process.on("SIGTERM", () => { log("signal", { signal: "SIGTERM" }); process.exit(0); }); process.on("SIGINT", () => { log("signal", { signal: "SIGINT" }); process.exit(0); }); process.on("uncaughtException", (error) => { log("uncaught exception", { error: String(error), stack: error.stack }); process.exit(1); }); log("started", { argv: process.argv.slice(2), cwd: process.cwd(), home: os.homedir() });