async_inspect/lsp/
mod.rs

1//! Language Server Protocol (LSP) integration for async-inspect
2//!
3//! This module provides a Language Server Protocol server that enables async-inspect
4//! functionality in any LSP-compatible editor (vim, emacs, Sublime Text, etc.).
5//!
6//! # Features
7//!
8//! - **Code Actions**: Quick-fix suggestions for adding async-inspect instrumentation
9//! - **Diagnostics**: Warnings for potential async issues and performance problems
10//! - **Hover Information**: Display task statistics and performance metrics
11//! - **Completion**: Auto-complete for async-inspect methods
12//!
13//! # Example
14//!
15//! ```no_run
16//! use async_inspect::lsp::AsyncInspectLanguageServer;
17//!
18//! #[tokio::main]
19//! async fn main() {
20//!     let (service, socket) = tower_lsp::LspService::new(|client| {
21//!         AsyncInspectLanguageServer::new(client)
22//!     });
23//!
24//!     tower_lsp::Server::new(tokio::io::stdin(), tokio::io::stdout(), socket)
25//!         .serve(service)
26//!         .await;
27//! }
28//! ```
29
30#[cfg(feature = "lsp")]
31use crate::inspector::Inspector;
32
33#[cfg(feature = "lsp")]
34#[allow(unused_imports)]
35use serde::{Deserialize, Serialize};
36
37#[cfg(feature = "lsp")]
38use tower_lsp::jsonrpc::Result;
39
40#[cfg(feature = "lsp")]
41use tower_lsp::lsp_types::{
42    ClientCapabilities, CodeAction, CodeActionKind, CodeActionOrCommand, CodeActionParams,
43    CodeActionProviderCapability, CodeActionResponse, CompletionItem, CompletionItemKind,
44    CompletionOptions, CompletionParams, CompletionResponse, Diagnostic, DiagnosticSeverity,
45    DidChangeTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams,
46    Documentation, Hover, HoverContents, HoverParams, HoverProviderCapability, InitializeParams,
47    InitializeResult, InitializedParams, InsertTextFormat, MarkupContent, MarkupKind, MessageType,
48    NumberOrString, Position, Range, ServerCapabilities, ServerInfo, TextDocumentSyncCapability,
49    TextDocumentSyncKind, TextEdit, Url, WorkspaceEdit,
50};
51
52#[cfg(feature = "lsp")]
53use tower_lsp::{Client, LanguageServer};
54
55#[cfg(feature = "lsp")]
56use std::sync::Arc;
57
58#[cfg(feature = "lsp")]
59use parking_lot::RwLock;
60
61/// Language server for async-inspect
62#[cfg(feature = "lsp")]
63pub struct AsyncInspectLanguageServer {
64    /// LSP client for sending notifications
65    client: Client,
66
67    /// Inspector instance
68    inspector: &'static Inspector,
69
70    /// Server state
71    state: Arc<RwLock<ServerState>>,
72}
73
74#[cfg(feature = "lsp")]
75struct ServerState {
76    /// Workspace root URI
77    workspace_root: Option<Url>,
78
79    /// Client capabilities
80    client_capabilities: Option<ClientCapabilities>,
81}
82
83#[cfg(feature = "lsp")]
84impl AsyncInspectLanguageServer {
85    /// Create a new language server instance
86    #[must_use]
87    pub fn new(client: Client) -> Self {
88        Self {
89            client,
90            inspector: Inspector::global(),
91            state: Arc::new(RwLock::new(ServerState {
92                workspace_root: None,
93                client_capabilities: None,
94            })),
95        }
96    }
97
98    /// Analyze document and provide diagnostics
99    async fn analyze_document(&self, _uri: Url, text: &str) -> Vec<Diagnostic> {
100        let mut diagnostics = Vec::new();
101
102        // Check for untracked async tasks
103        if text.contains("tokio::spawn") && !text.contains("spawn_tracked") {
104            // Find all tokio::spawn occurrences
105            for (line_num, line) in text.lines().enumerate() {
106                if line.contains("tokio::spawn") && !line.contains("spawn_tracked") {
107                    let col = line.find("tokio::spawn").unwrap_or(0);
108
109                    diagnostics.push(Diagnostic {
110                        range: Range {
111                            start: Position {
112                                line: line_num as u32,
113                                character: col as u32,
114                            },
115                            end: Position {
116                                line: line_num as u32,
117                                character: (col + "tokio::spawn".len()) as u32,
118                            },
119                        },
120                        severity: Some(DiagnosticSeverity::HINT),
121                        code: Some(NumberOrString::String("async-inspect-001".to_string())),
122                        source: Some("async-inspect".to_string()),
123                        message: "Consider using spawn_tracked for better async debugging"
124                            .to_string(),
125                        related_information: None,
126                        tags: None,
127                        code_description: None,
128                        data: None,
129                    });
130                }
131            }
132        }
133
134        // Check for missing .inspect() on awaits
135        if text.contains(".await") && text.contains("async fn") {
136            for (line_num, line) in text.lines().enumerate() {
137                if line.contains(".await") && !line.contains(".inspect(") {
138                    let col = line.find(".await").unwrap_or(0);
139
140                    diagnostics.push(Diagnostic {
141                        range: Range {
142                            start: Position {
143                                line: line_num as u32,
144                                character: col as u32,
145                            },
146                            end: Position {
147                                line: line_num as u32,
148                                character: (col + ".await".len()) as u32,
149                            },
150                        },
151                        severity: Some(DiagnosticSeverity::INFORMATION),
152                        code: Some(NumberOrString::String("async-inspect-002".to_string())),
153                        source: Some("async-inspect".to_string()),
154                        message: "Add .inspect() to track this await point".to_string(),
155                        related_information: None,
156                        tags: None,
157                        code_description: None,
158                        data: None,
159                    });
160                }
161            }
162        }
163
164        diagnostics
165    }
166
167    /// Provide code actions for diagnostics
168    fn provide_code_actions(&self, params: &CodeActionParams) -> Vec<CodeActionOrCommand> {
169        let mut actions = Vec::new();
170
171        for diagnostic in &params.context.diagnostics {
172            if let Some(NumberOrString::String(code)) = &diagnostic.code {
173                match code.as_str() {
174                    "async-inspect-001" => {
175                        // Convert tokio::spawn to spawn_tracked
176                        actions.push(CodeActionOrCommand::CodeAction(CodeAction {
177                            title: "Use spawn_tracked instead".to_string(),
178                            kind: Some(CodeActionKind::QUICKFIX),
179                            diagnostics: Some(vec![diagnostic.clone()]),
180                            edit: Some(WorkspaceEdit {
181                                changes: Some(
182                                    [(
183                                        params.text_document.uri.clone(),
184                                        vec![TextEdit {
185                                            range: diagnostic.range,
186                                            new_text: "spawn_tracked".to_string(),
187                                        }],
188                                    )]
189                                    .iter()
190                                    .cloned()
191                                    .collect(),
192                                ),
193                                document_changes: None,
194                                change_annotations: None,
195                            }),
196                            command: None,
197                            is_preferred: Some(true),
198                            disabled: None,
199                            data: None,
200                        }));
201                    }
202                    "async-inspect-002" => {
203                        // Add .inspect() before .await
204                        actions.push(CodeActionOrCommand::CodeAction(CodeAction {
205                            title: "Add .inspect() tracking".to_string(),
206                            kind: Some(CodeActionKind::QUICKFIX),
207                            diagnostics: Some(vec![diagnostic.clone()]),
208                            edit: Some(WorkspaceEdit {
209                                changes: Some(
210                                    [(
211                                        params.text_document.uri.clone(),
212                                        vec![TextEdit {
213                                            range: Range {
214                                                start: diagnostic.range.start,
215                                                end: diagnostic.range.start,
216                                            },
217                                            new_text: ".inspect(\"await_point\")".to_string(),
218                                        }],
219                                    )]
220                                    .iter()
221                                    .cloned()
222                                    .collect(),
223                                ),
224                                document_changes: None,
225                                change_annotations: None,
226                            }),
227                            command: None,
228                            is_preferred: Some(true),
229                            disabled: None,
230                            data: None,
231                        }));
232                    }
233                    _ => {}
234                }
235            }
236        }
237
238        actions
239    }
240
241    /// Provide hover information
242    fn provide_hover(&self, _params: &HoverParams) -> Option<Hover> {
243        let stats = self.inspector.stats();
244
245        let markdown = format!(
246            "## Async Inspect Statistics\n\n\
247            - **Total Tasks:** {}\n\
248            - **Running Tasks:** {}\n\
249            - **Completed Tasks:** {}\n\
250            - **Failed Tasks:** {}\n\
251            - **Blocked Tasks:** {}\n\n\
252            [Open Dashboard](http://localhost:8080)",
253            stats.total_tasks,
254            stats.running_tasks,
255            stats.completed_tasks,
256            stats.failed_tasks,
257            stats.blocked_tasks
258        );
259
260        Some(Hover {
261            contents: HoverContents::Markup(MarkupContent {
262                kind: MarkupKind::Markdown,
263                value: markdown,
264            }),
265            range: None,
266        })
267    }
268}
269
270#[cfg(feature = "lsp")]
271#[tower_lsp::async_trait]
272impl LanguageServer for AsyncInspectLanguageServer {
273    async fn initialize(&self, params: InitializeParams) -> Result<InitializeResult> {
274        {
275            let mut state = self.state.write();
276            state.workspace_root = params.root_uri;
277            state.client_capabilities = Some(params.capabilities);
278        } // Drop the lock before await
279
280        self.client
281            .log_message(MessageType::INFO, "async-inspect LSP server initialized")
282            .await;
283
284        Ok(InitializeResult {
285            server_info: Some(ServerInfo {
286                name: "async-inspect-lsp".to_string(),
287                version: Some(env!("CARGO_PKG_VERSION").to_string()),
288            }),
289            capabilities: ServerCapabilities {
290                text_document_sync: Some(TextDocumentSyncCapability::Kind(
291                    TextDocumentSyncKind::FULL,
292                )),
293                hover_provider: Some(HoverProviderCapability::Simple(true)),
294                code_action_provider: Some(CodeActionProviderCapability::Simple(true)),
295                completion_provider: Some(CompletionOptions {
296                    resolve_provider: Some(false),
297                    trigger_characters: Some(vec![".".to_string()]),
298                    work_done_progress_options: Default::default(),
299                    all_commit_characters: None,
300                    completion_item: None,
301                }),
302                ..Default::default()
303            },
304        })
305    }
306
307    async fn initialized(&self, _: InitializedParams) {
308        self.client
309            .log_message(MessageType::INFO, "async-inspect LSP server ready")
310            .await;
311    }
312
313    async fn shutdown(&self) -> Result<()> {
314        Ok(())
315    }
316
317    async fn did_open(&self, params: DidOpenTextDocumentParams) {
318        let uri = params.text_document.uri;
319        let text = params.text_document.text;
320
321        let diagnostics = self.analyze_document(uri.clone(), &text).await;
322
323        self.client
324            .publish_diagnostics(uri, diagnostics, Some(params.text_document.version))
325            .await;
326    }
327
328    async fn did_change(&self, params: DidChangeTextDocumentParams) {
329        let uri = params.text_document.uri;
330        let text = &params.content_changes[0].text;
331
332        let diagnostics = self.analyze_document(uri.clone(), text).await;
333
334        self.client
335            .publish_diagnostics(uri, diagnostics, Some(params.text_document.version))
336            .await;
337    }
338
339    async fn did_save(&self, params: DidSaveTextDocumentParams) {
340        if let Some(text) = params.text {
341            let diagnostics = self
342                .analyze_document(params.text_document.uri.clone(), &text)
343                .await;
344
345            self.client
346                .publish_diagnostics(params.text_document.uri, diagnostics, None)
347                .await;
348        }
349    }
350
351    async fn hover(&self, params: HoverParams) -> Result<Option<Hover>> {
352        Ok(self.provide_hover(&params))
353    }
354
355    async fn code_action(&self, params: CodeActionParams) -> Result<Option<CodeActionResponse>> {
356        let actions = self.provide_code_actions(&params);
357
358        if actions.is_empty() {
359            Ok(None)
360        } else {
361            Ok(Some(actions))
362        }
363    }
364
365    async fn completion(&self, _params: CompletionParams) -> Result<Option<CompletionResponse>> {
366        let items = vec![
367            CompletionItem {
368                label: "spawn_tracked".to_string(),
369                kind: Some(CompletionItemKind::FUNCTION),
370                detail: Some("Spawn a tracked async task".to_string()),
371                documentation: Some(Documentation::MarkupContent(MarkupContent {
372                    kind: MarkupKind::Markdown,
373                    value: "Spawns an async task with automatic tracking by async-inspect.\n\n\
374                            ```rust\n\
375                            spawn_tracked(\"task_name\", async { /* ... */ });\n\
376                            ```"
377                    .to_string(),
378                })),
379                insert_text: Some("spawn_tracked(\"${1:task_name}\", ${2:future})".to_string()),
380                insert_text_format: Some(InsertTextFormat::SNIPPET),
381                ..Default::default()
382            },
383            CompletionItem {
384                label: "inspect".to_string(),
385                kind: Some(CompletionItemKind::METHOD),
386                detail: Some("Track an await point".to_string()),
387                documentation: Some(Documentation::MarkupContent(MarkupContent {
388                    kind: MarkupKind::Markdown,
389                    value: "Track an await point with a label.\n\n\
390                            ```rust\n\
391                            future.inspect(\"await_label\").await\n\
392                            ```"
393                    .to_string(),
394                })),
395                insert_text: Some("inspect(\"${1:label}\")".to_string()),
396                insert_text_format: Some(InsertTextFormat::SNIPPET),
397                ..Default::default()
398            },
399        ];
400
401        Ok(Some(CompletionResponse::Array(items)))
402    }
403}
404
405#[cfg(not(feature = "lsp"))]
406compile_error!("The lsp module requires the 'lsp' feature to be enabled");