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, 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
359use crate::services::lsp::manager::ServerCapabilitySummary;
360
361/// Extract a complete capability summary from the server's initialize response.
362///
363/// Follows the LSP 3.17 specification for each capability field:
364/// - `boolean | XxxOptions` → true if `true` or options present
365/// - Options-only fields (e.g. completionProvider) → true if present
366fn extract_capability_summary(caps: &ServerCapabilities) -> ServerCapabilitySummary {
367    let (sem_legend, sem_full, sem_full_delta, sem_range) = caps
368        .semantic_tokens_provider
369        .as_ref()
370        .map(|provider| {
371            let (legend, full_opt) = match provider {
372                SemanticTokensServerCapabilities::SemanticTokensOptions(o) => {
373                    (o.legend.clone(), &o.full)
374                }
375                SemanticTokensServerCapabilities::SemanticTokensRegistrationOptions(o) => (
376                    o.semantic_tokens_options.legend.clone(),
377                    &o.semantic_tokens_options.full,
378                ),
379            };
380            let range = match provider {
381                SemanticTokensServerCapabilities::SemanticTokensOptions(o) => {
382                    o.range.unwrap_or(false)
383                }
384                SemanticTokensServerCapabilities::SemanticTokensRegistrationOptions(o) => {
385                    o.semantic_tokens_options.range.unwrap_or(false)
386                }
387            };
388            let full = match full_opt {
389                Some(SemanticTokensFullOptions::Bool(v)) => *v,
390                Some(SemanticTokensFullOptions::Delta { .. }) => true,
391                None => false,
392            };
393            let delta = match full_opt {
394                Some(SemanticTokensFullOptions::Delta { delta }) => delta.unwrap_or(false),
395                _ => false,
396            };
397            (Some(legend), full, delta, range)
398        })
399        .unwrap_or((None, false, false, false));
400
401    ServerCapabilitySummary {
402        initialized: false, // set to true by set_server_capabilities
403        hover: bool_or_options(&caps.hover_provider, |p| match p {
404            lsp_types::HoverProviderCapability::Simple(v) => *v,
405            lsp_types::HoverProviderCapability::Options(_) => true,
406        }),
407        completion: caps.completion_provider.is_some(),
408        completion_resolve: caps
409            .completion_provider
410            .as_ref()
411            .and_then(|cp| cp.resolve_provider)
412            .unwrap_or(false),
413        completion_trigger_characters: caps
414            .completion_provider
415            .as_ref()
416            .and_then(|cp| cp.trigger_characters.clone())
417            .unwrap_or_default(),
418        definition: bool_or_options(&caps.definition_provider, |p| match p {
419            lsp_types::OneOf::Left(v) => *v,
420            lsp_types::OneOf::Right(_) => true,
421        }),
422        references: bool_or_options(&caps.references_provider, |p| match p {
423            lsp_types::OneOf::Left(v) => *v,
424            lsp_types::OneOf::Right(_) => true,
425        }),
426        document_formatting: bool_or_options(&caps.document_formatting_provider, |p| match p {
427            lsp_types::OneOf::Left(v) => *v,
428            lsp_types::OneOf::Right(_) => true,
429        }),
430        document_range_formatting: bool_or_options(&caps.document_range_formatting_provider, |p| {
431            match p {
432                lsp_types::OneOf::Left(v) => *v,
433                lsp_types::OneOf::Right(_) => true,
434            }
435        }),
436        rename: bool_or_options(&caps.rename_provider, |p| match p {
437            lsp_types::OneOf::Left(v) => *v,
438            lsp_types::OneOf::Right(_) => true,
439        }),
440        signature_help: caps.signature_help_provider.is_some(),
441        inlay_hints: bool_or_options(&caps.inlay_hint_provider, |p| match p {
442            lsp_types::OneOf::Left(v) => *v,
443            lsp_types::OneOf::Right(_) => true,
444        }),
445        folding_ranges: bool_or_options(&caps.folding_range_provider, |p| match p {
446            lsp_types::FoldingRangeProviderCapability::Simple(v) => *v,
447            _ => true,
448        }),
449        semantic_tokens_full: sem_full,
450        semantic_tokens_full_delta: sem_full_delta,
451        semantic_tokens_range: sem_range,
452        semantic_tokens_legend: sem_legend,
453        document_highlight: bool_or_options(&caps.document_highlight_provider, |p| match p {
454            lsp_types::OneOf::Left(v) => *v,
455            lsp_types::OneOf::Right(_) => true,
456        }),
457        code_action: bool_or_options(&caps.code_action_provider, |p| match p {
458            lsp_types::CodeActionProviderCapability::Simple(v) => *v,
459            lsp_types::CodeActionProviderCapability::Options(_) => true,
460        }),
461        code_action_resolve: caps.code_action_provider.as_ref().is_some_and(|p| match p {
462            lsp_types::CodeActionProviderCapability::Options(opts) => {
463                opts.resolve_provider.unwrap_or(false)
464            }
465            _ => false,
466        }),
467        document_symbols: bool_or_options(&caps.document_symbol_provider, |p| match p {
468            lsp_types::OneOf::Left(v) => *v,
469            lsp_types::OneOf::Right(_) => true,
470        }),
471        workspace_symbols: bool_or_options(&caps.workspace_symbol_provider, |p| match p {
472            lsp_types::OneOf::Left(v) => *v,
473            lsp_types::OneOf::Right(_) => true,
474        }),
475        diagnostics: caps.diagnostic_provider.is_some(),
476    }
477}
478
479/// Helper: check an `Option<T>` capability field using a predicate.
480fn bool_or_options<T>(opt: &Option<T>, check: impl FnOnce(&T) -> bool) -> bool {
481    opt.as_ref().is_some_and(check)
482}
483
484/// Commands sent from the main loop to the LSP task
485#[derive(Debug)]
486enum LspCommand {
487    /// Initialize the server
488    Initialize {
489        root_uri: Option<Uri>,
490        initialization_options: Option<Value>,
491        response: oneshot::Sender<Result<InitializeResult, String>>,
492    },
493
494    /// Notify document opened
495    DidOpen {
496        uri: Uri,
497        text: String,
498        language_id: String,
499    },
500
501    /// Notify document changed
502    DidChange {
503        uri: Uri,
504        content_changes: Vec<TextDocumentContentChangeEvent>,
505    },
506
507    /// Notify document closed
508    DidClose { uri: Uri },
509
510    /// Notify document saved
511    DidSave { uri: Uri, text: Option<String> },
512
513    /// Notify workspace folders changed
514    DidChangeWorkspaceFolders {
515        added: Vec<lsp_types::WorkspaceFolder>,
516        removed: Vec<lsp_types::WorkspaceFolder>,
517    },
518
519    /// Request completion at position
520    Completion {
521        request_id: u64,
522        uri: Uri,
523        line: u32,
524        character: u32,
525    },
526
527    /// Request go-to-definition
528    GotoDefinition {
529        request_id: u64,
530        uri: Uri,
531        line: u32,
532        character: u32,
533    },
534
535    /// Request rename
536    Rename {
537        request_id: u64,
538        uri: Uri,
539        line: u32,
540        character: u32,
541        new_name: String,
542    },
543
544    /// Request hover documentation
545    Hover {
546        request_id: u64,
547        uri: Uri,
548        line: u32,
549        character: u32,
550    },
551
552    /// Request find references
553    References {
554        request_id: u64,
555        uri: Uri,
556        line: u32,
557        character: u32,
558    },
559
560    /// Request signature help
561    SignatureHelp {
562        request_id: u64,
563        uri: Uri,
564        line: u32,
565        character: u32,
566    },
567
568    /// Request code actions
569    CodeActions {
570        request_id: u64,
571        uri: Uri,
572        start_line: u32,
573        start_char: u32,
574        end_line: u32,
575        end_char: u32,
576        diagnostics: Vec<lsp_types::Diagnostic>,
577    },
578
579    /// Request document diagnostics (pull model)
580    DocumentDiagnostic {
581        request_id: u64,
582        uri: Uri,
583        /// Previous result_id for incremental updates (None for full refresh)
584        previous_result_id: Option<String>,
585    },
586
587    /// Request inlay hints for a range (LSP 3.17+)
588    InlayHints {
589        request_id: u64,
590        uri: Uri,
591        /// Range to get hints for (typically viewport)
592        start_line: u32,
593        start_char: u32,
594        end_line: u32,
595        end_char: u32,
596    },
597
598    /// Request folding ranges for a document
599    FoldingRange { request_id: u64, uri: Uri },
600
601    /// Request semantic tokens for the entire document
602    SemanticTokensFull { request_id: u64, uri: Uri },
603
604    /// Request semantic tokens delta for the entire document
605    SemanticTokensFullDelta {
606        request_id: u64,
607        uri: Uri,
608        previous_result_id: String,
609    },
610
611    /// Request semantic tokens for a range
612    SemanticTokensRange {
613        request_id: u64,
614        uri: Uri,
615        range: lsp_types::Range,
616    },
617
618    /// Execute a command on the server (workspace/executeCommand)
619    ExecuteCommand {
620        command: String,
621        arguments: Option<Vec<Value>>,
622    },
623
624    /// Resolve a code action to get full edit/command details (codeAction/resolve)
625    CodeActionResolve {
626        request_id: u64,
627        action: Box<lsp_types::CodeAction>,
628    },
629
630    /// Resolve a completion item to get full details (completionItem/resolve)
631    CompletionResolve {
632        request_id: u64,
633        item: Box<lsp_types::CompletionItem>,
634    },
635
636    /// Format a document (textDocument/formatting)
637    DocumentFormatting {
638        request_id: u64,
639        uri: Uri,
640        tab_size: u32,
641        insert_spaces: bool,
642    },
643
644    /// Format a range in a document (textDocument/rangeFormatting)
645    DocumentRangeFormatting {
646        request_id: u64,
647        uri: Uri,
648        start_line: u32,
649        start_char: u32,
650        end_line: u32,
651        end_char: u32,
652        tab_size: u32,
653        insert_spaces: bool,
654    },
655
656    /// Prepare rename — validate rename at position (textDocument/prepareRename)
657    PrepareRename {
658        request_id: u64,
659        uri: Uri,
660        line: u32,
661        character: u32,
662    },
663
664    /// Cancel a pending request
665    CancelRequest {
666        /// Editor's request ID to cancel
667        request_id: u64,
668    },
669
670    /// Custom request initiated by a plugin
671    PluginRequest {
672        request_id: u64,
673        method: String,
674        params: Option<Value>,
675    },
676
677    /// Shutdown the server
678    Shutdown,
679}
680
681/// Mutable state for LSP command processing
682struct LspState {
683    /// Stdin for sending messages (shared with stdout reader for server responses)
684    stdin: Arc<tokio::sync::Mutex<ChildStdin>>,
685
686    /// Next request ID
687    next_id: i64,
688
689    /// Server capabilities
690    capabilities: Option<ServerCapabilities>,
691
692    /// Document versions (shared with stdout reader for stale diagnostic filtering)
693    document_versions: Arc<std::sync::Mutex<HashMap<PathBuf, i64>>>,
694
695    /// Track when didOpen was sent for each document to avoid race with didChange
696    /// The LSP server needs time to process didOpen before it can handle didChange
697    pending_opens: HashMap<PathBuf, Instant>,
698
699    /// Whether initialized
700    initialized: bool,
701
702    /// Sender for async messages to main loop
703    async_tx: std_mpsc::Sender<AsyncMessage>,
704
705    /// Language ID (for error reporting)
706    language: String,
707
708    /// Server name (for multi-server status tracking)
709    server_name: String,
710
711    /// Mapping from editor request_id to LSP JSON-RPC id for cancellation
712    /// Key: editor request_id, Value: LSP JSON-RPC id
713    active_requests: HashMap<u64, i64>,
714
715    /// Extension-to-languageId overrides for textDocument/didOpen
716    language_id_overrides: HashMap<String, String>,
717}
718
719// Channel sends (`async_tx.send()`) throughout LspState are best-effort: if the receiver
720// (main editor loop) has been dropped, the editor is shutting down and there is nothing
721// to do with the error. Handler method results (`handle_*`) are similarly safe to discard
722// since errors are already logged within those methods. State transitions in error-handling
723// paths are secondary to the actual error being handled.
724#[allow(clippy::let_underscore_must_use)]
725impl LspState {
726    /// Replay pending commands that were queued before initialization
727    #[allow(clippy::type_complexity)]
728    async fn replay_pending_commands(
729        &mut self,
730        commands: Vec<LspCommand>,
731        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
732    ) {
733        if commands.is_empty() {
734            return;
735        }
736        tracing::info!(
737            "Replaying {} pending commands after initialization",
738            commands.len()
739        );
740        for cmd in commands {
741            match cmd {
742                LspCommand::DidOpen {
743                    uri,
744                    text,
745                    language_id,
746                } => {
747                    tracing::info!("Replaying DidOpen for {}", uri.as_str());
748                    let _ = self
749                        .handle_did_open_sequential(uri, text, language_id, pending)
750                        .await;
751                }
752                LspCommand::DidChange {
753                    uri,
754                    content_changes,
755                } => {
756                    tracing::info!("Replaying DidChange for {}", uri.as_str());
757                    let _ = self
758                        .handle_did_change_sequential(uri, content_changes, pending)
759                        .await;
760                }
761                LspCommand::DidClose { uri } => {
762                    tracing::info!("Replaying DidClose for {}", uri.as_str());
763                    let _ = self.handle_did_close(uri).await;
764                }
765                LspCommand::DidSave { uri, text } => {
766                    tracing::info!("Replaying DidSave for {}", uri.as_str());
767                    let _ = self.handle_did_save(uri, text).await;
768                }
769                LspCommand::DidChangeWorkspaceFolders { added, removed } => {
770                    tracing::info!(
771                        "Replaying DidChangeWorkspaceFolders: +{} -{}",
772                        added.len(),
773                        removed.len()
774                    );
775                    let _ = self
776                        .send_notification::<lsp_types::notification::DidChangeWorkspaceFolders>(
777                            lsp_types::DidChangeWorkspaceFoldersParams {
778                                event: lsp_types::WorkspaceFoldersChangeEvent { added, removed },
779                            },
780                        )
781                        .await;
782                }
783                LspCommand::SemanticTokensFull { request_id, uri } => {
784                    tracing::info!("Replaying semantic tokens request for {}", uri.as_str());
785                    let _ = self
786                        .handle_semantic_tokens_full(request_id, uri, pending)
787                        .await;
788                }
789                LspCommand::SemanticTokensFullDelta {
790                    request_id,
791                    uri,
792                    previous_result_id,
793                } => {
794                    tracing::info!(
795                        "Replaying semantic tokens delta request for {}",
796                        uri.as_str()
797                    );
798                    let _ = self
799                        .handle_semantic_tokens_full_delta(
800                            request_id,
801                            uri,
802                            previous_result_id,
803                            pending,
804                        )
805                        .await;
806                }
807                LspCommand::SemanticTokensRange {
808                    request_id,
809                    uri,
810                    range,
811                } => {
812                    tracing::info!(
813                        "Replaying semantic tokens range request for {}",
814                        uri.as_str()
815                    );
816                    let _ = self
817                        .handle_semantic_tokens_range(request_id, uri, range, pending)
818                        .await;
819                }
820                LspCommand::FoldingRange { request_id, uri } => {
821                    tracing::info!("Replaying folding range request for {}", uri.as_str());
822                    let _ = self.handle_folding_ranges(request_id, uri, pending).await;
823                }
824                _ => {}
825            }
826        }
827    }
828
829    /// Write a message to stdin
830    async fn write_message<T: Serialize>(&mut self, message: &T) -> Result<(), String> {
831        let json =
832            serde_json::to_string(message).map_err(|e| format!("Serialization error: {}", e))?;
833
834        let content = format!("Content-Length: {}\r\n\r\n{}", json.len(), json);
835
836        tracing::trace!("Writing LSP message to stdin ({} bytes)", content.len());
837
838        let mut stdin = self.stdin.lock().await;
839        stdin
840            .write_all(content.as_bytes())
841            .await
842            .map_err(|e| format!("Failed to write to stdin: {}", e))?;
843
844        stdin
845            .flush()
846            .await
847            .map_err(|e| format!("Failed to flush stdin: {}", e))?;
848
849        tracing::trace!("Successfully sent LSP message");
850
851        Ok(())
852    }
853
854    /// Send a notification using lsp-types Notification trait (type-safe)
855    async fn send_notification<N>(&mut self, params: N::Params) -> Result<(), String>
856    where
857        N: Notification,
858    {
859        let notification = JsonRpcNotification {
860            jsonrpc: "2.0".to_string(),
861            method: N::METHOD.to_string(),
862            params: Some(
863                serde_json::to_value(params)
864                    .map_err(|e| format!("Failed to serialize params: {}", e))?,
865            ),
866        };
867
868        self.write_message(&notification).await
869    }
870
871    /// Send request using shared pending map
872    #[allow(clippy::type_complexity)]
873    async fn send_request_sequential<P: Serialize, R: for<'de> Deserialize<'de>>(
874        &mut self,
875        method: &str,
876        params: Option<P>,
877        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
878    ) -> Result<R, String> {
879        self.send_request_sequential_tracked(method, params, pending, None)
880            .await
881    }
882
883    /// Send request using shared pending map with optional editor request tracking
884    #[allow(clippy::type_complexity)]
885    async fn send_request_sequential_tracked<P: Serialize, R: for<'de> Deserialize<'de>>(
886        &mut self,
887        method: &str,
888        params: Option<P>,
889        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
890        editor_request_id: Option<u64>,
891    ) -> Result<R, String> {
892        let id = self.next_id;
893        self.next_id += 1;
894
895        // Track the mapping if editor_request_id is provided
896        if let Some(editor_id) = editor_request_id {
897            self.active_requests.insert(editor_id, id);
898            tracing::trace!("Tracking request: editor_id={}, lsp_id={}", editor_id, id);
899        }
900
901        let params_value = params
902            .map(|p| serde_json::to_value(p))
903            .transpose()
904            .map_err(|e| format!("Failed to serialize params: {}", e))?;
905        let request = JsonRpcRequest {
906            jsonrpc: "2.0".to_string(),
907            id,
908            method: method.to_string(),
909            params: params_value,
910        };
911
912        let (tx, rx) = oneshot::channel();
913        pending.lock().unwrap().insert(id, tx);
914
915        self.write_message(&request).await?;
916
917        tracing::trace!("Sent LSP request id={}, waiting for response...", id);
918
919        // Await response (this is OK now because the reader task will send it)
920        let result = rx
921            .await
922            .map_err(|_| "Response channel closed".to_string())??;
923
924        tracing::trace!("Received LSP response for request id={}", id);
925
926        // Remove tracking after response received
927        if let Some(editor_id) = editor_request_id {
928            self.active_requests.remove(&editor_id);
929            tracing::trace!("Completed request: editor_id={}, lsp_id={}", editor_id, id);
930        }
931
932        serde_json::from_value(result).map_err(|e| format!("Failed to deserialize response: {}", e))
933    }
934
935    /// Handle initialize command
936    #[allow(clippy::type_complexity)]
937    async fn handle_initialize_sequential(
938        &mut self,
939        root_uri: Option<Uri>,
940        initialization_options: Option<Value>,
941        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
942    ) -> Result<InitializeResult, String> {
943        tracing::info!(
944            "Initializing async LSP server with root_uri: {:?}, initialization_options: {:?}",
945            root_uri,
946            initialization_options
947        );
948
949        let workspace_folders = root_uri.as_ref().map(|uri| {
950            vec![WorkspaceFolder {
951                uri: uri.clone(),
952                name: uri
953                    .path()
954                    .as_str()
955                    .split('/')
956                    .next_back()
957                    .unwrap_or("workspace")
958                    .to_string(),
959            }]
960        });
961
962        #[allow(deprecated)]
963        let params = InitializeParams {
964            process_id: Some(std::process::id()),
965            capabilities: create_client_capabilities(),
966            workspace_folders,
967            initialization_options,
968            // Set the deprecated root_uri field for compatibility with LSP servers
969            // like csharp-ls that still require it (see issue #366)
970            root_uri: root_uri.clone(),
971            ..Default::default()
972        };
973
974        let result: InitializeResult = self
975            .send_request_sequential(Initialize::METHOD, Some(params), pending)
976            .await?;
977
978        tracing::info!(
979            "LSP initialize result: position_encoding={:?}",
980            result.capabilities.position_encoding
981        );
982        self.capabilities = Some(result.capabilities.clone());
983
984        // Send initialized notification
985        self.send_notification::<Initialized>(InitializedParams {})
986            .await?;
987
988        self.initialized = true;
989
990        let capabilities = extract_capability_summary(&result.capabilities);
991
992        // Notify main loop
993        let _ = self.async_tx.send(AsyncMessage::LspInitialized {
994            language: self.language.clone(),
995            server_name: self.server_name.clone(),
996            capabilities,
997        });
998
999        // Send running status
1000        let _ = self.async_tx.send(AsyncMessage::LspStatusUpdate {
1001            language: self.language.clone(),
1002            server_name: self.server_name.clone(),
1003            status: LspServerStatus::Running,
1004            message: None,
1005        });
1006
1007        tracing::info!("Async LSP server initialized successfully");
1008
1009        Ok(result)
1010    }
1011
1012    /// Handle did_open command
1013    #[allow(clippy::type_complexity)]
1014    async fn handle_did_open_sequential(
1015        &mut self,
1016        uri: Uri,
1017        text: String,
1018        language_id: String,
1019        _pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
1020    ) -> Result<(), String> {
1021        let path = PathBuf::from(uri.path().as_str());
1022
1023        if should_skip_did_open(&self.document_versions, &path, &self.language, &uri) {
1024            return Ok(());
1025        }
1026
1027        tracing::trace!("LSP: did_open for {}", uri.as_str());
1028
1029        // Remap languageId based on file extension using configured overrides.
1030        // For example, .tsx → "typescriptreact", .jsx → "javascriptreact"
1031        let lsp_language_id = path
1032            .extension()
1033            .and_then(|e| e.to_str())
1034            .and_then(|ext| self.language_id_overrides.get(ext))
1035            .cloned()
1036            .unwrap_or(language_id);
1037
1038        let params = DidOpenTextDocumentParams {
1039            text_document: TextDocumentItem {
1040                uri: uri.clone(),
1041                language_id: lsp_language_id,
1042                version: 0,
1043                text,
1044            },
1045        };
1046
1047        self.document_versions
1048            .lock()
1049            .unwrap()
1050            .insert(path.clone(), 0);
1051
1052        // Record when we sent didOpen so didChange can wait if needed
1053        self.pending_opens.insert(path, Instant::now());
1054
1055        self.send_notification::<DidOpenTextDocument>(params).await
1056    }
1057
1058    /// Handle did_change command
1059    #[allow(clippy::type_complexity)]
1060    async fn handle_did_change_sequential(
1061        &mut self,
1062        uri: Uri,
1063        content_changes: Vec<TextDocumentContentChangeEvent>,
1064        _pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
1065    ) -> Result<(), String> {
1066        tracing::trace!("LSP: did_change for {}", uri.as_str());
1067
1068        let path = PathBuf::from(uri.path().as_str());
1069
1070        // If the document hasn't been opened yet (not in document_versions),
1071        // skip this change - the upcoming didOpen will have the current content
1072        if !self.document_versions.lock().unwrap().contains_key(&path) {
1073            tracing::debug!(
1074                "LSP ({}): skipping didChange - document not yet opened",
1075                self.language
1076            );
1077            return Ok(());
1078        }
1079
1080        // Check if this document was recently opened and wait if needed
1081        // This prevents race conditions where the server receives didChange
1082        // before it has finished processing didOpen
1083        if let Some(opened_at) = self.pending_opens.get(&path) {
1084            let elapsed = opened_at.elapsed();
1085            let grace_period = std::time::Duration::from_millis(DID_OPEN_GRACE_PERIOD_MS);
1086            if elapsed < grace_period {
1087                let wait_time = grace_period - elapsed;
1088                tracing::debug!(
1089                    "LSP ({}): waiting {:?} for didOpen grace period before didChange",
1090                    self.language,
1091                    wait_time
1092                );
1093                tokio::time::sleep(wait_time).await;
1094            }
1095            // Remove from pending_opens after grace period has passed
1096            self.pending_opens.remove(&path);
1097        }
1098
1099        let new_version = {
1100            let mut versions = self.document_versions.lock().unwrap();
1101            let version = versions.entry(path).or_insert(0);
1102            *version += 1;
1103            *version
1104        };
1105
1106        let params = DidChangeTextDocumentParams {
1107            text_document: VersionedTextDocumentIdentifier {
1108                uri: uri.clone(),
1109                version: new_version as i32,
1110            },
1111            content_changes,
1112        };
1113
1114        self.send_notification::<DidChangeTextDocument>(params)
1115            .await
1116    }
1117
1118    /// Handle did_save command
1119    async fn handle_did_save(&mut self, uri: Uri, text: Option<String>) -> Result<(), String> {
1120        tracing::trace!("LSP: did_save for {}", uri.as_str());
1121
1122        let params = DidSaveTextDocumentParams {
1123            text_document: TextDocumentIdentifier { uri },
1124            text,
1125        };
1126
1127        self.send_notification::<DidSaveTextDocument>(params).await
1128    }
1129
1130    /// Handle did_close command
1131    async fn handle_did_close(&mut self, uri: Uri) -> Result<(), String> {
1132        let path = PathBuf::from(uri.path().as_str());
1133
1134        // Remove from document_versions so that a subsequent didOpen will be accepted
1135        if self
1136            .document_versions
1137            .lock()
1138            .unwrap()
1139            .remove(&path)
1140            .is_some()
1141        {
1142            tracing::info!("LSP ({}): didClose for {}", self.language, uri.as_str());
1143        } else {
1144            tracing::debug!(
1145                "LSP ({}): didClose for {} but document was not tracked",
1146                self.language,
1147                uri.as_str()
1148            );
1149        }
1150
1151        // Also remove from pending_opens
1152        self.pending_opens.remove(&path);
1153
1154        let params = DidCloseTextDocumentParams {
1155            text_document: TextDocumentIdentifier { uri },
1156        };
1157
1158        self.send_notification::<DidCloseTextDocument>(params).await
1159    }
1160
1161    /// Handle completion request
1162    #[allow(clippy::type_complexity)]
1163    async fn handle_completion(
1164        &mut self,
1165        request_id: u64,
1166        uri: Uri,
1167        line: u32,
1168        character: u32,
1169        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
1170    ) -> Result<(), String> {
1171        use lsp_types::{
1172            CompletionParams, PartialResultParams, Position, TextDocumentIdentifier,
1173            TextDocumentPositionParams, WorkDoneProgressParams,
1174        };
1175
1176        tracing::trace!(
1177            "LSP: completion request at {}:{}:{}",
1178            uri.as_str(),
1179            line,
1180            character
1181        );
1182
1183        let params = CompletionParams {
1184            text_document_position: TextDocumentPositionParams {
1185                text_document: TextDocumentIdentifier { uri },
1186                position: Position { line, character },
1187            },
1188            work_done_progress_params: WorkDoneProgressParams::default(),
1189            partial_result_params: PartialResultParams::default(),
1190            context: None,
1191        };
1192
1193        // Send request and get response (tracked for cancellation)
1194        match self
1195            .send_request_sequential_tracked::<_, Value>(
1196                "textDocument/completion",
1197                Some(params),
1198                pending,
1199                Some(request_id),
1200            )
1201            .await
1202        {
1203            Ok(result) => {
1204                // Parse the completion response
1205                let items = if let Ok(list) =
1206                    serde_json::from_value::<lsp_types::CompletionList>(result.clone())
1207                {
1208                    list.items
1209                } else {
1210                    serde_json::from_value::<Vec<lsp_types::CompletionItem>>(result)
1211                        .unwrap_or_default()
1212                };
1213
1214                // Send to main loop
1215                let _ = self
1216                    .async_tx
1217                    .send(AsyncMessage::LspCompletion { request_id, items });
1218                Ok(())
1219            }
1220            Err(e) => {
1221                tracing::debug!("Completion request failed: {}", e);
1222                // Send empty completion on error
1223                let _ = self.async_tx.send(AsyncMessage::LspCompletion {
1224                    request_id,
1225                    items: vec![],
1226                });
1227                Err(e)
1228            }
1229        }
1230    }
1231
1232    /// Handle go-to-definition request
1233    #[allow(clippy::type_complexity)]
1234    async fn handle_goto_definition(
1235        &mut self,
1236        request_id: u64,
1237        uri: Uri,
1238        line: u32,
1239        character: u32,
1240        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
1241    ) -> Result<(), String> {
1242        use lsp_types::{
1243            GotoDefinitionParams, PartialResultParams, Position, TextDocumentIdentifier,
1244            TextDocumentPositionParams, WorkDoneProgressParams,
1245        };
1246
1247        tracing::trace!(
1248            "LSP: go-to-definition request at {}:{}:{}",
1249            uri.as_str(),
1250            line,
1251            character
1252        );
1253
1254        let params = GotoDefinitionParams {
1255            text_document_position_params: TextDocumentPositionParams {
1256                text_document: TextDocumentIdentifier { uri },
1257                position: Position { line, character },
1258            },
1259            work_done_progress_params: WorkDoneProgressParams::default(),
1260            partial_result_params: PartialResultParams::default(),
1261        };
1262
1263        // Send request and get response
1264        match self
1265            .send_request_sequential::<_, Value>("textDocument/definition", Some(params), pending)
1266            .await
1267        {
1268            Ok(result) => {
1269                // Parse the definition response (can be Location, Vec<Location>, or LocationLink)
1270                let locations = if let Ok(loc) =
1271                    serde_json::from_value::<lsp_types::Location>(result.clone())
1272                {
1273                    vec![loc]
1274                } else if let Ok(locs) =
1275                    serde_json::from_value::<Vec<lsp_types::Location>>(result.clone())
1276                {
1277                    locs
1278                } else if let Ok(links) =
1279                    serde_json::from_value::<Vec<lsp_types::LocationLink>>(result)
1280                {
1281                    // Convert LocationLink to Location
1282                    links
1283                        .into_iter()
1284                        .map(|link| lsp_types::Location {
1285                            uri: link.target_uri,
1286                            range: link.target_selection_range,
1287                        })
1288                        .collect()
1289                } else {
1290                    vec![]
1291                };
1292
1293                // Send to main loop
1294                let _ = self.async_tx.send(AsyncMessage::LspGotoDefinition {
1295                    request_id,
1296                    locations,
1297                });
1298                Ok(())
1299            }
1300            Err(e) => {
1301                tracing::debug!("Go-to-definition request failed: {}", e);
1302                // Send empty locations on error
1303                let _ = self.async_tx.send(AsyncMessage::LspGotoDefinition {
1304                    request_id,
1305                    locations: vec![],
1306                });
1307                Err(e)
1308            }
1309        }
1310    }
1311
1312    /// Handle rename request
1313    #[allow(clippy::type_complexity)]
1314    async fn handle_rename(
1315        &mut self,
1316        request_id: u64,
1317        uri: Uri,
1318        line: u32,
1319        character: u32,
1320        new_name: String,
1321        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
1322    ) -> Result<(), String> {
1323        use lsp_types::{
1324            Position, RenameParams, TextDocumentIdentifier, TextDocumentPositionParams,
1325            WorkDoneProgressParams,
1326        };
1327
1328        tracing::trace!(
1329            "LSP: rename request at {}:{}:{} to '{}'",
1330            uri.as_str(),
1331            line,
1332            character,
1333            new_name
1334        );
1335
1336        let params = RenameParams {
1337            text_document_position: TextDocumentPositionParams {
1338                text_document: TextDocumentIdentifier { uri },
1339                position: Position { line, character },
1340            },
1341            new_name,
1342            work_done_progress_params: WorkDoneProgressParams::default(),
1343        };
1344
1345        // Send request and get response
1346        match self
1347            .send_request_sequential::<_, Value>("textDocument/rename", Some(params), pending)
1348            .await
1349        {
1350            Ok(result) => {
1351                // Parse the workspace edit response
1352                match serde_json::from_value::<lsp_types::WorkspaceEdit>(result) {
1353                    Ok(workspace_edit) => {
1354                        // Send to main loop
1355                        let _ = self.async_tx.send(AsyncMessage::LspRename {
1356                            request_id,
1357                            result: Ok(workspace_edit),
1358                        });
1359                        Ok(())
1360                    }
1361                    Err(e) => {
1362                        tracing::error!("Failed to parse rename response: {}", e);
1363                        let _ = self.async_tx.send(AsyncMessage::LspRename {
1364                            request_id,
1365                            result: Err(format!("Failed to parse rename response: {}", e)),
1366                        });
1367                        Err(format!("Failed to parse rename response: {}", e))
1368                    }
1369                }
1370            }
1371            Err(e) => {
1372                tracing::debug!("Rename request failed: {}", e);
1373                // Send error to main loop
1374                let _ = self.async_tx.send(AsyncMessage::LspRename {
1375                    request_id,
1376                    result: Err(e.clone()),
1377                });
1378                Err(e)
1379            }
1380        }
1381    }
1382
1383    /// Handle hover documentation request
1384    #[allow(clippy::type_complexity)]
1385    async fn handle_hover(
1386        &mut self,
1387        request_id: u64,
1388        uri: Uri,
1389        line: u32,
1390        character: u32,
1391        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
1392    ) -> Result<(), String> {
1393        use lsp_types::{
1394            HoverParams, Position, TextDocumentIdentifier, TextDocumentPositionParams,
1395            WorkDoneProgressParams,
1396        };
1397
1398        tracing::trace!(
1399            "LSP: hover request at {}:{}:{}",
1400            uri.as_str(),
1401            line,
1402            character
1403        );
1404
1405        let params = HoverParams {
1406            text_document_position_params: TextDocumentPositionParams {
1407                text_document: TextDocumentIdentifier { uri },
1408                position: Position { line, character },
1409            },
1410            work_done_progress_params: WorkDoneProgressParams::default(),
1411        };
1412
1413        // Send request and get response
1414        match self
1415            .send_request_sequential::<_, Value>("textDocument/hover", Some(params), pending)
1416            .await
1417        {
1418            Ok(result) => {
1419                tracing::debug!("Raw LSP hover response: {:?}", result);
1420                // Parse the hover response
1421                let (contents, is_markdown, range) = if result.is_null() {
1422                    // No hover information available
1423                    (String::new(), false, None)
1424                } else {
1425                    match serde_json::from_value::<lsp_types::Hover>(result) {
1426                        Ok(hover) => {
1427                            // Extract text from hover contents
1428                            let (contents, is_markdown) =
1429                                Self::extract_hover_contents(&hover.contents);
1430                            // Extract the range if provided (tells us which symbol was hovered)
1431                            let range = hover.range.map(|r| {
1432                                (
1433                                    (r.start.line, r.start.character),
1434                                    (r.end.line, r.end.character),
1435                                )
1436                            });
1437                            (contents, is_markdown, range)
1438                        }
1439                        Err(e) => {
1440                            tracing::error!("Failed to parse hover response: {}", e);
1441                            (String::new(), false, None)
1442                        }
1443                    }
1444                };
1445
1446                // Send to main loop
1447                let _ = self.async_tx.send(AsyncMessage::LspHover {
1448                    request_id,
1449                    contents,
1450                    is_markdown,
1451                    range,
1452                });
1453                Ok(())
1454            }
1455            Err(e) => {
1456                tracing::debug!("Hover request failed: {}", e);
1457                // Send empty result on error (no hover available)
1458                let _ = self.async_tx.send(AsyncMessage::LspHover {
1459                    request_id,
1460                    contents: String::new(),
1461                    is_markdown: false,
1462                    range: None,
1463                });
1464                Err(e)
1465            }
1466        }
1467    }
1468
1469    /// Extract text from hover contents (handles both MarkedString and MarkupContent)
1470    /// Returns (content_string, is_markdown)
1471    fn extract_hover_contents(contents: &lsp_types::HoverContents) -> (String, bool) {
1472        use lsp_types::{HoverContents, MarkedString, MarkupContent, MarkupKind};
1473
1474        match contents {
1475            HoverContents::Scalar(marked) => match marked {
1476                MarkedString::String(s) => (s.clone(), false),
1477                MarkedString::LanguageString(ls) => {
1478                    // Language strings are formatted as markdown code blocks
1479                    (format!("```{}\n{}\n```", ls.language, ls.value), true)
1480                }
1481            },
1482            HoverContents::Array(arr) => {
1483                // Array of marked strings - format as markdown
1484                let content = arr
1485                    .iter()
1486                    .map(|marked| match marked {
1487                        MarkedString::String(s) => s.clone(),
1488                        MarkedString::LanguageString(ls) => {
1489                            format!("```{}\n{}\n```", ls.language, ls.value)
1490                        }
1491                    })
1492                    .collect::<Vec<_>>()
1493                    .join("\n\n");
1494                (content, true)
1495            }
1496            HoverContents::Markup(MarkupContent { kind, value }) => {
1497                // Check if it's markdown or plaintext
1498                let is_markdown = matches!(kind, MarkupKind::Markdown);
1499                (value.clone(), is_markdown)
1500            }
1501        }
1502    }
1503
1504    /// Handle find references request
1505    #[allow(clippy::type_complexity)]
1506    async fn handle_references(
1507        &mut self,
1508        request_id: u64,
1509        uri: Uri,
1510        line: u32,
1511        character: u32,
1512        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
1513    ) -> Result<(), String> {
1514        use lsp_types::{
1515            PartialResultParams, Position, ReferenceContext, ReferenceParams,
1516            TextDocumentIdentifier, WorkDoneProgressParams,
1517        };
1518
1519        tracing::trace!(
1520            "LSP: find references request at {}:{}:{}",
1521            uri.as_str(),
1522            line,
1523            character
1524        );
1525
1526        let params = ReferenceParams {
1527            text_document_position: lsp_types::TextDocumentPositionParams {
1528                text_document: TextDocumentIdentifier { uri },
1529                position: Position { line, character },
1530            },
1531            work_done_progress_params: WorkDoneProgressParams::default(),
1532            partial_result_params: PartialResultParams::default(),
1533            context: ReferenceContext {
1534                include_declaration: true,
1535            },
1536        };
1537
1538        // Send request and get response
1539        match self
1540            .send_request_sequential::<_, Value>("textDocument/references", Some(params), pending)
1541            .await
1542        {
1543            Ok(result) => {
1544                // Parse the references response (Vec<Location> or null)
1545                let locations = if result.is_null() {
1546                    Vec::new()
1547                } else {
1548                    serde_json::from_value::<Vec<lsp_types::Location>>(result).unwrap_or_default()
1549                };
1550
1551                tracing::trace!("LSP: found {} references", locations.len());
1552
1553                // Send to main loop
1554                let _ = self.async_tx.send(AsyncMessage::LspReferences {
1555                    request_id,
1556                    locations,
1557                });
1558                Ok(())
1559            }
1560            Err(e) => {
1561                tracing::debug!("Find references request failed: {}", e);
1562                // Send empty result on error
1563                let _ = self.async_tx.send(AsyncMessage::LspReferences {
1564                    request_id,
1565                    locations: Vec::new(),
1566                });
1567                Err(e)
1568            }
1569        }
1570    }
1571
1572    /// Handle signature help request
1573    #[allow(clippy::type_complexity)]
1574    async fn handle_signature_help(
1575        &mut self,
1576        request_id: u64,
1577        uri: Uri,
1578        line: u32,
1579        character: u32,
1580        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
1581    ) -> Result<(), String> {
1582        use lsp_types::{
1583            Position, SignatureHelpParams, TextDocumentIdentifier, TextDocumentPositionParams,
1584            WorkDoneProgressParams,
1585        };
1586
1587        tracing::trace!(
1588            "LSP: signature help request at {}:{}:{}",
1589            uri.as_str(),
1590            line,
1591            character
1592        );
1593
1594        let params = SignatureHelpParams {
1595            text_document_position_params: TextDocumentPositionParams {
1596                text_document: TextDocumentIdentifier { uri },
1597                position: Position { line, character },
1598            },
1599            work_done_progress_params: WorkDoneProgressParams::default(),
1600            context: None, // We can add context later for re-triggers
1601        };
1602
1603        // Send request and get response
1604        match self
1605            .send_request_sequential::<_, Value>(
1606                "textDocument/signatureHelp",
1607                Some(params),
1608                pending,
1609            )
1610            .await
1611        {
1612            Ok(result) => {
1613                // Parse the signature help response (SignatureHelp or null)
1614                let signature_help = if result.is_null() {
1615                    None
1616                } else {
1617                    serde_json::from_value::<lsp_types::SignatureHelp>(result).ok()
1618                };
1619
1620                tracing::trace!(
1621                    "LSP: signature help received: {} signatures",
1622                    signature_help
1623                        .as_ref()
1624                        .map(|h| h.signatures.len())
1625                        .unwrap_or(0)
1626                );
1627
1628                // Send to main loop
1629                let _ = self.async_tx.send(AsyncMessage::LspSignatureHelp {
1630                    request_id,
1631                    signature_help,
1632                });
1633                Ok(())
1634            }
1635            Err(e) => {
1636                tracing::debug!("Signature help request failed: {}", e);
1637                // Send empty result on error
1638                let _ = self.async_tx.send(AsyncMessage::LspSignatureHelp {
1639                    request_id,
1640                    signature_help: None,
1641                });
1642                Err(e)
1643            }
1644        }
1645    }
1646
1647    /// Handle code actions request
1648    #[allow(clippy::type_complexity)]
1649    #[allow(clippy::too_many_arguments)]
1650    async fn handle_code_actions(
1651        &mut self,
1652        request_id: u64,
1653        uri: Uri,
1654        start_line: u32,
1655        start_char: u32,
1656        end_line: u32,
1657        end_char: u32,
1658        diagnostics: Vec<lsp_types::Diagnostic>,
1659        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
1660    ) -> Result<(), String> {
1661        use lsp_types::{
1662            CodeActionContext, CodeActionParams, PartialResultParams, Position, Range,
1663            TextDocumentIdentifier, WorkDoneProgressParams,
1664        };
1665
1666        tracing::trace!(
1667            "LSP: code actions request at {}:{}:{}-{}:{}",
1668            uri.as_str(),
1669            start_line,
1670            start_char,
1671            end_line,
1672            end_char
1673        );
1674
1675        let params = CodeActionParams {
1676            text_document: TextDocumentIdentifier { uri },
1677            range: Range {
1678                start: Position {
1679                    line: start_line,
1680                    character: start_char,
1681                },
1682                end: Position {
1683                    line: end_line,
1684                    character: end_char,
1685                },
1686            },
1687            context: CodeActionContext {
1688                diagnostics,
1689                only: None,
1690                trigger_kind: None,
1691            },
1692            work_done_progress_params: WorkDoneProgressParams::default(),
1693            partial_result_params: PartialResultParams::default(),
1694        };
1695
1696        // Send request and get response
1697        match self
1698            .send_request_sequential::<_, Value>("textDocument/codeAction", Some(params), pending)
1699            .await
1700        {
1701            Ok(result) => {
1702                // Parse the code actions response (Vec<CodeActionOrCommand> or null)
1703                let actions = if result.is_null() {
1704                    Vec::new()
1705                } else {
1706                    serde_json::from_value::<Vec<lsp_types::CodeActionOrCommand>>(result)
1707                        .unwrap_or_default()
1708                };
1709
1710                tracing::trace!("LSP: received {} code actions", actions.len());
1711
1712                // Send to main loop
1713                let _ = self.async_tx.send(AsyncMessage::LspCodeActions {
1714                    request_id,
1715                    actions,
1716                });
1717                Ok(())
1718            }
1719            Err(e) => {
1720                tracing::debug!("Code actions request failed: {}", e);
1721                // Send empty result on error
1722                let _ = self.async_tx.send(AsyncMessage::LspCodeActions {
1723                    request_id,
1724                    actions: Vec::new(),
1725                });
1726                Err(e)
1727            }
1728        }
1729    }
1730
1731    /// Handle workspace/executeCommand request
1732    #[allow(clippy::type_complexity)]
1733    async fn handle_execute_command(
1734        &mut self,
1735        command: String,
1736        arguments: Option<Vec<Value>>,
1737        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
1738    ) -> Result<(), String> {
1739        let params = lsp_types::ExecuteCommandParams {
1740            command: command.clone(),
1741            arguments: arguments.unwrap_or_default(),
1742            work_done_progress_params: lsp_types::WorkDoneProgressParams::default(),
1743        };
1744
1745        match self
1746            .send_request_sequential::<_, Value>("workspace/executeCommand", Some(params), pending)
1747            .await
1748        {
1749            Ok(_) => {
1750                tracing::info!("ExecuteCommand '{}' completed", command);
1751                Ok(())
1752            }
1753            Err(e) => {
1754                tracing::debug!("ExecuteCommand '{}' failed: {}", command, e);
1755                Err(e)
1756            }
1757        }
1758    }
1759
1760    /// Handle codeAction/resolve request
1761    #[allow(clippy::type_complexity)]
1762    async fn handle_code_action_resolve(
1763        &mut self,
1764        request_id: u64,
1765        action: lsp_types::CodeAction,
1766        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
1767    ) -> Result<(), String> {
1768        match self
1769            .send_request_sequential::<_, Value>("codeAction/resolve", Some(action), pending)
1770            .await
1771        {
1772            Ok(result) => {
1773                let resolved = serde_json::from_value::<lsp_types::CodeAction>(result)
1774                    .map_err(|e| format!("Failed to parse codeAction/resolve response: {}", e));
1775                let _ = self.async_tx.send(AsyncMessage::LspCodeActionResolved {
1776                    request_id,
1777                    action: resolved,
1778                });
1779                Ok(())
1780            }
1781            Err(e) => {
1782                tracing::debug!("codeAction/resolve failed: {}", e);
1783                let _ = self.async_tx.send(AsyncMessage::LspCodeActionResolved {
1784                    request_id,
1785                    action: Err(e.clone()),
1786                });
1787                Err(e)
1788            }
1789        }
1790    }
1791
1792    /// Handle completionItem/resolve request
1793    #[allow(clippy::type_complexity)]
1794    async fn handle_completion_resolve(
1795        &mut self,
1796        request_id: u64,
1797        item: lsp_types::CompletionItem,
1798        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
1799    ) -> Result<(), String> {
1800        match self
1801            .send_request_sequential::<_, Value>("completionItem/resolve", Some(item), pending)
1802            .await
1803        {
1804            Ok(result) => {
1805                let resolved = serde_json::from_value::<lsp_types::CompletionItem>(result)
1806                    .map_err(|e| format!("Failed to parse completionItem/resolve response: {}", e));
1807                let _ = self.async_tx.send(AsyncMessage::LspCompletionResolved {
1808                    request_id,
1809                    item: resolved,
1810                });
1811                Ok(())
1812            }
1813            Err(e) => {
1814                tracing::debug!("completionItem/resolve failed: {}", e);
1815                Err(e)
1816            }
1817        }
1818    }
1819
1820    /// Handle textDocument/formatting request
1821    #[allow(clippy::type_complexity)]
1822    async fn handle_document_formatting(
1823        &mut self,
1824        request_id: u64,
1825        uri: Uri,
1826        tab_size: u32,
1827        insert_spaces: bool,
1828        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
1829    ) -> Result<(), String> {
1830        use lsp_types::{
1831            DocumentFormattingParams, FormattingOptions, TextDocumentIdentifier,
1832            WorkDoneProgressParams,
1833        };
1834
1835        let params = DocumentFormattingParams {
1836            text_document: TextDocumentIdentifier { uri: uri.clone() },
1837            options: FormattingOptions {
1838                tab_size,
1839                insert_spaces,
1840                ..Default::default()
1841            },
1842            work_done_progress_params: WorkDoneProgressParams::default(),
1843        };
1844
1845        match self
1846            .send_request_sequential::<_, Value>("textDocument/formatting", Some(params), pending)
1847            .await
1848        {
1849            Ok(result) => {
1850                let edits = if result.is_null() {
1851                    Vec::new()
1852                } else {
1853                    serde_json::from_value::<Vec<lsp_types::TextEdit>>(result).unwrap_or_default()
1854                };
1855                let _ = self.async_tx.send(AsyncMessage::LspFormatting {
1856                    request_id,
1857                    uri: uri.as_str().to_string(),
1858                    edits,
1859                });
1860                Ok(())
1861            }
1862            Err(e) => {
1863                tracing::debug!("textDocument/formatting failed: {}", e);
1864                Err(e)
1865            }
1866        }
1867    }
1868
1869    /// Handle document diagnostic request (pull diagnostics)
1870    /// Handle textDocument/rangeFormatting request
1871    #[allow(clippy::too_many_arguments)]
1872    #[allow(clippy::type_complexity)]
1873    async fn handle_document_range_formatting(
1874        &mut self,
1875        request_id: u64,
1876        uri: Uri,
1877        start_line: u32,
1878        start_char: u32,
1879        end_line: u32,
1880        end_char: u32,
1881        tab_size: u32,
1882        insert_spaces: bool,
1883        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
1884    ) -> Result<(), String> {
1885        use lsp_types::{
1886            DocumentRangeFormattingParams, FormattingOptions, Position, Range,
1887            TextDocumentIdentifier, WorkDoneProgressParams,
1888        };
1889
1890        let params = DocumentRangeFormattingParams {
1891            text_document: TextDocumentIdentifier { uri: uri.clone() },
1892            range: Range {
1893                start: Position::new(start_line, start_char),
1894                end: Position::new(end_line, end_char),
1895            },
1896            options: FormattingOptions {
1897                tab_size,
1898                insert_spaces,
1899                ..Default::default()
1900            },
1901            work_done_progress_params: WorkDoneProgressParams::default(),
1902        };
1903
1904        match self
1905            .send_request_sequential::<_, Value>(
1906                "textDocument/rangeFormatting",
1907                Some(params),
1908                pending,
1909            )
1910            .await
1911        {
1912            Ok(result) => {
1913                let edits = if result.is_null() {
1914                    Vec::new()
1915                } else {
1916                    serde_json::from_value::<Vec<lsp_types::TextEdit>>(result).unwrap_or_default()
1917                };
1918                let _ = self.async_tx.send(AsyncMessage::LspFormatting {
1919                    request_id,
1920                    uri: uri.as_str().to_string(),
1921                    edits,
1922                });
1923                Ok(())
1924            }
1925            Err(e) => {
1926                tracing::debug!("textDocument/rangeFormatting failed: {}", e);
1927                Err(e)
1928            }
1929        }
1930    }
1931
1932    /// Handle textDocument/prepareRename request
1933    #[allow(clippy::type_complexity)]
1934    async fn handle_prepare_rename(
1935        &mut self,
1936        request_id: u64,
1937        uri: Uri,
1938        line: u32,
1939        character: u32,
1940        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
1941    ) -> Result<(), String> {
1942        use lsp_types::{Position, TextDocumentIdentifier, TextDocumentPositionParams};
1943
1944        let params = TextDocumentPositionParams {
1945            text_document: TextDocumentIdentifier { uri },
1946            position: Position::new(line, character),
1947        };
1948
1949        match self
1950            .send_request_sequential::<_, Value>(
1951                "textDocument/prepareRename",
1952                Some(params),
1953                pending,
1954            )
1955            .await
1956        {
1957            Ok(result) => {
1958                let _ = self.async_tx.send(AsyncMessage::LspPrepareRename {
1959                    request_id,
1960                    result: Ok(result),
1961                });
1962                Ok(())
1963            }
1964            Err(e) => {
1965                let _ = self.async_tx.send(AsyncMessage::LspPrepareRename {
1966                    request_id,
1967                    result: Err(e.clone()),
1968                });
1969                Err(e)
1970            }
1971        }
1972    }
1973
1974    #[allow(clippy::type_complexity)]
1975    async fn handle_document_diagnostic(
1976        &mut self,
1977        request_id: u64,
1978        uri: Uri,
1979        previous_result_id: Option<String>,
1980        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
1981    ) -> Result<(), String> {
1982        use lsp_types::{
1983            DocumentDiagnosticParams, PartialResultParams, TextDocumentIdentifier,
1984            WorkDoneProgressParams,
1985        };
1986
1987        // Check if server supports pull diagnostics (diagnosticProvider capability)
1988        if self
1989            .capabilities
1990            .as_ref()
1991            .and_then(|c| c.diagnostic_provider.as_ref())
1992            .is_none()
1993        {
1994            tracing::trace!(
1995                "LSP: server does not support pull diagnostics, skipping request for {}",
1996                uri.as_str()
1997            );
1998            return Ok(());
1999        }
2000
2001        tracing::trace!(
2002            "LSP: document diagnostic request for {} (previous_result_id: {:?})",
2003            uri.as_str(),
2004            previous_result_id
2005        );
2006
2007        let params = DocumentDiagnosticParams {
2008            text_document: TextDocumentIdentifier { uri: uri.clone() },
2009            identifier: None,
2010            previous_result_id,
2011            work_done_progress_params: WorkDoneProgressParams::default(),
2012            partial_result_params: PartialResultParams::default(),
2013        };
2014
2015        // Send request and get response
2016        match self
2017            .send_request_sequential::<_, Value>("textDocument/diagnostic", Some(params), pending)
2018            .await
2019        {
2020            Ok(result) => {
2021                // Parse the diagnostic report result
2022                // Can be RelatedFullDocumentDiagnosticReport or RelatedUnchangedDocumentDiagnosticReport
2023                let uri_string = uri.as_str().to_string();
2024
2025                // Try to parse as full report first
2026                if let Ok(full_report) = serde_json::from_value::<
2027                    lsp_types::RelatedFullDocumentDiagnosticReport,
2028                >(result.clone())
2029                {
2030                    let diagnostics = full_report.full_document_diagnostic_report.items;
2031                    let result_id = full_report.full_document_diagnostic_report.result_id;
2032
2033                    tracing::trace!(
2034                        "LSP: received {} diagnostics for {} (result_id: {:?})",
2035                        diagnostics.len(),
2036                        uri_string,
2037                        result_id
2038                    );
2039
2040                    let _ = self.async_tx.send(AsyncMessage::LspPulledDiagnostics {
2041                        request_id,
2042                        uri: uri_string,
2043                        result_id,
2044                        diagnostics,
2045                        unchanged: false,
2046                    });
2047                } else if let Ok(unchanged_report) = serde_json::from_value::<
2048                    lsp_types::RelatedUnchangedDocumentDiagnosticReport,
2049                >(result.clone())
2050                {
2051                    let result_id = unchanged_report
2052                        .unchanged_document_diagnostic_report
2053                        .result_id;
2054
2055                    tracing::trace!(
2056                        "LSP: diagnostics unchanged for {} (result_id: {:?})",
2057                        uri_string,
2058                        result_id
2059                    );
2060
2061                    let _ = self.async_tx.send(AsyncMessage::LspPulledDiagnostics {
2062                        request_id,
2063                        uri: uri_string,
2064                        result_id: Some(result_id),
2065                        diagnostics: Vec::new(),
2066                        unchanged: true,
2067                    });
2068                } else {
2069                    // Fallback: try to parse as DocumentDiagnosticReportResult
2070                    tracing::warn!(
2071                        "LSP: could not parse diagnostic report, sending empty: {}",
2072                        result
2073                    );
2074                    let _ = self.async_tx.send(AsyncMessage::LspPulledDiagnostics {
2075                        request_id,
2076                        uri: uri_string,
2077                        result_id: None,
2078                        diagnostics: Vec::new(),
2079                        unchanged: false,
2080                    });
2081                }
2082
2083                Ok(())
2084            }
2085            Err(e) => {
2086                tracing::debug!("Document diagnostic request failed: {}", e);
2087                // Send empty result on error
2088                let _ = self.async_tx.send(AsyncMessage::LspPulledDiagnostics {
2089                    request_id,
2090                    uri: uri.as_str().to_string(),
2091                    result_id: None,
2092                    diagnostics: Vec::new(),
2093                    unchanged: false,
2094                });
2095                Err(e)
2096            }
2097        }
2098    }
2099
2100    /// Handle inlay hints request (LSP 3.17+)
2101    #[allow(clippy::type_complexity)]
2102    #[allow(clippy::too_many_arguments)]
2103    async fn handle_inlay_hints(
2104        &mut self,
2105        request_id: u64,
2106        uri: Uri,
2107        start_line: u32,
2108        start_char: u32,
2109        end_line: u32,
2110        end_char: u32,
2111        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
2112    ) -> Result<(), String> {
2113        use lsp_types::{
2114            InlayHintParams, Position, Range, TextDocumentIdentifier, WorkDoneProgressParams,
2115        };
2116
2117        tracing::trace!(
2118            "LSP: inlay hints request for {} ({}:{} - {}:{})",
2119            uri.as_str(),
2120            start_line,
2121            start_char,
2122            end_line,
2123            end_char
2124        );
2125
2126        let params = InlayHintParams {
2127            text_document: TextDocumentIdentifier { uri: uri.clone() },
2128            range: Range {
2129                start: Position {
2130                    line: start_line,
2131                    character: start_char,
2132                },
2133                end: Position {
2134                    line: end_line,
2135                    character: end_char,
2136                },
2137            },
2138            work_done_progress_params: WorkDoneProgressParams::default(),
2139        };
2140
2141        match self
2142            .send_request_sequential::<_, Option<Vec<lsp_types::InlayHint>>>(
2143                "textDocument/inlayHint",
2144                Some(params),
2145                pending,
2146            )
2147            .await
2148        {
2149            Ok(hints) => {
2150                let hints = hints.unwrap_or_default();
2151                let uri_string = uri.as_str().to_string();
2152
2153                tracing::trace!(
2154                    "LSP: received {} inlay hints for {}",
2155                    hints.len(),
2156                    uri_string
2157                );
2158
2159                let _ = self.async_tx.send(AsyncMessage::LspInlayHints {
2160                    request_id,
2161                    uri: uri_string,
2162                    hints,
2163                });
2164
2165                Ok(())
2166            }
2167            Err(e) => {
2168                tracing::debug!("Inlay hints request failed: {}", e);
2169                // Send empty result on error
2170                let _ = self.async_tx.send(AsyncMessage::LspInlayHints {
2171                    request_id,
2172                    uri: uri.as_str().to_string(),
2173                    hints: Vec::new(),
2174                });
2175                Err(e)
2176            }
2177        }
2178    }
2179
2180    /// Handle folding range request
2181    #[allow(clippy::type_complexity)]
2182    async fn handle_folding_ranges(
2183        &mut self,
2184        request_id: u64,
2185        uri: Uri,
2186        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
2187    ) -> Result<(), String> {
2188        use lsp_types::{
2189            FoldingRangeParams, PartialResultParams, TextDocumentIdentifier, WorkDoneProgressParams,
2190        };
2191
2192        tracing::trace!("LSP: folding range request for {}", uri.as_str());
2193
2194        let params = FoldingRangeParams {
2195            text_document: TextDocumentIdentifier { uri: uri.clone() },
2196            work_done_progress_params: WorkDoneProgressParams::default(),
2197            partial_result_params: PartialResultParams::default(),
2198        };
2199
2200        match self
2201            .send_request_sequential::<_, Option<Vec<lsp_types::FoldingRange>>>(
2202                "textDocument/foldingRange",
2203                Some(params),
2204                pending,
2205            )
2206            .await
2207        {
2208            Ok(ranges) => {
2209                let ranges = ranges.unwrap_or_default();
2210                let uri_string = uri.as_str().to_string();
2211
2212                tracing::trace!(
2213                    "LSP: received {} folding ranges for {}",
2214                    ranges.len(),
2215                    uri_string
2216                );
2217
2218                let _ = self.async_tx.send(AsyncMessage::LspFoldingRanges {
2219                    request_id,
2220                    uri: uri_string,
2221                    ranges,
2222                });
2223
2224                Ok(())
2225            }
2226            Err(e) => {
2227                tracing::debug!("Folding range request failed: {}", e);
2228                let _ = self.async_tx.send(AsyncMessage::LspFoldingRanges {
2229                    request_id,
2230                    uri: uri.as_str().to_string(),
2231                    ranges: Vec::new(),
2232                });
2233                Err(e)
2234            }
2235        }
2236    }
2237
2238    #[allow(clippy::type_complexity)]
2239    async fn handle_semantic_tokens_full(
2240        &mut self,
2241        request_id: u64,
2242        uri: Uri,
2243        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
2244    ) -> Result<(), String> {
2245        use lsp_types::{
2246            request::SemanticTokensFullRequest, PartialResultParams, TextDocumentIdentifier,
2247            WorkDoneProgressParams,
2248        };
2249
2250        tracing::trace!("LSP: semanticTokens/full request for {}", uri.as_str());
2251
2252        let params = SemanticTokensParams {
2253            work_done_progress_params: WorkDoneProgressParams::default(),
2254            partial_result_params: PartialResultParams::default(),
2255            text_document: TextDocumentIdentifier { uri: uri.clone() },
2256        };
2257
2258        match self
2259            .send_request_sequential_tracked::<_, Option<SemanticTokensResult>>(
2260                SemanticTokensFullRequest::METHOD,
2261                Some(params),
2262                pending,
2263                Some(request_id),
2264            )
2265            .await
2266        {
2267            Ok(result) => {
2268                let _ = self.async_tx.send(AsyncMessage::LspSemanticTokens {
2269                    request_id,
2270                    uri: uri.as_str().to_string(),
2271                    response: LspSemanticTokensResponse::Full(Ok(result)),
2272                });
2273                Ok(())
2274            }
2275            Err(e) => {
2276                tracing::debug!("Semantic tokens request failed: {}", e);
2277                let _ = self.async_tx.send(AsyncMessage::LspSemanticTokens {
2278                    request_id,
2279                    uri: uri.as_str().to_string(),
2280                    response: LspSemanticTokensResponse::Full(Err(e.clone())),
2281                });
2282                Err(e)
2283            }
2284        }
2285    }
2286
2287    #[allow(clippy::type_complexity)]
2288    async fn handle_semantic_tokens_full_delta(
2289        &mut self,
2290        request_id: u64,
2291        uri: Uri,
2292        previous_result_id: String,
2293        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
2294    ) -> Result<(), String> {
2295        use lsp_types::{
2296            request::SemanticTokensFullDeltaRequest, PartialResultParams,
2297            SemanticTokensDeltaParams, SemanticTokensFullDeltaResult, TextDocumentIdentifier,
2298            WorkDoneProgressParams,
2299        };
2300
2301        tracing::trace!(
2302            "LSP: semanticTokens/full/delta request for {}",
2303            uri.as_str()
2304        );
2305
2306        let params = SemanticTokensDeltaParams {
2307            work_done_progress_params: WorkDoneProgressParams::default(),
2308            partial_result_params: PartialResultParams::default(),
2309            text_document: TextDocumentIdentifier { uri: uri.clone() },
2310            previous_result_id,
2311        };
2312
2313        match self
2314            .send_request_sequential_tracked::<_, Option<SemanticTokensFullDeltaResult>>(
2315                SemanticTokensFullDeltaRequest::METHOD,
2316                Some(params),
2317                pending,
2318                Some(request_id),
2319            )
2320            .await
2321        {
2322            Ok(result) => {
2323                let _ = self.async_tx.send(AsyncMessage::LspSemanticTokens {
2324                    request_id,
2325                    uri: uri.as_str().to_string(),
2326                    response: LspSemanticTokensResponse::FullDelta(Ok(result)),
2327                });
2328                Ok(())
2329            }
2330            Err(e) => {
2331                tracing::debug!("Semantic tokens delta request failed: {}", e);
2332                let _ = self.async_tx.send(AsyncMessage::LspSemanticTokens {
2333                    request_id,
2334                    uri: uri.as_str().to_string(),
2335                    response: LspSemanticTokensResponse::FullDelta(Err(e.clone())),
2336                });
2337                Err(e)
2338            }
2339        }
2340    }
2341
2342    #[allow(clippy::type_complexity)]
2343    async fn handle_semantic_tokens_range(
2344        &mut self,
2345        request_id: u64,
2346        uri: Uri,
2347        range: lsp_types::Range,
2348        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
2349    ) -> Result<(), String> {
2350        use lsp_types::{
2351            request::SemanticTokensRangeRequest, PartialResultParams, SemanticTokensRangeParams,
2352            TextDocumentIdentifier, WorkDoneProgressParams,
2353        };
2354
2355        tracing::trace!("LSP: semanticTokens/range request for {}", uri.as_str());
2356
2357        let params = SemanticTokensRangeParams {
2358            work_done_progress_params: WorkDoneProgressParams::default(),
2359            partial_result_params: PartialResultParams::default(),
2360            text_document: TextDocumentIdentifier { uri: uri.clone() },
2361            range,
2362        };
2363
2364        match self
2365            .send_request_sequential_tracked::<_, Option<lsp_types::SemanticTokensRangeResult>>(
2366                SemanticTokensRangeRequest::METHOD,
2367                Some(params),
2368                pending,
2369                Some(request_id),
2370            )
2371            .await
2372        {
2373            Ok(result) => {
2374                let _ = self.async_tx.send(AsyncMessage::LspSemanticTokens {
2375                    request_id,
2376                    uri: uri.as_str().to_string(),
2377                    response: LspSemanticTokensResponse::Range(Ok(result)),
2378                });
2379                Ok(())
2380            }
2381            Err(e) => {
2382                tracing::debug!("Semantic tokens range request failed: {}", e);
2383                let _ = self.async_tx.send(AsyncMessage::LspSemanticTokens {
2384                    request_id,
2385                    uri: uri.as_str().to_string(),
2386                    response: LspSemanticTokensResponse::Range(Err(e.clone())),
2387                });
2388                Err(e)
2389            }
2390        }
2391    }
2392
2393    /// Handle a plugin-initiated request by forwarding it to the server
2394    #[allow(clippy::type_complexity)]
2395    async fn handle_plugin_request(
2396        &mut self,
2397        request_id: u64,
2398        method: String,
2399        params: Option<Value>,
2400        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
2401    ) {
2402        tracing::trace!(
2403            "Plugin request {} => method={} params={:?}",
2404            request_id,
2405            method,
2406            params
2407        );
2408        let result = self
2409            .send_request_sequential_tracked::<Value, Value>(
2410                &method,
2411                params,
2412                pending,
2413                Some(request_id),
2414            )
2415            .await;
2416
2417        tracing::trace!(
2418            "Plugin request {} completed with result {:?}",
2419            request_id,
2420            &result
2421        );
2422        let _ = self.async_tx.send(AsyncMessage::PluginLspResponse {
2423            language: self.language.clone(),
2424            request_id,
2425            result,
2426        });
2427    }
2428
2429    /// Handle shutdown command
2430    async fn handle_shutdown(&mut self) -> Result<(), String> {
2431        tracing::info!("Shutting down async LSP server");
2432
2433        let notification = JsonRpcNotification {
2434            jsonrpc: "2.0".to_string(),
2435            method: "shutdown".to_string(),
2436            params: None,
2437        };
2438
2439        self.write_message(&notification).await?;
2440
2441        let exit = JsonRpcNotification {
2442            jsonrpc: "2.0".to_string(),
2443            method: "exit".to_string(),
2444            params: None,
2445        };
2446
2447        self.write_message(&exit).await
2448    }
2449
2450    /// Send a cancel request notification to the server
2451    async fn send_cancel_request(&mut self, lsp_id: i64) -> Result<(), String> {
2452        tracing::trace!("Sending $/cancelRequest for LSP id {}", lsp_id);
2453
2454        let notification = JsonRpcNotification {
2455            jsonrpc: "2.0".to_string(),
2456            method: "$/cancelRequest".to_string(),
2457            params: Some(serde_json::json!({ "id": lsp_id })),
2458        };
2459
2460        self.write_message(&notification).await
2461    }
2462
2463    /// Cancel a request by editor request_id
2464    async fn handle_cancel_request(&mut self, request_id: u64) -> Result<(), String> {
2465        if let Some(lsp_id) = self.active_requests.remove(&request_id) {
2466            tracing::info!(
2467                "Cancelling request: editor_id={}, lsp_id={}",
2468                request_id,
2469                lsp_id
2470            );
2471            self.send_cancel_request(lsp_id).await
2472        } else {
2473            tracing::trace!(
2474                "Cancel request ignored: no active LSP request for editor_id={}",
2475                request_id
2476            );
2477            Ok(())
2478        }
2479    }
2480}
2481
2482/// Async LSP task that handles all I/O
2483struct LspTask {
2484    /// Process handle - kept alive for lifetime management (kill_on_drop)
2485    _process: Child,
2486
2487    /// Stdin for sending messages
2488    stdin: ChildStdin,
2489
2490    /// Stdout for receiving messages
2491    stdout: BufReader<ChildStdout>,
2492
2493    /// Next request ID
2494    next_id: i64,
2495
2496    /// Pending requests waiting for response
2497    pending: HashMap<i64, oneshot::Sender<Result<Value, String>>>,
2498
2499    /// Server capabilities
2500    capabilities: Option<ServerCapabilities>,
2501
2502    /// Document versions (shared with stdout reader for stale diagnostic filtering)
2503    document_versions: Arc<std::sync::Mutex<HashMap<PathBuf, i64>>>,
2504
2505    /// Track when didOpen was sent for each document to avoid race with didChange
2506    /// The LSP server needs time to process didOpen before it can handle didChange
2507    pending_opens: HashMap<PathBuf, Instant>,
2508
2509    /// Whether initialized
2510    initialized: bool,
2511
2512    /// Sender for async messages to main loop
2513    async_tx: std_mpsc::Sender<AsyncMessage>,
2514
2515    /// Language ID (for error reporting)
2516    language: String,
2517
2518    /// Display name for this server (for diagnostics attribution)
2519    server_name: String,
2520
2521    /// Server command (for plugin identification)
2522    server_command: String,
2523
2524    /// Path to stderr log file
2525    stderr_log_path: std::path::PathBuf,
2526
2527    /// Extension-to-languageId overrides for textDocument/didOpen
2528    language_id_overrides: HashMap<String, String>,
2529}
2530
2531impl LspTask {
2532    /// Create a new LSP task
2533    #[allow(clippy::too_many_arguments)]
2534    async fn spawn(
2535        command: &str,
2536        args: &[String],
2537        env: &std::collections::HashMap<String, String>,
2538        language: String,
2539        server_name: String,
2540        async_tx: std_mpsc::Sender<AsyncMessage>,
2541        process_limits: &ProcessLimits,
2542        stderr_log_path: std::path::PathBuf,
2543        language_id_overrides: HashMap<String, String>,
2544        document_versions: Arc<std::sync::Mutex<HashMap<PathBuf, i64>>>,
2545    ) -> Result<Self, String> {
2546        tracing::info!("Spawning async LSP server: {} {:?}", command, args);
2547        tracing::info!("Process limits: {:?}", process_limits);
2548        tracing::info!("LSP stderr will be logged to: {:?}", stderr_log_path);
2549
2550        // Check if the command exists before trying to spawn
2551        // This provides a clearer error message than the generic "No such file or directory"
2552        if !Self::command_exists(command) {
2553            return Err(format!(
2554                "LSP server executable '{}' not found. Please install it or check your PATH.",
2555                command
2556            ));
2557        }
2558
2559        // Create stderr log file and redirect process stderr directly to it
2560        let stderr_file = std::fs::File::create(&stderr_log_path).map_err(|e| {
2561            format!(
2562                "Failed to create LSP stderr log file {:?}: {}",
2563                stderr_log_path, e
2564            )
2565        })?;
2566
2567        let mut cmd = Command::new(command);
2568        cmd.args(args)
2569            .envs(env)
2570            .stdin(std::process::Stdio::piped())
2571            .stdout(std::process::Stdio::piped())
2572            .stderr(std::process::Stdio::from(stderr_file))
2573            .kill_on_drop(true);
2574
2575        // Apply resource limits to the process.  The returned `PostSpawnAction`
2576        // must be applied by us after `cmd.spawn()` returns — cgroup
2577        // assignment happens from the parent using the child's PID, because
2578        // doing it in a `pre_exec` closure is not fork-safe (allocation
2579        // inside the forked child can deadlock on a malloc lock held by
2580        // another thread of the parent at fork time).
2581        let post_spawn = process_limits
2582            .apply_to_command(&mut cmd)
2583            .map_err(|e| format!("Failed to apply process limits: {}", e))?;
2584
2585        let mut process = cmd.spawn().map_err(|e| {
2586            format!(
2587                "Failed to spawn LSP server '{}': {}",
2588                command,
2589                match e.kind() {
2590                    std::io::ErrorKind::NotFound => "executable not found in PATH".to_string(),
2591                    std::io::ErrorKind::PermissionDenied =>
2592                        "permission denied (check file permissions)".to_string(),
2593                    _ => e.to_string(),
2594                }
2595            )
2596        })?;
2597
2598        // Move the newly-spawned child into its cgroup (if configured).
2599        // Best-effort: on failure the child keeps running under the parent's
2600        // cgroup, which is fine for correctness even if it means the resource
2601        // limit isn't enforced.
2602        if let Some(child_pid) = process.id() {
2603            post_spawn.apply_to_child(child_pid);
2604        }
2605
2606        let stdin = process
2607            .stdin
2608            .take()
2609            .ok_or_else(|| "Failed to get stdin".to_string())?;
2610
2611        let stdout = BufReader::new(
2612            process
2613                .stdout
2614                .take()
2615                .ok_or_else(|| "Failed to get stdout".to_string())?,
2616        );
2617
2618        Ok(Self {
2619            _process: process,
2620            stdin,
2621            stdout,
2622            next_id: 0,
2623            pending: HashMap::new(),
2624            capabilities: None,
2625            document_versions,
2626            pending_opens: HashMap::new(),
2627            initialized: false,
2628            async_tx,
2629            language,
2630            server_name,
2631            server_command: command.to_string(),
2632            stderr_log_path,
2633            language_id_overrides,
2634        })
2635    }
2636
2637    /// Check if a command exists in PATH or as an absolute path
2638    fn command_exists(command: &str) -> bool {
2639        use std::path::Path;
2640
2641        // If it's an absolute path, check if the file exists and is executable
2642        if command.contains('/') || command.contains('\\') {
2643            let path = Path::new(command);
2644            return path.exists() && path.is_file();
2645        }
2646
2647        // Otherwise, search in PATH
2648        if let Ok(path_var) = std::env::var("PATH") {
2649            #[cfg(unix)]
2650            let separator = ':';
2651            #[cfg(windows)]
2652            let separator = ';';
2653
2654            for dir in path_var.split(separator) {
2655                let full_path = Path::new(dir).join(command);
2656                if full_path.exists() && full_path.is_file() {
2657                    return true;
2658                }
2659                // On Windows, also check with .exe extension
2660                #[cfg(windows)]
2661                {
2662                    let with_exe = Path::new(dir).join(format!("{}.exe", command));
2663                    if with_exe.exists() && with_exe.is_file() {
2664                        return true;
2665                    }
2666                }
2667            }
2668        }
2669
2670        false
2671    }
2672
2673    /// Spawn the stdout reader task that continuously reads and dispatches LSP messages
2674    #[allow(clippy::type_complexity)]
2675    #[allow(clippy::too_many_arguments)]
2676    #[allow(clippy::let_underscore_must_use)] // async_tx.send() is best-effort; receiver drop means editor shutdown
2677    fn spawn_stdout_reader(
2678        mut stdout: BufReader<ChildStdout>,
2679        pending: Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
2680        async_tx: std_mpsc::Sender<AsyncMessage>,
2681        language: String,
2682        server_name: String,
2683        server_command: String,
2684        stdin_writer: Arc<tokio::sync::Mutex<ChildStdin>>,
2685        stderr_log_path: std::path::PathBuf,
2686        shutting_down: Arc<AtomicBool>,
2687        document_versions: Arc<std::sync::Mutex<HashMap<PathBuf, i64>>>,
2688    ) {
2689        tokio::spawn(async move {
2690            tracing::info!("LSP stdout reader task started for {}", language);
2691            loop {
2692                match read_message_from_stdout(&mut stdout).await {
2693                    Ok(message) => {
2694                        tracing::trace!("Read message from LSP server: {:?}", message);
2695                        if let Err(e) = handle_message_dispatch(
2696                            message,
2697                            &pending,
2698                            &async_tx,
2699                            &language,
2700                            &server_name,
2701                            &server_command,
2702                            &stdin_writer,
2703                            &document_versions,
2704                        )
2705                        .await
2706                        {
2707                            tracing::error!("Error handling LSP message: {}", e);
2708                        }
2709                    }
2710                    Err(e) => {
2711                        // Only report error if this wasn't an intentional shutdown
2712                        if shutting_down.load(Ordering::SeqCst) {
2713                            tracing::info!(
2714                                "LSP stdout reader exiting due to graceful shutdown for {}",
2715                                language
2716                            );
2717                        } else {
2718                            tracing::error!("Error reading from LSP server: {}", e);
2719                            let _ = async_tx.send(AsyncMessage::LspStatusUpdate {
2720                                language: language.clone(),
2721                                server_name: server_name.clone(),
2722                                status: LspServerStatus::Error,
2723                                message: None,
2724                            });
2725                            let _ = async_tx.send(AsyncMessage::LspError {
2726                                language: language.clone(),
2727                                error: format!("Read error: {}", e),
2728                                stderr_log_path: Some(stderr_log_path.clone()),
2729                            });
2730                        }
2731                        break;
2732                    }
2733                }
2734            }
2735            // Drain all pending requests so the command loop doesn't block
2736            // forever waiting for responses that will never arrive.
2737            {
2738                let mut pending_guard = pending.lock().unwrap();
2739                let count = pending_guard.len();
2740                if count > 0 {
2741                    tracing::info!(
2742                        "LSP stdout reader: draining {} pending requests for {}",
2743                        count,
2744                        language
2745                    );
2746                    for (id, tx) in pending_guard.drain() {
2747                        tracing::debug!(
2748                            "LSP stdout reader: failing pending request id={} for {}",
2749                            id,
2750                            language
2751                        );
2752                        let _ = tx.send(Err(
2753                            "LSP server connection closed while awaiting response".to_string(),
2754                        ));
2755                    }
2756                }
2757            }
2758
2759            tracing::info!("LSP stdout reader task exiting for {}", language);
2760        });
2761    }
2762
2763    /// Run the task (processes commands and reads from stdout)
2764    // Channel sends and handler results are best-effort: errors are already logged
2765    // within handler methods, and channel send failures mean the editor is shutting down.
2766    #[allow(clippy::let_underscore_must_use)]
2767    async fn run(self, mut command_rx: mpsc::Receiver<LspCommand>) {
2768        tracing::info!("LspTask::run() started for language: {}", self.language);
2769
2770        // Create shared stdin writer so both command processing and stdout reader can write
2771        let stdin_writer = Arc::new(tokio::sync::Mutex::new(self.stdin));
2772
2773        // Create state struct for command processing
2774        let mut state = LspState {
2775            stdin: stdin_writer.clone(),
2776            next_id: self.next_id,
2777            capabilities: self.capabilities,
2778            document_versions: self.document_versions.clone(),
2779            pending_opens: self.pending_opens,
2780            initialized: self.initialized,
2781            async_tx: self.async_tx.clone(),
2782            language: self.language.clone(),
2783            server_name: self.server_name.clone(),
2784            active_requests: HashMap::new(),
2785            language_id_overrides: self.language_id_overrides.clone(),
2786        };
2787
2788        let pending = Arc::new(Mutex::new(self.pending));
2789        let async_tx = state.async_tx.clone();
2790        let language_clone = state.language.clone();
2791        let server_name = state.server_name.clone();
2792
2793        // Flag to indicate intentional shutdown (prevents spurious error messages)
2794        let shutting_down = Arc::new(AtomicBool::new(false));
2795
2796        // Spawn stdout reader task (shares stdin_writer for responding to server requests)
2797        Self::spawn_stdout_reader(
2798            self.stdout,
2799            pending.clone(),
2800            async_tx.clone(),
2801            language_clone.clone(),
2802            self.server_name.clone(),
2803            self.server_command.clone(),
2804            stdin_writer.clone(),
2805            self.stderr_log_path,
2806            shutting_down.clone(),
2807            self.document_versions.clone(),
2808        );
2809
2810        // Sequential command processing loop
2811        //
2812        // Design: while a handler is blocked waiting for a server response
2813        // (via send_request_sequential), incoming commands are drained from
2814        // command_rx into `draining_buffer` so they don't pile up.  Buffered
2815        // commands are processed first in the next loop iteration, before
2816        // polling command_rx again.  This prevents head-of-line blocking:
2817        // notifications like DidClose are handled as soon as the current
2818        // in-flight response resolves, rather than waiting behind it in the
2819        // channel.
2820        //
2821        // Note: Server responses (workspace/configuration, etc.) are now written directly
2822        // by the stdout reader task using the shared stdin_writer, avoiding deadlocks
2823        // when the main loop is blocked waiting for an LSP response.
2824
2825        /// Await a handler future while draining any commands that arrive on
2826        /// `command_rx` into `buf`.  The commands are NOT processed here
2827        /// (because `state` is borrowed by `fut`); they are replayed from
2828        /// `buf` in subsequent iterations of the main loop.
2829        macro_rules! await_draining {
2830            ($fut:expr, $command_rx:expr, $buf:expr) => {{
2831                let fut = $fut;
2832                tokio::pin!(fut);
2833                loop {
2834                    tokio::select! {
2835                        biased;  // prefer completing the handler
2836                        result = &mut fut => break result,
2837                        Some(cmd) = $command_rx.recv() => {
2838                            $buf.push_back(cmd);
2839                        }
2840                    }
2841                }
2842            }};
2843        }
2844
2845        let mut pending_commands = Vec::new();
2846        let mut draining_buffer: std::collections::VecDeque<LspCommand> =
2847            std::collections::VecDeque::new();
2848        loop {
2849            // Drain buffered commands (from a previous handler's await)
2850            // before polling the channel for new ones.
2851            let cmd = if let Some(cmd) = draining_buffer.pop_front() {
2852                cmd
2853            } else {
2854                match command_rx.recv().await {
2855                    Some(cmd) => cmd,
2856                    None => {
2857                        tracing::info!("Command channel closed");
2858                        break;
2859                    }
2860                }
2861            };
2862
2863            tracing::trace!("LspTask received command: {:?}", cmd);
2864            match cmd {
2865                LspCommand::Initialize {
2866                    root_uri,
2867                    initialization_options,
2868                    response,
2869                } => {
2870                    // Send initializing status
2871                    let _ = async_tx.send(AsyncMessage::LspStatusUpdate {
2872                        language: language_clone.clone(),
2873                        server_name: server_name.clone(),
2874                        status: LspServerStatus::Initializing,
2875                        message: None,
2876                    });
2877                    tracing::info!("Processing Initialize command");
2878                    let result = await_draining!(
2879                        state.handle_initialize_sequential(
2880                            root_uri,
2881                            initialization_options,
2882                            &pending
2883                        ),
2884                        command_rx,
2885                        draining_buffer
2886                    );
2887                    let success = result.is_ok();
2888                    let _ = response.send(result);
2889
2890                    // After successful initialization, replay pending commands
2891                    if success {
2892                        let queued = std::mem::take(&mut pending_commands);
2893                        await_draining!(
2894                            state.replay_pending_commands(queued, &pending),
2895                            command_rx,
2896                            draining_buffer
2897                        );
2898                    }
2899                }
2900                LspCommand::DidOpen {
2901                    uri,
2902                    text,
2903                    language_id,
2904                } => {
2905                    if state.initialized {
2906                        tracing::info!("Processing DidOpen for {}", uri.as_str());
2907                        let _ = state
2908                            .handle_did_open_sequential(uri, text, language_id, &pending)
2909                            .await;
2910                    } else {
2911                        tracing::trace!(
2912                            "Queueing DidOpen for {} until initialization completes",
2913                            uri.as_str()
2914                        );
2915                        pending_commands.push(LspCommand::DidOpen {
2916                            uri,
2917                            text,
2918                            language_id,
2919                        });
2920                    }
2921                }
2922                LspCommand::DidChange {
2923                    uri,
2924                    content_changes,
2925                } => {
2926                    if state.initialized {
2927                        tracing::trace!("Processing DidChange for {}", uri.as_str());
2928                        let _ = state
2929                            .handle_did_change_sequential(uri, content_changes, &pending)
2930                            .await;
2931                    } else {
2932                        tracing::trace!(
2933                            "Queueing DidChange for {} until initialization completes",
2934                            uri.as_str()
2935                        );
2936                        pending_commands.push(LspCommand::DidChange {
2937                            uri,
2938                            content_changes,
2939                        });
2940                    }
2941                }
2942                LspCommand::DidClose { uri } => {
2943                    if state.initialized {
2944                        tracing::info!("Processing DidClose for {}", uri.as_str());
2945                        let _ = state.handle_did_close(uri).await;
2946                    } else {
2947                        tracing::trace!(
2948                            "Queueing DidClose for {} until initialization completes",
2949                            uri.as_str()
2950                        );
2951                        pending_commands.push(LspCommand::DidClose { uri });
2952                    }
2953                }
2954                LspCommand::DidSave { uri, text } => {
2955                    if state.initialized {
2956                        tracing::info!("Processing DidSave for {}", uri.as_str());
2957                        let _ = state.handle_did_save(uri, text).await;
2958                    } else {
2959                        tracing::trace!(
2960                            "Queueing DidSave for {} until initialization completes",
2961                            uri.as_str()
2962                        );
2963                        pending_commands.push(LspCommand::DidSave { uri, text });
2964                    }
2965                }
2966                LspCommand::DidChangeWorkspaceFolders { added, removed } => {
2967                    if state.initialized {
2968                        tracing::info!(
2969                            "Processing DidChangeWorkspaceFolders: +{} -{}",
2970                            added.len(),
2971                            removed.len()
2972                        );
2973                        let _ = state
2974                                    .send_notification::<lsp_types::notification::DidChangeWorkspaceFolders>(
2975                                        lsp_types::DidChangeWorkspaceFoldersParams {
2976                                            event: lsp_types::WorkspaceFoldersChangeEvent {
2977                                                added,
2978                                                removed,
2979                                            },
2980                                        },
2981                                    )
2982                                    .await;
2983                    } else {
2984                        tracing::trace!(
2985                            "Queueing DidChangeWorkspaceFolders until initialization completes"
2986                        );
2987                        pending_commands
2988                            .push(LspCommand::DidChangeWorkspaceFolders { added, removed });
2989                    }
2990                }
2991                LspCommand::Completion {
2992                    request_id,
2993                    uri,
2994                    line,
2995                    character,
2996                } => {
2997                    if state.initialized {
2998                        tracing::info!("Processing Completion request for {}", uri.as_str());
2999                        let _ = await_draining!(
3000                            state.handle_completion(request_id, uri, line, character, &pending),
3001                            command_rx,
3002                            draining_buffer
3003                        );
3004                    } else {
3005                        tracing::trace!("LSP not initialized, sending empty completion");
3006                        let _ = state.async_tx.send(AsyncMessage::LspCompletion {
3007                            request_id,
3008                            items: vec![],
3009                        });
3010                    }
3011                }
3012                LspCommand::GotoDefinition {
3013                    request_id,
3014                    uri,
3015                    line,
3016                    character,
3017                } => {
3018                    if state.initialized {
3019                        tracing::info!("Processing GotoDefinition request for {}", uri.as_str());
3020                        let _ = await_draining!(
3021                                    state.handle_goto_definition(
3022                                        request_id, uri, line, character, &pending,
3023                                    ),
3024                                    command_rx,
3025                                    draining_buffer
3026                                );
3027                    } else {
3028                        tracing::trace!("LSP not initialized, sending empty locations");
3029                        let _ = state.async_tx.send(AsyncMessage::LspGotoDefinition {
3030                            request_id,
3031                            locations: vec![],
3032                        });
3033                    }
3034                }
3035                LspCommand::Rename {
3036                    request_id,
3037                    uri,
3038                    line,
3039                    character,
3040                    new_name,
3041                } => {
3042                    if state.initialized {
3043                        tracing::info!("Processing Rename request for {}", uri.as_str());
3044                        let _ = await_draining!(
3045                            state.handle_rename(
3046                                request_id, uri, line, character, new_name, &pending,
3047                            ),
3048                            command_rx,
3049                            draining_buffer
3050                        );
3051                    } else {
3052                        tracing::trace!("LSP not initialized, cannot rename");
3053                        let _ = state.async_tx.send(AsyncMessage::LspRename {
3054                            request_id,
3055                            result: Err("LSP not initialized".to_string()),
3056                        });
3057                    }
3058                }
3059                LspCommand::Hover {
3060                    request_id,
3061                    uri,
3062                    line,
3063                    character,
3064                } => {
3065                    if state.initialized {
3066                        tracing::info!("Processing Hover request for {}", uri.as_str());
3067                        let _ = await_draining!(
3068                            state.handle_hover(request_id, uri, line, character, &pending),
3069                            command_rx,
3070                            draining_buffer
3071                        );
3072                    } else {
3073                        tracing::trace!("LSP not initialized, cannot get hover");
3074                        let _ = state.async_tx.send(AsyncMessage::LspHover {
3075                            request_id,
3076                            contents: String::new(),
3077                            is_markdown: false,
3078                            range: None,
3079                        });
3080                    }
3081                }
3082                LspCommand::References {
3083                    request_id,
3084                    uri,
3085                    line,
3086                    character,
3087                } => {
3088                    if state.initialized {
3089                        tracing::info!("Processing References request for {}", uri.as_str());
3090                        let _ = await_draining!(
3091                            state.handle_references(request_id, uri, line, character, &pending),
3092                            command_rx,
3093                            draining_buffer
3094                        );
3095                    } else {
3096                        tracing::trace!("LSP not initialized, cannot get references");
3097                        let _ = state.async_tx.send(AsyncMessage::LspReferences {
3098                            request_id,
3099                            locations: Vec::new(),
3100                        });
3101                    }
3102                }
3103                LspCommand::SignatureHelp {
3104                    request_id,
3105                    uri,
3106                    line,
3107                    character,
3108                } => {
3109                    if state.initialized {
3110                        tracing::info!("Processing SignatureHelp request for {}", uri.as_str());
3111                        let _ = await_draining!(
3112                            state.handle_signature_help(request_id, uri, line, character, &pending),
3113                            command_rx,
3114                            draining_buffer
3115                        );
3116                    } else {
3117                        tracing::trace!("LSP not initialized, cannot get signature help");
3118                        let _ = state.async_tx.send(AsyncMessage::LspSignatureHelp {
3119                            request_id,
3120                            signature_help: None,
3121                        });
3122                    }
3123                }
3124                LspCommand::CodeActions {
3125                    request_id,
3126                    uri,
3127                    start_line,
3128                    start_char,
3129                    end_line,
3130                    end_char,
3131                    diagnostics,
3132                } => {
3133                    if state.initialized {
3134                        tracing::info!("Processing CodeActions request for {}", uri.as_str());
3135                        let _ = await_draining!(
3136                            state.handle_code_actions(
3137                                request_id,
3138                                uri,
3139                                start_line,
3140                                start_char,
3141                                end_line,
3142                                end_char,
3143                                diagnostics,
3144                                &pending,
3145                            ),
3146                            command_rx,
3147                            draining_buffer
3148                        );
3149                    } else {
3150                        tracing::trace!("LSP not initialized, cannot get code actions");
3151                        let _ = state.async_tx.send(AsyncMessage::LspCodeActions {
3152                            request_id,
3153                            actions: Vec::new(),
3154                        });
3155                    }
3156                }
3157                LspCommand::DocumentDiagnostic {
3158                    request_id,
3159                    uri,
3160                    previous_result_id,
3161                } => {
3162                    if state.initialized {
3163                        tracing::info!(
3164                            "Processing DocumentDiagnostic request for {}",
3165                            uri.as_str()
3166                        );
3167                        let _ = await_draining!(
3168                            state.handle_document_diagnostic(
3169                                request_id,
3170                                uri,
3171                                previous_result_id,
3172                                &pending,
3173                            ),
3174                            command_rx,
3175                            draining_buffer
3176                        );
3177                    } else {
3178                        tracing::trace!("LSP not initialized, cannot get document diagnostics");
3179                        let _ = state.async_tx.send(AsyncMessage::LspPulledDiagnostics {
3180                            request_id,
3181                            uri: uri.as_str().to_string(),
3182                            result_id: None,
3183                            diagnostics: Vec::new(),
3184                            unchanged: false,
3185                        });
3186                    }
3187                }
3188                LspCommand::InlayHints {
3189                    request_id,
3190                    uri,
3191                    start_line,
3192                    start_char,
3193                    end_line,
3194                    end_char,
3195                } => {
3196                    if state.initialized {
3197                        tracing::info!("Processing InlayHints request for {}", uri.as_str());
3198                        let _ = await_draining!(
3199                            state.handle_inlay_hints(
3200                                request_id, uri, start_line, start_char, end_line, end_char,
3201                                &pending,
3202                            ),
3203                            command_rx,
3204                            draining_buffer
3205                        );
3206                    } else {
3207                        tracing::trace!("LSP not initialized, cannot get inlay hints");
3208                        let _ = state.async_tx.send(AsyncMessage::LspInlayHints {
3209                            request_id,
3210                            uri: uri.as_str().to_string(),
3211                            hints: Vec::new(),
3212                        });
3213                    }
3214                }
3215                LspCommand::FoldingRange { request_id, uri } => {
3216                    if state.initialized {
3217                        tracing::info!("Processing FoldingRange request for {}", uri.as_str());
3218                        let _ = await_draining!(
3219                            state.handle_folding_ranges(request_id, uri, &pending),
3220                            command_rx,
3221                            draining_buffer
3222                        );
3223                    } else {
3224                        tracing::trace!("LSP not initialized, cannot get folding ranges");
3225                        let _ = state.async_tx.send(AsyncMessage::LspFoldingRanges {
3226                            request_id,
3227                            uri: uri.as_str().to_string(),
3228                            ranges: Vec::new(),
3229                        });
3230                    }
3231                }
3232                LspCommand::SemanticTokensFull { request_id, uri } => {
3233                    if state.initialized {
3234                        tracing::info!("Processing SemanticTokens request for {}", uri.as_str());
3235                        let _ = await_draining!(
3236                            state.handle_semantic_tokens_full(request_id, uri, &pending),
3237                            command_rx,
3238                            draining_buffer
3239                        );
3240                    } else {
3241                        tracing::trace!("LSP not initialized, cannot get semantic tokens");
3242                        let _ = state.async_tx.send(AsyncMessage::LspSemanticTokens {
3243                            request_id,
3244                            uri: uri.as_str().to_string(),
3245                            response: LspSemanticTokensResponse::Full(Err(
3246                                "LSP not initialized".to_string()
3247                            )),
3248                        });
3249                    }
3250                }
3251                LspCommand::SemanticTokensFullDelta {
3252                    request_id,
3253                    uri,
3254                    previous_result_id,
3255                } => {
3256                    if state.initialized {
3257                        tracing::info!(
3258                            "Processing SemanticTokens delta request for {}",
3259                            uri.as_str()
3260                        );
3261                        let _ = await_draining!(
3262                            state.handle_semantic_tokens_full_delta(
3263                                request_id,
3264                                uri,
3265                                previous_result_id,
3266                                &pending,
3267                            ),
3268                            command_rx,
3269                            draining_buffer
3270                        );
3271                    } else {
3272                        tracing::trace!("LSP not initialized, cannot get semantic tokens");
3273                        let _ = state.async_tx.send(AsyncMessage::LspSemanticTokens {
3274                            request_id,
3275                            uri: uri.as_str().to_string(),
3276                            response: LspSemanticTokensResponse::FullDelta(Err(
3277                                "LSP not initialized".to_string(),
3278                            )),
3279                        });
3280                    }
3281                }
3282                LspCommand::SemanticTokensRange {
3283                    request_id,
3284                    uri,
3285                    range,
3286                } => {
3287                    if state.initialized {
3288                        tracing::info!(
3289                            "Processing SemanticTokens range request for {}",
3290                            uri.as_str()
3291                        );
3292                        let _ = await_draining!(
3293                            state.handle_semantic_tokens_range(request_id, uri, range, &pending),
3294                            command_rx,
3295                            draining_buffer
3296                        );
3297                    } else {
3298                        tracing::trace!("LSP not initialized, cannot get semantic tokens");
3299                        let _ = state.async_tx.send(AsyncMessage::LspSemanticTokens {
3300                            request_id,
3301                            uri: uri.as_str().to_string(),
3302                            response: LspSemanticTokensResponse::Range(Err(
3303                                "LSP not initialized".to_string()
3304                            )),
3305                        });
3306                    }
3307                }
3308                LspCommand::ExecuteCommand { command, arguments } => {
3309                    if state.initialized {
3310                        tracing::info!("Processing ExecuteCommand: {}", command);
3311                        let _ = await_draining!(
3312                            state.handle_execute_command(command, arguments, &pending),
3313                            command_rx,
3314                            draining_buffer
3315                        );
3316                    } else {
3317                        tracing::trace!("LSP not initialized, cannot execute command");
3318                    }
3319                }
3320                LspCommand::CodeActionResolve { request_id, action } => {
3321                    if state.initialized {
3322                        tracing::info!("Processing CodeActionResolve (request_id={})", request_id);
3323                        let _ = await_draining!(
3324                            state.handle_code_action_resolve(request_id, *action, &pending),
3325                            command_rx,
3326                            draining_buffer
3327                        );
3328                    } else {
3329                        tracing::trace!("LSP not initialized, cannot resolve code action");
3330                        let _ = state.async_tx.send(AsyncMessage::LspCodeActionResolved {
3331                            request_id,
3332                            action: Err("LSP not initialized".to_string()),
3333                        });
3334                    }
3335                }
3336                LspCommand::CompletionResolve { request_id, item } => {
3337                    if state.initialized {
3338                        let _ = await_draining!(
3339                            state.handle_completion_resolve(request_id, *item, &pending),
3340                            command_rx,
3341                            draining_buffer
3342                        );
3343                    }
3344                }
3345                LspCommand::DocumentFormatting {
3346                    request_id,
3347                    uri,
3348                    tab_size,
3349                    insert_spaces,
3350                } => {
3351                    if state.initialized {
3352                        tracing::info!("Processing DocumentFormatting for {}", uri.as_str());
3353                        let _ = await_draining!(
3354                            state.handle_document_formatting(
3355                                request_id,
3356                                uri,
3357                                tab_size,
3358                                insert_spaces,
3359                                &pending,
3360                            ),
3361                            command_rx,
3362                            draining_buffer
3363                        );
3364                    }
3365                }
3366                LspCommand::DocumentRangeFormatting {
3367                    request_id,
3368                    uri,
3369                    start_line,
3370                    start_char,
3371                    end_line,
3372                    end_char,
3373                    tab_size,
3374                    insert_spaces,
3375                } => {
3376                    if state.initialized {
3377                        let _ = await_draining!(
3378                            state.handle_document_range_formatting(
3379                                request_id,
3380                                uri,
3381                                start_line,
3382                                start_char,
3383                                end_line,
3384                                end_char,
3385                                tab_size,
3386                                insert_spaces,
3387                                &pending,
3388                            ),
3389                            command_rx,
3390                            draining_buffer
3391                        );
3392                    }
3393                }
3394                LspCommand::PrepareRename {
3395                    request_id,
3396                    uri,
3397                    line,
3398                    character,
3399                } => {
3400                    if state.initialized {
3401                        let _ = await_draining!(
3402                            state
3403                                .handle_prepare_rename(request_id, uri, line, character, &pending,),
3404                            command_rx,
3405                            draining_buffer
3406                        );
3407                    }
3408                }
3409                LspCommand::CancelRequest { request_id } => {
3410                    tracing::info!("Processing CancelRequest for editor_id={}", request_id);
3411                    let _ = state.handle_cancel_request(request_id).await;
3412                }
3413                LspCommand::PluginRequest {
3414                    request_id,
3415                    method,
3416                    params,
3417                } => {
3418                    if state.initialized {
3419                        tracing::trace!("Processing plugin request {} ({})", request_id, method);
3420                        await_draining!(
3421                            state.handle_plugin_request(request_id, method, params, &pending,),
3422                            command_rx,
3423                            draining_buffer
3424                        );
3425                    } else {
3426                        tracing::trace!(
3427                            "Plugin LSP request {} received before initialization",
3428                            request_id
3429                        );
3430                        let _ = state.async_tx.send(AsyncMessage::PluginLspResponse {
3431                            language: language_clone.clone(),
3432                            request_id,
3433                            result: Err("LSP not initialized".to_string()),
3434                        });
3435                    }
3436                }
3437                LspCommand::Shutdown => {
3438                    tracing::info!("Processing Shutdown command");
3439                    // Set flag before shutdown to prevent spurious error messages
3440                    shutting_down.store(true, Ordering::SeqCst);
3441                    let _ = state.handle_shutdown().await;
3442                    break;
3443                }
3444            }
3445        }
3446
3447        tracing::info!("LSP task exiting for language: {}", self.language);
3448    }
3449}
3450
3451/// Standalone function to read a message from stdout (for reader task)
3452async fn read_message_from_stdout(
3453    stdout: &mut BufReader<ChildStdout>,
3454) -> Result<JsonRpcMessage, String> {
3455    // Read headers
3456    let mut content_length: Option<usize> = None;
3457
3458    loop {
3459        let mut line = String::new();
3460        let bytes_read = stdout
3461            .read_line(&mut line)
3462            .await
3463            .map_err(|e| format!("Failed to read from stdout: {}", e))?;
3464
3465        // EOF detected - LSP server closed stdout
3466        if bytes_read == 0 {
3467            return Err("LSP server closed stdout (EOF)".to_string());
3468        }
3469
3470        if line == "\r\n" {
3471            break;
3472        }
3473
3474        if let Some(len_str) = line.strip_prefix("Content-Length: ") {
3475            content_length = Some(
3476                len_str
3477                    .trim()
3478                    .parse()
3479                    .map_err(|e| format!("Invalid Content-Length: {}", e))?,
3480            );
3481        }
3482    }
3483
3484    let content_length =
3485        content_length.ok_or_else(|| "Missing Content-Length header".to_string())?;
3486
3487    // Read content
3488    let mut content = vec![0u8; content_length];
3489    stdout
3490        .read_exact(&mut content)
3491        .await
3492        .map_err(|e| format!("Failed to read content: {}", e))?;
3493
3494    let json = String::from_utf8(content).map_err(|e| format!("Invalid UTF-8: {}", e))?;
3495
3496    tracing::trace!("Received LSP message: {}", json);
3497
3498    serde_json::from_str(&json).map_err(|e| format!("Failed to deserialize message: {}", e))
3499}
3500
3501/// Standalone function to handle and dispatch messages (for reader task)
3502#[allow(clippy::too_many_arguments)]
3503#[allow(clippy::type_complexity)]
3504#[allow(clippy::let_underscore_must_use)] // oneshot/mpsc send results are best-effort; receiver drop is not actionable
3505async fn handle_message_dispatch(
3506    message: JsonRpcMessage,
3507    pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
3508    async_tx: &std_mpsc::Sender<AsyncMessage>,
3509    language: &str,
3510    server_name: &str,
3511    server_command: &str,
3512    stdin_writer: &Arc<tokio::sync::Mutex<ChildStdin>>,
3513    document_versions: &Arc<std::sync::Mutex<HashMap<PathBuf, i64>>>,
3514) -> Result<(), String> {
3515    match message {
3516        JsonRpcMessage::Response(response) => {
3517            tracing::trace!("Received LSP response for request id={}", response.id);
3518            if let Some(tx) = pending.lock().unwrap().remove(&response.id) {
3519                let result = if let Some(error) = response.error {
3520                    // Per LSP spec: ContentModified and ServerCancelled are expected
3521                    // during editing. Suppress them like VS Code and Neovim do.
3522                    if error.code == LSP_ERROR_CONTENT_MODIFIED
3523                        || error.code == LSP_ERROR_SERVER_CANCELLED
3524                    {
3525                        tracing::debug!(
3526                            "LSP response: {} (code {}), discarding",
3527                            error.message,
3528                            error.code
3529                        );
3530                    } else {
3531                        tracing::warn!(
3532                            "LSP response error: {} (code {})",
3533                            error.message,
3534                            error.code
3535                        );
3536                    }
3537                    Err(format!(
3538                        "LSP error: {} (code {})",
3539                        error.message, error.code
3540                    ))
3541                } else {
3542                    tracing::trace!("LSP response success for request id={}", response.id);
3543                    // null is a valid result for many LSP methods (e.g., inlay hints with no hints)
3544                    Ok(response.result.unwrap_or(serde_json::Value::Null))
3545                };
3546                let _ = tx.send(result);
3547            } else {
3548                tracing::warn!(
3549                    "Received LSP response for unknown request id={}",
3550                    response.id
3551                );
3552            }
3553        }
3554        JsonRpcMessage::Notification(notification) => {
3555            tracing::trace!("Received LSP notification: {}", notification.method);
3556            handle_notification_dispatch(
3557                notification,
3558                async_tx,
3559                language,
3560                server_name,
3561                document_versions,
3562            )
3563            .await?;
3564        }
3565        JsonRpcMessage::Request(request) => {
3566            // Handle server-to-client requests - MUST respond to avoid timeouts
3567            tracing::trace!("Received request from server: {}", request.method);
3568            let response = match request.method.as_str() {
3569                "window/workDoneProgress/create" => {
3570                    // Server wants to create a progress token - acknowledge it
3571                    tracing::trace!("Acknowledging workDoneProgress/create (id={})", request.id);
3572                    JsonRpcResponse {
3573                        jsonrpc: "2.0".to_string(),
3574                        id: request.id,
3575                        result: Some(Value::Null),
3576                        error: None,
3577                    }
3578                }
3579                "workspace/configuration" => {
3580                    // Return configuration with inlay hints enabled for rust-analyzer
3581                    // The request contains items asking for configuration sections
3582                    // We return an array with one config object per requested item
3583                    tracing::trace!(
3584                        "Responding to workspace/configuration with inlay hints enabled"
3585                    );
3586
3587                    // Parse request params to see how many items are requested
3588                    let num_items = request
3589                        .params
3590                        .as_ref()
3591                        .and_then(|p| p.get("items"))
3592                        .and_then(|items| items.as_array())
3593                        .map(|arr| arr.len())
3594                        .unwrap_or(1);
3595
3596                    // rust-analyzer configuration with inlay hints enabled
3597                    let ra_config = serde_json::json!({
3598                        "inlayHints": {
3599                            "typeHints": {
3600                                "enable": true
3601                            },
3602                            "parameterHints": {
3603                                "enable": true
3604                            },
3605                            "chainingHints": {
3606                                "enable": true
3607                            },
3608                            "closureReturnTypeHints": {
3609                                "enable": "always"
3610                            }
3611                        }
3612                    });
3613
3614                    // Return one config object for each requested item
3615                    let configs: Vec<Value> = (0..num_items).map(|_| ra_config.clone()).collect();
3616
3617                    JsonRpcResponse {
3618                        jsonrpc: "2.0".to_string(),
3619                        id: request.id,
3620                        result: Some(Value::Array(configs)),
3621                        error: None,
3622                    }
3623                }
3624                "client/registerCapability" => {
3625                    // Server wants to register a capability dynamically - acknowledge
3626                    tracing::trace!(
3627                        "Acknowledging client/registerCapability (id={})",
3628                        request.id
3629                    );
3630                    JsonRpcResponse {
3631                        jsonrpc: "2.0".to_string(),
3632                        id: request.id,
3633                        result: Some(Value::Null),
3634                        error: None,
3635                    }
3636                }
3637                "workspace/diagnostic/refresh" => {
3638                    // Server wants us to re-pull diagnostics for all open documents
3639                    // This typically happens after the project finishes loading
3640                    tracing::info!(
3641                        "LSP ({}) requested diagnostic refresh (workspace/diagnostic/refresh)",
3642                        language
3643                    );
3644                    let _ = async_tx.send(AsyncMessage::LspDiagnosticRefresh {
3645                        language: language.to_string(),
3646                    });
3647                    JsonRpcResponse {
3648                        jsonrpc: "2.0".to_string(),
3649                        id: request.id,
3650                        result: Some(Value::Null),
3651                        error: None,
3652                    }
3653                }
3654                "workspace/applyEdit" => {
3655                    // Server asks client to apply a workspace edit (e.g. during executeCommand)
3656                    tracing::info!("LSP ({}) received workspace/applyEdit request", language);
3657                    let applied = if let Some(params) = &request.params {
3658                        match serde_json::from_value::<lsp_types::ApplyWorkspaceEditParams>(
3659                            params.clone(),
3660                        ) {
3661                            Ok(apply_params) => {
3662                                let label = apply_params.label.clone();
3663                                let _ = async_tx.send(AsyncMessage::LspApplyEdit {
3664                                    edit: apply_params.edit,
3665                                    label,
3666                                });
3667                                true
3668                            }
3669                            Err(e) => {
3670                                tracing::error!(
3671                                    "Failed to parse workspace/applyEdit params: {}",
3672                                    e
3673                                );
3674                                false
3675                            }
3676                        }
3677                    } else {
3678                        false
3679                    };
3680                    JsonRpcResponse {
3681                        jsonrpc: "2.0".to_string(),
3682                        id: request.id,
3683                        result: Some(serde_json::json!({ "applied": applied })),
3684                        error: None,
3685                    }
3686                }
3687                _ => {
3688                    // For unknown methods, notify plugins and return null to acknowledge receipt
3689                    tracing::debug!("Server request for plugins: {}", request.method);
3690                    let _ = async_tx.send(AsyncMessage::LspServerRequest {
3691                        language: language.to_string(),
3692                        server_command: server_command.to_string(),
3693                        method: request.method.clone(),
3694                        params: request.params.clone(),
3695                    });
3696                    JsonRpcResponse {
3697                        jsonrpc: "2.0".to_string(),
3698                        id: request.id,
3699                        result: Some(Value::Null),
3700                        error: None,
3701                    }
3702                }
3703            };
3704
3705            // Write response directly to stdin (avoids deadlock when main loop is waiting for LSP response)
3706            let json = serde_json::to_string(&response)
3707                .map_err(|e| format!("Failed to serialize response: {}", e))?;
3708            let message = format!("Content-Length: {}\r\n\r\n{}", json.len(), json);
3709
3710            let mut stdin = stdin_writer.lock().await;
3711            use tokio::io::AsyncWriteExt;
3712            if let Err(e) = stdin.write_all(message.as_bytes()).await {
3713                tracing::error!("Failed to write server response: {}", e);
3714            }
3715            if let Err(e) = stdin.flush().await {
3716                tracing::error!("Failed to flush server response: {}", e);
3717            }
3718            tracing::trace!("Sent response to server request id={}", response.id);
3719        }
3720    }
3721    Ok(())
3722}
3723
3724/// Standalone function to handle notifications (for reader task)
3725#[allow(clippy::let_underscore_must_use)] // async_tx.send() is best-effort; receiver drop means editor shutdown
3726async fn handle_notification_dispatch(
3727    notification: JsonRpcNotification,
3728    async_tx: &std_mpsc::Sender<AsyncMessage>,
3729    language: &str,
3730    server_name: &str,
3731    document_versions: &Arc<std::sync::Mutex<HashMap<PathBuf, i64>>>,
3732) -> Result<(), String> {
3733    match notification.method.as_str() {
3734        PublishDiagnostics::METHOD => {
3735            if let Some(params) = notification.params {
3736                let params: PublishDiagnosticsParams = serde_json::from_value(params)
3737                    .map_err(|e| format!("Failed to deserialize diagnostics: {}", e))?;
3738
3739                // Drop stale diagnostics: if the server reports a version older than
3740                // the document version we last sent via didOpen/didChange, the diagnostics
3741                // are for an outdated snapshot and should be discarded.
3742                if let Some(diag_version) = params.version {
3743                    let path = PathBuf::from(params.uri.path().as_str());
3744                    let current_version = document_versions.lock().unwrap().get(&path).copied();
3745                    if let Some(current) = current_version {
3746                        if (diag_version as i64) < current {
3747                            tracing::debug!(
3748                                "LSP ({}): dropping stale diagnostics for {} (diag version {} < current {})",
3749                                language,
3750                                params.uri.as_str(),
3751                                diag_version,
3752                                current
3753                            );
3754                            return Ok(());
3755                        }
3756                    }
3757                }
3758
3759                tracing::trace!(
3760                    "Received {} diagnostics for {}",
3761                    params.diagnostics.len(),
3762                    params.uri.as_str()
3763                );
3764
3765                // Send to main loop
3766                let _ = async_tx.send(AsyncMessage::LspDiagnostics {
3767                    uri: params.uri.to_string(),
3768                    diagnostics: params.diagnostics,
3769                    server_name: server_name.to_string(),
3770                });
3771            }
3772        }
3773        "window/showMessage" => {
3774            if let Some(params) = notification.params {
3775                if let Ok(msg) = serde_json::from_value::<serde_json::Map<String, Value>>(params) {
3776                    let message_type_num = msg.get("type").and_then(|v| v.as_i64()).unwrap_or(3);
3777                    let message = msg
3778                        .get("message")
3779                        .and_then(|v| v.as_str())
3780                        .unwrap_or("(no message)")
3781                        .to_string();
3782
3783                    let message_type = match message_type_num {
3784                        1 => LspMessageType::Error,
3785                        2 => LspMessageType::Warning,
3786                        3 => LspMessageType::Info,
3787                        _ => LspMessageType::Log,
3788                    };
3789
3790                    // Log it as well
3791                    match message_type {
3792                        LspMessageType::Error => tracing::error!("LSP ({}): {}", language, message),
3793                        LspMessageType::Warning => {
3794                            tracing::warn!("LSP ({}): {}", language, message)
3795                        }
3796                        LspMessageType::Info => tracing::info!("LSP ({}): {}", language, message),
3797                        LspMessageType::Log => tracing::trace!("LSP ({}): {}", language, message),
3798                    }
3799
3800                    // Send to UI
3801                    let _ = async_tx.send(AsyncMessage::LspWindowMessage {
3802                        language: language.to_string(),
3803                        message_type,
3804                        message,
3805                    });
3806                }
3807            }
3808        }
3809        "window/logMessage" => {
3810            if let Some(params) = notification.params {
3811                if let Ok(msg) = serde_json::from_value::<serde_json::Map<String, Value>>(params) {
3812                    let message_type_num = msg.get("type").and_then(|v| v.as_i64()).unwrap_or(4);
3813                    let message = msg
3814                        .get("message")
3815                        .and_then(|v| v.as_str())
3816                        .unwrap_or("(no message)")
3817                        .to_string();
3818
3819                    let message_type = match message_type_num {
3820                        1 => LspMessageType::Error,
3821                        2 => LspMessageType::Warning,
3822                        3 => LspMessageType::Info,
3823                        _ => LspMessageType::Log,
3824                    };
3825
3826                    // Log it as well
3827                    match message_type {
3828                        LspMessageType::Error => tracing::error!("LSP ({}): {}", language, message),
3829                        LspMessageType::Warning => {
3830                            tracing::warn!("LSP ({}): {}", language, message)
3831                        }
3832                        LspMessageType::Info => tracing::info!("LSP ({}): {}", language, message),
3833                        LspMessageType::Log => tracing::trace!("LSP ({}): {}", language, message),
3834                    }
3835
3836                    // Send to UI
3837                    let _ = async_tx.send(AsyncMessage::LspLogMessage {
3838                        language: language.to_string(),
3839                        message_type,
3840                        message,
3841                    });
3842                }
3843            }
3844        }
3845        "$/progress" => {
3846            if let Some(params) = notification.params {
3847                if let Ok(progress) =
3848                    serde_json::from_value::<serde_json::Map<String, Value>>(params)
3849                {
3850                    let token = progress
3851                        .get("token")
3852                        .and_then(|v| {
3853                            v.as_str()
3854                                .map(|s| s.to_string())
3855                                .or_else(|| v.as_i64().map(|n| n.to_string()))
3856                        })
3857                        .unwrap_or_else(|| "unknown".to_string());
3858
3859                    if let Some(value_obj) = progress.get("value").and_then(|v| v.as_object()) {
3860                        let kind = value_obj.get("kind").and_then(|v| v.as_str());
3861
3862                        let value = match kind {
3863                            Some("begin") => {
3864                                let title = value_obj
3865                                    .get("title")
3866                                    .and_then(|v| v.as_str())
3867                                    .unwrap_or("Working...")
3868                                    .to_string();
3869                                let message = value_obj
3870                                    .get("message")
3871                                    .and_then(|v| v.as_str())
3872                                    .map(|s| s.to_string());
3873                                let percentage = value_obj
3874                                    .get("percentage")
3875                                    .and_then(|v| v.as_u64())
3876                                    .map(|p| p as u32);
3877
3878                                tracing::info!(
3879                                    "LSP ({}) progress begin: {} {:?} {:?}",
3880                                    language,
3881                                    title,
3882                                    message,
3883                                    percentage
3884                                );
3885
3886                                Some(LspProgressValue::Begin {
3887                                    title,
3888                                    message,
3889                                    percentage,
3890                                })
3891                            }
3892                            Some("report") => {
3893                                let message = value_obj
3894                                    .get("message")
3895                                    .and_then(|v| v.as_str())
3896                                    .map(|s| s.to_string());
3897                                let percentage = value_obj
3898                                    .get("percentage")
3899                                    .and_then(|v| v.as_u64())
3900                                    .map(|p| p as u32);
3901
3902                                tracing::trace!(
3903                                    "LSP ({}) progress report: {:?} {:?}",
3904                                    language,
3905                                    message,
3906                                    percentage
3907                                );
3908
3909                                Some(LspProgressValue::Report {
3910                                    message,
3911                                    percentage,
3912                                })
3913                            }
3914                            Some("end") => {
3915                                let message = value_obj
3916                                    .get("message")
3917                                    .and_then(|v| v.as_str())
3918                                    .map(|s| s.to_string());
3919
3920                                tracing::info!("LSP ({}) progress end: {:?}", language, message);
3921
3922                                Some(LspProgressValue::End { message })
3923                            }
3924                            _ => None,
3925                        };
3926
3927                        if let Some(value) = value {
3928                            let _ = async_tx.send(AsyncMessage::LspProgress {
3929                                language: language.to_string(),
3930                                token,
3931                                value,
3932                            });
3933                        }
3934                    }
3935                }
3936            }
3937        }
3938        "experimental/serverStatus" => {
3939            // rust-analyzer specific: server status notification
3940            // When quiescent is true, the project is fully loaded
3941            if let Some(params) = notification.params {
3942                if let Ok(status) = serde_json::from_value::<serde_json::Map<String, Value>>(params)
3943                {
3944                    let quiescent = status
3945                        .get("quiescent")
3946                        .and_then(|v| v.as_bool())
3947                        .unwrap_or(false);
3948
3949                    tracing::info!("LSP ({}) server status: quiescent={}", language, quiescent);
3950
3951                    if quiescent {
3952                        // Project is fully loaded - notify editor to re-request inlay hints
3953                        let _ = async_tx.send(AsyncMessage::LspServerQuiescent {
3954                            language: language.to_string(),
3955                        });
3956                    }
3957                }
3958            }
3959        }
3960        _ => {
3961            tracing::debug!("Unhandled notification: {}", notification.method);
3962        }
3963    }
3964
3965    Ok(())
3966}
3967
3968/// Counter for generating unique LSP handle IDs
3969static NEXT_HANDLE_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
3970
3971/// Synchronous handle to an async LSP task
3972pub struct LspHandle {
3973    /// Unique identifier for this handle instance
3974    id: u64,
3975
3976    /// Which languages this handle serves.
3977    scope: crate::services::lsp::manager::LanguageScope,
3978
3979    /// Channel for sending commands to the task
3980    command_tx: mpsc::Sender<LspCommand>,
3981
3982    /// Client state
3983    state: Arc<Mutex<LspClientState>>,
3984
3985    /// Runtime handle for blocking operations
3986    runtime: tokio::runtime::Handle,
3987
3988    /// Document version tracking (shared with the async LSP task).
3989    /// Used to check document versions in workspace/applyEdit.
3990    document_versions: Arc<std::sync::Mutex<HashMap<PathBuf, i64>>>,
3991}
3992
3993// Channel sends and state transitions in LspHandle are best-effort: async_tx.send()
3994// failures mean the editor is shutting down, state transition errors in error-handling
3995// paths are secondary, and try_send in Drop is inherently best-effort cleanup.
3996#[allow(clippy::let_underscore_must_use)]
3997impl LspHandle {
3998    /// Spawn a new LSP server in an async task
3999    #[allow(clippy::too_many_arguments)]
4000    pub fn spawn(
4001        runtime: &tokio::runtime::Handle,
4002        command: &str,
4003        args: &[String],
4004        env: std::collections::HashMap<String, String>,
4005        scope: crate::services::lsp::manager::LanguageScope,
4006        server_name: String,
4007        async_bridge: &AsyncBridge,
4008        process_limits: ProcessLimits,
4009        language_id_overrides: std::collections::HashMap<String, String>,
4010    ) -> Result<Self, String> {
4011        let (command_tx, command_rx) = mpsc::channel(100); // Buffer up to 100 commands
4012        let async_tx = async_bridge.sender();
4013        let language_label = scope.label().to_string();
4014        let language_clone = language_label.clone();
4015        let server_name_clone = server_name.clone();
4016        let command = command.to_string();
4017        let args = args.to_vec();
4018        let state = Arc::new(Mutex::new(LspClientState::Starting));
4019
4020        // Create stderr log path in XDG state directory
4021        let stderr_log_path = crate::services::log_dirs::lsp_log_path(&language_label);
4022
4023        // Send starting status
4024        let _ = async_tx.send(AsyncMessage::LspStatusUpdate {
4025            language: language_label.clone(),
4026            server_name: server_name_clone.clone(),
4027            status: LspServerStatus::Starting,
4028            message: None,
4029        });
4030
4031        // Create shared document version tracking — shared between
4032        // the async LSP task and the LspHandle so the editor can check
4033        // versions when applying workspace edits from the server.
4034        let document_versions: Arc<std::sync::Mutex<HashMap<PathBuf, i64>>> =
4035            Arc::new(std::sync::Mutex::new(HashMap::new()));
4036        let document_versions_for_task = document_versions.clone();
4037
4038        let state_clone = state.clone();
4039        let stderr_log_path_clone = stderr_log_path.clone();
4040        runtime.spawn(async move {
4041            match LspTask::spawn(
4042                &command,
4043                &args,
4044                &env,
4045                language_clone.clone(),
4046                server_name_clone.clone(),
4047                async_tx.clone(),
4048                &process_limits,
4049                stderr_log_path_clone.clone(),
4050                language_id_overrides,
4051                document_versions_for_task,
4052            )
4053            .await
4054            {
4055                Ok(task) => {
4056                    task.run(command_rx).await;
4057                }
4058                Err(e) => {
4059                    tracing::error!("Failed to spawn LSP task: {}", e);
4060
4061                    // Transition to error state
4062                    if let Ok(mut s) = state_clone.lock() {
4063                        let _ = s.transition_to(LspClientState::Error);
4064                    }
4065
4066                    let _ = async_tx.send(AsyncMessage::LspStatusUpdate {
4067                        language: language_clone.clone(),
4068                        server_name: server_name_clone.clone(),
4069                        status: LspServerStatus::Error,
4070                        message: None,
4071                    });
4072                    let _ = async_tx.send(AsyncMessage::LspError {
4073                        language: language_clone,
4074                        error: e,
4075                        stderr_log_path: Some(stderr_log_path_clone),
4076                    });
4077                }
4078            }
4079        });
4080
4081        let id = NEXT_HANDLE_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
4082
4083        Ok(Self {
4084            id,
4085            scope,
4086            command_tx,
4087            state,
4088            runtime: runtime.clone(),
4089            document_versions,
4090        })
4091    }
4092
4093    /// Get the unique ID for this handle instance
4094    pub fn id(&self) -> u64 {
4095        self.id
4096    }
4097
4098    /// Get the language scope this handle serves.
4099    pub fn scope(&self) -> &crate::services::lsp::manager::LanguageScope {
4100        &self.scope
4101    }
4102
4103    /// Get the document version for a file path, as last sent via didOpen/didChange.
4104    /// Returns None if the document hasn't been opened with this server.
4105    pub fn document_version(&self, path: &std::path::Path) -> Option<i64> {
4106        self.document_versions
4107            .lock()
4108            .ok()
4109            .and_then(|versions| versions.get(path).copied())
4110    }
4111
4112    /// Initialize the server (non-blocking)
4113    ///
4114    /// This sends the initialize request asynchronously. The server will be ready
4115    /// when `is_initialized()` returns true. Other methods that require initialization
4116    /// will fail gracefully until then.
4117    ///
4118    /// The `initialization_options` are passed to the server during initialization.
4119    /// Some servers like Deno require specific options (e.g., `{"enable": true}`).
4120    pub fn initialize(
4121        &self,
4122        root_uri: Option<Uri>,
4123        initialization_options: Option<Value>,
4124    ) -> Result<(), String> {
4125        // Validate state transition
4126        {
4127            let mut state = self.state.lock().unwrap();
4128            if !state.can_initialize() {
4129                return Err(format!(
4130                    "Cannot initialize: client is in state {:?}",
4131                    *state
4132                ));
4133            }
4134            // Transition to Initializing
4135            state.transition_to(LspClientState::Initializing)?;
4136        }
4137
4138        let state = self.state.clone();
4139
4140        // Create a channel for the response, but don't wait for it
4141        let (tx, rx) = oneshot::channel();
4142
4143        self.command_tx
4144            .try_send(LspCommand::Initialize {
4145                root_uri,
4146                initialization_options,
4147                response: tx,
4148            })
4149            .map_err(|_| "Failed to send initialize command".to_string())?;
4150
4151        // Spawn a task to wait for the response and update the state
4152        let runtime = self.runtime.clone();
4153        runtime.spawn(async move {
4154            match tokio::time::timeout(std::time::Duration::from_secs(60), rx).await {
4155                Ok(Ok(Ok(_))) => {
4156                    // Successfully initialized
4157                    if let Ok(mut s) = state.lock() {
4158                        let _ = s.transition_to(LspClientState::Running);
4159                    }
4160                    tracing::info!("LSP initialization completed successfully");
4161                }
4162                Ok(Ok(Err(e))) => {
4163                    tracing::error!("LSP initialization failed: {}", e);
4164                    if let Ok(mut s) = state.lock() {
4165                        let _ = s.transition_to(LspClientState::Error);
4166                    }
4167                }
4168                Ok(Err(_)) => {
4169                    tracing::error!("LSP initialization response channel closed");
4170                    if let Ok(mut s) = state.lock() {
4171                        let _ = s.transition_to(LspClientState::Error);
4172                    }
4173                }
4174                Err(_) => {
4175                    tracing::error!("LSP initialization timed out after 60 seconds");
4176                    if let Ok(mut s) = state.lock() {
4177                        let _ = s.transition_to(LspClientState::Error);
4178                    }
4179                }
4180            }
4181        });
4182
4183        Ok(())
4184    }
4185
4186    /// Check if the server is initialized
4187    pub fn is_initialized(&self) -> bool {
4188        self.state.lock().unwrap().can_send_requests()
4189    }
4190
4191    /// Get the current client state
4192    pub fn state(&self) -> LspClientState {
4193        *self.state.lock().unwrap()
4194    }
4195
4196    /// Notify document opened
4197    ///
4198    /// The `language_id` should match this handle's language. If it doesn't,
4199    /// a warning is logged but the notification is still sent (the server
4200    /// will receive it with the specified language_id).
4201    pub fn did_open(&self, uri: Uri, text: String, language_id: String) -> Result<(), String> {
4202        // Verify the document language is accepted by this handle.
4203        if !self.scope.accepts(&language_id) {
4204            tracing::warn!(
4205                "did_open: document language '{}' not accepted by LSP handle (serves {:?}) for {}",
4206                language_id,
4207                self.scope,
4208                uri.as_str()
4209            );
4210            return Err(format!(
4211                "Language mismatch: document is '{}' but LSP serves {:?}",
4212                language_id, self.scope
4213            ));
4214        }
4215
4216        // Send command to LspTask which will queue it if not initialized yet
4217        self.command_tx
4218            .try_send(LspCommand::DidOpen {
4219                uri,
4220                text,
4221                language_id,
4222            })
4223            .map_err(|_| "Failed to send did_open command".to_string())
4224    }
4225
4226    /// Notify document changed
4227    pub fn did_change(
4228        &self,
4229        uri: Uri,
4230        content_changes: Vec<TextDocumentContentChangeEvent>,
4231    ) -> Result<(), String> {
4232        // Send command to LspTask which will queue it if not initialized yet
4233        self.command_tx
4234            .try_send(LspCommand::DidChange {
4235                uri,
4236                content_changes,
4237            })
4238            .map_err(|_| "Failed to send did_change command".to_string())
4239    }
4240
4241    /// Send didClose notification
4242    pub fn did_close(&self, uri: Uri) -> Result<(), String> {
4243        self.command_tx
4244            .try_send(LspCommand::DidClose { uri })
4245            .map_err(|_| "Failed to send did_close command".to_string())
4246    }
4247
4248    /// Send didSave notification
4249    pub fn did_save(&self, uri: Uri, text: Option<String>) -> Result<(), String> {
4250        self.command_tx
4251            .try_send(LspCommand::DidSave { uri, text })
4252            .map_err(|_| "Failed to send did_save command".to_string())
4253    }
4254
4255    /// Add a workspace folder to the running LSP server
4256    pub fn add_workspace_folder(&self, uri: lsp_types::Uri, name: String) -> Result<(), String> {
4257        self.command_tx
4258            .try_send(LspCommand::DidChangeWorkspaceFolders {
4259                added: vec![lsp_types::WorkspaceFolder { uri, name }],
4260                removed: vec![],
4261            })
4262            .map_err(|_| "Failed to send workspace folder change".to_string())
4263    }
4264
4265    /// Request completion at position
4266    pub fn completion(
4267        &self,
4268        request_id: u64,
4269        uri: Uri,
4270        line: u32,
4271        character: u32,
4272    ) -> Result<(), String> {
4273        self.command_tx
4274            .try_send(LspCommand::Completion {
4275                request_id,
4276                uri,
4277                line,
4278                character,
4279            })
4280            .map_err(|_| "Failed to send completion command".to_string())
4281    }
4282
4283    /// Request go-to-definition
4284    pub fn goto_definition(
4285        &self,
4286        request_id: u64,
4287        uri: Uri,
4288        line: u32,
4289        character: u32,
4290    ) -> Result<(), String> {
4291        self.command_tx
4292            .try_send(LspCommand::GotoDefinition {
4293                request_id,
4294                uri,
4295                line,
4296                character,
4297            })
4298            .map_err(|_| "Failed to send goto_definition command".to_string())
4299    }
4300
4301    /// Request rename
4302    pub fn rename(
4303        &self,
4304        request_id: u64,
4305        uri: Uri,
4306        line: u32,
4307        character: u32,
4308        new_name: String,
4309    ) -> Result<(), String> {
4310        self.command_tx
4311            .try_send(LspCommand::Rename {
4312                request_id,
4313                uri,
4314                line,
4315                character,
4316                new_name,
4317            })
4318            .map_err(|_| "Failed to send rename command".to_string())
4319    }
4320
4321    /// Request hover documentation
4322    pub fn hover(
4323        &self,
4324        request_id: u64,
4325        uri: Uri,
4326        line: u32,
4327        character: u32,
4328    ) -> Result<(), String> {
4329        self.command_tx
4330            .try_send(LspCommand::Hover {
4331                request_id,
4332                uri,
4333                line,
4334                character,
4335            })
4336            .map_err(|_| "Failed to send hover command".to_string())
4337    }
4338
4339    /// Request find references
4340    pub fn references(
4341        &self,
4342        request_id: u64,
4343        uri: Uri,
4344        line: u32,
4345        character: u32,
4346    ) -> Result<(), String> {
4347        self.command_tx
4348            .try_send(LspCommand::References {
4349                request_id,
4350                uri,
4351                line,
4352                character,
4353            })
4354            .map_err(|_| "Failed to send references command".to_string())
4355    }
4356
4357    /// Request signature help
4358    pub fn signature_help(
4359        &self,
4360        request_id: u64,
4361        uri: Uri,
4362        line: u32,
4363        character: u32,
4364    ) -> Result<(), String> {
4365        self.command_tx
4366            .try_send(LspCommand::SignatureHelp {
4367                request_id,
4368                uri,
4369                line,
4370                character,
4371            })
4372            .map_err(|_| "Failed to send signature_help command".to_string())
4373    }
4374
4375    /// Request code actions
4376    #[allow(clippy::too_many_arguments)]
4377    pub fn code_actions(
4378        &self,
4379        request_id: u64,
4380        uri: Uri,
4381        start_line: u32,
4382        start_char: u32,
4383        end_line: u32,
4384        end_char: u32,
4385        diagnostics: Vec<lsp_types::Diagnostic>,
4386    ) -> Result<(), String> {
4387        self.command_tx
4388            .try_send(LspCommand::CodeActions {
4389                request_id,
4390                uri,
4391                start_line,
4392                start_char,
4393                end_line,
4394                end_char,
4395                diagnostics,
4396            })
4397            .map_err(|_| "Failed to send code_actions command".to_string())
4398    }
4399
4400    /// Execute a command on the server (workspace/executeCommand)
4401    ///
4402    /// The response is usually null — the real effect comes via workspace/applyEdit
4403    /// requests sent by the server during command execution.
4404    pub fn execute_command(
4405        &self,
4406        command: String,
4407        arguments: Option<Vec<Value>>,
4408    ) -> Result<(), String> {
4409        self.command_tx
4410            .try_send(LspCommand::ExecuteCommand { command, arguments })
4411            .map_err(|_| "Failed to send execute_command command".to_string())
4412    }
4413
4414    /// Resolve a code action to get full edit/command details (codeAction/resolve)
4415    ///
4416    /// Only call this when the action has no `edit` and no `command` but has `data`,
4417    /// and the server supports resolveProvider.
4418    pub fn code_action_resolve(
4419        &self,
4420        request_id: u64,
4421        action: lsp_types::CodeAction,
4422    ) -> Result<(), String> {
4423        self.command_tx
4424            .try_send(LspCommand::CodeActionResolve {
4425                request_id,
4426                action: Box::new(action),
4427            })
4428            .map_err(|_| "Failed to send code_action_resolve command".to_string())
4429    }
4430
4431    /// Resolve a completion item to get full details (completionItem/resolve)
4432    pub fn completion_resolve(
4433        &self,
4434        request_id: u64,
4435        item: lsp_types::CompletionItem,
4436    ) -> Result<(), String> {
4437        self.command_tx
4438            .try_send(LspCommand::CompletionResolve {
4439                request_id,
4440                item: Box::new(item),
4441            })
4442            .map_err(|_| "Failed to send completion_resolve command".to_string())
4443    }
4444
4445    /// Format a document (textDocument/formatting)
4446    pub fn document_formatting(
4447        &self,
4448        request_id: u64,
4449        uri: Uri,
4450        tab_size: u32,
4451        insert_spaces: bool,
4452    ) -> Result<(), String> {
4453        self.command_tx
4454            .try_send(LspCommand::DocumentFormatting {
4455                request_id,
4456                uri,
4457                tab_size,
4458                insert_spaces,
4459            })
4460            .map_err(|_| "Failed to send document_formatting command".to_string())
4461    }
4462
4463    /// Format a range in a document (textDocument/rangeFormatting)
4464    #[allow(clippy::too_many_arguments)]
4465    pub fn document_range_formatting(
4466        &self,
4467        request_id: u64,
4468        uri: Uri,
4469        start_line: u32,
4470        start_char: u32,
4471        end_line: u32,
4472        end_char: u32,
4473        tab_size: u32,
4474        insert_spaces: bool,
4475    ) -> Result<(), String> {
4476        self.command_tx
4477            .try_send(LspCommand::DocumentRangeFormatting {
4478                request_id,
4479                uri,
4480                start_line,
4481                start_char,
4482                end_line,
4483                end_char,
4484                tab_size,
4485                insert_spaces,
4486            })
4487            .map_err(|_| "Failed to send document_range_formatting command".to_string())
4488    }
4489
4490    /// Validate rename at position (textDocument/prepareRename)
4491    pub fn prepare_rename(
4492        &self,
4493        request_id: u64,
4494        uri: Uri,
4495        line: u32,
4496        character: u32,
4497    ) -> Result<(), String> {
4498        self.command_tx
4499            .try_send(LspCommand::PrepareRename {
4500                request_id,
4501                uri,
4502                line,
4503                character,
4504            })
4505            .map_err(|_| "Failed to send prepare_rename command".to_string())
4506    }
4507
4508    /// Request document diagnostics (pull model)
4509    ///
4510    /// This sends a textDocument/diagnostic request to fetch diagnostics on demand.
4511    /// Use `previous_result_id` for incremental updates (server may return unchanged).
4512    pub fn document_diagnostic(
4513        &self,
4514        request_id: u64,
4515        uri: Uri,
4516        previous_result_id: Option<String>,
4517    ) -> Result<(), String> {
4518        self.command_tx
4519            .try_send(LspCommand::DocumentDiagnostic {
4520                request_id,
4521                uri,
4522                previous_result_id,
4523            })
4524            .map_err(|_| "Failed to send document_diagnostic command".to_string())
4525    }
4526
4527    /// Request inlay hints for a range (LSP 3.17+)
4528    ///
4529    /// Inlay hints are virtual text annotations displayed inline (e.g., type hints, parameter names).
4530    pub fn inlay_hints(
4531        &self,
4532        request_id: u64,
4533        uri: Uri,
4534        start_line: u32,
4535        start_char: u32,
4536        end_line: u32,
4537        end_char: u32,
4538    ) -> Result<(), String> {
4539        self.command_tx
4540            .try_send(LspCommand::InlayHints {
4541                request_id,
4542                uri,
4543                start_line,
4544                start_char,
4545                end_line,
4546                end_char,
4547            })
4548            .map_err(|_| "Failed to send inlay_hints command".to_string())
4549    }
4550
4551    /// Request folding ranges for a document
4552    pub fn folding_ranges(&self, request_id: u64, uri: Uri) -> Result<(), String> {
4553        self.command_tx
4554            .try_send(LspCommand::FoldingRange { request_id, uri })
4555            .map_err(|_| "Failed to send folding_range command".to_string())
4556    }
4557
4558    /// Request semantic tokens for an entire document
4559    pub fn semantic_tokens_full(&self, request_id: u64, uri: Uri) -> Result<(), String> {
4560        self.command_tx
4561            .try_send(LspCommand::SemanticTokensFull { request_id, uri })
4562            .map_err(|_| "Failed to send semantic_tokens command".to_string())
4563    }
4564
4565    /// Request semantic tokens delta for an entire document
4566    pub fn semantic_tokens_full_delta(
4567        &self,
4568        request_id: u64,
4569        uri: Uri,
4570        previous_result_id: String,
4571    ) -> Result<(), String> {
4572        self.command_tx
4573            .try_send(LspCommand::SemanticTokensFullDelta {
4574                request_id,
4575                uri,
4576                previous_result_id,
4577            })
4578            .map_err(|_| "Failed to send semantic_tokens delta command".to_string())
4579    }
4580
4581    /// Request semantic tokens for a range
4582    pub fn semantic_tokens_range(
4583        &self,
4584        request_id: u64,
4585        uri: Uri,
4586        range: lsp_types::Range,
4587    ) -> Result<(), String> {
4588        self.command_tx
4589            .try_send(LspCommand::SemanticTokensRange {
4590                request_id,
4591                uri,
4592                range,
4593            })
4594            .map_err(|_| "Failed to send semantic_tokens_range command".to_string())
4595    }
4596
4597    /// Cancel a pending request by its editor request_id
4598    ///
4599    /// This sends a $/cancelRequest notification to the LSP server.
4600    /// If the request has already completed or doesn't exist, this is a no-op.
4601    pub fn cancel_request(&self, request_id: u64) -> Result<(), String> {
4602        self.command_tx
4603            .try_send(LspCommand::CancelRequest { request_id })
4604            .map_err(|_| "Failed to send cancel_request command".to_string())
4605    }
4606
4607    /// Send a custom LSP request initiated by a plugin
4608    pub fn send_plugin_request(
4609        &self,
4610        request_id: u64,
4611        method: String,
4612        params: Option<Value>,
4613    ) -> Result<(), String> {
4614        tracing::trace!(
4615            "LspHandle sending plugin request {}: method={}",
4616            request_id,
4617            method
4618        );
4619        match self.command_tx.try_send(LspCommand::PluginRequest {
4620            request_id,
4621            method,
4622            params,
4623        }) {
4624            Ok(()) => {
4625                tracing::trace!(
4626                    "LspHandle enqueued plugin request {} successfully",
4627                    request_id
4628                );
4629                Ok(())
4630            }
4631            Err(e) => {
4632                tracing::error!("Failed to enqueue plugin request {}: {}", request_id, e);
4633                Err("Failed to send plugin LSP request".to_string())
4634            }
4635        }
4636    }
4637
4638    /// Shutdown the server
4639    pub fn shutdown(&self) -> Result<(), String> {
4640        // Transition to Stopping state
4641        {
4642            let mut state = self.state.lock().unwrap();
4643            if let Err(e) = state.transition_to(LspClientState::Stopping) {
4644                tracing::warn!("State transition warning during shutdown: {}", e);
4645                // Don't fail shutdown due to state transition errors
4646            }
4647        }
4648
4649        self.command_tx
4650            .try_send(LspCommand::Shutdown)
4651            .map_err(|_| "Failed to send shutdown command".to_string())?;
4652
4653        // Transition to Stopped state
4654        // Note: This happens optimistically. The actual shutdown might take time.
4655        {
4656            let mut state = self.state.lock().unwrap();
4657            let _ = state.transition_to(LspClientState::Stopped);
4658        }
4659
4660        Ok(())
4661    }
4662}
4663
4664#[allow(clippy::let_underscore_must_use)] // Best-effort cleanup in Drop; failures are not actionable
4665impl Drop for LspHandle {
4666    fn drop(&mut self) {
4667        // Best-effort shutdown on drop
4668        // Use try_send instead of blocking_send to avoid panicking if:
4669        // 1. The tokio runtime is shut down
4670        // 2. The channel is full or closed
4671        // 3. We're dropping during a panic
4672        let _ = self.command_tx.try_send(LspCommand::Shutdown);
4673
4674        // Update state to Stopped
4675        if let Ok(mut state) = self.state.lock() {
4676            let _ = state.transition_to(LspClientState::Stopped);
4677        }
4678    }
4679}
4680
4681#[cfg(test)]
4682mod tests {
4683    use super::*;
4684    use crate::services::lsp::manager::LanguageScope;
4685
4686    #[test]
4687    fn test_json_rpc_request_serialization() {
4688        let request = JsonRpcRequest {
4689            jsonrpc: "2.0".to_string(),
4690            id: 1,
4691            method: "initialize".to_string(),
4692            params: Some(serde_json::json!({"rootUri": "file:///test"})),
4693        };
4694
4695        let json = serde_json::to_string(&request).unwrap();
4696        assert!(json.contains("\"jsonrpc\":\"2.0\""));
4697        assert!(json.contains("\"id\":1"));
4698        assert!(json.contains("\"method\":\"initialize\""));
4699        assert!(json.contains("\"rootUri\":\"file:///test\""));
4700    }
4701
4702    #[test]
4703    fn test_json_rpc_response_serialization() {
4704        let response = JsonRpcResponse {
4705            jsonrpc: "2.0".to_string(),
4706            id: 1,
4707            result: Some(serde_json::json!({"success": true})),
4708            error: None,
4709        };
4710
4711        let json = serde_json::to_string(&response).unwrap();
4712        assert!(json.contains("\"jsonrpc\":\"2.0\""));
4713        assert!(json.contains("\"id\":1"));
4714        assert!(json.contains("\"success\":true"));
4715        assert!(!json.contains("\"error\""));
4716    }
4717
4718    #[test]
4719    fn test_json_rpc_error_response() {
4720        let response = JsonRpcResponse {
4721            jsonrpc: "2.0".to_string(),
4722            id: 1,
4723            result: None,
4724            error: Some(JsonRpcError {
4725                code: -32600,
4726                message: "Invalid request".to_string(),
4727                data: None,
4728            }),
4729        };
4730
4731        let json = serde_json::to_string(&response).unwrap();
4732        assert!(json.contains("\"error\""));
4733        assert!(json.contains("\"code\":-32600"));
4734        assert!(json.contains("\"message\":\"Invalid request\""));
4735    }
4736
4737    #[test]
4738    fn test_json_rpc_notification_serialization() {
4739        let notification = JsonRpcNotification {
4740            jsonrpc: "2.0".to_string(),
4741            method: "textDocument/didOpen".to_string(),
4742            params: Some(serde_json::json!({"uri": "file:///test.rs"})),
4743        };
4744
4745        let json = serde_json::to_string(&notification).unwrap();
4746        assert!(json.contains("\"jsonrpc\":\"2.0\""));
4747        assert!(json.contains("\"method\":\"textDocument/didOpen\""));
4748        assert!(json.contains("\"uri\":\"file:///test.rs\""));
4749        assert!(!json.contains("\"id\"")); // Notifications have no ID
4750    }
4751
4752    #[test]
4753    fn test_json_rpc_message_deserialization_request() {
4754        let json =
4755            r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"rootUri":"file:///test"}}"#;
4756        let message: JsonRpcMessage = serde_json::from_str(json).unwrap();
4757
4758        match message {
4759            JsonRpcMessage::Request(request) => {
4760                assert_eq!(request.jsonrpc, "2.0");
4761                assert_eq!(request.id, 1);
4762                assert_eq!(request.method, "initialize");
4763                assert!(request.params.is_some());
4764            }
4765            _ => panic!("Expected Request"),
4766        }
4767    }
4768
4769    #[test]
4770    fn test_json_rpc_message_deserialization_response() {
4771        let json = r#"{"jsonrpc":"2.0","id":1,"result":{"success":true}}"#;
4772        let message: JsonRpcMessage = serde_json::from_str(json).unwrap();
4773
4774        match message {
4775            JsonRpcMessage::Response(response) => {
4776                assert_eq!(response.jsonrpc, "2.0");
4777                assert_eq!(response.id, 1);
4778                assert!(response.result.is_some());
4779                assert!(response.error.is_none());
4780            }
4781            _ => panic!("Expected Response"),
4782        }
4783    }
4784
4785    #[test]
4786    fn test_json_rpc_message_deserialization_notification() {
4787        let json = r#"{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"uri":"file:///test.rs"}}"#;
4788        let message: JsonRpcMessage = serde_json::from_str(json).unwrap();
4789
4790        match message {
4791            JsonRpcMessage::Notification(notification) => {
4792                assert_eq!(notification.jsonrpc, "2.0");
4793                assert_eq!(notification.method, "textDocument/didOpen");
4794                assert!(notification.params.is_some());
4795            }
4796            _ => panic!("Expected Notification"),
4797        }
4798    }
4799
4800    #[test]
4801    fn test_json_rpc_error_deserialization() {
4802        let json =
4803            r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"Invalid request"}}"#;
4804        let message: JsonRpcMessage = serde_json::from_str(json).unwrap();
4805
4806        match message {
4807            JsonRpcMessage::Response(response) => {
4808                assert_eq!(response.jsonrpc, "2.0");
4809                assert_eq!(response.id, 1);
4810                assert!(response.result.is_none());
4811                assert!(response.error.is_some());
4812                let error = response.error.unwrap();
4813                assert_eq!(error.code, -32600);
4814                assert_eq!(error.message, "Invalid request");
4815            }
4816            _ => panic!("Expected Response with error"),
4817        }
4818    }
4819
4820    #[tokio::test]
4821    async fn test_lsp_handle_spawn_and_drop() {
4822        // This test spawns a mock LSP server (cat command that echoes input)
4823        // and tests the spawn/drop lifecycle
4824        let runtime = tokio::runtime::Handle::current();
4825        let async_bridge = AsyncBridge::new();
4826
4827        // Use 'cat' as a mock LSP server (it will just echo stdin to stdout)
4828        // This will fail to initialize but allows us to test the spawn mechanism
4829        let result = LspHandle::spawn(
4830            &runtime,
4831            "cat",
4832            &[],
4833            Default::default(),
4834            LanguageScope::single("test"),
4835            "test-server".to_string(),
4836            &async_bridge,
4837            ProcessLimits::unlimited(),
4838            Default::default(),
4839        );
4840
4841        // Should succeed in spawning
4842        assert!(result.is_ok());
4843
4844        let handle = result.unwrap();
4845
4846        // Let handle drop (which calls shutdown via Drop impl)
4847        drop(handle);
4848
4849        // Give task time to receive shutdown and exit
4850        tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
4851    }
4852
4853    #[tokio::test]
4854    async fn test_lsp_handle_did_open_queues_before_initialization() {
4855        let runtime = tokio::runtime::Handle::current();
4856        let async_bridge = AsyncBridge::new();
4857
4858        let handle = LspHandle::spawn(
4859            &runtime,
4860            "cat",
4861            &[],
4862            Default::default(),
4863            LanguageScope::single("test"),
4864            "test-server".to_string(),
4865            &async_bridge,
4866            ProcessLimits::unlimited(),
4867            Default::default(),
4868        )
4869        .unwrap();
4870
4871        // did_open now succeeds and queues the command for when server is initialized
4872        let result = handle.did_open(
4873            "file:///test.txt".parse().unwrap(),
4874            "fn main() {}".to_string(),
4875            "test".to_string(),
4876        );
4877
4878        // Should succeed (command is queued)
4879        assert!(result.is_ok());
4880    }
4881
4882    #[tokio::test]
4883    async fn test_lsp_handle_did_change_queues_before_initialization() {
4884        let runtime = tokio::runtime::Handle::current();
4885        let async_bridge = AsyncBridge::new();
4886
4887        let handle = LspHandle::spawn(
4888            &runtime,
4889            "cat",
4890            &[],
4891            Default::default(),
4892            LanguageScope::single("test"),
4893            "test-server".to_string(),
4894            &async_bridge,
4895            ProcessLimits::unlimited(),
4896            Default::default(),
4897        )
4898        .unwrap();
4899
4900        // Test incremental sync: insert "fn main() {}" at position (0, 0)
4901        let result = handle.did_change(
4902            "file:///test.rs".parse().unwrap(),
4903            vec![TextDocumentContentChangeEvent {
4904                range: Some(lsp_types::Range::new(
4905                    lsp_types::Position::new(0, 0),
4906                    lsp_types::Position::new(0, 0),
4907                )),
4908                range_length: None,
4909                text: "fn main() {}".to_string(),
4910            }],
4911        );
4912
4913        // Should succeed (command is queued)
4914        assert!(result.is_ok());
4915    }
4916
4917    #[tokio::test]
4918    async fn test_lsp_handle_incremental_change_with_range() {
4919        let runtime = tokio::runtime::Handle::current();
4920        let async_bridge = AsyncBridge::new();
4921
4922        let handle = LspHandle::spawn(
4923            &runtime,
4924            "cat",
4925            &[],
4926            Default::default(),
4927            LanguageScope::single("test"),
4928            "test-server".to_string(),
4929            &async_bridge,
4930            ProcessLimits::unlimited(),
4931            Default::default(),
4932        )
4933        .unwrap();
4934
4935        // Test incremental delete: remove text from (0, 3) to (0, 7)
4936        let result = handle.did_change(
4937            "file:///test.rs".parse().unwrap(),
4938            vec![TextDocumentContentChangeEvent {
4939                range: Some(lsp_types::Range::new(
4940                    lsp_types::Position::new(0, 3),
4941                    lsp_types::Position::new(0, 7),
4942                )),
4943                range_length: None,
4944                text: String::new(), // Empty string for deletion
4945            }],
4946        );
4947
4948        // Should succeed (command is queued)
4949        assert!(result.is_ok());
4950    }
4951
4952    #[tokio::test]
4953    async fn test_lsp_handle_spawn_invalid_command() {
4954        let runtime = tokio::runtime::Handle::current();
4955        let async_bridge = AsyncBridge::new();
4956
4957        // Try to spawn with an invalid command
4958        let result = LspHandle::spawn(
4959            &runtime,
4960            "this-command-does-not-exist-12345",
4961            &[],
4962            Default::default(),
4963            LanguageScope::single("test"),
4964            "test-server".to_string(),
4965            &async_bridge,
4966            ProcessLimits::unlimited(),
4967            Default::default(),
4968        );
4969
4970        // Should succeed in creating handle (error happens asynchronously)
4971        // The error will be sent to async_bridge
4972        assert!(result.is_ok());
4973
4974        // Give the task time to fail
4975        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
4976
4977        // Check that we received an error message
4978        let messages = async_bridge.try_recv_all();
4979        assert!(!messages.is_empty());
4980
4981        let has_error = messages
4982            .iter()
4983            .any(|msg| matches!(msg, AsyncMessage::LspError { .. }));
4984        assert!(has_error, "Expected LspError message");
4985    }
4986
4987    #[test]
4988    fn test_lsp_handle_shutdown_from_sync_context() {
4989        // Test shutdown from a synchronous context (requires spawning a separate thread)
4990        // This simulates how shutdown is called from the main editor loop
4991        std::thread::spawn(|| {
4992            // Create a tokio runtime for this thread
4993            let rt = tokio::runtime::Runtime::new().unwrap();
4994            let async_bridge = AsyncBridge::new();
4995
4996            let handle = rt.block_on(async {
4997                let runtime = tokio::runtime::Handle::current();
4998                LspHandle::spawn(
4999                    &runtime,
5000                    "cat",
5001                    &[],
5002                    Default::default(),
5003                    LanguageScope::single("test"),
5004                    "test-server".to_string(),
5005                    &async_bridge,
5006                    ProcessLimits::unlimited(),
5007                    Default::default(),
5008                )
5009                .unwrap()
5010            });
5011
5012            // This should succeed from a non-async context
5013            assert!(handle.shutdown().is_ok());
5014
5015            // Give task time to exit
5016            std::thread::sleep(std::time::Duration::from_millis(50));
5017        })
5018        .join()
5019        .unwrap();
5020    }
5021
5022    #[test]
5023    fn test_lsp_command_debug_format() {
5024        // Test that LspCommand has Debug implementation
5025        let cmd = LspCommand::Shutdown;
5026        let debug_str = format!("{:?}", cmd);
5027        assert!(debug_str.contains("Shutdown"));
5028    }
5029
5030    #[test]
5031    fn test_lsp_client_state_can_initialize_from_starting() {
5032        // This test verifies that the state machine allows initialization from the Starting state.
5033        // This is critical because LspHandle::spawn() sets state to Starting, and then
5034        // get_or_spawn() immediately calls handle.initialize(). Without this fix,
5035        // initialization would fail with "Cannot initialize: client is in state Starting".
5036
5037        let state = LspClientState::Starting;
5038
5039        // The fix: Starting state should allow initialization
5040        assert!(
5041            state.can_initialize(),
5042            "Starting state must allow initialization to avoid race condition"
5043        );
5044
5045        // Verify the full initialization flow works
5046        let mut state = LspClientState::Starting;
5047
5048        // Should be able to transition to Initializing
5049        assert!(state.can_transition_to(LspClientState::Initializing));
5050        assert!(state.transition_to(LspClientState::Initializing).is_ok());
5051
5052        // And then to Running
5053        assert!(state.can_transition_to(LspClientState::Running));
5054        assert!(state.transition_to(LspClientState::Running).is_ok());
5055    }
5056
5057    #[tokio::test]
5058    async fn test_lsp_handle_initialize_from_starting_state() {
5059        // This test reproduces the bug where initialize() would fail because
5060        // the handle's state is Starting (set by spawn()) but can_initialize()
5061        // only allowed Initial or Stopped states.
5062        //
5063        // The bug manifested as:
5064        // ERROR: Failed to send initialize command for rust: Cannot initialize: client is in state Starting
5065
5066        let runtime = tokio::runtime::Handle::current();
5067        let async_bridge = AsyncBridge::new();
5068
5069        // Spawn creates the handle with state = Starting
5070        let handle = LspHandle::spawn(
5071            &runtime,
5072            "cat", // Simple command that will exit immediately
5073            &[],
5074            Default::default(),
5075            LanguageScope::single("test"),
5076            "test-server".to_string(),
5077            &async_bridge,
5078            ProcessLimits::unlimited(),
5079            Default::default(),
5080        )
5081        .unwrap();
5082
5083        // Immediately call initialize - this is what get_or_spawn() does
5084        // Before the fix, this would fail with "Cannot initialize: client is in state Starting"
5085        let result = handle.initialize(None, None);
5086
5087        assert!(
5088            result.is_ok(),
5089            "initialize() must succeed from Starting state. Got error: {:?}",
5090            result.err()
5091        );
5092    }
5093
5094    #[tokio::test]
5095    async fn test_lsp_state_machine_race_condition_fix() {
5096        // Integration test that simulates the exact flow that was broken:
5097        // 1. LspManager::get_or_spawn() calls LspHandle::spawn()
5098        // 2. spawn() sets state to Starting and spawns async task
5099        // 3. get_or_spawn() immediately calls handle.initialize()
5100        // 4. initialize() should succeed even though state is Starting
5101
5102        let runtime = tokio::runtime::Handle::current();
5103        let async_bridge = AsyncBridge::new();
5104
5105        // Create a simple fake LSP server script that responds to initialize
5106        let fake_lsp_script = r#"
5107            read -r line  # Read Content-Length header
5108            read -r empty # Read empty line
5109            read -r json  # Read JSON body
5110
5111            # Send a valid initialize response
5112            response='{"jsonrpc":"2.0","id":1,"result":{"capabilities":{}}}'
5113            echo "Content-Length: ${#response}"
5114            echo ""
5115            echo -n "$response"
5116
5117            # Keep running to avoid EOF
5118            sleep 10
5119        "#;
5120
5121        // Spawn with bash to execute the fake LSP
5122        let handle = LspHandle::spawn(
5123            &runtime,
5124            "bash",
5125            &["-c".to_string(), fake_lsp_script.to_string()],
5126            Default::default(),
5127            LanguageScope::single("fake"),
5128            "test-server".to_string(),
5129            &async_bridge,
5130            ProcessLimits::unlimited(),
5131            Default::default(),
5132        )
5133        .unwrap();
5134
5135        // This is the critical test: initialize must succeed from Starting state
5136        let init_result = handle.initialize(None, None);
5137        assert!(
5138            init_result.is_ok(),
5139            "initialize() failed from Starting state: {:?}",
5140            init_result.err()
5141        );
5142
5143        // Give the async task time to process
5144        tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
5145
5146        // Check that we received status update messages
5147        let messages = async_bridge.try_recv_all();
5148        let has_status_update = messages
5149            .iter()
5150            .any(|msg| matches!(msg, AsyncMessage::LspStatusUpdate { .. }));
5151
5152        assert!(
5153            has_status_update,
5154            "Expected status update messages from LSP initialization"
5155        );
5156
5157        // Cleanup - best-effort, test is ending
5158        #[allow(clippy::let_underscore_must_use)]
5159        let _ = handle.shutdown();
5160    }
5161}