1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
//! Request/response and document management for the LSP client.
//!
//! Contains methods for sending JSON-RPC requests and notifications,
//! low-level message I/O, and document lifecycle operations
//! (open, close, diagnostics).
use serde_json::Value;
use std::{
io::{BufRead, ErrorKind, Read, Write},
sync::atomic::Ordering,
thread,
time::Duration,
};
use super::{LspDiagnostic, TsgoLspClient};
use vize_carton::cstr;
use vize_carton::FxHashMap;
use vize_carton::String;
impl TsgoLspClient {
/// Open a virtual document (waits for diagnostics - slower but convenient for single files)
pub fn did_open(&mut self, uri: &str, content: &str) -> Result<(), String> {
self.did_open_fast(uri, content)?;
// Read any diagnostics that might be published
self.read_notifications()?;
Ok(())
}
/// Open a virtual document without waiting for diagnostics (faster for batch operations)
/// Call wait_for_diagnostics() after opening all files to collect diagnostics
pub fn did_open_fast(&mut self, uri: &str, content: &str) -> Result<(), String> {
let params = serde_json::json!({
"textDocument": {
"uri": uri,
"languageId": "typescript",
"version": 1,
"text": content
}
});
self.send_notification("textDocument/didOpen", params)?;
// Drain any pending messages to prevent pipe buffer from filling up
self.drain_pending_messages();
Ok(())
}
/// Close a virtual document
pub fn did_close(&mut self, uri: &str) -> Result<(), String> {
let params = serde_json::json!({
"textDocument": {
"uri": uri
}
});
self.send_notification("textDocument/didClose", params)?;
// Remove cached diagnostics
self.diagnostics.remove(uri);
Ok(())
}
/// Get diagnostics for a URI
pub fn get_diagnostics(&self, uri: &str) -> Vec<LspDiagnostic> {
self.diagnostics.get(uri).cloned().unwrap_or_default()
}
/// Request diagnostics using textDocument/diagnostic (LSP 3.17+)
pub fn request_diagnostics(&mut self, uri: &str) -> Result<Vec<LspDiagnostic>, String> {
let params = serde_json::json!({
"textDocument": {
"uri": uri
}
});
match self.send_request("textDocument/diagnostic", params) {
Ok(result) => {
// Parse the diagnostic response
if let Some(items) = result.get("items").and_then(|i| i.as_array()) {
let diags: Vec<LspDiagnostic> = items
.iter()
.filter_map(|d| serde_json::from_value(d.clone()).ok())
.collect();
return Ok(diags);
}
Ok(vec![])
}
Err(_) => {
// Fallback to cached diagnostics from publishDiagnostics
Ok(self.diagnostics.get(uri).cloned().unwrap_or_default())
}
}
}
/// Request diagnostics for multiple URIs in batch (pipelined)
/// Sends all requests first, then collects all responses
pub fn request_diagnostics_batch(
&mut self,
uris: &[String],
) -> Vec<(String, Vec<LspDiagnostic>)> {
// Phase 1: Send all requests
let mut request_ids: FxHashMap<i64, String> = FxHashMap::default();
for uri in uris {
let id = self.request_id.fetch_add(1, Ordering::SeqCst);
let request = serde_json::json!({
"jsonrpc": "2.0",
"id": id,
"method": "textDocument/diagnostic",
"params": {
"textDocument": {
"uri": uri
}
}
});
if self.send_message(&request).is_ok() {
request_ids.insert(id, uri.clone());
}
}
// Phase 2: Collect all responses
let mut results: Vec<(String, Vec<LspDiagnostic>)> = Vec::new();
let max_wait = Duration::from_secs(30);
let start = std::time::Instant::now();
while !request_ids.is_empty() && start.elapsed() < max_wait {
match self.try_read_message_nonblocking() {
Some(Ok(msg)) => {
// Check if this is a response
if let Some(msg_id) = msg.get("id").and_then(|i| i.as_i64()) {
if let Some(uri) = request_ids.remove(&msg_id) {
// Parse diagnostics from result
let diags = msg
.get("result")
.and_then(|r| r.get("items"))
.and_then(|i| i.as_array())
.map(|items| {
items
.iter()
.filter_map(|d| serde_json::from_value(d.clone()).ok())
.collect()
})
.unwrap_or_default();
results.push((uri, diags));
}
} else {
// Handle notification
self.handle_notification(&msg);
}
}
Some(Err(_)) => break,
None => {
thread::sleep(Duration::from_millis(1));
}
}
}
results
}
/// Send a JSON-RPC request and wait for response
pub(crate) fn send_request(&mut self, method: &str, params: Value) -> Result<Value, String> {
let id = self.request_id.fetch_add(1, Ordering::SeqCst);
let request = serde_json::json!({
"jsonrpc": "2.0",
"id": id,
"method": method,
"params": params
});
self.send_message(&request)?;
// Read response (and any notifications)
loop {
let msg = self.read_message()?;
// Check if this is our response
if let Some(msg_id) = msg.get("id") {
if msg_id.as_i64() == Some(id) {
if let Some(error) = msg.get("error") {
return Err(cstr!("LSP error: {error:?}"));
}
return Ok(msg.get("result").cloned().unwrap_or(Value::Null));
}
}
// Handle notification
self.handle_notification(&msg);
}
}
/// Send a JSON-RPC notification (no response expected)
pub(crate) fn send_notification(&mut self, method: &str, params: Value) -> Result<(), String> {
let notification = serde_json::json!({
"jsonrpc": "2.0",
"method": method,
"params": params
});
self.send_message(¬ification)
}
/// Send a message with Content-Length header
pub(crate) fn send_message(&mut self, msg: &Value) -> Result<(), String> {
#[allow(clippy::disallowed_methods)]
let content = serde_json::to_string(msg).map_err(|e| cstr!("JSON error: {e}"))?;
let header = cstr!("Content-Length: {}\r\n\r\n", content.len());
self.stdin
.write_all(header.as_bytes())
.map_err(|e| cstr!("Write error: {e}"))?;
self.stdin
.write_all(content.as_bytes())
.map_err(|e| cstr!("Write error: {e}"))?;
self.stdin.flush().map_err(|e| cstr!("Flush error: {e}"))?;
Ok(())
}
/// Read a single LSP message
#[allow(clippy::disallowed_types)]
pub(crate) fn read_message(&mut self) -> Result<Value, String> {
// Read headers (with retry on WouldBlock for non-blocking mode)
let mut content_length: usize = 0;
let mut headers_read: Vec<std::string::String> = Vec::new();
loop {
let mut line = std::string::String::new();
let bytes_read = loop {
match self.stdout.read_line(&mut line) {
Ok(n) => break n,
Err(e) if e.kind() == ErrorKind::WouldBlock => {
// Non-blocking mode: wait a bit and retry
thread::sleep(Duration::from_millis(1));
continue;
}
Err(e) => return Err(cstr!("Read error: {e}")),
}
};
if bytes_read == 0 {
// EOF - process may have exited
return Err(cstr!(
"EOF while reading headers. Headers read so far: {headers_read:?}"
));
}
headers_read.push(line.clone());
let line = line.trim();
if line.is_empty() {
break;
}
if let Some(len_str) = line.strip_prefix("Content-Length: ") {
content_length = len_str
.parse()
.map_err(|e| cstr!("Invalid Content-Length: {e}"))?;
}
}
if content_length == 0 {
return Err(cstr!("No Content-Length header. Headers: {headers_read:?}"));
}
// Read content (with retry on WouldBlock)
let mut content = vec![0u8; content_length];
let mut bytes_read = 0;
while bytes_read < content_length {
match self.stdout.read(&mut content[bytes_read..]) {
Ok(0) => return Err("EOF while reading content".into()),
Ok(n) => bytes_read += n,
Err(e) if e.kind() == ErrorKind::WouldBlock => {
thread::sleep(Duration::from_millis(1));
continue;
}
Err(e) => return Err(cstr!("Read error: {e}")),
}
}
let msg: Value =
serde_json::from_slice(&content).map_err(|e| cstr!("JSON parse error: {e}"))?;
Ok(msg)
}
}