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, DidOpenTextDocument, DidSaveTextDocument, Initialized, Notification,
22        PublishDiagnostics,
23    },
24    request::{Initialize, Request},
25    ClientCapabilities, DidChangeTextDocumentParams, DidOpenTextDocumentParams,
26    DidSaveTextDocumentParams, InitializeParams, InitializeResult, InitializedParams,
27    PublishDiagnosticsParams, SemanticTokenModifier, SemanticTokenType,
28    SemanticTokensClientCapabilities, SemanticTokensClientCapabilitiesRequests,
29    SemanticTokensFullOptions, SemanticTokensLegend, SemanticTokensParams, SemanticTokensResult,
30    SemanticTokensServerCapabilities, ServerCapabilities, TextDocumentContentChangeEvent,
31    TextDocumentIdentifier, TextDocumentItem, TokenFormat, Uri, VersionedTextDocumentIdentifier,
32    WindowClientCapabilities, WorkspaceFolder,
33};
34use serde::{Deserialize, Serialize};
35use serde_json::Value;
36use std::collections::HashMap;
37use std::path::PathBuf;
38use std::sync::atomic::{AtomicBool, Ordering};
39use std::sync::{mpsc as std_mpsc, Arc, Mutex};
40use std::time::Instant;
41use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
42use tokio::process::{Child, ChildStdin, ChildStdout, Command};
43use tokio::sync::{mpsc, oneshot};
44
45/// Grace period after didOpen before sending didChange (in milliseconds)
46/// This gives the LSP server time to process didOpen before receiving changes
47const DID_OPEN_GRACE_PERIOD_MS: u64 = 200;
48
49/// Check if a document is already open and should skip didOpen.
50/// Returns true if the document is already open (should skip), false if it should proceed.
51fn should_skip_did_open(
52    document_versions: &HashMap<PathBuf, i64>,
53    path: &PathBuf,
54    language: &str,
55    uri: &Uri,
56) -> bool {
57    if document_versions.contains_key(path) {
58        tracing::debug!(
59            "LSP ({}): skipping didOpen - document already open: {}",
60            language,
61            uri.as_str()
62        );
63        true
64    } else {
65        false
66    }
67}
68
69/// A JSON-RPC message
70#[derive(Debug, Clone, Serialize, Deserialize)]
71#[serde(untagged)]
72pub enum JsonRpcMessage {
73    Request(JsonRpcRequest),
74    Response(JsonRpcResponse),
75    Notification(JsonRpcNotification),
76}
77
78/// A JSON-RPC request
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct JsonRpcRequest {
81    pub jsonrpc: String,
82    pub id: i64,
83    pub method: String,
84    pub params: Option<Value>,
85}
86
87/// A JSON-RPC response
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct JsonRpcResponse {
90    pub jsonrpc: String,
91    pub id: i64,
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub result: Option<Value>,
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub error: Option<JsonRpcError>,
96}
97
98/// A JSON-RPC notification (no response expected)
99#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct JsonRpcNotification {
101    pub jsonrpc: String,
102    pub method: String,
103    pub params: Option<Value>,
104}
105
106/// A JSON-RPC error
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct JsonRpcError {
109    pub code: i64,
110    pub message: String,
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub data: Option<Value>,
113}
114
115/// LSP client state machine
116///
117/// Tracks the lifecycle of the LSP client connection with proper state transitions.
118/// This prevents invalid operations (e.g., can't initialize twice, can't send requests when stopped).
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120pub enum LspClientState {
121    /// Initial state before spawning
122    Initial,
123    /// Process spawning in progress
124    Starting,
125    /// Initialize request sent, waiting for response
126    Initializing,
127    /// Initialized and ready for requests
128    Running,
129    /// Shutdown in progress
130    Stopping,
131    /// Cleanly stopped
132    Stopped,
133    /// Failed or crashed
134    Error,
135}
136
137impl LspClientState {
138    /// Check if this state can transition to another state
139    pub fn can_transition_to(&self, next: LspClientState) -> bool {
140        use LspClientState::*;
141        match (self, next) {
142            // From Initial, can only start
143            (Initial, Starting) => true,
144            // From Starting, can initialize or error
145            (Starting, Initializing) | (Starting, Error) => true,
146            // From Initializing, can become running or error
147            (Initializing, Running) | (Initializing, Error) => true,
148            // From Running, can stop or error
149            (Running, Stopping) | (Running, Error) => true,
150            // From Stopping, can become stopped or error
151            (Stopping, Stopped) | (Stopping, Error) => true,
152            // From Stopped or Error, can restart
153            (Stopped, Starting) | (Error, Starting) => true,
154            // Any state can become error
155            (_, Error) => true,
156            // Same state is always valid (no-op)
157            (a, b) if *a == b => true,
158            // All other transitions are invalid
159            _ => false,
160        }
161    }
162
163    /// Transition to a new state, returning error if invalid
164    pub fn transition_to(&mut self, next: LspClientState) -> Result<(), String> {
165        if self.can_transition_to(next) {
166            *self = next;
167            Ok(())
168        } else {
169            Err(format!(
170                "Invalid state transition from {:?} to {:?}",
171                self, next
172            ))
173        }
174    }
175
176    /// Check if the client is ready to send requests
177    pub fn can_send_requests(&self) -> bool {
178        matches!(self, Self::Running)
179    }
180
181    /// Check if the client can accept initialization
182    pub fn can_initialize(&self) -> bool {
183        matches!(self, Self::Initial | Self::Starting | Self::Stopped)
184    }
185
186    /// Convert to LspServerStatus for UI reporting
187    pub fn to_server_status(&self) -> LspServerStatus {
188        match self {
189            Self::Initial => LspServerStatus::Starting,
190            Self::Starting => LspServerStatus::Starting,
191            Self::Initializing => LspServerStatus::Initializing,
192            Self::Running => LspServerStatus::Running,
193            Self::Stopping => LspServerStatus::Shutdown,
194            Self::Stopped => LspServerStatus::Shutdown,
195            Self::Error => LspServerStatus::Error,
196        }
197    }
198}
199
200/// Create common LSP client capabilities with workDoneProgress support
201fn create_client_capabilities() -> ClientCapabilities {
202    use lsp_types::{
203        GeneralClientCapabilities, RenameClientCapabilities, TextDocumentClientCapabilities,
204        WorkspaceClientCapabilities, WorkspaceEditClientCapabilities,
205    };
206
207    ClientCapabilities {
208        window: Some(WindowClientCapabilities {
209            work_done_progress: Some(true),
210            ..Default::default()
211        }),
212        workspace: Some(WorkspaceClientCapabilities {
213            apply_edit: Some(true),
214            workspace_edit: Some(WorkspaceEditClientCapabilities {
215                document_changes: Some(true),
216                ..Default::default()
217            }),
218            ..Default::default()
219        }),
220        text_document: Some(TextDocumentClientCapabilities {
221            rename: Some(RenameClientCapabilities {
222                dynamic_registration: Some(true),
223                prepare_support: Some(true),
224                honors_change_annotations: Some(true),
225                ..Default::default()
226            }),
227            semantic_tokens: Some(SemanticTokensClientCapabilities {
228                dynamic_registration: Some(true),
229                requests: SemanticTokensClientCapabilitiesRequests {
230                    range: Some(true),
231                    full: Some(SemanticTokensFullOptions::Delta { delta: Some(true) }),
232                },
233                token_types: vec![
234                    SemanticTokenType::NAMESPACE,
235                    SemanticTokenType::TYPE,
236                    SemanticTokenType::CLASS,
237                    SemanticTokenType::ENUM,
238                    SemanticTokenType::INTERFACE,
239                    SemanticTokenType::STRUCT,
240                    SemanticTokenType::TYPE_PARAMETER,
241                    SemanticTokenType::PARAMETER,
242                    SemanticTokenType::VARIABLE,
243                    SemanticTokenType::PROPERTY,
244                    SemanticTokenType::ENUM_MEMBER,
245                    SemanticTokenType::EVENT,
246                    SemanticTokenType::FUNCTION,
247                    SemanticTokenType::METHOD,
248                    SemanticTokenType::MACRO,
249                    SemanticTokenType::KEYWORD,
250                    SemanticTokenType::MODIFIER,
251                    SemanticTokenType::COMMENT,
252                    SemanticTokenType::STRING,
253                    SemanticTokenType::NUMBER,
254                    SemanticTokenType::REGEXP,
255                    SemanticTokenType::OPERATOR,
256                    SemanticTokenType::DECORATOR,
257                ],
258                token_modifiers: vec![
259                    SemanticTokenModifier::DECLARATION,
260                    SemanticTokenModifier::DEFINITION,
261                    SemanticTokenModifier::READONLY,
262                    SemanticTokenModifier::STATIC,
263                    SemanticTokenModifier::DEPRECATED,
264                    SemanticTokenModifier::ABSTRACT,
265                    SemanticTokenModifier::ASYNC,
266                    SemanticTokenModifier::MODIFICATION,
267                    SemanticTokenModifier::DOCUMENTATION,
268                    SemanticTokenModifier::DEFAULT_LIBRARY,
269                ],
270                formats: vec![TokenFormat::RELATIVE],
271                overlapping_token_support: Some(true),
272                multiline_token_support: Some(true),
273                server_cancel_support: Some(true),
274                augments_syntax_tokens: Some(true),
275            }),
276            ..Default::default()
277        }),
278        general: Some(GeneralClientCapabilities {
279            ..Default::default()
280        }),
281        // Enable rust-analyzer experimental features
282        experimental: Some(serde_json::json!({
283            "serverStatusNotification": true
284        })),
285        ..Default::default()
286    }
287}
288
289fn extract_semantic_token_capability(
290    capabilities: &ServerCapabilities,
291) -> (Option<SemanticTokensLegend>, bool, bool, bool) {
292    capabilities
293        .semantic_tokens_provider
294        .as_ref()
295        .map(|provider| match provider {
296            SemanticTokensServerCapabilities::SemanticTokensOptions(options) => (
297                Some(options.legend.clone()),
298                semantic_tokens_full_supported(&options.full),
299                semantic_tokens_full_delta_supported(&options.full),
300                options.range.unwrap_or(false),
301            ),
302            SemanticTokensServerCapabilities::SemanticTokensRegistrationOptions(options) => {
303                let legend = options.semantic_tokens_options.legend.clone();
304                let full = semantic_tokens_full_supported(&options.semantic_tokens_options.full);
305                let delta =
306                    semantic_tokens_full_delta_supported(&options.semantic_tokens_options.full);
307                let range = options.semantic_tokens_options.range.unwrap_or(false);
308                (Some(legend), full, delta, range)
309            }
310        })
311        .unwrap_or((None, false, false, false))
312}
313
314fn semantic_tokens_full_supported(full: &Option<SemanticTokensFullOptions>) -> bool {
315    match full {
316        Some(SemanticTokensFullOptions::Bool(v)) => *v,
317        Some(SemanticTokensFullOptions::Delta { .. }) => true,
318        None => false,
319    }
320}
321
322fn semantic_tokens_full_delta_supported(full: &Option<SemanticTokensFullOptions>) -> bool {
323    match full {
324        Some(SemanticTokensFullOptions::Delta { delta }) => delta.unwrap_or(false),
325        _ => false,
326    }
327}
328
329/// Commands sent from the main loop to the LSP task
330#[derive(Debug)]
331enum LspCommand {
332    /// Initialize the server
333    Initialize {
334        root_uri: Option<Uri>,
335        initialization_options: Option<Value>,
336        response: oneshot::Sender<Result<InitializeResult, String>>,
337    },
338
339    /// Notify document opened
340    DidOpen {
341        uri: Uri,
342        text: String,
343        language_id: String,
344    },
345
346    /// Notify document changed
347    DidChange {
348        uri: Uri,
349        content_changes: Vec<TextDocumentContentChangeEvent>,
350    },
351
352    /// Notify document saved
353    DidSave { uri: Uri, text: Option<String> },
354
355    /// Request completion at position
356    Completion {
357        request_id: u64,
358        uri: Uri,
359        line: u32,
360        character: u32,
361    },
362
363    /// Request go-to-definition
364    GotoDefinition {
365        request_id: u64,
366        uri: Uri,
367        line: u32,
368        character: u32,
369    },
370
371    /// Request rename
372    Rename {
373        request_id: u64,
374        uri: Uri,
375        line: u32,
376        character: u32,
377        new_name: String,
378    },
379
380    /// Request hover documentation
381    Hover {
382        request_id: u64,
383        uri: Uri,
384        line: u32,
385        character: u32,
386    },
387
388    /// Request find references
389    References {
390        request_id: u64,
391        uri: Uri,
392        line: u32,
393        character: u32,
394    },
395
396    /// Request signature help
397    SignatureHelp {
398        request_id: u64,
399        uri: Uri,
400        line: u32,
401        character: u32,
402    },
403
404    /// Request code actions
405    CodeActions {
406        request_id: u64,
407        uri: Uri,
408        start_line: u32,
409        start_char: u32,
410        end_line: u32,
411        end_char: u32,
412        diagnostics: Vec<lsp_types::Diagnostic>,
413    },
414
415    /// Request document diagnostics (pull model)
416    DocumentDiagnostic {
417        request_id: u64,
418        uri: Uri,
419        /// Previous result_id for incremental updates (None for full refresh)
420        previous_result_id: Option<String>,
421    },
422
423    /// Request inlay hints for a range (LSP 3.17+)
424    InlayHints {
425        request_id: u64,
426        uri: Uri,
427        /// Range to get hints for (typically viewport)
428        start_line: u32,
429        start_char: u32,
430        end_line: u32,
431        end_char: u32,
432    },
433
434    /// Request semantic tokens for the entire document
435    SemanticTokensFull { request_id: u64, uri: Uri },
436
437    /// Request semantic tokens delta for the entire document
438    SemanticTokensFullDelta {
439        request_id: u64,
440        uri: Uri,
441        previous_result_id: String,
442    },
443
444    /// Request semantic tokens for a range
445    SemanticTokensRange {
446        request_id: u64,
447        uri: Uri,
448        range: lsp_types::Range,
449    },
450
451    /// Cancel a pending request
452    CancelRequest {
453        /// Editor's request ID to cancel
454        request_id: u64,
455    },
456
457    /// Custom request initiated by a plugin
458    PluginRequest {
459        request_id: u64,
460        method: String,
461        params: Option<Value>,
462    },
463
464    /// Shutdown the server
465    Shutdown,
466}
467
468/// Mutable state for LSP command processing
469struct LspState {
470    /// Stdin for sending messages (shared with stdout reader for server responses)
471    stdin: Arc<tokio::sync::Mutex<ChildStdin>>,
472
473    /// Next request ID
474    next_id: i64,
475
476    /// Server capabilities
477    capabilities: Option<ServerCapabilities>,
478
479    /// Document versions
480    document_versions: HashMap<PathBuf, i64>,
481
482    /// Track when didOpen was sent for each document to avoid race with didChange
483    /// The LSP server needs time to process didOpen before it can handle didChange
484    pending_opens: HashMap<PathBuf, Instant>,
485
486    /// Whether initialized
487    initialized: bool,
488
489    /// Sender for async messages to main loop
490    async_tx: std_mpsc::Sender<AsyncMessage>,
491
492    /// Language ID (for error reporting)
493    language: String,
494
495    /// Mapping from editor request_id to LSP JSON-RPC id for cancellation
496    /// Key: editor request_id, Value: LSP JSON-RPC id
497    active_requests: HashMap<u64, i64>,
498}
499
500impl LspState {
501    /// Replay pending commands that were queued before initialization
502    #[allow(clippy::type_complexity)]
503    async fn replay_pending_commands(
504        &mut self,
505        commands: Vec<LspCommand>,
506        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
507    ) {
508        if commands.is_empty() {
509            return;
510        }
511        tracing::info!(
512            "Replaying {} pending commands after initialization",
513            commands.len()
514        );
515        for cmd in commands {
516            match cmd {
517                LspCommand::DidOpen {
518                    uri,
519                    text,
520                    language_id,
521                } => {
522                    tracing::info!("Replaying DidOpen for {}", uri.as_str());
523                    let _ = self
524                        .handle_did_open_sequential(uri, text, language_id, pending)
525                        .await;
526                }
527                LspCommand::DidChange {
528                    uri,
529                    content_changes,
530                } => {
531                    tracing::info!("Replaying DidChange for {}", uri.as_str());
532                    let _ = self
533                        .handle_did_change_sequential(uri, content_changes, pending)
534                        .await;
535                }
536                LspCommand::DidSave { uri, text } => {
537                    tracing::info!("Replaying DidSave for {}", uri.as_str());
538                    let _ = self.handle_did_save(uri, text).await;
539                }
540                LspCommand::SemanticTokensFull { request_id, uri } => {
541                    tracing::info!("Replaying semantic tokens request for {}", uri.as_str());
542                    let _ = self
543                        .handle_semantic_tokens_full(request_id, uri, pending)
544                        .await;
545                }
546                LspCommand::SemanticTokensFullDelta {
547                    request_id,
548                    uri,
549                    previous_result_id,
550                } => {
551                    tracing::info!(
552                        "Replaying semantic tokens delta request for {}",
553                        uri.as_str()
554                    );
555                    let _ = self
556                        .handle_semantic_tokens_full_delta(
557                            request_id,
558                            uri,
559                            previous_result_id,
560                            pending,
561                        )
562                        .await;
563                }
564                LspCommand::SemanticTokensRange {
565                    request_id,
566                    uri,
567                    range,
568                } => {
569                    tracing::info!(
570                        "Replaying semantic tokens range request for {}",
571                        uri.as_str()
572                    );
573                    let _ = self
574                        .handle_semantic_tokens_range(request_id, uri, range, pending)
575                        .await;
576                }
577                _ => {}
578            }
579        }
580    }
581
582    /// Write a message to stdin
583    async fn write_message<T: Serialize>(&mut self, message: &T) -> Result<(), String> {
584        let json =
585            serde_json::to_string(message).map_err(|e| format!("Serialization error: {}", e))?;
586
587        let content = format!("Content-Length: {}\r\n\r\n{}", json.len(), json);
588
589        tracing::trace!("Writing LSP message to stdin ({} bytes)", content.len());
590
591        let mut stdin = self.stdin.lock().await;
592        stdin
593            .write_all(content.as_bytes())
594            .await
595            .map_err(|e| format!("Failed to write to stdin: {}", e))?;
596
597        stdin
598            .flush()
599            .await
600            .map_err(|e| format!("Failed to flush stdin: {}", e))?;
601
602        tracing::trace!("Successfully sent LSP message");
603
604        Ok(())
605    }
606
607    /// Send a notification using lsp-types Notification trait (type-safe)
608    async fn send_notification<N>(&mut self, params: N::Params) -> Result<(), String>
609    where
610        N: Notification,
611    {
612        let notification = JsonRpcNotification {
613            jsonrpc: "2.0".to_string(),
614            method: N::METHOD.to_string(),
615            params: Some(
616                serde_json::to_value(params)
617                    .map_err(|e| format!("Failed to serialize params: {}", e))?,
618            ),
619        };
620
621        self.write_message(&notification).await
622    }
623
624    /// Send request using shared pending map
625    #[allow(clippy::type_complexity)]
626    async fn send_request_sequential<P: Serialize, R: for<'de> Deserialize<'de>>(
627        &mut self,
628        method: &str,
629        params: Option<P>,
630        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
631    ) -> Result<R, String> {
632        self.send_request_sequential_tracked(method, params, pending, None)
633            .await
634    }
635
636    /// Send request using shared pending map with optional editor request tracking
637    #[allow(clippy::type_complexity)]
638    async fn send_request_sequential_tracked<P: Serialize, R: for<'de> Deserialize<'de>>(
639        &mut self,
640        method: &str,
641        params: Option<P>,
642        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
643        editor_request_id: Option<u64>,
644    ) -> Result<R, String> {
645        let id = self.next_id;
646        self.next_id += 1;
647
648        // Track the mapping if editor_request_id is provided
649        if let Some(editor_id) = editor_request_id {
650            self.active_requests.insert(editor_id, id);
651            tracing::trace!("Tracking request: editor_id={}, lsp_id={}", editor_id, id);
652        }
653
654        let params_value = params
655            .map(|p| serde_json::to_value(p))
656            .transpose()
657            .map_err(|e| format!("Failed to serialize params: {}", e))?;
658        let request = JsonRpcRequest {
659            jsonrpc: "2.0".to_string(),
660            id,
661            method: method.to_string(),
662            params: params_value,
663        };
664
665        let (tx, rx) = oneshot::channel();
666        pending.lock().unwrap().insert(id, tx);
667
668        self.write_message(&request).await?;
669
670        tracing::trace!("Sent LSP request id={}, waiting for response...", id);
671
672        // Await response (this is OK now because the reader task will send it)
673        let result = rx
674            .await
675            .map_err(|_| "Response channel closed".to_string())??;
676
677        tracing::trace!("Received LSP response for request id={}", id);
678
679        // Remove tracking after response received
680        if let Some(editor_id) = editor_request_id {
681            self.active_requests.remove(&editor_id);
682            tracing::trace!("Completed request: editor_id={}, lsp_id={}", editor_id, id);
683        }
684
685        serde_json::from_value(result).map_err(|e| format!("Failed to deserialize response: {}", e))
686    }
687
688    /// Handle initialize command
689    #[allow(clippy::type_complexity)]
690    async fn handle_initialize_sequential(
691        &mut self,
692        root_uri: Option<Uri>,
693        initialization_options: Option<Value>,
694        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
695    ) -> Result<InitializeResult, String> {
696        tracing::info!(
697            "Initializing async LSP server with root_uri: {:?}, initialization_options: {:?}",
698            root_uri,
699            initialization_options
700        );
701
702        let workspace_folders = root_uri.as_ref().map(|uri| {
703            vec![WorkspaceFolder {
704                uri: uri.clone(),
705                name: uri
706                    .path()
707                    .as_str()
708                    .split('/')
709                    .next_back()
710                    .unwrap_or("workspace")
711                    .to_string(),
712            }]
713        });
714
715        #[allow(deprecated)]
716        let params = InitializeParams {
717            process_id: Some(std::process::id()),
718            capabilities: create_client_capabilities(),
719            workspace_folders,
720            initialization_options,
721            // Set the deprecated root_uri field for compatibility with LSP servers
722            // like csharp-ls that still require it (see issue #366)
723            root_uri: root_uri.clone(),
724            ..Default::default()
725        };
726
727        let result: InitializeResult = self
728            .send_request_sequential(Initialize::METHOD, Some(params), pending)
729            .await?;
730
731        self.capabilities = Some(result.capabilities.clone());
732
733        // Send initialized notification
734        self.send_notification::<Initialized>(InitializedParams {})
735            .await?;
736
737        self.initialized = true;
738
739        // Extract completion trigger characters from server capabilities
740        let completion_trigger_characters = result
741            .capabilities
742            .completion_provider
743            .as_ref()
744            .and_then(|cp| cp.trigger_characters.clone())
745            .unwrap_or_default();
746
747        let (
748            semantic_tokens_legend,
749            semantic_tokens_full,
750            semantic_tokens_full_delta,
751            semantic_tokens_range,
752        ) = extract_semantic_token_capability(&result.capabilities);
753
754        // Notify main loop
755        let _ = self.async_tx.send(AsyncMessage::LspInitialized {
756            language: self.language.clone(),
757            completion_trigger_characters,
758            semantic_tokens_legend,
759            semantic_tokens_full,
760            semantic_tokens_full_delta,
761            semantic_tokens_range,
762        });
763
764        // Send running status
765        let _ = self.async_tx.send(AsyncMessage::LspStatusUpdate {
766            language: self.language.clone(),
767            status: LspServerStatus::Running,
768            message: None,
769        });
770
771        tracing::info!("Async LSP server initialized successfully");
772
773        Ok(result)
774    }
775
776    /// Handle did_open command
777    #[allow(clippy::type_complexity)]
778    async fn handle_did_open_sequential(
779        &mut self,
780        uri: Uri,
781        text: String,
782        language_id: String,
783        _pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
784    ) -> Result<(), String> {
785        let path = PathBuf::from(uri.path().as_str());
786
787        if should_skip_did_open(&self.document_versions, &path, &self.language, &uri) {
788            return Ok(());
789        }
790
791        tracing::trace!("LSP: did_open for {}", uri.as_str());
792
793        let params = DidOpenTextDocumentParams {
794            text_document: TextDocumentItem {
795                uri: uri.clone(),
796                language_id,
797                version: 0,
798                text,
799            },
800        };
801
802        self.document_versions.insert(path.clone(), 0);
803
804        // Record when we sent didOpen so didChange can wait if needed
805        self.pending_opens.insert(path, Instant::now());
806
807        self.send_notification::<DidOpenTextDocument>(params).await
808    }
809
810    /// Handle did_change command
811    #[allow(clippy::type_complexity)]
812    async fn handle_did_change_sequential(
813        &mut self,
814        uri: Uri,
815        content_changes: Vec<TextDocumentContentChangeEvent>,
816        _pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
817    ) -> Result<(), String> {
818        tracing::trace!("LSP: did_change for {}", uri.as_str());
819
820        let path = PathBuf::from(uri.path().as_str());
821
822        // If the document hasn't been opened yet (not in document_versions),
823        // skip this change - the upcoming didOpen will have the current content
824        if !self.document_versions.contains_key(&path) {
825            tracing::debug!(
826                "LSP ({}): skipping didChange - document not yet opened",
827                self.language
828            );
829            return Ok(());
830        }
831
832        // Check if this document was recently opened and wait if needed
833        // This prevents race conditions where the server receives didChange
834        // before it has finished processing didOpen
835        if let Some(opened_at) = self.pending_opens.get(&path) {
836            let elapsed = opened_at.elapsed();
837            let grace_period = std::time::Duration::from_millis(DID_OPEN_GRACE_PERIOD_MS);
838            if elapsed < grace_period {
839                let wait_time = grace_period - elapsed;
840                tracing::debug!(
841                    "LSP ({}): waiting {:?} for didOpen grace period before didChange",
842                    self.language,
843                    wait_time
844                );
845                tokio::time::sleep(wait_time).await;
846            }
847            // Remove from pending_opens after grace period has passed
848            self.pending_opens.remove(&path);
849        }
850
851        let version = self.document_versions.entry(path).or_insert(0);
852        *version += 1;
853
854        let params = DidChangeTextDocumentParams {
855            text_document: VersionedTextDocumentIdentifier {
856                uri: uri.clone(),
857                version: *version as i32,
858            },
859            content_changes,
860        };
861
862        self.send_notification::<DidChangeTextDocument>(params)
863            .await
864    }
865
866    /// Handle did_save command
867    async fn handle_did_save(&mut self, uri: Uri, text: Option<String>) -> Result<(), String> {
868        tracing::trace!("LSP: did_save for {}", uri.as_str());
869
870        let params = DidSaveTextDocumentParams {
871            text_document: TextDocumentIdentifier { uri },
872            text,
873        };
874
875        self.send_notification::<DidSaveTextDocument>(params).await
876    }
877
878    /// Handle completion request
879    #[allow(clippy::type_complexity)]
880    async fn handle_completion(
881        &mut self,
882        request_id: u64,
883        uri: Uri,
884        line: u32,
885        character: u32,
886        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
887    ) -> Result<(), String> {
888        use lsp_types::{
889            CompletionParams, PartialResultParams, Position, TextDocumentIdentifier,
890            TextDocumentPositionParams, WorkDoneProgressParams,
891        };
892
893        tracing::trace!(
894            "LSP: completion request at {}:{}:{}",
895            uri.as_str(),
896            line,
897            character
898        );
899
900        let params = CompletionParams {
901            text_document_position: TextDocumentPositionParams {
902                text_document: TextDocumentIdentifier { uri },
903                position: Position { line, character },
904            },
905            work_done_progress_params: WorkDoneProgressParams::default(),
906            partial_result_params: PartialResultParams::default(),
907            context: None,
908        };
909
910        // Send request and get response (tracked for cancellation)
911        match self
912            .send_request_sequential_tracked::<_, Value>(
913                "textDocument/completion",
914                Some(params),
915                pending,
916                Some(request_id),
917            )
918            .await
919        {
920            Ok(result) => {
921                // Parse the completion response
922                let items = if let Ok(list) =
923                    serde_json::from_value::<lsp_types::CompletionList>(result.clone())
924                {
925                    list.items
926                } else {
927                    serde_json::from_value::<Vec<lsp_types::CompletionItem>>(result)
928                        .unwrap_or_default()
929                };
930
931                // Send to main loop
932                let _ = self
933                    .async_tx
934                    .send(AsyncMessage::LspCompletion { request_id, items });
935                Ok(())
936            }
937            Err(e) => {
938                tracing::error!("Completion request failed: {}", e);
939                // Send empty completion on error
940                let _ = self.async_tx.send(AsyncMessage::LspCompletion {
941                    request_id,
942                    items: vec![],
943                });
944                Err(e)
945            }
946        }
947    }
948
949    /// Handle go-to-definition request
950    #[allow(clippy::type_complexity)]
951    async fn handle_goto_definition(
952        &mut self,
953        request_id: u64,
954        uri: Uri,
955        line: u32,
956        character: u32,
957        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
958    ) -> Result<(), String> {
959        use lsp_types::{
960            GotoDefinitionParams, PartialResultParams, Position, TextDocumentIdentifier,
961            TextDocumentPositionParams, WorkDoneProgressParams,
962        };
963
964        tracing::trace!(
965            "LSP: go-to-definition request at {}:{}:{}",
966            uri.as_str(),
967            line,
968            character
969        );
970
971        let params = GotoDefinitionParams {
972            text_document_position_params: TextDocumentPositionParams {
973                text_document: TextDocumentIdentifier { uri },
974                position: Position { line, character },
975            },
976            work_done_progress_params: WorkDoneProgressParams::default(),
977            partial_result_params: PartialResultParams::default(),
978        };
979
980        // Send request and get response
981        match self
982            .send_request_sequential::<_, Value>("textDocument/definition", Some(params), pending)
983            .await
984        {
985            Ok(result) => {
986                // Parse the definition response (can be Location, Vec<Location>, or LocationLink)
987                let locations = if let Ok(loc) =
988                    serde_json::from_value::<lsp_types::Location>(result.clone())
989                {
990                    vec![loc]
991                } else if let Ok(locs) =
992                    serde_json::from_value::<Vec<lsp_types::Location>>(result.clone())
993                {
994                    locs
995                } else if let Ok(links) =
996                    serde_json::from_value::<Vec<lsp_types::LocationLink>>(result)
997                {
998                    // Convert LocationLink to Location
999                    links
1000                        .into_iter()
1001                        .map(|link| lsp_types::Location {
1002                            uri: link.target_uri,
1003                            range: link.target_selection_range,
1004                        })
1005                        .collect()
1006                } else {
1007                    vec![]
1008                };
1009
1010                // Send to main loop
1011                let _ = self.async_tx.send(AsyncMessage::LspGotoDefinition {
1012                    request_id,
1013                    locations,
1014                });
1015                Ok(())
1016            }
1017            Err(e) => {
1018                tracing::error!("Go-to-definition request failed: {}", e);
1019                // Send empty locations on error
1020                let _ = self.async_tx.send(AsyncMessage::LspGotoDefinition {
1021                    request_id,
1022                    locations: vec![],
1023                });
1024                Err(e)
1025            }
1026        }
1027    }
1028
1029    /// Handle rename request
1030    #[allow(clippy::type_complexity)]
1031    async fn handle_rename(
1032        &mut self,
1033        request_id: u64,
1034        uri: Uri,
1035        line: u32,
1036        character: u32,
1037        new_name: String,
1038        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
1039    ) -> Result<(), String> {
1040        use lsp_types::{
1041            Position, RenameParams, TextDocumentIdentifier, TextDocumentPositionParams,
1042            WorkDoneProgressParams,
1043        };
1044
1045        tracing::trace!(
1046            "LSP: rename request at {}:{}:{} to '{}'",
1047            uri.as_str(),
1048            line,
1049            character,
1050            new_name
1051        );
1052
1053        let params = RenameParams {
1054            text_document_position: TextDocumentPositionParams {
1055                text_document: TextDocumentIdentifier { uri },
1056                position: Position { line, character },
1057            },
1058            new_name,
1059            work_done_progress_params: WorkDoneProgressParams::default(),
1060        };
1061
1062        // Send request and get response
1063        match self
1064            .send_request_sequential::<_, Value>("textDocument/rename", Some(params), pending)
1065            .await
1066        {
1067            Ok(result) => {
1068                // Parse the workspace edit response
1069                match serde_json::from_value::<lsp_types::WorkspaceEdit>(result) {
1070                    Ok(workspace_edit) => {
1071                        // Send to main loop
1072                        let _ = self.async_tx.send(AsyncMessage::LspRename {
1073                            request_id,
1074                            result: Ok(workspace_edit),
1075                        });
1076                        Ok(())
1077                    }
1078                    Err(e) => {
1079                        tracing::error!("Failed to parse rename response: {}", e);
1080                        let _ = self.async_tx.send(AsyncMessage::LspRename {
1081                            request_id,
1082                            result: Err(format!("Failed to parse rename response: {}", e)),
1083                        });
1084                        Err(format!("Failed to parse rename response: {}", e))
1085                    }
1086                }
1087            }
1088            Err(e) => {
1089                tracing::error!("Rename request failed: {}", e);
1090                // Send error to main loop
1091                let _ = self.async_tx.send(AsyncMessage::LspRename {
1092                    request_id,
1093                    result: Err(e.clone()),
1094                });
1095                Err(e)
1096            }
1097        }
1098    }
1099
1100    /// Handle hover documentation request
1101    #[allow(clippy::type_complexity)]
1102    async fn handle_hover(
1103        &mut self,
1104        request_id: u64,
1105        uri: Uri,
1106        line: u32,
1107        character: u32,
1108        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
1109    ) -> Result<(), String> {
1110        use lsp_types::{
1111            HoverParams, Position, TextDocumentIdentifier, TextDocumentPositionParams,
1112            WorkDoneProgressParams,
1113        };
1114
1115        tracing::trace!(
1116            "LSP: hover request at {}:{}:{}",
1117            uri.as_str(),
1118            line,
1119            character
1120        );
1121
1122        let params = HoverParams {
1123            text_document_position_params: TextDocumentPositionParams {
1124                text_document: TextDocumentIdentifier { uri },
1125                position: Position { line, character },
1126            },
1127            work_done_progress_params: WorkDoneProgressParams::default(),
1128        };
1129
1130        // Send request and get response
1131        match self
1132            .send_request_sequential::<_, Value>("textDocument/hover", Some(params), pending)
1133            .await
1134        {
1135            Ok(result) => {
1136                tracing::debug!("Raw LSP hover response: {:?}", result);
1137                // Parse the hover response
1138                let (contents, is_markdown, range) = if result.is_null() {
1139                    // No hover information available
1140                    (String::new(), false, None)
1141                } else {
1142                    match serde_json::from_value::<lsp_types::Hover>(result) {
1143                        Ok(hover) => {
1144                            // Extract text from hover contents
1145                            let (contents, is_markdown) =
1146                                Self::extract_hover_contents(&hover.contents);
1147                            // Extract the range if provided (tells us which symbol was hovered)
1148                            let range = hover.range.map(|r| {
1149                                (
1150                                    (r.start.line, r.start.character),
1151                                    (r.end.line, r.end.character),
1152                                )
1153                            });
1154                            (contents, is_markdown, range)
1155                        }
1156                        Err(e) => {
1157                            tracing::error!("Failed to parse hover response: {}", e);
1158                            (String::new(), false, None)
1159                        }
1160                    }
1161                };
1162
1163                // Send to main loop
1164                let _ = self.async_tx.send(AsyncMessage::LspHover {
1165                    request_id,
1166                    contents,
1167                    is_markdown,
1168                    range,
1169                });
1170                Ok(())
1171            }
1172            Err(e) => {
1173                tracing::error!("Hover request failed: {}", e);
1174                // Send empty result on error (no hover available)
1175                let _ = self.async_tx.send(AsyncMessage::LspHover {
1176                    request_id,
1177                    contents: String::new(),
1178                    is_markdown: false,
1179                    range: None,
1180                });
1181                Err(e)
1182            }
1183        }
1184    }
1185
1186    /// Extract text from hover contents (handles both MarkedString and MarkupContent)
1187    /// Returns (content_string, is_markdown)
1188    fn extract_hover_contents(contents: &lsp_types::HoverContents) -> (String, bool) {
1189        use lsp_types::{HoverContents, MarkedString, MarkupContent, MarkupKind};
1190
1191        match contents {
1192            HoverContents::Scalar(marked) => match marked {
1193                MarkedString::String(s) => (s.clone(), false),
1194                MarkedString::LanguageString(ls) => {
1195                    // Language strings are formatted as markdown code blocks
1196                    (format!("```{}\n{}\n```", ls.language, ls.value), true)
1197                }
1198            },
1199            HoverContents::Array(arr) => {
1200                // Array of marked strings - format as markdown
1201                let content = arr
1202                    .iter()
1203                    .map(|marked| match marked {
1204                        MarkedString::String(s) => s.clone(),
1205                        MarkedString::LanguageString(ls) => {
1206                            format!("```{}\n{}\n```", ls.language, ls.value)
1207                        }
1208                    })
1209                    .collect::<Vec<_>>()
1210                    .join("\n\n");
1211                (content, true)
1212            }
1213            HoverContents::Markup(MarkupContent { kind, value }) => {
1214                // Check if it's markdown or plaintext
1215                let is_markdown = matches!(kind, MarkupKind::Markdown);
1216                (value.clone(), is_markdown)
1217            }
1218        }
1219    }
1220
1221    /// Handle find references request
1222    #[allow(clippy::type_complexity)]
1223    async fn handle_references(
1224        &mut self,
1225        request_id: u64,
1226        uri: Uri,
1227        line: u32,
1228        character: u32,
1229        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
1230    ) -> Result<(), String> {
1231        use lsp_types::{
1232            PartialResultParams, Position, ReferenceContext, ReferenceParams,
1233            TextDocumentIdentifier, WorkDoneProgressParams,
1234        };
1235
1236        tracing::trace!(
1237            "LSP: find references request at {}:{}:{}",
1238            uri.as_str(),
1239            line,
1240            character
1241        );
1242
1243        let params = ReferenceParams {
1244            text_document_position: lsp_types::TextDocumentPositionParams {
1245                text_document: TextDocumentIdentifier { uri },
1246                position: Position { line, character },
1247            },
1248            work_done_progress_params: WorkDoneProgressParams::default(),
1249            partial_result_params: PartialResultParams::default(),
1250            context: ReferenceContext {
1251                include_declaration: true,
1252            },
1253        };
1254
1255        // Send request and get response
1256        match self
1257            .send_request_sequential::<_, Value>("textDocument/references", Some(params), pending)
1258            .await
1259        {
1260            Ok(result) => {
1261                // Parse the references response (Vec<Location> or null)
1262                let locations = if result.is_null() {
1263                    Vec::new()
1264                } else {
1265                    serde_json::from_value::<Vec<lsp_types::Location>>(result).unwrap_or_default()
1266                };
1267
1268                tracing::trace!("LSP: found {} references", locations.len());
1269
1270                // Send to main loop
1271                let _ = self.async_tx.send(AsyncMessage::LspReferences {
1272                    request_id,
1273                    locations,
1274                });
1275                Ok(())
1276            }
1277            Err(e) => {
1278                tracing::error!("Find references request failed: {}", e);
1279                // Send empty result on error
1280                let _ = self.async_tx.send(AsyncMessage::LspReferences {
1281                    request_id,
1282                    locations: Vec::new(),
1283                });
1284                Err(e)
1285            }
1286        }
1287    }
1288
1289    /// Handle signature help request
1290    #[allow(clippy::type_complexity)]
1291    async fn handle_signature_help(
1292        &mut self,
1293        request_id: u64,
1294        uri: Uri,
1295        line: u32,
1296        character: u32,
1297        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
1298    ) -> Result<(), String> {
1299        use lsp_types::{
1300            Position, SignatureHelpParams, TextDocumentIdentifier, TextDocumentPositionParams,
1301            WorkDoneProgressParams,
1302        };
1303
1304        tracing::trace!(
1305            "LSP: signature help request at {}:{}:{}",
1306            uri.as_str(),
1307            line,
1308            character
1309        );
1310
1311        let params = SignatureHelpParams {
1312            text_document_position_params: TextDocumentPositionParams {
1313                text_document: TextDocumentIdentifier { uri },
1314                position: Position { line, character },
1315            },
1316            work_done_progress_params: WorkDoneProgressParams::default(),
1317            context: None, // We can add context later for re-triggers
1318        };
1319
1320        // Send request and get response
1321        match self
1322            .send_request_sequential::<_, Value>(
1323                "textDocument/signatureHelp",
1324                Some(params),
1325                pending,
1326            )
1327            .await
1328        {
1329            Ok(result) => {
1330                // Parse the signature help response (SignatureHelp or null)
1331                let signature_help = if result.is_null() {
1332                    None
1333                } else {
1334                    serde_json::from_value::<lsp_types::SignatureHelp>(result).ok()
1335                };
1336
1337                tracing::trace!(
1338                    "LSP: signature help received: {} signatures",
1339                    signature_help
1340                        .as_ref()
1341                        .map(|h| h.signatures.len())
1342                        .unwrap_or(0)
1343                );
1344
1345                // Send to main loop
1346                let _ = self.async_tx.send(AsyncMessage::LspSignatureHelp {
1347                    request_id,
1348                    signature_help,
1349                });
1350                Ok(())
1351            }
1352            Err(e) => {
1353                tracing::error!("Signature help request failed: {}", e);
1354                // Send empty result on error
1355                let _ = self.async_tx.send(AsyncMessage::LspSignatureHelp {
1356                    request_id,
1357                    signature_help: None,
1358                });
1359                Err(e)
1360            }
1361        }
1362    }
1363
1364    /// Handle code actions request
1365    #[allow(clippy::type_complexity)]
1366    #[allow(clippy::too_many_arguments)]
1367    async fn handle_code_actions(
1368        &mut self,
1369        request_id: u64,
1370        uri: Uri,
1371        start_line: u32,
1372        start_char: u32,
1373        end_line: u32,
1374        end_char: u32,
1375        diagnostics: Vec<lsp_types::Diagnostic>,
1376        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
1377    ) -> Result<(), String> {
1378        use lsp_types::{
1379            CodeActionContext, CodeActionParams, PartialResultParams, Position, Range,
1380            TextDocumentIdentifier, WorkDoneProgressParams,
1381        };
1382
1383        tracing::trace!(
1384            "LSP: code actions request at {}:{}:{}-{}:{}",
1385            uri.as_str(),
1386            start_line,
1387            start_char,
1388            end_line,
1389            end_char
1390        );
1391
1392        let params = CodeActionParams {
1393            text_document: TextDocumentIdentifier { uri },
1394            range: Range {
1395                start: Position {
1396                    line: start_line,
1397                    character: start_char,
1398                },
1399                end: Position {
1400                    line: end_line,
1401                    character: end_char,
1402                },
1403            },
1404            context: CodeActionContext {
1405                diagnostics,
1406                only: None,
1407                trigger_kind: None,
1408            },
1409            work_done_progress_params: WorkDoneProgressParams::default(),
1410            partial_result_params: PartialResultParams::default(),
1411        };
1412
1413        // Send request and get response
1414        match self
1415            .send_request_sequential::<_, Value>("textDocument/codeAction", Some(params), pending)
1416            .await
1417        {
1418            Ok(result) => {
1419                // Parse the code actions response (Vec<CodeActionOrCommand> or null)
1420                let actions = if result.is_null() {
1421                    Vec::new()
1422                } else {
1423                    serde_json::from_value::<Vec<lsp_types::CodeActionOrCommand>>(result)
1424                        .unwrap_or_default()
1425                };
1426
1427                tracing::trace!("LSP: received {} code actions", actions.len());
1428
1429                // Send to main loop
1430                let _ = self.async_tx.send(AsyncMessage::LspCodeActions {
1431                    request_id,
1432                    actions,
1433                });
1434                Ok(())
1435            }
1436            Err(e) => {
1437                tracing::error!("Code actions request failed: {}", e);
1438                // Send empty result on error
1439                let _ = self.async_tx.send(AsyncMessage::LspCodeActions {
1440                    request_id,
1441                    actions: Vec::new(),
1442                });
1443                Err(e)
1444            }
1445        }
1446    }
1447
1448    /// Handle document diagnostic request (pull diagnostics)
1449    #[allow(clippy::type_complexity)]
1450    async fn handle_document_diagnostic(
1451        &mut self,
1452        request_id: u64,
1453        uri: Uri,
1454        previous_result_id: Option<String>,
1455        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
1456    ) -> Result<(), String> {
1457        use lsp_types::{
1458            DocumentDiagnosticParams, PartialResultParams, TextDocumentIdentifier,
1459            WorkDoneProgressParams,
1460        };
1461
1462        // Check if server supports pull diagnostics (diagnosticProvider capability)
1463        if self
1464            .capabilities
1465            .as_ref()
1466            .and_then(|c| c.diagnostic_provider.as_ref())
1467            .is_none()
1468        {
1469            tracing::trace!(
1470                "LSP: server does not support pull diagnostics, skipping request for {}",
1471                uri.as_str()
1472            );
1473            return Ok(());
1474        }
1475
1476        tracing::trace!(
1477            "LSP: document diagnostic request for {} (previous_result_id: {:?})",
1478            uri.as_str(),
1479            previous_result_id
1480        );
1481
1482        let params = DocumentDiagnosticParams {
1483            text_document: TextDocumentIdentifier { uri: uri.clone() },
1484            identifier: None,
1485            previous_result_id,
1486            work_done_progress_params: WorkDoneProgressParams::default(),
1487            partial_result_params: PartialResultParams::default(),
1488        };
1489
1490        // Send request and get response
1491        match self
1492            .send_request_sequential::<_, Value>("textDocument/diagnostic", Some(params), pending)
1493            .await
1494        {
1495            Ok(result) => {
1496                // Parse the diagnostic report result
1497                // Can be RelatedFullDocumentDiagnosticReport or RelatedUnchangedDocumentDiagnosticReport
1498                let uri_string = uri.as_str().to_string();
1499
1500                // Try to parse as full report first
1501                if let Ok(full_report) = serde_json::from_value::<
1502                    lsp_types::RelatedFullDocumentDiagnosticReport,
1503                >(result.clone())
1504                {
1505                    let diagnostics = full_report.full_document_diagnostic_report.items;
1506                    let result_id = full_report.full_document_diagnostic_report.result_id;
1507
1508                    tracing::trace!(
1509                        "LSP: received {} diagnostics for {} (result_id: {:?})",
1510                        diagnostics.len(),
1511                        uri_string,
1512                        result_id
1513                    );
1514
1515                    let _ = self.async_tx.send(AsyncMessage::LspPulledDiagnostics {
1516                        request_id,
1517                        uri: uri_string,
1518                        result_id,
1519                        diagnostics,
1520                        unchanged: false,
1521                    });
1522                } else if let Ok(unchanged_report) = serde_json::from_value::<
1523                    lsp_types::RelatedUnchangedDocumentDiagnosticReport,
1524                >(result.clone())
1525                {
1526                    let result_id = unchanged_report
1527                        .unchanged_document_diagnostic_report
1528                        .result_id;
1529
1530                    tracing::trace!(
1531                        "LSP: diagnostics unchanged for {} (result_id: {:?})",
1532                        uri_string,
1533                        result_id
1534                    );
1535
1536                    let _ = self.async_tx.send(AsyncMessage::LspPulledDiagnostics {
1537                        request_id,
1538                        uri: uri_string,
1539                        result_id: Some(result_id),
1540                        diagnostics: Vec::new(),
1541                        unchanged: true,
1542                    });
1543                } else {
1544                    // Fallback: try to parse as DocumentDiagnosticReportResult
1545                    tracing::warn!(
1546                        "LSP: could not parse diagnostic report, sending empty: {}",
1547                        result
1548                    );
1549                    let _ = self.async_tx.send(AsyncMessage::LspPulledDiagnostics {
1550                        request_id,
1551                        uri: uri_string,
1552                        result_id: None,
1553                        diagnostics: Vec::new(),
1554                        unchanged: false,
1555                    });
1556                }
1557
1558                Ok(())
1559            }
1560            Err(e) => {
1561                tracing::error!("Document diagnostic request failed: {}", e);
1562                // Send empty result on error
1563                let _ = self.async_tx.send(AsyncMessage::LspPulledDiagnostics {
1564                    request_id,
1565                    uri: uri.as_str().to_string(),
1566                    result_id: None,
1567                    diagnostics: Vec::new(),
1568                    unchanged: false,
1569                });
1570                Err(e)
1571            }
1572        }
1573    }
1574
1575    /// Handle inlay hints request (LSP 3.17+)
1576    #[allow(clippy::type_complexity)]
1577    #[allow(clippy::too_many_arguments)]
1578    async fn handle_inlay_hints(
1579        &mut self,
1580        request_id: u64,
1581        uri: Uri,
1582        start_line: u32,
1583        start_char: u32,
1584        end_line: u32,
1585        end_char: u32,
1586        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
1587    ) -> Result<(), String> {
1588        use lsp_types::{
1589            InlayHintParams, Position, Range, TextDocumentIdentifier, WorkDoneProgressParams,
1590        };
1591
1592        tracing::trace!(
1593            "LSP: inlay hints request for {} ({}:{} - {}:{})",
1594            uri.as_str(),
1595            start_line,
1596            start_char,
1597            end_line,
1598            end_char
1599        );
1600
1601        let params = InlayHintParams {
1602            text_document: TextDocumentIdentifier { uri: uri.clone() },
1603            range: Range {
1604                start: Position {
1605                    line: start_line,
1606                    character: start_char,
1607                },
1608                end: Position {
1609                    line: end_line,
1610                    character: end_char,
1611                },
1612            },
1613            work_done_progress_params: WorkDoneProgressParams::default(),
1614        };
1615
1616        match self
1617            .send_request_sequential::<_, Option<Vec<lsp_types::InlayHint>>>(
1618                "textDocument/inlayHint",
1619                Some(params),
1620                pending,
1621            )
1622            .await
1623        {
1624            Ok(hints) => {
1625                let hints = hints.unwrap_or_default();
1626                let uri_string = uri.as_str().to_string();
1627
1628                tracing::trace!(
1629                    "LSP: received {} inlay hints for {}",
1630                    hints.len(),
1631                    uri_string
1632                );
1633
1634                let _ = self.async_tx.send(AsyncMessage::LspInlayHints {
1635                    request_id,
1636                    uri: uri_string,
1637                    hints,
1638                });
1639
1640                Ok(())
1641            }
1642            Err(e) => {
1643                tracing::error!("Inlay hints request failed: {}", e);
1644                // Send empty result on error
1645                let _ = self.async_tx.send(AsyncMessage::LspInlayHints {
1646                    request_id,
1647                    uri: uri.as_str().to_string(),
1648                    hints: Vec::new(),
1649                });
1650                Err(e)
1651            }
1652        }
1653    }
1654
1655    #[allow(clippy::type_complexity)]
1656    async fn handle_semantic_tokens_full(
1657        &mut self,
1658        request_id: u64,
1659        uri: Uri,
1660        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
1661    ) -> Result<(), String> {
1662        use lsp_types::{
1663            request::SemanticTokensFullRequest, PartialResultParams, TextDocumentIdentifier,
1664            WorkDoneProgressParams,
1665        };
1666
1667        tracing::trace!("LSP: semanticTokens/full request for {}", uri.as_str());
1668
1669        let params = SemanticTokensParams {
1670            work_done_progress_params: WorkDoneProgressParams::default(),
1671            partial_result_params: PartialResultParams::default(),
1672            text_document: TextDocumentIdentifier { uri: uri.clone() },
1673        };
1674
1675        match self
1676            .send_request_sequential_tracked::<_, Option<SemanticTokensResult>>(
1677                SemanticTokensFullRequest::METHOD,
1678                Some(params),
1679                pending,
1680                Some(request_id),
1681            )
1682            .await
1683        {
1684            Ok(result) => {
1685                let _ = self.async_tx.send(AsyncMessage::LspSemanticTokens {
1686                    request_id,
1687                    uri: uri.as_str().to_string(),
1688                    response: LspSemanticTokensResponse::Full(Ok(result)),
1689                });
1690                Ok(())
1691            }
1692            Err(e) => {
1693                tracing::error!("Semantic tokens request failed: {}", e);
1694                let _ = self.async_tx.send(AsyncMessage::LspSemanticTokens {
1695                    request_id,
1696                    uri: uri.as_str().to_string(),
1697                    response: LspSemanticTokensResponse::Full(Err(e.clone())),
1698                });
1699                Err(e)
1700            }
1701        }
1702    }
1703
1704    #[allow(clippy::type_complexity)]
1705    async fn handle_semantic_tokens_full_delta(
1706        &mut self,
1707        request_id: u64,
1708        uri: Uri,
1709        previous_result_id: String,
1710        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
1711    ) -> Result<(), String> {
1712        use lsp_types::{
1713            request::SemanticTokensFullDeltaRequest, PartialResultParams,
1714            SemanticTokensDeltaParams, SemanticTokensFullDeltaResult, TextDocumentIdentifier,
1715            WorkDoneProgressParams,
1716        };
1717
1718        tracing::trace!(
1719            "LSP: semanticTokens/full/delta request for {}",
1720            uri.as_str()
1721        );
1722
1723        let params = SemanticTokensDeltaParams {
1724            work_done_progress_params: WorkDoneProgressParams::default(),
1725            partial_result_params: PartialResultParams::default(),
1726            text_document: TextDocumentIdentifier { uri: uri.clone() },
1727            previous_result_id,
1728        };
1729
1730        match self
1731            .send_request_sequential_tracked::<_, Option<SemanticTokensFullDeltaResult>>(
1732                SemanticTokensFullDeltaRequest::METHOD,
1733                Some(params),
1734                pending,
1735                Some(request_id),
1736            )
1737            .await
1738        {
1739            Ok(result) => {
1740                let _ = self.async_tx.send(AsyncMessage::LspSemanticTokens {
1741                    request_id,
1742                    uri: uri.as_str().to_string(),
1743                    response: LspSemanticTokensResponse::FullDelta(Ok(result)),
1744                });
1745                Ok(())
1746            }
1747            Err(e) => {
1748                tracing::error!("Semantic tokens delta request failed: {}", e);
1749                let _ = self.async_tx.send(AsyncMessage::LspSemanticTokens {
1750                    request_id,
1751                    uri: uri.as_str().to_string(),
1752                    response: LspSemanticTokensResponse::FullDelta(Err(e.clone())),
1753                });
1754                Err(e)
1755            }
1756        }
1757    }
1758
1759    #[allow(clippy::type_complexity)]
1760    async fn handle_semantic_tokens_range(
1761        &mut self,
1762        request_id: u64,
1763        uri: Uri,
1764        range: lsp_types::Range,
1765        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
1766    ) -> Result<(), String> {
1767        use lsp_types::{
1768            request::SemanticTokensRangeRequest, PartialResultParams, SemanticTokensRangeParams,
1769            TextDocumentIdentifier, WorkDoneProgressParams,
1770        };
1771
1772        tracing::trace!("LSP: semanticTokens/range request for {}", uri.as_str());
1773
1774        let params = SemanticTokensRangeParams {
1775            work_done_progress_params: WorkDoneProgressParams::default(),
1776            partial_result_params: PartialResultParams::default(),
1777            text_document: TextDocumentIdentifier { uri: uri.clone() },
1778            range,
1779        };
1780
1781        match self
1782            .send_request_sequential_tracked::<_, Option<lsp_types::SemanticTokensRangeResult>>(
1783                SemanticTokensRangeRequest::METHOD,
1784                Some(params),
1785                pending,
1786                Some(request_id),
1787            )
1788            .await
1789        {
1790            Ok(result) => {
1791                let _ = self.async_tx.send(AsyncMessage::LspSemanticTokens {
1792                    request_id,
1793                    uri: uri.as_str().to_string(),
1794                    response: LspSemanticTokensResponse::Range(Ok(result)),
1795                });
1796                Ok(())
1797            }
1798            Err(e) => {
1799                tracing::error!("Semantic tokens range request failed: {}", e);
1800                let _ = self.async_tx.send(AsyncMessage::LspSemanticTokens {
1801                    request_id,
1802                    uri: uri.as_str().to_string(),
1803                    response: LspSemanticTokensResponse::Range(Err(e.clone())),
1804                });
1805                Err(e)
1806            }
1807        }
1808    }
1809
1810    /// Handle a plugin-initiated request by forwarding it to the server
1811    #[allow(clippy::type_complexity)]
1812    async fn handle_plugin_request(
1813        &mut self,
1814        request_id: u64,
1815        method: String,
1816        params: Option<Value>,
1817        pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
1818    ) {
1819        tracing::trace!(
1820            "Plugin request {} => method={} params={:?}",
1821            request_id,
1822            method,
1823            params
1824        );
1825        let result = self
1826            .send_request_sequential_tracked::<Value, Value>(
1827                &method,
1828                params,
1829                pending,
1830                Some(request_id),
1831            )
1832            .await;
1833
1834        tracing::trace!(
1835            "Plugin request {} completed with result {:?}",
1836            request_id,
1837            &result
1838        );
1839        let _ = self.async_tx.send(AsyncMessage::PluginLspResponse {
1840            language: self.language.clone(),
1841            request_id,
1842            result,
1843        });
1844    }
1845
1846    /// Handle shutdown command
1847    async fn handle_shutdown(&mut self) -> Result<(), String> {
1848        tracing::info!("Shutting down async LSP server");
1849
1850        let notification = JsonRpcNotification {
1851            jsonrpc: "2.0".to_string(),
1852            method: "shutdown".to_string(),
1853            params: None,
1854        };
1855
1856        self.write_message(&notification).await?;
1857
1858        let exit = JsonRpcNotification {
1859            jsonrpc: "2.0".to_string(),
1860            method: "exit".to_string(),
1861            params: None,
1862        };
1863
1864        self.write_message(&exit).await
1865    }
1866
1867    /// Send a cancel request notification to the server
1868    async fn send_cancel_request(&mut self, lsp_id: i64) -> Result<(), String> {
1869        tracing::trace!("Sending $/cancelRequest for LSP id {}", lsp_id);
1870
1871        let notification = JsonRpcNotification {
1872            jsonrpc: "2.0".to_string(),
1873            method: "$/cancelRequest".to_string(),
1874            params: Some(serde_json::json!({ "id": lsp_id })),
1875        };
1876
1877        self.write_message(&notification).await
1878    }
1879
1880    /// Cancel a request by editor request_id
1881    async fn handle_cancel_request(&mut self, request_id: u64) -> Result<(), String> {
1882        if let Some(lsp_id) = self.active_requests.remove(&request_id) {
1883            tracing::info!(
1884                "Cancelling request: editor_id={}, lsp_id={}",
1885                request_id,
1886                lsp_id
1887            );
1888            self.send_cancel_request(lsp_id).await
1889        } else {
1890            tracing::trace!(
1891                "Cancel request ignored: no active LSP request for editor_id={}",
1892                request_id
1893            );
1894            Ok(())
1895        }
1896    }
1897}
1898
1899/// Async LSP task that handles all I/O
1900struct LspTask {
1901    /// Process handle - kept alive for lifetime management (kill_on_drop)
1902    _process: Child,
1903
1904    /// Stdin for sending messages
1905    stdin: ChildStdin,
1906
1907    /// Stdout for receiving messages
1908    stdout: BufReader<ChildStdout>,
1909
1910    /// Next request ID
1911    next_id: i64,
1912
1913    /// Pending requests waiting for response
1914    pending: HashMap<i64, oneshot::Sender<Result<Value, String>>>,
1915
1916    /// Server capabilities
1917    capabilities: Option<ServerCapabilities>,
1918
1919    /// Document versions
1920    document_versions: HashMap<PathBuf, i64>,
1921
1922    /// Track when didOpen was sent for each document to avoid race with didChange
1923    /// The LSP server needs time to process didOpen before it can handle didChange
1924    pending_opens: HashMap<PathBuf, Instant>,
1925
1926    /// Whether initialized
1927    initialized: bool,
1928
1929    /// Sender for async messages to main loop
1930    async_tx: std_mpsc::Sender<AsyncMessage>,
1931
1932    /// Language ID (for error reporting)
1933    language: String,
1934
1935    /// Server command (for plugin identification)
1936    server_command: String,
1937
1938    /// Path to stderr log file
1939    stderr_log_path: std::path::PathBuf,
1940}
1941
1942impl LspTask {
1943    /// Create a new LSP task
1944    async fn spawn(
1945        command: &str,
1946        args: &[String],
1947        language: String,
1948        async_tx: std_mpsc::Sender<AsyncMessage>,
1949        process_limits: &ProcessLimits,
1950        stderr_log_path: std::path::PathBuf,
1951    ) -> Result<Self, String> {
1952        tracing::info!("Spawning async LSP server: {} {:?}", command, args);
1953        tracing::info!("Process limits: {:?}", process_limits);
1954        tracing::info!("LSP stderr will be logged to: {:?}", stderr_log_path);
1955
1956        // Check if the command exists before trying to spawn
1957        // This provides a clearer error message than the generic "No such file or directory"
1958        if !Self::command_exists(command) {
1959            return Err(format!(
1960                "LSP server executable '{}' not found. Please install it or check your PATH.",
1961                command
1962            ));
1963        }
1964
1965        // Create stderr log file and redirect process stderr directly to it
1966        let stderr_file = std::fs::File::create(&stderr_log_path).map_err(|e| {
1967            format!(
1968                "Failed to create LSP stderr log file {:?}: {}",
1969                stderr_log_path, e
1970            )
1971        })?;
1972
1973        let mut cmd = Command::new(command);
1974        cmd.args(args)
1975            .stdin(std::process::Stdio::piped())
1976            .stdout(std::process::Stdio::piped())
1977            .stderr(std::process::Stdio::from(stderr_file))
1978            .kill_on_drop(true);
1979
1980        // Apply resource limits to the process
1981        process_limits
1982            .apply_to_command(&mut cmd)
1983            .map_err(|e| format!("Failed to apply process limits: {}", e))?;
1984
1985        let mut process = cmd.spawn().map_err(|e| {
1986            format!(
1987                "Failed to spawn LSP server '{}': {}",
1988                command,
1989                match e.kind() {
1990                    std::io::ErrorKind::NotFound => "executable not found in PATH".to_string(),
1991                    std::io::ErrorKind::PermissionDenied =>
1992                        "permission denied (check file permissions)".to_string(),
1993                    _ => e.to_string(),
1994                }
1995            )
1996        })?;
1997
1998        let stdin = process
1999            .stdin
2000            .take()
2001            .ok_or_else(|| "Failed to get stdin".to_string())?;
2002
2003        let stdout = BufReader::new(
2004            process
2005                .stdout
2006                .take()
2007                .ok_or_else(|| "Failed to get stdout".to_string())?,
2008        );
2009
2010        Ok(Self {
2011            _process: process,
2012            stdin,
2013            stdout,
2014            next_id: 0,
2015            pending: HashMap::new(),
2016            capabilities: None,
2017            document_versions: HashMap::new(),
2018            pending_opens: HashMap::new(),
2019            initialized: false,
2020            async_tx,
2021            language,
2022            server_command: command.to_string(),
2023            stderr_log_path,
2024        })
2025    }
2026
2027    /// Check if a command exists in PATH or as an absolute path
2028    fn command_exists(command: &str) -> bool {
2029        use std::path::Path;
2030
2031        // If it's an absolute path, check if the file exists and is executable
2032        if command.contains('/') || command.contains('\\') {
2033            let path = Path::new(command);
2034            return path.exists() && path.is_file();
2035        }
2036
2037        // Otherwise, search in PATH
2038        if let Ok(path_var) = std::env::var("PATH") {
2039            #[cfg(unix)]
2040            let separator = ':';
2041            #[cfg(windows)]
2042            let separator = ';';
2043
2044            for dir in path_var.split(separator) {
2045                let full_path = Path::new(dir).join(command);
2046                if full_path.exists() && full_path.is_file() {
2047                    return true;
2048                }
2049                // On Windows, also check with .exe extension
2050                #[cfg(windows)]
2051                {
2052                    let with_exe = Path::new(dir).join(format!("{}.exe", command));
2053                    if with_exe.exists() && with_exe.is_file() {
2054                        return true;
2055                    }
2056                }
2057            }
2058        }
2059
2060        false
2061    }
2062
2063    /// Spawn the stdout reader task that continuously reads and dispatches LSP messages
2064    #[allow(clippy::type_complexity)]
2065    #[allow(clippy::too_many_arguments)]
2066    fn spawn_stdout_reader(
2067        mut stdout: BufReader<ChildStdout>,
2068        pending: Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
2069        async_tx: std_mpsc::Sender<AsyncMessage>,
2070        language: String,
2071        server_command: String,
2072        stdin_writer: Arc<tokio::sync::Mutex<ChildStdin>>,
2073        stderr_log_path: std::path::PathBuf,
2074        shutting_down: Arc<AtomicBool>,
2075    ) {
2076        tokio::spawn(async move {
2077            tracing::info!("LSP stdout reader task started for {}", language);
2078            loop {
2079                match read_message_from_stdout(&mut stdout).await {
2080                    Ok(message) => {
2081                        tracing::trace!("Read message from LSP server: {:?}", message);
2082                        if let Err(e) = handle_message_dispatch(
2083                            message,
2084                            &pending,
2085                            &async_tx,
2086                            &language,
2087                            &server_command,
2088                            &stdin_writer,
2089                        )
2090                        .await
2091                        {
2092                            tracing::error!("Error handling LSP message: {}", e);
2093                        }
2094                    }
2095                    Err(e) => {
2096                        // Only report error if this wasn't an intentional shutdown
2097                        if shutting_down.load(Ordering::SeqCst) {
2098                            tracing::info!(
2099                                "LSP stdout reader exiting due to graceful shutdown for {}",
2100                                language
2101                            );
2102                        } else {
2103                            tracing::error!("Error reading from LSP server: {}", e);
2104                            let _ = async_tx.send(AsyncMessage::LspStatusUpdate {
2105                                language: language.clone(),
2106                                status: LspServerStatus::Error,
2107                                message: None,
2108                            });
2109                            let _ = async_tx.send(AsyncMessage::LspError {
2110                                language: language.clone(),
2111                                error: format!("Read error: {}", e),
2112                                stderr_log_path: Some(stderr_log_path.clone()),
2113                            });
2114                        }
2115                        break;
2116                    }
2117                }
2118            }
2119            tracing::info!("LSP stdout reader task exiting for {}", language);
2120        });
2121    }
2122
2123    /// Run the task (processes commands and reads from stdout)
2124    async fn run(self, mut command_rx: mpsc::Receiver<LspCommand>) {
2125        tracing::info!("LspTask::run() started for language: {}", self.language);
2126
2127        // Create shared stdin writer so both command processing and stdout reader can write
2128        let stdin_writer = Arc::new(tokio::sync::Mutex::new(self.stdin));
2129
2130        // Create state struct for command processing
2131        let mut state = LspState {
2132            stdin: stdin_writer.clone(),
2133            next_id: self.next_id,
2134            capabilities: self.capabilities,
2135            document_versions: self.document_versions,
2136            pending_opens: self.pending_opens,
2137            initialized: self.initialized,
2138            async_tx: self.async_tx.clone(),
2139            language: self.language.clone(),
2140            active_requests: HashMap::new(),
2141        };
2142
2143        let pending = Arc::new(Mutex::new(self.pending));
2144        let async_tx = state.async_tx.clone();
2145        let language_clone = state.language.clone();
2146
2147        // Flag to indicate intentional shutdown (prevents spurious error messages)
2148        let shutting_down = Arc::new(AtomicBool::new(false));
2149
2150        // Spawn stdout reader task (shares stdin_writer for responding to server requests)
2151        Self::spawn_stdout_reader(
2152            self.stdout,
2153            pending.clone(),
2154            async_tx.clone(),
2155            language_clone.clone(),
2156            self.server_command.clone(),
2157            stdin_writer.clone(),
2158            self.stderr_log_path,
2159            shutting_down.clone(),
2160        );
2161
2162        // Sequential command processing loop
2163        // Note: Server responses (workspace/configuration, etc.) are now written directly
2164        // by the stdout reader task using the shared stdin_writer, avoiding deadlocks
2165        // when the main loop is blocked waiting for an LSP response.
2166        let mut pending_commands = Vec::new();
2167        loop {
2168            tokio::select! {
2169                // Handle commands from the editor
2170                Some(cmd) = command_rx.recv() => {
2171                    tracing::trace!("LspTask received command: {:?}", cmd);
2172                    match cmd {
2173                        LspCommand::Initialize { root_uri, initialization_options, response } => {
2174                            // Send initializing status
2175                            let _ = async_tx.send(AsyncMessage::LspStatusUpdate {
2176                                language: language_clone.clone(),
2177                                status: LspServerStatus::Initializing,
2178                                message: None,
2179                            });
2180                            tracing::info!("Processing Initialize command");
2181                            let result =
2182                                state.handle_initialize_sequential(root_uri, initialization_options, &pending).await;
2183                            let success = result.is_ok();
2184                            let _ = response.send(result);
2185
2186                            // After successful initialization, replay pending commands
2187                            if success {
2188                                let queued = std::mem::take(&mut pending_commands);
2189                                state.replay_pending_commands(queued, &pending).await;
2190                            }
2191                        }
2192                        LspCommand::DidOpen {
2193                            uri,
2194                            text,
2195                            language_id,
2196                        } => {
2197                            if state.initialized {
2198                                tracing::info!("Processing DidOpen for {}", uri.as_str());
2199                                let _ = state
2200                                    .handle_did_open_sequential(uri, text, language_id, &pending)
2201                                    .await;
2202                            } else {
2203                                tracing::trace!(
2204                                    "Queueing DidOpen for {} until initialization completes",
2205                                    uri.as_str()
2206                                );
2207                                pending_commands.push(LspCommand::DidOpen {
2208                                    uri,
2209                                    text,
2210                                    language_id,
2211                                });
2212                            }
2213                        }
2214                        LspCommand::DidChange {
2215                            uri,
2216                            content_changes,
2217                        } => {
2218                            if state.initialized {
2219                                tracing::trace!("Processing DidChange for {}", uri.as_str());
2220                                let _ = state
2221                                    .handle_did_change_sequential(uri, content_changes, &pending)
2222                                    .await;
2223                            } else {
2224                                tracing::trace!(
2225                                    "Queueing DidChange for {} until initialization completes",
2226                                    uri.as_str()
2227                                );
2228                                pending_commands.push(LspCommand::DidChange {
2229                                    uri,
2230                                    content_changes,
2231                                });
2232                            }
2233                        }
2234                        LspCommand::DidSave { uri, text } => {
2235                            if state.initialized {
2236                                tracing::info!("Processing DidSave for {}", uri.as_str());
2237                                let _ = state.handle_did_save(uri, text).await;
2238                            } else {
2239                                tracing::trace!(
2240                                    "Queueing DidSave for {} until initialization completes",
2241                                    uri.as_str()
2242                                );
2243                                pending_commands.push(LspCommand::DidSave { uri, text });
2244                            }
2245                        }
2246                        LspCommand::Completion {
2247                            request_id,
2248                            uri,
2249                            line,
2250                            character,
2251                        } => {
2252                            if state.initialized {
2253                                tracing::info!(
2254                                    "Processing Completion request for {}",
2255                                    uri.as_str()
2256                                );
2257                                let _ = state
2258                                    .handle_completion(request_id, uri, line, character, &pending)
2259                                    .await;
2260                            } else {
2261                                tracing::trace!("LSP not initialized, sending empty completion");
2262                                let _ = state.async_tx.send(AsyncMessage::LspCompletion {
2263                                    request_id,
2264                                    items: vec![],
2265                                });
2266                            }
2267                        }
2268                        LspCommand::GotoDefinition {
2269                            request_id,
2270                            uri,
2271                            line,
2272                            character,
2273                        } => {
2274                            if state.initialized {
2275                                tracing::info!(
2276                                    "Processing GotoDefinition request for {}",
2277                                    uri.as_str()
2278                                );
2279                                let _ = state
2280                                    .handle_goto_definition(
2281                                        request_id, uri, line, character, &pending,
2282                                    )
2283                                    .await;
2284                            } else {
2285                                tracing::trace!("LSP not initialized, sending empty locations");
2286                                let _ = state.async_tx.send(AsyncMessage::LspGotoDefinition {
2287                                    request_id,
2288                                    locations: vec![],
2289                                });
2290                            }
2291                        }
2292                        LspCommand::Rename {
2293                            request_id,
2294                            uri,
2295                            line,
2296                            character,
2297                            new_name,
2298                        } => {
2299                            if state.initialized {
2300                                tracing::info!("Processing Rename request for {}", uri.as_str());
2301                                let _ = state
2302                                    .handle_rename(
2303                                        request_id, uri, line, character, new_name, &pending,
2304                                    )
2305                                    .await;
2306                            } else {
2307                                tracing::trace!("LSP not initialized, cannot rename");
2308                                let _ = state.async_tx.send(AsyncMessage::LspRename {
2309                                    request_id,
2310                                    result: Err("LSP not initialized".to_string()),
2311                                });
2312                            }
2313                        }
2314                        LspCommand::Hover {
2315                            request_id,
2316                            uri,
2317                            line,
2318                            character,
2319                        } => {
2320                            if state.initialized {
2321                                tracing::info!("Processing Hover request for {}", uri.as_str());
2322                                let _ = state
2323                                    .handle_hover(request_id, uri, line, character, &pending)
2324                                    .await;
2325                            } else {
2326                                tracing::trace!("LSP not initialized, cannot get hover");
2327                                let _ = state.async_tx.send(AsyncMessage::LspHover {
2328                                    request_id,
2329                                    contents: String::new(),
2330                                    is_markdown: false,
2331                                    range: None,
2332                                });
2333                            }
2334                        }
2335                        LspCommand::References {
2336                            request_id,
2337                            uri,
2338                            line,
2339                            character,
2340                        } => {
2341                            if state.initialized {
2342                                tracing::info!("Processing References request for {}", uri.as_str());
2343                                let _ = state
2344                                    .handle_references(request_id, uri, line, character, &pending)
2345                                    .await;
2346                            } else {
2347                                tracing::trace!("LSP not initialized, cannot get references");
2348                                let _ = state.async_tx.send(AsyncMessage::LspReferences {
2349                                    request_id,
2350                                    locations: Vec::new(),
2351                                });
2352                            }
2353                        }
2354                        LspCommand::SignatureHelp {
2355                            request_id,
2356                            uri,
2357                            line,
2358                            character,
2359                        } => {
2360                            if state.initialized {
2361                                tracing::info!("Processing SignatureHelp request for {}", uri.as_str());
2362                                let _ = state
2363                                    .handle_signature_help(request_id, uri, line, character, &pending)
2364                                    .await;
2365                            } else {
2366                                tracing::trace!("LSP not initialized, cannot get signature help");
2367                                let _ = state.async_tx.send(AsyncMessage::LspSignatureHelp {
2368                                    request_id,
2369                                    signature_help: None,
2370                                });
2371                            }
2372                        }
2373                        LspCommand::CodeActions {
2374                            request_id,
2375                            uri,
2376                            start_line,
2377                            start_char,
2378                            end_line,
2379                            end_char,
2380                            diagnostics,
2381                        } => {
2382                            if state.initialized {
2383                                tracing::info!("Processing CodeActions request for {}", uri.as_str());
2384                                let _ = state
2385                                    .handle_code_actions(
2386                                        request_id,
2387                                        uri,
2388                                        start_line,
2389                                        start_char,
2390                                        end_line,
2391                                        end_char,
2392                                        diagnostics,
2393                                        &pending,
2394                                    )
2395                                    .await;
2396                            } else {
2397                                tracing::trace!("LSP not initialized, cannot get code actions");
2398                                let _ = state.async_tx.send(AsyncMessage::LspCodeActions {
2399                                    request_id,
2400                                    actions: Vec::new(),
2401                                });
2402                            }
2403                        }
2404                        LspCommand::DocumentDiagnostic {
2405                            request_id,
2406                            uri,
2407                            previous_result_id,
2408                        } => {
2409                            if state.initialized {
2410                                tracing::info!(
2411                                    "Processing DocumentDiagnostic request for {}",
2412                                    uri.as_str()
2413                                );
2414                                let _ = state
2415                                    .handle_document_diagnostic(
2416                                        request_id,
2417                                        uri,
2418                                        previous_result_id,
2419                                        &pending,
2420                                    )
2421                                    .await;
2422                            } else {
2423                                tracing::trace!(
2424                                    "LSP not initialized, cannot get document diagnostics"
2425                                );
2426                                let _ = state.async_tx.send(AsyncMessage::LspPulledDiagnostics {
2427                                    request_id,
2428                                    uri: uri.as_str().to_string(),
2429                                    result_id: None,
2430                                    diagnostics: Vec::new(),
2431                                    unchanged: false,
2432                                });
2433                            }
2434                        }
2435                        LspCommand::InlayHints {
2436                            request_id,
2437                            uri,
2438                            start_line,
2439                            start_char,
2440                            end_line,
2441                            end_char,
2442                        } => {
2443                            if state.initialized {
2444                                tracing::info!(
2445                                    "Processing InlayHints request for {}",
2446                                    uri.as_str()
2447                                );
2448                                let _ = state
2449                                    .handle_inlay_hints(
2450                                        request_id,
2451                                        uri,
2452                                        start_line,
2453                                        start_char,
2454                                        end_line,
2455                                        end_char,
2456                                        &pending,
2457                                    )
2458                                    .await;
2459                            } else {
2460                                tracing::trace!(
2461                                    "LSP not initialized, cannot get inlay hints"
2462                                );
2463                                let _ = state.async_tx.send(AsyncMessage::LspInlayHints {
2464                                    request_id,
2465                                    uri: uri.as_str().to_string(),
2466                                    hints: Vec::new(),
2467                                });
2468                            }
2469                        }
2470                        LspCommand::SemanticTokensFull { request_id, uri } => {
2471                            if state.initialized {
2472                                tracing::info!(
2473                                    "Processing SemanticTokens request for {}",
2474                                    uri.as_str()
2475                                );
2476                                let _ = state
2477                                    .handle_semantic_tokens_full(request_id, uri, &pending)
2478                                    .await;
2479                            } else {
2480                                tracing::trace!(
2481                                    "LSP not initialized, cannot get semantic tokens"
2482                                );
2483                                let _ = state.async_tx.send(AsyncMessage::LspSemanticTokens {
2484                                    request_id,
2485                                    uri: uri.as_str().to_string(),
2486                                    response: LspSemanticTokensResponse::Full(Err(
2487                                        "LSP not initialized".to_string(),
2488                                    )),
2489                                });
2490                            }
2491                        }
2492                        LspCommand::SemanticTokensFullDelta {
2493                            request_id,
2494                            uri,
2495                            previous_result_id,
2496                        } => {
2497                            if state.initialized {
2498                                tracing::info!(
2499                                    "Processing SemanticTokens delta request for {}",
2500                                    uri.as_str()
2501                                );
2502                                let _ = state
2503                                    .handle_semantic_tokens_full_delta(
2504                                        request_id,
2505                                        uri,
2506                                        previous_result_id,
2507                                        &pending,
2508                                    )
2509                                    .await;
2510                            } else {
2511                                tracing::trace!(
2512                                    "LSP not initialized, cannot get semantic tokens"
2513                                );
2514                                let _ = state.async_tx.send(AsyncMessage::LspSemanticTokens {
2515                                    request_id,
2516                                    uri: uri.as_str().to_string(),
2517                                    response: LspSemanticTokensResponse::FullDelta(Err(
2518                                        "LSP not initialized".to_string(),
2519                                    )),
2520                                });
2521                            }
2522                        }
2523                        LspCommand::SemanticTokensRange {
2524                            request_id,
2525                            uri,
2526                            range,
2527                        } => {
2528                            if state.initialized {
2529                                tracing::info!(
2530                                    "Processing SemanticTokens range request for {}",
2531                                    uri.as_str()
2532                                );
2533                                let _ = state
2534                                    .handle_semantic_tokens_range(request_id, uri, range, &pending)
2535                                    .await;
2536                            } else {
2537                                tracing::trace!(
2538                                    "LSP not initialized, cannot get semantic tokens"
2539                                );
2540                                let _ = state.async_tx.send(AsyncMessage::LspSemanticTokens {
2541                                    request_id,
2542                                    uri: uri.as_str().to_string(),
2543                                    response: LspSemanticTokensResponse::Range(Err(
2544                                        "LSP not initialized".to_string(),
2545                                    )),
2546                                });
2547                            }
2548                        }
2549                        LspCommand::CancelRequest { request_id } => {
2550                            tracing::info!(
2551                                "Processing CancelRequest for editor_id={}",
2552                                request_id
2553                            );
2554                            let _ = state.handle_cancel_request(request_id).await;
2555                        }
2556                        LspCommand::PluginRequest {
2557                            request_id,
2558                            method,
2559                            params,
2560                        } => {
2561                            if state.initialized {
2562                                tracing::trace!(
2563                                    "Processing plugin request {} ({})",
2564                                    request_id,
2565                                    method
2566                                );
2567                                let _ = state
2568                                    .handle_plugin_request(
2569                                        request_id,
2570                                        method,
2571                                        params,
2572                                        &pending,
2573                                    )
2574                                    .await;
2575                            } else {
2576                                tracing::trace!(
2577                                    "Plugin LSP request {} received before initialization",
2578                                    request_id
2579                                );
2580                                let _ = state.async_tx.send(AsyncMessage::PluginLspResponse {
2581                                    language: language_clone.clone(),
2582                                    request_id,
2583                                    result: Err("LSP not initialized".to_string()),
2584                                });
2585                            }
2586                        }
2587                        LspCommand::Shutdown => {
2588                            tracing::info!("Processing Shutdown command");
2589                            // Set flag before shutdown to prevent spurious error messages
2590                            shutting_down.store(true, Ordering::SeqCst);
2591                            let _ = state.handle_shutdown().await;
2592                            break;
2593                        }
2594                    }
2595                }
2596                // Handle channel closure
2597                else => {
2598                    tracing::info!("Command channel closed");
2599                    break;
2600                }
2601            }
2602        }
2603
2604        tracing::info!("LSP task exiting for language: {}", self.language);
2605    }
2606}
2607
2608/// Standalone function to read a message from stdout (for reader task)
2609async fn read_message_from_stdout(
2610    stdout: &mut BufReader<ChildStdout>,
2611) -> Result<JsonRpcMessage, String> {
2612    // Read headers
2613    let mut content_length: Option<usize> = None;
2614
2615    loop {
2616        let mut line = String::new();
2617        let bytes_read = stdout
2618            .read_line(&mut line)
2619            .await
2620            .map_err(|e| format!("Failed to read from stdout: {}", e))?;
2621
2622        // EOF detected - LSP server closed stdout
2623        if bytes_read == 0 {
2624            return Err("LSP server closed stdout (EOF)".to_string());
2625        }
2626
2627        if line == "\r\n" {
2628            break;
2629        }
2630
2631        if let Some(len_str) = line.strip_prefix("Content-Length: ") {
2632            content_length = Some(
2633                len_str
2634                    .trim()
2635                    .parse()
2636                    .map_err(|e| format!("Invalid Content-Length: {}", e))?,
2637            );
2638        }
2639    }
2640
2641    let content_length =
2642        content_length.ok_or_else(|| "Missing Content-Length header".to_string())?;
2643
2644    // Read content
2645    let mut content = vec![0u8; content_length];
2646    stdout
2647        .read_exact(&mut content)
2648        .await
2649        .map_err(|e| format!("Failed to read content: {}", e))?;
2650
2651    let json = String::from_utf8(content).map_err(|e| format!("Invalid UTF-8: {}", e))?;
2652
2653    tracing::trace!("Received LSP message: {}", json);
2654
2655    serde_json::from_str(&json).map_err(|e| format!("Failed to deserialize message: {}", e))
2656}
2657
2658/// Standalone function to handle and dispatch messages (for reader task)
2659#[allow(clippy::type_complexity)]
2660async fn handle_message_dispatch(
2661    message: JsonRpcMessage,
2662    pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value, String>>>>>,
2663    async_tx: &std_mpsc::Sender<AsyncMessage>,
2664    language: &str,
2665    server_command: &str,
2666    stdin_writer: &Arc<tokio::sync::Mutex<ChildStdin>>,
2667) -> Result<(), String> {
2668    match message {
2669        JsonRpcMessage::Response(response) => {
2670            tracing::trace!("Received LSP response for request id={}", response.id);
2671            if let Some(tx) = pending.lock().unwrap().remove(&response.id) {
2672                let result = if let Some(error) = response.error {
2673                    tracing::warn!(
2674                        "LSP response error: {} (code {})",
2675                        error.message,
2676                        error.code
2677                    );
2678                    Err(format!(
2679                        "LSP error: {} (code {})",
2680                        error.message, error.code
2681                    ))
2682                } else {
2683                    tracing::trace!("LSP response success for request id={}", response.id);
2684                    // null is a valid result for many LSP methods (e.g., inlay hints with no hints)
2685                    Ok(response.result.unwrap_or(serde_json::Value::Null))
2686                };
2687                let _ = tx.send(result);
2688            } else {
2689                tracing::warn!(
2690                    "Received LSP response for unknown request id={}",
2691                    response.id
2692                );
2693            }
2694        }
2695        JsonRpcMessage::Notification(notification) => {
2696            tracing::trace!("Received LSP notification: {}", notification.method);
2697            handle_notification_dispatch(notification, async_tx, language).await?;
2698        }
2699        JsonRpcMessage::Request(request) => {
2700            // Handle server-to-client requests - MUST respond to avoid timeouts
2701            tracing::trace!("Received request from server: {}", request.method);
2702            let response = match request.method.as_str() {
2703                "window/workDoneProgress/create" => {
2704                    // Server wants to create a progress token - acknowledge it
2705                    tracing::trace!("Acknowledging workDoneProgress/create (id={})", request.id);
2706                    JsonRpcResponse {
2707                        jsonrpc: "2.0".to_string(),
2708                        id: request.id,
2709                        result: Some(Value::Null),
2710                        error: None,
2711                    }
2712                }
2713                "workspace/configuration" => {
2714                    // Return configuration with inlay hints enabled for rust-analyzer
2715                    // The request contains items asking for configuration sections
2716                    // We return an array with one config object per requested item
2717                    tracing::trace!(
2718                        "Responding to workspace/configuration with inlay hints enabled"
2719                    );
2720
2721                    // Parse request params to see how many items are requested
2722                    let num_items = request
2723                        .params
2724                        .as_ref()
2725                        .and_then(|p| p.get("items"))
2726                        .and_then(|items| items.as_array())
2727                        .map(|arr| arr.len())
2728                        .unwrap_or(1);
2729
2730                    // rust-analyzer configuration with inlay hints enabled
2731                    let ra_config = serde_json::json!({
2732                        "inlayHints": {
2733                            "typeHints": {
2734                                "enable": true
2735                            },
2736                            "parameterHints": {
2737                                "enable": true
2738                            },
2739                            "chainingHints": {
2740                                "enable": true
2741                            },
2742                            "closureReturnTypeHints": {
2743                                "enable": "always"
2744                            }
2745                        }
2746                    });
2747
2748                    // Return one config object for each requested item
2749                    let configs: Vec<Value> = (0..num_items).map(|_| ra_config.clone()).collect();
2750
2751                    JsonRpcResponse {
2752                        jsonrpc: "2.0".to_string(),
2753                        id: request.id,
2754                        result: Some(Value::Array(configs)),
2755                        error: None,
2756                    }
2757                }
2758                "client/registerCapability" => {
2759                    // Server wants to register a capability dynamically - acknowledge
2760                    tracing::trace!(
2761                        "Acknowledging client/registerCapability (id={})",
2762                        request.id
2763                    );
2764                    JsonRpcResponse {
2765                        jsonrpc: "2.0".to_string(),
2766                        id: request.id,
2767                        result: Some(Value::Null),
2768                        error: None,
2769                    }
2770                }
2771                _ => {
2772                    // For unknown methods, notify plugins and return null to acknowledge receipt
2773                    tracing::debug!("Server request for plugins: {}", request.method);
2774                    let _ = async_tx.send(AsyncMessage::LspServerRequest {
2775                        language: language.to_string(),
2776                        server_command: server_command.to_string(),
2777                        method: request.method.clone(),
2778                        params: request.params.clone(),
2779                    });
2780                    JsonRpcResponse {
2781                        jsonrpc: "2.0".to_string(),
2782                        id: request.id,
2783                        result: Some(Value::Null),
2784                        error: None,
2785                    }
2786                }
2787            };
2788
2789            // Write response directly to stdin (avoids deadlock when main loop is waiting for LSP response)
2790            let json = serde_json::to_string(&response)
2791                .map_err(|e| format!("Failed to serialize response: {}", e))?;
2792            let message = format!("Content-Length: {}\r\n\r\n{}", json.len(), json);
2793
2794            let mut stdin = stdin_writer.lock().await;
2795            use tokio::io::AsyncWriteExt;
2796            if let Err(e) = stdin.write_all(message.as_bytes()).await {
2797                tracing::error!("Failed to write server response: {}", e);
2798            }
2799            if let Err(e) = stdin.flush().await {
2800                tracing::error!("Failed to flush server response: {}", e);
2801            }
2802            tracing::trace!("Sent response to server request id={}", response.id);
2803        }
2804    }
2805    Ok(())
2806}
2807
2808/// Standalone function to handle notifications (for reader task)
2809async fn handle_notification_dispatch(
2810    notification: JsonRpcNotification,
2811    async_tx: &std_mpsc::Sender<AsyncMessage>,
2812    language: &str,
2813) -> Result<(), String> {
2814    match notification.method.as_str() {
2815        PublishDiagnostics::METHOD => {
2816            if let Some(params) = notification.params {
2817                let params: PublishDiagnosticsParams = serde_json::from_value(params)
2818                    .map_err(|e| format!("Failed to deserialize diagnostics: {}", e))?;
2819
2820                tracing::trace!(
2821                    "Received {} diagnostics for {}",
2822                    params.diagnostics.len(),
2823                    params.uri.as_str()
2824                );
2825
2826                // Send to main loop
2827                let _ = async_tx.send(AsyncMessage::LspDiagnostics {
2828                    uri: params.uri.to_string(),
2829                    diagnostics: params.diagnostics,
2830                });
2831            }
2832        }
2833        "window/showMessage" => {
2834            if let Some(params) = notification.params {
2835                if let Ok(msg) = serde_json::from_value::<serde_json::Map<String, Value>>(params) {
2836                    let message_type_num = msg.get("type").and_then(|v| v.as_i64()).unwrap_or(3);
2837                    let message = msg
2838                        .get("message")
2839                        .and_then(|v| v.as_str())
2840                        .unwrap_or("(no message)")
2841                        .to_string();
2842
2843                    let message_type = match message_type_num {
2844                        1 => LspMessageType::Error,
2845                        2 => LspMessageType::Warning,
2846                        3 => LspMessageType::Info,
2847                        _ => LspMessageType::Log,
2848                    };
2849
2850                    // Log it as well
2851                    match message_type {
2852                        LspMessageType::Error => tracing::error!("LSP ({}): {}", language, message),
2853                        LspMessageType::Warning => {
2854                            tracing::warn!("LSP ({}): {}", language, message)
2855                        }
2856                        LspMessageType::Info => tracing::info!("LSP ({}): {}", language, message),
2857                        LspMessageType::Log => tracing::trace!("LSP ({}): {}", language, message),
2858                    }
2859
2860                    // Send to UI
2861                    let _ = async_tx.send(AsyncMessage::LspWindowMessage {
2862                        language: language.to_string(),
2863                        message_type,
2864                        message,
2865                    });
2866                }
2867            }
2868        }
2869        "window/logMessage" => {
2870            if let Some(params) = notification.params {
2871                if let Ok(msg) = serde_json::from_value::<serde_json::Map<String, Value>>(params) {
2872                    let message_type_num = msg.get("type").and_then(|v| v.as_i64()).unwrap_or(4);
2873                    let message = msg
2874                        .get("message")
2875                        .and_then(|v| v.as_str())
2876                        .unwrap_or("(no message)")
2877                        .to_string();
2878
2879                    let message_type = match message_type_num {
2880                        1 => LspMessageType::Error,
2881                        2 => LspMessageType::Warning,
2882                        3 => LspMessageType::Info,
2883                        _ => LspMessageType::Log,
2884                    };
2885
2886                    // Log it as well
2887                    match message_type {
2888                        LspMessageType::Error => tracing::error!("LSP ({}): {}", language, message),
2889                        LspMessageType::Warning => {
2890                            tracing::warn!("LSP ({}): {}", language, message)
2891                        }
2892                        LspMessageType::Info => tracing::info!("LSP ({}): {}", language, message),
2893                        LspMessageType::Log => tracing::trace!("LSP ({}): {}", language, message),
2894                    }
2895
2896                    // Send to UI
2897                    let _ = async_tx.send(AsyncMessage::LspLogMessage {
2898                        language: language.to_string(),
2899                        message_type,
2900                        message,
2901                    });
2902                }
2903            }
2904        }
2905        "$/progress" => {
2906            if let Some(params) = notification.params {
2907                if let Ok(progress) =
2908                    serde_json::from_value::<serde_json::Map<String, Value>>(params)
2909                {
2910                    let token = progress
2911                        .get("token")
2912                        .and_then(|v| {
2913                            v.as_str()
2914                                .map(|s| s.to_string())
2915                                .or_else(|| v.as_i64().map(|n| n.to_string()))
2916                        })
2917                        .unwrap_or_else(|| "unknown".to_string());
2918
2919                    if let Some(value_obj) = progress.get("value").and_then(|v| v.as_object()) {
2920                        let kind = value_obj.get("kind").and_then(|v| v.as_str());
2921
2922                        let value = match kind {
2923                            Some("begin") => {
2924                                let title = value_obj
2925                                    .get("title")
2926                                    .and_then(|v| v.as_str())
2927                                    .unwrap_or("Working...")
2928                                    .to_string();
2929                                let message = value_obj
2930                                    .get("message")
2931                                    .and_then(|v| v.as_str())
2932                                    .map(|s| s.to_string());
2933                                let percentage = value_obj
2934                                    .get("percentage")
2935                                    .and_then(|v| v.as_u64())
2936                                    .map(|p| p as u32);
2937
2938                                tracing::info!(
2939                                    "LSP ({}) progress begin: {} {:?} {:?}",
2940                                    language,
2941                                    title,
2942                                    message,
2943                                    percentage
2944                                );
2945
2946                                Some(LspProgressValue::Begin {
2947                                    title,
2948                                    message,
2949                                    percentage,
2950                                })
2951                            }
2952                            Some("report") => {
2953                                let message = value_obj
2954                                    .get("message")
2955                                    .and_then(|v| v.as_str())
2956                                    .map(|s| s.to_string());
2957                                let percentage = value_obj
2958                                    .get("percentage")
2959                                    .and_then(|v| v.as_u64())
2960                                    .map(|p| p as u32);
2961
2962                                tracing::trace!(
2963                                    "LSP ({}) progress report: {:?} {:?}",
2964                                    language,
2965                                    message,
2966                                    percentage
2967                                );
2968
2969                                Some(LspProgressValue::Report {
2970                                    message,
2971                                    percentage,
2972                                })
2973                            }
2974                            Some("end") => {
2975                                let message = value_obj
2976                                    .get("message")
2977                                    .and_then(|v| v.as_str())
2978                                    .map(|s| s.to_string());
2979
2980                                tracing::info!("LSP ({}) progress end: {:?}", language, message);
2981
2982                                Some(LspProgressValue::End { message })
2983                            }
2984                            _ => None,
2985                        };
2986
2987                        if let Some(value) = value {
2988                            let _ = async_tx.send(AsyncMessage::LspProgress {
2989                                language: language.to_string(),
2990                                token,
2991                                value,
2992                            });
2993                        }
2994                    }
2995                }
2996            }
2997        }
2998        "experimental/serverStatus" => {
2999            // rust-analyzer specific: server status notification
3000            // When quiescent is true, the project is fully loaded
3001            if let Some(params) = notification.params {
3002                if let Ok(status) = serde_json::from_value::<serde_json::Map<String, Value>>(params)
3003                {
3004                    let quiescent = status
3005                        .get("quiescent")
3006                        .and_then(|v| v.as_bool())
3007                        .unwrap_or(false);
3008
3009                    tracing::info!("LSP ({}) server status: quiescent={}", language, quiescent);
3010
3011                    if quiescent {
3012                        // Project is fully loaded - notify editor to re-request inlay hints
3013                        let _ = async_tx.send(AsyncMessage::LspServerQuiescent {
3014                            language: language.to_string(),
3015                        });
3016                    }
3017                }
3018            }
3019        }
3020        _ => {
3021            tracing::debug!("Unhandled notification: {}", notification.method);
3022        }
3023    }
3024
3025    Ok(())
3026}
3027
3028/// Counter for generating unique LSP handle IDs
3029static NEXT_HANDLE_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
3030
3031/// Synchronous handle to an async LSP task
3032pub struct LspHandle {
3033    /// Unique identifier for this handle instance
3034    id: u64,
3035
3036    /// Channel for sending commands to the task
3037    command_tx: mpsc::Sender<LspCommand>,
3038
3039    /// Client state
3040    state: Arc<Mutex<LspClientState>>,
3041
3042    /// Runtime handle for blocking operations
3043    runtime: tokio::runtime::Handle,
3044}
3045
3046impl LspHandle {
3047    /// Spawn a new LSP server in an async task
3048    pub fn spawn(
3049        runtime: &tokio::runtime::Handle,
3050        command: &str,
3051        args: &[String],
3052        language: String,
3053        async_bridge: &AsyncBridge,
3054        process_limits: ProcessLimits,
3055    ) -> Result<Self, String> {
3056        let (command_tx, command_rx) = mpsc::channel(100); // Buffer up to 100 commands
3057        let async_tx = async_bridge.sender();
3058        let language_clone = language.clone();
3059        let command = command.to_string();
3060        let args = args.to_vec();
3061        let state = Arc::new(Mutex::new(LspClientState::Starting));
3062
3063        // Create stderr log path in XDG state directory
3064        let stderr_log_path = crate::services::log_dirs::lsp_log_path(&language);
3065
3066        // Send starting status
3067        let _ = async_tx.send(AsyncMessage::LspStatusUpdate {
3068            language: language.clone(),
3069            status: LspServerStatus::Starting,
3070            message: None,
3071        });
3072
3073        let state_clone = state.clone();
3074        let stderr_log_path_clone = stderr_log_path.clone();
3075        runtime.spawn(async move {
3076            match LspTask::spawn(
3077                &command,
3078                &args,
3079                language_clone.clone(),
3080                async_tx.clone(),
3081                &process_limits,
3082                stderr_log_path_clone.clone(),
3083            )
3084            .await
3085            {
3086                Ok(task) => {
3087                    task.run(command_rx).await;
3088                }
3089                Err(e) => {
3090                    tracing::error!("Failed to spawn LSP task: {}", e);
3091
3092                    // Transition to error state
3093                    if let Ok(mut s) = state_clone.lock() {
3094                        let _ = s.transition_to(LspClientState::Error);
3095                    }
3096
3097                    let _ = async_tx.send(AsyncMessage::LspStatusUpdate {
3098                        language: language_clone.clone(),
3099                        status: LspServerStatus::Error,
3100                        message: None,
3101                    });
3102                    let _ = async_tx.send(AsyncMessage::LspError {
3103                        language: language_clone,
3104                        error: e,
3105                        stderr_log_path: Some(stderr_log_path_clone),
3106                    });
3107                }
3108            }
3109        });
3110
3111        let id = NEXT_HANDLE_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3112
3113        Ok(Self {
3114            id,
3115            command_tx,
3116            state,
3117            runtime: runtime.clone(),
3118        })
3119    }
3120
3121    /// Get the unique ID for this handle instance
3122    pub fn id(&self) -> u64 {
3123        self.id
3124    }
3125
3126    /// Initialize the server (non-blocking)
3127    ///
3128    /// This sends the initialize request asynchronously. The server will be ready
3129    /// when `is_initialized()` returns true. Other methods that require initialization
3130    /// will fail gracefully until then.
3131    ///
3132    /// The `initialization_options` are passed to the server during initialization.
3133    /// Some servers like Deno require specific options (e.g., `{"enable": true}`).
3134    pub fn initialize(
3135        &self,
3136        root_uri: Option<Uri>,
3137        initialization_options: Option<Value>,
3138    ) -> Result<(), String> {
3139        // Validate state transition
3140        {
3141            let mut state = self.state.lock().unwrap();
3142            if !state.can_initialize() {
3143                return Err(format!(
3144                    "Cannot initialize: client is in state {:?}",
3145                    *state
3146                ));
3147            }
3148            // Transition to Initializing
3149            state.transition_to(LspClientState::Initializing)?;
3150        }
3151
3152        let state = self.state.clone();
3153
3154        // Create a channel for the response, but don't wait for it
3155        let (tx, rx) = oneshot::channel();
3156
3157        self.command_tx
3158            .try_send(LspCommand::Initialize {
3159                root_uri,
3160                initialization_options,
3161                response: tx,
3162            })
3163            .map_err(|_| "Failed to send initialize command".to_string())?;
3164
3165        // Spawn a task to wait for the response and update the state
3166        let runtime = self.runtime.clone();
3167        runtime.spawn(async move {
3168            match tokio::time::timeout(std::time::Duration::from_secs(10), rx).await {
3169                Ok(Ok(Ok(_))) => {
3170                    // Successfully initialized
3171                    if let Ok(mut s) = state.lock() {
3172                        let _ = s.transition_to(LspClientState::Running);
3173                    }
3174                    tracing::info!("LSP initialization completed successfully");
3175                }
3176                Ok(Ok(Err(e))) => {
3177                    tracing::error!("LSP initialization failed: {}", e);
3178                    if let Ok(mut s) = state.lock() {
3179                        let _ = s.transition_to(LspClientState::Error);
3180                    }
3181                }
3182                Ok(Err(_)) => {
3183                    tracing::error!("LSP initialization response channel closed");
3184                    if let Ok(mut s) = state.lock() {
3185                        let _ = s.transition_to(LspClientState::Error);
3186                    }
3187                }
3188                Err(_) => {
3189                    tracing::error!("LSP initialization timed out after 10 seconds");
3190                    if let Ok(mut s) = state.lock() {
3191                        let _ = s.transition_to(LspClientState::Error);
3192                    }
3193                }
3194            }
3195        });
3196
3197        Ok(())
3198    }
3199
3200    /// Check if the server is initialized
3201    pub fn is_initialized(&self) -> bool {
3202        self.state.lock().unwrap().can_send_requests()
3203    }
3204
3205    /// Get the current client state
3206    pub fn state(&self) -> LspClientState {
3207        *self.state.lock().unwrap()
3208    }
3209
3210    /// Notify document opened
3211    pub fn did_open(&self, uri: Uri, text: String, language_id: String) -> Result<(), String> {
3212        // Send command to LspTask which will queue it if not initialized yet
3213        self.command_tx
3214            .try_send(LspCommand::DidOpen {
3215                uri,
3216                text,
3217                language_id,
3218            })
3219            .map_err(|_| "Failed to send did_open command".to_string())
3220    }
3221
3222    /// Notify document changed
3223    pub fn did_change(
3224        &self,
3225        uri: Uri,
3226        content_changes: Vec<TextDocumentContentChangeEvent>,
3227    ) -> Result<(), String> {
3228        // Send command to LspTask which will queue it if not initialized yet
3229        self.command_tx
3230            .try_send(LspCommand::DidChange {
3231                uri,
3232                content_changes,
3233            })
3234            .map_err(|_| "Failed to send did_change command".to_string())
3235    }
3236
3237    /// Send didSave notification
3238    pub fn did_save(&self, uri: Uri, text: Option<String>) -> Result<(), String> {
3239        self.command_tx
3240            .try_send(LspCommand::DidSave { uri, text })
3241            .map_err(|_| "Failed to send did_save command".to_string())
3242    }
3243
3244    /// Request completion at position
3245    pub fn completion(
3246        &self,
3247        request_id: u64,
3248        uri: Uri,
3249        line: u32,
3250        character: u32,
3251    ) -> Result<(), String> {
3252        self.command_tx
3253            .try_send(LspCommand::Completion {
3254                request_id,
3255                uri,
3256                line,
3257                character,
3258            })
3259            .map_err(|_| "Failed to send completion command".to_string())
3260    }
3261
3262    /// Request go-to-definition
3263    pub fn goto_definition(
3264        &self,
3265        request_id: u64,
3266        uri: Uri,
3267        line: u32,
3268        character: u32,
3269    ) -> Result<(), String> {
3270        self.command_tx
3271            .try_send(LspCommand::GotoDefinition {
3272                request_id,
3273                uri,
3274                line,
3275                character,
3276            })
3277            .map_err(|_| "Failed to send goto_definition command".to_string())
3278    }
3279
3280    /// Request rename
3281    pub fn rename(
3282        &self,
3283        request_id: u64,
3284        uri: Uri,
3285        line: u32,
3286        character: u32,
3287        new_name: String,
3288    ) -> Result<(), String> {
3289        self.command_tx
3290            .try_send(LspCommand::Rename {
3291                request_id,
3292                uri,
3293                line,
3294                character,
3295                new_name,
3296            })
3297            .map_err(|_| "Failed to send rename command".to_string())
3298    }
3299
3300    /// Request hover documentation
3301    pub fn hover(
3302        &self,
3303        request_id: u64,
3304        uri: Uri,
3305        line: u32,
3306        character: u32,
3307    ) -> Result<(), String> {
3308        self.command_tx
3309            .try_send(LspCommand::Hover {
3310                request_id,
3311                uri,
3312                line,
3313                character,
3314            })
3315            .map_err(|_| "Failed to send hover command".to_string())
3316    }
3317
3318    /// Request find references
3319    pub fn references(
3320        &self,
3321        request_id: u64,
3322        uri: Uri,
3323        line: u32,
3324        character: u32,
3325    ) -> Result<(), String> {
3326        self.command_tx
3327            .try_send(LspCommand::References {
3328                request_id,
3329                uri,
3330                line,
3331                character,
3332            })
3333            .map_err(|_| "Failed to send references command".to_string())
3334    }
3335
3336    /// Request signature help
3337    pub fn signature_help(
3338        &self,
3339        request_id: u64,
3340        uri: Uri,
3341        line: u32,
3342        character: u32,
3343    ) -> Result<(), String> {
3344        self.command_tx
3345            .try_send(LspCommand::SignatureHelp {
3346                request_id,
3347                uri,
3348                line,
3349                character,
3350            })
3351            .map_err(|_| "Failed to send signature_help command".to_string())
3352    }
3353
3354    /// Request code actions
3355    #[allow(clippy::too_many_arguments)]
3356    pub fn code_actions(
3357        &self,
3358        request_id: u64,
3359        uri: Uri,
3360        start_line: u32,
3361        start_char: u32,
3362        end_line: u32,
3363        end_char: u32,
3364        diagnostics: Vec<lsp_types::Diagnostic>,
3365    ) -> Result<(), String> {
3366        self.command_tx
3367            .try_send(LspCommand::CodeActions {
3368                request_id,
3369                uri,
3370                start_line,
3371                start_char,
3372                end_line,
3373                end_char,
3374                diagnostics,
3375            })
3376            .map_err(|_| "Failed to send code_actions command".to_string())
3377    }
3378
3379    /// Request document diagnostics (pull model)
3380    ///
3381    /// This sends a textDocument/diagnostic request to fetch diagnostics on demand.
3382    /// Use `previous_result_id` for incremental updates (server may return unchanged).
3383    pub fn document_diagnostic(
3384        &self,
3385        request_id: u64,
3386        uri: Uri,
3387        previous_result_id: Option<String>,
3388    ) -> Result<(), String> {
3389        self.command_tx
3390            .try_send(LspCommand::DocumentDiagnostic {
3391                request_id,
3392                uri,
3393                previous_result_id,
3394            })
3395            .map_err(|_| "Failed to send document_diagnostic command".to_string())
3396    }
3397
3398    /// Request inlay hints for a range (LSP 3.17+)
3399    ///
3400    /// Inlay hints are virtual text annotations displayed inline (e.g., type hints, parameter names).
3401    pub fn inlay_hints(
3402        &self,
3403        request_id: u64,
3404        uri: Uri,
3405        start_line: u32,
3406        start_char: u32,
3407        end_line: u32,
3408        end_char: u32,
3409    ) -> Result<(), String> {
3410        self.command_tx
3411            .try_send(LspCommand::InlayHints {
3412                request_id,
3413                uri,
3414                start_line,
3415                start_char,
3416                end_line,
3417                end_char,
3418            })
3419            .map_err(|_| "Failed to send inlay_hints command".to_string())
3420    }
3421
3422    /// Request semantic tokens for an entire document
3423    pub fn semantic_tokens_full(&self, request_id: u64, uri: Uri) -> Result<(), String> {
3424        self.command_tx
3425            .try_send(LspCommand::SemanticTokensFull { request_id, uri })
3426            .map_err(|_| "Failed to send semantic_tokens command".to_string())
3427    }
3428
3429    /// Request semantic tokens delta for an entire document
3430    pub fn semantic_tokens_full_delta(
3431        &self,
3432        request_id: u64,
3433        uri: Uri,
3434        previous_result_id: String,
3435    ) -> Result<(), String> {
3436        self.command_tx
3437            .try_send(LspCommand::SemanticTokensFullDelta {
3438                request_id,
3439                uri,
3440                previous_result_id,
3441            })
3442            .map_err(|_| "Failed to send semantic_tokens delta command".to_string())
3443    }
3444
3445    /// Request semantic tokens for a range
3446    pub fn semantic_tokens_range(
3447        &self,
3448        request_id: u64,
3449        uri: Uri,
3450        range: lsp_types::Range,
3451    ) -> Result<(), String> {
3452        self.command_tx
3453            .try_send(LspCommand::SemanticTokensRange {
3454                request_id,
3455                uri,
3456                range,
3457            })
3458            .map_err(|_| "Failed to send semantic_tokens_range command".to_string())
3459    }
3460
3461    /// Cancel a pending request by its editor request_id
3462    ///
3463    /// This sends a $/cancelRequest notification to the LSP server.
3464    /// If the request has already completed or doesn't exist, this is a no-op.
3465    pub fn cancel_request(&self, request_id: u64) -> Result<(), String> {
3466        self.command_tx
3467            .try_send(LspCommand::CancelRequest { request_id })
3468            .map_err(|_| "Failed to send cancel_request command".to_string())
3469    }
3470
3471    /// Send a custom LSP request initiated by a plugin
3472    pub fn send_plugin_request(
3473        &self,
3474        request_id: u64,
3475        method: String,
3476        params: Option<Value>,
3477    ) -> Result<(), String> {
3478        tracing::trace!(
3479            "LspHandle sending plugin request {}: method={}",
3480            request_id,
3481            method
3482        );
3483        match self.command_tx.try_send(LspCommand::PluginRequest {
3484            request_id,
3485            method,
3486            params,
3487        }) {
3488            Ok(()) => {
3489                tracing::trace!(
3490                    "LspHandle enqueued plugin request {} successfully",
3491                    request_id
3492                );
3493                Ok(())
3494            }
3495            Err(e) => {
3496                tracing::error!("Failed to enqueue plugin request {}: {}", request_id, e);
3497                Err("Failed to send plugin LSP request".to_string())
3498            }
3499        }
3500    }
3501
3502    /// Shutdown the server
3503    pub fn shutdown(&self) -> Result<(), String> {
3504        // Transition to Stopping state
3505        {
3506            let mut state = self.state.lock().unwrap();
3507            if let Err(e) = state.transition_to(LspClientState::Stopping) {
3508                tracing::warn!("State transition warning during shutdown: {}", e);
3509                // Don't fail shutdown due to state transition errors
3510            }
3511        }
3512
3513        self.command_tx
3514            .try_send(LspCommand::Shutdown)
3515            .map_err(|_| "Failed to send shutdown command".to_string())?;
3516
3517        // Transition to Stopped state
3518        // Note: This happens optimistically. The actual shutdown might take time.
3519        {
3520            let mut state = self.state.lock().unwrap();
3521            let _ = state.transition_to(LspClientState::Stopped);
3522        }
3523
3524        Ok(())
3525    }
3526}
3527
3528impl Drop for LspHandle {
3529    fn drop(&mut self) {
3530        // Best-effort shutdown on drop
3531        // Use try_send instead of blocking_send to avoid panicking if:
3532        // 1. The tokio runtime is shut down
3533        // 2. The channel is full or closed
3534        // 3. We're dropping during a panic
3535        let _ = self.command_tx.try_send(LspCommand::Shutdown);
3536
3537        // Update state to Stopped
3538        if let Ok(mut state) = self.state.lock() {
3539            let _ = state.transition_to(LspClientState::Stopped);
3540        }
3541    }
3542}
3543
3544#[cfg(test)]
3545mod tests {
3546    use super::*;
3547
3548    #[test]
3549    fn test_json_rpc_request_serialization() {
3550        let request = JsonRpcRequest {
3551            jsonrpc: "2.0".to_string(),
3552            id: 1,
3553            method: "initialize".to_string(),
3554            params: Some(serde_json::json!({"rootUri": "file:///test"})),
3555        };
3556
3557        let json = serde_json::to_string(&request).unwrap();
3558        assert!(json.contains("\"jsonrpc\":\"2.0\""));
3559        assert!(json.contains("\"id\":1"));
3560        assert!(json.contains("\"method\":\"initialize\""));
3561        assert!(json.contains("\"rootUri\":\"file:///test\""));
3562    }
3563
3564    #[test]
3565    fn test_json_rpc_response_serialization() {
3566        let response = JsonRpcResponse {
3567            jsonrpc: "2.0".to_string(),
3568            id: 1,
3569            result: Some(serde_json::json!({"success": true})),
3570            error: None,
3571        };
3572
3573        let json = serde_json::to_string(&response).unwrap();
3574        assert!(json.contains("\"jsonrpc\":\"2.0\""));
3575        assert!(json.contains("\"id\":1"));
3576        assert!(json.contains("\"success\":true"));
3577        assert!(!json.contains("\"error\""));
3578    }
3579
3580    #[test]
3581    fn test_json_rpc_error_response() {
3582        let response = JsonRpcResponse {
3583            jsonrpc: "2.0".to_string(),
3584            id: 1,
3585            result: None,
3586            error: Some(JsonRpcError {
3587                code: -32600,
3588                message: "Invalid request".to_string(),
3589                data: None,
3590            }),
3591        };
3592
3593        let json = serde_json::to_string(&response).unwrap();
3594        assert!(json.contains("\"error\""));
3595        assert!(json.contains("\"code\":-32600"));
3596        assert!(json.contains("\"message\":\"Invalid request\""));
3597    }
3598
3599    #[test]
3600    fn test_json_rpc_notification_serialization() {
3601        let notification = JsonRpcNotification {
3602            jsonrpc: "2.0".to_string(),
3603            method: "textDocument/didOpen".to_string(),
3604            params: Some(serde_json::json!({"uri": "file:///test.rs"})),
3605        };
3606
3607        let json = serde_json::to_string(&notification).unwrap();
3608        assert!(json.contains("\"jsonrpc\":\"2.0\""));
3609        assert!(json.contains("\"method\":\"textDocument/didOpen\""));
3610        assert!(json.contains("\"uri\":\"file:///test.rs\""));
3611        assert!(!json.contains("\"id\"")); // Notifications have no ID
3612    }
3613
3614    #[test]
3615    fn test_json_rpc_message_deserialization_request() {
3616        let json =
3617            r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"rootUri":"file:///test"}}"#;
3618        let message: JsonRpcMessage = serde_json::from_str(json).unwrap();
3619
3620        match message {
3621            JsonRpcMessage::Request(request) => {
3622                assert_eq!(request.jsonrpc, "2.0");
3623                assert_eq!(request.id, 1);
3624                assert_eq!(request.method, "initialize");
3625                assert!(request.params.is_some());
3626            }
3627            _ => panic!("Expected Request"),
3628        }
3629    }
3630
3631    #[test]
3632    fn test_json_rpc_message_deserialization_response() {
3633        let json = r#"{"jsonrpc":"2.0","id":1,"result":{"success":true}}"#;
3634        let message: JsonRpcMessage = serde_json::from_str(json).unwrap();
3635
3636        match message {
3637            JsonRpcMessage::Response(response) => {
3638                assert_eq!(response.jsonrpc, "2.0");
3639                assert_eq!(response.id, 1);
3640                assert!(response.result.is_some());
3641                assert!(response.error.is_none());
3642            }
3643            _ => panic!("Expected Response"),
3644        }
3645    }
3646
3647    #[test]
3648    fn test_json_rpc_message_deserialization_notification() {
3649        let json = r#"{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"uri":"file:///test.rs"}}"#;
3650        let message: JsonRpcMessage = serde_json::from_str(json).unwrap();
3651
3652        match message {
3653            JsonRpcMessage::Notification(notification) => {
3654                assert_eq!(notification.jsonrpc, "2.0");
3655                assert_eq!(notification.method, "textDocument/didOpen");
3656                assert!(notification.params.is_some());
3657            }
3658            _ => panic!("Expected Notification"),
3659        }
3660    }
3661
3662    #[test]
3663    fn test_json_rpc_error_deserialization() {
3664        let json =
3665            r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"Invalid request"}}"#;
3666        let message: JsonRpcMessage = serde_json::from_str(json).unwrap();
3667
3668        match message {
3669            JsonRpcMessage::Response(response) => {
3670                assert_eq!(response.jsonrpc, "2.0");
3671                assert_eq!(response.id, 1);
3672                assert!(response.result.is_none());
3673                assert!(response.error.is_some());
3674                let error = response.error.unwrap();
3675                assert_eq!(error.code, -32600);
3676                assert_eq!(error.message, "Invalid request");
3677            }
3678            _ => panic!("Expected Response with error"),
3679        }
3680    }
3681
3682    #[tokio::test]
3683    async fn test_lsp_handle_spawn_and_drop() {
3684        // This test spawns a mock LSP server (cat command that echoes input)
3685        // and tests the spawn/drop lifecycle
3686        let runtime = tokio::runtime::Handle::current();
3687        let async_bridge = AsyncBridge::new();
3688
3689        // Use 'cat' as a mock LSP server (it will just echo stdin to stdout)
3690        // This will fail to initialize but allows us to test the spawn mechanism
3691        let result = LspHandle::spawn(
3692            &runtime,
3693            "cat",
3694            &[],
3695            "test".to_string(),
3696            &async_bridge,
3697            ProcessLimits::unlimited(),
3698        );
3699
3700        // Should succeed in spawning
3701        assert!(result.is_ok());
3702
3703        let handle = result.unwrap();
3704
3705        // Let handle drop (which calls shutdown via Drop impl)
3706        drop(handle);
3707
3708        // Give task time to receive shutdown and exit
3709        tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
3710    }
3711
3712    #[tokio::test]
3713    async fn test_lsp_handle_did_open_queues_before_initialization() {
3714        let runtime = tokio::runtime::Handle::current();
3715        let async_bridge = AsyncBridge::new();
3716
3717        let handle = LspHandle::spawn(
3718            &runtime,
3719            "cat",
3720            &[],
3721            "test".to_string(),
3722            &async_bridge,
3723            ProcessLimits::unlimited(),
3724        )
3725        .unwrap();
3726
3727        // did_open now succeeds and queues the command for when server is initialized
3728        let result = handle.did_open(
3729            "file:///test.rs".parse().unwrap(),
3730            "fn main() {}".to_string(),
3731            "rust".to_string(),
3732        );
3733
3734        // Should succeed (command is queued)
3735        assert!(result.is_ok());
3736    }
3737
3738    #[tokio::test]
3739    async fn test_lsp_handle_did_change_queues_before_initialization() {
3740        let runtime = tokio::runtime::Handle::current();
3741        let async_bridge = AsyncBridge::new();
3742
3743        let handle = LspHandle::spawn(
3744            &runtime,
3745            "cat",
3746            &[],
3747            "test".to_string(),
3748            &async_bridge,
3749            ProcessLimits::unlimited(),
3750        )
3751        .unwrap();
3752
3753        // Test incremental sync: insert "fn main() {}" at position (0, 0)
3754        let result = handle.did_change(
3755            "file:///test.rs".parse().unwrap(),
3756            vec![TextDocumentContentChangeEvent {
3757                range: Some(lsp_types::Range::new(
3758                    lsp_types::Position::new(0, 0),
3759                    lsp_types::Position::new(0, 0),
3760                )),
3761                range_length: None,
3762                text: "fn main() {}".to_string(),
3763            }],
3764        );
3765
3766        // Should succeed (command is queued)
3767        assert!(result.is_ok());
3768    }
3769
3770    #[tokio::test]
3771    async fn test_lsp_handle_incremental_change_with_range() {
3772        let runtime = tokio::runtime::Handle::current();
3773        let async_bridge = AsyncBridge::new();
3774
3775        let handle = LspHandle::spawn(
3776            &runtime,
3777            "cat",
3778            &[],
3779            "test".to_string(),
3780            &async_bridge,
3781            ProcessLimits::unlimited(),
3782        )
3783        .unwrap();
3784
3785        // Test incremental delete: remove text from (0, 3) to (0, 7)
3786        let result = handle.did_change(
3787            "file:///test.rs".parse().unwrap(),
3788            vec![TextDocumentContentChangeEvent {
3789                range: Some(lsp_types::Range::new(
3790                    lsp_types::Position::new(0, 3),
3791                    lsp_types::Position::new(0, 7),
3792                )),
3793                range_length: None,
3794                text: String::new(), // Empty string for deletion
3795            }],
3796        );
3797
3798        // Should succeed (command is queued)
3799        assert!(result.is_ok());
3800    }
3801
3802    #[tokio::test]
3803    async fn test_lsp_handle_spawn_invalid_command() {
3804        let runtime = tokio::runtime::Handle::current();
3805        let async_bridge = AsyncBridge::new();
3806
3807        // Try to spawn with an invalid command
3808        let result = LspHandle::spawn(
3809            &runtime,
3810            "this-command-does-not-exist-12345",
3811            &[],
3812            "test".to_string(),
3813            &async_bridge,
3814            ProcessLimits::unlimited(),
3815        );
3816
3817        // Should succeed in creating handle (error happens asynchronously)
3818        // The error will be sent to async_bridge
3819        assert!(result.is_ok());
3820
3821        // Give the task time to fail
3822        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
3823
3824        // Check that we received an error message
3825        let messages = async_bridge.try_recv_all();
3826        assert!(!messages.is_empty());
3827
3828        let has_error = messages
3829            .iter()
3830            .any(|msg| matches!(msg, AsyncMessage::LspError { .. }));
3831        assert!(has_error, "Expected LspError message");
3832    }
3833
3834    #[test]
3835    fn test_lsp_handle_shutdown_from_sync_context() {
3836        // Test shutdown from a synchronous context (requires spawning a separate thread)
3837        // This simulates how shutdown is called from the main editor loop
3838        std::thread::spawn(|| {
3839            // Create a tokio runtime for this thread
3840            let rt = tokio::runtime::Runtime::new().unwrap();
3841            let async_bridge = AsyncBridge::new();
3842
3843            let handle = rt.block_on(async {
3844                let runtime = tokio::runtime::Handle::current();
3845                LspHandle::spawn(
3846                    &runtime,
3847                    "cat",
3848                    &[],
3849                    "test".to_string(),
3850                    &async_bridge,
3851                    ProcessLimits::unlimited(),
3852                )
3853                .unwrap()
3854            });
3855
3856            // This should succeed from a non-async context
3857            assert!(handle.shutdown().is_ok());
3858
3859            // Give task time to exit
3860            std::thread::sleep(std::time::Duration::from_millis(50));
3861        })
3862        .join()
3863        .unwrap();
3864    }
3865
3866    #[test]
3867    fn test_lsp_command_debug_format() {
3868        // Test that LspCommand has Debug implementation
3869        let cmd = LspCommand::Shutdown;
3870        let debug_str = format!("{:?}", cmd);
3871        assert!(debug_str.contains("Shutdown"));
3872    }
3873
3874    #[test]
3875    fn test_lsp_client_state_can_initialize_from_starting() {
3876        // This test verifies that the state machine allows initialization from the Starting state.
3877        // This is critical because LspHandle::spawn() sets state to Starting, and then
3878        // get_or_spawn() immediately calls handle.initialize(). Without this fix,
3879        // initialization would fail with "Cannot initialize: client is in state Starting".
3880
3881        let state = LspClientState::Starting;
3882
3883        // The fix: Starting state should allow initialization
3884        assert!(
3885            state.can_initialize(),
3886            "Starting state must allow initialization to avoid race condition"
3887        );
3888
3889        // Verify the full initialization flow works
3890        let mut state = LspClientState::Starting;
3891
3892        // Should be able to transition to Initializing
3893        assert!(state.can_transition_to(LspClientState::Initializing));
3894        assert!(state.transition_to(LspClientState::Initializing).is_ok());
3895
3896        // And then to Running
3897        assert!(state.can_transition_to(LspClientState::Running));
3898        assert!(state.transition_to(LspClientState::Running).is_ok());
3899    }
3900
3901    #[tokio::test]
3902    async fn test_lsp_handle_initialize_from_starting_state() {
3903        // This test reproduces the bug where initialize() would fail because
3904        // the handle's state is Starting (set by spawn()) but can_initialize()
3905        // only allowed Initial or Stopped states.
3906        //
3907        // The bug manifested as:
3908        // ERROR: Failed to send initialize command for rust: Cannot initialize: client is in state Starting
3909
3910        let runtime = tokio::runtime::Handle::current();
3911        let async_bridge = AsyncBridge::new();
3912
3913        // Spawn creates the handle with state = Starting
3914        let handle = LspHandle::spawn(
3915            &runtime,
3916            "cat", // Simple command that will exit immediately
3917            &[],
3918            "test".to_string(),
3919            &async_bridge,
3920            ProcessLimits::unlimited(),
3921        )
3922        .unwrap();
3923
3924        // Immediately call initialize - this is what get_or_spawn() does
3925        // Before the fix, this would fail with "Cannot initialize: client is in state Starting"
3926        let result = handle.initialize(None, None);
3927
3928        assert!(
3929            result.is_ok(),
3930            "initialize() must succeed from Starting state. Got error: {:?}",
3931            result.err()
3932        );
3933    }
3934
3935    #[tokio::test]
3936    async fn test_lsp_state_machine_race_condition_fix() {
3937        // Integration test that simulates the exact flow that was broken:
3938        // 1. LspManager::get_or_spawn() calls LspHandle::spawn()
3939        // 2. spawn() sets state to Starting and spawns async task
3940        // 3. get_or_spawn() immediately calls handle.initialize()
3941        // 4. initialize() should succeed even though state is Starting
3942
3943        let runtime = tokio::runtime::Handle::current();
3944        let async_bridge = AsyncBridge::new();
3945
3946        // Create a simple fake LSP server script that responds to initialize
3947        let fake_lsp_script = r#"
3948            read -r line  # Read Content-Length header
3949            read -r empty # Read empty line
3950            read -r json  # Read JSON body
3951
3952            # Send a valid initialize response
3953            response='{"jsonrpc":"2.0","id":1,"result":{"capabilities":{}}}'
3954            echo "Content-Length: ${#response}"
3955            echo ""
3956            echo -n "$response"
3957
3958            # Keep running to avoid EOF
3959            sleep 10
3960        "#;
3961
3962        // Spawn with bash to execute the fake LSP
3963        let handle = LspHandle::spawn(
3964            &runtime,
3965            "bash",
3966            &["-c".to_string(), fake_lsp_script.to_string()],
3967            "fake".to_string(),
3968            &async_bridge,
3969            ProcessLimits::unlimited(),
3970        )
3971        .unwrap();
3972
3973        // This is the critical test: initialize must succeed from Starting state
3974        let init_result = handle.initialize(None, None);
3975        assert!(
3976            init_result.is_ok(),
3977            "initialize() failed from Starting state: {:?}",
3978            init_result.err()
3979        );
3980
3981        // Give the async task time to process
3982        tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
3983
3984        // Check that we received status update messages
3985        let messages = async_bridge.try_recv_all();
3986        let has_status_update = messages
3987            .iter()
3988            .any(|msg| matches!(msg, AsyncMessage::LspStatusUpdate { .. }));
3989
3990        assert!(
3991            has_status_update,
3992            "Expected status update messages from LSP initialization"
3993        );
3994
3995        // Cleanup
3996        let _ = handle.shutdown();
3997    }
3998}