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