1use crate::project::ProjectRoot;
2use anyhow::{Context, Result, bail};
3use dashmap::DashMap;
4use serde_json::{Value, json};
5use std::collections::HashMap;
6use std::io::{BufRead, BufReader};
7use std::path::Path;
8use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
9use std::sync::{Arc, Mutex};
10use std::thread;
11use std::time::{Duration, Instant};
12use url::Url;
13
14use super::parsers::{
15 diagnostics_from_response, method_suffix_to_hierarchy, references_from_response,
16 rename_plan_from_response, type_hierarchy_node_from_item, type_hierarchy_to_map,
17 workspace_symbols_from_response,
18};
19use super::protocol::{language_id_for_path, poll_readable, read_message, send_message};
20use super::registry::resolve_lsp_binary;
21use super::types::{
22 LspDiagnostic, LspDiagnosticRequest, LspReference, LspRenamePlan, LspRenamePlanRequest,
23 LspRequest, LspTypeHierarchyNode, LspTypeHierarchyRequest, LspWorkspaceSymbol,
24 LspWorkspaceSymbolRequest,
25};
26
27#[derive(Debug, Clone, PartialEq, Eq, Hash)]
28struct SessionKey {
29 command: String,
30 args: Vec<String>,
31}
32
33#[derive(Debug, Clone)]
34struct OpenDocumentState {
35 version: i32,
36 text: String,
37}
38
39pub struct LspSessionPool {
47 project: ProjectRoot,
48 sessions: DashMap<SessionKey, Arc<Mutex<LspSession>>>,
49 readiness: DashMap<SessionKey, Arc<super::readiness::ReadinessState>>,
54}
55
56struct LspSession {
57 project: ProjectRoot,
58 child: Child,
59 stdin: ChildStdin,
60 reader: BufReader<ChildStdout>,
61 next_request_id: u64,
62 documents: HashMap<String, OpenDocumentState>,
63 #[allow(dead_code)] stderr_buffer: std::sync::Arc<std::sync::Mutex<String>>,
65}
66
67pub(super) fn is_allowed_lsp_command(command: &str) -> bool {
69 let binary = std::path::Path::new(command)
71 .file_name()
72 .and_then(|n| n.to_str())
73 .unwrap_or(command);
74
75 ALLOWED_COMMANDS.contains(&binary)
76}
77
78pub(super) const ALLOWED_COMMANDS: &[&str] = &[
79 "pyright-langserver",
81 "typescript-language-server",
82 "rust-analyzer",
83 "gopls",
84 "jdtls",
85 "kotlin-language-server",
86 "clangd",
87 "solargraph",
88 "intelephense",
89 "sourcekit-lsp",
90 "csharp-ls",
91 "dart",
92 "metals",
94 "lua-language-server",
95 "terraform-ls",
96 "yaml-language-server",
97 "python3",
99 "python",
100];
101
102fn get_or_start_session(
107 sessions: &DashMap<SessionKey, Arc<Mutex<LspSession>>>,
108 readiness: &DashMap<SessionKey, Arc<super::readiness::ReadinessState>>,
109 project: &ProjectRoot,
110 command: &str,
111 args: &[String],
112) -> Result<(
113 Arc<Mutex<LspSession>>,
114 Arc<super::readiness::ReadinessState>,
115)> {
116 if !is_allowed_lsp_command(command) {
117 bail!(
118 "Blocked: '{command}' is not a known LSP server. Only whitelisted LSP binaries are allowed."
119 );
120 }
121
122 let key = SessionKey {
123 command: command.to_owned(),
124 args: args.to_owned(),
125 };
126
127 if let Some(existing) = sessions.get(&key) {
129 let arc = existing.clone();
130 drop(existing); let dead = {
132 let mut guard = arc.lock().unwrap_or_else(|p| p.into_inner());
133 match guard.child.try_wait() {
134 Ok(Some(_status)) => true, Err(_) => true, Ok(None) => false, }
138 };
139 if !dead {
140 let ready = readiness
141 .get(&key)
142 .map(|r| r.clone())
143 .unwrap_or_else(|| {
144 let r = Arc::new(super::readiness::ReadinessState::new(
149 command.to_owned(),
150 args.to_owned(),
151 ));
152 readiness.insert(key.clone(), r.clone());
153 r
154 });
155 return Ok((arc, ready));
156 }
157 sessions.remove(&key);
158 readiness.remove(&key);
159 }
160
161 use dashmap::mapref::entry::Entry;
165 match sessions.entry(key.clone()) {
166 Entry::Occupied(e) => {
167 let arc = e.get().clone();
168 let ready = readiness
169 .get(&key)
170 .map(|r| r.clone())
171 .unwrap_or_else(|| {
172 let r = Arc::new(super::readiness::ReadinessState::new(
173 command.to_owned(),
174 args.to_owned(),
175 ));
176 readiness.insert(key.clone(), r.clone());
177 r
178 });
179 Ok((arc, ready))
180 }
181 Entry::Vacant(e) => {
182 let ready = Arc::new(super::readiness::ReadinessState::new(
190 command.to_owned(),
191 args.to_owned(),
192 ));
193 readiness.insert(key.clone(), ready.clone());
194 match LspSession::start(project, command, args) {
195 Ok(session) => {
196 let arc = Arc::new(Mutex::new(session));
197 e.insert(arc.clone());
198 Ok((arc, ready))
199 }
200 Err(err) => {
201 ready.record_failure();
202 Err(err)
206 }
207 }
208 }
209 }
210}
211
212impl LspSessionPool {
213 pub fn new(project: ProjectRoot) -> Self {
214 Self {
215 project,
216 sessions: DashMap::new(),
217 readiness: DashMap::new(),
218 }
219 }
220
221 pub fn reset(&self, project: ProjectRoot) -> Self {
223 self.sessions.clear();
225 self.readiness.clear();
226 Self::new(project)
227 }
228
229 pub fn session_count(&self) -> usize {
230 self.sessions.len()
231 }
232
233 pub fn readiness_snapshot(&self) -> Vec<super::readiness::ReadinessSnapshot> {
240 let mut out: Vec<super::readiness::ReadinessSnapshot> = self
241 .readiness
242 .iter()
243 .map(|entry| entry.value().snapshot())
244 .collect();
245 out.sort_by(|a, b| a.command.cmp(&b.command).then(a.args.cmp(&b.args)));
248 out
249 }
250
251 pub fn find_referencing_symbols(&self, request: &LspRequest) -> Result<Vec<LspReference>> {
252 let (arc, readiness) = get_or_start_session(
253 &self.sessions,
254 &self.readiness,
255 &self.project,
256 &request.command,
257 &request.args,
258 )?;
259 let result = {
260 let mut session = arc.lock().unwrap_or_else(|p| p.into_inner());
261 session.find_references(request)
262 };
263 match &result {
264 Ok(refs) => readiness.record_ok(!refs.is_empty()),
265 Err(_) => readiness.record_failure(),
266 }
267 result
268 }
269
270 pub fn get_diagnostics(&self, request: &LspDiagnosticRequest) -> Result<Vec<LspDiagnostic>> {
271 let (arc, readiness) = get_or_start_session(
272 &self.sessions,
273 &self.readiness,
274 &self.project,
275 &request.command,
276 &request.args,
277 )?;
278 let result = {
279 let mut session = arc.lock().unwrap_or_else(|p| p.into_inner());
280 session.get_diagnostics(request)
281 };
282 match &result {
283 Ok(diags) => readiness.record_ok(!diags.is_empty()),
284 Err(_) => readiness.record_failure(),
285 }
286 result
287 }
288
289 pub fn search_workspace_symbols(
290 &self,
291 request: &LspWorkspaceSymbolRequest,
292 ) -> Result<Vec<LspWorkspaceSymbol>> {
293 let (arc, readiness) = get_or_start_session(
294 &self.sessions,
295 &self.readiness,
296 &self.project,
297 &request.command,
298 &request.args,
299 )?;
300 let result = {
301 let mut session = arc.lock().unwrap_or_else(|p| p.into_inner());
302 session.search_workspace_symbols(request)
303 };
304 match &result {
305 Ok(symbols) => readiness.record_ok(!symbols.is_empty()),
306 Err(_) => readiness.record_failure(),
307 }
308 result
309 }
310
311 pub fn get_type_hierarchy(
312 &self,
313 request: &LspTypeHierarchyRequest,
314 ) -> Result<HashMap<String, Value>> {
315 let (arc, readiness) = get_or_start_session(
316 &self.sessions,
317 &self.readiness,
318 &self.project,
319 &request.command,
320 &request.args,
321 )?;
322 let result = {
323 let mut session = arc.lock().unwrap_or_else(|p| p.into_inner());
324 session.get_type_hierarchy(request)
325 };
326 match &result {
327 Ok(map) => readiness.record_ok(!map.is_empty()),
328 Err(_) => readiness.record_failure(),
329 }
330 result
331 }
332
333 pub fn get_rename_plan(&self, request: &LspRenamePlanRequest) -> Result<LspRenamePlan> {
334 let (arc, readiness) = get_or_start_session(
335 &self.sessions,
336 &self.readiness,
337 &self.project,
338 &request.command,
339 &request.args,
340 )?;
341 let result = {
342 let mut session = arc.lock().unwrap_or_else(|p| p.into_inner());
343 session.get_rename_plan(request)
344 };
345 match &result {
346 Ok(plan) => {
347 let nonempty = !plan.current_name.is_empty();
353 readiness.record_ok(nonempty);
354 }
355 Err(_) => readiness.record_failure(),
356 }
357 result
358 }
359}
360
361impl LspSession {
362 fn start(project: &ProjectRoot, command: &str, args: &[String]) -> Result<Self> {
363 let command_path = resolve_lsp_binary(command).unwrap_or_else(|| command.into());
364 let mut child = Command::new(&command_path)
365 .args(args)
366 .stdin(Stdio::piped())
367 .stdout(Stdio::piped())
368 .stderr(Stdio::piped())
369 .spawn()
370 .with_context(|| format!("failed to spawn LSP server {}", command_path.display()))?;
371
372 let stdin = child.stdin.take().context("failed to open LSP stdin")?;
373 let stdout = child.stdout.take().context("failed to open LSP stdout")?;
374
375 let stderr_buffer = std::sync::Arc::new(std::sync::Mutex::new(String::new()));
377 if let Some(stderr) = child.stderr.take() {
378 let buf = std::sync::Arc::clone(&stderr_buffer);
379 thread::spawn(move || {
380 let mut reader = BufReader::new(stderr);
381 let mut line = String::new();
382 while reader.read_line(&mut line).unwrap_or(0) > 0 {
383 if let Ok(mut b) = buf.lock() {
384 if b.len() > 4096 {
385 let drain_to = b.len() - 2048;
386 b.drain(..drain_to);
387 }
388 b.push_str(&line);
389 }
390 line.clear();
391 }
392 });
393 }
394
395 let mut session = Self {
396 project: project.clone(),
397 child,
398 stdin,
399 reader: BufReader::new(stdout),
400 next_request_id: 1,
401 documents: HashMap::new(),
402 stderr_buffer,
403 };
404 session.initialize()?;
405 Ok(session)
406 }
407
408 fn initialize(&mut self) -> Result<()> {
409 let id = self.next_id();
410 let root_uri = Url::from_directory_path(self.project.as_path())
411 .ok()
412 .map(|url| url.to_string());
413 self.send_request(
414 id,
415 "initialize",
416 json!({
417 "processId":null,
418 "rootUri": root_uri,
419 "capabilities":{},
420 "workspaceFolders":[
421 {
422 "uri": Url::from_directory_path(self.project.as_path()).ok().map(|url| url.to_string()),
423 "name": self.project.as_path().file_name().and_then(|n| n.to_str()).unwrap_or("workspace")
424 }
425 ]
426 }),
427 )?;
428 let _ = self.read_response_for_id(id)?;
429 self.send_notification("initialized", json!({}))?;
430 Ok(())
431 }
432
433 fn find_references(&mut self, request: &LspRequest) -> Result<Vec<LspReference>> {
434 let absolute_path = self.project.resolve(&request.file_path)?;
435 let (uri_string, _source) = self.prepare_document(&absolute_path)?;
436
437 let id = self.next_id();
438 self.send_request(
439 id,
440 "textDocument/references",
441 json!({
442 "textDocument":{"uri":uri_string},
443 "position":{"line":request.line.saturating_sub(1),"character":request.column.saturating_sub(1)},
444 "context":{"includeDeclaration":true}
445 }),
446 )?;
447 let response = self.read_response_for_id(id)?;
448 references_from_response(&self.project, response, request.max_results)
449 }
450
451 fn get_diagnostics(&mut self, request: &LspDiagnosticRequest) -> Result<Vec<LspDiagnostic>> {
452 let absolute_path = self.project.resolve(&request.file_path)?;
453 let (uri_string, _source) = self.prepare_document(&absolute_path)?;
454
455 let id = self.next_id();
456 self.send_request(
457 id,
458 "textDocument/diagnostic",
459 json!({
460 "textDocument":{"uri":uri_string}
461 }),
462 )?;
463 let response = self.read_response_for_id(id)?;
464 diagnostics_from_response(&self.project, response, request.max_results)
465 }
466
467 fn search_workspace_symbols(
468 &mut self,
469 request: &LspWorkspaceSymbolRequest,
470 ) -> Result<Vec<LspWorkspaceSymbol>> {
471 let id = self.next_id();
472 self.send_request(
473 id,
474 "workspace/symbol",
475 json!({
476 "query": request.query
477 }),
478 )?;
479 let response = self.read_response_for_id(id)?;
480 workspace_symbols_from_response(&self.project, response, request.max_results)
481 }
482
483 fn get_type_hierarchy(
484 &mut self,
485 request: &LspTypeHierarchyRequest,
486 ) -> Result<HashMap<String, Value>> {
487 let workspace_symbols = self.search_workspace_symbols(&LspWorkspaceSymbolRequest {
488 command: request.command.clone(),
489 args: request.args.clone(),
490 query: request.query.clone(),
491 max_results: 20,
492 })?;
493 let seed = workspace_symbols
494 .into_iter()
495 .find(|symbol| match &request.relative_path {
496 Some(path) => &symbol.file_path == path,
497 None => true,
498 })
499 .with_context(|| format!("No workspace symbol found for '{}'", request.query))?;
500
501 let absolute_path = self.project.resolve(&seed.file_path)?;
502 let (uri_string, _source) = self.prepare_document(&absolute_path)?;
503
504 let id = self.next_id();
505 self.send_request(
506 id,
507 "textDocument/prepareTypeHierarchy",
508 json!({
509 "textDocument":{"uri":uri_string},
510 "position":{"line":seed.line.saturating_sub(1),"character":seed.column.saturating_sub(1)}
511 }),
512 )?;
513 let response = self.read_response_for_id(id)?;
514 let items = response
515 .get("result")
516 .and_then(Value::as_array)
517 .cloned()
518 .unwrap_or_default();
519 let root_item = items
520 .into_iter()
521 .next()
522 .context("LSP prepareTypeHierarchy returned no items")?;
523
524 let root = self.build_type_hierarchy_node(
525 &root_item,
526 request.depth,
527 request.hierarchy_type.as_str(),
528 )?;
529 Ok(type_hierarchy_to_map(&root))
530 }
531
532 fn get_rename_plan(&mut self, request: &LspRenamePlanRequest) -> Result<LspRenamePlan> {
533 let absolute_path = self.project.resolve(&request.file_path)?;
534 let (uri_string, source) = self.prepare_document(&absolute_path)?;
535
536 let id = self.next_id();
537 self.send_request(
538 id,
539 "textDocument/prepareRename",
540 json!({
541 "textDocument":{"uri":uri_string},
542 "position":{"line":request.line.saturating_sub(1),"character":request.column.saturating_sub(1)}
543 }),
544 )?;
545 let response = self.read_response_for_id(id)?;
546 rename_plan_from_response(
547 &self.project,
548 &request.file_path,
549 &source,
550 response,
551 request.new_name.clone(),
552 )
553 }
554
555 fn build_type_hierarchy_node(
556 &mut self,
557 item: &Value,
558 depth: usize,
559 hierarchy_type: &str,
560 ) -> Result<LspTypeHierarchyNode> {
561 let mut node = type_hierarchy_node_from_item(item)?;
562
563 if depth == 0 {
564 return Ok(node);
565 }
566
567 let next_depth = depth.saturating_sub(1);
568 if hierarchy_type == "super" || hierarchy_type == "both" {
569 node.supertypes = self.fetch_type_hierarchy_branch(item, "supertypes", next_depth)?;
570 }
571 if hierarchy_type == "sub" || hierarchy_type == "both" {
572 node.subtypes = self.fetch_type_hierarchy_branch(item, "subtypes", next_depth)?;
573 }
574 Ok(node)
575 }
576
577 fn fetch_type_hierarchy_branch(
578 &mut self,
579 item: &Value,
580 method_suffix: &str,
581 depth: usize,
582 ) -> Result<Vec<LspTypeHierarchyNode>> {
583 let id = self.next_id();
584 self.send_request(
585 id,
586 &format!("typeHierarchy/{method_suffix}"),
587 json!({
588 "item": item
589 }),
590 )?;
591 let response = self.read_response_for_id(id)?;
592 let Some(items) = response.get("result").and_then(Value::as_array) else {
593 return Ok(Vec::new());
594 };
595
596 let mut nodes = Vec::new();
597 for child in items {
598 nodes.push(self.build_type_hierarchy_node(
599 child,
600 depth,
601 method_suffix_to_hierarchy(method_suffix),
602 )?);
603 }
604 Ok(nodes)
605 }
606
607 fn prepare_document(&mut self, absolute_path: &Path) -> Result<(String, String)> {
608 let uri = Url::from_file_path(absolute_path).map_err(|_| {
609 anyhow::anyhow!("failed to build file uri for {}", absolute_path.display())
610 })?;
611 let uri_string = uri.to_string();
612 let source = std::fs::read_to_string(absolute_path)
613 .with_context(|| format!("failed to read {}", absolute_path.display()))?;
614 let language_id = language_id_for_path(absolute_path)?;
615 self.sync_document(&uri_string, language_id, &source)?;
616 Ok((uri_string, source))
617 }
618
619 fn sync_document(&mut self, uri: &str, language_id: &str, source: &str) -> Result<()> {
620 if let Some(state) = self.documents.get(uri)
621 && state.text == source
622 {
623 return Ok(());
624 }
625
626 if let Some(state) = self.documents.get_mut(uri) {
627 state.version += 1;
628 state.text = source.to_owned();
629 let version = state.version;
630 return self.send_notification(
631 "textDocument/didChange",
632 json!({
633 "textDocument":{"uri":uri,"version":version},
634 "contentChanges":[{"text":source}]
635 }),
636 );
637 }
638
639 self.documents.insert(
640 uri.to_owned(),
641 OpenDocumentState {
642 version: 1,
643 text: source.to_owned(),
644 },
645 );
646 self.send_notification(
647 "textDocument/didOpen",
648 json!({
649 "textDocument":{
650 "uri":uri,
651 "languageId":language_id,
652 "version":1,
653 "text":source
654 }
655 }),
656 )
657 }
658
659 fn next_id(&mut self) -> u64 {
660 let id = self.next_request_id;
661 self.next_request_id += 1;
662 id
663 }
664
665 fn send_request(&mut self, id: u64, method: &str, params: Value) -> Result<()> {
666 send_message(
667 &mut self.stdin,
668 &json!({
669 "jsonrpc":"2.0",
670 "id":id,
671 "method":method,
672 "params":params
673 }),
674 )
675 }
676
677 fn send_notification(&mut self, method: &str, params: Value) -> Result<()> {
678 send_message(
679 &mut self.stdin,
680 &json!({
681 "jsonrpc":"2.0",
682 "method":method,
683 "params":params
684 }),
685 )
686 }
687
688 fn read_response_for_id(&mut self, expected_id: u64) -> Result<Value> {
689 let deadline = Instant::now() + Duration::from_secs(30);
690 let mut discarded = 0u32;
691 const MAX_DISCARDED: u32 = 500;
692
693 loop {
694 let remaining = deadline.saturating_duration_since(Instant::now());
695 if remaining.is_zero() {
696 bail!(
697 "LSP response timeout: no response for request id {expected_id} within 30s \
698 ({discarded} unrelated messages discarded)"
699 );
700 }
701 if discarded >= MAX_DISCARDED {
702 bail!(
703 "LSP response loop: discarded {MAX_DISCARDED} messages without finding id {expected_id}"
704 );
705 }
706
707 if !poll_readable(self.reader.get_ref(), remaining.min(Duration::from_secs(5))) {
709 continue; }
711
712 let message = read_message(&mut self.reader)?;
713 let matches_id = message
714 .get("id")
715 .and_then(Value::as_u64)
716 .map(|id| id == expected_id)
717 .unwrap_or(false);
718 if matches_id {
719 if let Some(error) = message.get("error") {
720 let code = error.get("code").and_then(Value::as_i64).unwrap_or(-1);
721 let error_message = error
722 .get("message")
723 .and_then(Value::as_str)
724 .unwrap_or("unknown LSP error");
725 bail!("LSP request failed ({code}): {error_message}");
726 }
727 return Ok(message);
728 }
729 discarded += 1;
730 }
731 }
732
733 fn shutdown(&mut self) -> Result<()> {
734 let id = self.next_id();
735 self.send_request(id, "shutdown", Value::Null)?;
736 let _ = self.read_response_for_id(id)?;
737 self.send_notification("exit", Value::Null)
738 }
739}
740
741impl Drop for LspSession {
742 fn drop(&mut self) {
743 let _ = self.shutdown();
744 let deadline = Instant::now() + Duration::from_millis(250);
745 while Instant::now() < deadline {
746 match self.child.try_wait() {
747 Ok(Some(_status)) => return,
748 Ok(None) => thread::sleep(Duration::from_millis(10)),
749 Err(_) => break,
750 }
751 }
752 let _ = self.child.kill();
753 let _ = self.child.wait();
754 }
755}