Skip to main content

fresh/services/lsp/
async_handler.rs

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