Skip to main content

php_lsp/backend/
server.rs

1use std::sync::Arc;
2
3use tower_lsp::jsonrpc::Result;
4use tower_lsp::lsp_types::*;
5use tower_lsp::{LanguageServer, async_trait};
6
7use super::panic_guard::{guard_async, guard_async_result};
8use crate::completion::{CompletionCtx, filtered_completions_at};
9use crate::document::ast::ParsedDoc;
10use crate::document::open_files::compute_open_file_diagnostics;
11use crate::hover::{
12    class_hover_from_index, docs_for_symbol_from_index, extract_static_class_before_cursor,
13    hover_info_with_maps, method_hover_from_index, signature_for_symbol_from_index,
14};
15use crate::index::workspace_scan::{scan_workspace, send_refresh_requests};
16use crate::lang::config::LspConfig;
17use crate::navigation::symbols::{
18    document_symbols, resolve_workspace_symbol, workspace_symbols_from_workspace,
19};
20use crate::text::word_at_position;
21
22use crate::navigation::call_hierarchy::{
23    incoming_calls, outgoing_calls_indexed, prepare_call_hierarchy_indexed,
24};
25use crate::navigation::declaration::{goto_declaration, goto_declaration_from_index};
26use crate::navigation::implementation::{
27    find_implementations, find_implementations_mir_backed, find_method_implementations_mir_backed,
28};
29use crate::navigation::moniker::moniker_at;
30use crate::navigation::type_definition::{goto_type_definition, goto_type_definition_from_index};
31use crate::navigation::type_hierarchy::{
32    prepare_type_hierarchy_from_workspace, subtypes_of_mir_backed, supertypes_of_from_workspace,
33};
34
35use crate::analysis::code_lens::code_lenses;
36use crate::analysis::diagnostics::{diagnostics_from_doc, parse_document, parse_document_no_diags};
37use crate::analysis::document_highlight::document_highlights;
38use crate::analysis::inlay_hints::inlay_hints;
39use crate::analysis::inline_value::inline_values_in_range;
40use crate::analysis::semantic_tokens::{
41    compute_token_delta, semantic_tokens, semantic_tokens_range, token_hash,
42};
43
44use crate::editing::document_link::document_links;
45use crate::editing::folding::folding_ranges;
46use crate::editing::formatting::{format_document, format_range};
47use crate::editing::on_type_format::on_type_format;
48use crate::editing::rename::{prepare_rename, rename, rename_property, rename_variable};
49use crate::editing::selection_range::selection_ranges;
50use crate::editing::signature_help::signature_help;
51
52use super::helpers::{
53    cursor_is_on_property_decl, is_after_arrow, promoted_property_at_cursor, run_phpunit,
54};
55use super::{Backend, publish_with_dependents};
56
57#[async_trait]
58impl LanguageServer for Backend {
59    async fn initialize(&self, params: InitializeParams) -> Result<InitializeResult> {
60        self.handle_initialize(params).await
61    }
62
63    async fn initialized(&self, _params: InitializedParams) {
64        guard_async("initialized", async move {
65            self.handle_initialized(_params).await
66        })
67        .await
68    }
69
70    async fn did_change_configuration(&self, _params: DidChangeConfigurationParams) {
71        guard_async("did_change_configuration", async move {
72            // Pull the current configuration from the client rather than parsing the
73            // (often-null) params.settings, which not all clients populate.
74            let items = vec![ConfigurationItem {
75                scope_uri: None,
76                section: Some("php-lsp".to_string()),
77            }];
78            if let Ok(values) = self.client.configuration(items).await
79                && let Some(value) = values.into_iter().next()
80            {
81                let roots = self.root_paths.load_full();
82
83                // Re-read .php-lsp.json so a user who edits the file and then
84                // triggers a configuration reload picks up the latest values.
85                let file_cfg = crate::lang::autoload::load_project_config_json(&roots);
86
87                if let Some(ver) = value.get("phpVersion").and_then(|v| v.as_str())
88                    && !crate::lang::autoload::is_valid_php_version(ver)
89                {
90                    self.client
91                        .log_message(
92                            tower_lsp::lsp_types::MessageType::WARNING,
93                            format!(
94                                "php-lsp: unsupported phpVersion {ver:?} — valid values: {}",
95                                crate::lang::autoload::SUPPORTED_PHP_VERSIONS.join(", ")
96                            ),
97                        )
98                        .await;
99                }
100
101                let file_obj = file_cfg.as_ref().filter(|v| v.is_object());
102                let merged = LspConfig::merge_project_configs(file_obj, Some(&value));
103                let mut cfg = LspConfig::from_value(&merged);
104
105                // Resolve the PHP version and log what was chosen and why.
106                let (ver, source) = self.resolve_php_version(cfg.php_version.as_deref());
107                self.client
108                    .log_message(
109                        tower_lsp::lsp_types::MessageType::INFO,
110                        format!("php-lsp: using PHP {ver} ({source})"),
111                    )
112                    .await;
113                // Clamp unsupported versions to the nearest supported one and warn.
114                let ver = if source != "set by editor"
115                    && !crate::lang::autoload::is_valid_php_version(&ver)
116                {
117                    let clamped = crate::lang::autoload::clamp_php_version(&ver);
118                    self.client
119                        .show_message(
120                            tower_lsp::lsp_types::MessageType::WARNING,
121                            format!(
122                                "php-lsp: detected PHP {ver} is outside the supported range ({}); \
123                             using PHP {clamped} for analysis",
124                                crate::lang::autoload::SUPPORTED_PHP_VERSIONS.join(", ")
125                            ),
126                        )
127                        .await;
128                    clamped.to_string()
129                } else {
130                    ver
131                };
132                cfg.php_version = Some(ver.clone());
133                if let Ok(pv) = ver.parse::<mir_analyzer::PhpVersion>() {
134                    self.docs.set_php_version(pv);
135                }
136                self.config.store(Arc::new(cfg));
137                send_refresh_requests(&self.client).await;
138            }
139        })
140        .await
141    }
142
143    async fn did_change_workspace_folders(&self, params: DidChangeWorkspaceFoldersParams) {
144        guard_async("did_change_workspace_folders", async move {
145            // Remove folders from our tracked roots.
146            {
147                let mut roots = (**self.root_paths.load()).clone();
148                for removed in &params.event.removed {
149                    if let Ok(path) = removed.uri.to_file_path() {
150                        roots.retain(|r| r != &path);
151                    }
152                }
153                self.root_paths.store(Arc::new(roots));
154            }
155
156            // Add new folders and kick off background scans for each.
157            let (exclude_paths, include_paths, max_indexed_files) = {
158                let cfg = self.config.load();
159                (
160                    cfg.exclude_paths.clone(),
161                    cfg.include_paths.clone(),
162                    cfg.max_indexed_files,
163                )
164            };
165            for added in &params.event.added {
166                if let Ok(path) = added.uri.to_file_path() {
167                    let is_new = {
168                        let mut roots = (**self.root_paths.load()).clone();
169                        if !roots.contains(&path) {
170                            roots.push(path.clone());
171                            self.root_paths.store(Arc::new(roots));
172                            true
173                        } else {
174                            false
175                        }
176                    };
177                    if is_new {
178                        let docs = Arc::clone(&self.docs);
179                        let open_files = self.open_files.clone();
180                        let ex = exclude_paths.clone();
181                        let ip = include_paths.clone();
182                        let path_clone = path.clone();
183                        let client = self.client.clone();
184                        tokio::spawn(async move {
185                            let cache = crate::index::cache::WorkspaceCache::new(&path_clone);
186                            scan_workspace(
187                                path_clone,
188                                docs,
189                                open_files,
190                                cache,
191                                &ex,
192                                &ip,
193                                max_indexed_files,
194                            )
195                            .await;
196                            send_refresh_requests(&client).await;
197                        });
198                    }
199                }
200            }
201        })
202        .await
203    }
204
205    async fn shutdown(&self) -> Result<()> {
206        Ok(())
207    }
208
209    #[tracing::instrument(skip_all)]
210    async fn did_open(&self, params: DidOpenTextDocumentParams) {
211        guard_async("did_open", async move {
212            let uri = params.text_document.uri;
213            let text = params.text_document.text;
214
215            // Store text immediately so other features work while parsing.
216            // This also mirrors the new text into salsa, so the codebase query
217            // sees it when semantic_diagnostics runs below.
218            self.set_open_text(uri.clone(), text);
219
220            // Seed parse diagnostics from the salsa-cached doc. On the fast
221            // path this is a lock-free DashMap lookup — no re-parse.
222            let parse_diags = self
223                .docs
224                .get_doc_salsa(&uri)
225                .map(|doc| diagnostics_from_doc(&doc))
226                .unwrap_or_default();
227            self.set_parse_diagnostics(&uri, parse_diags);
228
229            publish_with_dependents(
230                self.client.clone(),
231                Arc::clone(&self.docs),
232                self.open_files.clone(),
233                uri,
234                self.config.load().diagnostics.clone(),
235            )
236            .await;
237        })
238        .await
239    }
240
241    #[tracing::instrument(skip_all)]
242    async fn did_change(&self, params: DidChangeTextDocumentParams) {
243        guard_async("did_change", async move {
244            let uri = params.text_document.uri;
245            // Incremental sync: apply changes in order to the live buffer.
246            // Each ranged change refers to the document state produced by the
247            // previous one; a change without a range is a full-document
248            // replacement (clients may still send those under INCREMENTAL).
249            let mut updated: Option<String> = None;
250            for change in params.content_changes {
251                match change.range {
252                    None => updated = Some(change.text),
253                    Some(range) => {
254                        let mut cur = match updated.take() {
255                            Some(t) => t,
256                            None => self.get_open_text(&uri).unwrap_or_default(),
257                        };
258                        crate::text::apply_content_change(&mut cur, range, &change.text);
259                        updated = Some(cur);
260                    }
261                }
262            }
263            let Some(text) = updated else { return };
264
265            // Store text immediately and capture the version token.
266            // Features (completion, hover, …) see the new text instantly while
267            // the parse runs in the background.
268            let version = self.set_open_text(uri.clone(), text.clone());
269
270            let docs = Arc::clone(&self.docs);
271            let open_files = self.open_files.clone();
272            let client = self.client.clone();
273            let cfg = self.config.load();
274            let diag_cfg = cfg.diagnostics.clone();
275            let debounce_ms = cfg.debounce_ms;
276            tokio::spawn(async move {
277                // Debounce: if another edit arrives before we parse, the version
278                // gate below will discard this result.
279                tokio::time::sleep(std::time::Duration::from_millis(debounce_ms)).await;
280
281                // Skip the expensive parse+analyze if a newer edit already
282                // superseded this one. Collapses N rapid keystrokes into 1
283                // spawn_blocking call instead of N.
284                if open_files.current_version(&uri) != Some(version) {
285                    return;
286                }
287
288                let (_doc, parse_diags) =
289                    tokio::task::spawn_blocking(move || parse_document(&text))
290                        .await
291                        .unwrap_or_else(|_| (ParsedDoc::default(), vec![]));
292
293                // Only apply if no newer edit arrived while we were parsing.
294                if open_files.current_version(&uri) == Some(version) {
295                    open_files.set_parse_diagnostics(&uri, parse_diags);
296                    publish_with_dependents(client, docs, open_files, uri, diag_cfg).await;
297                }
298            });
299        })
300        .await
301    }
302
303    async fn did_close(&self, params: DidCloseTextDocumentParams) {
304        guard_async("did_close", async move {
305            let uri = params.text_document.uri;
306            self.close_open_file(&uri);
307            // The salsa-mirrored text still holds the last edited buffer, which may
308            // include unsaved changes the user just discarded on close. Re-sync from
309            // disk so cross-file features resolve against the on-disk content; if the
310            // file is gone (never saved / deleted), drop it from the index.
311            let disk_text = match uri.to_file_path() {
312                Ok(path) => tokio::fs::read_to_string(&path).await.ok(),
313                Err(_) => None,
314            };
315            // The disk read awaited above; a reopen may have raced in. Only mutate
316            // the index if the file is still closed, so we never clobber a fresh
317            // buffer.
318            if !self.open_files.contains(&uri) {
319                match disk_text {
320                    Some(text) => self.docs.ingest(uri.clone(), &text),
321                    None => self.docs.remove(&uri),
322                }
323            }
324            // Clear editor diagnostics; the file stays indexed for cross-file features
325            self.client.publish_diagnostics(uri, vec![], None).await;
326        })
327        .await
328    }
329
330    async fn will_save(&self, _params: WillSaveTextDocumentParams) {}
331
332    async fn will_save_wait_until(
333        &self,
334        params: WillSaveTextDocumentParams,
335    ) -> Result<Option<Vec<TextEdit>>> {
336        guard_async_result("will_save_wait_until", async move {
337            let source = self
338                .get_open_text(&params.text_document.uri)
339                .unwrap_or_default();
340            Ok(format_document(&source))
341        })
342        .await
343    }
344
345    async fn did_save(&self, params: DidSaveTextDocumentParams) {
346        guard_async("did_save", async move {
347            let uri = params.text_document.uri;
348            // Re-publish diagnostics on save so editors that defer diagnostics
349            // until save (rather than on every keystroke) see up-to-date results.
350            // Must include semantic diagnostics — publishDiagnostics replaces the
351            // prior set entirely, so omitting them would clear errors the editor
352            // showed after the last did_change.
353            let diag_cfg = self.config.load().diagnostics.clone();
354            let all = compute_open_file_diagnostics(&self.docs, &self.open_files, &uri, &diag_cfg);
355            self.client
356                .publish_diagnostics(uri.clone(), all, None)
357                .await;
358
359            // Persist the FileIndex to the disk cache so that a server restart
360            // can skip re-parsing this file even for edits that happened between
361            // workspace scans. Use the same stat-based key as the workspace scan
362            // so the entry is found on the next cold start.
363            //
364            // Both the stat and the content are read from disk inside the blocking
365            // task so they come from the same on-disk snapshot. Using the editor
366            // buffer (open_files text) instead would risk writing an index derived
367            // from unsaved edits typed after the save but before this task runs,
368            // producing a cache entry where the key (disk stat) and the value
369            // (index of newer buffer content) describe different file versions.
370            if let (Some(root), Ok(path)) =
371                (self.root_paths.load().first().cloned(), uri.to_file_path())
372            {
373                tokio::task::spawn_blocking(move || {
374                    let Some(cache) = crate::index::cache::WorkspaceCache::new(&root) else {
375                        return;
376                    };
377                    // Stat before read to match workspace_scan ordering and
378                    // minimise the TOCTOU window.
379                    let Ok(meta) = std::fs::metadata(&path) else {
380                        return;
381                    };
382                    let Ok(text) = std::fs::read_to_string(&path) else {
383                        return;
384                    };
385                    let mtime_secs = meta
386                        .modified()
387                        .ok()
388                        .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
389                        .map(|d| d.as_secs())
390                        .unwrap_or(0);
391                    let key = crate::index::cache::WorkspaceCache::key_for_stat(
392                        uri.as_str(),
393                        mtime_secs,
394                        meta.len(),
395                    );
396                    let doc = parse_document_no_diags(&text);
397                    let index = crate::index::file_index::FileIndex::extract(&doc);
398                    let _ = cache.write(&key, &index);
399                });
400            }
401        })
402        .await
403    }
404
405    async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) {
406        guard_async("did_change_watched_files", async move {
407            for change in params.changes {
408                match change.typ {
409                    FileChangeType::CREATED | FileChangeType::CHANGED => {
410                        if let Ok(path) = change.uri.to_file_path()
411                            && let Ok(text) = tokio::fs::read_to_string(&path).await
412                        {
413                            // Salsa path: ingest_from_doc mirrors the new text into
414                            // the SourceFile input. On the next codebase() call,
415                            // salsa re-runs file_definitions for this file and the
416                            // aggregator re-folds — no manual remove/collect/finalize.
417                            let doc = parse_document_no_diags(&text);
418                            self.ingest_from_doc_if_not_open(change.uri.clone(), &doc);
419                        }
420                    }
421                    FileChangeType::DELETED => {
422                        self.docs.remove(&change.uri);
423                    }
424                    _ => {}
425                }
426            }
427            // File changes may affect cross-file features — refresh all live editors.
428            send_refresh_requests(&self.client).await;
429        })
430        .await
431    }
432
433    #[tracing::instrument(skip_all)]
434    async fn completion(&self, params: CompletionParams) -> Result<Option<CompletionResponse>> {
435        guard_async_result("completion", async move {
436            let uri = &params.text_document_position.text_document.uri;
437            let position = params.text_document_position.position;
438            let source = self.get_open_text(uri).unwrap_or_default();
439            let doc = match self.get_doc(uri) {
440                Some(d) => d,
441                None => return Ok(Some(CompletionResponse::Array(vec![]))),
442            };
443            let other_docs: Vec<Arc<ParsedDoc>> = self
444                .docs
445                .other_docs(uri, &self.open_urls())
446                .into_iter()
447                .map(|(_, d)| d)
448                .collect();
449            let trigger = params
450                .context
451                .as_ref()
452                .and_then(|c| c.trigger_character.as_deref());
453            let meta_loaded = self.meta.load();
454            let meta_opt = if meta_loaded.is_empty() {
455                None
456            } else {
457                Some(&**meta_loaded)
458            };
459            let imports = self.file_imports(uri);
460            let wi = self.workspace_index_async().await;
461            let docs_for_lookup = Arc::clone(&self.docs);
462            let find_class_doc_fn = move |name: &str| -> Option<Arc<ParsedDoc>> {
463                let cr = *wi.classes_by_name.get(name)?.first()?;
464                let (uri, _) = wi.at(cr)?;
465                docs_for_lookup.get_doc_salsa(uri)
466            };
467            let analysis = self.cached_analysis_async(uri).await;
468            // Cross-request TypeMap cache: rebuilt only when the document text
469            // (or PHPStorm meta) changes, instead of one full AST walk per
470            // completion request.
471            let docs_for_tm = Arc::clone(&self.docs);
472            let doc_for_tm = Arc::clone(&doc);
473            let uri_for_tm = uri.clone();
474            let get_type_map =
475                move || docs_for_tm.cached_type_map(&uri_for_tm, &doc_for_tm, meta_opt);
476            let session = self
477                .docs
478                .analysis_session(self.docs.workspace_php_version());
479            let ctx = CompletionCtx {
480                source: Some(&source),
481                position: Some(position),
482                meta: meta_opt,
483                doc_uri: Some(uri),
484                file_imports: Some(&imports),
485                find_class_doc: Some(&find_class_doc_fn),
486                analysis: analysis.as_deref(),
487                type_map: Some(&get_type_map),
488                session: Some(session),
489            };
490            Ok(Some(CompletionResponse::Array(filtered_completions_at(
491                &doc,
492                &other_docs,
493                trigger,
494                &ctx,
495            ))))
496        })
497        .await
498    }
499
500    async fn completion_resolve(&self, mut item: CompletionItem) -> Result<CompletionItem> {
501        guard_async_result("completion_resolve", async move {
502            if item.documentation.is_some() && item.detail.is_some() {
503                return Ok(item);
504            }
505            // Strip trailing ':' from named-argument labels (e.g. "param:") before lookup.
506            let name = item.label.trim_end_matches(':');
507            let all_indexes = self.docs.all_indexes();
508            if item.detail.is_none()
509                && let Some(sig) = signature_for_symbol_from_index(name, &all_indexes)
510            {
511                item.detail = Some(sig);
512            }
513            if item.documentation.is_none()
514                && let Some(md) = docs_for_symbol_from_index(name, &all_indexes)
515            {
516                item.documentation = Some(Documentation::MarkupContent(MarkupContent {
517                    kind: MarkupKind::Markdown,
518                    value: md,
519                }));
520            }
521            Ok(item)
522        })
523        .await
524    }
525
526    async fn goto_definition(
527        &self,
528        params: GotoDefinitionParams,
529    ) -> Result<Option<GotoDefinitionResponse>> {
530        guard_async_result("goto_definition", async move {
531            self.handle_goto_definition(params).await
532        })
533        .await
534    }
535
536    async fn references(&self, params: ReferenceParams) -> Result<Option<Vec<Location>>> {
537        guard_async_result(
538            "references",
539            async move { self.handle_references(params).await },
540        )
541        .await
542    }
543
544    async fn prepare_rename(
545        &self,
546        params: TextDocumentPositionParams,
547    ) -> Result<Option<PrepareRenameResponse>> {
548        guard_async_result("prepare_rename", async move {
549            let uri = &params.text_document.uri;
550            let source = self.get_open_text(uri).unwrap_or_default();
551            Ok(prepare_rename(&source, params.position).map(PrepareRenameResponse::Range))
552        })
553        .await
554    }
555
556    async fn rename(&self, params: RenameParams) -> Result<Option<WorkspaceEdit>> {
557        guard_async_result("rename", async move {
558            let uri = &params.text_document_position.text_document.uri;
559            let position = params.text_document_position.position;
560            let source = self.get_open_text(uri).unwrap_or_default();
561            let word = match word_at_position(&source, position) {
562                Some(w) => w,
563                None => return Ok(None),
564            };
565            if word.starts_with('$') {
566                let doc = match self.get_doc(uri) {
567                    Some(d) => d,
568                    None => return Ok(None),
569                };
570                // Cursor on a property declaration (`public int $x`) or a promoted
571                // constructor parameter (`private string $x`) — both act as property
572                // declarations and must use the cross-file property rename path.
573                let prop_name = cursor_is_on_property_decl(&source, &doc.program().stmts, position)
574                    .or_else(|| {
575                        promoted_property_at_cursor(&source, &doc.program().stmts, position)
576                    });
577                if let Some(prop_name) = prop_name {
578                    let all_docs = self.docs.all_docs_for_scan();
579                    return Ok(Some(rename_property(
580                        &prop_name,
581                        &params.new_name,
582                        &all_docs,
583                    )));
584                }
585                Ok(Some(rename_variable(
586                    &word,
587                    &params.new_name,
588                    uri,
589                    &doc,
590                    position,
591                )))
592            } else if is_after_arrow(&source, position) {
593                let all_docs = self.docs.all_docs_for_scan();
594                Ok(Some(rename_property(&word, &params.new_name, &all_docs)))
595            } else {
596                let all_docs = self.docs.all_docs_for_scan();
597                let doc_opt = self.get_doc(uri);
598                let target_fqn: Option<String> = doc_opt.as_ref().map(|doc| {
599                    let imports = self.file_imports(uri);
600                    crate::navigation::moniker::resolve_fqn(doc, &word, &imports)
601                });
602                Ok(Some(rename(
603                    &word,
604                    &params.new_name,
605                    &all_docs,
606                    target_fqn.as_deref(),
607                )))
608            }
609        })
610        .await
611    }
612
613    async fn signature_help(&self, params: SignatureHelpParams) -> Result<Option<SignatureHelp>> {
614        guard_async_result("signature_help", async move {
615            let uri = &params.text_document_position_params.text_document.uri;
616            let position = params.text_document_position_params.position;
617            let source = self.get_open_text(uri).unwrap_or_default();
618            let doc = match self.get_doc(uri) {
619                Some(d) => d,
620                None => return Ok(None),
621            };
622            let all_indexes = self.docs.all_indexes();
623            Ok(signature_help(&source, &doc, position, &all_indexes))
624        })
625        .await
626    }
627
628    #[tracing::instrument(skip_all)]
629    async fn hover(&self, params: HoverParams) -> Result<Option<Hover>> {
630        guard_async_result("hover", async move {
631            let uri = &params.text_document_position_params.text_document.uri;
632            let position = params.text_document_position_params.position;
633            let source = self.get_open_text(uri).unwrap_or_default();
634            let doc = match self.get_doc(uri) {
635                Some(d) => d,
636                None => return Ok(None),
637            };
638            let other_docs = self.docs.other_docs(uri, &self.open_urls());
639            let other_maps = self.docs.other_symbol_maps(uri, &self.open_urls());
640            let analysis = self.cached_analysis_async(uri).await;
641            let hover_session = self
642                .docs
643                .analysis_session(self.docs.workspace_php_version());
644            let result = hover_info_with_maps(
645                &source,
646                &doc,
647                analysis.as_deref(),
648                position,
649                &other_docs,
650                &other_maps,
651                Some(&hover_session),
652            );
653            if result.is_some() {
654                return Ok(result);
655            }
656            // Fallback: look up the word in the workspace index so class names in
657            // extends clauses and parameter types resolve even when their defining
658            // file is never opened.  Also try the alias-resolved name so that
659            // `use Foo as Bar` works even when Foo is only in the index.
660            if let Some(word) = crate::text::word_at_position(&source, position) {
661                let wi = self.workspace_index_async().await;
662                // Try the literal word first.
663                if let Some(h) = class_hover_from_index(&word, &wi.files) {
664                    return Ok(Some(h));
665                }
666                // Try alias resolution.
667                if let Some(resolved) = crate::hover::resolve_use_alias(&doc.program().stmts, &word)
668                    && let Some(h) = class_hover_from_index(&resolved, &wi.files)
669                {
670                    return Ok(Some(h));
671                }
672                // Try static method hover: `ClassName::method(…)`.
673                if let Some(line_text) = source.lines().nth(position.line as usize)
674                    && let Some(class_token) =
675                        extract_static_class_before_cursor(line_text, position.character as usize)
676                {
677                    if let Some(h) = method_hover_from_index(&class_token, &word, &wi.files) {
678                        return Ok(Some(h));
679                    }
680                    if let Some(resolved_class) =
681                        crate::hover::resolve_use_alias(&doc.program().stmts, &class_token)
682                        && let Some(h) = method_hover_from_index(&resolved_class, &word, &wi.files)
683                    {
684                        return Ok(Some(h));
685                    }
686                }
687            }
688            Ok(None)
689        })
690        .await
691    }
692
693    async fn document_symbol(
694        &self,
695        params: DocumentSymbolParams,
696    ) -> Result<Option<DocumentSymbolResponse>> {
697        guard_async_result("document_symbol", async move {
698            let uri = &params.text_document.uri;
699            let doc = match self.get_doc(uri) {
700                Some(d) => d,
701                None => return Ok(None),
702            };
703            Ok(Some(DocumentSymbolResponse::Nested(document_symbols(
704                doc.source(),
705                &doc,
706            ))))
707        })
708        .await
709    }
710
711    async fn folding_range(&self, params: FoldingRangeParams) -> Result<Option<Vec<FoldingRange>>> {
712        guard_async_result("folding_range", async move {
713            let uri = &params.text_document.uri;
714            let doc = match self.get_doc(uri) {
715                Some(d) => d,
716                None => return Ok(None),
717            };
718            let ranges = folding_ranges(doc.source(), &doc);
719            Ok(if ranges.is_empty() {
720                None
721            } else {
722                Some(ranges)
723            })
724        })
725        .await
726    }
727
728    async fn inlay_hint(&self, params: InlayHintParams) -> Result<Option<Vec<InlayHint>>> {
729        guard_async_result("inlay_hint", async move {
730            let uri = &params.text_document.uri;
731            let doc = match self.get_doc(uri) {
732                Some(d) => d,
733                None => return Ok(None),
734            };
735            let analysis = self.cached_analysis_async(uri).await;
736            let wi = self.workspace_index_async().await;
737            Ok(Some(inlay_hints(
738                doc.source(),
739                &doc,
740                analysis.as_deref(),
741                params.range,
742                &wi.files,
743            )))
744        })
745        .await
746    }
747
748    async fn inlay_hint_resolve(&self, mut item: InlayHint) -> Result<InlayHint> {
749        let fallback = item.clone();
750        let resolved = guard_async("inlay_hint_resolve", async move {
751            if item.tooltip.is_some() {
752                return Some(item);
753            }
754            let func_name = item
755                .data
756                .as_ref()
757                .and_then(|d| d.get("php_lsp_fn"))
758                .and_then(|v| v.as_str())
759                .map(str::to_string);
760            if let Some(name) = func_name {
761                let all_indexes = self.docs.all_indexes();
762                if let Some(md) = docs_for_symbol_from_index(&name, &all_indexes) {
763                    item.tooltip = Some(InlayHintTooltip::MarkupContent(MarkupContent {
764                        kind: MarkupKind::Markdown,
765                        value: md,
766                    }));
767                }
768            }
769            Some(item)
770        })
771        .await;
772        Ok(resolved.unwrap_or(fallback))
773    }
774
775    async fn symbol(
776        &self,
777        params: WorkspaceSymbolParams,
778    ) -> Result<Option<Vec<SymbolInformation>>> {
779        guard_async_result("symbol", async move {
780            // Phase J: read through the salsa-memoized aggregate so repeated
781            // workspace-symbol queries (every keystroke in the picker) share the
782            // same `Arc` until a file changes.
783            let wi = self.workspace_index_async().await;
784            let results = workspace_symbols_from_workspace(&params.query, &wi);
785            Ok(Some(results))
786        })
787        .await
788    }
789
790    async fn symbol_resolve(&self, params: WorkspaceSymbol) -> Result<WorkspaceSymbol> {
791        // For resolve, we need the full range from the ParsedDoc of open files.
792        let docs = self.docs.docs_for(&self.open_urls());
793        let fallback = params.clone();
794        let resolved = guard_async("symbol_resolve", async move {
795            Some(resolve_workspace_symbol(params, &docs))
796        })
797        .await;
798        Ok(resolved.unwrap_or(fallback))
799    }
800
801    #[tracing::instrument(skip_all)]
802    async fn semantic_tokens_full(
803        &self,
804        params: SemanticTokensParams,
805    ) -> Result<Option<SemanticTokensResult>> {
806        guard_async_result("semantic_tokens_full", async move {
807            let uri = &params.text_document.uri;
808            let doc = match self.get_doc(uri) {
809                Some(d) => d,
810                None => {
811                    return Ok(Some(SemanticTokensResult::Tokens(SemanticTokens {
812                        result_id: None,
813                        data: vec![],
814                    })));
815                }
816            };
817            let tokens = semantic_tokens(doc.source(), &doc);
818            let result_id = token_hash(&tokens);
819            let tokens_arc = Arc::new(tokens);
820            self.docs
821                .store_token_cache(uri, result_id.clone(), Arc::clone(&tokens_arc));
822            let data = Arc::try_unwrap(tokens_arc).unwrap_or_else(|arc| (*arc).clone());
823            Ok(Some(SemanticTokensResult::Tokens(SemanticTokens {
824                result_id: Some(result_id),
825                data,
826            })))
827        })
828        .await
829    }
830
831    async fn semantic_tokens_range(
832        &self,
833        params: SemanticTokensRangeParams,
834    ) -> Result<Option<SemanticTokensRangeResult>> {
835        guard_async_result("semantic_tokens_range", async move {
836            let uri = &params.text_document.uri;
837            let doc = match self.get_doc(uri) {
838                Some(d) => d,
839                None => {
840                    return Ok(Some(SemanticTokensRangeResult::Tokens(SemanticTokens {
841                        result_id: None,
842                        data: vec![],
843                    })));
844                }
845            };
846            let tokens = semantic_tokens_range(doc.source(), &doc, params.range);
847            Ok(Some(SemanticTokensRangeResult::Tokens(SemanticTokens {
848                result_id: None,
849                data: tokens,
850            })))
851        })
852        .await
853    }
854
855    async fn semantic_tokens_full_delta(
856        &self,
857        params: SemanticTokensDeltaParams,
858    ) -> Result<Option<SemanticTokensFullDeltaResult>> {
859        guard_async_result("semantic_tokens_full_delta", async move {
860            let uri = &params.text_document.uri;
861            let doc = match self.get_doc(uri) {
862                Some(d) => d,
863                None => return Ok(None),
864            };
865
866            let new_tokens = Arc::new(semantic_tokens(doc.source(), &doc));
867            let new_result_id = token_hash(&new_tokens);
868            let prev_id = &params.previous_result_id;
869
870            let result = match self.docs.get_token_cache(uri, prev_id) {
871                Some(old_tokens) => {
872                    let edits = compute_token_delta(&old_tokens, &new_tokens);
873                    SemanticTokensFullDeltaResult::TokensDelta(SemanticTokensDelta {
874                        result_id: Some(new_result_id.clone()),
875                        edits,
876                    })
877                }
878                // Unknown previous result — fall back to full tokens
879                None => SemanticTokensFullDeltaResult::Tokens(SemanticTokens {
880                    result_id: Some(new_result_id.clone()),
881                    data: (*new_tokens).clone(),
882                }),
883            };
884
885            self.docs.store_token_cache(uri, new_result_id, new_tokens);
886            Ok(Some(result))
887        })
888        .await
889    }
890
891    async fn selection_range(
892        &self,
893        params: SelectionRangeParams,
894    ) -> Result<Option<Vec<SelectionRange>>> {
895        guard_async_result("selection_range", async move {
896            let uri = &params.text_document.uri;
897            let doc = match self.get_doc(uri) {
898                Some(d) => d,
899                None => return Ok(None),
900            };
901            let ranges = selection_ranges(&doc, &params.positions);
902            Ok(if ranges.is_empty() {
903                None
904            } else {
905                Some(ranges)
906            })
907        })
908        .await
909    }
910
911    async fn prepare_call_hierarchy(
912        &self,
913        params: CallHierarchyPrepareParams,
914    ) -> Result<Option<Vec<CallHierarchyItem>>> {
915        guard_async_result("prepare_call_hierarchy", async move {
916            let uri = &params.text_document_position_params.text_document.uri;
917            let position = params.text_document_position_params.position;
918            let source = self.get_open_text(uri).unwrap_or_default();
919            let word = match word_at_position(&source, position) {
920                Some(w) => w,
921                None => return Ok(None),
922            };
923            // O(matches) lookup via the aggregate's `decls_by_name` map instead
924            // of scanning every workspace doc.
925            let wi = self.workspace_index_async().await;
926            let docs = Arc::clone(&self.docs);
927            let get_doc = move |u: &Url| docs.get_doc_salsa(u);
928            Ok(prepare_call_hierarchy_indexed(&word, &wi, &get_doc).map(|item| vec![item]))
929        })
930        .await
931    }
932
933    async fn incoming_calls(
934        &self,
935        params: CallHierarchyIncomingCallsParams,
936    ) -> Result<Option<Vec<CallHierarchyIncomingCall>>> {
937        guard_async_result("incoming_calls", async move {
938            // Genuinely needs every doc (call sites are body-level, not indexed);
939            // run the workspace scan on the blocking pool.
940            let docs = Arc::clone(&self.docs);
941            let item = params.item;
942            let calls = tokio::task::spawn_blocking(move || {
943                let all_docs = docs.all_docs_for_scan();
944                incoming_calls(&item, &all_docs)
945            })
946            .await
947            .unwrap_or_default();
948            Ok(if calls.is_empty() { None } else { Some(calls) })
949        })
950        .await
951    }
952
953    async fn outgoing_calls(
954        &self,
955        params: CallHierarchyOutgoingCallsParams,
956    ) -> Result<Option<Vec<CallHierarchyOutgoingCall>>> {
957        guard_async_result("outgoing_calls", async move {
958            // Per-callee declaration lookups go through `decls_by_name` — the old
959            // path re-scanned the whole workspace once per distinct callee.
960            let wi = self.workspace_index_async().await;
961            let docs = Arc::clone(&self.docs);
962            let item = params.item;
963            let calls = tokio::task::spawn_blocking(move || {
964                let get_doc = |u: &Url| docs.get_doc_salsa(u);
965                outgoing_calls_indexed(&item, &wi, &get_doc)
966            })
967            .await
968            .unwrap_or_default();
969            Ok(if calls.is_empty() { None } else { Some(calls) })
970        })
971        .await
972    }
973
974    async fn document_highlight(
975        &self,
976        params: DocumentHighlightParams,
977    ) -> Result<Option<Vec<DocumentHighlight>>> {
978        guard_async_result("document_highlight", async move {
979            let uri = &params.text_document_position_params.text_document.uri;
980            let position = params.text_document_position_params.position;
981            let source = self.get_open_text(uri).unwrap_or_default();
982            let doc = match self.get_doc(uri) {
983                Some(d) => d,
984                None => return Ok(None),
985            };
986            let highlights = document_highlights(&source, &doc, position);
987            Ok(if highlights.is_empty() {
988                None
989            } else {
990                Some(highlights)
991            })
992        })
993        .await
994    }
995
996    async fn linked_editing_range(
997        &self,
998        params: LinkedEditingRangeParams,
999    ) -> Result<Option<LinkedEditingRanges>> {
1000        guard_async_result("linked_editing_range", async move {
1001            self.handle_linked_editing_range(params).await
1002        })
1003        .await
1004    }
1005
1006    async fn goto_implementation(
1007        &self,
1008        params: tower_lsp::lsp_types::request::GotoImplementationParams,
1009    ) -> Result<Option<tower_lsp::lsp_types::request::GotoImplementationResponse>> {
1010        guard_async_result("goto_implementation", async move {
1011            let uri = &params.text_document_position_params.text_document.uri;
1012            let position = params.text_document_position_params.position;
1013            let source = self.get_open_text(uri).unwrap_or_default();
1014            let imports = self.file_imports(uri);
1015            let raw_word = crate::text::word_at_position(&source, position).unwrap_or_default();
1016            // `word_at_position` includes `\` as a word character, so the cursor on
1017            // a use-statement import (`use A\B\Foo`) returns the full qualified name.
1018            // Split to recover the short name and treat the rest as the FQN so the
1019            // workspace index lookup (keyed by short name) still finds subtypes.
1020            let (word, fqn_owned): (String, Option<String>) = if raw_word.contains('\\') {
1021                let short = raw_word
1022                    .rsplit('\\')
1023                    .next()
1024                    .unwrap_or(&raw_word)
1025                    .to_string();
1026                let full = raw_word.trim_start_matches('\\').to_string();
1027                (short, Some(full))
1028            } else {
1029                let fqn = imports.get(&raw_word).cloned();
1030                (raw_word, fqn)
1031            };
1032            let fqn = fqn_owned.as_deref();
1033            // First pass: open-file ParsedDocs give accurate character positions.
1034            let open_docs = self.docs.docs_for(&self.open_urls());
1035            let mut locs = find_implementations(&word, fqn, &open_docs);
1036            // Second pass: mir-backed subtype graph (fixes aliased/FQN extends),
1037            // falling back to the raw-name subtypes_of map when mir is cold.
1038            let wi = self.workspace_index_async().await;
1039            if locs.is_empty() {
1040                let fqn_for_mir = fqn.unwrap_or(&word).to_string();
1041                let subtype_urls = self.docs.class_subtype_urls(&fqn_for_mir);
1042                locs = find_implementations_mir_backed(&word, fqn, &wi, &subtype_urls);
1043            }
1044            // Third pass: treat word as a method name inside its declaring
1045            // class/interface — returns concrete overrides in every subtype.
1046            // Handles cursor on an interface method like `Factory::guard()`.
1047            if locs.is_empty()
1048                && let Some(doc) = self.get_doc(uri)
1049                && let Some(enclosing) =
1050                    crate::types::type_map::enclosing_class_at(&source, &doc, position)
1051            {
1052                // Resolve the enclosing class's FQN for mir's subtype graph.
1053                let enclosing_fqn = wi
1054                    .classes_by_name
1055                    .get(&enclosing)
1056                    .and_then(|refs| refs.first())
1057                    .and_then(|r| wi.at(*r))
1058                    .map(|(_, cls)| cls.fqn.as_ref().to_string());
1059                let method_subtype_urls = enclosing_fqn
1060                    .as_deref()
1061                    .map(|f| self.docs.class_subtype_urls(f))
1062                    .unwrap_or_default();
1063                locs = find_method_implementations_mir_backed(
1064                    &word,
1065                    &enclosing,
1066                    &wi,
1067                    &method_subtype_urls,
1068                );
1069            }
1070            if locs.is_empty() {
1071                Ok(None)
1072            } else {
1073                Ok(Some(GotoDefinitionResponse::Array(locs)))
1074            }
1075        })
1076        .await
1077    }
1078
1079    async fn goto_declaration(
1080        &self,
1081        params: tower_lsp::lsp_types::request::GotoDeclarationParams,
1082    ) -> Result<Option<tower_lsp::lsp_types::request::GotoDeclarationResponse>> {
1083        guard_async_result("goto_declaration", async move {
1084            let uri = &params.text_document_position_params.text_document.uri;
1085            let position = params.text_document_position_params.position;
1086            let source = self.get_open_text(uri).unwrap_or_default();
1087            // First pass: open-file ParsedDocs give accurate character positions.
1088            let open_docs = self.docs.docs_for(&self.open_urls());
1089            if let Some(loc) = goto_declaration(&source, &open_docs, position) {
1090                return Ok(Some(GotoDefinitionResponse::Scalar(loc)));
1091            }
1092            // Second pass: background files via FileIndex (line-only positions).
1093            let all_indexes = self.docs.all_indexes();
1094            Ok(goto_declaration_from_index(&source, &all_indexes, position)
1095                .map(GotoDefinitionResponse::Scalar))
1096        })
1097        .await
1098    }
1099
1100    async fn goto_type_definition(
1101        &self,
1102        params: tower_lsp::lsp_types::request::GotoTypeDefinitionParams,
1103    ) -> Result<Option<tower_lsp::lsp_types::request::GotoTypeDefinitionResponse>> {
1104        guard_async_result("goto_type_definition", async move {
1105            let uri = &params.text_document_position_params.text_document.uri;
1106            let position = params.text_document_position_params.position;
1107            let source = self.get_open_text(uri).unwrap_or_default();
1108            let doc = match self.get_doc(uri) {
1109                Some(d) => d,
1110                None => return Ok(None),
1111            };
1112            let analysis = self.cached_analysis_async(uri).await;
1113            // First pass: open-file ParsedDocs give accurate character positions.
1114            let open_docs = self.docs.docs_for(&self.open_urls());
1115            let mut results =
1116                goto_type_definition(&source, &doc, analysis.as_deref(), &open_docs, position);
1117
1118            // If no results from first pass, try background files via FileIndex (line-only positions).
1119            if results.is_empty() {
1120                let all_indexes = self.docs.all_indexes();
1121                results = goto_type_definition_from_index(
1122                    &source,
1123                    &doc,
1124                    analysis.as_deref(),
1125                    &all_indexes,
1126                    position,
1127                );
1128            }
1129
1130            // Format response: scalar for single result, array for multiple, none for empty
1131            let response = match results.len() {
1132                0 => None,
1133                1 => Some(GotoDefinitionResponse::Scalar(
1134                    results.into_iter().next().unwrap(),
1135                )),
1136                _ => Some(GotoDefinitionResponse::Array(results)),
1137            };
1138            Ok(response)
1139        })
1140        .await
1141    }
1142
1143    async fn prepare_type_hierarchy(
1144        &self,
1145        params: TypeHierarchyPrepareParams,
1146    ) -> Result<Option<Vec<TypeHierarchyItem>>> {
1147        guard_async_result("prepare_type_hierarchy", async move {
1148            let uri = &params.text_document_position_params.text_document.uri;
1149            let position = params.text_document_position_params.position;
1150            let source = self.get_open_text(uri).unwrap_or_default();
1151            // Phase J: use the salsa-memoized aggregate's `classes_by_name` map.
1152            let wi = self.workspace_index_async().await;
1153            Ok(
1154                prepare_type_hierarchy_from_workspace(&source, &wi, position)
1155                    .map(|item| vec![item]),
1156            )
1157        })
1158        .await
1159    }
1160
1161    async fn supertypes(
1162        &self,
1163        params: TypeHierarchySupertypesParams,
1164    ) -> Result<Option<Vec<TypeHierarchyItem>>> {
1165        guard_async_result("supertypes", async move {
1166            // Phase J: resolve parents via the aggregate's `classes_by_name` map.
1167            // Pre-load any direct vendor supertypes via PSR-4 so they appear in the
1168            // workspace index before the lookup runs.
1169            let wi = self.workspace_index_async().await;
1170            let loaded_new = self
1171                .ensure_direct_supertypes_loaded(&params.item.name, &wi)
1172                .await;
1173            let wi = if loaded_new {
1174                self.workspace_index_async().await
1175            } else {
1176                wi
1177            };
1178            let result = supertypes_of_from_workspace(&params.item, &wi);
1179            Ok(if result.is_empty() {
1180                None
1181            } else {
1182                Some(result)
1183            })
1184        })
1185        .await
1186    }
1187
1188    async fn subtypes(
1189        &self,
1190        params: TypeHierarchySubtypesParams,
1191    ) -> Result<Option<Vec<TypeHierarchyItem>>> {
1192        guard_async_result("subtypes", async move {
1193            let wi = self.workspace_index_async().await;
1194            // Resolve the item's FQCN for mir's subtype graph.
1195            let item_fqn = wi
1196                .classes_by_name
1197                .get(&params.item.name)
1198                .and_then(|refs| refs.first())
1199                .and_then(|r| wi.at(*r))
1200                .map(|(_, cls)| cls.fqn.as_ref().to_string());
1201            let subtype_urls = item_fqn
1202                .as_deref()
1203                .map(|f| self.docs.class_subtype_urls(f))
1204                .unwrap_or_default();
1205            let result =
1206                subtypes_of_mir_backed(&params.item, item_fqn.as_deref(), &wi, &subtype_urls);
1207            Ok(if result.is_empty() {
1208                None
1209            } else {
1210                Some(result)
1211            })
1212        })
1213        .await
1214    }
1215
1216    async fn code_lens(&self, params: CodeLensParams) -> Result<Option<Vec<CodeLens>>> {
1217        guard_async_result("code_lens", async move {
1218            let uri = &params.text_document.uri;
1219            let doc = match self.get_doc(uri) {
1220                Some(d) => d,
1221                None => return Ok(None),
1222            };
1223            // Reference-count lenses scan every doc per declaration; run the
1224            // whole computation on the blocking pool.
1225            let docs = Arc::clone(&self.docs);
1226            let uri_owned = uri.clone();
1227            let lenses = tokio::task::spawn_blocking(move || {
1228                let all_docs = docs.all_docs_for_scan();
1229                code_lenses(&uri_owned, &doc, &all_docs)
1230            })
1231            .await
1232            .unwrap_or_default();
1233            Ok(if lenses.is_empty() {
1234                None
1235            } else {
1236                Some(lenses)
1237            })
1238        })
1239        .await
1240    }
1241
1242    async fn code_lens_resolve(&self, params: CodeLens) -> Result<CodeLens> {
1243        let fallback = params.clone();
1244        let resolved = guard_async("code_lens_resolve", async move {
1245            // Lenses are fully populated by code_lens; nothing to add.
1246            Some(params)
1247        })
1248        .await;
1249        Ok(resolved.unwrap_or(fallback))
1250    }
1251
1252    async fn document_link(&self, params: DocumentLinkParams) -> Result<Option<Vec<DocumentLink>>> {
1253        guard_async_result("document_link", async move {
1254            let uri = &params.text_document.uri;
1255            let doc = match self.get_doc(uri) {
1256                Some(d) => d,
1257                None => return Ok(None),
1258            };
1259            let links = document_links(uri, &doc, doc.source());
1260            Ok(if links.is_empty() { None } else { Some(links) })
1261        })
1262        .await
1263    }
1264
1265    async fn document_link_resolve(&self, params: DocumentLink) -> Result<DocumentLink> {
1266        let fallback = params.clone();
1267        let resolved = guard_async("document_link_resolve", async move {
1268            // Links already carry their target URI; nothing to add.
1269            Some(params)
1270        })
1271        .await;
1272        Ok(resolved.unwrap_or(fallback))
1273    }
1274
1275    async fn formatting(&self, params: DocumentFormattingParams) -> Result<Option<Vec<TextEdit>>> {
1276        guard_async_result("formatting", async move {
1277            let uri = &params.text_document.uri;
1278            let source = self.get_open_text(uri).unwrap_or_default();
1279            Ok(format_document(&source))
1280        })
1281        .await
1282    }
1283
1284    async fn range_formatting(
1285        &self,
1286        params: DocumentRangeFormattingParams,
1287    ) -> Result<Option<Vec<TextEdit>>> {
1288        guard_async_result("range_formatting", async move {
1289            let uri = &params.text_document.uri;
1290            let source = self.get_open_text(uri).unwrap_or_default();
1291            Ok(format_range(&source, params.range))
1292        })
1293        .await
1294    }
1295
1296    async fn on_type_formatting(
1297        &self,
1298        params: DocumentOnTypeFormattingParams,
1299    ) -> Result<Option<Vec<TextEdit>>> {
1300        guard_async_result("on_type_formatting", async move {
1301            let uri = &params.text_document_position.text_document.uri;
1302            let source = self.get_open_text(uri).unwrap_or_default();
1303            let edits = on_type_format(
1304                &source,
1305                params.text_document_position.position,
1306                &params.ch,
1307                &params.options,
1308            );
1309            Ok(if edits.is_empty() { None } else { Some(edits) })
1310        })
1311        .await
1312    }
1313
1314    async fn execute_command(
1315        &self,
1316        params: ExecuteCommandParams,
1317    ) -> Result<Option<serde_json::Value>> {
1318        guard_async_result("execute_command", async move {
1319            match params.command.as_str() {
1320                "php-lsp.runTest" => {
1321                    // Arguments: [uri_string, "ClassName::methodName"]
1322                    let file_uri = params
1323                        .arguments
1324                        .first()
1325                        .and_then(|v| v.as_str())
1326                        .and_then(|s| Url::parse(s).ok());
1327                    let filter = params
1328                        .arguments
1329                        .get(1)
1330                        .and_then(|v| v.as_str())
1331                        .unwrap_or("")
1332                        .to_string();
1333
1334                    let root = self.root_paths.load().first().cloned();
1335                    let client = self.client.clone();
1336
1337                    tokio::spawn(async move {
1338                        run_phpunit(&client, &filter, root.as_deref(), file_uri.as_ref()).await;
1339                    });
1340
1341                    Ok(None)
1342                }
1343                _ => Ok(None),
1344            }
1345        })
1346        .await
1347    }
1348
1349    async fn will_rename_files(&self, params: RenameFilesParams) -> Result<Option<WorkspaceEdit>> {
1350        guard_async_result("will_rename_files", async move {
1351            self.handle_will_rename_files(params).await
1352        })
1353        .await
1354    }
1355
1356    async fn did_rename_files(&self, params: RenameFilesParams) {
1357        guard_async("did_rename_files", async move {
1358            self.handle_did_rename_files(params).await
1359        })
1360        .await
1361    }
1362
1363    async fn will_create_files(&self, params: CreateFilesParams) -> Result<Option<WorkspaceEdit>> {
1364        guard_async_result("will_create_files", async move {
1365            self.handle_will_create_files(params).await
1366        })
1367        .await
1368    }
1369
1370    async fn did_create_files(&self, params: CreateFilesParams) {
1371        guard_async("did_create_files", async move {
1372            self.handle_did_create_files(params).await
1373        })
1374        .await
1375    }
1376
1377    /// Before a file is deleted, return workspace edits that remove every
1378    /// `use` import referencing its PSR-4 class name.
1379    async fn will_delete_files(&self, params: DeleteFilesParams) -> Result<Option<WorkspaceEdit>> {
1380        guard_async_result("will_delete_files", async move {
1381            self.handle_will_delete_files(params).await
1382        })
1383        .await
1384    }
1385
1386    async fn did_delete_files(&self, params: DeleteFilesParams) {
1387        guard_async("did_delete_files", async move {
1388            self.handle_did_delete_files(params).await
1389        })
1390        .await
1391    }
1392
1393    async fn moniker(&self, params: MonikerParams) -> Result<Option<Vec<Moniker>>> {
1394        guard_async_result("moniker", async move {
1395            let uri = &params.text_document_position_params.text_document.uri;
1396            let position = params.text_document_position_params.position;
1397            let source = self.get_open_text(uri).unwrap_or_default();
1398            let doc = match self.get_doc(uri) {
1399                Some(d) => d,
1400                None => return Ok(None),
1401            };
1402            let imports = self.file_imports(uri);
1403            Ok(moniker_at(&source, &doc, position, &imports).map(|m| vec![m]))
1404        })
1405        .await
1406    }
1407
1408    async fn inline_value(&self, params: InlineValueParams) -> Result<Option<Vec<InlineValue>>> {
1409        guard_async_result("inline_value", async move {
1410            let uri = &params.text_document.uri;
1411            let source = self.get_open_text(uri).unwrap_or_default();
1412            let values = inline_values_in_range(&source, params.range);
1413            Ok(if values.is_empty() {
1414                None
1415            } else {
1416                Some(values)
1417            })
1418        })
1419        .await
1420    }
1421
1422    async fn diagnostic(
1423        &self,
1424        params: DocumentDiagnosticParams,
1425    ) -> Result<DocumentDiagnosticReportResult> {
1426        let report = guard_async("diagnostic", async move {
1427            Some(self.handle_diagnostic(params).await)
1428        })
1429        .await;
1430        match report {
1431            Some(r) => r,
1432            None => Ok(DocumentDiagnosticReportResult::Report(
1433                DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport::default()),
1434            )),
1435        }
1436    }
1437
1438    async fn workspace_diagnostic(
1439        &self,
1440        params: WorkspaceDiagnosticParams,
1441    ) -> Result<WorkspaceDiagnosticReportResult> {
1442        let report = guard_async("workspace_diagnostic", async move {
1443            Some(self.handle_workspace_diagnostic(params).await)
1444        })
1445        .await;
1446        match report {
1447            Some(r) => r,
1448            None => Ok(WorkspaceDiagnosticReportResult::Report(
1449                WorkspaceDiagnosticReport::default(),
1450            )),
1451        }
1452    }
1453
1454    async fn code_action(&self, params: CodeActionParams) -> Result<Option<CodeActionResponse>> {
1455        guard_async_result("code_action", async move {
1456            self.handle_code_action(params).await
1457        })
1458        .await
1459    }
1460
1461    async fn code_action_resolve(&self, item: CodeAction) -> Result<CodeAction> {
1462        guard_async_result("code_action_resolve", async move {
1463            self.handle_code_action_resolve(item).await
1464        })
1465        .await
1466    }
1467}