1use crate::project::ProjectRoot;
2use anyhow::{Context, Result, bail};
3use serde_json::{Value, json};
4use std::collections::HashMap;
5use std::io::{BufRead, BufReader};
6use std::path::Path;
7use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
8use std::thread;
9use std::time::{Duration, Instant};
10use url::Url;
11
12use super::parsers::{
13 diagnostics_from_response, method_suffix_to_hierarchy, references_from_response,
14 rename_plan_from_response, type_hierarchy_node_from_item, type_hierarchy_to_map,
15 workspace_symbols_from_response,
16};
17use super::protocol::{language_id_for_path, poll_readable, read_message, send_message};
18use super::registry::resolve_lsp_binary;
19use super::types::{
20 LspDiagnostic, LspDiagnosticRequest, LspReference, LspRenamePlan, LspRenamePlanRequest,
21 LspRequest, LspTypeHierarchyNode, LspTypeHierarchyRequest, LspWorkspaceSymbol,
22 LspWorkspaceSymbolRequest,
23};
24
25#[derive(Debug, Clone, PartialEq, Eq, Hash)]
26struct SessionKey {
27 command: String,
28 args: Vec<String>,
29}
30
31#[derive(Debug, Clone)]
32struct OpenDocumentState {
33 version: i32,
34 text: String,
35}
36
37pub struct LspSessionPool {
38 project: ProjectRoot,
39 sessions: std::sync::Mutex<HashMap<SessionKey, LspSession>>,
40}
41
42struct LspSession {
43 project: ProjectRoot,
44 child: Child,
45 stdin: ChildStdin,
46 reader: BufReader<ChildStdout>,
47 next_request_id: u64,
48 documents: HashMap<String, OpenDocumentState>,
49 #[allow(dead_code)] stderr_buffer: std::sync::Arc<std::sync::Mutex<String>>,
51}
52
53pub(super) fn is_allowed_lsp_command(command: &str) -> bool {
55 let binary = std::path::Path::new(command)
57 .file_name()
58 .and_then(|n| n.to_str())
59 .unwrap_or(command);
60
61 ALLOWED_COMMANDS.contains(&binary)
62}
63
64pub(super) const ALLOWED_COMMANDS: &[&str] = &[
65 "pyright-langserver",
67 "typescript-language-server",
68 "rust-analyzer",
69 "gopls",
70 "jdtls",
71 "kotlin-language-server",
72 "clangd",
73 "solargraph",
74 "intelephense",
75 "sourcekit-lsp",
76 "csharp-ls",
77 "dart",
78 "metals",
80 "lua-language-server",
81 "terraform-ls",
82 "yaml-language-server",
83 "python3",
85 "python",
86];
87
88fn ensure_session<'a>(
89 sessions: &'a mut HashMap<SessionKey, LspSession>,
90 project: &ProjectRoot,
91 command: &str,
92 args: &[String],
93) -> Result<&'a mut LspSession> {
94 if !is_allowed_lsp_command(command) {
95 bail!(
96 "Blocked: '{command}' is not a known LSP server. Only whitelisted LSP binaries are allowed."
97 );
98 }
99
100 let key = SessionKey {
101 command: command.to_owned(),
102 args: args.to_owned(),
103 };
104
105 if let Some(session) = sessions.get_mut(&key) {
107 match session.child.try_wait() {
108 Ok(Some(_status)) => {
109 sessions.remove(&key);
111 }
112 Ok(None) => {} Err(_) => {
114 sessions.remove(&key);
115 }
116 }
117 }
118
119 match sessions.entry(key) {
120 std::collections::hash_map::Entry::Occupied(e) => Ok(e.into_mut()),
121 std::collections::hash_map::Entry::Vacant(e) => {
122 let session = LspSession::start(project, command, args)?;
123 Ok(e.insert(session))
124 }
125 }
126}
127
128impl LspSessionPool {
129 pub fn new(project: ProjectRoot) -> Self {
130 Self {
131 project,
132 sessions: std::sync::Mutex::new(HashMap::new()),
133 }
134 }
135
136 pub fn reset(&self, project: ProjectRoot) -> Self {
138 self.sessions
140 .lock()
141 .unwrap_or_else(|p| p.into_inner())
142 .clear();
143 Self::new(project)
144 }
145
146 pub fn session_count(&self) -> usize {
147 self.sessions
148 .lock()
149 .unwrap_or_else(|p| p.into_inner())
150 .len()
151 }
152
153 pub fn find_referencing_symbols(&self, request: &LspRequest) -> Result<Vec<LspReference>> {
154 let mut sessions = self.sessions.lock().unwrap_or_else(|p| p.into_inner());
155 let session = ensure_session(
156 &mut sessions,
157 &self.project,
158 &request.command,
159 &request.args,
160 )?;
161 session.find_references(request)
162 }
163
164 pub fn get_diagnostics(&self, request: &LspDiagnosticRequest) -> Result<Vec<LspDiagnostic>> {
165 let mut sessions = self.sessions.lock().unwrap_or_else(|p| p.into_inner());
166 let session = ensure_session(
167 &mut sessions,
168 &self.project,
169 &request.command,
170 &request.args,
171 )?;
172 session.get_diagnostics(request)
173 }
174
175 pub fn search_workspace_symbols(
176 &self,
177 request: &LspWorkspaceSymbolRequest,
178 ) -> Result<Vec<LspWorkspaceSymbol>> {
179 let mut sessions = self.sessions.lock().unwrap_or_else(|p| p.into_inner());
180 let session = ensure_session(
181 &mut sessions,
182 &self.project,
183 &request.command,
184 &request.args,
185 )?;
186 session.search_workspace_symbols(request)
187 }
188
189 pub fn get_type_hierarchy(
190 &self,
191 request: &LspTypeHierarchyRequest,
192 ) -> Result<HashMap<String, Value>> {
193 let mut sessions = self.sessions.lock().unwrap_or_else(|p| p.into_inner());
194 let session = ensure_session(
195 &mut sessions,
196 &self.project,
197 &request.command,
198 &request.args,
199 )?;
200 session.get_type_hierarchy(request)
201 }
202
203 pub fn get_rename_plan(&self, request: &LspRenamePlanRequest) -> Result<LspRenamePlan> {
204 let mut sessions = self.sessions.lock().unwrap_or_else(|p| p.into_inner());
205 let session = ensure_session(
206 &mut sessions,
207 &self.project,
208 &request.command,
209 &request.args,
210 )?;
211 session.get_rename_plan(request)
212 }
213}
214
215impl LspSession {
216 fn start(project: &ProjectRoot, command: &str, args: &[String]) -> Result<Self> {
217 let command_path = resolve_lsp_binary(command).unwrap_or_else(|| command.into());
218 let mut child = Command::new(&command_path)
219 .args(args)
220 .stdin(Stdio::piped())
221 .stdout(Stdio::piped())
222 .stderr(Stdio::piped())
223 .spawn()
224 .with_context(|| format!("failed to spawn LSP server {}", command_path.display()))?;
225
226 let stdin = child.stdin.take().context("failed to open LSP stdin")?;
227 let stdout = child.stdout.take().context("failed to open LSP stdout")?;
228
229 let stderr_buffer = std::sync::Arc::new(std::sync::Mutex::new(String::new()));
231 if let Some(stderr) = child.stderr.take() {
232 let buf = std::sync::Arc::clone(&stderr_buffer);
233 thread::spawn(move || {
234 let mut reader = BufReader::new(stderr);
235 let mut line = String::new();
236 while reader.read_line(&mut line).unwrap_or(0) > 0 {
237 if let Ok(mut b) = buf.lock() {
238 if b.len() > 4096 {
239 let drain_to = b.len() - 2048;
240 b.drain(..drain_to);
241 }
242 b.push_str(&line);
243 }
244 line.clear();
245 }
246 });
247 }
248
249 let mut session = Self {
250 project: project.clone(),
251 child,
252 stdin,
253 reader: BufReader::new(stdout),
254 next_request_id: 1,
255 documents: HashMap::new(),
256 stderr_buffer,
257 };
258 session.initialize()?;
259 Ok(session)
260 }
261
262 fn initialize(&mut self) -> Result<()> {
263 let id = self.next_id();
264 let root_uri = Url::from_directory_path(self.project.as_path())
265 .ok()
266 .map(|url| url.to_string());
267 self.send_request(
268 id,
269 "initialize",
270 json!({
271 "processId":null,
272 "rootUri": root_uri,
273 "capabilities":{},
274 "workspaceFolders":[
275 {
276 "uri": Url::from_directory_path(self.project.as_path()).ok().map(|url| url.to_string()),
277 "name": self.project.as_path().file_name().and_then(|n| n.to_str()).unwrap_or("workspace")
278 }
279 ]
280 }),
281 )?;
282 let _ = self.read_response_for_id(id)?;
283 self.send_notification("initialized", json!({}))?;
284 Ok(())
285 }
286
287 fn find_references(&mut self, request: &LspRequest) -> Result<Vec<LspReference>> {
288 let absolute_path = self.project.resolve(&request.file_path)?;
289 let (uri_string, _source) = self.prepare_document(&absolute_path)?;
290
291 let id = self.next_id();
292 self.send_request(
293 id,
294 "textDocument/references",
295 json!({
296 "textDocument":{"uri":uri_string},
297 "position":{"line":request.line.saturating_sub(1),"character":request.column.saturating_sub(1)},
298 "context":{"includeDeclaration":true}
299 }),
300 )?;
301 let response = self.read_response_for_id(id)?;
302 references_from_response(&self.project, response, request.max_results)
303 }
304
305 fn get_diagnostics(&mut self, request: &LspDiagnosticRequest) -> Result<Vec<LspDiagnostic>> {
306 let absolute_path = self.project.resolve(&request.file_path)?;
307 let (uri_string, _source) = self.prepare_document(&absolute_path)?;
308
309 let id = self.next_id();
310 self.send_request(
311 id,
312 "textDocument/diagnostic",
313 json!({
314 "textDocument":{"uri":uri_string}
315 }),
316 )?;
317 let response = self.read_response_for_id(id)?;
318 diagnostics_from_response(&self.project, response, request.max_results)
319 }
320
321 fn search_workspace_symbols(
322 &mut self,
323 request: &LspWorkspaceSymbolRequest,
324 ) -> Result<Vec<LspWorkspaceSymbol>> {
325 let id = self.next_id();
326 self.send_request(
327 id,
328 "workspace/symbol",
329 json!({
330 "query": request.query
331 }),
332 )?;
333 let response = self.read_response_for_id(id)?;
334 workspace_symbols_from_response(&self.project, response, request.max_results)
335 }
336
337 fn get_type_hierarchy(
338 &mut self,
339 request: &LspTypeHierarchyRequest,
340 ) -> Result<HashMap<String, Value>> {
341 let workspace_symbols = self.search_workspace_symbols(&LspWorkspaceSymbolRequest {
342 command: request.command.clone(),
343 args: request.args.clone(),
344 query: request.query.clone(),
345 max_results: 20,
346 })?;
347 let seed = workspace_symbols
348 .into_iter()
349 .find(|symbol| match &request.relative_path {
350 Some(path) => &symbol.file_path == path,
351 None => true,
352 })
353 .with_context(|| format!("No workspace symbol found for '{}'", request.query))?;
354
355 let absolute_path = self.project.resolve(&seed.file_path)?;
356 let (uri_string, _source) = self.prepare_document(&absolute_path)?;
357
358 let id = self.next_id();
359 self.send_request(
360 id,
361 "textDocument/prepareTypeHierarchy",
362 json!({
363 "textDocument":{"uri":uri_string},
364 "position":{"line":seed.line.saturating_sub(1),"character":seed.column.saturating_sub(1)}
365 }),
366 )?;
367 let response = self.read_response_for_id(id)?;
368 let items = response
369 .get("result")
370 .and_then(Value::as_array)
371 .cloned()
372 .unwrap_or_default();
373 let root_item = items
374 .into_iter()
375 .next()
376 .context("LSP prepareTypeHierarchy returned no items")?;
377
378 let root = self.build_type_hierarchy_node(
379 &root_item,
380 request.depth,
381 request.hierarchy_type.as_str(),
382 )?;
383 Ok(type_hierarchy_to_map(&root))
384 }
385
386 fn get_rename_plan(&mut self, request: &LspRenamePlanRequest) -> Result<LspRenamePlan> {
387 let absolute_path = self.project.resolve(&request.file_path)?;
388 let (uri_string, source) = self.prepare_document(&absolute_path)?;
389
390 let id = self.next_id();
391 self.send_request(
392 id,
393 "textDocument/prepareRename",
394 json!({
395 "textDocument":{"uri":uri_string},
396 "position":{"line":request.line.saturating_sub(1),"character":request.column.saturating_sub(1)}
397 }),
398 )?;
399 let response = self.read_response_for_id(id)?;
400 rename_plan_from_response(
401 &self.project,
402 &request.file_path,
403 &source,
404 response,
405 request.new_name.clone(),
406 )
407 }
408
409 fn build_type_hierarchy_node(
410 &mut self,
411 item: &Value,
412 depth: usize,
413 hierarchy_type: &str,
414 ) -> Result<LspTypeHierarchyNode> {
415 let mut node = type_hierarchy_node_from_item(item)?;
416
417 if depth == 0 {
418 return Ok(node);
419 }
420
421 let next_depth = depth.saturating_sub(1);
422 if hierarchy_type == "super" || hierarchy_type == "both" {
423 node.supertypes = self.fetch_type_hierarchy_branch(item, "supertypes", next_depth)?;
424 }
425 if hierarchy_type == "sub" || hierarchy_type == "both" {
426 node.subtypes = self.fetch_type_hierarchy_branch(item, "subtypes", next_depth)?;
427 }
428 Ok(node)
429 }
430
431 fn fetch_type_hierarchy_branch(
432 &mut self,
433 item: &Value,
434 method_suffix: &str,
435 depth: usize,
436 ) -> Result<Vec<LspTypeHierarchyNode>> {
437 let id = self.next_id();
438 self.send_request(
439 id,
440 &format!("typeHierarchy/{method_suffix}"),
441 json!({
442 "item": item
443 }),
444 )?;
445 let response = self.read_response_for_id(id)?;
446 let Some(items) = response.get("result").and_then(Value::as_array) else {
447 return Ok(Vec::new());
448 };
449
450 let mut nodes = Vec::new();
451 for child in items {
452 nodes.push(self.build_type_hierarchy_node(
453 child,
454 depth,
455 method_suffix_to_hierarchy(method_suffix),
456 )?);
457 }
458 Ok(nodes)
459 }
460
461 fn prepare_document(&mut self, absolute_path: &Path) -> Result<(String, String)> {
462 let uri = Url::from_file_path(absolute_path).map_err(|_| {
463 anyhow::anyhow!("failed to build file uri for {}", absolute_path.display())
464 })?;
465 let uri_string = uri.to_string();
466 let source = std::fs::read_to_string(absolute_path)
467 .with_context(|| format!("failed to read {}", absolute_path.display()))?;
468 let language_id = language_id_for_path(absolute_path)?;
469 self.sync_document(&uri_string, language_id, &source)?;
470 Ok((uri_string, source))
471 }
472
473 fn sync_document(&mut self, uri: &str, language_id: &str, source: &str) -> Result<()> {
474 if let Some(state) = self.documents.get(uri)
475 && state.text == source
476 {
477 return Ok(());
478 }
479
480 if let Some(state) = self.documents.get_mut(uri) {
481 state.version += 1;
482 state.text = source.to_owned();
483 let version = state.version;
484 return self.send_notification(
485 "textDocument/didChange",
486 json!({
487 "textDocument":{"uri":uri,"version":version},
488 "contentChanges":[{"text":source}]
489 }),
490 );
491 }
492
493 self.documents.insert(
494 uri.to_owned(),
495 OpenDocumentState {
496 version: 1,
497 text: source.to_owned(),
498 },
499 );
500 self.send_notification(
501 "textDocument/didOpen",
502 json!({
503 "textDocument":{
504 "uri":uri,
505 "languageId":language_id,
506 "version":1,
507 "text":source
508 }
509 }),
510 )
511 }
512
513 fn next_id(&mut self) -> u64 {
514 let id = self.next_request_id;
515 self.next_request_id += 1;
516 id
517 }
518
519 fn send_request(&mut self, id: u64, method: &str, params: Value) -> Result<()> {
520 send_message(
521 &mut self.stdin,
522 &json!({
523 "jsonrpc":"2.0",
524 "id":id,
525 "method":method,
526 "params":params
527 }),
528 )
529 }
530
531 fn send_notification(&mut self, method: &str, params: Value) -> Result<()> {
532 send_message(
533 &mut self.stdin,
534 &json!({
535 "jsonrpc":"2.0",
536 "method":method,
537 "params":params
538 }),
539 )
540 }
541
542 fn read_response_for_id(&mut self, expected_id: u64) -> Result<Value> {
543 let deadline = Instant::now() + Duration::from_secs(30);
544 let mut discarded = 0u32;
545 const MAX_DISCARDED: u32 = 500;
546
547 loop {
548 let remaining = deadline.saturating_duration_since(Instant::now());
549 if remaining.is_zero() {
550 bail!(
551 "LSP response timeout: no response for request id {expected_id} within 30s \
552 ({discarded} unrelated messages discarded)"
553 );
554 }
555 if discarded >= MAX_DISCARDED {
556 bail!(
557 "LSP response loop: discarded {MAX_DISCARDED} messages without finding id {expected_id}"
558 );
559 }
560
561 if !poll_readable(self.reader.get_ref(), remaining.min(Duration::from_secs(5))) {
563 continue; }
565
566 let message = read_message(&mut self.reader)?;
567 let matches_id = message
568 .get("id")
569 .and_then(Value::as_u64)
570 .map(|id| id == expected_id)
571 .unwrap_or(false);
572 if matches_id {
573 if let Some(error) = message.get("error") {
574 let code = error.get("code").and_then(Value::as_i64).unwrap_or(-1);
575 let error_message = error
576 .get("message")
577 .and_then(Value::as_str)
578 .unwrap_or("unknown LSP error");
579 bail!("LSP request failed ({code}): {error_message}");
580 }
581 return Ok(message);
582 }
583 discarded += 1;
584 }
585 }
586
587 fn shutdown(&mut self) -> Result<()> {
588 let id = self.next_id();
589 self.send_request(id, "shutdown", Value::Null)?;
590 let _ = self.read_response_for_id(id)?;
591 self.send_notification("exit", Value::Null)
592 }
593}
594
595impl Drop for LspSession {
596 fn drop(&mut self) {
597 let _ = self.shutdown();
598 let deadline = Instant::now() + Duration::from_millis(250);
599 while Instant::now() < deadline {
600 match self.child.try_wait() {
601 Ok(Some(_status)) => return,
602 Ok(None) => thread::sleep(Duration::from_millis(10)),
603 Err(_) => break,
604 }
605 }
606 let _ = self.child.kill();
607 let _ = self.child.wait();
608 }
609}