1#[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#[cfg(feature = "lsp")]
63pub struct AsyncInspectLanguageServer {
64 client: Client,
66
67 inspector: &'static Inspector,
69
70 state: Arc<RwLock<ServerState>>,
72}
73
74#[cfg(feature = "lsp")]
75struct ServerState {
76 workspace_root: Option<Url>,
78
79 client_capabilities: Option<ClientCapabilities>,
81}
82
83#[cfg(feature = "lsp")]
84impl AsyncInspectLanguageServer {
85 #[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 async fn analyze_document(&self, _uri: Url, text: &str) -> Vec<Diagnostic> {
100 let mut diagnostics = Vec::new();
101
102 if text.contains("tokio::spawn") && !text.contains("spawn_tracked") {
104 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 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 fn provide_code_actions(&self, params: &CodeActionParams) -> Vec<CodeActionOrCommand> {
169 let mut actions = Vec::new();
170
171 for diagnostic in ¶ms.context.diagnostics {
172 if let Some(NumberOrString::String(code)) = &diagnostic.code {
173 match code.as_str() {
174 "async-inspect-001" => {
175 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 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 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 } 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 = ¶ms.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(¶ms))
353 }
354
355 async fn code_action(&self, params: CodeActionParams) -> Result<Option<CodeActionResponse>> {
356 let actions = self.provide_code_actions(¶ms);
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");