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 = / C o n t e n t - L e n g t h : \s * ( \d + ) / i. exec ( headers ) ;
242+ return match ? parseInt ( match [ 1 ] , 10 ) : null ;
243+ }
0 commit comments