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