Skip to main content

fresh/services/lsp/
async_handler.rs

1//! Async LSP Client using Tokio
2//!
3//! This module implements an asynchronous LSP client that:
4//! - Runs in a separate Tokio task
5//! - Uses tokio::process for async process I/O
6//! - Sends notifications to main loop via AsyncBridge
7//! - Handles LSP notifications asynchronously (diagnostics, etc.)
8//!
9//! Architecture:
10//! - LspTask: Async task that manages LSP process and I/O
11//! - LspHandle: Sync handle that can send commands to the task
12//! - Uses tokio channels for command/response communication
13
14use crate::services::async_bridge::{
15    AsyncBridge, AsyncMessage, LspMessageType, LspProgressValue, LspSemanticTokensResponse,
16    LspServerStatus,
17};
18use crate::services::process_limits::ProcessLimits;
19use lsp_types::{
20    notification::{
21        DidChangeTextDocument, DidCloseTextDocument, DidOpenTextDocument, DidSaveTextDocument,
22        Initialized, Notification, PublishDiagnostics,
23    },
24    request::{Initialize, Request},
25    ClientCapabilities, DidChangeTextDocumentParams, DidCloseTextDocumentParams,
26    DidOpenTextDocumentParams, DidSaveTextDocumentParams, InitializeParams, InitializeResult,
27    InitializedParams, PublishDiagnosticsParams, SemanticTokenModifier, SemanticTokenType,
28    SemanticTokensClientCapabilities, SemanticTokensClientCapabilitiesRequests,
29    SemanticTokensFullOptions, SemanticTokensLegend, SemanticTokensParams, SemanticTokensResult,
30    SemanticTokensServerCapabilities, ServerCapabilities, TextDocumentContentChangeEvent,
31    TextDocumentIdentifier, TextDocumentItem, TokenFormat, Uri, VersionedTextDocumentIdentifier,
32    WindowClientCapabilities, WorkspaceFolder,
33};
34use serde::{Deserialize, Serialize};
35use serde_json::Value;
36use std::collections::HashMap;
37use std::path::PathBuf;
38use std::sync::atomic::{AtomicBool, Ordering};
39use std::sync::{mpsc as std_mpsc, Arc, Mutex};
40use std::time::Instant;
41use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
42use tokio::process::{Child, ChildStdin, ChildStdout, Command};
43use tokio::sync::{mpsc, oneshot};
44
45/// Grace period after didOpen before sending didChange (in milliseconds)
46/// This gives the LSP server time to process didOpen before receiving changes
47const DID_OPEN_GRACE_PERIOD_MS: u64 = 200;
48
49/// LSP error codes that should be silently discarded per the LSP spec.
50///
51/// From [LSP 3.17 specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/):
52/// - ContentModified (-32801): "If clients receive a ContentModified error,
53///   it generally should not show it in the UI for the end-user."
54/// - ServerCancelled (-32802): Server cancelled the request (e.g. due to newer request).
55///
56/// These are normal during editing and all major editors (VS Code, Neovim) suppress them.
57const LSP_ERROR_CONTENT_MODIFIED: i64 = -32801;
58const LSP_ERROR_SERVER_CANCELLED: i64 = -32802;
59
60/// Check if a document is already open and should skip didOpen.
61/// Returns true if the document is already open (should skip), false if it should proceed.
62fn should_skip_did_open(
63    document_versions: &Arc<std::sync::Mutex<HashMap<PathBuf, i64>>>,
64    path: &PathBuf,
65    language: &str,
66    uri: &Uri,
67) -> bool {
68    if document_versions.lock().unwrap().contains_key(path) {
69        tracing::debug!(
70            "LSP ({}): skipping didOpen - document already open: {}",
71            language,
72            uri.as_str()
73        );
74        true
75    } else {
76        false
77    }
78}
79
80/// A JSON-RPC message
81#[derive(Debug, Clone, Serialize, Deserialize)]
82#[serde(untagged)]
83pub enum JsonRpcMessage {
84    Request(JsonRpcRequest),
85    Response(JsonRpcResponse),
86    Notification(JsonRpcNotification),
87}
88
89/// A JSON-RPC request
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct JsonRpcRequest {
92    pub jsonrpc: String,
93    pub id: i64,
94    pub method: String,
95    pub params: Option<Value>,
96}
97
98/// A JSON-RPC response
99#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct JsonRpcResponse {
101    pub jsonrpc: String,
102    pub id: i64,
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub result: Option<Value>,
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub error: Option<JsonRpcError>,
107}
108
109/// A JSON-RPC notification (no response expected)
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct JsonRpcNotification {
112    pub jsonrpc: String,
113    pub method: String,
114    pub params: Option<Value>,
115}
116
117/// A JSON-RPC error
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct JsonRpcError {
120    pub code: i64,
121    pub message: String,
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub data: Option<Value>,
124}
125
126/// LSP client state machine
127///
128/// Tracks the lifecycle of the LSP client connection with proper state transitions.
129/// This prevents invalid operations (e.g., can't initialize twice, can't send requests when stopped).
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub enum LspClientState {
132    /// Initial state before spawning
133    Initial,
134    /// Process spawning in progress
135    Starting,
136    /// Initialize request sent, waiting for response
137    Initializing,
138    /// Initialized and ready for requests
139    Running,
140    /// Shutdown in progress
141    Stopping,
142    /// Cleanly stopped
143    Stopped,
144    /// Failed or crashed
145    Error,
146}
147
148impl LspClientState {
149    /// Check if this state can transition to another state
150    pub fn can_transition_to(&self, next: LspClientState) -> bool {
151        use LspClientState::*;
152        match (self, next) {
153            // From Initial, can only start
154            (Initial, Starting) => true,
155            // From Starting, can initialize or error
156            (Starting, Initializing) | (Starting, Error) => true,
157            // From Initializing, can become running or error
158            (Initializing, Running) | (Initializing, Error) => true,
159            // From Running, can stop or error
160            (Running, Stopping) | (Running, Error) => true,
161            // From Stopping, can become stopped or error
162            (Stopping, Stopped) | (Stopping, Error) => true,
163            // From Stopped or Error, can restart
164            (Stopped, Starting) | (Error, Starting) => true,
165            // Any state can become error
166            (_, Error) => true,
167            // Same state is always valid (no-op)
168            (a, b) if *a == b => true,
169            // All other transitions are invalid
170            _ => false,
171        }
172    }
173
174    /// Transition to a new state, returning error if invalid
175    pub fn transition_to(&mut self, next: LspClientState) -> Result<(), String> {
176        if self.can_transition_to(next) {
177            *self = next;
178            Ok(())
179        } else {
180            Err(format!(
181                "Invalid state transition from {:?} to {:?}",
182                self, next
183            ))
184        }
185    }
186
187    /// Check if the client is ready to send requests
188    pub fn can_send_requests(&self) -> bool {
189        matches!(self, Self::Running)
190    }
191
192    /// Check if the client can accept initialization
193    pub fn can_initialize(&self) -> bool {
194        matches!(self, Self::Initial | Self::Starting | Self::Stopped)
195    }
196
197    /// Convert to LspServerStatus for UI reporting
198    pub fn to_server_status(&self) -> LspServerStatus {
199        match self {
200            Self::Initial => LspServerStatus::Starting,
201            Self::Starting => LspServerStatus::Starting,
202            Self::Initializing => LspServerStatus::Initializing,
203            Self::Running => LspServerStatus::Running,
204            Self::Stopping => LspServerStatus::Shutdown,
205            Self::Stopped => LspServerStatus::Shutdown,
206            Self::Error => LspServerStatus::Error,
207        }
208    }
209}
210
211/// Create common LSP client capabilities with workDoneProgress support
212fn create_client_capabilities() -> ClientCapabilities {
213    use lsp_types::{
214        CodeActionClientCapabilities, CompletionClientCapabilities, DiagnosticClientCapabilities,
215        DiagnosticTag, DynamicRegistrationClientCapabilities, FoldingRangeCapability,
216        FoldingRangeClientCapabilities, FoldingRangeKind, FoldingRangeKindCapability,
217        GeneralClientCapabilities, GotoCapability, HoverClientCapabilities,
218        InlayHintClientCapabilities, MarkupKind, PublishDiagnosticsClientCapabilities,
219        RenameClientCapabilities, SignatureHelpClientCapabilities, TagSupport,
220        TextDocumentClientCapabilities, TextDocumentSyncClientCapabilities,
221        WorkspaceClientCapabilities, WorkspaceEditClientCapabilities,
222    };
223
224    ClientCapabilities {
225        window: Some(WindowClientCapabilities {
226            work_done_progress: Some(true),
227            ..Default::default()
228        }),
229        workspace: Some(WorkspaceClientCapabilities {
230            apply_edit: Some(true),
231            workspace_edit: Some(WorkspaceEditClientCapabilities {
232                document_changes: Some(true),
233                ..Default::default()
234            }),
235            workspace_folders: Some(true),
236            ..Default::default()
237        }),
238        text_document: Some(TextDocumentClientCapabilities {
239            synchronization: Some(TextDocumentSyncClientCapabilities {
240                did_save: Some(true),
241                ..Default::default()
242            }),
243            completion: Some(CompletionClientCapabilities {
244                ..Default::default()
245            }),
246            hover: Some(HoverClientCapabilities {
247                content_format: Some(vec![MarkupKind::Markdown, MarkupKind::PlainText]),
248                ..Default::default()
249            }),
250            signature_help: Some(SignatureHelpClientCapabilities {
251                ..Default::default()
252            }),
253            definition: Some(GotoCapability {
254                link_support: Some(true),
255                ..Default::default()
256            }),
257            references: Some(DynamicRegistrationClientCapabilities::default()),
258            code_action: Some(CodeActionClientCapabilities {
259                ..Default::default()
260            }),
261            rename: Some(RenameClientCapabilities {
262                dynamic_registration: Some(true),
263                prepare_support: Some(true),
264                honors_change_annotations: Some(true),
265                ..Default::default()
266            }),
267            publish_diagnostics: Some(PublishDiagnosticsClientCapabilities {
268                related_information: Some(true),
269                tag_support: Some(TagSupport {
270                    value_set: vec![DiagnosticTag::UNNECESSARY, DiagnosticTag::DEPRECATED],
271                }),
272                version_support: Some(true),
273                code_description_support: Some(true),
274                data_support: Some(true),
275            }),
276            inlay_hint: Some(InlayHintClientCapabilities {
277                ..Default::default()
278            }),
279            diagnostic: Some(DiagnosticClientCapabilities {
280                ..Default::default()
281            }),
282            folding_range: Some(FoldingRangeClientCapabilities {
283                dynamic_registration: Some(true),
284                line_folding_only: Some(true),
285                folding_range_kind: Some(FoldingRangeKindCapability {
286                    value_set: Some(vec![
287                        FoldingRangeKind::Comment,
288                        FoldingRangeKind::Imports,
289                        FoldingRangeKind::Region,
290                    ]),
291                }),
292                folding_range: Some(FoldingRangeCapability {
293                    collapsed_text: Some(true),
294                }),
295                ..Default::default()
296            }),
297            semantic_tokens: Some(SemanticTokensClientCapabilities {
298                dynamic_registration: Some(true),
299                requests: SemanticTokensClientCapabilitiesRequests {
300                    range: Some(true),
301                    full: Some(SemanticTokensFullOptions::Delta { delta: Some(true) }),
302                },
303                token_types: vec![
304                    SemanticTokenType::NAMESPACE,
305                    SemanticTokenType::TYPE,
306                    SemanticTokenType::CLASS,
307                    SemanticTokenType::ENUM,
308                    SemanticTokenType::INTERFACE,
309                    SemanticTokenType::STRUCT,
310                    SemanticTokenType::TYPE_PARAMETER,
311                    SemanticTokenType::PARAMETER,
312                    SemanticTokenType::VARIABLE,
313                    SemanticTokenType::PROPERTY,
314                    SemanticTokenType::ENUM_MEMBER,
315                    SemanticTokenType::EVENT,
316                    SemanticTokenType::FUNCTION,
317                    SemanticTokenType::METHOD,
318                    SemanticTokenType::MACRO,
319                    SemanticTokenType::KEYWORD,
320                    SemanticTokenType::MODIFIER,
321                    SemanticTokenType::COMMENT,
322                    SemanticTokenType::STRING,
323                    SemanticTokenType::NUMBER,
324                    SemanticTokenType::REGEXP,
325                    SemanticTokenType::OPERATOR,
326                    SemanticTokenType::DECORATOR,
327                ],
328                token_modifiers: vec![
329                    SemanticTokenModifier::DECLARATION,
330                    SemanticTokenModifier::DEFINITION,
331                    SemanticTokenModifier::READONLY,
332                    SemanticTokenModifier::STATIC,
333                    SemanticTokenModifier::DEPRECATED,
334                    SemanticTokenModifier::ABSTRACT,
335                    SemanticTokenModifier::ASYNC,
336                    SemanticTokenModifier::MODIFICATION,
337                    SemanticTokenModifier::DOCUMENTATION,
338                    SemanticTokenModifier::DEFAULT_LIBRARY,
339                ],
340                formats: vec![TokenFormat::RELATIVE],
341                overlapping_token_support: Some(true),
342                multiline_token_support: Some(true),
343                server_cancel_support: Some(true),
344                augments_syntax_tokens: Some(true),
345            }),
346            ..Default::default()
347        }),
348        general: Some(GeneralClientCapabilities {
349            ..Default::default()
350        }),
351        // Enable rust-analyzer experimental features
352        experimental: Some(serde_json::json!({
353            "serverStatusNotification": true
354        })),
355        ..Default::default()
356    }
357}
358
359fn extract_semantic_token_capability(
360    capabilities: &ServerCapabilities,
361) -> (Option<SemanticTokensLegend>, bool, bool, bool) {
362    capabilities
363        .semantic_tokens_provider
364        .as_ref()
365        .map(|provider| match provider {
366            SemanticTokensServerCapabilities::SemanticTokensOptions(options) => (
367                Some(options.legend.clone()),
368                semantic_tokens_full_supported(&options.full),
369                semantic_tokens_full_delta_supported(&options.full),
370                options.range.unwrap_or(false),
371            ),
372            SemanticTokensServerCapabilities::SemanticTokensRegistrationOptions(options) => {
373                let legend = options.semantic_tokens_options.legend.clone();
374                let full = semantic_tokens_full_supported(&options.semantic_tokens_options.full);
375                let delta =
376                    semantic_tokens_full_delta_supported(&options.semantic_tokens_options.full);
377                let range = options.semantic_tokens_options.range.unwrap_or(false);
378                (Some(legend), full, delta, range)
379            }
380        })
381        .unwrap_or((None, false, false, false))
382}
383
384fn semantic_tokens_full_supported(full: &Option<SemanticTokensFullOptions>) -> bool {
385    match full {
386        Some(SemanticTokensFullOptions::Bool(v)) => *v,
387        Some(SemanticTokensFullOptions::Delta { .. }) => true,
388        None => false,
389    }
390}
391
392fn semantic_tokens_full_delta_supported(full: &Option<SemanticTokensFullOptions>) -> bool {
393    match full {
394        Some(SemanticTokensFullOptions::Delta { delta }) => delta.unwrap_or(false),
395        _ => false,
396    }
397}
398
399fn folding_ranges_supported(capabilities: &ServerCapabilities) -> bool {
400    match capabilities.folding_range_provider.as_ref() {
401        Some(lsp_types::FoldingRangeProviderCapability::Simple(v)) => *v,
402        Some(_) => true,
403        None => false,
404    }
405}
406
407/// Commands sent from the main loop to the LSP task
408#[derive(Debug)]
409enum LspCommand {
410    /// Initialize the server
411    Initialize {
412        root_uri: Option<Uri>,
413        initialization_options: Option<Value>,
414        response: oneshot::Sender<Result<InitializeResult, String>>,
415    },
416
417    /// Notify document opened
418    DidOpen {
419        uri: Uri,
420        text: String,
421        language_id: String,
422    },
423
424    /// Notify document changed
425    DidChange {
426        uri: Uri,
427        content_changes: Vec<TextDocumentContentChangeEvent>,
428    },
429
430    /// Notify document closed
431    DidClose { uri: Uri },
432
433    /// Notify document saved
434    DidSave { uri: Uri, text: Option<String> },
435
436    /// Notify workspace folders changed
437    DidChangeWorkspaceFolders {
438        added: Vec<lsp_types::WorkspaceFolder>,
439        removed: Vec<lsp_types::WorkspaceFolder>,
440    },
441
442    /// Request completion at position
443    Completion {
444        request_id: u64,
445        uri: Uri,
446        line: u32,
447        character: u32,
448    },
449
450    /// Request go-to-definition
451    GotoDefinition {
452        request_id: u64,
453        uri: Uri,
454        line: u32,
455        character: u32,
456    },
457
458    /// Request rename
459    Rename {
460        request_id: u64,
461        uri: Uri,
462        line: u32,
463        character: u32,
464        new_name: String,
465    },
466
467    /// Request hover documentation
468    Hover {
469        request_id: u64,
470        uri: Uri,
471        line: u32,
472        character: u32,
473    },
474
475    /// Request find references
476    References {
477        request_id: u64,
478        uri: Uri,
479        line: u32,
480        character: u32,
481    },
482
483    /// Request signature help
484    SignatureHelp {
485        request_id: u64,
486        uri: Uri,
487        line: u32,
488        character: u32,
489    },
490
491    /// Request code actions
492    CodeActions {
493        request_id: u64,
494        uri: Uri,
495        start_line: u32,
496        start_char: u32,
497        end_line: u32,
498        end_char: u32,
499        diagnostics: Vec<lsp_types::Diagnostic>,
500    },
501
502    /// Request document diagnostics (pull model)
503    DocumentDiagnostic {
504        request_id: u64,
505        uri: Uri,
506        /// Previous result_id for incremental updates (None for full refresh)
507        previous_result_id: Option<String>,
508    },
509
510    /// Request inlay hints for a range (LSP 3.17+)
511    InlayHints {
512        request_id: u64,
513        uri: Uri,
514        /// Range to get hints for (typically viewport)
515        start_line: u32,
516        start_char: u32,
517        end_line: u32,
518        end_char: u32,
519    },
520
521    /// Request folding ranges for a document
522    FoldingRange { request_id: u64, uri: Uri },
523
524    /// Request semantic tokens for the entire document
525    SemanticTokensFull { request_id: u64, uri: Uri },
526
527    /// Request semantic tokens delta for the entire document
528    SemanticTokensFullDelta {
529        request_id: u64,
530        uri: Uri,
531        previous_result_id: String,
532    },
533
534    /// Request semantic tokens for a range
535    SemanticTokensRange {
536        request_id: u64,
537        uri: Uri,
538        range: lsp_types::Range,
539    },
540
541    /// Cancel a pending request
542    CancelRequest {
543        /// Editor's request ID to cancel
544        request_id: u64,
545    },
546
547    /// Custom request initiated by a plugin
548    PluginRequest {
549        request_id: u64,
550        method: String,
551        params: Option<Value>,
552    },
553
554    /// Shutdown the server
555    Shutdown,
556}
557
558/// Mutable state for LSP command processing
559struct LspState {
560    /// Stdin for sending messages (shared with stdout reader for server responses)
561    stdin: Arc<tokio::sync::Mutex<ChildStdin>>,
562
563    /// Next request ID
564    next_id: i64,
565
566    /// Server capabilities
567    capabilities: Option<ServerCapabilities>,
568
569    /// Document versions (shared with stdout reader for stale diagnostic filtering)
570    document_versions: Arc<std::sync::Mutex<HashMap<PathBuf, i64>>>,
571
572    /// Track when didOpen was sent for each document to avoid race with didChange
573    /// The LSP server needs time to process didOpen before it can handle didChange
574    pending_opens: HashMap<PathBuf, Instant>,
575
576    /// Whether initialized
577    initialized: bool,
578
579    /// Sender for async messages to main loop
580    async_tx: std_mpsc::Sender<AsyncMessage>,
581
582    /// Language ID (for error reporting)
583    language: String,
584
585    /// Mapping from editor request_id to LSP JSON-RPC id for cancellation
586    /// Key: editor request_id, Value: LSP JSON-RPC id
587    active_requests: HashMap<u64, i64>,
588
589    /// Extension-to-languageId overrides for textDocument/didOpen
590    language_id_overrides: HashMap<String, String>,
591}
592
593// Channel sends (`async_tx.send()`) throughout LspState are best-effort: if the receiver
594// (main editor loop) has been dropped, the editor is shutting down and there is nothing
595// to do with the error. Handler method results (`handle_*`) are similarly safe to discard
596// since errors are already logged within those methods. State transitions in error-handling
597// paths are secondary to the actual error being handled.
598#[allow(clippy::let_underscore_must_use)]
599impl LspState {
600    /// Replay pending commands that were queued before initialization
601    #[allow(clippy::type_complexity)]
602    async fn replay_pending_commands(
603        &mut self,
604        commands: Vec<LspCommand>,
605        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
606    ) {
607        if commands.is_empty() {
608            return;
609        }
610        tracing::info!(
611            "Replaying {} pending commands after initialization",
612            commands.len()
613        );
614        for cmd in commands {
615            match cmd {
616                LspCommand::DidOpen {
617                    uri,
618                    text,
619                    language_id,
620                } => {
621                    tracing::info!("Replaying DidOpen for {}", uri.as_str());
622                    let _ = self
623                        .handle_did_open_sequential(uri, text, language_id, pending)
624                        .await;
625                }
626                LspCommand::DidChange {
627                    uri,
628                    content_changes,
629                } => {
630                    tracing::info!("Replaying DidChange for {}", uri.as_str());
631                    let _ = self
632                        .handle_did_change_sequential(uri, content_changes, pending)
633                        .await;
634                }
635                LspCommand::DidClose { uri } => {
636                    tracing::info!("Replaying DidClose for {}", uri.as_str());
637                    let _ = self.handle_did_close(uri).await;
638                }
639                LspCommand::DidSave { uri, text } => {
640                    tracing::info!("Replaying DidSave for {}", uri.as_str());
641                    let _ = self.handle_did_save(uri, text).await;
642                }
643                LspCommand::DidChangeWorkspaceFolders { added, removed } => {
644                    tracing::info!(
645                        "Replaying DidChangeWorkspaceFolders: +{} -{}",
646                        added.len(),
647                        removed.len()
648                    );
649                    let _ = self
650                        .send_notification::<lsp_types::notification::DidChangeWorkspaceFolders>(
651                            lsp_types::DidChangeWorkspaceFoldersParams {
652                                event: lsp_types::WorkspaceFoldersChangeEvent { added, removed },
653                            },
654                        )
655                        .await;
656                }
657                LspCommand::SemanticTokensFull { request_id, uri } => {
658                    tracing::info!("Replaying semantic tokens request for {}", uri.as_str());
659                    let _ = self
660                        .handle_semantic_tokens_full(request_id, uri, pending)
661                        .await;
662                }
663                LspCommand::SemanticTokensFullDelta {
664                    request_id,
665                    uri,
666                    previous_result_id,
667                } => {
668                    tracing::info!(
669                        "Replaying semantic tokens delta request for {}",
670                        uri.as_str()
671                    );
672                    let _ = self
673                        .handle_semantic_tokens_full_delta(
674                            request_id,
675                            uri,
676                            previous_result_id,
677                            pending,
678                        )
679                        .await;
680                }
681                LspCommand::SemanticTokensRange {
682                    request_id,
683                    uri,
684                    range,
685                } => {
686                    tracing::info!(
687                        "Replaying semantic tokens range request for {}",
688                        uri.as_str()
689                    );
690                    let _ = self
691                        .handle_semantic_tokens_range(request_id, uri, range, pending)
692                        .await;
693                }
694                LspCommand::FoldingRange { request_id, uri } => {
695                    tracing::info!("Replaying folding range request for {}", uri.as_str());
696                    let _ = self.handle_folding_ranges(request_id, uri, pending).await;
697                }
698                _ => {}
699            }
700        }
701    }
702
703    /// Write a message to stdin
704    async fn write_message<T: Serialize>(&mut self, message: &T) -> Result<(), String> {
705        let json =
706            serde_json::to_string(message).map_err(|e| format!("Serialization error: {}", e))?;
707
708        let content = format!("Content-Length: {}\r\n\r\n{}", json.len(), json);
709
710        tracing::trace!("Writing LSP message to stdin ({} bytes)", content.len());
711
712        let mut stdin = self.stdin.lock().await;
713        stdin
714            .write_all(content.as_bytes())
715            .await
716            .map_err(|e| format!("Failed to write to stdin: {}", e))?;
717
718        stdin
719            .flush()
720            .await
721            .map_err(|e| format!("Failed to flush stdin: {}", e))?;
722
723        tracing::trace!("Successfully sent LSP message");
724
725        Ok(())
726    }
727
728    /// Send a notification using lsp-types Notification trait (type-safe)
729    async fn send_notification<N>(&mut self, params: N::Params) -> Result<(), String>
730    where
731        N: Notification,
732    {
733        let notification = JsonRpcNotification {
734            jsonrpc: "2.0".to_string(),
735            method: N::METHOD.to_string(),
736            params: Some(
737                serde_json::to_value(params)
738                    .map_err(|e| format!("Failed to serialize params: {}", e))?,
739            ),
740        };
741
742        self.write_message(&notification).await
743    }
744
745    /// Send request using shared pending map
746    #[allow(clippy::type_complexity)]
747    async fn send_request_sequential<P: Serialize, R: for<'de> Deserialize<'de>>(
748        &mut self,
749        method: &str,
750        params: Option<P>,
751        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
752    ) -> Result<R, String> {
753        self.send_request_sequential_tracked(method, params, pending, None)
754            .await
755    }
756
757    /// Send request using shared pending map with optional editor request tracking
758    #[allow(clippy::type_complexity)]
759    async fn send_request_sequential_tracked<P: Serialize, R: for<'de> Deserialize<'de>>(
760        &mut self,
761        method: &str,
762        params: Option<P>,
763        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
764        editor_request_id: Option<u64>,
765    ) -> Result<R, String> {
766        let id = self.next_id;
767        self.next_id += 1;
768
769        // Track the mapping if editor_request_id is provided
770        if let Some(editor_id) = editor_request_id {
771            self.active_requests.insert(editor_id, id);
772            tracing::trace!("Tracking request: editor_id={}, lsp_id={}", editor_id, id);
773        }
774
775        let params_value = params
776            .map(|p| serde_json::to_value(p))
777            .transpose()
778            .map_err(|e| format!("Failed to serialize params: {}", e))?;
779        let request = JsonRpcRequest {
780            jsonrpc: "2.0".to_string(),
781            id,
782            method: method.to_string(),
783            params: params_value,
784        };
785
786        let (tx, rx) = oneshot::channel();
787        pending.lock().unwrap().insert(id, tx);
788
789        self.write_message(&request).await?;
790
791        tracing::trace!("Sent LSP request id={}, waiting for response...", id);
792
793        // Await response (this is OK now because the reader task will send it)
794        let result = rx
795            .await
796            .map_err(|_| "Response channel closed".to_string())??;
797
798        tracing::trace!("Received LSP response for request id={}", id);
799
800        // Remove tracking after response received
801        if let Some(editor_id) = editor_request_id {
802            self.active_requests.remove(&editor_id);
803            tracing::trace!("Completed request: editor_id={}, lsp_id={}", editor_id, id);
804        }
805
806        serde_json::from_value(result).map_err(|e| format!("Failed to deserialize response: {}", e))
807    }
808
809    /// Handle initialize command
810    #[allow(clippy::type_complexity)]
811    async fn handle_initialize_sequential(
812        &mut self,
813        root_uri: Option<Uri>,
814        initialization_options: Option<Value>,
815        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
816    ) -> Result<InitializeResult, String> {
817        tracing::info!(
818            "Initializing async LSP server with root_uri: {:?}, initialization_options: {:?}",
819            root_uri,
820            initialization_options
821        );
822
823        let workspace_folders = root_uri.as_ref().map(|uri| {
824            vec![WorkspaceFolder {
825                uri: uri.clone(),
826                name: uri
827                    .path()
828                    .as_str()
829                    .split('/')
830                    .next_back()
831                    .unwrap_or("workspace")
832                    .to_string(),
833            }]
834        });
835
836        #[allow(deprecated)]
837        let params = InitializeParams {
838            process_id: Some(std::process::id()),
839            capabilities: create_client_capabilities(),
840            workspace_folders,
841            initialization_options,
842            // Set the deprecated root_uri field for compatibility with LSP servers
843            // like csharp-ls that still require it (see issue #366)
844            root_uri: root_uri.clone(),
845            ..Default::default()
846        };
847
848        let result: InitializeResult = self
849            .send_request_sequential(Initialize::METHOD, Some(params), pending)
850            .await?;
851
852        self.capabilities = Some(result.capabilities.clone());
853
854        // Send initialized notification
855        self.send_notification::<Initialized>(InitializedParams {})
856            .await?;
857
858        self.initialized = true;
859
860        // Extract completion trigger characters from server capabilities
861        let completion_trigger_characters = result
862            .capabilities
863            .completion_provider
864            .as_ref()
865            .and_then(|cp| cp.trigger_characters.clone())
866            .unwrap_or_default();
867
868        let (
869            semantic_tokens_legend,
870            semantic_tokens_full,
871            semantic_tokens_full_delta,
872            semantic_tokens_range,
873        ) = extract_semantic_token_capability(&result.capabilities);
874        let folding_ranges_supported = folding_ranges_supported(&result.capabilities);
875
876        // Notify main loop
877        let _ = self.async_tx.send(AsyncMessage::LspInitialized {
878            language: self.language.clone(),
879            completion_trigger_characters,
880            semantic_tokens_legend,
881            semantic_tokens_full,
882            semantic_tokens_full_delta,
883            semantic_tokens_range,
884            folding_ranges_supported,
885        });
886
887        // Send running status
888        let _ = self.async_tx.send(AsyncMessage::LspStatusUpdate {
889            language: self.language.clone(),
890            status: LspServerStatus::Running,
891            message: None,
892        });
893
894        tracing::info!("Async LSP server initialized successfully");
895
896        Ok(result)
897    }
898
899    /// Handle did_open command
900    #[allow(clippy::type_complexity)]
901    async fn handle_did_open_sequential(
902        &mut self,
903        uri: Uri,
904        text: String,
905        language_id: String,
906        _pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
907    ) -> Result<(), String> {
908        let path = PathBuf::from(uri.path().as_str());
909
910        if should_skip_did_open(&self.document_versions, &path, &self.language, &uri) {
911            return Ok(());
912        }
913
914        tracing::trace!("LSP: did_open for {}", uri.as_str());
915
916        // Remap languageId based on file extension using configured overrides.
917        // For example, .tsx → "typescriptreact", .jsx → "javascriptreact"
918        let lsp_language_id = path
919            .extension()
920            .and_then(|e| e.to_str())
921            .and_then(|ext| self.language_id_overrides.get(ext))
922            .cloned()
923            .unwrap_or(language_id);
924
925        let params = DidOpenTextDocumentParams {
926            text_document: TextDocumentItem {
927                uri: uri.clone(),
928                language_id: lsp_language_id,
929                version: 0,
930                text,
931            },
932        };
933
934        self.document_versions
935            .lock()
936            .unwrap()
937            .insert(path.clone(), 0);
938
939        // Record when we sent didOpen so didChange can wait if needed
940        self.pending_opens.insert(path, Instant::now());
941
942        self.send_notification::<DidOpenTextDocument>(params).await
943    }
944
945    /// Handle did_change command
946    #[allow(clippy::type_complexity)]
947    async fn handle_did_change_sequential(
948        &mut self,
949        uri: Uri,
950        content_changes: Vec<TextDocumentContentChangeEvent>,
951        _pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
952    ) -> Result<(), String> {
953        tracing::trace!("LSP: did_change for {}", uri.as_str());
954
955        let path = PathBuf::from(uri.path().as_str());
956
957        // If the document hasn't been opened yet (not in document_versions),
958        // skip this change - the upcoming didOpen will have the current content
959        if !self.document_versions.lock().unwrap().contains_key(&path) {
960            tracing::debug!(
961                "LSP ({}): skipping didChange - document not yet opened",
962                self.language
963            );
964            return Ok(());
965        }
966
967        // Check if this document was recently opened and wait if needed
968        // This prevents race conditions where the server receives didChange
969        // before it has finished processing didOpen
970        if let Some(opened_at) = self.pending_opens.get(&path) {
971            let elapsed = opened_at.elapsed();
972            let grace_period = std::time::Duration::from_millis(DID_OPEN_GRACE_PERIOD_MS);
973            if elapsed < grace_period {
974                let wait_time = grace_period - elapsed;
975                tracing::debug!(
976                    "LSP ({}): waiting {:?} for didOpen grace period before didChange",
977                    self.language,
978                    wait_time
979                );
980                tokio::time::sleep(wait_time).await;
981            }
982            // Remove from pending_opens after grace period has passed
983            self.pending_opens.remove(&path);
984        }
985
986        let new_version = {
987            let mut versions = self.document_versions.lock().unwrap();
988            let version = versions.entry(path).or_insert(0);
989            *version += 1;
990            *version
991        };
992
993        let params = DidChangeTextDocumentParams {
994            text_document: VersionedTextDocumentIdentifier {
995                uri: uri.clone(),
996                version: new_version as i32,
997            },
998            content_changes,
999        };
1000
1001        self.send_notification::<DidChangeTextDocument>(params)
1002            .await
1003    }
1004
1005    /// Handle did_save command
1006    async fn handle_did_save(&mut self, uri: Uri, text: Option<String>) -> Result<(), String> {
1007        tracing::trace!("LSP: did_save for {}", uri.as_str());
1008
1009        let params = DidSaveTextDocumentParams {
1010            text_document: TextDocumentIdentifier { uri },
1011            text,
1012        };
1013
1014        self.send_notification::<DidSaveTextDocument>(params).await
1015    }
1016
1017    /// Handle did_close command
1018    async fn handle_did_close(&mut self, uri: Uri) -> Result<(), String> {
1019        let path = PathBuf::from(uri.path().as_str());
1020
1021        // Remove from document_versions so that a subsequent didOpen will be accepted
1022        if self
1023            .document_versions
1024            .lock()
1025            .unwrap()
1026            .remove(&path)
1027            .is_some()
1028        {
1029            tracing::info!("LSP ({}): didClose for {}", self.language, uri.as_str());
1030        } else {
1031            tracing::debug!(
1032                "LSP ({}): didClose for {} but document was not tracked",
1033                self.language,
1034                uri.as_str()
1035            );
1036        }
1037
1038        // Also remove from pending_opens
1039        self.pending_opens.remove(&path);
1040
1041        let params = DidCloseTextDocumentParams {
1042            text_document: TextDocumentIdentifier { uri },
1043        };
1044
1045        self.send_notification::<DidCloseTextDocument>(params).await
1046    }
1047
1048    /// Handle completion request
1049    #[allow(clippy::type_complexity)]
1050    async fn handle_completion(
1051        &mut self,
1052        request_id: u64,
1053        uri: Uri,
1054        line: u32,
1055        character: u32,
1056        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
1057    ) -> Result<(), String> {
1058        use lsp_types::{
1059            CompletionParams, PartialResultParams, Position, TextDocumentIdentifier,
1060            TextDocumentPositionParams, WorkDoneProgressParams,
1061        };
1062
1063        tracing::trace!(
1064            "LSP: completion request at {}:{}:{}",
1065            uri.as_str(),
1066            line,
1067            character
1068        );
1069
1070        let params = CompletionParams {
1071            text_document_position: TextDocumentPositionParams {
1072                text_document: TextDocumentIdentifier { uri },
1073                position: Position { line, character },
1074            },
1075            work_done_progress_params: WorkDoneProgressParams::default(),
1076            partial_result_params: PartialResultParams::default(),
1077            context: None,
1078        };
1079
1080        // Send request and get response (tracked for cancellation)
1081        match self
1082            .send_request_sequential_tracked::<_, Value>(
1083                "textDocument/completion",
1084                Some(params),
1085                pending,
1086                Some(request_id),
1087            )
1088            .await
1089        {
1090            Ok(result) => {
1091                // Parse the completion response
1092                let items = if let Ok(list) =
1093                    serde_json::from_value::<lsp_types::CompletionList>(result.clone())
1094                {
1095                    list.items
1096                } else {
1097                    serde_json::from_value::<Vec<lsp_types::CompletionItem>>(result)
1098                        .unwrap_or_default()
1099                };
1100
1101                // Send to main loop
1102                let _ = self
1103                    .async_tx
1104                    .send(AsyncMessage::LspCompletion { request_id, items });
1105                Ok(())
1106            }
1107            Err(e) => {
1108                tracing::debug!("Completion request failed: {}", e);
1109                // Send empty completion on error
1110                let _ = self.async_tx.send(AsyncMessage::LspCompletion {
1111                    request_id,
1112                    items: vec![],
1113                });
1114                Err(e)
1115            }
1116        }
1117    }
1118
1119    /// Handle go-to-definition request
1120    #[allow(clippy::type_complexity)]
1121    async fn handle_goto_definition(
1122        &mut self,
1123        request_id: u64,
1124        uri: Uri,
1125        line: u32,
1126        character: u32,
1127        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
1128    ) -> Result<(), String> {
1129        use lsp_types::{
1130            GotoDefinitionParams, PartialResultParams, Position, TextDocumentIdentifier,
1131            TextDocumentPositionParams, WorkDoneProgressParams,
1132        };
1133
1134        tracing::trace!(
1135            "LSP: go-to-definition request at {}:{}:{}",
1136            uri.as_str(),
1137            line,
1138            character
1139        );
1140
1141        let params = GotoDefinitionParams {
1142            text_document_position_params: TextDocumentPositionParams {
1143                text_document: TextDocumentIdentifier { uri },
1144                position: Position { line, character },
1145            },
1146            work_done_progress_params: WorkDoneProgressParams::default(),
1147            partial_result_params: PartialResultParams::default(),
1148        };
1149
1150        // Send request and get response
1151        match self
1152            .send_request_sequential::<_, Value>("textDocument/definition", Some(params), pending)
1153            .await
1154        {
1155            Ok(result) => {
1156                // Parse the definition response (can be Location, Vec<Location>, or LocationLink)
1157                let locations = if let Ok(loc) =
1158                    serde_json::from_value::<lsp_types::Location>(result.clone())
1159                {
1160                    vec![loc]
1161                } else if let Ok(locs) =
1162                    serde_json::from_value::<Vec<lsp_types::Location>>(result.clone())
1163                {
1164                    locs
1165                } else if let Ok(links) =
1166                    serde_json::from_value::<Vec<lsp_types::LocationLink>>(result)
1167                {
1168                    // Convert LocationLink to Location
1169                    links
1170                        .into_iter()
1171                        .map(|link| lsp_types::Location {
1172                            uri: link.target_uri,
1173                            range: link.target_selection_range,
1174                        })
1175                        .collect()
1176                } else {
1177                    vec![]
1178                };
1179
1180                // Send to main loop
1181                let _ = self.async_tx.send(AsyncMessage::LspGotoDefinition {
1182                    request_id,
1183                    locations,
1184                });
1185                Ok(())
1186            }
1187            Err(e) => {
1188                tracing::debug!("Go-to-definition request failed: {}", e);
1189                // Send empty locations on error
1190                let _ = self.async_tx.send(AsyncMessage::LspGotoDefinition {
1191                    request_id,
1192                    locations: vec![],
1193                });
1194                Err(e)
1195            }
1196        }
1197    }
1198
1199    /// Handle rename request
1200    #[allow(clippy::type_complexity)]
1201    async fn handle_rename(
1202        &mut self,
1203        request_id: u64,
1204        uri: Uri,
1205        line: u32,
1206        character: u32,
1207        new_name: String,
1208        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
1209    ) -> Result<(), String> {
1210        use lsp_types::{
1211            Position, RenameParams, TextDocumentIdentifier, TextDocumentPositionParams,
1212            WorkDoneProgressParams,
1213        };
1214
1215        tracing::trace!(
1216            "LSP: rename request at {}:{}:{} to '{}'",
1217            uri.as_str(),
1218            line,
1219            character,
1220            new_name
1221        );
1222
1223        let params = RenameParams {
1224            text_document_position: TextDocumentPositionParams {
1225                text_document: TextDocumentIdentifier { uri },
1226                position: Position { line, character },
1227            },
1228            new_name,
1229            work_done_progress_params: WorkDoneProgressParams::default(),
1230        };
1231
1232        // Send request and get response
1233        match self
1234            .send_request_sequential::<_, Value>("textDocument/rename", Some(params), pending)
1235            .await
1236        {
1237            Ok(result) => {
1238                // Parse the workspace edit response
1239                match serde_json::from_value::<lsp_types::WorkspaceEdit>(result) {
1240                    Ok(workspace_edit) => {
1241                        // Send to main loop
1242                        let _ = self.async_tx.send(AsyncMessage::LspRename {
1243                            request_id,
1244                            result: Ok(workspace_edit),
1245                        });
1246                        Ok(())
1247                    }
1248                    Err(e) => {
1249                        tracing::error!("Failed to parse rename response: {}", e);
1250                        let _ = self.async_tx.send(AsyncMessage::LspRename {
1251                            request_id,
1252                            result: Err(format!("Failed to parse rename response: {}", e)),
1253                        });
1254                        Err(format!("Failed to parse rename response: {}", e))
1255                    }
1256                }
1257            }
1258            Err(e) => {
1259                tracing::debug!("Rename request failed: {}", e);
1260                // Send error to main loop
1261                let _ = self.async_tx.send(AsyncMessage::LspRename {
1262                    request_id,
1263                    result: Err(e.clone()),
1264                });
1265                Err(e)
1266            }
1267        }
1268    }
1269
1270    /// Handle hover documentation request
1271    #[allow(clippy::type_complexity)]
1272    async fn handle_hover(
1273        &mut self,
1274        request_id: u64,
1275        uri: Uri,
1276        line: u32,
1277        character: u32,
1278        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
1279    ) -> Result<(), String> {
1280        use lsp_types::{
1281            HoverParams, Position, TextDocumentIdentifier, TextDocumentPositionParams,
1282            WorkDoneProgressParams,
1283        };
1284
1285        tracing::trace!(
1286            "LSP: hover request at {}:{}:{}",
1287            uri.as_str(),
1288            line,
1289            character
1290        );
1291
1292        let params = HoverParams {
1293            text_document_position_params: TextDocumentPositionParams {
1294                text_document: TextDocumentIdentifier { uri },
1295                position: Position { line, character },
1296            },
1297            work_done_progress_params: WorkDoneProgressParams::default(),
1298        };
1299
1300        // Send request and get response
1301        match self
1302            .send_request_sequential::<_, Value>("textDocument/hover", Some(params), pending)
1303            .await
1304        {
1305            Ok(result) => {
1306                tracing::debug!("Raw LSP hover response: {:?}", result);
1307                // Parse the hover response
1308                let (contents, is_markdown, range) = if result.is_null() {
1309                    // No hover information available
1310                    (String::new(), false, None)
1311                } else {
1312                    match serde_json::from_value::<lsp_types::Hover>(result) {
1313                        Ok(hover) => {
1314                            // Extract text from hover contents
1315                            let (contents, is_markdown) =
1316                                Self::extract_hover_contents(&hover.contents);
1317                            // Extract the range if provided (tells us which symbol was hovered)
1318                            let range = hover.range.map(|r| {
1319                                (
1320                                    (r.start.line, r.start.character),
1321                                    (r.end.line, r.end.character),
1322                                )
1323                            });
1324                            (contents, is_markdown, range)
1325                        }
1326                        Err(e) => {
1327                            tracing::error!("Failed to parse hover response: {}", e);
1328                            (String::new(), false, None)
1329                        }
1330                    }
1331                };
1332
1333                // Send to main loop
1334                let _ = self.async_tx.send(AsyncMessage::LspHover {
1335                    request_id,
1336                    contents,
1337                    is_markdown,
1338                    range,
1339                });
1340                Ok(())
1341            }
1342            Err(e) => {
1343                tracing::debug!("Hover request failed: {}", e);
1344                // Send empty result on error (no hover available)
1345                let _ = self.async_tx.send(AsyncMessage::LspHover {
1346                    request_id,
1347                    contents: String::new(),
1348                    is_markdown: false,
1349                    range: None,
1350                });
1351                Err(e)
1352            }
1353        }
1354    }
1355
1356    /// Extract text from hover contents (handles both MarkedString and MarkupContent)
1357    /// Returns (content_string, is_markdown)
1358    fn extract_hover_contents(contents: &lsp_types::HoverContents) -> (String, bool) {
1359        use lsp_types::{HoverContents, MarkedString, MarkupContent, MarkupKind};
1360
1361        match contents {
1362            HoverContents::Scalar(marked) => match marked {
1363                MarkedString::String(s) => (s.clone(), false),
1364                MarkedString::LanguageString(ls) => {
1365                    // Language strings are formatted as markdown code blocks
1366                    (format!("```{}\n{}\n```", ls.language, ls.value), true)
1367                }
1368            },
1369            HoverContents::Array(arr) => {
1370                // Array of marked strings - format as markdown
1371                let content = arr
1372                    .iter()
1373                    .map(|marked| match marked {
1374                        MarkedString::String(s) => s.clone(),
1375                        MarkedString::LanguageString(ls) => {
1376                            format!("```{}\n{}\n```", ls.language, ls.value)
1377                        }
1378                    })
1379                    .collect::<Vec<_>>()
1380                    .join("\n\n");
1381                (content, true)
1382            }
1383            HoverContents::Markup(MarkupContent { kind, value }) => {
1384                // Check if it's markdown or plaintext
1385                let is_markdown = matches!(kind, MarkupKind::Markdown);
1386                (value.clone(), is_markdown)
1387            }
1388        }
1389    }
1390
1391    /// Handle find references request
1392    #[allow(clippy::type_complexity)]
1393    async fn handle_references(
1394        &mut self,
1395        request_id: u64,
1396        uri: Uri,
1397        line: u32,
1398        character: u32,
1399        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
1400    ) -> Result<(), String> {
1401        use lsp_types::{
1402            PartialResultParams, Position, ReferenceContext, ReferenceParams,
1403            TextDocumentIdentifier, WorkDoneProgressParams,
1404        };
1405
1406        tracing::trace!(
1407            "LSP: find references request at {}:{}:{}",
1408            uri.as_str(),
1409            line,
1410            character
1411        );
1412
1413        let params = ReferenceParams {
1414            text_document_position: lsp_types::TextDocumentPositionParams {
1415                text_document: TextDocumentIdentifier { uri },
1416                position: Position { line, character },
1417            },
1418            work_done_progress_params: WorkDoneProgressParams::default(),
1419            partial_result_params: PartialResultParams::default(),
1420            context: ReferenceContext {
1421                include_declaration: true,
1422            },
1423        };
1424
1425        // Send request and get response
1426        match self
1427            .send_request_sequential::<_, Value>("textDocument/references", Some(params), pending)
1428            .await
1429        {
1430            Ok(result) => {
1431                // Parse the references response (Vec<Location> or null)
1432                let locations = if result.is_null() {
1433                    Vec::new()
1434                } else {
1435                    serde_json::from_value::<Vec<lsp_types::Location>>(result).unwrap_or_default()
1436                };
1437
1438                tracing::trace!("LSP: found {} references", locations.len());
1439
1440                // Send to main loop
1441                let _ = self.async_tx.send(AsyncMessage::LspReferences {
1442                    request_id,
1443                    locations,
1444                });
1445                Ok(())
1446            }
1447            Err(e) => {
1448                tracing::debug!("Find references request failed: {}", e);
1449                // Send empty result on error
1450                let _ = self.async_tx.send(AsyncMessage::LspReferences {
1451                    request_id,
1452                    locations: Vec::new(),
1453                });
1454                Err(e)
1455            }
1456        }
1457    }
1458
1459    /// Handle signature help request
1460    #[allow(clippy::type_complexity)]
1461    async fn handle_signature_help(
1462        &mut self,
1463        request_id: u64,
1464        uri: Uri,
1465        line: u32,
1466        character: u32,
1467        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
1468    ) -> Result<(), String> {
1469        use lsp_types::{
1470            Position, SignatureHelpParams, TextDocumentIdentifier, TextDocumentPositionParams,
1471            WorkDoneProgressParams,
1472        };
1473
1474        tracing::trace!(
1475            "LSP: signature help request at {}:{}:{}",
1476            uri.as_str(),
1477            line,
1478            character
1479        );
1480
1481        let params = SignatureHelpParams {
1482            text_document_position_params: TextDocumentPositionParams {
1483                text_document: TextDocumentIdentifier { uri },
1484                position: Position { line, character },
1485            },
1486            work_done_progress_params: WorkDoneProgressParams::default(),
1487            context: None, // We can add context later for re-triggers
1488        };
1489
1490        // Send request and get response
1491        match self
1492            .send_request_sequential::<_, Value>(
1493                "textDocument/signatureHelp",
1494                Some(params),
1495                pending,
1496            )
1497            .await
1498        {
1499            Ok(result) => {
1500                // Parse the signature help response (SignatureHelp or null)
1501                let signature_help = if result.is_null() {
1502                    None
1503                } else {
1504                    serde_json::from_value::<lsp_types::SignatureHelp>(result).ok()
1505                };
1506
1507                tracing::trace!(
1508                    "LSP: signature help received: {} signatures",
1509                    signature_help
1510                        .as_ref()
1511                        .map(|h| h.signatures.len())
1512                        .unwrap_or(0)
1513                );
1514
1515                // Send to main loop
1516                let _ = self.async_tx.send(AsyncMessage::LspSignatureHelp {
1517                    request_id,
1518                    signature_help,
1519                });
1520                Ok(())
1521            }
1522            Err(e) => {
1523                tracing::debug!("Signature help request failed: {}", e);
1524                // Send empty result on error
1525                let _ = self.async_tx.send(AsyncMessage::LspSignatureHelp {
1526                    request_id,
1527                    signature_help: None,
1528                });
1529                Err(e)
1530            }
1531        }
1532    }
1533
1534    /// Handle code actions request
1535    #[allow(clippy::type_complexity)]
1536    #[allow(clippy::too_many_arguments)]
1537    async fn handle_code_actions(
1538        &mut self,
1539        request_id: u64,
1540        uri: Uri,
1541        start_line: u32,
1542        start_char: u32,
1543        end_line: u32,
1544        end_char: u32,
1545        diagnostics: Vec<lsp_types::Diagnostic>,
1546        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
1547    ) -> Result<(), String> {
1548        use lsp_types::{
1549            CodeActionContext, CodeActionParams, PartialResultParams, Position, Range,
1550            TextDocumentIdentifier, WorkDoneProgressParams,
1551        };
1552
1553        tracing::trace!(
1554            "LSP: code actions request at {}:{}:{}-{}:{}",
1555            uri.as_str(),
1556            start_line,
1557            start_char,
1558            end_line,
1559            end_char
1560        );
1561
1562        let params = CodeActionParams {
1563            text_document: TextDocumentIdentifier { uri },
1564            range: Range {
1565                start: Position {
1566                    line: start_line,
1567                    character: start_char,
1568                },
1569                end: Position {
1570                    line: end_line,
1571                    character: end_char,
1572                },
1573            },
1574            context: CodeActionContext {
1575                diagnostics,
1576                only: None,
1577                trigger_kind: None,
1578            },
1579            work_done_progress_params: WorkDoneProgressParams::default(),
1580            partial_result_params: PartialResultParams::default(),
1581        };
1582
1583        // Send request and get response
1584        match self
1585            .send_request_sequential::<_, Value>("textDocument/codeAction", Some(params), pending)
1586            .await
1587        {
1588            Ok(result) => {
1589                // Parse the code actions response (Vec<CodeActionOrCommand> or null)
1590                let actions = if result.is_null() {
1591                    Vec::new()
1592                } else {
1593                    serde_json::from_value::<Vec<lsp_types::CodeActionOrCommand>>(result)
1594                        .unwrap_or_default()
1595                };
1596
1597                tracing::trace!("LSP: received {} code actions", actions.len());
1598
1599                // Send to main loop
1600                let _ = self.async_tx.send(AsyncMessage::LspCodeActions {
1601                    request_id,
1602                    actions,
1603                });
1604                Ok(())
1605            }
1606            Err(e) => {
1607                tracing::debug!("Code actions request failed: {}", e);
1608                // Send empty result on error
1609                let _ = self.async_tx.send(AsyncMessage::LspCodeActions {
1610                    request_id,
1611                    actions: Vec::new(),
1612                });
1613                Err(e)
1614            }
1615        }
1616    }
1617
1618    /// Handle document diagnostic request (pull diagnostics)
1619    #[allow(clippy::type_complexity)]
1620    async fn handle_document_diagnostic(
1621        &mut self,
1622        request_id: u64,
1623        uri: Uri,
1624        previous_result_id: Option<String>,
1625        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
1626    ) -> Result<(), String> {
1627        use lsp_types::{
1628            DocumentDiagnosticParams, PartialResultParams, TextDocumentIdentifier,
1629            WorkDoneProgressParams,
1630        };
1631
1632        // Check if server supports pull diagnostics (diagnosticProvider capability)
1633        if self
1634            .capabilities
1635            .as_ref()
1636            .and_then(|c| c.diagnostic_provider.as_ref())
1637            .is_none()
1638        {
1639            tracing::trace!(
1640                "LSP: server does not support pull diagnostics, skipping request for {}",
1641                uri.as_str()
1642            );
1643            return Ok(());
1644        }
1645
1646        tracing::trace!(
1647            "LSP: document diagnostic request for {} (previous_result_id: {:?})",
1648            uri.as_str(),
1649            previous_result_id
1650        );
1651
1652        let params = DocumentDiagnosticParams {
1653            text_document: TextDocumentIdentifier { uri: uri.clone() },
1654            identifier: None,
1655            previous_result_id,
1656            work_done_progress_params: WorkDoneProgressParams::default(),
1657            partial_result_params: PartialResultParams::default(),
1658        };
1659
1660        // Send request and get response
1661        match self
1662            .send_request_sequential::<_, Value>("textDocument/diagnostic", Some(params), pending)
1663            .await
1664        {
1665            Ok(result) => {
1666                // Parse the diagnostic report result
1667                // Can be RelatedFullDocumentDiagnosticReport or RelatedUnchangedDocumentDiagnosticReport
1668                let uri_string = uri.as_str().to_string();
1669
1670                // Try to parse as full report first
1671                if let Ok(full_report) = serde_json::from_value::<
1672                    lsp_types::RelatedFullDocumentDiagnosticReport,
1673                >(result.clone())
1674                {
1675                    let diagnostics = full_report.full_document_diagnostic_report.items;
1676                    let result_id = full_report.full_document_diagnostic_report.result_id;
1677
1678                    tracing::trace!(
1679                        "LSP: received {} diagnostics for {} (result_id: {:?})",
1680                        diagnostics.len(),
1681                        uri_string,
1682                        result_id
1683                    );
1684
1685                    let _ = self.async_tx.send(AsyncMessage::LspPulledDiagnostics {
1686                        request_id,
1687                        uri: uri_string,
1688                        result_id,
1689                        diagnostics,
1690                        unchanged: false,
1691                    });
1692                } else if let Ok(unchanged_report) = serde_json::from_value::<
1693                    lsp_types::RelatedUnchangedDocumentDiagnosticReport,
1694                >(result.clone())
1695                {
1696                    let result_id = unchanged_report
1697                        .unchanged_document_diagnostic_report
1698                        .result_id;
1699
1700                    tracing::trace!(
1701                        "LSP: diagnostics unchanged for {} (result_id: {:?})",
1702                        uri_string,
1703                        result_id
1704                    );
1705
1706                    let _ = self.async_tx.send(AsyncMessage::LspPulledDiagnostics {
1707                        request_id,
1708                        uri: uri_string,
1709                        result_id: Some(result_id),
1710                        diagnostics: Vec::new(),
1711                        unchanged: true,
1712                    });
1713                } else {
1714                    // Fallback: try to parse as DocumentDiagnosticReportResult
1715                    tracing::warn!(
1716                        "LSP: could not parse diagnostic report, sending empty: {}",
1717                        result
1718                    );
1719                    let _ = self.async_tx.send(AsyncMessage::LspPulledDiagnostics {
1720                        request_id,
1721                        uri: uri_string,
1722                        result_id: None,
1723                        diagnostics: Vec::new(),
1724                        unchanged: false,
1725                    });
1726                }
1727
1728                Ok(())
1729            }
1730            Err(e) => {
1731                tracing::debug!("Document diagnostic request failed: {}", e);
1732                // Send empty result on error
1733                let _ = self.async_tx.send(AsyncMessage::LspPulledDiagnostics {
1734                    request_id,
1735                    uri: uri.as_str().to_string(),
1736                    result_id: None,
1737                    diagnostics: Vec::new(),
1738                    unchanged: false,
1739                });
1740                Err(e)
1741            }
1742        }
1743    }
1744
1745    /// Handle inlay hints request (LSP 3.17+)
1746    #[allow(clippy::type_complexity)]
1747    #[allow(clippy::too_many_arguments)]
1748    async fn handle_inlay_hints(
1749        &mut self,
1750        request_id: u64,
1751        uri: Uri,
1752        start_line: u32,
1753        start_char: u32,
1754        end_line: u32,
1755        end_char: u32,
1756        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
1757    ) -> Result<(), String> {
1758        use lsp_types::{
1759            InlayHintParams, Position, Range, TextDocumentIdentifier, WorkDoneProgressParams,
1760        };
1761
1762        tracing::trace!(
1763            "LSP: inlay hints request for {} ({}:{} - {}:{})",
1764            uri.as_str(),
1765            start_line,
1766            start_char,
1767            end_line,
1768            end_char
1769        );
1770
1771        let params = InlayHintParams {
1772            text_document: TextDocumentIdentifier { uri: uri.clone() },
1773            range: Range {
1774                start: Position {
1775                    line: start_line,
1776                    character: start_char,
1777                },
1778                end: Position {
1779                    line: end_line,
1780                    character: end_char,
1781                },
1782            },
1783            work_done_progress_params: WorkDoneProgressParams::default(),
1784        };
1785
1786        match self
1787            .send_request_sequential::<_, Option<Vec<lsp_types::InlayHint>>>(
1788                "textDocument/inlayHint",
1789                Some(params),
1790                pending,
1791            )
1792            .await
1793        {
1794            Ok(hints) => {
1795                let hints = hints.unwrap_or_default();
1796                let uri_string = uri.as_str().to_string();
1797
1798                tracing::trace!(
1799                    "LSP: received {} inlay hints for {}",
1800                    hints.len(),
1801                    uri_string
1802                );
1803
1804                let _ = self.async_tx.send(AsyncMessage::LspInlayHints {
1805                    request_id,
1806                    uri: uri_string,
1807                    hints,
1808                });
1809
1810                Ok(())
1811            }
1812            Err(e) => {
1813                tracing::debug!("Inlay hints request failed: {}", e);
1814                // Send empty result on error
1815                let _ = self.async_tx.send(AsyncMessage::LspInlayHints {
1816                    request_id,
1817                    uri: uri.as_str().to_string(),
1818                    hints: Vec::new(),
1819                });
1820                Err(e)
1821            }
1822        }
1823    }
1824
1825    /// Handle folding range request
1826    #[allow(clippy::type_complexity)]
1827    async fn handle_folding_ranges(
1828        &mut self,
1829        request_id: u64,
1830        uri: Uri,
1831        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
1832    ) -> Result<(), String> {
1833        use lsp_types::{
1834            FoldingRangeParams, PartialResultParams, TextDocumentIdentifier, WorkDoneProgressParams,
1835        };
1836
1837        tracing::trace!("LSP: folding range request for {}", uri.as_str());
1838
1839        let params = FoldingRangeParams {
1840            text_document: TextDocumentIdentifier { uri: uri.clone() },
1841            work_done_progress_params: WorkDoneProgressParams::default(),
1842            partial_result_params: PartialResultParams::default(),
1843        };
1844
1845        match self
1846            .send_request_sequential::<_, Option<Vec<lsp_types::FoldingRange>>>(
1847                "textDocument/foldingRange",
1848                Some(params),
1849                pending,
1850            )
1851            .await
1852        {
1853            Ok(ranges) => {
1854                let ranges = ranges.unwrap_or_default();
1855                let uri_string = uri.as_str().to_string();
1856
1857                tracing::trace!(
1858                    "LSP: received {} folding ranges for {}",
1859                    ranges.len(),
1860                    uri_string
1861                );
1862
1863                let _ = self.async_tx.send(AsyncMessage::LspFoldingRanges {
1864                    request_id,
1865                    uri: uri_string,
1866                    ranges,
1867                });
1868
1869                Ok(())
1870            }
1871            Err(e) => {
1872                tracing::debug!("Folding range request failed: {}", e);
1873                let _ = self.async_tx.send(AsyncMessage::LspFoldingRanges {
1874                    request_id,
1875                    uri: uri.as_str().to_string(),
1876                    ranges: Vec::new(),
1877                });
1878                Err(e)
1879            }
1880        }
1881    }
1882
1883    #[allow(clippy::type_complexity)]
1884    async fn handle_semantic_tokens_full(
1885        &mut self,
1886        request_id: u64,
1887        uri: Uri,
1888        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
1889    ) -> Result<(), String> {
1890        use lsp_types::{
1891            request::SemanticTokensFullRequest, PartialResultParams, TextDocumentIdentifier,
1892            WorkDoneProgressParams,
1893        };
1894
1895        tracing::trace!("LSP: semanticTokens/full request for {}", uri.as_str());
1896
1897        let params = SemanticTokensParams {
1898            work_done_progress_params: WorkDoneProgressParams::default(),
1899            partial_result_params: PartialResultParams::default(),
1900            text_document: TextDocumentIdentifier { uri: uri.clone() },
1901        };
1902
1903        match self
1904            .send_request_sequential_tracked::<_, Option<SemanticTokensResult>>(
1905                SemanticTokensFullRequest::METHOD,
1906                Some(params),
1907                pending,
1908                Some(request_id),
1909            )
1910            .await
1911        {
1912            Ok(result) => {
1913                let _ = self.async_tx.send(AsyncMessage::LspSemanticTokens {
1914                    request_id,
1915                    uri: uri.as_str().to_string(),
1916                    response: LspSemanticTokensResponse::Full(Ok(result)),
1917                });
1918                Ok(())
1919            }
1920            Err(e) => {
1921                tracing::debug!("Semantic tokens request failed: {}", e);
1922                let _ = self.async_tx.send(AsyncMessage::LspSemanticTokens {
1923                    request_id,
1924                    uri: uri.as_str().to_string(),
1925                    response: LspSemanticTokensResponse::Full(Err(e.clone())),
1926                });
1927                Err(e)
1928            }
1929        }
1930    }
1931
1932    #[allow(clippy::type_complexity)]
1933    async fn handle_semantic_tokens_full_delta(
1934        &mut self,
1935        request_id: u64,
1936        uri: Uri,
1937        previous_result_id: String,
1938        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
1939    ) -> Result<(), String> {
1940        use lsp_types::{
1941            request::SemanticTokensFullDeltaRequest, PartialResultParams,
1942            SemanticTokensDeltaParams, SemanticTokensFullDeltaResult, TextDocumentIdentifier,
1943            WorkDoneProgressParams,
1944        };
1945
1946        tracing::trace!(
1947            "LSP: semanticTokens/full/delta request for {}",
1948            uri.as_str()
1949        );
1950
1951        let params = SemanticTokensDeltaParams {
1952            work_done_progress_params: WorkDoneProgressParams::default(),
1953            partial_result_params: PartialResultParams::default(),
1954            text_document: TextDocumentIdentifier { uri: uri.clone() },
1955            previous_result_id,
1956        };
1957
1958        match self
1959            .send_request_sequential_tracked::<_, Option<SemanticTokensFullDeltaResult>>(
1960                SemanticTokensFullDeltaRequest::METHOD,
1961                Some(params),
1962                pending,
1963                Some(request_id),
1964            )
1965            .await
1966        {
1967            Ok(result) => {
1968                let _ = self.async_tx.send(AsyncMessage::LspSemanticTokens {
1969                    request_id,
1970                    uri: uri.as_str().to_string(),
1971                    response: LspSemanticTokensResponse::FullDelta(Ok(result)),
1972                });
1973                Ok(())
1974            }
1975            Err(e) => {
1976                tracing::debug!("Semantic tokens delta request failed: {}", e);
1977                let _ = self.async_tx.send(AsyncMessage::LspSemanticTokens {
1978                    request_id,
1979                    uri: uri.as_str().to_string(),
1980                    response: LspSemanticTokensResponse::FullDelta(Err(e.clone())),
1981                });
1982                Err(e)
1983            }
1984        }
1985    }
1986
1987    #[allow(clippy::type_complexity)]
1988    async fn handle_semantic_tokens_range(
1989        &mut self,
1990        request_id: u64,
1991        uri: Uri,
1992        range: lsp_types::Range,
1993        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
1994    ) -> Result<(), String> {
1995        use lsp_types::{
1996            request::SemanticTokensRangeRequest, PartialResultParams, SemanticTokensRangeParams,
1997            TextDocumentIdentifier, WorkDoneProgressParams,
1998        };
1999
2000        tracing::trace!("LSP: semanticTokens/range request for {}", uri.as_str());
2001
2002        let params = SemanticTokensRangeParams {
2003            work_done_progress_params: WorkDoneProgressParams::default(),
2004            partial_result_params: PartialResultParams::default(),
2005            text_document: TextDocumentIdentifier { uri: uri.clone() },
2006            range,
2007        };
2008
2009        match self
2010            .send_request_sequential_tracked::<_, Option<lsp_types::SemanticTokensRangeResult>>(
2011                SemanticTokensRangeRequest::METHOD,
2012                Some(params),
2013                pending,
2014                Some(request_id),
2015            )
2016            .await
2017        {
2018            Ok(result) => {
2019                let _ = self.async_tx.send(AsyncMessage::LspSemanticTokens {
2020                    request_id,
2021                    uri: uri.as_str().to_string(),
2022                    response: LspSemanticTokensResponse::Range(Ok(result)),
2023                });
2024                Ok(())
2025            }
2026            Err(e) => {
2027                tracing::debug!("Semantic tokens range request failed: {}", e);
2028                let _ = self.async_tx.send(AsyncMessage::LspSemanticTokens {
2029                    request_id,
2030                    uri: uri.as_str().to_string(),
2031                    response: LspSemanticTokensResponse::Range(Err(e.clone())),
2032                });
2033                Err(e)
2034            }
2035        }
2036    }
2037
2038    /// Handle a plugin-initiated request by forwarding it to the server
2039    #[allow(clippy::type_complexity)]
2040    async fn handle_plugin_request(
2041        &mut self,
2042        request_id: u64,
2043        method: String,
2044        params: Option<Value>,
2045        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
2046    ) {
2047        tracing::trace!(
2048            "Plugin request {} => method={} params={:?}",
2049            request_id,
2050            method,
2051            params
2052        );
2053        let result = self
2054            .send_request_sequential_tracked::<Value, Value>(
2055                &method,
2056                params,
2057                pending,
2058                Some(request_id),
2059            )
2060            .await;
2061
2062        tracing::trace!(
2063            "Plugin request {} completed with result {:?}",
2064            request_id,
2065            &result
2066        );
2067        let _ = self.async_tx.send(AsyncMessage::PluginLspResponse {
2068            language: self.language.clone(),
2069            request_id,
2070            result,
2071        });
2072    }
2073
2074    /// Handle shutdown command
2075    async fn handle_shutdown(&mut self) -> Result<(), String> {
2076        tracing::info!("Shutting down async LSP server");
2077
2078        let notification = JsonRpcNotification {
2079            jsonrpc: "2.0".to_string(),
2080            method: "shutdown".to_string(),
2081            params: None,
2082        };
2083
2084        self.write_message(&notification).await?;
2085
2086        let exit = JsonRpcNotification {
2087            jsonrpc: "2.0".to_string(),
2088            method: "exit".to_string(),
2089            params: None,
2090        };
2091
2092        self.write_message(&exit).await
2093    }
2094
2095    /// Send a cancel request notification to the server
2096    async fn send_cancel_request(&mut self, lsp_id: i64) -> Result<(), String> {
2097        tracing::trace!("Sending $/cancelRequest for LSP id {}", lsp_id);
2098
2099        let notification = JsonRpcNotification {
2100            jsonrpc: "2.0".to_string(),
2101            method: "$/cancelRequest".to_string(),
2102            params: Some(serde_json::json!({ "id": lsp_id })),
2103        };
2104
2105        self.write_message(&notification).await
2106    }
2107
2108    /// Cancel a request by editor request_id
2109    async fn handle_cancel_request(&mut self, request_id: u64) -> Result<(), String> {
2110        if let Some(lsp_id) = self.active_requests.remove(&request_id) {
2111            tracing::info!(
2112                "Cancelling request: editor_id={}, lsp_id={}",
2113                request_id,
2114                lsp_id
2115            );
2116            self.send_cancel_request(lsp_id).await
2117        } else {
2118            tracing::trace!(
2119                "Cancel request ignored: no active LSP request for editor_id={}",
2120                request_id
2121            );
2122            Ok(())
2123        }
2124    }
2125}
2126
2127/// Async LSP task that handles all I/O
2128struct LspTask {
2129    /// Process handle - kept alive for lifetime management (kill_on_drop)
2130    _process: Child,
2131
2132    /// Stdin for sending messages
2133    stdin: ChildStdin,
2134
2135    /// Stdout for receiving messages
2136    stdout: BufReader<ChildStdout>,
2137
2138    /// Next request ID
2139    next_id: i64,
2140
2141    /// Pending requests waiting for response
2142    pending: HashMap<i64, oneshot::Sender<Result<Value, String>>>,
2143
2144    /// Server capabilities
2145    capabilities: Option<ServerCapabilities>,
2146
2147    /// Document versions (shared with stdout reader for stale diagnostic filtering)
2148    document_versions: Arc<std::sync::Mutex<HashMap<PathBuf, i64>>>,
2149
2150    /// Track when didOpen was sent for each document to avoid race with didChange
2151    /// The LSP server needs time to process didOpen before it can handle didChange
2152    pending_opens: HashMap<PathBuf, Instant>,
2153
2154    /// Whether initialized
2155    initialized: bool,
2156
2157    /// Sender for async messages to main loop
2158    async_tx: std_mpsc::Sender<AsyncMessage>,
2159
2160    /// Language ID (for error reporting)
2161    language: String,
2162
2163    /// Server command (for plugin identification)
2164    server_command: String,
2165
2166    /// Path to stderr log file
2167    stderr_log_path: std::path::PathBuf,
2168
2169    /// Extension-to-languageId overrides for textDocument/didOpen
2170    language_id_overrides: HashMap<String, String>,
2171}
2172
2173impl LspTask {
2174    /// Create a new LSP task
2175    async fn spawn(
2176        command: &str,
2177        args: &[String],
2178        env: &std::collections::HashMap<String, String>,
2179        language: String,
2180        async_tx: std_mpsc::Sender<AsyncMessage>,
2181        process_limits: &ProcessLimits,
2182        stderr_log_path: std::path::PathBuf,
2183        language_id_overrides: HashMap<String, String>,
2184    ) -> Result<Self, String> {
2185        tracing::info!("Spawning async LSP server: {} {:?}", command, args);
2186        tracing::info!("Process limits: {:?}", process_limits);
2187        tracing::info!("LSP stderr will be logged to: {:?}", stderr_log_path);
2188
2189        // Check if the command exists before trying to spawn
2190        // This provides a clearer error message than the generic "No such file or directory"
2191        if !Self::command_exists(command) {
2192            return Err(format!(
2193                "LSP server executable '{}' not found. Please install it or check your PATH.",
2194                command
2195            ));
2196        }
2197
2198        // Create stderr log file and redirect process stderr directly to it
2199        let stderr_file = std::fs::File::create(&stderr_log_path).map_err(|e| {
2200            format!(
2201                "Failed to create LSP stderr log file {:?}: {}",
2202                stderr_log_path, e
2203            )
2204        })?;
2205
2206        let mut cmd = Command::new(command);
2207        cmd.args(args)
2208            .envs(env)
2209            .stdin(std::process::Stdio::piped())
2210            .stdout(std::process::Stdio::piped())
2211            .stderr(std::process::Stdio::from(stderr_file))
2212            .kill_on_drop(true);
2213
2214        // Apply resource limits to the process
2215        process_limits
2216            .apply_to_command(&mut cmd)
2217            .map_err(|e| format!("Failed to apply process limits: {}", e))?;
2218
2219        let mut process = cmd.spawn().map_err(|e| {
2220            format!(
2221                "Failed to spawn LSP server '{}': {}",
2222                command,
2223                match e.kind() {
2224                    std::io::ErrorKind::NotFound => "executable not found in PATH".to_string(),
2225                    std::io::ErrorKind::PermissionDenied =>
2226                        "permission denied (check file permissions)".to_string(),
2227                    _ => e.to_string(),
2228                }
2229            )
2230        })?;
2231
2232        let stdin = process
2233            .stdin
2234            .take()
2235            .ok_or_else(|| "Failed to get stdin".to_string())?;
2236
2237        let stdout = BufReader::new(
2238            process
2239                .stdout
2240                .take()
2241                .ok_or_else(|| "Failed to get stdout".to_string())?,
2242        );
2243
2244        Ok(Self {
2245            _process: process,
2246            stdin,
2247            stdout,
2248            next_id: 0,
2249            pending: HashMap::new(),
2250            capabilities: None,
2251            document_versions: Arc::new(std::sync::Mutex::new(HashMap::new())),
2252            pending_opens: HashMap::new(),
2253            initialized: false,
2254            async_tx,
2255            language,
2256            server_command: command.to_string(),
2257            stderr_log_path,
2258            language_id_overrides,
2259        })
2260    }
2261
2262    /// Check if a command exists in PATH or as an absolute path
2263    fn command_exists(command: &str) -> bool {
2264        use std::path::Path;
2265
2266        // If it's an absolute path, check if the file exists and is executable
2267        if command.contains('/') || command.contains('\\') {
2268            let path = Path::new(command);
2269            return path.exists() && path.is_file();
2270        }
2271
2272        // Otherwise, search in PATH
2273        if let Ok(path_var) = std::env::var("PATH") {
2274            #[cfg(unix)]
2275            let separator = ':';
2276            #[cfg(windows)]
2277            let separator = ';';
2278
2279            for dir in path_var.split(separator) {
2280                let full_path = Path::new(dir).join(command);
2281                if full_path.exists() && full_path.is_file() {
2282                    return true;
2283                }
2284                // On Windows, also check with .exe extension
2285                #[cfg(windows)]
2286                {
2287                    let with_exe = Path::new(dir).join(format!("{}.exe", command));
2288                    if with_exe.exists() && with_exe.is_file() {
2289                        return true;
2290                    }
2291                }
2292            }
2293        }
2294
2295        false
2296    }
2297
2298    /// Spawn the stdout reader task that continuously reads and dispatches LSP messages
2299    #[allow(clippy::type_complexity)]
2300    #[allow(clippy::too_many_arguments)]
2301    #[allow(clippy::let_underscore_must_use)] // async_tx.send() is best-effort; receiver drop means editor shutdown
2302    fn spawn_stdout_reader(
2303        mut stdout: BufReader<ChildStdout>,
2304        pending: Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
2305        async_tx: std_mpsc::Sender<AsyncMessage>,
2306        language: String,
2307        server_command: String,
2308        stdin_writer: Arc<tokio::sync::Mutex<ChildStdin>>,
2309        stderr_log_path: std::path::PathBuf,
2310        shutting_down: Arc<AtomicBool>,
2311        document_versions: Arc<std::sync::Mutex<HashMap<PathBuf, i64>>>,
2312    ) {
2313        tokio::spawn(async move {
2314            tracing::info!("LSP stdout reader task started for {}", language);
2315            loop {
2316                match read_message_from_stdout(&mut stdout).await {
2317                    Ok(message) => {
2318                        tracing::trace!("Read message from LSP server: {:?}", message);
2319                        if let Err(e) = handle_message_dispatch(
2320                            message,
2321                            &pending,
2322                            &async_tx,
2323                            &language,
2324                            &server_command,
2325                            &stdin_writer,
2326                            &document_versions,
2327                        )
2328                        .await
2329                        {
2330                            tracing::error!("Error handling LSP message: {}", e);
2331                        }
2332                    }
2333                    Err(e) => {
2334                        // Only report error if this wasn't an intentional shutdown
2335                        if shutting_down.load(Ordering::SeqCst) {
2336                            tracing::info!(
2337                                "LSP stdout reader exiting due to graceful shutdown for {}",
2338                                language
2339                            );
2340                        } else {
2341                            tracing::error!("Error reading from LSP server: {}", e);
2342                            let _ = async_tx.send(AsyncMessage::LspStatusUpdate {
2343                                language: language.clone(),
2344                                status: LspServerStatus::Error,
2345                                message: None,
2346                            });
2347                            let _ = async_tx.send(AsyncMessage::LspError {
2348                                language: language.clone(),
2349                                error: format!("Read error: {}", e),
2350                                stderr_log_path: Some(stderr_log_path.clone()),
2351                            });
2352                        }
2353                        break;
2354                    }
2355                }
2356            }
2357            tracing::info!("LSP stdout reader task exiting for {}", language);
2358        });
2359    }
2360
2361    /// Run the task (processes commands and reads from stdout)
2362    // Channel sends and handler results are best-effort: errors are already logged
2363    // within handler methods, and channel send failures mean the editor is shutting down.
2364    #[allow(clippy::let_underscore_must_use)]
2365    async fn run(self, mut command_rx: mpsc::Receiver<LspCommand>) {
2366        tracing::info!("LspTask::run() started for language: {}", self.language);
2367
2368        // Create shared stdin writer so both command processing and stdout reader can write
2369        let stdin_writer = Arc::new(tokio::sync::Mutex::new(self.stdin));
2370
2371        // Create state struct for command processing
2372        let mut state = LspState {
2373            stdin: stdin_writer.clone(),
2374            next_id: self.next_id,
2375            capabilities: self.capabilities,
2376            document_versions: self.document_versions.clone(),
2377            pending_opens: self.pending_opens,
2378            initialized: self.initialized,
2379            async_tx: self.async_tx.clone(),
2380            language: self.language.clone(),
2381            active_requests: HashMap::new(),
2382            language_id_overrides: self.language_id_overrides.clone(),
2383        };
2384
2385        let pending = Arc::new(Mutex::new(self.pending));
2386        let async_tx = state.async_tx.clone();
2387        let language_clone = state.language.clone();
2388
2389        // Flag to indicate intentional shutdown (prevents spurious error messages)
2390        let shutting_down = Arc::new(AtomicBool::new(false));
2391
2392        // Spawn stdout reader task (shares stdin_writer for responding to server requests)
2393        Self::spawn_stdout_reader(
2394            self.stdout,
2395            pending.clone(),
2396            async_tx.clone(),
2397            language_clone.clone(),
2398            self.server_command.clone(),
2399            stdin_writer.clone(),
2400            self.stderr_log_path,
2401            shutting_down.clone(),
2402            self.document_versions.clone(),
2403        );
2404
2405        // Sequential command processing loop
2406        // Note: Server responses (workspace/configuration, etc.) are now written directly
2407        // by the stdout reader task using the shared stdin_writer, avoiding deadlocks
2408        // when the main loop is blocked waiting for an LSP response.
2409        let mut pending_commands = Vec::new();
2410        loop {
2411            tokio::select! {
2412                // Handle commands from the editor
2413                Some(cmd) = command_rx.recv() => {
2414                    tracing::trace!("LspTask received command: {:?}", cmd);
2415                    match cmd {
2416                        LspCommand::Initialize { root_uri, initialization_options, response } => {
2417                            // Send initializing status
2418                            let _ = async_tx.send(AsyncMessage::LspStatusUpdate {
2419                                language: language_clone.clone(),
2420                                status: LspServerStatus::Initializing,
2421                                message: None,
2422                            });
2423                            tracing::info!("Processing Initialize command");
2424                            let result =
2425                                state.handle_initialize_sequential(root_uri, initialization_options, &pending).await;
2426                            let success = result.is_ok();
2427                            let _ = response.send(result);
2428
2429                            // After successful initialization, replay pending commands
2430                            if success {
2431                                let queued = std::mem::take(&mut pending_commands);
2432                                state.replay_pending_commands(queued, &pending).await;
2433                            }
2434                        }
2435                        LspCommand::DidOpen {
2436                            uri,
2437                            text,
2438                            language_id,
2439                        } => {
2440                            if state.initialized {
2441                                tracing::info!("Processing DidOpen for {}", uri.as_str());
2442                                let _ = state
2443                                    .handle_did_open_sequential(uri, text, language_id, &pending)
2444                                    .await;
2445                            } else {
2446                                tracing::trace!(
2447                                    "Queueing DidOpen for {} until initialization completes",
2448                                    uri.as_str()
2449                                );
2450                                pending_commands.push(LspCommand::DidOpen {
2451                                    uri,
2452                                    text,
2453                                    language_id,
2454                                });
2455                            }
2456                        }
2457                        LspCommand::DidChange {
2458                            uri,
2459                            content_changes,
2460                        } => {
2461                            if state.initialized {
2462                                tracing::trace!("Processing DidChange for {}", uri.as_str());
2463                                let _ = state
2464                                    .handle_did_change_sequential(uri, content_changes, &pending)
2465                                    .await;
2466                            } else {
2467                                tracing::trace!(
2468                                    "Queueing DidChange for {} until initialization completes",
2469                                    uri.as_str()
2470                                );
2471                                pending_commands.push(LspCommand::DidChange {
2472                                    uri,
2473                                    content_changes,
2474                                });
2475                            }
2476                        }
2477                        LspCommand::DidClose { uri } => {
2478                            if state.initialized {
2479                                tracing::info!("Processing DidClose for {}", uri.as_str());
2480                                let _ = state.handle_did_close(uri).await;
2481                            } else {
2482                                tracing::trace!(
2483                                    "Queueing DidClose for {} until initialization completes",
2484                                    uri.as_str()
2485                                );
2486                                pending_commands.push(LspCommand::DidClose { uri });
2487                            }
2488                        }
2489                        LspCommand::DidSave { uri, text } => {
2490                            if state.initialized {
2491                                tracing::info!("Processing DidSave for {}", uri.as_str());
2492                                let _ = state.handle_did_save(uri, text).await;
2493                            } else {
2494                                tracing::trace!(
2495                                    "Queueing DidSave for {} until initialization completes",
2496                                    uri.as_str()
2497                                );
2498                                pending_commands.push(LspCommand::DidSave { uri, text });
2499                            }
2500                        }
2501                        LspCommand::DidChangeWorkspaceFolders { added, removed } => {
2502                            if state.initialized {
2503                                tracing::info!(
2504                                    "Processing DidChangeWorkspaceFolders: +{} -{}",
2505                                    added.len(),
2506                                    removed.len()
2507                                );
2508                                let _ = state
2509                                    .send_notification::<lsp_types::notification::DidChangeWorkspaceFolders>(
2510                                        lsp_types::DidChangeWorkspaceFoldersParams {
2511                                            event: lsp_types::WorkspaceFoldersChangeEvent {
2512                                                added,
2513                                                removed,
2514                                            },
2515                                        },
2516                                    )
2517                                    .await;
2518                            } else {
2519                                tracing::trace!(
2520                                    "Queueing DidChangeWorkspaceFolders until initialization completes"
2521                                );
2522                                pending_commands.push(LspCommand::DidChangeWorkspaceFolders { added, removed });
2523                            }
2524                        }
2525                        LspCommand::Completion {
2526                            request_id,
2527                            uri,
2528                            line,
2529                            character,
2530                        } => {
2531                            if state.initialized {
2532                                tracing::info!(
2533                                    "Processing Completion request for {}",
2534                                    uri.as_str()
2535                                );
2536                                let _ = state
2537                                    .handle_completion(request_id, uri, line, character, &pending)
2538                                    .await;
2539                            } else {
2540                                tracing::trace!("LSP not initialized, sending empty completion");
2541                                let _ = state.async_tx.send(AsyncMessage::LspCompletion {
2542                                    request_id,
2543                                    items: vec![],
2544                                });
2545                            }
2546                        }
2547                        LspCommand::GotoDefinition {
2548                            request_id,
2549                            uri,
2550                            line,
2551                            character,
2552                        } => {
2553                            if state.initialized {
2554                                tracing::info!(
2555                                    "Processing GotoDefinition request for {}",
2556                                    uri.as_str()
2557                                );
2558                                let _ = state
2559                                    .handle_goto_definition(
2560                                        request_id, uri, line, character, &pending,
2561                                    )
2562                                    .await;
2563                            } else {
2564                                tracing::trace!("LSP not initialized, sending empty locations");
2565                                let _ = state.async_tx.send(AsyncMessage::LspGotoDefinition {
2566                                    request_id,
2567                                    locations: vec![],
2568                                });
2569                            }
2570                        }
2571                        LspCommand::Rename {
2572                            request_id,
2573                            uri,
2574                            line,
2575                            character,
2576                            new_name,
2577                        } => {
2578                            if state.initialized {
2579                                tracing::info!("Processing Rename request for {}", uri.as_str());
2580                                let _ = state
2581                                    .handle_rename(
2582                                        request_id, uri, line, character, new_name, &pending,
2583                                    )
2584                                    .await;
2585                            } else {
2586                                tracing::trace!("LSP not initialized, cannot rename");
2587                                let _ = state.async_tx.send(AsyncMessage::LspRename {
2588                                    request_id,
2589                                    result: Err("LSP not initialized".to_string()),
2590                                });
2591                            }
2592                        }
2593                        LspCommand::Hover {
2594                            request_id,
2595                            uri,
2596                            line,
2597                            character,
2598                        } => {
2599                            if state.initialized {
2600                                tracing::info!("Processing Hover request for {}", uri.as_str());
2601                                let _ = state
2602                                    .handle_hover(request_id, uri, line, character, &pending)
2603                                    .await;
2604                            } else {
2605                                tracing::trace!("LSP not initialized, cannot get hover");
2606                                let _ = state.async_tx.send(AsyncMessage::LspHover {
2607                                    request_id,
2608                                    contents: String::new(),
2609                                    is_markdown: false,
2610                                    range: None,
2611                                });
2612                            }
2613                        }
2614                        LspCommand::References {
2615                            request_id,
2616                            uri,
2617                            line,
2618                            character,
2619                        } => {
2620                            if state.initialized {
2621                                tracing::info!("Processing References request for {}", uri.as_str());
2622                                let _ = state
2623                                    .handle_references(request_id, uri, line, character, &pending)
2624                                    .await;
2625                            } else {
2626                                tracing::trace!("LSP not initialized, cannot get references");
2627                                let _ = state.async_tx.send(AsyncMessage::LspReferences {
2628                                    request_id,
2629                                    locations: Vec::new(),
2630                                });
2631                            }
2632                        }
2633                        LspCommand::SignatureHelp {
2634                            request_id,
2635                            uri,
2636                            line,
2637                            character,
2638                        } => {
2639                            if state.initialized {
2640                                tracing::info!("Processing SignatureHelp request for {}", uri.as_str());
2641                                let _ = state
2642                                    .handle_signature_help(request_id, uri, line, character, &pending)
2643                                    .await;
2644                            } else {
2645                                tracing::trace!("LSP not initialized, cannot get signature help");
2646                                let _ = state.async_tx.send(AsyncMessage::LspSignatureHelp {
2647                                    request_id,
2648                                    signature_help: None,
2649                                });
2650                            }
2651                        }
2652                        LspCommand::CodeActions {
2653                            request_id,
2654                            uri,
2655                            start_line,
2656                            start_char,
2657                            end_line,
2658                            end_char,
2659                            diagnostics,
2660                        } => {
2661                            if state.initialized {
2662                                tracing::info!("Processing CodeActions request for {}", uri.as_str());
2663                                let _ = state
2664                                    .handle_code_actions(
2665                                        request_id,
2666                                        uri,
2667                                        start_line,
2668                                        start_char,
2669                                        end_line,
2670                                        end_char,
2671                                        diagnostics,
2672                                        &pending,
2673                                    )
2674                                    .await;
2675                            } else {
2676                                tracing::trace!("LSP not initialized, cannot get code actions");
2677                                let _ = state.async_tx.send(AsyncMessage::LspCodeActions {
2678                                    request_id,
2679                                    actions: Vec::new(),
2680                                });
2681                            }
2682                        }
2683                        LspCommand::DocumentDiagnostic {
2684                            request_id,
2685                            uri,
2686                            previous_result_id,
2687                        } => {
2688                            if state.initialized {
2689                                tracing::info!(
2690                                    "Processing DocumentDiagnostic request for {}",
2691                                    uri.as_str()
2692                                );
2693                                let _ = state
2694                                    .handle_document_diagnostic(
2695                                        request_id,
2696                                        uri,
2697                                        previous_result_id,
2698                                        &pending,
2699                                    )
2700                                    .await;
2701                            } else {
2702                                tracing::trace!(
2703                                    "LSP not initialized, cannot get document diagnostics"
2704                                );
2705                                let _ = state.async_tx.send(AsyncMessage::LspPulledDiagnostics {
2706                                    request_id,
2707                                    uri: uri.as_str().to_string(),
2708                                    result_id: None,
2709                                    diagnostics: Vec::new(),
2710                                    unchanged: false,
2711                                });
2712                            }
2713                        }
2714                        LspCommand::InlayHints {
2715                            request_id,
2716                            uri,
2717                            start_line,
2718                            start_char,
2719                            end_line,
2720                            end_char,
2721                        } => {
2722                            if state.initialized {
2723                                tracing::info!(
2724                                    "Processing InlayHints request for {}",
2725                                    uri.as_str()
2726                                );
2727                                let _ = state
2728                                    .handle_inlay_hints(
2729                                        request_id,
2730                                        uri,
2731                                        start_line,
2732                                        start_char,
2733                                        end_line,
2734                                        end_char,
2735                                        &pending,
2736                                    )
2737                                    .await;
2738                            } else {
2739                                tracing::trace!(
2740                                    "LSP not initialized, cannot get inlay hints"
2741                                );
2742                                let _ = state.async_tx.send(AsyncMessage::LspInlayHints {
2743                                    request_id,
2744                                    uri: uri.as_str().to_string(),
2745                                    hints: Vec::new(),
2746                                });
2747                            }
2748                        }
2749                        LspCommand::FoldingRange { request_id, uri } => {
2750                            if state.initialized {
2751                                tracing::info!(
2752                                    "Processing FoldingRange request for {}",
2753                                    uri.as_str()
2754                                );
2755                                let _ = state
2756                                    .handle_folding_ranges(request_id, uri, &pending)
2757                                    .await;
2758                            } else {
2759                                tracing::trace!(
2760                                    "LSP not initialized, cannot get folding ranges"
2761                                );
2762                                let _ = state.async_tx.send(AsyncMessage::LspFoldingRanges {
2763                                    request_id,
2764                                    uri: uri.as_str().to_string(),
2765                                    ranges: Vec::new(),
2766                                });
2767                            }
2768                        }
2769                        LspCommand::SemanticTokensFull { request_id, uri } => {
2770                            if state.initialized {
2771                                tracing::info!(
2772                                    "Processing SemanticTokens request for {}",
2773                                    uri.as_str()
2774                                );
2775                                let _ = state
2776                                    .handle_semantic_tokens_full(request_id, uri, &pending)
2777                                    .await;
2778                            } else {
2779                                tracing::trace!(
2780                                    "LSP not initialized, cannot get semantic tokens"
2781                                );
2782                                let _ = state.async_tx.send(AsyncMessage::LspSemanticTokens {
2783                                    request_id,
2784                                    uri: uri.as_str().to_string(),
2785                                    response: LspSemanticTokensResponse::Full(Err(
2786                                        "LSP not initialized".to_string(),
2787                                    )),
2788                                });
2789                            }
2790                        }
2791                        LspCommand::SemanticTokensFullDelta {
2792                            request_id,
2793                            uri,
2794                            previous_result_id,
2795                        } => {
2796                            if state.initialized {
2797                                tracing::info!(
2798                                    "Processing SemanticTokens delta request for {}",
2799                                    uri.as_str()
2800                                );
2801                                let _ = state
2802                                    .handle_semantic_tokens_full_delta(
2803                                        request_id,
2804                                        uri,
2805                                        previous_result_id,
2806                                        &pending,
2807                                    )
2808                                    .await;
2809                            } else {
2810                                tracing::trace!(
2811                                    "LSP not initialized, cannot get semantic tokens"
2812                                );
2813                                let _ = state.async_tx.send(AsyncMessage::LspSemanticTokens {
2814                                    request_id,
2815                                    uri: uri.as_str().to_string(),
2816                                    response: LspSemanticTokensResponse::FullDelta(Err(
2817                                        "LSP not initialized".to_string(),
2818                                    )),
2819                                });
2820                            }
2821                        }
2822                        LspCommand::SemanticTokensRange {
2823                            request_id,
2824                            uri,
2825                            range,
2826                        } => {
2827                            if state.initialized {
2828                                tracing::info!(
2829                                    "Processing SemanticTokens range request for {}",
2830                                    uri.as_str()
2831                                );
2832                                let _ = state
2833                                    .handle_semantic_tokens_range(request_id, uri, range, &pending)
2834                                    .await;
2835                            } else {
2836                                tracing::trace!(
2837                                    "LSP not initialized, cannot get semantic tokens"
2838                                );
2839                                let _ = state.async_tx.send(AsyncMessage::LspSemanticTokens {
2840                                    request_id,
2841                                    uri: uri.as_str().to_string(),
2842                                    response: LspSemanticTokensResponse::Range(Err(
2843                                        "LSP not initialized".to_string(),
2844                                    )),
2845                                });
2846                            }
2847                        }
2848                        LspCommand::CancelRequest { request_id } => {
2849                            tracing::info!(
2850                                "Processing CancelRequest for editor_id={}",
2851                                request_id
2852                            );
2853                            let _ = state.handle_cancel_request(request_id).await;
2854                        }
2855                        LspCommand::PluginRequest {
2856                            request_id,
2857                            method,
2858                            params,
2859                        } => {
2860                            if state.initialized {
2861                                tracing::trace!(
2862                                    "Processing plugin request {} ({})",
2863                                    request_id,
2864                                    method
2865                                );
2866                                let _ = state
2867                                    .handle_plugin_request(
2868                                        request_id,
2869                                        method,
2870                                        params,
2871                                        &pending,
2872                                    )
2873                                    .await;
2874                            } else {
2875                                tracing::trace!(
2876                                    "Plugin LSP request {} received before initialization",
2877                                    request_id
2878                                );
2879                                let _ = state.async_tx.send(AsyncMessage::PluginLspResponse {
2880                                    language: language_clone.clone(),
2881                                    request_id,
2882                                    result: Err("LSP not initialized".to_string()),
2883                                });
2884                            }
2885                        }
2886                        LspCommand::Shutdown => {
2887                            tracing::info!("Processing Shutdown command");
2888                            // Set flag before shutdown to prevent spurious error messages
2889                            shutting_down.store(true, Ordering::SeqCst);
2890                            let _ = state.handle_shutdown().await;
2891                            break;
2892                        }
2893                    }
2894                }
2895                // Handle channel closure
2896                else => {
2897                    tracing::info!("Command channel closed");
2898                    break;
2899                }
2900            }
2901        }
2902
2903        tracing::info!("LSP task exiting for language: {}", self.language);
2904    }
2905}
2906
2907/// Standalone function to read a message from stdout (for reader task)
2908async fn read_message_from_stdout(
2909    stdout: &mut BufReader<ChildStdout>,
2910) -> Result<JsonRpcMessage, String> {
2911    // Read headers
2912    let mut content_length: Option<usize> = None;
2913
2914    loop {
2915        let mut line = String::new();
2916        let bytes_read = stdout
2917            .read_line(&mut line)
2918            .await
2919            .map_err(|e| format!("Failed to read from stdout: {}", e))?;
2920
2921        // EOF detected - LSP server closed stdout
2922        if bytes_read == 0 {
2923            return Err("LSP server closed stdout (EOF)".to_string());
2924        }
2925
2926        if line == "\r\n" {
2927            break;
2928        }
2929
2930        if let Some(len_str) = line.strip_prefix("Content-Length: ") {
2931            content_length = Some(
2932                len_str
2933                    .trim()
2934                    .parse()
2935                    .map_err(|e| format!("Invalid Content-Length: {}", e))?,
2936            );
2937        }
2938    }
2939
2940    let content_length =
2941        content_length.ok_or_else(|| "Missing Content-Length header".to_string())?;
2942
2943    // Read content
2944    let mut content = vec![0u8; content_length];
2945    stdout
2946        .read_exact(&mut content)
2947        .await
2948        .map_err(|e| format!("Failed to read content: {}", e))?;
2949
2950    let json = String::from_utf8(content).map_err(|e| format!("Invalid UTF-8: {}", e))?;
2951
2952    tracing::trace!("Received LSP message: {}", json);
2953
2954    serde_json::from_str(&json).map_err(|e| format!("Failed to deserialize message: {}", e))
2955}
2956
2957/// Standalone function to handle and dispatch messages (for reader task)
2958#[allow(clippy::type_complexity)]
2959#[allow(clippy::let_underscore_must_use)] // oneshot/mpsc send results are best-effort; receiver drop is not actionable
2960async fn handle_message_dispatch(
2961    message: JsonRpcMessage,
2962    pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
2963    async_tx: &std_mpsc::Sender<AsyncMessage>,
2964    language: &str,
2965    server_command: &str,
2966    stdin_writer: &Arc<tokio::sync::Mutex<ChildStdin>>,
2967    document_versions: &Arc<std::sync::Mutex<HashMap<PathBuf, i64>>>,
2968) -> Result<(), String> {
2969    match message {
2970        JsonRpcMessage::Response(response) => {
2971            tracing::trace!("Received LSP response for request id={}", response.id);
2972            if let Some(tx) = pending.lock().unwrap().remove(&response.id) {
2973                let result = if let Some(error) = response.error {
2974                    // Per LSP spec: ContentModified and ServerCancelled are expected
2975                    // during editing. Suppress them like VS Code and Neovim do.
2976                    if error.code == LSP_ERROR_CONTENT_MODIFIED
2977                        || error.code == LSP_ERROR_SERVER_CANCELLED
2978                    {
2979                        tracing::debug!(
2980                            "LSP response: {} (code {}), discarding",
2981                            error.message,
2982                            error.code
2983                        );
2984                    } else {
2985                        tracing::warn!(
2986                            "LSP response error: {} (code {})",
2987                            error.message,
2988                            error.code
2989                        );
2990                    }
2991                    Err(format!(
2992                        "LSP error: {} (code {})",
2993                        error.message, error.code
2994                    ))
2995                } else {
2996                    tracing::trace!("LSP response success for request id={}", response.id);
2997                    // null is a valid result for many LSP methods (e.g., inlay hints with no hints)
2998                    Ok(response.result.unwrap_or(serde_json::Value::Null))
2999                };
3000                let _ = tx.send(result);
3001            } else {
3002                tracing::warn!(
3003                    "Received LSP response for unknown request id={}",
3004                    response.id
3005                );
3006            }
3007        }
3008        JsonRpcMessage::Notification(notification) => {
3009            tracing::trace!("Received LSP notification: {}", notification.method);
3010            handle_notification_dispatch(notification, async_tx, language, document_versions)
3011                .await?;
3012        }
3013        JsonRpcMessage::Request(request) => {
3014            // Handle server-to-client requests - MUST respond to avoid timeouts
3015            tracing::trace!("Received request from server: {}", request.method);
3016            let response = match request.method.as_str() {
3017                "window/workDoneProgress/create" => {
3018                    // Server wants to create a progress token - acknowledge it
3019                    tracing::trace!("Acknowledging workDoneProgress/create (id={})", request.id);
3020                    JsonRpcResponse {
3021                        jsonrpc: "2.0".to_string(),
3022                        id: request.id,
3023                        result: Some(Value::Null),
3024                        error: None,
3025                    }
3026                }
3027                "workspace/configuration" => {
3028                    // Return configuration with inlay hints enabled for rust-analyzer
3029                    // The request contains items asking for configuration sections
3030                    // We return an array with one config object per requested item
3031                    tracing::trace!(
3032                        "Responding to workspace/configuration with inlay hints enabled"
3033                    );
3034
3035                    // Parse request params to see how many items are requested
3036                    let num_items = request
3037                        .params
3038                        .as_ref()
3039                        .and_then(|p| p.get("items"))
3040                        .and_then(|items| items.as_array())
3041                        .map(|arr| arr.len())
3042                        .unwrap_or(1);
3043
3044                    // rust-analyzer configuration with inlay hints enabled
3045                    let ra_config = serde_json::json!({
3046                        "inlayHints": {
3047                            "typeHints": {
3048                                "enable": true
3049                            },
3050                            "parameterHints": {
3051                                "enable": true
3052                            },
3053                            "chainingHints": {
3054                                "enable": true
3055                            },
3056                            "closureReturnTypeHints": {
3057                                "enable": "always"
3058                            }
3059                        }
3060                    });
3061
3062                    // Return one config object for each requested item
3063                    let configs: Vec<Value> = (0..num_items).map(|_| ra_config.clone()).collect();
3064
3065                    JsonRpcResponse {
3066                        jsonrpc: "2.0".to_string(),
3067                        id: request.id,
3068                        result: Some(Value::Array(configs)),
3069                        error: None,
3070                    }
3071                }
3072                "client/registerCapability" => {
3073                    // Server wants to register a capability dynamically - acknowledge
3074                    tracing::trace!(
3075                        "Acknowledging client/registerCapability (id={})",
3076                        request.id
3077                    );
3078                    JsonRpcResponse {
3079                        jsonrpc: "2.0".to_string(),
3080                        id: request.id,
3081                        result: Some(Value::Null),
3082                        error: None,
3083                    }
3084                }
3085                "workspace/diagnostic/refresh" => {
3086                    // Server wants us to re-pull diagnostics for all open documents
3087                    // This typically happens after the project finishes loading
3088                    tracing::info!(
3089                        "LSP ({}) requested diagnostic refresh (workspace/diagnostic/refresh)",
3090                        language
3091                    );
3092                    let _ = async_tx.send(AsyncMessage::LspDiagnosticRefresh {
3093                        language: language.to_string(),
3094                    });
3095                    JsonRpcResponse {
3096                        jsonrpc: "2.0".to_string(),
3097                        id: request.id,
3098                        result: Some(Value::Null),
3099                        error: None,
3100                    }
3101                }
3102                _ => {
3103                    // For unknown methods, notify plugins and return null to acknowledge receipt
3104                    tracing::debug!("Server request for plugins: {}", request.method);
3105                    let _ = async_tx.send(AsyncMessage::LspServerRequest {
3106                        language: language.to_string(),
3107                        server_command: server_command.to_string(),
3108                        method: request.method.clone(),
3109                        params: request.params.clone(),
3110                    });
3111                    JsonRpcResponse {
3112                        jsonrpc: "2.0".to_string(),
3113                        id: request.id,
3114                        result: Some(Value::Null),
3115                        error: None,
3116                    }
3117                }
3118            };
3119
3120            // Write response directly to stdin (avoids deadlock when main loop is waiting for LSP response)
3121            let json = serde_json::to_string(&response)
3122                .map_err(|e| format!("Failed to serialize response: {}", e))?;
3123            let message = format!("Content-Length: {}\r\n\r\n{}", json.len(), json);
3124
3125            let mut stdin = stdin_writer.lock().await;
3126            use tokio::io::AsyncWriteExt;
3127            if let Err(e) = stdin.write_all(message.as_bytes()).await {
3128                tracing::error!("Failed to write server response: {}", e);
3129            }
3130            if let Err(e) = stdin.flush().await {
3131                tracing::error!("Failed to flush server response: {}", e);
3132            }
3133            tracing::trace!("Sent response to server request id={}", response.id);
3134        }
3135    }
3136    Ok(())
3137}
3138
3139/// Standalone function to handle notifications (for reader task)
3140#[allow(clippy::let_underscore_must_use)] // async_tx.send() is best-effort; receiver drop means editor shutdown
3141async fn handle_notification_dispatch(
3142    notification: JsonRpcNotification,
3143    async_tx: &std_mpsc::Sender<AsyncMessage>,
3144    language: &str,
3145    document_versions: &Arc<std::sync::Mutex<HashMap<PathBuf, i64>>>,
3146) -> Result<(), String> {
3147    match notification.method.as_str() {
3148        PublishDiagnostics::METHOD => {
3149            if let Some(params) = notification.params {
3150                let params: PublishDiagnosticsParams = serde_json::from_value(params)
3151                    .map_err(|e| format!("Failed to deserialize diagnostics: {}", e))?;
3152
3153                // Drop stale diagnostics: if the server reports a version older than
3154                // the document version we last sent via didOpen/didChange, the diagnostics
3155                // are for an outdated snapshot and should be discarded.
3156                if let Some(diag_version) = params.version {
3157                    let path = PathBuf::from(params.uri.path().as_str());
3158                    let current_version = document_versions.lock().unwrap().get(&path).copied();
3159                    if let Some(current) = current_version {
3160                        if (diag_version as i64) < current {
3161                            tracing::debug!(
3162                                "LSP ({}): dropping stale diagnostics for {} (diag version {} < current {})",
3163                                language,
3164                                params.uri.as_str(),
3165                                diag_version,
3166                                current
3167                            );
3168                            return Ok(());
3169                        }
3170                    }
3171                }
3172
3173                tracing::trace!(
3174                    "Received {} diagnostics for {}",
3175                    params.diagnostics.len(),
3176                    params.uri.as_str()
3177                );
3178
3179                // Send to main loop
3180                let _ = async_tx.send(AsyncMessage::LspDiagnostics {
3181                    uri: params.uri.to_string(),
3182                    diagnostics: params.diagnostics,
3183                });
3184            }
3185        }
3186        "window/showMessage" => {
3187            if let Some(params) = notification.params {
3188                if let Ok(msg) = serde_json::from_value::<serde_json::Map<String, Value>>(params) {
3189                    let message_type_num = msg.get("type").and_then(|v| v.as_i64()).unwrap_or(3);
3190                    let message = msg
3191                        .get("message")
3192                        .and_then(|v| v.as_str())
3193                        .unwrap_or("(no message)")
3194                        .to_string();
3195
3196                    let message_type = match message_type_num {
3197                        1 => LspMessageType::Error,
3198                        2 => LspMessageType::Warning,
3199                        3 => LspMessageType::Info,
3200                        _ => LspMessageType::Log,
3201                    };
3202
3203                    // Log it as well
3204                    match message_type {
3205                        LspMessageType::Error => tracing::error!("LSP ({}): {}", language, message),
3206                        LspMessageType::Warning => {
3207                            tracing::warn!("LSP ({}): {}", language, message)
3208                        }
3209                        LspMessageType::Info => tracing::info!("LSP ({}): {}", language, message),
3210                        LspMessageType::Log => tracing::trace!("LSP ({}): {}", language, message),
3211                    }
3212
3213                    // Send to UI
3214                    let _ = async_tx.send(AsyncMessage::LspWindowMessage {
3215                        language: language.to_string(),
3216                        message_type,
3217                        message,
3218                    });
3219                }
3220            }
3221        }
3222        "window/logMessage" => {
3223            if let Some(params) = notification.params {
3224                if let Ok(msg) = serde_json::from_value::<serde_json::Map<String, Value>>(params) {
3225                    let message_type_num = msg.get("type").and_then(|v| v.as_i64()).unwrap_or(4);
3226                    let message = msg
3227                        .get("message")
3228                        .and_then(|v| v.as_str())
3229                        .unwrap_or("(no message)")
3230                        .to_string();
3231
3232                    let message_type = match message_type_num {
3233                        1 => LspMessageType::Error,
3234                        2 => LspMessageType::Warning,
3235                        3 => LspMessageType::Info,
3236                        _ => LspMessageType::Log,
3237                    };
3238
3239                    // Log it as well
3240                    match message_type {
3241                        LspMessageType::Error => tracing::error!("LSP ({}): {}", language, message),
3242                        LspMessageType::Warning => {
3243                            tracing::warn!("LSP ({}): {}", language, message)
3244                        }
3245                        LspMessageType::Info => tracing::info!("LSP ({}): {}", language, message),
3246                        LspMessageType::Log => tracing::trace!("LSP ({}): {}", language, message),
3247                    }
3248
3249                    // Send to UI
3250                    let _ = async_tx.send(AsyncMessage::LspLogMessage {
3251                        language: language.to_string(),
3252                        message_type,
3253                        message,
3254                    });
3255                }
3256            }
3257        }
3258        "$/progress" => {
3259            if let Some(params) = notification.params {
3260                if let Ok(progress) =
3261                    serde_json::from_value::<serde_json::Map<String, Value>>(params)
3262                {
3263                    let token = progress
3264                        .get("token")
3265                        .and_then(|v| {
3266                            v.as_str()
3267                                .map(|s| s.to_string())
3268                                .or_else(|| v.as_i64().map(|n| n.to_string()))
3269                        })
3270                        .unwrap_or_else(|| "unknown".to_string());
3271
3272                    if let Some(value_obj) = progress.get("value").and_then(|v| v.as_object()) {
3273                        let kind = value_obj.get("kind").and_then(|v| v.as_str());
3274
3275                        let value = match kind {
3276                            Some("begin") => {
3277                                let title = value_obj
3278                                    .get("title")
3279                                    .and_then(|v| v.as_str())
3280                                    .unwrap_or("Working...")
3281                                    .to_string();
3282                                let message = value_obj
3283                                    .get("message")
3284                                    .and_then(|v| v.as_str())
3285                                    .map(|s| s.to_string());
3286                                let percentage = value_obj
3287                                    .get("percentage")
3288                                    .and_then(|v| v.as_u64())
3289                                    .map(|p| p as u32);
3290
3291                                tracing::info!(
3292                                    "LSP ({}) progress begin: {} {:?} {:?}",
3293                                    language,
3294                                    title,
3295                                    message,
3296                                    percentage
3297                                );
3298
3299                                Some(LspProgressValue::Begin {
3300                                    title,
3301                                    message,
3302                                    percentage,
3303                                })
3304                            }
3305                            Some("report") => {
3306                                let message = value_obj
3307                                    .get("message")
3308                                    .and_then(|v| v.as_str())
3309                                    .map(|s| s.to_string());
3310                                let percentage = value_obj
3311                                    .get("percentage")
3312                                    .and_then(|v| v.as_u64())
3313                                    .map(|p| p as u32);
3314
3315                                tracing::trace!(
3316                                    "LSP ({}) progress report: {:?} {:?}",
3317                                    language,
3318                                    message,
3319                                    percentage
3320                                );
3321
3322                                Some(LspProgressValue::Report {
3323                                    message,
3324                                    percentage,
3325                                })
3326                            }
3327                            Some("end") => {
3328                                let message = value_obj
3329                                    .get("message")
3330                                    .and_then(|v| v.as_str())
3331                                    .map(|s| s.to_string());
3332
3333                                tracing::info!("LSP ({}) progress end: {:?}", language, message);
3334
3335                                Some(LspProgressValue::End { message })
3336                            }
3337                            _ => None,
3338                        };
3339
3340                        if let Some(value) = value {
3341                            let _ = async_tx.send(AsyncMessage::LspProgress {
3342                                language: language.to_string(),
3343                                token,
3344                                value,
3345                            });
3346                        }
3347                    }
3348                }
3349            }
3350        }
3351        "experimental/serverStatus" => {
3352            // rust-analyzer specific: server status notification
3353            // When quiescent is true, the project is fully loaded
3354            if let Some(params) = notification.params {
3355                if let Ok(status) = serde_json::from_value::<serde_json::Map<String, Value>>(params)
3356                {
3357                    let quiescent = status
3358                        .get("quiescent")
3359                        .and_then(|v| v.as_bool())
3360                        .unwrap_or(false);
3361
3362                    tracing::info!("LSP ({}) server status: quiescent={}", language, quiescent);
3363
3364                    if quiescent {
3365                        // Project is fully loaded - notify editor to re-request inlay hints
3366                        let _ = async_tx.send(AsyncMessage::LspServerQuiescent {
3367                            language: language.to_string(),
3368                        });
3369                    }
3370                }
3371            }
3372        }
3373        _ => {
3374            tracing::debug!("Unhandled notification: {}", notification.method);
3375        }
3376    }
3377
3378    Ok(())
3379}
3380
3381/// Counter for generating unique LSP handle IDs
3382static NEXT_HANDLE_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
3383
3384/// Synchronous handle to an async LSP task
3385pub struct LspHandle {
3386    /// Unique identifier for this handle instance
3387    id: u64,
3388
3389    /// Language this handle serves
3390    language: String,
3391
3392    /// Channel for sending commands to the task
3393    command_tx: mpsc::Sender<LspCommand>,
3394
3395    /// Client state
3396    state: Arc<Mutex<LspClientState>>,
3397
3398    /// Runtime handle for blocking operations
3399    runtime: tokio::runtime::Handle,
3400}
3401
3402// Channel sends and state transitions in LspHandle are best-effort: async_tx.send()
3403// failures mean the editor is shutting down, state transition errors in error-handling
3404// paths are secondary, and try_send in Drop is inherently best-effort cleanup.
3405#[allow(clippy::let_underscore_must_use)]
3406impl LspHandle {
3407    /// Spawn a new LSP server in an async task
3408    pub fn spawn(
3409        runtime: &tokio::runtime::Handle,
3410        command: &str,
3411        args: &[String],
3412        env: std::collections::HashMap<String, String>,
3413        language: String,
3414        async_bridge: &AsyncBridge,
3415        process_limits: ProcessLimits,
3416        language_id_overrides: std::collections::HashMap<String, String>,
3417    ) -> Result<Self, String> {
3418        let (command_tx, command_rx) = mpsc::channel(100); // Buffer up to 100 commands
3419        let async_tx = async_bridge.sender();
3420        let language_clone = language.clone();
3421        let command = command.to_string();
3422        let args = args.to_vec();
3423        let state = Arc::new(Mutex::new(LspClientState::Starting));
3424
3425        // Create stderr log path in XDG state directory
3426        let stderr_log_path = crate::services::log_dirs::lsp_log_path(&language);
3427
3428        // Send starting status
3429        let _ = async_tx.send(AsyncMessage::LspStatusUpdate {
3430            language: language.clone(),
3431            status: LspServerStatus::Starting,
3432            message: None,
3433        });
3434
3435        let state_clone = state.clone();
3436        let stderr_log_path_clone = stderr_log_path.clone();
3437        runtime.spawn(async move {
3438            match LspTask::spawn(
3439                &command,
3440                &args,
3441                &env,
3442                language_clone.clone(),
3443                async_tx.clone(),
3444                &process_limits,
3445                stderr_log_path_clone.clone(),
3446                language_id_overrides,
3447            )
3448            .await
3449            {
3450                Ok(task) => {
3451                    task.run(command_rx).await;
3452                }
3453                Err(e) => {
3454                    tracing::error!("Failed to spawn LSP task: {}", e);
3455
3456                    // Transition to error state
3457                    if let Ok(mut s) = state_clone.lock() {
3458                        let _ = s.transition_to(LspClientState::Error);
3459                    }
3460
3461                    let _ = async_tx.send(AsyncMessage::LspStatusUpdate {
3462                        language: language_clone.clone(),
3463                        status: LspServerStatus::Error,
3464                        message: None,
3465                    });
3466                    let _ = async_tx.send(AsyncMessage::LspError {
3467                        language: language_clone,
3468                        error: e,
3469                        stderr_log_path: Some(stderr_log_path_clone),
3470                    });
3471                }
3472            }
3473        });
3474
3475        let id = NEXT_HANDLE_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3476
3477        Ok(Self {
3478            id,
3479            language,
3480            command_tx,
3481            state,
3482            runtime: runtime.clone(),
3483        })
3484    }
3485
3486    /// Get the unique ID for this handle instance
3487    pub fn id(&self) -> u64 {
3488        self.id
3489    }
3490
3491    /// Get the language this handle serves
3492    pub fn language(&self) -> &str {
3493        &self.language
3494    }
3495
3496    /// Initialize the server (non-blocking)
3497    ///
3498    /// This sends the initialize request asynchronously. The server will be ready
3499    /// when `is_initialized()` returns true. Other methods that require initialization
3500    /// will fail gracefully until then.
3501    ///
3502    /// The `initialization_options` are passed to the server during initialization.
3503    /// Some servers like Deno require specific options (e.g., `{"enable": true}`).
3504    pub fn initialize(
3505        &self,
3506        root_uri: Option<Uri>,
3507        initialization_options: Option<Value>,
3508    ) -> Result<(), String> {
3509        // Validate state transition
3510        {
3511            let mut state = self.state.lock().unwrap();
3512            if !state.can_initialize() {
3513                return Err(format!(
3514                    "Cannot initialize: client is in state {:?}",
3515                    *state
3516                ));
3517            }
3518            // Transition to Initializing
3519            state.transition_to(LspClientState::Initializing)?;
3520        }
3521
3522        let state = self.state.clone();
3523
3524        // Create a channel for the response, but don't wait for it
3525        let (tx, rx) = oneshot::channel();
3526
3527        self.command_tx
3528            .try_send(LspCommand::Initialize {
3529                root_uri,
3530                initialization_options,
3531                response: tx,
3532            })
3533            .map_err(|_| "Failed to send initialize command".to_string())?;
3534
3535        // Spawn a task to wait for the response and update the state
3536        let runtime = self.runtime.clone();
3537        runtime.spawn(async move {
3538            match tokio::time::timeout(std::time::Duration::from_secs(10), rx).await {
3539                Ok(Ok(Ok(_))) => {
3540                    // Successfully initialized
3541                    if let Ok(mut s) = state.lock() {
3542                        let _ = s.transition_to(LspClientState::Running);
3543                    }
3544                    tracing::info!("LSP initialization completed successfully");
3545                }
3546                Ok(Ok(Err(e))) => {
3547                    tracing::error!("LSP initialization failed: {}", e);
3548                    if let Ok(mut s) = state.lock() {
3549                        let _ = s.transition_to(LspClientState::Error);
3550                    }
3551                }
3552                Ok(Err(_)) => {
3553                    tracing::error!("LSP initialization response channel closed");
3554                    if let Ok(mut s) = state.lock() {
3555                        let _ = s.transition_to(LspClientState::Error);
3556                    }
3557                }
3558                Err(_) => {
3559                    tracing::error!("LSP initialization timed out after 10 seconds");
3560                    if let Ok(mut s) = state.lock() {
3561                        let _ = s.transition_to(LspClientState::Error);
3562                    }
3563                }
3564            }
3565        });
3566
3567        Ok(())
3568    }
3569
3570    /// Check if the server is initialized
3571    pub fn is_initialized(&self) -> bool {
3572        self.state.lock().unwrap().can_send_requests()
3573    }
3574
3575    /// Get the current client state
3576    pub fn state(&self) -> LspClientState {
3577        *self.state.lock().unwrap()
3578    }
3579
3580    /// Notify document opened
3581    ///
3582    /// The `language_id` should match this handle's language. If it doesn't,
3583    /// a warning is logged but the notification is still sent (the server
3584    /// will receive it with the specified language_id).
3585    pub fn did_open(&self, uri: Uri, text: String, language_id: String) -> Result<(), String> {
3586        // Verify the document language matches this handle's language
3587        if language_id != self.language {
3588            tracing::warn!(
3589                "did_open: document language '{}' does not match LSP handle language '{}' for {}",
3590                language_id,
3591                self.language,
3592                uri.as_str()
3593            );
3594            // Return early - don't send to wrong LSP
3595            return Err(format!(
3596                "Language mismatch: document is '{}' but LSP handles '{}'",
3597                language_id, self.language
3598            ));
3599        }
3600
3601        // Send command to LspTask which will queue it if not initialized yet
3602        self.command_tx
3603            .try_send(LspCommand::DidOpen {
3604                uri,
3605                text,
3606                language_id,
3607            })
3608            .map_err(|_| "Failed to send did_open command".to_string())
3609    }
3610
3611    /// Notify document changed
3612    pub fn did_change(
3613        &self,
3614        uri: Uri,
3615        content_changes: Vec<TextDocumentContentChangeEvent>,
3616    ) -> Result<(), String> {
3617        // Send command to LspTask which will queue it if not initialized yet
3618        self.command_tx
3619            .try_send(LspCommand::DidChange {
3620                uri,
3621                content_changes,
3622            })
3623            .map_err(|_| "Failed to send did_change command".to_string())
3624    }
3625
3626    /// Send didClose notification
3627    pub fn did_close(&self, uri: Uri) -> Result<(), String> {
3628        self.command_tx
3629            .try_send(LspCommand::DidClose { uri })
3630            .map_err(|_| "Failed to send did_close command".to_string())
3631    }
3632
3633    /// Send didSave notification
3634    pub fn did_save(&self, uri: Uri, text: Option<String>) -> Result<(), String> {
3635        self.command_tx
3636            .try_send(LspCommand::DidSave { uri, text })
3637            .map_err(|_| "Failed to send did_save command".to_string())
3638    }
3639
3640    /// Add a workspace folder to the running LSP server
3641    pub fn add_workspace_folder(&self, uri: lsp_types::Uri, name: String) -> Result<(), String> {
3642        self.command_tx
3643            .try_send(LspCommand::DidChangeWorkspaceFolders {
3644                added: vec![lsp_types::WorkspaceFolder { uri, name }],
3645                removed: vec![],
3646            })
3647            .map_err(|_| "Failed to send workspace folder change".to_string())
3648    }
3649
3650    /// Request completion at position
3651    pub fn completion(
3652        &self,
3653        request_id: u64,
3654        uri: Uri,
3655        line: u32,
3656        character: u32,
3657    ) -> Result<(), String> {
3658        self.command_tx
3659            .try_send(LspCommand::Completion {
3660                request_id,
3661                uri,
3662                line,
3663                character,
3664            })
3665            .map_err(|_| "Failed to send completion command".to_string())
3666    }
3667
3668    /// Request go-to-definition
3669    pub fn goto_definition(
3670        &self,
3671        request_id: u64,
3672        uri: Uri,
3673        line: u32,
3674        character: u32,
3675    ) -> Result<(), String> {
3676        self.command_tx
3677            .try_send(LspCommand::GotoDefinition {
3678                request_id,
3679                uri,
3680                line,
3681                character,
3682            })
3683            .map_err(|_| "Failed to send goto_definition command".to_string())
3684    }
3685
3686    /// Request rename
3687    pub fn rename(
3688        &self,
3689        request_id: u64,
3690        uri: Uri,
3691        line: u32,
3692        character: u32,
3693        new_name: String,
3694    ) -> Result<(), String> {
3695        self.command_tx
3696            .try_send(LspCommand::Rename {
3697                request_id,
3698                uri,
3699                line,
3700                character,
3701                new_name,
3702            })
3703            .map_err(|_| "Failed to send rename command".to_string())
3704    }
3705
3706    /// Request hover documentation
3707    pub fn hover(
3708        &self,
3709        request_id: u64,
3710        uri: Uri,
3711        line: u32,
3712        character: u32,
3713    ) -> Result<(), String> {
3714        self.command_tx
3715            .try_send(LspCommand::Hover {
3716                request_id,
3717                uri,
3718                line,
3719                character,
3720            })
3721            .map_err(|_| "Failed to send hover command".to_string())
3722    }
3723
3724    /// Request find references
3725    pub fn references(
3726        &self,
3727        request_id: u64,
3728        uri: Uri,
3729        line: u32,
3730        character: u32,
3731    ) -> Result<(), String> {
3732        self.command_tx
3733            .try_send(LspCommand::References {
3734                request_id,
3735                uri,
3736                line,
3737                character,
3738            })
3739            .map_err(|_| "Failed to send references command".to_string())
3740    }
3741
3742    /// Request signature help
3743    pub fn signature_help(
3744        &self,
3745        request_id: u64,
3746        uri: Uri,
3747        line: u32,
3748        character: u32,
3749    ) -> Result<(), String> {
3750        self.command_tx
3751            .try_send(LspCommand::SignatureHelp {
3752                request_id,
3753                uri,
3754                line,
3755                character,
3756            })
3757            .map_err(|_| "Failed to send signature_help command".to_string())
3758    }
3759
3760    /// Request code actions
3761    #[allow(clippy::too_many_arguments)]
3762    pub fn code_actions(
3763        &self,
3764        request_id: u64,
3765        uri: Uri,
3766        start_line: u32,
3767        start_char: u32,
3768        end_line: u32,
3769        end_char: u32,
3770        diagnostics: Vec<lsp_types::Diagnostic>,
3771    ) -> Result<(), String> {
3772        self.command_tx
3773            .try_send(LspCommand::CodeActions {
3774                request_id,
3775                uri,
3776                start_line,
3777                start_char,
3778                end_line,
3779                end_char,
3780                diagnostics,
3781            })
3782            .map_err(|_| "Failed to send code_actions command".to_string())
3783    }
3784
3785    /// Request document diagnostics (pull model)
3786    ///
3787    /// This sends a textDocument/diagnostic request to fetch diagnostics on demand.
3788    /// Use `previous_result_id` for incremental updates (server may return unchanged).
3789    pub fn document_diagnostic(
3790        &self,
3791        request_id: u64,
3792        uri: Uri,
3793        previous_result_id: Option<String>,
3794    ) -> Result<(), String> {
3795        self.command_tx
3796            .try_send(LspCommand::DocumentDiagnostic {
3797                request_id,
3798                uri,
3799                previous_result_id,
3800            })
3801            .map_err(|_| "Failed to send document_diagnostic command".to_string())
3802    }
3803
3804    /// Request inlay hints for a range (LSP 3.17+)
3805    ///
3806    /// Inlay hints are virtual text annotations displayed inline (e.g., type hints, parameter names).
3807    pub fn inlay_hints(
3808        &self,
3809        request_id: u64,
3810        uri: Uri,
3811        start_line: u32,
3812        start_char: u32,
3813        end_line: u32,
3814        end_char: u32,
3815    ) -> Result<(), String> {
3816        self.command_tx
3817            .try_send(LspCommand::InlayHints {
3818                request_id,
3819                uri,
3820                start_line,
3821                start_char,
3822                end_line,
3823                end_char,
3824            })
3825            .map_err(|_| "Failed to send inlay_hints command".to_string())
3826    }
3827
3828    /// Request folding ranges for a document
3829    pub fn folding_ranges(&self, request_id: u64, uri: Uri) -> Result<(), String> {
3830        self.command_tx
3831            .try_send(LspCommand::FoldingRange { request_id, uri })
3832            .map_err(|_| "Failed to send folding_range command".to_string())
3833    }
3834
3835    /// Request semantic tokens for an entire document
3836    pub fn semantic_tokens_full(&self, request_id: u64, uri: Uri) -> Result<(), String> {
3837        self.command_tx
3838            .try_send(LspCommand::SemanticTokensFull { request_id, uri })
3839            .map_err(|_| "Failed to send semantic_tokens command".to_string())
3840    }
3841
3842    /// Request semantic tokens delta for an entire document
3843    pub fn semantic_tokens_full_delta(
3844        &self,
3845        request_id: u64,
3846        uri: Uri,
3847        previous_result_id: String,
3848    ) -> Result<(), String> {
3849        self.command_tx
3850            .try_send(LspCommand::SemanticTokensFullDelta {
3851                request_id,
3852                uri,
3853                previous_result_id,
3854            })
3855            .map_err(|_| "Failed to send semantic_tokens delta command".to_string())
3856    }
3857
3858    /// Request semantic tokens for a range
3859    pub fn semantic_tokens_range(
3860        &self,
3861        request_id: u64,
3862        uri: Uri,
3863        range: lsp_types::Range,
3864    ) -> Result<(), String> {
3865        self.command_tx
3866            .try_send(LspCommand::SemanticTokensRange {
3867                request_id,
3868                uri,
3869                range,
3870            })
3871            .map_err(|_| "Failed to send semantic_tokens_range command".to_string())
3872    }
3873
3874    /// Cancel a pending request by its editor request_id
3875    ///
3876    /// This sends a $/cancelRequest notification to the LSP server.
3877    /// If the request has already completed or doesn't exist, this is a no-op.
3878    pub fn cancel_request(&self, request_id: u64) -> Result<(), String> {
3879        self.command_tx
3880            .try_send(LspCommand::CancelRequest { request_id })
3881            .map_err(|_| "Failed to send cancel_request command".to_string())
3882    }
3883
3884    /// Send a custom LSP request initiated by a plugin
3885    pub fn send_plugin_request(
3886        &self,
3887        request_id: u64,
3888        method: String,
3889        params: Option<Value>,
3890    ) -> Result<(), String> {
3891        tracing::trace!(
3892            "LspHandle sending plugin request {}: method={}",
3893            request_id,
3894            method
3895        );
3896        match self.command_tx.try_send(LspCommand::PluginRequest {
3897            request_id,
3898            method,
3899            params,
3900        }) {
3901            Ok(()) => {
3902                tracing::trace!(
3903                    "LspHandle enqueued plugin request {} successfully",
3904                    request_id
3905                );
3906                Ok(())
3907            }
3908            Err(e) => {
3909                tracing::error!("Failed to enqueue plugin request {}: {}", request_id, e);
3910                Err("Failed to send plugin LSP request".to_string())
3911            }
3912        }
3913    }
3914
3915    /// Shutdown the server
3916    pub fn shutdown(&self) -> Result<(), String> {
3917        // Transition to Stopping state
3918        {
3919            let mut state = self.state.lock().unwrap();
3920            if let Err(e) = state.transition_to(LspClientState::Stopping) {
3921                tracing::warn!("State transition warning during shutdown: {}", e);
3922                // Don't fail shutdown due to state transition errors
3923            }
3924        }
3925
3926        self.command_tx
3927            .try_send(LspCommand::Shutdown)
3928            .map_err(|_| "Failed to send shutdown command".to_string())?;
3929
3930        // Transition to Stopped state
3931        // Note: This happens optimistically. The actual shutdown might take time.
3932        {
3933            let mut state = self.state.lock().unwrap();
3934            let _ = state.transition_to(LspClientState::Stopped);
3935        }
3936
3937        Ok(())
3938    }
3939}
3940
3941#[allow(clippy::let_underscore_must_use)] // Best-effort cleanup in Drop; failures are not actionable
3942impl Drop for LspHandle {
3943    fn drop(&mut self) {
3944        // Best-effort shutdown on drop
3945        // Use try_send instead of blocking_send to avoid panicking if:
3946        // 1. The tokio runtime is shut down
3947        // 2. The channel is full or closed
3948        // 3. We're dropping during a panic
3949        let _ = self.command_tx.try_send(LspCommand::Shutdown);
3950
3951        // Update state to Stopped
3952        if let Ok(mut state) = self.state.lock() {
3953            let _ = state.transition_to(LspClientState::Stopped);
3954        }
3955    }
3956}
3957
3958#[cfg(test)]
3959mod tests {
3960    use super::*;
3961
3962    #[test]
3963    fn test_json_rpc_request_serialization() {
3964        let request = JsonRpcRequest {
3965            jsonrpc: "2.0".to_string(),
3966            id: 1,
3967            method: "initialize".to_string(),
3968            params: Some(serde_json::json!({"rootUri": "file:///test"})),
3969        };
3970
3971        let json = serde_json::to_string(&request).unwrap();
3972        assert!(json.contains("\"jsonrpc\":\"2.0\""));
3973        assert!(json.contains("\"id\":1"));
3974        assert!(json.contains("\"method\":\"initialize\""));
3975        assert!(json.contains("\"rootUri\":\"file:///test\""));
3976    }
3977
3978    #[test]
3979    fn test_json_rpc_response_serialization() {
3980        let response = JsonRpcResponse {
3981            jsonrpc: "2.0".to_string(),
3982            id: 1,
3983            result: Some(serde_json::json!({"success": true})),
3984            error: None,
3985        };
3986
3987        let json = serde_json::to_string(&response).unwrap();
3988        assert!(json.contains("\"jsonrpc\":\"2.0\""));
3989        assert!(json.contains("\"id\":1"));
3990        assert!(json.contains("\"success\":true"));
3991        assert!(!json.contains("\"error\""));
3992    }
3993
3994    #[test]
3995    fn test_json_rpc_error_response() {
3996        let response = JsonRpcResponse {
3997            jsonrpc: "2.0".to_string(),
3998            id: 1,
3999            result: None,
4000            error: Some(JsonRpcError {
4001                code: -32600,
4002                message: "Invalid request".to_string(),
4003                data: None,
4004            }),
4005        };
4006
4007        let json = serde_json::to_string(&response).unwrap();
4008        assert!(json.contains("\"error\""));
4009        assert!(json.contains("\"code\":-32600"));
4010        assert!(json.contains("\"message\":\"Invalid request\""));
4011    }
4012
4013    #[test]
4014    fn test_json_rpc_notification_serialization() {
4015        let notification = JsonRpcNotification {
4016            jsonrpc: "2.0".to_string(),
4017            method: "textDocument/didOpen".to_string(),
4018            params: Some(serde_json::json!({"uri": "file:///test.rs"})),
4019        };
4020
4021        let json = serde_json::to_string(&notification).unwrap();
4022        assert!(json.contains("\"jsonrpc\":\"2.0\""));
4023        assert!(json.contains("\"method\":\"textDocument/didOpen\""));
4024        assert!(json.contains("\"uri\":\"file:///test.rs\""));
4025        assert!(!json.contains("\"id\"")); // Notifications have no ID
4026    }
4027
4028    #[test]
4029    fn test_json_rpc_message_deserialization_request() {
4030        let json =
4031            r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"rootUri":"file:///test"}}"#;
4032        let message: JsonRpcMessage = serde_json::from_str(json).unwrap();
4033
4034        match message {
4035            JsonRpcMessage::Request(request) => {
4036                assert_eq!(request.jsonrpc, "2.0");
4037                assert_eq!(request.id, 1);
4038                assert_eq!(request.method, "initialize");
4039                assert!(request.params.is_some());
4040            }
4041            _ => panic!("Expected Request"),
4042        }
4043    }
4044
4045    #[test]
4046    fn test_json_rpc_message_deserialization_response() {
4047        let json = r#"{"jsonrpc":"2.0","id":1,"result":{"success":true}}"#;
4048        let message: JsonRpcMessage = serde_json::from_str(json).unwrap();
4049
4050        match message {
4051            JsonRpcMessage::Response(response) => {
4052                assert_eq!(response.jsonrpc, "2.0");
4053                assert_eq!(response.id, 1);
4054                assert!(response.result.is_some());
4055                assert!(response.error.is_none());
4056            }
4057            _ => panic!("Expected Response"),
4058        }
4059    }
4060
4061    #[test]
4062    fn test_json_rpc_message_deserialization_notification() {
4063        let json = r#"{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"uri":"file:///test.rs"}}"#;
4064        let message: JsonRpcMessage = serde_json::from_str(json).unwrap();
4065
4066        match message {
4067            JsonRpcMessage::Notification(notification) => {
4068                assert_eq!(notification.jsonrpc, "2.0");
4069                assert_eq!(notification.method, "textDocument/didOpen");
4070                assert!(notification.params.is_some());
4071            }
4072            _ => panic!("Expected Notification"),
4073        }
4074    }
4075
4076    #[test]
4077    fn test_json_rpc_error_deserialization() {
4078        let json =
4079            r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"Invalid request"}}"#;
4080        let message: JsonRpcMessage = serde_json::from_str(json).unwrap();
4081
4082        match message {
4083            JsonRpcMessage::Response(response) => {
4084                assert_eq!(response.jsonrpc, "2.0");
4085                assert_eq!(response.id, 1);
4086                assert!(response.result.is_none());
4087                assert!(response.error.is_some());
4088                let error = response.error.unwrap();
4089                assert_eq!(error.code, -32600);
4090                assert_eq!(error.message, "Invalid request");
4091            }
4092            _ => panic!("Expected Response with error"),
4093        }
4094    }
4095
4096    #[tokio::test]
4097    async fn test_lsp_handle_spawn_and_drop() {
4098        // This test spawns a mock LSP server (cat command that echoes input)
4099        // and tests the spawn/drop lifecycle
4100        let runtime = tokio::runtime::Handle::current();
4101        let async_bridge = AsyncBridge::new();
4102
4103        // Use 'cat' as a mock LSP server (it will just echo stdin to stdout)
4104        // This will fail to initialize but allows us to test the spawn mechanism
4105        let result = LspHandle::spawn(
4106            &runtime,
4107            "cat",
4108            &[],
4109            Default::default(),
4110            "test".to_string(),
4111            &async_bridge,
4112            ProcessLimits::unlimited(),
4113            Default::default(),
4114        );
4115
4116        // Should succeed in spawning
4117        assert!(result.is_ok());
4118
4119        let handle = result.unwrap();
4120
4121        // Let handle drop (which calls shutdown via Drop impl)
4122        drop(handle);
4123
4124        // Give task time to receive shutdown and exit
4125        tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
4126    }
4127
4128    #[tokio::test]
4129    async fn test_lsp_handle_did_open_queues_before_initialization() {
4130        let runtime = tokio::runtime::Handle::current();
4131        let async_bridge = AsyncBridge::new();
4132
4133        let handle = LspHandle::spawn(
4134            &runtime,
4135            "cat",
4136            &[],
4137            Default::default(),
4138            "test".to_string(),
4139            &async_bridge,
4140            ProcessLimits::unlimited(),
4141            Default::default(),
4142        )
4143        .unwrap();
4144
4145        // did_open now succeeds and queues the command for when server is initialized
4146        let result = handle.did_open(
4147            "file:///test.txt".parse().unwrap(),
4148            "fn main() {}".to_string(),
4149            "test".to_string(),
4150        );
4151
4152        // Should succeed (command is queued)
4153        assert!(result.is_ok());
4154    }
4155
4156    #[tokio::test]
4157    async fn test_lsp_handle_did_change_queues_before_initialization() {
4158        let runtime = tokio::runtime::Handle::current();
4159        let async_bridge = AsyncBridge::new();
4160
4161        let handle = LspHandle::spawn(
4162            &runtime,
4163            "cat",
4164            &[],
4165            Default::default(),
4166            "test".to_string(),
4167            &async_bridge,
4168            ProcessLimits::unlimited(),
4169            Default::default(),
4170        )
4171        .unwrap();
4172
4173        // Test incremental sync: insert "fn main() {}" at position (0, 0)
4174        let result = handle.did_change(
4175            "file:///test.rs".parse().unwrap(),
4176            vec![TextDocumentContentChangeEvent {
4177                range: Some(lsp_types::Range::new(
4178                    lsp_types::Position::new(0, 0),
4179                    lsp_types::Position::new(0, 0),
4180                )),
4181                range_length: None,
4182                text: "fn main() {}".to_string(),
4183            }],
4184        );
4185
4186        // Should succeed (command is queued)
4187        assert!(result.is_ok());
4188    }
4189
4190    #[tokio::test]
4191    async fn test_lsp_handle_incremental_change_with_range() {
4192        let runtime = tokio::runtime::Handle::current();
4193        let async_bridge = AsyncBridge::new();
4194
4195        let handle = LspHandle::spawn(
4196            &runtime,
4197            "cat",
4198            &[],
4199            Default::default(),
4200            "test".to_string(),
4201            &async_bridge,
4202            ProcessLimits::unlimited(),
4203            Default::default(),
4204        )
4205        .unwrap();
4206
4207        // Test incremental delete: remove text from (0, 3) to (0, 7)
4208        let result = handle.did_change(
4209            "file:///test.rs".parse().unwrap(),
4210            vec![TextDocumentContentChangeEvent {
4211                range: Some(lsp_types::Range::new(
4212                    lsp_types::Position::new(0, 3),
4213                    lsp_types::Position::new(0, 7),
4214                )),
4215                range_length: None,
4216                text: String::new(), // Empty string for deletion
4217            }],
4218        );
4219
4220        // Should succeed (command is queued)
4221        assert!(result.is_ok());
4222    }
4223
4224    #[tokio::test]
4225    async fn test_lsp_handle_spawn_invalid_command() {
4226        let runtime = tokio::runtime::Handle::current();
4227        let async_bridge = AsyncBridge::new();
4228
4229        // Try to spawn with an invalid command
4230        let result = LspHandle::spawn(
4231            &runtime,
4232            "this-command-does-not-exist-12345",
4233            &[],
4234            Default::default(),
4235            "test".to_string(),
4236            &async_bridge,
4237            ProcessLimits::unlimited(),
4238            Default::default(),
4239        );
4240
4241        // Should succeed in creating handle (error happens asynchronously)
4242        // The error will be sent to async_bridge
4243        assert!(result.is_ok());
4244
4245        // Give the task time to fail
4246        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
4247
4248        // Check that we received an error message
4249        let messages = async_bridge.try_recv_all();
4250        assert!(!messages.is_empty());
4251
4252        let has_error = messages
4253            .iter()
4254            .any(|msg| matches!(msg, AsyncMessage::LspError { .. }));
4255        assert!(has_error, "Expected LspError message");
4256    }
4257
4258    #[test]
4259    fn test_lsp_handle_shutdown_from_sync_context() {
4260        // Test shutdown from a synchronous context (requires spawning a separate thread)
4261        // This simulates how shutdown is called from the main editor loop
4262        std::thread::spawn(|| {
4263            // Create a tokio runtime for this thread
4264            let rt = tokio::runtime::Runtime::new().unwrap();
4265            let async_bridge = AsyncBridge::new();
4266
4267            let handle = rt.block_on(async {
4268                let runtime = tokio::runtime::Handle::current();
4269                LspHandle::spawn(
4270                    &runtime,
4271                    "cat",
4272                    &[],
4273                    Default::default(),
4274                    "test".to_string(),
4275                    &async_bridge,
4276                    ProcessLimits::unlimited(),
4277                    Default::default(),
4278                )
4279                .unwrap()
4280            });
4281
4282            // This should succeed from a non-async context
4283            assert!(handle.shutdown().is_ok());
4284
4285            // Give task time to exit
4286            std::thread::sleep(std::time::Duration::from_millis(50));
4287        })
4288        .join()
4289        .unwrap();
4290    }
4291
4292    #[test]
4293    fn test_lsp_command_debug_format() {
4294        // Test that LspCommand has Debug implementation
4295        let cmd = LspCommand::Shutdown;
4296        let debug_str = format!("{:?}", cmd);
4297        assert!(debug_str.contains("Shutdown"));
4298    }
4299
4300    #[test]
4301    fn test_lsp_client_state_can_initialize_from_starting() {
4302        // This test verifies that the state machine allows initialization from the Starting state.
4303        // This is critical because LspHandle::spawn() sets state to Starting, and then
4304        // get_or_spawn() immediately calls handle.initialize(). Without this fix,
4305        // initialization would fail with "Cannot initialize: client is in state Starting".
4306
4307        let state = LspClientState::Starting;
4308
4309        // The fix: Starting state should allow initialization
4310        assert!(
4311            state.can_initialize(),
4312            "Starting state must allow initialization to avoid race condition"
4313        );
4314
4315        // Verify the full initialization flow works
4316        let mut state = LspClientState::Starting;
4317
4318        // Should be able to transition to Initializing
4319        assert!(state.can_transition_to(LspClientState::Initializing));
4320        assert!(state.transition_to(LspClientState::Initializing).is_ok());
4321
4322        // And then to Running
4323        assert!(state.can_transition_to(LspClientState::Running));
4324        assert!(state.transition_to(LspClientState::Running).is_ok());
4325    }
4326
4327    #[tokio::test]
4328    async fn test_lsp_handle_initialize_from_starting_state() {
4329        // This test reproduces the bug where initialize() would fail because
4330        // the handle's state is Starting (set by spawn()) but can_initialize()
4331        // only allowed Initial or Stopped states.
4332        //
4333        // The bug manifested as:
4334        // ERROR: Failed to send initialize command for rust: Cannot initialize: client is in state Starting
4335
4336        let runtime = tokio::runtime::Handle::current();
4337        let async_bridge = AsyncBridge::new();
4338
4339        // Spawn creates the handle with state = Starting
4340        let handle = LspHandle::spawn(
4341            &runtime,
4342            "cat", // Simple command that will exit immediately
4343            &[],
4344            Default::default(),
4345            "test".to_string(),
4346            &async_bridge,
4347            ProcessLimits::unlimited(),
4348            Default::default(),
4349        )
4350        .unwrap();
4351
4352        // Immediately call initialize - this is what get_or_spawn() does
4353        // Before the fix, this would fail with "Cannot initialize: client is in state Starting"
4354        let result = handle.initialize(None, None);
4355
4356        assert!(
4357            result.is_ok(),
4358            "initialize() must succeed from Starting state. Got error: {:?}",
4359            result.err()
4360        );
4361    }
4362
4363    #[tokio::test]
4364    async fn test_lsp_state_machine_race_condition_fix() {
4365        // Integration test that simulates the exact flow that was broken:
4366        // 1. LspManager::get_or_spawn() calls LspHandle::spawn()
4367        // 2. spawn() sets state to Starting and spawns async task
4368        // 3. get_or_spawn() immediately calls handle.initialize()
4369        // 4. initialize() should succeed even though state is Starting
4370
4371        let runtime = tokio::runtime::Handle::current();
4372        let async_bridge = AsyncBridge::new();
4373
4374        // Create a simple fake LSP server script that responds to initialize
4375        let fake_lsp_script = r#"
4376            read -r line  # Read Content-Length header
4377            read -r empty # Read empty line
4378            read -r json  # Read JSON body
4379
4380            # Send a valid initialize response
4381            response='{"jsonrpc":"2.0","id":1,"result":{"capabilities":{}}}'
4382            echo "Content-Length: ${#response}"
4383            echo ""
4384            echo -n "$response"
4385
4386            # Keep running to avoid EOF
4387            sleep 10
4388        "#;
4389
4390        // Spawn with bash to execute the fake LSP
4391        let handle = LspHandle::spawn(
4392            &runtime,
4393            "bash",
4394            &["-c".to_string(), fake_lsp_script.to_string()],
4395            Default::default(),
4396            "fake".to_string(),
4397            &async_bridge,
4398            ProcessLimits::unlimited(),
4399            Default::default(),
4400        )
4401        .unwrap();
4402
4403        // This is the critical test: initialize must succeed from Starting state
4404        let init_result = handle.initialize(None, None);
4405        assert!(
4406            init_result.is_ok(),
4407            "initialize() failed from Starting state: {:?}",
4408            init_result.err()
4409        );
4410
4411        // Give the async task time to process
4412        tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
4413
4414        // Check that we received status update messages
4415        let messages = async_bridge.try_recv_all();
4416        let has_status_update = messages
4417            .iter()
4418            .any(|msg| matches!(msg, AsyncMessage::LspStatusUpdate { .. }));
4419
4420        assert!(
4421            has_status_update,
4422            "Expected status update messages from LSP initialization"
4423        );
4424
4425        // Cleanup - best-effort, test is ending
4426        #[allow(clippy::let_underscore_must_use)]
4427        let _ = handle.shutdown();
4428    }
4429}