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