Skip to main content

codelens_engine/lsp/
session.rs

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
39/// P1-1: each live language-server session is wrapped in its own
40/// `Mutex<LspSession>`. The pool itself is a lock-free `DashMap` so that
41/// requests routed to *different* (command, args) sessions (e.g. pyright
42/// vs rust-analyzer in a polyglot monorepo) no longer serialize on a
43/// single pool-level mutex. Requests hitting the **same** session still
44/// take a session-local mutex — the LSP JSON-RPC wire is inherently
45/// serial per stdin/stdout pair.
46pub struct LspSessionPool {
47    project: ProjectRoot,
48    sessions: DashMap<SessionKey, Arc<Mutex<LspSession>>>,
49    /// Per-session readiness state kept alongside `sessions`. Lock-free
50    /// reads so the MCP `get_lsp_readiness` handler can poll at
51    /// 500 ms cadence without contending with the per-session I/O
52    /// mutex. See `lsp::readiness` for the full rationale.
53    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)] // retained for future stderr diagnostics
64    stderr_buffer: std::sync::Arc<std::sync::Mutex<String>>,
65}
66
67/// Known-safe LSP server binaries. Commands not in this list are rejected.
68pub(super) fn is_allowed_lsp_command(command: &str) -> bool {
69    // Extract the binary name from the command path (e.g., "/usr/bin/pyright-langserver" → "pyright-langserver")
70    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    // From LSP_RECIPES
80    "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    // Additional well-known LSP servers
93    "metals",
94    "lua-language-server",
95    "terraform-ls",
96    "yaml-language-server",
97    // Test support: allow python3/python for mock LSP in tests
98    "python3",
99    "python",
100];
101
102/// Fetch an existing live session or start a new one for the given
103/// (command, args). Lock-scope: the DashMap shard lock is held only long
104/// enough to check liveness + swap in a new entry; session execution
105/// itself uses a per-session `Arc<Mutex<…>>` returned to the caller.
106fn 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    // Fast path: check whether a live session already exists.
128    if let Some(existing) = sessions.get(&key) {
129        let arc = existing.clone();
130        drop(existing); // release the DashMap shard read-lock
131        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, // process exited
135                Err(_) => true,            // child gone
136                Ok(None) => false,         // still running
137            }
138        };
139        if !dead {
140            let ready = readiness
141                .get(&key)
142                .map(|r| r.clone())
143                .unwrap_or_else(|| {
144                    // Defensive: if readiness was somehow pruned under
145                    // a live session, reattach a fresh marker rather
146                    // than panic. The observable effect is a reset
147                    // timer for this session, which is benign.
148                    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    // Slow path: spawn a new LSP process. We use `entry(..).or_try_insert_with`
162    // semantics via match so that if two threads race to create the session,
163    // only one spawn actually succeeds.
164    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            // P0-4: insert the readiness row *before* `LspSession::start`
183            // so a slow-to-handshake or failed-to-start LSP still leaves
184            // a visible trail. Without this, a poller for
185            // `get_lsp_readiness` sees `sessions=[]` for the entire
186            // spawn window and cannot distinguish "still warming" from
187            // "failed silently" — which is the exact failure mode the
188            // wait-for-ready feature was built to surface.
189            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                    // Leave the readiness row in place with
203                    // `failure_count > 0 && is_alive=false` so callers
204                    // can distinguish a warming LSP from a dead one.
205                    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    /// Replace the project root and close all existing sessions.
222    pub fn reset(&self, project: ProjectRoot) -> Self {
223        // Drop existing sessions so LSP processes are killed.
224        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    /// Snapshot the per-session readiness state for all currently
234    /// pooled LSP servers. Cheap and lock-free: it allocates a `Vec`
235    /// and clones a handful of atomic counters per session. Intended
236    /// for the MCP `get_lsp_readiness` handler and for bench-harness
237    /// polling loops that need to wait for indexing to complete
238    /// instead of sleeping a fixed duration.
239    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        // Stable ordering: command, then args. Makes test and bench
246        // output deterministic.
247        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                // A rename plan is non-trivial when the server returned
348                // an actual edit block; fall back to counting
349                // current_name so a plain "prepareRename" round-trip
350                // still flips the ready bit when the response is
351                // meaningful.
352                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        // Capture stderr in a background thread (bounded 4KB ring buffer).
376        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            // Poll the pipe before blocking read — prevents infinite hang
708            if !poll_readable(self.reader.get_ref(), remaining.min(Duration::from_secs(5))) {
709                continue; // no data yet, re-check deadline
710            }
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}