perspt_agent/
lsp.rs

1//! Native LSP Client
2//!
3//! Lightweight JSON-RPC client for Language Server Protocol communication.
4//! Provides the "Sensor Architecture" for SRBN stability monitoring.
5
6use 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
19/// Type alias for pending LSP requests map
20type PendingRequests = Arc<Mutex<HashMap<u64, oneshot::Sender<Result<Value>>>>>;
21/// Type alias for diagnostics map
22type DiagnosticsMap = Arc<Mutex<HashMap<String, Vec<Diagnostic>>>>;
23
24/// Simplified document symbol information for agent use
25#[derive(Debug, Clone)]
26pub struct DocumentSymbolInfo {
27    /// Symbol name (e.g., function name, class name)
28    pub name: String,
29    /// Symbol kind (e.g., "Function", "Class", "Variable")
30    pub kind: String,
31    /// Range in the document
32    pub range: lsp_types::Range,
33    /// Optional detail (e.g., type signature)
34    pub detail: Option<String>,
35}
36
37/// LSP Client for real-time diagnostics and symbol information
38pub struct LspClient {
39    /// Server stdin writer
40    stdin: Option<Arc<Mutex<ChildStdin>>>,
41    /// Request ID counter
42    request_id: AtomicU64,
43    /// Pending requests (ID -> Sender)
44    pending_requests: Arc<Mutex<HashMap<u64, oneshot::Sender<Result<Value>>>>>,
45    /// Cached diagnostics per file
46    diagnostics: Arc<Mutex<HashMap<String, Vec<Diagnostic>>>>,
47    /// Server name (e.g., "rust-analyzer", "pyright")
48    server_name: String,
49    /// Keep track of the process to kill it on drop
50    process: Option<Child>,
51    /// Whether the server is initialized
52    initialized: bool,
53}
54
55impl LspClient {
56    /// Create a new LSP client (not connected)
57    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    /// Get the command for a known language server
70    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 is installed via uv, so use uvx to run it
75            "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    /// Start the language server process
83    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        // Handle stderr logging in background
103        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        // Handle stdout message loop
111        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                // Read headers
118                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, // EOF
123                        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                // Read body
149                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        // Send initialize request
169        self.initialize(workspace_root).await?;
170
171        Ok(())
172    }
173
174    /// Handle incoming LSP message
175    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            // It's a response
182            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            // It's a notification
194            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                        // Normalize URI to file path
203                        let path = uri.trim_start_matches("file://");
204
205                        // For ty/lsp, path might be absolute or relative, ensure we match broadly
206                        // Store exactly what we got for now
207                        let mut diag_map = diagnostics.lock().await;
208
209                        // Also try to simplify the key to just filename for easier lookup
210                        // This assumes unique filenames which is a simplification but useful
211                        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    /// Send the initialize request
225    #[allow(deprecated)]
226    async fn initialize(&mut self, workspace_root: &Path) -> Result<InitializeResult> {
227        // Build a file:// URI from the path
228        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        // Send initialized notification
249        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    /// Send a JSON-RPC request
257    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        // Register pending request
266        {
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            // Cleanup on error
280            let mut pending = self.pending_requests.lock().await;
281            pending.remove(&id);
282            return Err(e);
283        }
284
285        // Wait for response
286        let result = rx.await??;
287        Ok(serde_json::from_value(result)?)
288    }
289
290    /// Send a JSON-RPC notification
291    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(&notification).await
299    }
300
301    /// Write a message to the server stdin
302    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    /// Get diagnostics for a file
317    /// Get diagnostics for a file
318    pub async fn get_diagnostics(&self, path: &str) -> Vec<Diagnostic> {
319        let map = self.diagnostics.lock().await;
320
321        // Collect cached diagnostics from any matching key
322        let mut cached = Vec::new();
323
324        // 1. Exact match
325        if let Some(diags) = map.get(path) {
326            cached = diags.clone();
327        }
328        // 2. Clean path (no file://)
329        else if let Some(diags) = map.get(path.trim_start_matches("file://")) {
330            cached = diags.clone();
331        }
332        // 3. URI format
333        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        // 4. Filename fallback (only if still empty/not found)
341        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 we found diagnostics, verify they aren't empty?
351        // Actually, valid code has empty diagnostics.
352        // But for 'ty', we want to be paranoid if it reports nothing.
353        if !cached.is_empty() {
354            return cached;
355        }
356
357        // Trust but Verify: If cached is empty (meaning LSP says "clean" or "unknown"),
358        // AND we are using `ty`, double-check with CLI.
359        // This is crucial for stability monitoring to avoid false negatives from async races.
360        if self.server_name == "ty" {
361            // Drop lock before await
362            drop(map);
363            return self.run_type_check(path).await;
364        }
365
366        Vec::new()
367    }
368
369    /// Run ty check CLI to get diagnostics for a file
370    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        // ty check doesn't support JSON output yet, so we parse the default output
376        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                // Parse the output (ty outputs diagnostics to stderr in text format, but we check both)
392                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    /// Parse ty check text output into diagnostics
403    fn parse_ty_output(&self, output: &str, _path: &str) -> Vec<Diagnostic> {
404        let mut diagnostics = Vec::new();
405
406        // Look for lines like:
407        // error[invalid-return-type]: Return type does not match returned value
408        // ---> main.py:7:12
409
410        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            // Check for error/warning prefix
417            if line.contains("error") || line.contains("warning") {
418                // Heuristic parsing for severity
419                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                // Extract message
428                // Try to find the message part after "error:" or "error[...]:"
429                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                // Look for location in next lines
438                let mut line_num = 0;
439                let mut col_num = 0;
440
441                // Scan up to 3 following lines for location
442                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                            // Parse:   --> main.py:7:12
447                            // or: --> main.py:7:12
448                            if let Some(parts) = next_line.split("-->").nth(1) {
449                                let parts: Vec<&str> = parts.trim().split(':').collect();
450                                if parts.len() >= 3 {
451                                    // parts[0] is filename
452                                    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    /// Calculate syntactic energy from diagnostics
489    ///
490    /// V_syn = sum(severity_weight * count)
491    /// Error = 1.0, Warning = 0.1, Hint = 0.01
492    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, // Default for unknown or None
501            })
502            .sum()
503    }
504
505    /// Check if the server is running and initialized
506    pub fn is_ready(&self) -> bool {
507        self.initialized && self.process.is_some()
508    }
509
510    /// Notify language server that a file was opened
511    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", // Default to python for now instead of plaintext
524        };
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    /// Notify language server that a file changed
541    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    // =========================================================================
569    // Enhanced LSP Tools (PSP-4 Phase 2)
570    // =========================================================================
571
572    /// Go to definition of symbol at position
573    /// Uses textDocument/definition LSP request
574    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                // Convert GotoDefinitionResponse to Vec<Location>
600                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    /// Find all references to symbol at position
623    /// Uses textDocument/references LSP request
624    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    /// Get hover information (type, docs) at position
657    /// Uses textDocument/hover LSP request
658    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                // Extract text from hover contents
676                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    /// Extract text from MarkedString
699    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    /// Get all symbols in a document
709    /// Uses textDocument/documentSymbol LSP request
710    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    /// Flatten nested document symbols into a list
751    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            // Recursively add children
761            if let Some(ref children) = sym.children {
762                result.extend(Self::flatten_document_symbols(children));
763            }
764        }
765        result
766    }
767
768    /// Shutdown the language server
769    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}