1use anyhow::{Context, Result};
7use lsp_types::{Diagnostic, DiagnosticSeverity, InitializeParams, InitializeResult};
8use serde::Deserialize;
9use serde_json::{json, Value};
10use std::collections::HashMap;
11use std::path::Path;
12use std::process::Stdio;
13use std::sync::atomic::{AtomicU64, Ordering};
14use std::sync::Arc;
15use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
16use tokio::process::{Child, ChildStdin, Command};
17use tokio::sync::{oneshot, Mutex};
18
19type PendingRequests = Arc<Mutex<HashMap<u64, oneshot::Sender<Result<Value>>>>>;
21type DiagnosticsMap = Arc<Mutex<HashMap<String, Vec<Diagnostic>>>>;
23
24#[derive(Debug, Clone)]
26pub struct DocumentSymbolInfo {
27 pub name: String,
29 pub kind: String,
31 pub range: lsp_types::Range,
33 pub detail: Option<String>,
35}
36
37pub struct LspClient {
39 stdin: Option<Arc<Mutex<ChildStdin>>>,
41 request_id: AtomicU64,
43 pending_requests: Arc<Mutex<HashMap<u64, oneshot::Sender<Result<Value>>>>>,
45 diagnostics: Arc<Mutex<HashMap<String, Vec<Diagnostic>>>>,
47 server_name: String,
49 process: Option<Child>,
51 initialized: bool,
53}
54
55impl LspClient {
56 pub fn new(server_name: &str) -> Self {
58 Self {
59 stdin: None,
60 request_id: AtomicU64::new(1),
61 pending_requests: Arc::new(Mutex::new(HashMap::new())),
62 diagnostics: Arc::new(Mutex::new(HashMap::new())),
63 server_name: server_name.to_string(),
64 process: None,
65 initialized: false,
66 }
67 }
68
69 fn get_server_command(server_name: &str) -> Option<(&'static str, Vec<&'static str>)> {
71 match server_name {
72 "rust-analyzer" => Some(("rust-analyzer", vec![])),
73 "pyright" => Some(("pyright-langserver", vec!["--stdio"])),
74 "ty" => Some(("uvx", vec!["ty", "server"])),
76 "typescript" => Some(("typescript-language-server", vec!["--stdio"])),
77 "gopls" => Some(("gopls", vec!["serve"])),
78 _ => None,
79 }
80 }
81
82 pub async fn start(&mut self, workspace_root: &Path) -> Result<()> {
84 let (cmd, args) = Self::get_server_command(&self.server_name)
85 .context(format!("Unknown language server: {}", self.server_name))?;
86
87 log::info!("Starting LSP server: {} {:?}", cmd, args);
88
89 let mut child = Command::new(cmd)
90 .args(&args)
91 .current_dir(workspace_root)
92 .stdin(Stdio::piped())
93 .stdout(Stdio::piped())
94 .stderr(Stdio::piped())
95 .spawn()
96 .context(format!("Failed to start {}", cmd))?;
97
98 let stdin = child.stdin.take().context("No stdin")?;
99 let stdout = child.stdout.take().context("No stdout")?;
100 let stderr = child.stderr.take().context("No stderr")?;
101
102 tokio::spawn(async move {
104 let mut reader = BufReader::new(stderr).lines();
105 while let Ok(Some(line)) = reader.next_line().await {
106 log::debug!("[LSP stderr] {}", line);
107 }
108 });
109
110 let pending_requests = self.pending_requests.clone();
112 let diagnostics = self.diagnostics.clone();
113
114 tokio::spawn(async move {
115 let mut reader = BufReader::new(stdout);
116 loop {
117 let mut content_length = 0;
119 loop {
120 let mut line = String::new();
121 match reader.read_line(&mut line).await {
122 Ok(0) => return, Ok(_) => {
124 if line == "\r\n" {
125 break;
126 }
127 if line.starts_with("Content-Length:") {
128 if let Ok(len) = line
129 .trim_start_matches("Content-Length:")
130 .trim()
131 .parse::<usize>()
132 {
133 content_length = len;
134 }
135 }
136 }
137 Err(e) => {
138 log::error!("Error reading LSP header: {}", e);
139 return;
140 }
141 }
142 }
143
144 if content_length == 0 {
145 continue;
146 }
147
148 let mut body = vec![0u8; content_length];
150 match reader.read_exact(&mut body).await {
151 Ok(_) => {
152 if let Ok(value) = serde_json::from_slice::<Value>(&body) {
153 Self::handle_message(value, &pending_requests, &diagnostics).await;
154 }
155 }
156 Err(e) => {
157 log::error!("Error reading LSP body: {}", e);
158 return;
159 }
160 }
161 }
162 });
163
164 self.stdin = Some(Arc::new(Mutex::new(stdin)));
165 self.process = Some(child);
166 self.initialized = false;
167
168 self.initialize(workspace_root).await?;
170
171 Ok(())
172 }
173
174 async fn handle_message(
176 msg: Value,
177 pending_requests: &PendingRequests,
178 diagnostics: &DiagnosticsMap,
179 ) {
180 if let Some(id) = msg.get("id").and_then(|id| id.as_u64()) {
181 let mut pending = pending_requests.lock().await;
183 if let Some(tx) = pending.remove(&id) {
184 if let Some(error) = msg.get("error") {
185 let _ = tx.send(Err(anyhow::anyhow!("LSP error: {}", error)));
186 } else if let Some(result) = msg.get("result") {
187 let _ = tx.send(Ok(result.clone()));
188 } else {
189 let _ = tx.send(Ok(Value::Null));
190 }
191 }
192 } else if let Some(method) = msg.get("method").and_then(|m| m.as_str()) {
193 if method == "textDocument/publishDiagnostics" {
195 if let Some(params) = msg.get("params") {
196 if let (Some(uri), Some(diags)) = (
197 params.get("uri").and_then(|u| u.as_str()),
198 params.get("diagnostics").and_then(|d| {
199 serde_json::from_value::<Vec<Diagnostic>>(d.clone()).ok()
200 }),
201 ) {
202 let path = uri.trim_start_matches("file://");
204
205 let mut diag_map = diagnostics.lock().await;
208
209 if let Some(filename) = Path::new(path).file_name() {
212 let filename_str = filename.to_string_lossy().to_string();
213 diag_map.insert(filename_str, diags.clone());
214 }
215
216 diag_map.insert(path.to_string(), diags);
217 log::info!("Updated diagnostics for {}", path);
218 }
219 }
220 }
221 }
222 }
223
224 #[allow(deprecated)]
226 async fn initialize(&mut self, workspace_root: &Path) -> Result<InitializeResult> {
227 let path_str = workspace_root.to_string_lossy();
229 #[cfg(target_os = "windows")]
230 let uri_string = format!("file:///{}", path_str.replace('\\', "/"));
231 #[cfg(not(target_os = "windows"))]
232 let uri_string = format!("file://{}", path_str);
233
234 let root_uri: lsp_types::Uri = uri_string
235 .parse()
236 .map_err(|e| anyhow::anyhow!("Failed to parse URI: {:?}", e))?;
237
238 let params = InitializeParams {
239 root_uri: Some(root_uri),
240 capabilities: lsp_types::ClientCapabilities::default(),
241 ..Default::default()
242 };
243
244 let result: InitializeResult = self
245 .send_request("initialize", serde_json::to_value(params)?)
246 .await?;
247
248 self.send_notification("initialized", json!({})).await?;
250 self.initialized = true;
251
252 log::info!("LSP server initialized: {:?}", result.server_info);
253 Ok(result)
254 }
255
256 async fn send_request<T: for<'de> Deserialize<'de>>(
258 &mut self,
259 method: &str,
260 params: Value,
261 ) -> Result<T> {
262 let id = self.request_id.fetch_add(1, Ordering::SeqCst);
263 let (tx, rx) = oneshot::channel();
264
265 {
267 let mut pending = self.pending_requests.lock().await;
268 pending.insert(id, tx);
269 }
270
271 let request = json!({
272 "jsonrpc": "2.0",
273 "id": id,
274 "method": method,
275 "params": params
276 });
277
278 if let Err(e) = self.write_message(&request).await {
279 let mut pending = self.pending_requests.lock().await;
281 pending.remove(&id);
282 return Err(e);
283 }
284
285 let result = rx.await??;
287 Ok(serde_json::from_value(result)?)
288 }
289
290 async fn send_notification(&mut self, method: &str, params: Value) -> Result<()> {
292 let notification = json!({
293 "jsonrpc": "2.0",
294 "method": method,
295 "params": params
296 });
297
298 self.write_message(¬ification).await
299 }
300
301 async fn write_message(&mut self, msg: &Value) -> Result<()> {
303 let content = serde_json::to_string(msg)?;
304 let message = format!("Content-Length: {}\r\n\r\n{}", content.len(), content);
305
306 if let Some(ref stdin_arc) = self.stdin {
307 let mut stdin = stdin_arc.lock().await;
308 stdin.write_all(message.as_bytes()).await?;
309 stdin.flush().await?;
310 Ok(())
311 } else {
312 Err(anyhow::anyhow!("LSP stdin not available"))
313 }
314 }
315
316 pub async fn get_diagnostics(&self, path: &str) -> Vec<Diagnostic> {
319 let map = self.diagnostics.lock().await;
320
321 let mut cached = Vec::new();
323
324 if let Some(diags) = map.get(path) {
326 cached = diags.clone();
327 }
328 else if let Some(diags) = map.get(path.trim_start_matches("file://")) {
330 cached = diags.clone();
331 }
332 else if !path.starts_with("file://") {
334 let uri = format!("file://{}", path);
335 if let Some(diags) = map.get(&uri) {
336 cached = diags.clone();
337 }
338 }
339
340 if cached.is_empty() {
342 if let Some(filename) = Path::new(path).file_name() {
343 let filename_str = filename.to_string_lossy();
344 if let Some(diags) = map.get(filename_str.as_ref()) {
345 cached = diags.clone();
346 }
347 }
348 }
349
350 if !cached.is_empty() {
354 return cached;
355 }
356
357 if self.server_name == "ty" {
361 drop(map);
363 return self.run_type_check(path).await;
364 }
365
366 Vec::new()
367 }
368
369 async fn run_type_check(&self, path: &str) -> Vec<Diagnostic> {
371 use std::process::Command;
372
373 log::debug!("Running ty check on: {}", path);
374
375 let output = Command::new("uvx").args(["ty", "check", path]).output();
377
378 match output {
379 Ok(output) => {
380 let stdout = String::from_utf8_lossy(&output.stdout);
381 let stderr = String::from_utf8_lossy(&output.stderr);
382
383 log::debug!("ty check status: {}", output.status);
384 if !stdout.is_empty() {
385 log::debug!("ty check stdout: {}", stdout);
386 }
387 if !stderr.is_empty() {
388 log::debug!("ty check stderr: {}", stderr);
389 }
390
391 let combined = format!("{}\n{}", stdout, stderr);
393 self.parse_ty_output(&combined, path)
394 }
395 Err(e) => {
396 log::warn!("Failed to run ty check: {}", e);
397 Vec::new()
398 }
399 }
400 }
401
402 fn parse_ty_output(&self, output: &str, _path: &str) -> Vec<Diagnostic> {
404 let mut diagnostics = Vec::new();
405
406 let lines: Vec<&str> = output.lines().collect();
411 let mut i = 0;
412
413 while i < lines.len() {
414 let line = lines[i];
415
416 if line.contains("error") || line.contains("warning") {
418 let severity = if line.contains("error") {
420 Some(DiagnosticSeverity::ERROR)
421 } else if line.contains("warning") {
422 Some(DiagnosticSeverity::WARNING)
423 } else {
424 Some(DiagnosticSeverity::INFORMATION)
425 };
426
427 let message = if let Some(idx) = line.find("]: ") {
430 line[idx + 3..].to_string()
431 } else if let Some(idx) = line.find(": ") {
432 line[idx + 2..].to_string()
433 } else {
434 line.to_string()
435 };
436
437 let mut line_num = 0;
439 let mut col_num = 0;
440
441 for j in 1..4 {
443 if i + j < lines.len() {
444 let next_line = lines[i + j];
445 if next_line.trim().starts_with("-->") {
446 if let Some(parts) = next_line.split("-->").nth(1) {
449 let parts: Vec<&str> = parts.trim().split(':').collect();
450 if parts.len() >= 3 {
451 line_num = parts[1].parse().unwrap_or(0);
453 col_num = parts[2].parse().unwrap_or(0);
454 }
455 }
456 break;
457 }
458 }
459 }
460
461 diagnostics.push(Diagnostic {
462 range: lsp_types::Range {
463 start: lsp_types::Position {
464 line: if line_num > 0 { line_num - 1 } else { 0 },
465 character: if col_num > 0 { col_num - 1 } else { 0 },
466 },
467 end: lsp_types::Position {
468 line: if line_num > 0 { line_num - 1 } else { 0 },
469 character: if col_num > 0 { col_num } else { 1 },
470 },
471 },
472 severity,
473 message,
474 ..Default::default()
475 });
476 }
477
478 i += 1;
479 }
480
481 if !diagnostics.is_empty() {
482 log::info!("ty check found {} diagnostics", diagnostics.len());
483 }
484
485 diagnostics
486 }
487
488 pub fn calculate_syntactic_energy(diagnostics: &[Diagnostic]) -> f32 {
493 diagnostics
494 .iter()
495 .map(|d| match d.severity {
496 Some(DiagnosticSeverity::ERROR) => 1.0,
497 Some(DiagnosticSeverity::WARNING) => 0.1,
498 Some(DiagnosticSeverity::INFORMATION) => 0.01,
499 Some(DiagnosticSeverity::HINT) => 0.001,
500 _ => 0.1, })
502 .sum()
503 }
504
505 pub fn is_ready(&self) -> bool {
507 self.initialized && self.process.is_some()
508 }
509
510 pub async fn did_open(&mut self, path: &std::path::Path, content: &str) -> Result<()> {
512 if !self.is_ready() {
513 return Ok(());
514 }
515
516 let uri = format!("file://{}", path.display());
517 let language_id = match path.extension().and_then(|e| e.to_str()) {
518 Some("py") => "python",
519 Some("rs") => "rust",
520 Some("js") => "javascript",
521 Some("ts") => "typescript",
522 Some("go") => "go",
523 _ => "python", };
525
526 self.send_notification(
527 "textDocument/didOpen",
528 json!({
529 "textDocument": {
530 "uri": uri,
531 "languageId": language_id,
532 "version": 1,
533 "text": content
534 }
535 }),
536 )
537 .await
538 }
539
540 pub async fn did_change(
542 &mut self,
543 path: &std::path::Path,
544 content: &str,
545 version: i32,
546 ) -> Result<()> {
547 if !self.is_ready() {
548 return Ok(());
549 }
550
551 let uri = format!("file://{}", path.display());
552
553 self.send_notification(
554 "textDocument/didChange",
555 json!({
556 "textDocument": {
557 "uri": uri,
558 "version": version
559 },
560 "contentChanges": [{
561 "text": content
562 }]
563 }),
564 )
565 .await
566 }
567
568 pub async fn goto_definition(
575 &mut self,
576 path: &Path,
577 line: u32,
578 character: u32,
579 ) -> Option<Vec<lsp_types::Location>> {
580 if !self.is_ready() {
581 return None;
582 }
583
584 let uri = format!("file://{}", path.display());
585
586 let params = json!({
587 "textDocument": { "uri": uri },
588 "position": { "line": line, "character": character }
589 });
590
591 match self
592 .send_request::<Option<lsp_types::GotoDefinitionResponse>>(
593 "textDocument/definition",
594 params,
595 )
596 .await
597 {
598 Ok(Some(response)) => {
599 match response {
601 lsp_types::GotoDefinitionResponse::Scalar(loc) => Some(vec![loc]),
602 lsp_types::GotoDefinitionResponse::Array(locs) => Some(locs),
603 lsp_types::GotoDefinitionResponse::Link(links) => Some(
604 links
605 .into_iter()
606 .map(|l| lsp_types::Location {
607 uri: l.target_uri,
608 range: l.target_selection_range,
609 })
610 .collect(),
611 ),
612 }
613 }
614 Ok(None) => None,
615 Err(e) => {
616 log::warn!("goto_definition failed: {}", e);
617 None
618 }
619 }
620 }
621
622 pub async fn find_references(
625 &mut self,
626 path: &Path,
627 line: u32,
628 character: u32,
629 include_declaration: bool,
630 ) -> Vec<lsp_types::Location> {
631 if !self.is_ready() {
632 return Vec::new();
633 }
634
635 let uri = format!("file://{}", path.display());
636
637 let params = json!({
638 "textDocument": { "uri": uri },
639 "position": { "line": line, "character": character },
640 "context": { "includeDeclaration": include_declaration }
641 });
642
643 match self
644 .send_request::<Option<Vec<lsp_types::Location>>>("textDocument/references", params)
645 .await
646 {
647 Ok(Some(locs)) => locs,
648 Ok(None) => Vec::new(),
649 Err(e) => {
650 log::warn!("find_references failed: {}", e);
651 Vec::new()
652 }
653 }
654 }
655
656 pub async fn hover(&mut self, path: &Path, line: u32, character: u32) -> Option<String> {
659 if !self.is_ready() {
660 return None;
661 }
662
663 let uri = format!("file://{}", path.display());
664
665 let params = json!({
666 "textDocument": { "uri": uri },
667 "position": { "line": line, "character": character }
668 });
669
670 match self
671 .send_request::<Option<lsp_types::Hover>>("textDocument/hover", params)
672 .await
673 {
674 Ok(Some(hover)) => {
675 match hover.contents {
677 lsp_types::HoverContents::Scalar(content) => {
678 Some(Self::extract_marked_string(&content))
679 }
680 lsp_types::HoverContents::Array(contents) => Some(
681 contents
682 .iter()
683 .map(Self::extract_marked_string)
684 .collect::<Vec<_>>()
685 .join("\n"),
686 ),
687 lsp_types::HoverContents::Markup(markup) => Some(markup.value),
688 }
689 }
690 Ok(None) => None,
691 Err(e) => {
692 log::warn!("hover failed: {}", e);
693 None
694 }
695 }
696 }
697
698 fn extract_marked_string(content: &lsp_types::MarkedString) -> String {
700 match content {
701 lsp_types::MarkedString::String(s) => s.clone(),
702 lsp_types::MarkedString::LanguageString(ls) => {
703 format!("```{}\n{}\n```", ls.language, ls.value)
704 }
705 }
706 }
707
708 pub async fn get_symbols(&mut self, path: &Path) -> Vec<DocumentSymbolInfo> {
711 if !self.is_ready() {
712 return Vec::new();
713 }
714
715 let uri = format!("file://{}", path.display());
716
717 let params = json!({
718 "textDocument": { "uri": uri }
719 });
720
721 match self
722 .send_request::<Option<lsp_types::DocumentSymbolResponse>>(
723 "textDocument/documentSymbol",
724 params,
725 )
726 .await
727 {
728 Ok(Some(response)) => match response {
729 lsp_types::DocumentSymbolResponse::Flat(symbols) => symbols
730 .into_iter()
731 .map(|s| DocumentSymbolInfo {
732 name: s.name,
733 kind: format!("{:?}", s.kind),
734 range: s.location.range,
735 detail: None,
736 })
737 .collect(),
738 lsp_types::DocumentSymbolResponse::Nested(symbols) => {
739 Self::flatten_document_symbols(&symbols)
740 }
741 },
742 Ok(None) => Vec::new(),
743 Err(e) => {
744 log::warn!("get_symbols failed: {}", e);
745 Vec::new()
746 }
747 }
748 }
749
750 fn flatten_document_symbols(symbols: &[lsp_types::DocumentSymbol]) -> Vec<DocumentSymbolInfo> {
752 let mut result = Vec::new();
753 for sym in symbols {
754 result.push(DocumentSymbolInfo {
755 name: sym.name.clone(),
756 kind: format!("{:?}", sym.kind),
757 range: sym.range,
758 detail: sym.detail.clone(),
759 });
760 if let Some(ref children) = sym.children {
762 result.extend(Self::flatten_document_symbols(children));
763 }
764 }
765 result
766 }
767
768 pub async fn shutdown(&mut self) -> Result<()> {
770 if let Some(ref mut process) = self.process {
771 let _ = process.kill().await;
772 }
773 self.process = None;
774 self.initialized = false;
775 Ok(())
776 }
777}
778
779impl Drop for LspClient {
780 fn drop(&mut self) {
781 if let Some(ref mut process) = self.process {
782 drop(process.kill());
783 }
784 }
785}
786
787#[cfg(test)]
788mod tests {
789 use super::*;
790 use lsp_types::Range;
791
792 #[test]
793 fn test_syntactic_energy_calculation() {
794 let diagnostics = vec![
795 Diagnostic {
796 range: Range::default(),
797 severity: Some(DiagnosticSeverity::ERROR),
798 message: "error".to_string(),
799 ..Default::default()
800 },
801 Diagnostic {
802 range: Range::default(),
803 severity: Some(DiagnosticSeverity::WARNING),
804 message: "warning".to_string(),
805 ..Default::default()
806 },
807 ];
808
809 let energy = LspClient::calculate_syntactic_energy(&diagnostics);
810 assert!((energy - 1.1).abs() < 0.001);
811 }
812}