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 ..Default::default()
155 },
156 })
157 }
158
159 async fn initialized(&self, _: InitializedParams) {
160 info!("Server initialized notification received");
161 self.client
162 .log_message(MessageType::INFO, "pytest-language-server initialized")
163 .await;
164
165 let watch_init_py = Registration {
170 id: "watch-init-py".to_string(),
171 method: "workspace/didChangeWatchedFiles".to_string(),
172 register_options: Some(
173 serde_json::to_value(DidChangeWatchedFilesRegistrationOptions {
174 watchers: vec![FileSystemWatcher {
175 glob_pattern: GlobPattern::String("**/__init__.py".to_string()),
176 kind: Some(WatchKind::Create | WatchKind::Delete),
177 }],
178 })
179 .unwrap(),
180 ),
181 };
182
183 if let Err(e) = self.client.register_capability(vec![watch_init_py]).await {
184 info!(
187 "Failed to register __init__.py file watcher (client may not support it): {}",
188 e
189 );
190 }
191 }
192
193 async fn did_open(&self, params: DidOpenTextDocumentParams) {
194 let uri = params.text_document.uri.clone();
195 info!("did_open: {:?}", uri);
196 if let Some(file_path) = self.uri_to_path(&uri) {
197 self.uri_cache.insert(file_path.clone(), uri.clone());
200
201 info!("Analyzing file: {:?}", file_path);
202 self.fixture_db
203 .analyze_file(file_path.clone(), ¶ms.text_document.text);
204
205 self.publish_diagnostics_for_file(&uri, &file_path).await;
207 }
208 }
209
210 async fn did_change(&self, params: DidChangeTextDocumentParams) {
211 let uri = params.text_document.uri.clone();
212 info!("did_change: {:?}", uri);
213 if let Some(file_path) = self.uri_to_path(&uri) {
214 if let Some(change) = params.content_changes.first() {
215 info!("Re-analyzing file: {:?}", file_path);
216 self.fixture_db
217 .analyze_file(file_path.clone(), &change.text);
218
219 self.publish_diagnostics_for_file(&uri, &file_path).await;
221
222 if let Err(e) = self.client.inlay_hint_refresh().await {
225 info!(
227 "Inlay hint refresh request failed (client may not support it): {}",
228 e
229 );
230 }
231 }
232 }
233 }
234
235 async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) {
236 for event in ¶ms.changes {
240 if event.typ != FileChangeType::CREATED && event.typ != FileChangeType::DELETED {
241 continue;
242 }
243
244 let Some(init_path) = self.uri_to_path(&event.uri) else {
245 continue;
246 };
247
248 let affected_dir = match init_path.parent() {
252 Some(dir) => dir.to_path_buf(),
253 None => continue,
254 };
255
256 let kind = if event.typ == FileChangeType::CREATED {
257 "created"
258 } else {
259 "deleted"
260 };
261 info!(
262 "__init__.py {} in {:?} — re-analyzing affected fixture files",
263 kind, affected_dir
264 );
265
266 let files_to_reanalyze: Vec<std::path::PathBuf> = self
268 .fixture_db
269 .file_definitions
270 .iter()
271 .filter(|entry| entry.key().starts_with(&affected_dir))
272 .map(|entry| entry.key().clone())
273 .collect();
274
275 for file_path in files_to_reanalyze {
276 if let Some(content) = self.fixture_db.get_file_content(&file_path) {
277 info!("Re-analyzing {:?} after __init__.py change", file_path);
278 self.fixture_db.analyze_file(file_path.clone(), &content);
279
280 if let Some(uri) = self.uri_cache.get(&file_path) {
282 self.publish_diagnostics_for_file(&uri, &file_path).await;
283 }
284 }
285 }
286 }
287
288 if !params.changes.is_empty() {
290 if let Err(e) = self.client.inlay_hint_refresh().await {
291 info!(
292 "Inlay hint refresh after __init__.py change failed (client may not support it): {}",
293 e
294 );
295 }
296 }
297 }
298
299 async fn did_close(&self, params: DidCloseTextDocumentParams) {
300 let uri = params.text_document.uri;
301 info!("did_close: {:?}", uri);
302 if let Some(file_path) = self.uri_to_path(&uri) {
303 self.fixture_db.cleanup_file_cache(&file_path);
305 self.uri_cache.remove(&file_path);
307 }
308 }
309
310 async fn goto_definition(
311 &self,
312 params: GotoDefinitionParams,
313 ) -> Result<Option<GotoDefinitionResponse>> {
314 self.handle_goto_definition(params).await
315 }
316
317 async fn goto_implementation(
318 &self,
319 params: GotoImplementationParams,
320 ) -> Result<Option<GotoImplementationResponse>> {
321 self.handle_goto_implementation(params).await
322 }
323
324 async fn hover(&self, params: HoverParams) -> Result<Option<Hover>> {
325 self.handle_hover(params).await
326 }
327
328 async fn references(&self, params: ReferenceParams) -> Result<Option<Vec<Location>>> {
329 self.handle_references(params).await
330 }
331
332 async fn completion(&self, params: CompletionParams) -> Result<Option<CompletionResponse>> {
333 self.handle_completion(params).await
334 }
335
336 async fn code_action(&self, params: CodeActionParams) -> Result<Option<CodeActionResponse>> {
337 self.handle_code_action(params).await
338 }
339
340 async fn document_symbol(
341 &self,
342 params: DocumentSymbolParams,
343 ) -> Result<Option<DocumentSymbolResponse>> {
344 self.handle_document_symbol(params).await
345 }
346
347 async fn symbol(
348 &self,
349 params: WorkspaceSymbolParams,
350 ) -> Result<Option<WorkspaceSymbolResponse>> {
351 let result = self.handle_workspace_symbol(params).await?;
352 Ok(result.map(WorkspaceSymbolResponse::Flat))
353 }
354
355 async fn code_lens(&self, params: CodeLensParams) -> Result<Option<Vec<CodeLens>>> {
356 self.handle_code_lens(params).await
357 }
358
359 async fn inlay_hint(&self, params: InlayHintParams) -> Result<Option<Vec<InlayHint>>> {
360 self.handle_inlay_hint(params).await
361 }
362
363 async fn prepare_call_hierarchy(
364 &self,
365 params: CallHierarchyPrepareParams,
366 ) -> Result<Option<Vec<CallHierarchyItem>>> {
367 self.handle_prepare_call_hierarchy(params).await
368 }
369
370 async fn incoming_calls(
371 &self,
372 params: CallHierarchyIncomingCallsParams,
373 ) -> Result<Option<Vec<CallHierarchyIncomingCall>>> {
374 self.handle_incoming_calls(params).await
375 }
376
377 async fn outgoing_calls(
378 &self,
379 params: CallHierarchyOutgoingCallsParams,
380 ) -> Result<Option<Vec<CallHierarchyOutgoingCall>>> {
381 self.handle_outgoing_calls(params).await
382 }
383
384 async fn shutdown(&self) -> Result<()> {
385 info!("Shutdown request received");
386
387 if let Some(handle) = self.scan_task.lock().await.take() {
389 info!("Aborting background workspace scan task");
390 handle.abort();
391 match tokio::time::timeout(std::time::Duration::from_millis(100), handle).await {
393 Ok(Ok(_)) => info!("Background scan task already completed"),
394 Ok(Err(_)) => info!("Background scan task aborted"),
395 Err(_) => info!("Background scan task abort timed out, continuing shutdown"),
396 }
397 }
398
399 info!("Shutdown complete");
400
401 #[cfg(not(test))]
406 tokio::spawn(async {
407 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
408 info!("Forcing process exit");
409 std::process::exit(0);
410 });
411
412 Ok(())
413 }
414}