From 005368e111d4952db7d193a7d8867c50cb386bad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=8A?= =?UTF-8?q?=D1=80=20=D0=9A=D1=83=D1=80=D1=82=D0=B0=D0=BA=D0=BE=D0=B2?= Date: Thu, 26 Mar 2026 16:13:51 +0200 Subject: [PATCH] Update eslint server to 3.0.24 Use a proxy (like the markdown server one) to convert pull to push diagnostics. --- org.eclipse.wildwebdeveloper/package.json | 2 +- org.eclipse.wildwebdeveloper/pom.xml | 4 +- .../eslint/ESLintClientImpl.java | 11 +- .../eslint/ESLintLanguageServer.java | 3 +- .../eslint/eslint-lsp-proxy.js | 243 ++++++++++++++++++ 5 files changed, 256 insertions(+), 7 deletions(-) create mode 100644 org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/eslint/eslint-lsp-proxy.js diff --git a/org.eclipse.wildwebdeveloper/package.json b/org.eclipse.wildwebdeveloper/package.json index 30f0673e89..c7b3b2d005 100644 --- a/org.eclipse.wildwebdeveloper/package.json +++ b/org.eclipse.wildwebdeveloper/package.json @@ -18,7 +18,7 @@ "vscode-css-languageserver": "file:target/vscode-css-languageserver-10.0.0.tgz", "vscode-html-languageserver": "file:target/vscode-html-languageserver-10.0.0.tgz", "vscode-json-languageserver": "file:target/vscode-json-languageserver-1.3.4.tgz", - "eslint-server": "file:target/eslint-server-2.4.1.tgz" + "eslint-server": "file:target/eslint-server-3.0.24.tgz" }, "optionalDependencies": { "fsevents": "2.3.3" diff --git a/org.eclipse.wildwebdeveloper/pom.xml b/org.eclipse.wildwebdeveloper/pom.xml index cc9264a6f0..54e6f8d0e5 100644 --- a/org.eclipse.wildwebdeveloper/pom.xml +++ b/org.eclipse.wildwebdeveloper/pom.xml @@ -42,7 +42,7 @@ scm:git:https://github.com/microsoft/vscode-eslint.git tag - release/2.4.4 + release/3.0.24 ${project.build.directory} ${project.build.directory}/vscode-eslint-ls-package-json/extension @@ -72,7 +72,7 @@ wget - https://dbaeumer.gallery.vsassets.io/_apis/public/gallery/publisher/dbaeumer/extension/vscode-eslint/2.4.4/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage + https://dbaeumer.gallery.vsassets.io/_apis/public/gallery/publisher/dbaeumer/extension/vscode-eslint/3.0.24/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage vscode-eslint-ls.zip true ${project.build.directory}/vscode-eslint-ls diff --git a/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/eslint/ESLintClientImpl.java b/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/eslint/ESLintClientImpl.java index 73ab4ee40d..d5e6325d7b 100644 --- a/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/eslint/ESLintClientImpl.java +++ b/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/eslint/ESLintClientImpl.java @@ -61,9 +61,7 @@ public CompletableFuture> configuration(ConfigurationParams configu // `pre-release/2.3.0`: Disable using experimental Flat Config system config.put("experimental", Collections.emptyMap()); - // `pre-release/2.3.0`: Add stub `problems` settings due to: - // ESLint: Cannot read properties of undefined (reading \u0027shortenToSingleLine\u0027). Please see the \u0027ESLint\u0027 output channel for details. - config.put("problems", Collections.emptyMap()); + config.put("problems", Collections.singletonMap("shortenToSingleLine", Boolean.FALSE)); config.put("workspaceFolder", Collections.singletonMap("uri", FileUtils.toUri(highestPackageJsonDir).toString())); @@ -100,6 +98,13 @@ private String getESLintPackageDir(File highestPackageJsonDir) { // fall back to the folder containing "node_modules" return highestPackageJsonDir.getAbsolutePath(); } + + @Override + public CompletableFuture refreshDiagnostics() { + // ESLint 3.x uses diagnostic pull model; the eslint-lsp-proxy.js handles + // the pull-to-push conversion. Acknowledge the refresh request here. + return CompletableFuture.completedFuture(null); + } @Override public CompletableFuture eslintStatus(Object o) { diff --git a/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/eslint/ESLintLanguageServer.java b/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/eslint/ESLintLanguageServer.java index b2491535c0..985fdb5190 100644 --- a/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/eslint/ESLintLanguageServer.java +++ b/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/eslint/ESLintLanguageServer.java @@ -26,9 +26,10 @@ public ESLintLanguageServer() { commands.add(NodeJSManager.getNodeJsLocation().getAbsolutePath()); //commands.add("--inspect-brk"); // for local debug try { + URL proxyUrl = FileLocator.toFileURL(getClass().getResource("eslint-lsp-proxy.js")); + commands.add(new java.io.File(proxyUrl.getPath()).getAbsolutePath()); URL url = FileLocator.toFileURL(getClass().getResource("/node_modules/eslint-server/out/eslintServer.js")); commands.add(new java.io.File(url.getPath()).getAbsolutePath()); - // commands.add("/home/mistria/git/vscode-eslint/server/out/eslintServer.js"); // to use and debug against local sources commands.add("--stdio"); setCommands(commands); setWorkingDirectory(System.getProperty("user.dir")); diff --git a/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/eslint/eslint-lsp-proxy.js b/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/eslint/eslint-lsp-proxy.js new file mode 100644 index 0000000000..63ddccf099 --- /dev/null +++ b/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/eslint/eslint-lsp-proxy.js @@ -0,0 +1,243 @@ +#!/usr/bin/env node +/******************************************************************************* + * Copyright (c) 2026 Aleksandar Kurtakov and others. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ + +// LSP stdio proxy that converts the ESLint 3.x diagnostic pull model +// (textDocument/diagnostic + workspace/diagnostic/refresh) into the traditional +// push model (textDocument/publishDiagnostics) that LSP4E expects. +// +// The proxy: +// 1) Removes `diagnosticProvider` from the server's initialize response so +// the client does not try to use pull diagnostics. +// 2) After textDocument/didOpen and textDocument/didChange, sends a +// textDocument/diagnostic request to the server and converts the result +// into a textDocument/publishDiagnostics notification to the client. +// 3) Intercepts workspace/diagnostic/refresh requests from the server, +// responds with success, and re-pulls diagnostics for all tracked documents. + +const { spawn } = require('child_process'); + +if (process.argv.length < 3) { + process.stderr.write('Usage: eslint-lsp-proxy.js [args...]\n'); + process.exit(1); +} + +const serverMain = process.argv[2]; +const serverArgs = process.argv.slice(3); +const child = spawn(process.execPath, [serverMain, ...serverArgs], { + stdio: ['pipe', 'pipe', 'inherit'] +}); + +// Track open documents for re-pulling on refresh +const openDocuments = new Map(); // uri -> version + +// Request ID counter for proxy-initiated requests to the server +let nextProxyRequestId = 900000; + +// Map of proxy-initiated request IDs to document URIs +const pendingPullRequests = new Map(); + +// --- Client → Server --- +let inBuffer = Buffer.alloc(0); +process.stdin.on('data', chunk => { + inBuffer = Buffer.concat([inBuffer, chunk]); + drainInbound(); +}); + +// --- Server → Client --- +let outBuffer = Buffer.alloc(0); +child.stdout.on('data', chunk => { + outBuffer = Buffer.concat([outBuffer, chunk]); + drainOutbound(); +}); + +child.on('exit', (code) => { + try { process.stdout.end(); } catch (_e) { /* ignore */ } + process.exitCode = code ?? 0; +}); + +process.stdin.on('end', () => { + try { child.stdin.end(); } catch (_e) { /* ignore */ } +}); + +// ---- inbound (client → server) processing ---- + +function drainInbound() { + for (;;) { + const msg = readMessage(inBuffer); + if (!msg) return; + inBuffer = msg.rest; + handleClientMessage(msg.body); + } +} + +function handleClientMessage(bodyBuf) { + let parsed; + try { + parsed = JSON.parse(bodyBuf.toString('utf8')); + } catch (_e) { + sendToServer(bodyBuf); + return; + } + + const method = parsed.method; + + if (method === 'textDocument/didOpen' && parsed.params?.textDocument) { + const uri = parsed.params.textDocument.uri; + openDocuments.set(uri, parsed.params.textDocument.version); + sendToServer(bodyBuf); + schedulePull(uri); + return; + } + + if (method === 'textDocument/didChange' && parsed.params?.textDocument) { + const uri = parsed.params.textDocument.uri; + openDocuments.set(uri, parsed.params.textDocument.version); + sendToServer(bodyBuf); + schedulePull(uri); + return; + } + + if (method === 'textDocument/didClose' && parsed.params?.textDocument) { + openDocuments.delete(parsed.params.textDocument.uri); + } + + sendToServer(bodyBuf); +} + +// ---- outbound (server → client) processing ---- + +function drainOutbound() { + for (;;) { + const msg = readMessage(outBuffer); + if (!msg) return; + outBuffer = msg.rest; + handleServerMessage(msg.body); + } +} + +function handleServerMessage(bodyBuf) { + let parsed; + try { + parsed = JSON.parse(bodyBuf.toString('utf8')); + } catch (_e) { + sendToClient(bodyBuf); + return; + } + + // 1) Patch initialize response: remove diagnosticProvider + if (parsed.id !== undefined && parsed.result?.capabilities?.diagnosticProvider) { + delete parsed.result.capabilities.diagnosticProvider; + sendToClient(Buffer.from(JSON.stringify(parsed), 'utf8')); + return; + } + + // 2) Intercept workspace/diagnostic/refresh from server + if (parsed.method === 'workspace/diagnostic/refresh') { + // Respond with success + sendToClient(Buffer.from(JSON.stringify({ + jsonrpc: '2.0', + id: parsed.id, + result: null + }), 'utf8')); + // Re-pull diagnostics for every open document + for (const uri of openDocuments.keys()) { + pullDiagnostics(uri); + } + return; + } + + // 3) Handle responses to our proxy-initiated textDocument/diagnostic requests + if (parsed.id !== undefined && pendingPullRequests.has(parsed.id)) { + const uri = pendingPullRequests.get(parsed.id); + pendingPullRequests.delete(parsed.id); + + if (parsed.result?.kind === 'full' && Array.isArray(parsed.result.items)) { + sendToClient(Buffer.from(JSON.stringify({ + jsonrpc: '2.0', + method: 'textDocument/publishDiagnostics', + params: { + uri: uri, + diagnostics: parsed.result.items + } + }), 'utf8')); + } + return; + } + + // Everything else: forward unchanged + sendToClient(bodyBuf); +} + +// ---- diagnostic pull helpers ---- + +const pullTimers = new Map(); + +function schedulePull(uri) { + if (pullTimers.has(uri)) { + clearTimeout(pullTimers.get(uri)); + } + pullTimers.set(uri, setTimeout(() => { + pullTimers.delete(uri); + pullDiagnostics(uri); + }, 200)); +} + +function pullDiagnostics(uri) { + const id = nextProxyRequestId++; + pendingPullRequests.set(id, uri); + sendToServer(Buffer.from(JSON.stringify({ + jsonrpc: '2.0', + id: id, + method: 'textDocument/diagnostic', + params: { textDocument: { uri: uri } } + }), 'utf8')); +} + +// ---- LSP message framing ---- + +function readMessage(buf) { + const headerEnd = findDoubleNewline(buf); + if (headerEnd === -1) return null; + + const headers = buf.slice(0, headerEnd).toString('utf8'); + const contentLength = parseContentLength(headers); + if (contentLength == null) return null; + + const total = headerEnd + 4 + contentLength; + if (buf.length < total) return null; + + return { + body: buf.slice(headerEnd + 4, total), + rest: buf.slice(total) + }; +} + +function sendToServer(bodyBuf) { + child.stdin.write(Buffer.from(`Content-Length: ${bodyBuf.length}\r\n\r\n`, 'utf8')); + child.stdin.write(bodyBuf); +} + +function sendToClient(bodyBuf) { + process.stdout.write(Buffer.from(`Content-Length: ${bodyBuf.length}\r\n\r\n`, 'utf8')); + process.stdout.write(bodyBuf); +} + +function findDoubleNewline(buf) { + for (let i = 0; i + 3 < buf.length; i++) { + if (buf[i] === 13 && buf[i + 1] === 10 && buf[i + 2] === 13 && buf[i + 3] === 10) return i; + } + return -1; +} + +function parseContentLength(headers) { + const match = /Content-Length:\s*(\d+)/i.exec(headers); + return match ? parseInt(match[1], 10) : null; +} \ No newline at end of file