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
19use perspt_core::plugin::LspConfig;
20use perspt_sandbox::{ProcessPolicy, ProcessSandbox};
21
22type PendingRequests = Arc<Mutex<HashMap<u64, oneshot::Sender<Result<Value>>>>>;
24type DiagnosticsMap = Arc<Mutex<HashMap<String, Vec<Diagnostic>>>>;
26
27#[derive(Debug, Clone)]
29pub struct DocumentSymbolInfo {
30 pub name: String,
32 pub kind: String,
34 pub range: lsp_types::Range,
36 pub detail: Option<String>,
38}
39
40pub struct LspClient {
42 stdin: Option<Arc<Mutex<ChildStdin>>>,
44 request_id: AtomicU64,
46 pending_requests: Arc<Mutex<HashMap<u64, oneshot::Sender<Result<Value>>>>>,
48 diagnostics: Arc<Mutex<HashMap<String, Vec<Diagnostic>>>>,
50 server_name: String,
52 language_id: String,
54 process: Option<Child>,
56 initialized: bool,
58 allow_unisolated: bool,
60}
61
62impl LspClient {
63 pub fn new(server_name: &str) -> Self {
65 Self {
66 stdin: None,
67 request_id: AtomicU64::new(1),
68 pending_requests: Arc::new(Mutex::new(HashMap::new())),
69 diagnostics: Arc::new(Mutex::new(HashMap::new())),
70 server_name: server_name.to_string(),
71 language_id: String::new(),
72 process: None,
73 initialized: false,
74 allow_unisolated: false,
75 }
76 }
77
78 pub fn from_config(config: &LspConfig) -> Self {
83 Self {
84 stdin: None,
85 request_id: AtomicU64::new(1),
86 pending_requests: Arc::new(Mutex::new(HashMap::new())),
87 diagnostics: Arc::new(Mutex::new(HashMap::new())),
88 server_name: config.server_binary.clone(),
89 language_id: config.language_id.clone(),
90 process: None,
91 initialized: false,
92 allow_unisolated: false,
93 }
94 }
95
96 pub(crate) fn allow_unisolated(mut self, allow: bool) -> Self {
97 self.allow_unisolated = allow;
98 self
99 }
100
101 fn get_server_command(server_name: &str) -> Option<(&'static str, Vec<&'static str>)> {
103 match server_name {
104 "rust-analyzer" => Some(("rust-analyzer", vec![])),
105 "pyright" => Some(("pyright-langserver", vec!["--stdio"])),
106 "ty" => Some(("uvx", vec!["ty", "server"])),
108 "typescript" => Some(("typescript-language-server", vec!["--stdio"])),
109 "gopls" => Some(("gopls", vec!["serve"])),
110 _ => None,
111 }
112 }
113
114 pub async fn start(&mut self, workspace_root: &Path) -> Result<()> {
116 let (cmd, args) = Self::get_server_command(&self.server_name)
117 .context(format!("Unknown language server: {}", self.server_name))?;
118
119 let args_owned: Vec<String> = args.iter().map(|s| s.to_string()).collect();
120 self.start_process(cmd, &args_owned, workspace_root).await
121 }
122
123 pub async fn start_with_config(
128 &mut self,
129 config: &LspConfig,
130 workspace_root: &Path,
131 ) -> Result<()> {
132 self.start_process(&config.server_binary, &config.args, workspace_root)
133 .await
134 }
135
136 async fn start_process(
138 &mut self,
139 cmd: &str,
140 args: &[String],
141 workspace_root: &Path,
142 ) -> Result<()> {
143 log::info!("Starting LSP server: {} {:?}", cmd, args);
144
145 let policy = if self.allow_unisolated {
146 ProcessPolicy::inspection(workspace_root).best_effort()
147 } else {
148 ProcessPolicy::inspection(workspace_root)
149 };
150 let prepared = ProcessSandbox::new(cmd, args.to_vec(), policy)?.prepare_invocation()?;
151 let mut child = Command::new(&prepared.program);
152 child
153 .args(&prepared.args)
154 .current_dir(&prepared.working_dir)
155 .env_clear()
156 .envs(&prepared.environment)
157 .stdin(Stdio::piped())
158 .stdout(Stdio::piped())
159 .stderr(Stdio::piped());
160 let mut child = child.spawn().context(format!("Failed to start {}", cmd))?;
161
162 let stdin = child.stdin.take().context("No stdin")?;
163 let stdout = child.stdout.take().context("No stdout")?;
164 let stderr = child.stderr.take().context("No stderr")?;
165
166 tokio::spawn(async move {
168 let mut reader = BufReader::new(stderr).lines();
169 while let Ok(Some(line)) = reader.next_line().await {
170 log::debug!("[LSP stderr] {}", line);
171 }
172 });
173
174 let pending_requests = self.pending_requests.clone();
176 let diagnostics = self.diagnostics.clone();
177
178 tokio::spawn(async move {
179 let mut reader = BufReader::new(stdout);
180 loop {
181 let mut content_length = 0;
183 loop {
184 let mut line = String::new();
185 match reader.read_line(&mut line).await {
186 Ok(0) => return, Ok(_) => {
188 if line == "\r\n" {
189 break;
190 }
191 if line.starts_with("Content-Length:") {
192 if let Ok(len) = line
193 .trim_start_matches("Content-Length:")
194 .trim()
195 .parse::<usize>()
196 {
197 content_length = len;
198 }
199 }
200 }
201 Err(e) => {
202 log::error!("Error reading LSP header: {}", e);
203 return;
204 }
205 }
206 }
207
208 if content_length == 0 {
209 continue;
210 }
211
212 let mut body = vec![0u8; content_length];
214 match reader.read_exact(&mut body).await {
215 Ok(_) => {
216 if let Ok(value) = serde_json::from_slice::<Value>(&body) {
217 Self::handle_message(value, &pending_requests, &diagnostics).await;
218 }
219 }
220 Err(e) => {
221 log::error!("Error reading LSP body: {}", e);
222 return;
223 }
224 }
225 }
226 });
227
228 self.stdin = Some(Arc::new(Mutex::new(stdin)));
229 self.process = Some(child);
230 self.initialized = false;
231
232 self.initialize(workspace_root).await?;
234
235 Ok(())
236 }
237
238 async fn handle_message(
240 msg: Value,
241 pending_requests: &PendingRequests,
242 diagnostics: &DiagnosticsMap,
243 ) {
244 if let Some(id) = msg.get("id").and_then(|id| id.as_u64()) {
245 let mut pending = pending_requests.lock().await;
247 if let Some(tx) = pending.remove(&id) {
248 if let Some(error) = msg.get("error") {
249 let _ = tx.send(Err(anyhow::anyhow!("LSP error: {}", error)));
250 } else if let Some(result) = msg.get("result") {
251 let _ = tx.send(Ok(result.clone()));
252 } else {
253 let _ = tx.send(Ok(Value::Null));
254 }
255 }
256 } else if let Some(method) = msg.get("method").and_then(|m| m.as_str()) {
257 if method == "textDocument/publishDiagnostics" {
259 if let Some(params) = msg.get("params") {
260 if let (Some(uri), Some(diags)) = (
261 params.get("uri").and_then(|u| u.as_str()),
262 params.get("diagnostics").and_then(|d| {
263 serde_json::from_value::<Vec<Diagnostic>>(d.clone()).ok()
264 }),
265 ) {
266 let path = uri.trim_start_matches("file://");
268
269 let mut diag_map = diagnostics.lock().await;
272
273 if let Some(filename) = Path::new(path).file_name() {
276 let filename_str = filename.to_string_lossy().to_string();
277 diag_map.insert(filename_str, diags.clone());
278 }
279
280 diag_map.insert(path.to_string(), diags);
281 log::info!("Updated diagnostics for {}", path);
282 }
283 }
284 }
285 }
286 }
287
288 #[allow(deprecated)]
290 async fn initialize(&mut self, workspace_root: &Path) -> Result<InitializeResult> {
291 let path_str = workspace_root.to_string_lossy();
293 #[cfg(target_os = "windows")]
294 let uri_string = format!("file:///{}", path_str.replace('\\', "/"));
295 #[cfg(not(target_os = "windows"))]
296 let uri_string = format!("file://{}", path_str);
297
298 let root_uri: lsp_types::Uri = uri_string
299 .parse()
300 .map_err(|e| anyhow::anyhow!("Failed to parse URI: {:?}", e))?;
301
302 let params = InitializeParams {
303 root_uri: Some(root_uri),
304 capabilities: lsp_types::ClientCapabilities::default(),
305 ..Default::default()
306 };
307
308 let result: InitializeResult = self
309 .send_request("initialize", serde_json::to_value(params)?)
310 .await?;
311
312 self.send_notification("initialized", json!({})).await?;
314 self.initialized = true;
315
316 log::info!("LSP server initialized: {:?}", result.server_info);
317 Ok(result)
318 }
319
320 async fn send_request<T: for<'de> Deserialize<'de>>(
322 &mut self,
323 method: &str,
324 params: Value,
325 ) -> Result<T> {
326 let id = self.request_id.fetch_add(1, Ordering::SeqCst);
327 let (tx, rx) = oneshot::channel();
328
329 {
331 let mut pending = self.pending_requests.lock().await;
332 pending.insert(id, tx);
333 }
334
335 let request = json!({
336 "jsonrpc": "2.0",
337 "id": id,
338 "method": method,
339 "params": params
340 });
341
342 if let Err(e) = self.write_message(&request).await {
343 let mut pending = self.pending_requests.lock().await;
345 pending.remove(&id);
346 return Err(e);
347 }
348
349 let result = rx.await??;
351 Ok(serde_json::from_value(result)?)
352 }
353
354 async fn send_notification(&mut self, method: &str, params: Value) -> Result<()> {
356 let notification = json!({
357 "jsonrpc": "2.0",
358 "method": method,
359 "params": params
360 });
361
362 self.write_message(¬ification).await
363 }
364
365 async fn write_message(&mut self, msg: &Value) -> Result<()> {
367 let content = serde_json::to_string(msg)?;
368 let message = format!("Content-Length: {}\r\n\r\n{}", content.len(), content);
369
370 if let Some(ref stdin_arc) = self.stdin {
371 let mut stdin = stdin_arc.lock().await;
372 stdin.write_all(message.as_bytes()).await?;
373 stdin.flush().await?;
374 Ok(())
375 } else {
376 Err(anyhow::anyhow!("LSP stdin not available"))
377 }
378 }
379
380 pub async fn get_diagnostics(&self, path: &str) -> Vec<Diagnostic> {
383 let map = self.diagnostics.lock().await;
384
385 let mut cached = Vec::new();
387
388 if let Some(diags) = map.get(path) {
390 cached = diags.clone();
391 }
392 else if let Some(diags) = map.get(path.trim_start_matches("file://")) {
394 cached = diags.clone();
395 }
396 else if !path.starts_with("file://") {
398 let uri = format!("file://{}", path);
399 if let Some(diags) = map.get(&uri) {
400 cached = diags.clone();
401 }
402 }
403
404 if cached.is_empty() {
406 if let Some(filename) = Path::new(path).file_name() {
407 let filename_str = filename.to_string_lossy();
408 if let Some(diags) = map.get(filename_str.as_ref()) {
409 cached = diags.clone();
410 }
411 }
412 }
413
414 if !cached.is_empty() {
418 return cached;
419 }
420
421 if self.server_name == "ty" {
425 drop(map);
427 return self.run_type_check(path).await;
428 }
429
430 Vec::new()
431 }
432
433 async fn run_type_check(&self, path: &str) -> Vec<Diagnostic> {
435 use std::process::Command;
436
437 log::debug!("Running ty check on: {}", path);
438
439 let output = Command::new("uvx").args(["ty", "check", path]).output();
441
442 match output {
443 Ok(output) => {
444 let stdout = String::from_utf8_lossy(&output.stdout);
445 let stderr = String::from_utf8_lossy(&output.stderr);
446
447 log::debug!("ty check status: {}", output.status);
448 if !stdout.is_empty() {
449 log::debug!("ty check stdout: {}", stdout);
450 }
451 if !stderr.is_empty() {
452 log::debug!("ty check stderr: {}", stderr);
453 }
454
455 let combined = format!("{}\n{}", stdout, stderr);
457 self.parse_ty_output(&combined, path)
458 }
459 Err(e) => {
460 log::warn!("Failed to run ty check: {}", e);
461 Vec::new()
462 }
463 }
464 }
465
466 fn parse_ty_output(&self, output: &str, _path: &str) -> Vec<Diagnostic> {
468 let mut diagnostics = Vec::new();
469
470 let lines: Vec<&str> = output.lines().collect();
475 let mut i = 0;
476
477 while i < lines.len() {
478 let line = lines[i];
479
480 if line.contains("error") || line.contains("warning") {
482 let severity = if line.contains("error") {
484 Some(DiagnosticSeverity::ERROR)
485 } else if line.contains("warning") {
486 Some(DiagnosticSeverity::WARNING)
487 } else {
488 Some(DiagnosticSeverity::INFORMATION)
489 };
490
491 let message = if let Some(idx) = line.find("]: ") {
494 line[idx + 3..].to_string()
495 } else if let Some(idx) = line.find(": ") {
496 line[idx + 2..].to_string()
497 } else {
498 line.to_string()
499 };
500
501 let mut line_num = 0;
503 let mut col_num = 0;
504
505 for j in 1..4 {
507 if i + j < lines.len() {
508 let next_line = lines[i + j];
509 if next_line.trim().starts_with("-->") {
510 if let Some(parts) = next_line.split("-->").nth(1) {
513 let parts: Vec<&str> = parts.trim().split(':').collect();
514 if parts.len() >= 3 {
515 line_num = parts[1].parse().unwrap_or(0);
517 col_num = parts[2].parse().unwrap_or(0);
518 }
519 }
520 break;
521 }
522 }
523 }
524
525 diagnostics.push(Diagnostic {
526 range: lsp_types::Range {
527 start: lsp_types::Position {
528 line: if line_num > 0 { line_num - 1 } else { 0 },
529 character: if col_num > 0 { col_num - 1 } else { 0 },
530 },
531 end: lsp_types::Position {
532 line: if line_num > 0 { line_num - 1 } else { 0 },
533 character: if col_num > 0 { col_num } else { 1 },
534 },
535 },
536 severity,
537 message,
538 ..Default::default()
539 });
540 }
541
542 i += 1;
543 }
544
545 if !diagnostics.is_empty() {
546 log::info!("ty check found {} diagnostics", diagnostics.len());
547 }
548
549 diagnostics
550 }
551
552 pub fn calculate_syntactic_energy(diagnostics: &[Diagnostic]) -> f32 {
557 diagnostics
558 .iter()
559 .map(|d| match d.severity {
560 Some(DiagnosticSeverity::ERROR) => 1.0,
561 Some(DiagnosticSeverity::WARNING) => 0.1,
562 Some(DiagnosticSeverity::INFORMATION) => 0.01,
563 Some(DiagnosticSeverity::HINT) => 0.001,
564 _ => 0.1, })
566 .sum()
567 }
568
569 pub fn is_ready(&self) -> bool {
571 self.initialized && self.process.is_some()
572 }
573
574 pub async fn did_open(&mut self, path: &std::path::Path, content: &str) -> Result<()> {
576 if !self.is_ready() {
577 return Ok(());
578 }
579
580 let uri = format!("file://{}", path.display());
581
582 let language_id = if !self.language_id.is_empty() {
584 self.language_id.as_str()
585 } else {
586 match path.extension().and_then(|e| e.to_str()) {
587 Some("py") => "python",
588 Some("rs") => "rust",
589 Some("js") => "javascript",
590 Some("ts") => "typescript",
591 Some("go") => "go",
592 _ => "plaintext",
593 }
594 };
595
596 self.send_notification(
597 "textDocument/didOpen",
598 json!({
599 "textDocument": {
600 "uri": uri,
601 "languageId": language_id,
602 "version": 1,
603 "text": content
604 }
605 }),
606 )
607 .await
608 }
609
610 pub async fn did_change(
612 &mut self,
613 path: &std::path::Path,
614 content: &str,
615 version: i32,
616 ) -> Result<()> {
617 if !self.is_ready() {
618 return Ok(());
619 }
620
621 let uri = format!("file://{}", path.display());
622
623 self.send_notification(
624 "textDocument/didChange",
625 json!({
626 "textDocument": {
627 "uri": uri,
628 "version": version
629 },
630 "contentChanges": [{
631 "text": content
632 }]
633 }),
634 )
635 .await
636 }
637
638 pub async fn goto_definition(
645 &mut self,
646 path: &Path,
647 line: u32,
648 character: u32,
649 ) -> Option<Vec<lsp_types::Location>> {
650 if !self.is_ready() {
651 return None;
652 }
653
654 let uri = format!("file://{}", path.display());
655
656 let params = json!({
657 "textDocument": { "uri": uri },
658 "position": { "line": line, "character": character }
659 });
660
661 match self
662 .send_request::<Option<lsp_types::GotoDefinitionResponse>>(
663 "textDocument/definition",
664 params,
665 )
666 .await
667 {
668 Ok(Some(response)) => {
669 match response {
671 lsp_types::GotoDefinitionResponse::Scalar(loc) => Some(vec![loc]),
672 lsp_types::GotoDefinitionResponse::Array(locs) => Some(locs),
673 lsp_types::GotoDefinitionResponse::Link(links) => Some(
674 links
675 .into_iter()
676 .map(|l| lsp_types::Location {
677 uri: l.target_uri,
678 range: l.target_selection_range,
679 })
680 .collect(),
681 ),
682 }
683 }
684 Ok(None) => None,
685 Err(e) => {
686 log::warn!("goto_definition failed: {}", e);
687 None
688 }
689 }
690 }
691
692 pub async fn find_references(
695 &mut self,
696 path: &Path,
697 line: u32,
698 character: u32,
699 include_declaration: bool,
700 ) -> Vec<lsp_types::Location> {
701 if !self.is_ready() {
702 return Vec::new();
703 }
704
705 let uri = format!("file://{}", path.display());
706
707 let params = json!({
708 "textDocument": { "uri": uri },
709 "position": { "line": line, "character": character },
710 "context": { "includeDeclaration": include_declaration }
711 });
712
713 match self
714 .send_request::<Option<Vec<lsp_types::Location>>>("textDocument/references", params)
715 .await
716 {
717 Ok(Some(locs)) => locs,
718 Ok(None) => Vec::new(),
719 Err(e) => {
720 log::warn!("find_references failed: {}", e);
721 Vec::new()
722 }
723 }
724 }
725
726 pub async fn hover(&mut self, path: &Path, line: u32, character: u32) -> Option<String> {
729 if !self.is_ready() {
730 return None;
731 }
732
733 let uri = format!("file://{}", path.display());
734
735 let params = json!({
736 "textDocument": { "uri": uri },
737 "position": { "line": line, "character": character }
738 });
739
740 match self
741 .send_request::<Option<lsp_types::Hover>>("textDocument/hover", params)
742 .await
743 {
744 Ok(Some(hover)) => {
745 match hover.contents {
747 lsp_types::HoverContents::Scalar(content) => {
748 Some(Self::extract_marked_string(&content))
749 }
750 lsp_types::HoverContents::Array(contents) => Some(
751 contents
752 .iter()
753 .map(Self::extract_marked_string)
754 .collect::<Vec<_>>()
755 .join("\n"),
756 ),
757 lsp_types::HoverContents::Markup(markup) => Some(markup.value),
758 }
759 }
760 Ok(None) => None,
761 Err(e) => {
762 log::warn!("hover failed: {}", e);
763 None
764 }
765 }
766 }
767
768 fn extract_marked_string(content: &lsp_types::MarkedString) -> String {
770 match content {
771 lsp_types::MarkedString::String(s) => s.clone(),
772 lsp_types::MarkedString::LanguageString(ls) => {
773 format!("```{}\n{}\n```", ls.language, ls.value)
774 }
775 }
776 }
777
778 pub async fn get_symbols(&mut self, path: &Path) -> Vec<DocumentSymbolInfo> {
781 if !self.is_ready() {
782 return Vec::new();
783 }
784
785 let uri = format!("file://{}", path.display());
786
787 let params = json!({
788 "textDocument": { "uri": uri }
789 });
790
791 match self
792 .send_request::<Option<lsp_types::DocumentSymbolResponse>>(
793 "textDocument/documentSymbol",
794 params,
795 )
796 .await
797 {
798 Ok(Some(response)) => match response {
799 lsp_types::DocumentSymbolResponse::Flat(symbols) => symbols
800 .into_iter()
801 .map(|s| DocumentSymbolInfo {
802 name: s.name,
803 kind: format!("{:?}", s.kind),
804 range: s.location.range,
805 detail: None,
806 })
807 .collect(),
808 lsp_types::DocumentSymbolResponse::Nested(symbols) => {
809 Self::flatten_document_symbols(&symbols)
810 }
811 },
812 Ok(None) => Vec::new(),
813 Err(e) => {
814 log::warn!("get_symbols failed: {}", e);
815 Vec::new()
816 }
817 }
818 }
819
820 fn flatten_document_symbols(symbols: &[lsp_types::DocumentSymbol]) -> Vec<DocumentSymbolInfo> {
822 let mut result = Vec::new();
823 for sym in symbols {
824 result.push(DocumentSymbolInfo {
825 name: sym.name.clone(),
826 kind: format!("{:?}", sym.kind),
827 range: sym.range,
828 detail: sym.detail.clone(),
829 });
830 if let Some(ref children) = sym.children {
832 result.extend(Self::flatten_document_symbols(children));
833 }
834 }
835 result
836 }
837
838 pub async fn shutdown(&mut self) -> Result<()> {
840 if let Some(ref mut process) = self.process {
841 let _ = process.kill().await;
842 }
843 self.process = None;
844 self.initialized = false;
845 Ok(())
846 }
847}
848
849impl Drop for LspClient {
850 fn drop(&mut self) {
851 if let Some(ref mut process) = self.process {
852 let _ = process.start_kill();
853 }
854 }
855}
856
857#[cfg(test)]
858mod tests {
859 use super::*;
860 use lsp_types::Range;
861
862 #[test]
863 fn test_syntactic_energy_calculation() {
864 let diagnostics = vec![
865 Diagnostic {
866 range: Range::default(),
867 severity: Some(DiagnosticSeverity::ERROR),
868 message: "error".to_string(),
869 ..Default::default()
870 },
871 Diagnostic {
872 range: Range::default(),
873 severity: Some(DiagnosticSeverity::WARNING),
874 message: "warning".to_string(),
875 ..Default::default()
876 },
877 ];
878
879 let energy = LspClient::calculate_syntactic_energy(&diagnostics);
880 assert!((energy - 1.1).abs() < 0.001);
881 }
882}