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