Skip to main content

fresh/services/lsp/
async_handler.rs

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