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