@@ -10,7 +10,7 @@ use decompile::{rewrite_jdt_in_strings, rewrite_jdt_locations};
1010use http:: handle_http;
1111use lsp:: { parse_lsp_content, raw_has_id, write_raw, write_to_stdout, LspReader } ;
1212use platform:: spawn_parent_monitor;
13- use serde_json:: Value ;
13+ use serde_json:: { json , Value } ;
1414use std:: {
1515 collections:: HashMap ,
1616 env, fs,
@@ -26,9 +26,41 @@ use std::{
2626} ;
2727
2828#[ derive( Clone , Copy ) ]
29- enum TrackedKind {
29+ enum RewriteKind {
3030 Definition ,
31- Doc ,
31+ Documentation ,
32+ }
33+
34+ #[ derive( Clone ) ]
35+ enum FallbackResult {
36+ Null ,
37+ EmptyArray ,
38+ CompletionItem ( Value ) ,
39+ }
40+
41+ #[ derive( Clone ) ]
42+ struct TrackedRequest {
43+ method : String ,
44+ rewrite : Option < RewriteKind > ,
45+ fallback : FallbackResult ,
46+ }
47+
48+ impl TrackedRequest {
49+ fn new ( method : & str , rewrite : Option < RewriteKind > , fallback : FallbackResult ) -> Self {
50+ Self {
51+ method : method. to_string ( ) ,
52+ rewrite,
53+ fallback,
54+ }
55+ }
56+
57+ fn fallback_result ( & self ) -> Value {
58+ match & self . fallback {
59+ FallbackResult :: Null => Value :: Null ,
60+ FallbackResult :: EmptyArray => json ! ( [ ] ) ,
61+ FallbackResult :: CompletionItem ( item) => item. clone ( ) ,
62+ }
63+ }
3264}
3365
3466fn main ( ) {
@@ -96,9 +128,10 @@ fn main() {
96128
97129 let id_counter = Arc :: new ( AtomicU64 :: new ( 1 ) ) ;
98130
99- // Track definition/typeDefinition/implementation and documentation request IDs
100- // so their responses can be intercepted and rewritten.
101- let tracked_ids: Arc < Mutex < HashMap < Value , TrackedKind > > > = Arc :: new ( Mutex :: new ( HashMap :: new ( ) ) ) ;
131+ // Track requests whose responses need rewriting or a soft fallback when
132+ // JDTLS returns a JSON-RPC internal error.
133+ let tracked_ids: Arc < Mutex < HashMap < Value , TrackedRequest > > > =
134+ Arc :: new ( Mutex :: new ( HashMap :: new ( ) ) ) ;
102135
103136 // --- Thread 1: Zed stdin -> JDTLS stdin (track definition requests) ---
104137 let stdin_writer = Arc :: clone ( & child_stdin) ;
@@ -115,23 +148,8 @@ fn main() {
115148 // textDocument/didChange.
116149 if raw_has_id ( & raw ) {
117150 if let Some ( msg) = parse_lsp_content ( & raw ) {
118- if let Some ( method) = msg. get ( "method" ) . and_then ( |m| m. as_str ( ) ) {
119- let kind = match method {
120- "textDocument/definition"
121- | "textDocument/typeDefinition"
122- | "textDocument/implementation" => {
123- Some ( TrackedKind :: Definition )
124- }
125- "textDocument/hover"
126- | "textDocument/signatureHelp"
127- | "completionItem/resolve" => Some ( TrackedKind :: Doc ) ,
128- _ => None ,
129- } ;
130- if let Some ( kind) = kind {
131- if let Some ( id) = msg. get ( "id" ) . cloned ( ) {
132- tracked_in. lock ( ) . unwrap ( ) . insert ( id, kind) ;
133- }
134- }
151+ if let Some ( ( id, request) ) = tracked_request_for ( & msg) {
152+ tracked_in. lock ( ) . unwrap ( ) . insert ( id, request) ;
135153 }
136154 }
137155 }
@@ -179,11 +197,29 @@ fn main() {
179197 }
180198 }
181199
182- // Rewrite jdt:// URIs in definition or documentation responses.
183- // Spawns a thread so this loop stays unblocked and can route
184- // the java/classFileContents response back via `pending` .
200+ // Rewrite jdt:// URIs, or turn known JDTLS internal errors
201+ // into harmless fallback results so one bad request doesn't
202+ // break Java editing until the language server is restarted .
185203 if let Some ( id) = msg. get ( "id" ) . cloned ( ) {
186- if let Some ( kind) = tracked_out. lock ( ) . unwrap ( ) . remove ( & id) {
204+ if let Some ( request) = tracked_out. lock ( ) . unwrap ( ) . remove ( & id) {
205+ if is_jdtls_internal_error ( & msg) {
206+ lsp_warn ! (
207+ "JDTLS internal error for {}; returning fallback result" ,
208+ request. method
209+ ) ;
210+ write_to_stdout ( & fallback_response ( id, & request) ) ;
211+ continue ;
212+ }
213+
214+ let Some ( rewrite) = request. rewrite else {
215+ write_raw ( & mut io:: stdout ( ) . lock ( ) , & raw ) ;
216+ continue ;
217+ } ;
218+ let sanitize_signature_help =
219+ request. method == "textDocument/signatureHelp" ;
220+
221+ // Spawns a thread so this loop stays unblocked and can route
222+ // the java/classFileContents response back via `pending`.
187223 let writer = Arc :: clone ( & decompile_writer) ;
188224 let pending = Arc :: clone ( & decompile_pending) ;
189225 let pid = decompile_proxy_id. clone ( ) ;
@@ -193,16 +229,16 @@ fn main() {
193229 let seq = counter. fetch_add ( 1 , Ordering :: Relaxed ) ;
194230 Value :: String ( format ! ( "{pid}-decompile-{seq}" ) )
195231 } ;
196- match kind {
197- TrackedKind :: Definition => {
232+ match rewrite {
233+ RewriteKind :: Definition => {
198234 rewrite_jdt_locations (
199235 & mut msg,
200236 & writer,
201237 & pending,
202238 & mut next_id,
203239 ) ;
204240 }
205- TrackedKind :: Doc => {
241+ RewriteKind :: Documentation => {
206242 rewrite_jdt_in_strings (
207243 & mut msg,
208244 & writer,
@@ -212,6 +248,9 @@ fn main() {
212248 sanitize_resolved_completion ( & mut msg) ;
213249 }
214250 }
251+ if sanitize_signature_help {
252+ sanitize_signature_help_response ( & mut msg) ;
253+ }
215254 write_to_stdout ( & msg) ;
216255 } ) ;
217256 continue ;
@@ -272,3 +311,197 @@ fn main() {
272311fn hex_encode ( s : & str ) -> String {
273312 s. as_bytes ( ) . iter ( ) . map ( |b| format ! ( "{b:02x}" ) ) . collect ( )
274313}
314+
315+ fn tracked_request_for ( msg : & Value ) -> Option < ( Value , TrackedRequest ) > {
316+ let method = msg. get ( "method" ) ?. as_str ( ) ?;
317+ let id = msg. get ( "id" ) ?. clone ( ) ;
318+ let request = match method {
319+ "textDocument/definition"
320+ | "textDocument/typeDefinition"
321+ | "textDocument/implementation" => {
322+ TrackedRequest :: new ( method, Some ( RewriteKind :: Definition ) , FallbackResult :: Null )
323+ }
324+ "textDocument/hover" | "textDocument/signatureHelp" => TrackedRequest :: new (
325+ method,
326+ Some ( RewriteKind :: Documentation ) ,
327+ FallbackResult :: Null ,
328+ ) ,
329+ "completionItem/resolve" => TrackedRequest :: new (
330+ method,
331+ Some ( RewriteKind :: Documentation ) ,
332+ FallbackResult :: CompletionItem ( msg. get ( "params" ) . cloned ( ) . unwrap_or ( Value :: Null ) ) ,
333+ ) ,
334+ "textDocument/codeAction" | "textDocument/codeLens" | "textDocument/documentHighlight" => {
335+ TrackedRequest :: new ( method, None , FallbackResult :: EmptyArray )
336+ }
337+ _ => return None ,
338+ } ;
339+
340+ Some ( ( id, request) )
341+ }
342+
343+ fn is_jdtls_internal_error ( msg : & Value ) -> bool {
344+ let Some ( error) = msg. get ( "error" ) else {
345+ return false ;
346+ } ;
347+
348+ let has_internal_error_code = error
349+ . get ( "code" )
350+ . and_then ( |code| code. as_i64 ( ) )
351+ . is_some_and ( |code| code == -32603 ) ;
352+ let has_internal_error_message = error
353+ . get ( "message" )
354+ . and_then ( |message| message. as_str ( ) )
355+ . is_some_and ( |message| message. to_ascii_lowercase ( ) . contains ( "internal error" ) ) ;
356+
357+ has_internal_error_code || has_internal_error_message
358+ }
359+
360+ fn fallback_response ( id : Value , request : & TrackedRequest ) -> Value {
361+ json ! ( {
362+ "jsonrpc" : "2.0" ,
363+ "id" : id,
364+ "result" : request. fallback_result( ) ,
365+ } )
366+ }
367+
368+ fn sanitize_signature_help_response ( msg : & mut Value ) {
369+ let Some ( result) = msg
370+ . get_mut ( "result" )
371+ . and_then ( |result| result. as_object_mut ( ) )
372+ else {
373+ return ;
374+ } ;
375+
376+ let has_negative_active_parameter = result
377+ . get ( "activeParameter" )
378+ . and_then ( |active_parameter| active_parameter. as_i64 ( ) )
379+ . is_some_and ( |active_parameter| active_parameter < 0 ) ;
380+
381+ if has_negative_active_parameter {
382+ result. remove ( "activeParameter" ) ;
383+ }
384+ }
385+
386+ #[ cfg( test) ]
387+ mod tests {
388+ use super :: * ;
389+
390+ #[ test]
391+ fn detects_jdtls_internal_error_by_code ( ) {
392+ let msg = json ! ( {
393+ "jsonrpc" : "2.0" ,
394+ "id" : 1 ,
395+ "error" : {
396+ "code" : -32603 ,
397+ "message" : "Request failed"
398+ }
399+ } ) ;
400+
401+ assert ! ( is_jdtls_internal_error( & msg) ) ;
402+ }
403+
404+ #[ test]
405+ fn detects_jdtls_internal_error_by_message ( ) {
406+ let msg = json ! ( {
407+ "jsonrpc" : "2.0" ,
408+ "id" : 1 ,
409+ "error" : {
410+ "code" : 0 ,
411+ "message" : "Internal error."
412+ }
413+ } ) ;
414+
415+ assert ! ( is_jdtls_internal_error( & msg) ) ;
416+ }
417+
418+ #[ test]
419+ fn ignores_non_internal_errors ( ) {
420+ let msg = json ! ( {
421+ "jsonrpc" : "2.0" ,
422+ "id" : 1 ,
423+ "error" : {
424+ "code" : -32800 ,
425+ "message" : "Request cancelled"
426+ }
427+ } ) ;
428+
429+ assert ! ( !is_jdtls_internal_error( & msg) ) ;
430+ }
431+
432+ #[ test]
433+ fn builds_empty_array_fallback_response ( ) {
434+ let request =
435+ TrackedRequest :: new ( "textDocument/codeAction" , None , FallbackResult :: EmptyArray ) ;
436+
437+ assert_eq ! (
438+ fallback_response( json!( 7 ) , & request) ,
439+ json!( {
440+ "jsonrpc" : "2.0" ,
441+ "id" : 7 ,
442+ "result" : [ ]
443+ } )
444+ ) ;
445+ }
446+
447+ #[ test]
448+ fn completion_resolve_fallback_returns_original_item ( ) {
449+ let request = json ! ( {
450+ "jsonrpc" : "2.0" ,
451+ "id" : 2 ,
452+ "method" : "completionItem/resolve" ,
453+ "params" : {
454+ "label" : "String" ,
455+ "kind" : 7
456+ }
457+ } ) ;
458+
459+ let ( id, tracked) = tracked_request_for ( & request) . unwrap ( ) ;
460+
461+ assert_eq ! (
462+ fallback_response( id, & tracked) ,
463+ json!( {
464+ "jsonrpc" : "2.0" ,
465+ "id" : 2 ,
466+ "result" : {
467+ "label" : "String" ,
468+ "kind" : 7
469+ }
470+ } )
471+ ) ;
472+ }
473+
474+ #[ test]
475+ fn removes_negative_signature_help_active_parameter ( ) {
476+ let mut msg = json ! ( {
477+ "jsonrpc" : "2.0" ,
478+ "id" : 3 ,
479+ "result" : {
480+ "signatures" : [ ] ,
481+ "activeSignature" : 0 ,
482+ "activeParameter" : -1
483+ }
484+ } ) ;
485+
486+ sanitize_signature_help_response ( & mut msg) ;
487+
488+ assert ! ( msg[ "result" ] . get( "activeParameter" ) . is_none( ) ) ;
489+ }
490+
491+ #[ test]
492+ fn preserves_valid_signature_help_active_parameter ( ) {
493+ let mut msg = json ! ( {
494+ "jsonrpc" : "2.0" ,
495+ "id" : 3 ,
496+ "result" : {
497+ "signatures" : [ ] ,
498+ "activeSignature" : 0 ,
499+ "activeParameter" : 1
500+ }
501+ } ) ;
502+
503+ sanitize_signature_help_response ( & mut msg) ;
504+
505+ assert_eq ! ( msg[ "result" ] [ "activeParameter" ] , json!( 1 ) ) ;
506+ }
507+ }
0 commit comments