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