pytest_language_server/providers/
language_server.rs1use std::sync::Arc;
8
9use tower_lsp_server::jsonrpc::Result;
10use tower_lsp_server::ls_types::request::{GotoImplementationParams, GotoImplementationResponse};
11use tower_lsp_server::ls_types::*;
12use tower_lsp_server::LanguageServer;
13use tracing::{error, info, warn};
14
15use super::Backend;
16use crate::config;
17
18impl LanguageServer for Backend {
19 async fn initialize(&self, params: InitializeParams) -> Result<InitializeResult> {
20 info!("Initialize request received");
21
22 let root_uri = params
26 .workspace_folders
27 .as_ref()
28 .and_then(|folders| folders.first())
29 .map(|folder| folder.uri.clone())
30 .or_else(|| {
31 #[allow(deprecated)]
32 params.root_uri.clone()
33 });
34
35 if let Some(root_uri) = root_uri {
36 if let Some(root_path) = root_uri.to_file_path() {
37 let root_path = root_path.to_path_buf();
38 info!("Starting workspace scan: {:?}", root_path);
39
40 *self.original_workspace_root.write().await = Some(root_path.clone());
42
43 let canonical_root = root_path
45 .canonicalize()
46 .unwrap_or_else(|_| root_path.clone());
47 *self.workspace_root.write().await = Some(canonical_root.clone());
48
49 let loaded_config = config::Config::load(&root_path);
51 info!("Loaded config: {:?}", loaded_config);
52 *self.config.write().await = loaded_config;
53
54 let fixture_db = Arc::clone(&self.fixture_db);
56 let client = self.client.clone();
57 let exclude_patterns = self.config.read().await.exclude.clone();
58
59 let scan_handle = tokio::spawn(async move {
62 client
63 .log_message(
64 MessageType::INFO,
65 format!("Scanning workspace: {:?}", root_path),
66 )
67 .await;
68
69 let scan_result = tokio::task::spawn_blocking(move || {
71 fixture_db.scan_workspace_with_excludes(&root_path, &exclude_patterns);
72 })
73 .await;
74
75 match scan_result {
76 Ok(()) => {
77 info!("Workspace scan complete");
78 client
79 .log_message(MessageType::INFO, "Workspace scan complete")
80 .await;
81 }
82 Err(e) => {
83 error!("Workspace scan failed: {:?}", e);
84 client
85 .log_message(
86 MessageType::ERROR,
87 format!("Workspace scan failed: {:?}", e),
88 )
89 .await;
90 }
91 }
92 });
93
94 *self.scan_task.lock().await = Some(scan_handle);
96 }
97 } else {
98 warn!("No root URI provided in initialize - workspace scanning disabled");
99 self.client
100 .log_message(
101 MessageType::WARNING,
102 "No workspace root provided - fixture analysis disabled",
103 )
104 .await;
105 }
106
107 info!("Returning initialize result with capabilities");
108 Ok(InitializeResult {
109 server_info: Some(ServerInfo {
110 name: "pytest-language-server".to_string(),
111 version: Some(env!("CARGO_PKG_VERSION").to_string()),
112 }),
113 capabilities: ServerCapabilities {
114 definition_provider: Some(OneOf::Left(true)),
115 hover_provider: Some(HoverProviderCapability::Simple(true)),
116 references_provider: Some(OneOf::Left(true)),
117 text_document_sync: Some(TextDocumentSyncCapability::Kind(
118 TextDocumentSyncKind::FULL,
119 )),
120 code_action_provider: Some(CodeActionProviderCapability::Options(
121 CodeActionOptions {
122 code_action_kinds: Some(vec![
123 CodeActionKind::QUICKFIX,
124 CodeActionKind::new("source.pytest-ls"),
125 CodeActionKind::new("source.fixAll.pytest-ls"),
126 ]),
127 work_done_progress_options: WorkDoneProgressOptions {
128 work_done_progress: None,
129 },
130 resolve_provider: None,
131 },
132 )),
133 completion_provider: Some(CompletionOptions {
134 resolve_provider: Some(false),
135 trigger_characters: Some(vec![
136 "\"".to_string(),
137 "(".to_string(),
138 ",".to_string(),
139 ]),
140 all_commit_characters: None,
141 work_done_progress_options: WorkDoneProgressOptions {
142 work_done_progress: None,
143 },
144 completion_item: None,
145 }),
146 document_symbol_provider: Some(OneOf::Left(true)),
147 workspace_symbol_provider: Some(OneOf::Left(true)),
148 code_lens_provider: Some(CodeLensOptions {
149 resolve_provider: Some(false),
150 }),
151 inlay_hint_provider: Some(OneOf::Left(true)),
152 implementation_provider: Some(ImplementationProviderCapability::Simple(true)),
153 call_hierarchy_provider: Some(CallHierarchyServerCapability::Simple(true)),
154 rename_provider: Some(OneOf::Right(RenameOptions {
155 prepare_provider: Some(true),
156 work_done_progress_options: WorkDoneProgressOptions {
157 work_done_progress: None,
158 },
159 })),
160 ..Default::default()
161 },
162 })
163 }
164
165 async fn initialized(&self, _: InitializedParams) {
166 info!("Server initialized notification received");
167 self.client
168 .log_message(MessageType::INFO, "pytest-language-server initialized")
169 .await;
170
171 let watch_init_py = Registration {
176 id: "watch-init-py".to_string(),
177 method: "workspace/didChangeWatchedFiles".to_string(),
178 register_options: Some(
179 serde_json::to_value(DidChangeWatchedFilesRegistrationOptions {
180 watchers: vec![FileSystemWatcher {
181 glob_pattern: GlobPattern::String("**/__init__.py".to_string()),
182 kind: Some(WatchKind::Create | WatchKind::Delete),
183 }],
184 })
185 .unwrap(),
186 ),
187 };
188
189 if let Err(e) = self.client.register_capability(vec![watch_init_py]).await {
190 info!(
193 "Failed to register __init__.py file watcher (client may not support it): {}",
194 e
195 );
196 }
197 }
198
199 async fn did_open(&self, params: DidOpenTextDocumentParams) {
200 let uri = params.text_document.uri.clone();
201 info!("did_open: {:?}", uri);
202 if let Some(file_path) = self.uri_to_path(&uri) {
203 self.uri_cache.insert(file_path.clone(), uri.clone());
206
207 info!("Analyzing file: {:?}", file_path);
208 self.fixture_db
209 .analyze_file(file_path.clone(), ¶ms.text_document.text);
210
211 self.publish_diagnostics_for_file(&uri, &file_path).await;
213 }
214 }
215
216 async fn did_change(&self, params: DidChangeTextDocumentParams) {
217 let uri = params.text_document.uri.clone();
218 info!("did_change: {:?}", uri);
219 if let Some(file_path) = self.uri_to_path(&uri) {
220 if let Some(change) = params.content_changes.first() {
221 info!("Re-analyzing file: {:?}", file_path);
222 self.fixture_db
223 .analyze_file(file_path.clone(), &change.text);
224
225 self.publish_diagnostics_for_file(&uri, &file_path).await;
227
228 if let Err(e) = self.client.inlay_hint_refresh().await {
231 info!(
233 "Inlay hint refresh request failed (client may not support it): {}",
234 e
235 );
236 }
237 }
238 }
239 }
240
241 async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) {
242 for event in ¶ms.changes {
246 if event.typ != FileChangeType::CREATED && event.typ != FileChangeType::DELETED {
247 continue;
248 }
249
250 let Some(init_path) = self.uri_to_path(&event.uri) else {
251 continue;
252 };
253
254 let affected_dir = match init_path.parent() {
258 Some(dir) => dir.to_path_buf(),
259 None => continue,
260 };
261
262 let kind = if event.typ == FileChangeType::CREATED {
263 "created"
264 } else {
265 "deleted"
266 };
267 info!(
268 "__init__.py {} in {:?} — re-analyzing affected fixture files",
269 kind, affected_dir
270 );
271
272 let files_to_reanalyze: Vec<std::path::PathBuf> = self
274 .fixture_db
275 .file_definitions
276 .iter()
277 .filter(|entry| entry.key().starts_with(&affected_dir))
278 .map(|entry| entry.key().clone())
279 .collect();
280
281 for file_path in files_to_reanalyze {
282 if let Some(content) = self.fixture_db.get_file_content(&file_path) {
283 info!("Re-analyzing {:?} after __init__.py change", file_path);
284 self.fixture_db.analyze_file(file_path.clone(), &content);
285
286 if let Some(uri) = self.uri_cache.get(&file_path) {
288 self.publish_diagnostics_for_file(&uri, &file_path).await;
289 }
290 }
291 }
292 }
293
294 if !params.changes.is_empty() {
296 if let Err(e) = self.client.inlay_hint_refresh().await {
297 info!(
298 "Inlay hint refresh after __init__.py change failed (client may not support it): {}",
299 e
300 );
301 }
302 }
303 }
304
305 async fn did_close(&self, params: DidCloseTextDocumentParams) {
306 let uri = params.text_document.uri;
307 info!("did_close: {:?}", uri);
308 if let Some(file_path) = self.uri_to_path(&uri) {
309 self.fixture_db.cleanup_file_cache(&file_path);
311 self.uri_cache.remove(&file_path);
313 }
314 }
315
316 async fn goto_definition(
317 &self,
318 params: GotoDefinitionParams,
319 ) -> Result<Option<GotoDefinitionResponse>> {
320 self.handle_goto_definition(params).await
321 }
322
323 async fn goto_implementation(
324 &self,
325 params: GotoImplementationParams,
326 ) -> Result<Option<GotoImplementationResponse>> {
327 self.handle_goto_implementation(params).await
328 }
329
330 async fn hover(&self, params: HoverParams) -> Result<Option<Hover>> {
331 self.handle_hover(params).await
332 }
333
334 async fn references(&self, params: ReferenceParams) -> Result<Option<Vec<Location>>> {
335 self.handle_references(params).await
336 }
337
338 async fn prepare_rename(
339 &self,
340 params: TextDocumentPositionParams,
341 ) -> Result<Option<PrepareRenameResponse>> {
342 self.handle_prepare_rename(params).await
343 }
344
345 async fn rename(&self, params: RenameParams) -> Result<Option<WorkspaceEdit>> {
346 self.handle_rename(params).await
347 }
348
349 async fn completion(&self, params: CompletionParams) -> Result<Option<CompletionResponse>> {
350 self.handle_completion(params).await
351 }
352
353 async fn code_action(&self, params: CodeActionParams) -> Result<Option<CodeActionResponse>> {
354 self.handle_code_action(params).await
355 }
356
357 async fn document_symbol(
358 &self,
359 params: DocumentSymbolParams,
360 ) -> Result<Option<DocumentSymbolResponse>> {
361 self.handle_document_symbol(params).await
362 }
363
364 async fn symbol(
365 &self,
366 params: WorkspaceSymbolParams,
367 ) -> Result<Option<WorkspaceSymbolResponse>> {
368 let result = self.handle_workspace_symbol(params).await?;
369 Ok(result.map(WorkspaceSymbolResponse::Flat))
370 }
371
372 async fn code_lens(&self, params: CodeLensParams) -> Result<Option<Vec<CodeLens>>> {
373 self.handle_code_lens(params).await
374 }
375
376 async fn inlay_hint(&self, params: InlayHintParams) -> Result<Option<Vec<InlayHint>>> {
377 self.handle_inlay_hint(params).await
378 }
379
380 async fn prepare_call_hierarchy(
381 &self,
382 params: CallHierarchyPrepareParams,
383 ) -> Result<Option<Vec<CallHierarchyItem>>> {
384 self.handle_prepare_call_hierarchy(params).await
385 }
386
387 async fn incoming_calls(
388 &self,
389 params: CallHierarchyIncomingCallsParams,
390 ) -> Result<Option<Vec<CallHierarchyIncomingCall>>> {
391 self.handle_incoming_calls(params).await
392 }
393
394 async fn outgoing_calls(
395 &self,
396 params: CallHierarchyOutgoingCallsParams,
397 ) -> Result<Option<Vec<CallHierarchyOutgoingCall>>> {
398 self.handle_outgoing_calls(params).await
399 }
400
401 async fn shutdown(&self) -> Result<()> {
402 info!("Shutdown request received");
403
404 if let Some(handle) = self.scan_task.lock().await.take() {
406 info!("Aborting background workspace scan task");
407 handle.abort();
408 match tokio::time::timeout(std::time::Duration::from_millis(100), handle).await {
410 Ok(Ok(_)) => info!("Background scan task already completed"),
411 Ok(Err(_)) => info!("Background scan task aborted"),
412 Err(_) => info!("Background scan task abort timed out, continuing shutdown"),
413 }
414 }
415
416 info!("Shutdown complete");
417
418 #[cfg(not(test))]
423 tokio::spawn(async {
424 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
425 info!("Forcing process exit");
426 std::process::exit(0);
427 });
428
429 Ok(())
430 }
431}