Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
293 changes: 263 additions & 30 deletions proxy/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use decompile::{rewrite_jdt_in_strings, rewrite_jdt_locations};
use http::handle_http;
use lsp::{parse_lsp_content, raw_has_id, write_raw, write_to_stdout, LspReader};
use platform::spawn_parent_monitor;
use serde_json::Value;
use serde_json::{json, Value};
use std::{
collections::HashMap,
env, fs,
Expand All @@ -26,9 +26,41 @@ use std::{
};

#[derive(Clone, Copy)]
enum TrackedKind {
enum RewriteKind {
Definition,
Doc,
Documentation,
}

#[derive(Clone)]
enum FallbackResult {
Null,
EmptyArray,
CompletionItem(Value),
}

#[derive(Clone)]
struct TrackedRequest {
method: String,
rewrite: Option<RewriteKind>,
fallback: FallbackResult,
}

impl TrackedRequest {
fn new(method: &str, rewrite: Option<RewriteKind>, fallback: FallbackResult) -> Self {
Self {
method: method.to_string(),
rewrite,
fallback,
}
}

fn fallback_result(&self) -> Value {
match &self.fallback {
FallbackResult::Null => Value::Null,
FallbackResult::EmptyArray => json!([]),
FallbackResult::CompletionItem(item) => item.clone(),
}
}
}

fn main() {
Expand Down Expand Up @@ -96,9 +128,10 @@ fn main() {

let id_counter = Arc::new(AtomicU64::new(1));

// Track definition/typeDefinition/implementation and documentation request IDs
// so their responses can be intercepted and rewritten.
let tracked_ids: Arc<Mutex<HashMap<Value, TrackedKind>>> = Arc::new(Mutex::new(HashMap::new()));
// Track requests whose responses need rewriting or a soft fallback when
// JDTLS returns a JSON-RPC internal error.
let tracked_ids: Arc<Mutex<HashMap<Value, TrackedRequest>>> =
Arc::new(Mutex::new(HashMap::new()));

// --- Thread 1: Zed stdin -> JDTLS stdin (track definition requests) ---
let stdin_writer = Arc::clone(&child_stdin);
Expand All @@ -115,23 +148,8 @@ fn main() {
// textDocument/didChange.
if raw_has_id(&raw) {
if let Some(msg) = parse_lsp_content(&raw) {
if let Some(method) = msg.get("method").and_then(|m| m.as_str()) {
let kind = match method {
"textDocument/definition"
| "textDocument/typeDefinition"
| "textDocument/implementation" => {
Some(TrackedKind::Definition)
}
"textDocument/hover"
| "textDocument/signatureHelp"
| "completionItem/resolve" => Some(TrackedKind::Doc),
_ => None,
};
if let Some(kind) = kind {
if let Some(id) = msg.get("id").cloned() {
tracked_in.lock().unwrap().insert(id, kind);
}
}
if let Some((id, request)) = tracked_request_for(&msg) {
tracked_in.lock().unwrap().insert(id, request);
}
}
}
Expand Down Expand Up @@ -179,11 +197,29 @@ fn main() {
}
}

// Rewrite jdt:// URIs in definition or documentation responses.
// Spawns a thread so this loop stays unblocked and can route
// the java/classFileContents response back via `pending`.
// Rewrite jdt:// URIs, or turn known JDTLS internal errors
// into harmless fallback results so one bad request doesn't
// break Java editing until the language server is restarted.
if let Some(id) = msg.get("id").cloned() {
if let Some(kind) = tracked_out.lock().unwrap().remove(&id) {
if let Some(request) = tracked_out.lock().unwrap().remove(&id) {
if is_jdtls_internal_error(&msg) {
lsp_warn!(
"JDTLS internal error for {}; returning fallback result",
request.method
);
write_to_stdout(&fallback_response(id, &request));
continue;
}

let Some(rewrite) = request.rewrite else {
write_raw(&mut io::stdout().lock(), &raw);
continue;
};
let sanitize_signature_help =
request.method == "textDocument/signatureHelp";

// Spawns a thread so this loop stays unblocked and can route
// the java/classFileContents response back via `pending`.
let writer = Arc::clone(&decompile_writer);
let pending = Arc::clone(&decompile_pending);
let pid = decompile_proxy_id.clone();
Expand All @@ -193,16 +229,16 @@ fn main() {
let seq = counter.fetch_add(1, Ordering::Relaxed);
Value::String(format!("{pid}-decompile-{seq}"))
};
match kind {
TrackedKind::Definition => {
match rewrite {
RewriteKind::Definition => {
rewrite_jdt_locations(
&mut msg,
&writer,
&pending,
&mut next_id,
);
}
TrackedKind::Doc => {
RewriteKind::Documentation => {
rewrite_jdt_in_strings(
&mut msg,
&writer,
Expand All @@ -212,6 +248,9 @@ fn main() {
sanitize_resolved_completion(&mut msg);
}
}
if sanitize_signature_help {
sanitize_signature_help_response(&mut msg);
}
write_to_stdout(&msg);
});
continue;
Expand Down Expand Up @@ -272,3 +311,197 @@ fn main() {
fn hex_encode(s: &str) -> String {
s.as_bytes().iter().map(|b| format!("{b:02x}")).collect()
}

fn tracked_request_for(msg: &Value) -> Option<(Value, TrackedRequest)> {
let method = msg.get("method")?.as_str()?;
let id = msg.get("id")?.clone();
let request = match method {
"textDocument/definition"
| "textDocument/typeDefinition"
| "textDocument/implementation" => {
TrackedRequest::new(method, Some(RewriteKind::Definition), FallbackResult::Null)
}
"textDocument/hover" | "textDocument/signatureHelp" => TrackedRequest::new(
method,
Some(RewriteKind::Documentation),
FallbackResult::Null,
),
"completionItem/resolve" => TrackedRequest::new(
method,
Some(RewriteKind::Documentation),
FallbackResult::CompletionItem(msg.get("params").cloned().unwrap_or(Value::Null)),
),
"textDocument/codeAction" | "textDocument/codeLens" | "textDocument/documentHighlight" => {
TrackedRequest::new(method, None, FallbackResult::EmptyArray)
}
_ => return None,
};

Some((id, request))
}

fn is_jdtls_internal_error(msg: &Value) -> bool {
let Some(error) = msg.get("error") else {
return false;
};

let has_internal_error_code = error
.get("code")
.and_then(|code| code.as_i64())
.is_some_and(|code| code == -32603);
let has_internal_error_message = error
.get("message")
.and_then(|message| message.as_str())
.is_some_and(|message| message.to_ascii_lowercase().contains("internal error"));

has_internal_error_code || has_internal_error_message
}

fn fallback_response(id: Value, request: &TrackedRequest) -> Value {
json!({
"jsonrpc": "2.0",
"id": id,
"result": request.fallback_result(),
})
}

fn sanitize_signature_help_response(msg: &mut Value) {
let Some(result) = msg
.get_mut("result")
.and_then(|result| result.as_object_mut())
else {
return;
};

let has_negative_active_parameter = result
.get("activeParameter")
.and_then(|active_parameter| active_parameter.as_i64())
.is_some_and(|active_parameter| active_parameter < 0);

if has_negative_active_parameter {
result.remove("activeParameter");
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn detects_jdtls_internal_error_by_code() {
let msg = json!({
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32603,
"message": "Request failed"
}
});

assert!(is_jdtls_internal_error(&msg));
}

#[test]
fn detects_jdtls_internal_error_by_message() {
let msg = json!({
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": 0,
"message": "Internal error."
}
});

assert!(is_jdtls_internal_error(&msg));
}

#[test]
fn ignores_non_internal_errors() {
let msg = json!({
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32800,
"message": "Request cancelled"
}
});

assert!(!is_jdtls_internal_error(&msg));
}

#[test]
fn builds_empty_array_fallback_response() {
let request =
TrackedRequest::new("textDocument/codeAction", None, FallbackResult::EmptyArray);

assert_eq!(
fallback_response(json!(7), &request),
json!({
"jsonrpc": "2.0",
"id": 7,
"result": []
})
);
}

#[test]
fn completion_resolve_fallback_returns_original_item() {
let request = json!({
"jsonrpc": "2.0",
"id": 2,
"method": "completionItem/resolve",
"params": {
"label": "String",
"kind": 7
}
});

let (id, tracked) = tracked_request_for(&request).unwrap();

assert_eq!(
fallback_response(id, &tracked),
json!({
"jsonrpc": "2.0",
"id": 2,
"result": {
"label": "String",
"kind": 7
}
})
);
}

#[test]
fn removes_negative_signature_help_active_parameter() {
let mut msg = json!({
"jsonrpc": "2.0",
"id": 3,
"result": {
"signatures": [],
"activeSignature": 0,
"activeParameter": -1
}
});

sanitize_signature_help_response(&mut msg);

assert!(msg["result"].get("activeParameter").is_none());
}

#[test]
fn preserves_valid_signature_help_active_parameter() {
let mut msg = json!({
"jsonrpc": "2.0",
"id": 3,
"result": {
"signatures": [],
"activeSignature": 0,
"activeParameter": 1
}
});

sanitize_signature_help_response(&mut msg);

assert_eq!(msg["result"]["activeParameter"], json!(1));
}
}