250 lines
6.8 KiB
JavaScript
250 lines
6.8 KiB
JavaScript
// Simple JSON-RPC 2.0 LSP-like fake server over stdio
|
|
|
|
let nextId = 1
|
|
let readBuffer = Buffer.alloc(0)
|
|
let lastChange = null
|
|
let initializeParams = null
|
|
let diagnosticRequestCount = 0
|
|
let registeredCapability = false
|
|
const pendingClientRequests = new Map()
|
|
let pullConfig = {
|
|
delayMs: 0,
|
|
registerOn: undefined,
|
|
registrations: [],
|
|
documentDiagnostics: [],
|
|
documentDiagnosticsByIdentifier: {},
|
|
documentDelayMsByIdentifier: {},
|
|
workspaceDiagnostics: [],
|
|
workspaceDiagnosticsByIdentifier: {},
|
|
workspaceDelayMsByIdentifier: {},
|
|
}
|
|
|
|
function encode(message) {
|
|
const json = JSON.stringify(message)
|
|
const header = `Content-Length: ${Buffer.byteLength(json, "utf8")}\r\n\r\n`
|
|
return Buffer.concat([Buffer.from(header, "utf8"), Buffer.from(json, "utf8")])
|
|
}
|
|
|
|
function decodeFrames(buffer) {
|
|
const results = []
|
|
let idx
|
|
while ((idx = buffer.indexOf("\r\n\r\n")) !== -1) {
|
|
const header = buffer.slice(0, idx).toString("utf8")
|
|
const match = /Content-Length:\s*(\d+)/i.exec(header)
|
|
const length = match ? parseInt(match[1], 10) : 0
|
|
const bodyStart = idx + 4
|
|
const bodyEnd = bodyStart + length
|
|
if (buffer.length < bodyEnd) break
|
|
results.push(buffer.slice(bodyStart, bodyEnd).toString("utf8"))
|
|
buffer = buffer.slice(bodyEnd)
|
|
}
|
|
return { messages: results, rest: buffer }
|
|
}
|
|
|
|
function send(message) {
|
|
process.stdout.write(encode(message))
|
|
}
|
|
|
|
function sendRequest(method, params) {
|
|
const id = nextId++
|
|
send({ jsonrpc: "2.0", id, method, params })
|
|
return id
|
|
}
|
|
|
|
function sendResponse(id, result) {
|
|
send({ jsonrpc: "2.0", id, result })
|
|
}
|
|
|
|
function sendNotification(method, params) {
|
|
send({ jsonrpc: "2.0", method, params })
|
|
}
|
|
|
|
function maybeRegister(method) {
|
|
if (pullConfig.registerOn !== method || registeredCapability) return
|
|
registeredCapability = true
|
|
sendRequest("client/registerCapability", {
|
|
registrations: pullConfig.registrations.map((registration, index) => ({
|
|
id: registration.id ?? `pull-${index}`,
|
|
method: registration.method ?? "textDocument/diagnostic",
|
|
registerOptions: registration.registerOptions ?? registration,
|
|
})),
|
|
})
|
|
}
|
|
|
|
function delayed(id, result, delayMs = pullConfig.delayMs) {
|
|
if (!delayMs) {
|
|
sendResponse(id, result)
|
|
return
|
|
}
|
|
setTimeout(() => sendResponse(id, result), delayMs)
|
|
}
|
|
|
|
function diagnosticsForIdentifier(identifier) {
|
|
return pullConfig.documentDiagnosticsByIdentifier[identifier] ?? pullConfig.documentDiagnostics
|
|
}
|
|
|
|
function workspaceDiagnosticsForIdentifier(identifier) {
|
|
return pullConfig.workspaceDiagnosticsByIdentifier[identifier] ?? pullConfig.workspaceDiagnostics
|
|
}
|
|
|
|
function documentDelayForIdentifier(identifier) {
|
|
return pullConfig.documentDelayMsByIdentifier[identifier] ?? pullConfig.delayMs
|
|
}
|
|
|
|
function workspaceDelayForIdentifier(identifier) {
|
|
return pullConfig.workspaceDelayMsByIdentifier[identifier] ?? pullConfig.delayMs
|
|
}
|
|
|
|
function handle(raw) {
|
|
let data
|
|
try {
|
|
data = JSON.parse(raw)
|
|
} catch {
|
|
return
|
|
}
|
|
|
|
if (typeof data.method === "undefined" && typeof data.id !== "undefined") {
|
|
const pending = pendingClientRequests.get(data.id)
|
|
if (!pending) return
|
|
pendingClientRequests.delete(data.id)
|
|
sendResponse(pending, data.result ?? null)
|
|
return
|
|
}
|
|
|
|
if (data.method === "initialize") {
|
|
initializeParams = data.params
|
|
sendResponse(data.id, {
|
|
capabilities: {
|
|
textDocumentSync: {
|
|
change: 2,
|
|
},
|
|
},
|
|
})
|
|
return
|
|
}
|
|
|
|
if (data.method === "test/get-initialize-params") {
|
|
sendResponse(data.id, initializeParams)
|
|
return
|
|
}
|
|
|
|
if (data.method === "test/request-configuration") {
|
|
const id = sendRequest("workspace/configuration", data.params)
|
|
pendingClientRequests.set(id, data.id)
|
|
return
|
|
}
|
|
|
|
if (data.method === "initialized" || data.method === "workspace/didChangeConfiguration") {
|
|
return
|
|
}
|
|
|
|
if (data.method === "textDocument/didOpen") {
|
|
maybeRegister("didOpen")
|
|
return
|
|
}
|
|
|
|
if (data.method === "textDocument/didChange") {
|
|
lastChange = data.params
|
|
maybeRegister("didChange")
|
|
return
|
|
}
|
|
|
|
if (data.method === "test/trigger") {
|
|
const method = data.params && data.params.method
|
|
if (method === "client/registerCapability") {
|
|
sendRequest(method, {
|
|
registrations: [
|
|
{
|
|
id: "test-diagnostic-registration",
|
|
method: "textDocument/diagnostic",
|
|
registerOptions: { identifier: "syntax" },
|
|
},
|
|
],
|
|
})
|
|
return
|
|
}
|
|
if (method === "client/unregisterCapability") {
|
|
sendRequest(method, {
|
|
unregisterations: [{ id: "test-diagnostic-registration", method: "textDocument/diagnostic" }],
|
|
})
|
|
return
|
|
}
|
|
if (method) sendRequest(method, {})
|
|
return
|
|
}
|
|
|
|
if (data.method === "test/configure-pull-diagnostics") {
|
|
pullConfig = {
|
|
delayMs: data.params?.delayMs ?? 0,
|
|
registerOn: data.params?.registerOn,
|
|
registrations: data.params?.registrations ?? [],
|
|
documentDiagnostics: data.params?.documentDiagnostics ?? [],
|
|
documentDiagnosticsByIdentifier: data.params?.documentDiagnosticsByIdentifier ?? {},
|
|
documentDelayMsByIdentifier: data.params?.documentDelayMsByIdentifier ?? {},
|
|
workspaceDiagnostics: data.params?.workspaceDiagnostics ?? [],
|
|
workspaceDiagnosticsByIdentifier: data.params?.workspaceDiagnosticsByIdentifier ?? {},
|
|
workspaceDelayMsByIdentifier: data.params?.workspaceDelayMsByIdentifier ?? {},
|
|
}
|
|
registeredCapability = false
|
|
sendResponse(data.id, null)
|
|
return
|
|
}
|
|
|
|
if (data.method === "test/register-configured-pull-diagnostics") {
|
|
maybeRegister(undefined)
|
|
sendResponse(data.id, null)
|
|
return
|
|
}
|
|
|
|
if (data.method === "test/publish-diagnostics") {
|
|
sendNotification("textDocument/publishDiagnostics", data.params)
|
|
return
|
|
}
|
|
|
|
if (data.method === "test/get-last-change") {
|
|
sendResponse(data.id, lastChange)
|
|
return
|
|
}
|
|
|
|
if (data.method === "test/get-diagnostic-request-count") {
|
|
sendResponse(data.id, diagnosticRequestCount)
|
|
return
|
|
}
|
|
|
|
if (data.method === "textDocument/diagnostic") {
|
|
diagnosticRequestCount += 1
|
|
delayed(
|
|
data.id,
|
|
{
|
|
kind: "full",
|
|
items: diagnosticsForIdentifier(data.params?.identifier ?? ""),
|
|
},
|
|
documentDelayForIdentifier(data.params?.identifier ?? ""),
|
|
)
|
|
return
|
|
}
|
|
|
|
if (data.method === "workspace/diagnostic") {
|
|
diagnosticRequestCount += 1
|
|
delayed(
|
|
data.id,
|
|
{
|
|
items: workspaceDiagnosticsForIdentifier(data.params?.identifier ?? ""),
|
|
},
|
|
workspaceDelayForIdentifier(data.params?.identifier ?? ""),
|
|
)
|
|
return
|
|
}
|
|
|
|
if (typeof data.id !== "undefined") {
|
|
sendResponse(data.id, null)
|
|
}
|
|
}
|
|
|
|
process.stdin.on("data", (chunk) => {
|
|
readBuffer = Buffer.concat([readBuffer, chunk])
|
|
const { messages, rest } = decodeFrames(readBuffer)
|
|
readBuffer = rest
|
|
for (const message of messages) handle(message)
|
|
})
|