Skip to content

Commit 18e5c80

Browse files
committed
Update eslint server to 3.0.24
Use a proxy (like the markdown server one) to convert pull to push diagnostics.
1 parent 8ffd216 commit 18e5c80

5 files changed

Lines changed: 256 additions & 7 deletions

File tree

org.eclipse.wildwebdeveloper/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"vscode-css-languageserver": "file:target/vscode-css-languageserver-10.0.0.tgz",
1919
"vscode-html-languageserver": "file:target/vscode-html-languageserver-10.0.0.tgz",
2020
"vscode-json-languageserver": "file:target/vscode-json-languageserver-1.3.4.tgz",
21-
"eslint-server": "file:target/eslint-server-2.4.1.tgz"
21+
"eslint-server": "file:target/eslint-server-3.0.24.tgz"
2222
},
2323
"optionalDependencies": {
2424
"fsevents": "2.3.3"

org.eclipse.wildwebdeveloper/pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
<configuration>
4343
<connectionUrl>scm:git:https://github.com/microsoft/vscode-eslint.git</connectionUrl>
4444
<scmVersionType>tag</scmVersionType>
45-
<scmVersion>release/2.4.4</scmVersion>
45+
<scmVersion>release/3.0.24</scmVersion>
4646
<basedir>${project.build.directory}</basedir>
4747
<checkoutDirectory>${project.build.directory}/vscode-eslint-ls-package-json/extension</checkoutDirectory>
4848
</configuration>
@@ -72,7 +72,7 @@
7272
<goal>wget</goal>
7373
</goals>
7474
<configuration>
75-
<url>https://dbaeumer.gallery.vsassets.io/_apis/public/gallery/publisher/dbaeumer/extension/vscode-eslint/2.4.4/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage</url>
75+
<url>https://dbaeumer.gallery.vsassets.io/_apis/public/gallery/publisher/dbaeumer/extension/vscode-eslint/3.0.24/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage</url>
7676
<outputFileName>vscode-eslint-ls.zip</outputFileName>
7777
<unpack>true</unpack>
7878
<outputDirectory>${project.build.directory}/vscode-eslint-ls</outputDirectory>

org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/eslint/ESLintClientImpl.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,7 @@ public CompletableFuture<List<Object>> configuration(ConfigurationParams configu
6161
// `pre-release/2.3.0`: Disable using experimental Flat Config system
6262
config.put("experimental", Collections.emptyMap());
6363

64-
// `pre-release/2.3.0`: Add stub `problems` settings due to:
65-
// ESLint: Cannot read properties of undefined (reading \u0027shortenToSingleLine\u0027). Please see the \u0027ESLint\u0027 output channel for details.
66-
config.put("problems", Collections.emptyMap());
64+
config.put("problems", Collections.singletonMap("shortenToSingleLine", Boolean.FALSE));
6765

6866
config.put("workspaceFolder", Collections.singletonMap("uri", FileUtils.toUri(highestPackageJsonDir).toString()));
6967

@@ -100,6 +98,13 @@ private String getESLintPackageDir(File highestPackageJsonDir) {
10098
// fall back to the folder containing "node_modules"
10199
return highestPackageJsonDir.getAbsolutePath();
102100
}
101+
102+
@Override
103+
public CompletableFuture<Void> refreshDiagnostics() {
104+
// ESLint 3.x uses diagnostic pull model; the eslint-lsp-proxy.js handles
105+
// the pull-to-push conversion. Acknowledge the refresh request here.
106+
return CompletableFuture.completedFuture(null);
107+
}
103108

104109
@Override
105110
public CompletableFuture<Void> eslintStatus(Object o) {

org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/eslint/ESLintLanguageServer.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@ public ESLintLanguageServer() {
2626
commands.add(NodeJSManager.getNodeJsLocation().getAbsolutePath());
2727
//commands.add("--inspect-brk"); // for local debug
2828
try {
29+
URL proxyUrl = FileLocator.toFileURL(getClass().getResource("eslint-lsp-proxy.js"));
30+
commands.add(new java.io.File(proxyUrl.getPath()).getAbsolutePath());
2931
URL url = FileLocator.toFileURL(getClass().getResource("/node_modules/eslint-server/out/eslintServer.js"));
3032
commands.add(new java.io.File(url.getPath()).getAbsolutePath());
31-
// commands.add("/home/mistria/git/vscode-eslint/server/out/eslintServer.js"); // to use and debug against local sources
3233
commands.add("--stdio");
3334
setCommands(commands);
3435
setWorkingDirectory(System.getProperty("user.dir"));
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
#!/usr/bin/env node
2+
/*******************************************************************************
3+
* Copyright (c) 2026 Aleksandar Kurtakov and others.
4+
*
5+
* This program and the accompanying materials are made
6+
* available under the terms of the Eclipse Public License 2.0
7+
* which is available at https://www.eclipse.org/legal/epl-2.0/
8+
*
9+
* SPDX-License-Identifier: EPL-2.0
10+
*******************************************************************************/
11+
12+
// LSP stdio proxy that converts the ESLint 3.x diagnostic pull model
13+
// (textDocument/diagnostic + workspace/diagnostic/refresh) into the traditional
14+
// push model (textDocument/publishDiagnostics) that LSP4E expects.
15+
//
16+
// The proxy:
17+
// 1) Removes `diagnosticProvider` from the server's initialize response so
18+
// the client does not try to use pull diagnostics.
19+
// 2) After textDocument/didOpen and textDocument/didChange, sends a
20+
// textDocument/diagnostic request to the server and converts the result
21+
// into a textDocument/publishDiagnostics notification to the client.
22+
// 3) Intercepts workspace/diagnostic/refresh requests from the server,
23+
// responds with success, and re-pulls diagnostics for all tracked documents.
24+
25+
const { spawn } = require('child_process');
26+
27+
if (process.argv.length < 3) {
28+
process.stderr.write('Usage: eslint-lsp-proxy.js <serverMain.js> [args...]\n');
29+
process.exit(1);
30+
}
31+
32+
const serverMain = process.argv[2];
33+
const serverArgs = process.argv.slice(3);
34+
const child = spawn(process.execPath, [serverMain, ...serverArgs], {
35+
stdio: ['pipe', 'pipe', 'inherit']
36+
});
37+
38+
// Track open documents for re-pulling on refresh
39+
const openDocuments = new Map(); // uri -> version
40+
41+
// Request ID counter for proxy-initiated requests to the server
42+
let nextProxyRequestId = 900000;
43+
44+
// Map of proxy-initiated request IDs to document URIs
45+
const pendingPullRequests = new Map();
46+
47+
// --- Client → Server ---
48+
let inBuffer = Buffer.alloc(0);
49+
process.stdin.on('data', chunk => {
50+
inBuffer = Buffer.concat([inBuffer, chunk]);
51+
drainInbound();
52+
});
53+
54+
// --- Server → Client ---
55+
let outBuffer = Buffer.alloc(0);
56+
child.stdout.on('data', chunk => {
57+
outBuffer = Buffer.concat([outBuffer, chunk]);
58+
drainOutbound();
59+
});
60+
61+
child.on('exit', (code) => {
62+
try { process.stdout.end(); } catch (_e) { /* ignore */ }
63+
process.exitCode = code ?? 0;
64+
});
65+
66+
process.stdin.on('end', () => {
67+
try { child.stdin.end(); } catch (_e) { /* ignore */ }
68+
});
69+
70+
// ---- inbound (client → server) processing ----
71+
72+
function drainInbound() {
73+
for (;;) {
74+
const msg = readMessage(inBuffer);
75+
if (!msg) return;
76+
inBuffer = msg.rest;
77+
handleClientMessage(msg.body);
78+
}
79+
}
80+
81+
function handleClientMessage(bodyBuf) {
82+
let parsed;
83+
try {
84+
parsed = JSON.parse(bodyBuf.toString('utf8'));
85+
} catch (_e) {
86+
sendToServer(bodyBuf);
87+
return;
88+
}
89+
90+
const method = parsed.method;
91+
92+
if (method === 'textDocument/didOpen' && parsed.params?.textDocument) {
93+
const uri = parsed.params.textDocument.uri;
94+
openDocuments.set(uri, parsed.params.textDocument.version);
95+
sendToServer(bodyBuf);
96+
schedulePull(uri);
97+
return;
98+
}
99+
100+
if (method === 'textDocument/didChange' && parsed.params?.textDocument) {
101+
const uri = parsed.params.textDocument.uri;
102+
openDocuments.set(uri, parsed.params.textDocument.version);
103+
sendToServer(bodyBuf);
104+
schedulePull(uri);
105+
return;
106+
}
107+
108+
if (method === 'textDocument/didClose' && parsed.params?.textDocument) {
109+
openDocuments.delete(parsed.params.textDocument.uri);
110+
}
111+
112+
sendToServer(bodyBuf);
113+
}
114+
115+
// ---- outbound (server → client) processing ----
116+
117+
function drainOutbound() {
118+
for (;;) {
119+
const msg = readMessage(outBuffer);
120+
if (!msg) return;
121+
outBuffer = msg.rest;
122+
handleServerMessage(msg.body);
123+
}
124+
}
125+
126+
function handleServerMessage(bodyBuf) {
127+
let parsed;
128+
try {
129+
parsed = JSON.parse(bodyBuf.toString('utf8'));
130+
} catch (_e) {
131+
sendToClient(bodyBuf);
132+
return;
133+
}
134+
135+
// 1) Patch initialize response: remove diagnosticProvider
136+
if (parsed.id !== undefined && parsed.result?.capabilities?.diagnosticProvider) {
137+
delete parsed.result.capabilities.diagnosticProvider;
138+
sendToClient(Buffer.from(JSON.stringify(parsed), 'utf8'));
139+
return;
140+
}
141+
142+
// 2) Intercept workspace/diagnostic/refresh from server
143+
if (parsed.method === 'workspace/diagnostic/refresh') {
144+
// Respond with success
145+
sendToClient(Buffer.from(JSON.stringify({
146+
jsonrpc: '2.0',
147+
id: parsed.id,
148+
result: null
149+
}), 'utf8'));
150+
// Re-pull diagnostics for every open document
151+
for (const uri of openDocuments.keys()) {
152+
pullDiagnostics(uri);
153+
}
154+
return;
155+
}
156+
157+
// 3) Handle responses to our proxy-initiated textDocument/diagnostic requests
158+
if (parsed.id !== undefined && pendingPullRequests.has(parsed.id)) {
159+
const uri = pendingPullRequests.get(parsed.id);
160+
pendingPullRequests.delete(parsed.id);
161+
162+
if (parsed.result?.kind === 'full' && Array.isArray(parsed.result.items)) {
163+
sendToClient(Buffer.from(JSON.stringify({
164+
jsonrpc: '2.0',
165+
method: 'textDocument/publishDiagnostics',
166+
params: {
167+
uri: uri,
168+
diagnostics: parsed.result.items
169+
}
170+
}), 'utf8'));
171+
}
172+
return;
173+
}
174+
175+
// Everything else: forward unchanged
176+
sendToClient(bodyBuf);
177+
}
178+
179+
// ---- diagnostic pull helpers ----
180+
181+
const pullTimers = new Map();
182+
183+
function schedulePull(uri) {
184+
if (pullTimers.has(uri)) {
185+
clearTimeout(pullTimers.get(uri));
186+
}
187+
pullTimers.set(uri, setTimeout(() => {
188+
pullTimers.delete(uri);
189+
pullDiagnostics(uri);
190+
}, 200));
191+
}
192+
193+
function pullDiagnostics(uri) {
194+
const id = nextProxyRequestId++;
195+
pendingPullRequests.set(id, uri);
196+
sendToServer(Buffer.from(JSON.stringify({
197+
jsonrpc: '2.0',
198+
id: id,
199+
method: 'textDocument/diagnostic',
200+
params: { textDocument: { uri: uri } }
201+
}), 'utf8'));
202+
}
203+
204+
// ---- LSP message framing ----
205+
206+
function readMessage(buf) {
207+
const headerEnd = findDoubleNewline(buf);
208+
if (headerEnd === -1) return null;
209+
210+
const headers = buf.slice(0, headerEnd).toString('utf8');
211+
const contentLength = parseContentLength(headers);
212+
if (contentLength == null) return null;
213+
214+
const total = headerEnd + 4 + contentLength;
215+
if (buf.length < total) return null;
216+
217+
return {
218+
body: buf.slice(headerEnd + 4, total),
219+
rest: buf.slice(total)
220+
};
221+
}
222+
223+
function sendToServer(bodyBuf) {
224+
child.stdin.write(Buffer.from(`Content-Length: ${bodyBuf.length}\r\n\r\n`, 'utf8'));
225+
child.stdin.write(bodyBuf);
226+
}
227+
228+
function sendToClient(bodyBuf) {
229+
process.stdout.write(Buffer.from(`Content-Length: ${bodyBuf.length}\r\n\r\n`, 'utf8'));
230+
process.stdout.write(bodyBuf);
231+
}
232+
233+
function findDoubleNewline(buf) {
234+
for (let i = 0; i + 3 < buf.length; i++) {
235+
if (buf[i] === 13 && buf[i + 1] === 10 && buf[i + 2] === 13 && buf[i + 3] === 10) return i;
236+
}
237+
return -1;
238+
}
239+
240+
function parseContentLength(headers) {
241+
const match = /Content-Length:\s*(\d+)/i.exec(headers);
242+
return match ? parseInt(match[1], 10) : null;
243+
}

0 commit comments

Comments
 (0)