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