1use std::collections::HashMap;
2use std::sync::Arc;
3
4use tower_lsp_server::LanguageServer;
5use tower_lsp_server::jsonrpc::Result;
6use tower_lsp_server::ls_types::*;
7
8use super::DiagnosticPublish;
9use super::panic_guard::{guard_async, guard_async_result};
10use crate::analysis::callable_info::{callable_info_for_name, format_declared_params};
11use crate::completion::{CompletionCtx, filtered_completions_at};
12use crate::document::ast::ParsedDoc;
13use crate::document::open_files::compute_open_file_diagnostics;
14use crate::hover::{extract_static_class_before_cursor, hover_info_with_maps};
15use crate::index::file_index::ClassKind;
16use crate::index::workspace_scan::{scan_workspace, send_refresh_requests};
17use crate::lang::config::LspConfig;
18use crate::lang::is_unresolvable_bareword_at;
19use crate::navigation::symbols::{
20 document_symbols, resolve_workspace_symbol, workspace_symbols_from_workspace,
21};
22use crate::text::word_at_position;
23
24use crate::navigation::call_hierarchy::{
25 incoming_calls_indexed, outgoing_calls_via_mir, prepare_call_hierarchy_indexed,
26};
27use crate::navigation::declaration::{goto_declaration, goto_declaration_from_index};
28use crate::navigation::moniker::moniker_at;
29use crate::navigation::type_definition::type_class_fqns;
30use crate::navigation::type_hierarchy::{
31 prepare_type_hierarchy_from_fqn, subtypes_from_sites, supertypes_of_from_workspace,
32};
33
34use crate::analysis::code_lens::{code_lenses, subtype_site_to_location};
35use crate::analysis::diagnostics::{diagnostics_from_doc, parse_document, parse_document_no_diags};
36use crate::analysis::document_highlight::document_highlights;
37use crate::analysis::inlay_hints::inlay_hints;
38use crate::analysis::inline_value::inline_values_in_range;
39use crate::analysis::semantic_tokens::{
40 compute_token_delta, semantic_tokens, semantic_tokens_range, token_hash,
41};
42
43use crate::editing::document_link::document_links;
44use crate::editing::folding::folding_ranges;
45use crate::editing::formatting::{format_document, format_range};
46use crate::editing::on_type_format::on_type_format;
47use crate::editing::rename::{prepare_rename, rename_variable};
48use crate::editing::selection_range::selection_ranges;
49use crate::editing::signature_help::signature_help;
50
51use super::helpers::{cursor_is_on_property_decl, promoted_property_at_cursor, run_phpunit};
52use super::{Backend, publish_with_dependents};
53
54const WARM_RESWEEP_IDLE_MS: u64 = 1_000;
58
59fn normalized_php_fqn(name: &str) -> &str {
60 name.trim_start_matches('\\')
61}
62
63fn is_bare_php_name(name: &str) -> bool {
64 crate::text::fqn_short_name(name) == name
65}
66
67fn class_lookup_fqn(doc: &ParsedDoc, name: &str, imports: &HashMap<String, String>) -> String {
68 if is_bare_php_name(name) {
69 crate::navigation::moniker::resolve_fqn(doc, name, imports)
70 } else {
71 normalized_php_fqn(name).to_string()
72 }
73}
74
75fn inlay_hint_symbol_from_data(data: &serde_json::Value) -> Option<mir_analyzer::Name> {
76 let kind = data.get("php_lsp_symbol_kind")?.as_str()?;
77 let name = data.get("php_lsp_fn")?.as_str()?;
78 match kind {
79 "function" => Some(mir_analyzer::Name::function(name)),
80 "method" => {
81 let class = data.get("php_lsp_class")?.as_str()?;
82 Some(mir_analyzer::Name::method(class, name))
83 }
84 "class" => Some(mir_analyzer::Name::class(name)),
85 _ => None,
86 }
87}
88
89fn completion_symbol_from_item(
90 name: &str,
91 kind: Option<CompletionItemKind>,
92 class_hint: Option<&str>,
93) -> Option<mir_analyzer::Name> {
94 if let Some(class_name) = class_hint {
95 return Some(mir_analyzer::Name::method(class_name, name));
96 }
97 match kind {
98 Some(CompletionItemKind::FUNCTION) => Some(mir_analyzer::Name::function(name)),
99 Some(CompletionItemKind::CLASS)
100 | Some(CompletionItemKind::INTERFACE)
101 | Some(CompletionItemKind::ENUM) => Some(mir_analyzer::Name::class(name)),
102 _ => None,
103 }
104}
105
106fn hover_markup(value: String) -> Hover {
107 Hover {
108 contents: HoverContents::Markup(MarkupContent {
109 kind: MarkupKind::Markdown,
110 value,
111 }),
112 range: None,
113 }
114}
115
116fn hover_display_class_name(snapshot: &mir_analyzer::AnalysisSnapshot, name: &str) -> String {
117 let normalized = normalized_php_fqn(name);
118 if !is_bare_php_name(normalized) {
119 return normalized.to_string();
120 }
121 if let Ok(true) = snapshot.is_builtin_class(normalized) {
122 return format!("\\{normalized}");
123 }
124 normalized.to_string()
125}
126
127fn class_hover_for_fqcn(session: &mir_analyzer::AnalysisSnapshot, fqcn: &str) -> Option<Hover> {
128 let class = {
129 let db = session.db();
130 let here = mir_analyzer::db::Fqcn::from_str(db, fqcn);
131 mir_analyzer::db::find_class_like(db, here)?
132 };
133 let sig = match &class {
134 mir_analyzer::db::ClassLike::Class(c) => {
135 let mut sig = if c.is_abstract {
136 format!("abstract class {}", c.short_name)
137 } else if c.is_readonly {
138 format!("readonly class {}", c.short_name)
139 } else if c.is_final {
140 format!("final class {}", c.short_name)
141 } else {
142 format!("class {}", c.short_name)
143 };
144 if let Some(parent) = &c.parent {
145 sig.push_str(&format!(
146 " extends {}",
147 hover_display_class_name(session, parent)
148 ));
149 }
150 if !c.interfaces.is_empty() {
151 let list: Vec<String> = c
152 .interfaces
153 .iter()
154 .map(|s| hover_display_class_name(session, s))
155 .collect();
156 sig.push_str(&format!(" implements {}", list.join(", ")));
157 }
158 sig
159 }
160 mir_analyzer::db::ClassLike::Interface(i) => {
161 let mut sig = format!("interface {}", i.short_name);
162 if !i.extends.is_empty() {
163 let list: Vec<String> = i
164 .extends
165 .iter()
166 .map(|s| hover_display_class_name(session, s))
167 .collect();
168 sig.push_str(&format!(" extends {}", list.join(", ")));
169 }
170 sig
171 }
172 mir_analyzer::db::ClassLike::Trait(t) => format!("trait {}", t.short_name),
173 mir_analyzer::db::ClassLike::Enum(e) => {
174 let mut sig = if let Some(scalar) = &e.scalar_type {
175 format!("enum {}: {}", e.short_name, scalar)
176 } else {
177 format!("enum {}", e.short_name)
178 };
179 if !e.interfaces.is_empty() {
180 let list: Vec<String> = e
181 .interfaces
182 .iter()
183 .map(|s| hover_display_class_name(session, s))
184 .collect();
185 sig.push_str(&format!(" implements {}", list.join(", ")));
186 }
187 sig
188 }
189 };
190 Some(hover_markup(crate::hover::formatting::wrap_php(&sig)))
191}
192
193fn method_hover_for_fqcn(
194 session: &mir_analyzer::AnalysisSnapshot,
195 fqcn: &str,
196 method_name: &str,
197) -> Option<Hover> {
198 let db = session.db();
199 let (_, m) = mir_analyzer::db::find_method_in_chain(
200 db,
201 mir_analyzer::db::Fqcn::from_str(db, fqcn),
202 method_name,
203 )?;
204 let ret = m
205 .return_type
206 .as_deref()
207 .or(m.inferred_return_type.as_deref())
208 .map(|r| format!(": {}", r))
209 .unwrap_or_default();
210 let sig = format!(
211 "{}::{}({}){}",
212 crate::text::fqn_short_name(fqcn),
213 m.name,
214 format_declared_params(&m.params),
215 ret
216 );
217 let mut value = crate::hover::formatting::wrap_php(&sig);
218 if let Some(docstring) = m.docstring.as_deref().filter(|s| !s.is_empty()) {
219 value.push_str("\n\n---\n\n");
220 value.push_str(docstring);
221 }
222 Some(hover_markup(value))
223}
224
225fn callable_signature_for_symbol(
226 db: &mir_analyzer::db::MirDbStorage,
227 symbol: &mir_analyzer::Name,
228) -> Option<(String, Option<String>)> {
229 let info = callable_info_for_name(db, symbol)?;
230 match symbol {
231 mir_analyzer::Name::Function(fqn) => {
232 let short = fqn.rsplit('\\').next().unwrap_or(fqn.as_ref());
233 if mir_analyzer::is_builtin_function(short) {
234 return None;
235 }
236 let ret = info
237 .return_type
238 .as_deref()
239 .map(|ty| format!(": {ty}"))
240 .unwrap_or_default();
241 Some((
242 format!("function {}({}){}", short, info.params, ret),
243 info.documentation,
244 ))
245 }
246 mir_analyzer::Name::Method { class: _, name } => {
247 let ret = info
248 .return_type
249 .as_deref()
250 .map(|ty| format!(": {ty}"))
251 .unwrap_or_default();
252 Some((
253 format!("function {}({}){}", name, info.params, ret),
254 info.documentation,
255 ))
256 }
257 mir_analyzer::Name::Class(_) => Some((
258 format!("function __construct({})", info.params),
259 info.documentation,
260 )),
261 _ => None,
262 }
263}
264
265fn inlay_hint_tooltip_for_symbol(
266 db: &mir_analyzer::db::MirDbStorage,
267 symbol: &mir_analyzer::Name,
268) -> Option<String> {
269 let (signature, docstring) = callable_signature_for_symbol(db, symbol)?;
270 let mut value = format!("```php\n{signature}\n```");
271 if let Some(docstring) = docstring.filter(|s| !s.is_empty()) {
272 value.push_str("\n\n---\n\n");
273 value.push_str(&docstring);
274 }
275 Some(value)
276}
277
278fn completion_signature_and_docstring_for_symbol(
279 db: &mir_analyzer::db::MirDbStorage,
280 symbol: &mir_analyzer::Name,
281) -> (Option<String>, Option<String>) {
282 if let Some((signature, docstring)) = callable_signature_for_symbol(db, symbol) {
283 return (Some(signature), docstring.filter(|s| !s.is_empty()));
284 }
285 if let mir_analyzer::Name::Function(fqn) = symbol {
286 let short = fqn.rsplit('\\').next().unwrap_or(fqn.as_ref());
287 if mir_analyzer::is_builtin_function(short) {
288 return (
289 None,
290 Some(format!(
291 "```php\nfunction {}()\n```\n\n[php.net documentation]({})",
292 short,
293 crate::lang::php_names::php_doc_url(short)
294 )),
295 );
296 }
297 }
298 (None, None)
299}
300
301impl LanguageServer for Backend {
302 async fn initialize(&self, params: InitializeParams) -> Result<InitializeResult> {
303 self.handle_initialize(params).await
304 }
305
306 async fn initialized(&self, _params: InitializedParams) {
307 guard_async("initialized", async move {
308 self.handle_initialized(_params).await
309 })
310 .await
311 }
312
313 async fn did_change_configuration(&self, _params: DidChangeConfigurationParams) {
314 guard_async("did_change_configuration", async move {
315 let items = vec![ConfigurationItem {
318 scope_uri: None,
319 section: Some("php-lsp".to_string()),
320 }];
321 if let Ok(values) = self.client.configuration(items).await
322 && let Some(value) = values.into_iter().next()
323 {
324 let roots = self.root_paths.load_full();
325
326 let file_cfg = crate::lang::autoload::load_project_config_json(&roots);
329
330 if let Some(ver) = value.get("phpVersion").and_then(|v| v.as_str())
331 && !crate::lang::autoload::is_valid_php_version(ver)
332 {
333 self.client
334 .log_message(
335 tower_lsp_server::ls_types::MessageType::WARNING,
336 format!(
337 "php-lsp: unsupported phpVersion {ver:?} — valid values: {}",
338 crate::lang::autoload::SUPPORTED_PHP_VERSIONS.join(", ")
339 ),
340 )
341 .await;
342 }
343
344 let file_obj = file_cfg.as_ref().filter(|v| v.is_object());
345 let merged = LspConfig::merge_project_configs(file_obj, Some(&value));
346 let mut cfg = LspConfig::from_value(&merged);
347
348 let (ver, source) = self.resolve_php_version(cfg.php_version.as_deref());
350 self.client
351 .log_message(
352 tower_lsp_server::ls_types::MessageType::INFO,
353 format!("php-lsp: using PHP {ver} ({source})"),
354 )
355 .await;
356 let ver = if source != "set by editor"
358 && !crate::lang::autoload::is_valid_php_version(&ver)
359 {
360 let clamped = crate::lang::autoload::clamp_php_version(&ver);
361 self.client
362 .show_message(
363 tower_lsp_server::ls_types::MessageType::WARNING,
364 format!(
365 "php-lsp: detected PHP {ver} is outside the supported range ({}); \
366 using PHP {clamped} for analysis",
367 crate::lang::autoload::SUPPORTED_PHP_VERSIONS.join(", ")
368 ),
369 )
370 .await;
371 clamped.to_string()
372 } else {
373 ver
374 };
375 cfg.php_version = Some(ver.clone());
376 let version_changed = ver
377 .parse::<mir_analyzer::PhpVersion>()
378 .is_ok_and(|pv| self.docs.set_php_version(pv));
379 self.config.store(Arc::new(cfg));
380 if version_changed {
381 self.remirror_open_files();
389 self.rescan_roots_after_version_change(&roots).await;
390 }
391 send_refresh_requests(&self.client).await;
392 }
393 })
394 .await
395 }
396
397 async fn did_change_workspace_folders(&self, params: DidChangeWorkspaceFoldersParams) {
398 guard_async("did_change_workspace_folders", async move {
399 {
401 let mut roots = (**self.root_paths.load()).clone();
402 for removed in ¶ms.event.removed {
403 if let Some(path) = removed.uri.to_file_path() {
404 roots.retain(|r| r.as_path() != path.as_ref());
405 }
406 }
407 self.root_paths.store(Arc::new(roots));
408 }
409
410 let (exclude_paths, include_paths, max_indexed_files, cache_path) = {
412 let cfg = self.config.load();
413 let mut exclude = cfg.exclude_paths.clone();
414 if !cfg.index_vendor && !exclude.iter().any(|p| p == "vendor" || p == "vendor/") {
415 exclude.push("vendor/".to_string());
416 }
417 (
418 exclude,
419 cfg.include_paths.clone(),
420 cfg.max_indexed_files,
421 cfg.cache_path.clone(),
422 )
423 };
424 for added in ¶ms.event.added {
425 if let Some(path) = added.uri.to_file_path().map(|c| c.into_owned()) {
426 let is_new = {
427 let mut roots = (**self.root_paths.load()).clone();
428 if !roots.contains(&path) {
429 roots.push(path.clone());
430 self.root_paths.store(Arc::new(roots));
431 true
432 } else {
433 false
434 }
435 };
436 if is_new {
437 let docs = Arc::clone(&self.docs);
438 let open_files = self.open_files.clone();
439 let open_urls = open_files.urls();
440 let ex = exclude_paths.clone();
441 let ip = include_paths.clone();
442 let extensions = self.config.load().php_file_extensions();
443 let path_clone = path.clone();
444 let client = self.client.clone();
445 let cp = cache_path.clone();
446 let warm_analysis = self.config.load().warm_analysis;
447 tokio::spawn(async move {
448 let cache = if let Some(p) = cp {
449 Some(crate::index::cache::WorkspaceCache::with_dir(p))
450 } else {
451 crate::index::cache::WorkspaceCache::new(&path_clone)
452 };
453 scan_workspace(
454 path_clone,
455 Arc::clone(&docs),
456 open_files,
457 cache,
458 &ex,
459 &ip,
460 &extensions,
461 max_indexed_files,
462 None,
463 )
464 .await;
465 super::offload::run("workspaceFolders.warmStart", move || {
472 docs.get_workspace_index_salsa();
473 let untrusted = docs.warm_start_indexes();
474 docs.warm_references_phase(
475 untrusted,
476 &open_urls,
477 warm_analysis,
478 None,
479 );
480 })
481 .await;
482 send_refresh_requests(&client).await;
483 });
484 }
485 }
486 }
487 })
488 .await
489 }
490
491 async fn shutdown(&self) -> Result<()> {
492 let docs = Arc::clone(&self.docs);
495 self.blocking("shutdown.flush", move || docs.flush_analysis_cache())
496 .await;
497 Ok(())
498 }
499
500 #[tracing::instrument(skip_all)]
501 async fn did_open(&self, params: DidOpenTextDocumentParams) {
502 guard_async("did_open", async move {
503 let uri = params.text_document.uri;
504 let text = params.text_document.text;
505 let version = params.text_document.version;
506 let lifecycle_generation = super::lifecycle_generation();
507
508 if !self
512 .open_files
513 .should_apply_lifecycle(&uri, lifecycle_generation, true)
514 {
515 return;
516 }
517
518 self.set_open_text(uri.clone(), text);
522 self.docs.sync_analysis_cache_declarations(&uri);
526 let docs = Arc::clone(&self.docs);
530 let uri_for_task = uri.clone();
531 let parse_diags = self
532 .blocking_gated(super::debug_gate::GATE_DID_OPEN_PARSE, move || {
533 let target = docs.workspace_php_version();
534 docs.get_doc_salsa(&uri_for_task)
535 .map(|doc| diagnostics_from_doc(&doc, target))
536 .unwrap_or_default()
537 })
538 .await
539 .unwrap_or_default();
540 if !self
541 .open_files
542 .should_apply_lifecycle(&uri, lifecycle_generation, true)
543 {
544 return;
545 }
546 self.set_parse_diagnostics(&uri, parse_diags);
547
548 publish_with_dependents(
549 self.client.clone(),
550 Arc::clone(&self.docs),
551 self.open_files.clone(),
552 uri,
553 DiagnosticPublish::Open {
554 version,
555 generation: lifecycle_generation,
556 },
557 self.config.load().diagnostics.clone(),
558 self.laravel.load_full().is_laravel,
559 )
560 .await;
561 })
562 .await
563 }
564
565 #[tracing::instrument(skip_all)]
566 async fn did_change(&self, params: DidChangeTextDocumentParams) {
567 guard_async("did_change", async move {
568 let uri = params.text_document.uri;
569 let document_version = params.text_document.version;
570 let mut updated: Option<String> = None;
575 for change in params.content_changes {
576 match change.range {
577 None => updated = Some(change.text),
578 Some(range) => {
579 let mut cur = match updated.take() {
580 Some(t) => t,
581 None => self.get_open_text(&uri).unwrap_or_default().to_string(),
582 };
583 crate::text::apply_content_change(&mut cur, range, &change.text);
584 updated = Some(cur);
585 }
586 }
587 }
588 let Some(text) = updated else { return };
589
590 let version = self.set_open_text(uri.clone(), text.clone());
594
595 let docs = Arc::clone(&self.docs);
596 let open_files = self.open_files.clone();
597 let client = self.client.clone();
598 let cfg = self.config.load();
599 let diag_cfg = cfg.diagnostics.clone();
600 let debounce_ms = cfg.debounce_ms;
601 let warm_analysis = cfg.warm_analysis;
602 let is_laravel = self.laravel.load_full().is_laravel;
603 tokio::spawn(async move {
604 tokio::time::sleep(std::time::Duration::from_millis(debounce_ms)).await;
607
608 if open_files.current_version(&uri) != Some(version) {
612 return;
613 }
614
615 let parse_docs = Arc::clone(&docs);
616 let (_doc, parse_diags) = super::offload::run("did_change.parse", move || {
617 parse_document(&text, parse_docs.workspace_php_version())
618 })
619 .await
620 .unwrap_or_default();
621
622 if open_files.current_version(&uri) != Some(version) {
624 return;
625 }
626 open_files.set_parse_diagnostics(&uri, parse_diags);
627 publish_with_dependents(
628 client,
629 Arc::clone(&docs),
630 open_files.clone(),
631 uri.clone(),
632 DiagnosticPublish::Other(Some(document_version)),
633 diag_cfg,
634 is_laravel,
635 )
636 .await;
637
638 if warm_analysis && docs.is_index_ready() {
643 tokio::time::sleep(std::time::Duration::from_millis(WARM_RESWEEP_IDLE_MS))
644 .await;
645 if open_files.current_version(&uri) != Some(version) {
646 return;
647 }
648 let open_urls = open_files.urls();
649 drop(tokio::task::spawn_blocking(move || {
654 let cancel = docs.begin_warm_sweep();
655 docs.warm_analysis_sweep(&open_urls, &cancel);
656 }));
657 }
658 });
659 })
660 .await
661 }
662
663 async fn did_close(&self, params: DidCloseTextDocumentParams) {
664 guard_async("did_close", async move {
665 let uri = params.text_document.uri;
666 let lifecycle_generation = super::lifecycle_generation();
667 if !self
671 .open_files
672 .should_apply_lifecycle(&uri, lifecycle_generation, false)
673 {
674 return;
675 }
676 self.close_open_file(&uri);
677 let disk_path = uri.to_file_path().map(|path| path.into_owned());
682 let disk_text = self
683 .blocking_gated(super::debug_gate::GATE_DID_CLOSE_DISK_SYNC, move || {
684 disk_path.and_then(|path| std::fs::read_to_string(path).ok())
685 })
686 .await
687 .flatten();
688 if !self
689 .open_files
690 .should_apply_lifecycle(&uri, lifecycle_generation, false)
691 {
692 return;
693 }
694 if !self.open_files.contains(&uri) {
698 match disk_text {
699 Some(text) => self.docs.ingest(uri.clone(), &text),
700 None => self.docs.remove(&uri),
701 }
702 }
703 self.client.publish_diagnostics(uri, vec![], None).await;
705 })
706 .await
707 }
708
709 async fn will_save(&self, _params: WillSaveTextDocumentParams) {}
710
711 async fn will_save_wait_until(
712 &self,
713 params: WillSaveTextDocumentParams,
714 ) -> Result<Option<Vec<TextEdit>>> {
715 guard_async_result("will_save_wait_until", async move {
716 let source = self
717 .get_open_text(¶ms.text_document.uri)
718 .unwrap_or_default();
719 Ok(self
722 .blocking("willSaveWaitUntil.format", move || format_document(&source))
723 .await
724 .unwrap_or_default())
725 })
726 .await
727 }
728
729 async fn did_save(&self, params: DidSaveTextDocumentParams) {
730 guard_async("did_save", async move {
731 let uri = params.text_document.uri;
732 let diag_cfg = self.config.load().diagnostics.clone();
738 let is_laravel = self.laravel.load_full().is_laravel;
739 let docs = Arc::clone(&self.docs);
743 let open_files = self.open_files.clone();
744 let uri_for_diags = uri.clone();
745 let all = self
746 .blocking_gated(super::debug_gate::GATE_DID_SAVE_DIAGNOSTICS, move || {
747 compute_open_file_diagnostics(
748 &docs,
749 &open_files,
750 &uri_for_diags,
751 &diag_cfg,
752 is_laravel,
753 )
754 })
755 .await
756 .unwrap_or_default();
757 self.open_files
758 .note_published(&uri, super::diagnostics_content_hash(&all));
759 self.client
760 .publish_diagnostics(uri.clone(), all, None)
761 .await;
762
763 let cache_path = self.config.load().cache_path.clone();
768 if let Some(path) = uri.to_file_path().map(|c| c.into_owned()) {
769 let root = self.root_paths.load().first().cloned();
770 tokio::task::spawn_blocking({
774 let path = path.clone();
775 let uri = uri.clone();
776 let root = root.clone();
777 move || {
778 let cache = if let Some(p) = cache_path {
779 crate::index::cache::WorkspaceCache::with_dir(p)
780 } else if let Some(r) = &root {
781 let Some(c) = crate::index::cache::WorkspaceCache::new(r) else {
782 return;
783 };
784 c
785 } else {
786 return;
787 };
788 let Ok(text) = std::fs::read_to_string(&path) else {
789 return;
790 };
791 let key = crate::index::cache::WorkspaceCache::key_for(uri.as_str(), &text);
792 let doc = parse_document_no_diags(&text);
793 let index = crate::index::file_index::FileIndex::extract(&doc);
794 let _ = cache.write(&key, &index);
795 }
796 });
797
798 let ext_cfg = self.config.load().external_tools.clone();
806 if (ext_cfg.phpstan.enabled || ext_cfg.phpcs.enabled)
807 && let Some(version) = self.open_files.current_version(&uri)
808 {
809 let client = self.client.clone();
810 let docs = Arc::clone(&self.docs);
811 let open_files = self.open_files.clone();
812 let diag_cfg = self.config.load().diagnostics.clone();
813 let uri = uri.clone();
814 tokio::spawn(async move {
815 let diagnostics = crate::analysis::external::run_external_diagnostics(
816 &ext_cfg,
817 &path,
818 root.as_deref(),
819 )
820 .await;
821 if open_files.current_version(&uri) != Some(version) {
822 return;
823 }
824 open_files.set_external_diagnostics(&uri, version, diagnostics);
825 publish_with_dependents(
826 client,
827 docs,
828 open_files,
829 uri,
830 DiagnosticPublish::Other(None),
831 diag_cfg,
832 is_laravel,
833 )
834 .await;
835 });
836 }
837 }
838 })
839 .await
840 }
841
842 async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) {
843 guard_async("did_change_watched_files", async move {
844 enum Change {
845 Upsert(Uri, String),
846 Delete(Uri),
847 }
848 let mut changes = Vec::with_capacity(params.changes.len());
849 for change in params.changes {
850 match change.typ {
851 FileChangeType::CREATED | FileChangeType::CHANGED => {
852 if let Some(path) = change.uri.to_file_path()
853 && let Ok(text) = tokio::fs::read_to_string(&path).await
854 {
855 changes.push(Change::Upsert(change.uri.clone(), text));
856 }
857 }
858 FileChangeType::DELETED => changes.push(Change::Delete(change.uri.clone())),
859 _ => {}
860 }
861 }
862
863 let docs = Arc::clone(&self.docs);
868 let open_files = self.open_files.clone();
869 self.blocking_gated(
870 super::debug_gate::GATE_DID_CHANGE_WATCHED_FILES,
871 move || {
872 for change in changes {
873 match change {
874 Change::Upsert(uri, text) => {
875 let doc = parse_document_no_diags(&text);
880 if !open_files.contains(&uri) {
881 docs.ingest_from_doc(uri.clone(), &doc);
882 }
883 docs.sync_analysis_cache_declarations(&uri);
888 }
889 Change::Delete(uri) => docs.remove(&uri),
890 }
891 }
892 },
893 )
894 .await;
895 send_refresh_requests(&self.client).await;
897
898 if self.config.load().warm_analysis && self.docs.is_index_ready() {
902 let docs = Arc::clone(&self.docs);
903 let open_urls = self.open_files.urls();
904 drop(tokio::task::spawn_blocking(move || {
907 let cancel = docs.begin_warm_sweep();
908 docs.warm_analysis_sweep(&open_urls, &cancel);
909 }));
910 }
911 })
912 .await
913 }
914
915 #[tracing::instrument(skip_all)]
916 async fn completion(&self, params: CompletionParams) -> Result<Option<CompletionResponse>> {
917 guard_async_result("completion", async move {
918 let _interactive = self.docs.interactive_read_guard();
922 let uri = ¶ms.text_document_position.text_document.uri;
923 let position = params.text_document_position.position;
924 let cancel_rev = self.docs.write_rev();
925 let source = self.get_open_text(uri).unwrap_or_default();
926 let doc = match self.get_doc(uri) {
927 Some(d) => d,
928 None => return Ok(Some(CompletionResponse::Array(vec![]))),
929 };
930 let other_docs: Vec<Arc<ParsedDoc>> = self
931 .docs
932 .other_docs(uri, &self.open_urls())
933 .into_iter()
934 .map(|(_, d)| d)
935 .collect();
936 let trigger = params
938 .context
939 .as_ref()
940 .and_then(|c| c.trigger_character.clone());
941 let laravel_arc = self.laravel.load_full();
942 let imports = self.file_imports(uri);
943 let wi = self.workspace_index_async().await;
944 let wi_for_class_search = Arc::clone(&wi);
945 let uri_for_class_search = uri.clone();
946 let docs_for_lookup = Arc::clone(&self.docs);
947 let doc_for_lookup = Arc::clone(&doc);
948 let imports_for_lookup = imports.clone();
949 let find_class_doc_fn = move |name: &str| -> Option<Arc<ParsedDoc>> {
950 let fqn = class_lookup_fqn(&doc_for_lookup, name, &imports_for_lookup);
951 let cr = docs_for_lookup
952 .resolve_class_ref_by_fqn(&wi, &fqn)
953 .or_else(|| {
954 if !is_bare_php_name(name) {
955 return None;
956 }
957 let name_lc = name.to_lowercase();
958 let table = &wi.classes_by_lowercase_name;
959 let pos =
960 table.partition_point(|(short, _)| short.as_ref() < name_lc.as_str());
961 table.get(pos).and_then(|(short, cr)| {
962 (short.as_ref() == name_lc.as_str()).then_some(*cr)
963 })
964 })?;
965 let (uri, _) = wi.at(cr)?;
966 docs_for_lookup.get_doc_salsa(uri)
967 };
968 const WORKSPACE_CLASS_SEARCH_LIMIT: usize = 50;
979 let workspace_class_search_fn =
980 move |prefix: &str| -> Vec<(String, CompletionItemKind, String)> {
981 let prefix_lc = prefix.to_lowercase();
982 let table = &wi_for_class_search.classes_by_lowercase_name;
983 let start =
984 table.partition_point(|(name, _)| name.as_ref() < prefix_lc.as_str());
985 let mut out = Vec::new();
986 for (name, cr) in &table[start..] {
987 if !name.starts_with(prefix_lc.as_str()) {
988 break;
989 }
990 let Some((file_uri, cls)) = wi_for_class_search.at(*cr) else {
991 continue;
992 };
993 if *file_uri == uri_for_class_search {
994 continue;
995 }
996 let kind = match cls.kind {
997 ClassKind::Class | ClassKind::Trait => CompletionItemKind::CLASS,
998 ClassKind::Interface => CompletionItemKind::INTERFACE,
999 ClassKind::Enum => CompletionItemKind::ENUM,
1000 };
1001 out.push((cls.name.to_string(), kind, cls.fqn.to_string()));
1002 if out.len() >= WORKSPACE_CLASS_SEARCH_LIMIT {
1003 break;
1004 }
1005 }
1006 out
1007 };
1008 let analysis = self
1009 .cached_analysis_async_cancellable(uri, cancel_rev)
1010 .await;
1011 let docs = Arc::clone(&self.docs);
1012 let uri_owned = uri.clone();
1013 let items = self
1017 .blocking("completion", move || {
1018 let ctx = CompletionCtx {
1019 source: Some(&source),
1020 position: Some(position),
1021 doc_uri: Some(&uri_owned),
1022 file_imports: Some(&imports),
1023 find_class_doc: Some(&find_class_doc_fn),
1024 workspace_class_search: Some(&workspace_class_search_fn),
1025 analysis: analysis.as_deref(),
1026 session: Some(&docs),
1027 laravel: Some(&laravel_arc),
1028 };
1029 filtered_completions_at(&doc, &other_docs, trigger.as_deref(), &ctx)
1030 })
1031 .await
1032 .unwrap_or_default();
1033 Ok(Some(CompletionResponse::Array(items)))
1034 })
1035 .await
1036 }
1037
1038 async fn completion_resolve(&self, mut item: CompletionItem) -> Result<CompletionItem> {
1039 guard_async_result("completion_resolve", async move {
1040 if item.documentation.is_some() && item.detail.is_some() {
1041 return Ok(item);
1042 }
1043 let name = item.label.trim_end_matches(':').to_string();
1045 let class_hint = item
1049 .data
1050 .as_ref()
1051 .and_then(|d| d.get("class"))
1052 .and_then(|v| v.as_str())
1053 .map(str::to_string);
1054 let symbol = completion_symbol_from_item(&name, item.kind, class_hint.as_deref());
1055 let need_detail = item.detail.is_none();
1056 let need_docs = item.documentation.is_none();
1057 let docs = Arc::clone(&self.docs);
1058 let (detail, documentation) = self
1059 .blocking_gated(super::debug_gate::GATE_COMPLETION_RESOLVE, move || {
1060 let Some(symbol) = symbol.as_ref() else {
1061 return (None, None);
1062 };
1063 let (resolved_detail, resolved_docstring) = docs
1064 .read_snapshot(|snap| {
1065 completion_signature_and_docstring_for_symbol(snap.db(), symbol)
1066 })
1067 .unwrap_or_default();
1068 let detail = if need_detail {
1069 resolved_detail.clone()
1070 } else {
1071 None
1072 };
1073 let documentation = need_docs
1074 .then(|| {
1075 match (resolved_detail.as_deref(), resolved_docstring.as_deref()) {
1076 (Some(_), Some(doc)) if need_detail => Some(doc.to_string()),
1077 (Some(sig), Some(doc)) => {
1078 Some(format!("```php\n{sig}\n```\n\n---\n\n{doc}"))
1079 }
1080 (Some(sig), None) => Some(format!("```php\n{sig}\n```")),
1081 (None, Some(doc)) => Some(doc.to_string()),
1082 (None, None) => None,
1083 }
1084 })
1085 .flatten();
1086 (detail, documentation)
1087 })
1088 .await
1089 .unwrap_or_default();
1090 if let Some(sig) = detail {
1091 item.detail = Some(sig);
1092 }
1093 if let Some(md) = documentation {
1094 item.documentation = Some(Documentation::MarkupContent(MarkupContent {
1095 kind: MarkupKind::Markdown,
1096 value: md,
1097 }));
1098 }
1099 Ok(item)
1100 })
1101 .await
1102 }
1103
1104 async fn goto_definition(
1105 &self,
1106 params: GotoDefinitionParams,
1107 ) -> Result<Option<GotoDefinitionResponse>> {
1108 guard_async_result("goto_definition", async move {
1109 self.handle_goto_definition(params).await
1110 })
1111 .await
1112 }
1113
1114 async fn references(&self, params: ReferenceParams) -> Result<Option<Vec<Location>>> {
1115 guard_async_result(
1116 "references",
1117 async move { self.handle_references(params).await },
1118 )
1119 .await
1120 }
1121
1122 async fn prepare_rename(
1123 &self,
1124 params: TextDocumentPositionParams,
1125 ) -> Result<Option<PrepareRenameResponse>> {
1126 guard_async_result("prepare_rename", async move {
1127 let uri = ¶ms.text_document.uri;
1128 let doc = match self.get_doc(uri) {
1129 Some(d) => d,
1130 None => return Ok(None),
1131 };
1132 let position = params.position;
1133 let range = self
1137 .blocking_gated(super::debug_gate::GATE_PREPARE_RENAME, move || {
1138 prepare_rename(&doc, position)
1139 })
1140 .await
1141 .unwrap_or_default();
1142 Ok(range.map(PrepareRenameResponse::Range))
1143 })
1144 .await
1145 }
1146
1147 async fn rename(&self, params: RenameParams) -> Result<Option<WorkspaceEdit>> {
1148 guard_async_result("rename", async move {
1149 let uri = ¶ms.text_document_position.text_document.uri;
1150 let position = params.text_document_position.position;
1151 let source = self.get_open_text(uri).unwrap_or_default();
1152 let word = match word_at_position(&source, position) {
1153 Some(w) => w,
1154 None => return Ok(None),
1155 };
1156 if word.starts_with('$') {
1157 let doc = match self.get_doc(uri) {
1158 Some(d) => d,
1159 None => return Ok(None),
1160 };
1161 let word_task = word.clone();
1168 let new_name = params.new_name.clone();
1169 let uri_task = uri.clone();
1170 let doc_task = Arc::clone(&doc);
1171 let source_task = Arc::clone(&source);
1172 let variable_edit = self
1173 .blocking_gated(super::debug_gate::GATE_RENAME_VARIABLE, move || {
1174 let on_property_decl = cursor_is_on_property_decl(
1175 &source_task,
1176 &doc_task.program().stmts,
1177 position,
1178 )
1179 .is_some()
1180 || promoted_property_at_cursor(
1181 &source_task,
1182 &doc_task.program().stmts,
1183 position,
1184 )
1185 .is_some();
1186 if on_property_decl {
1187 None
1188 } else {
1189 Some(rename_variable(
1190 &word_task, &new_name, &uri_task, &doc_task, position,
1191 ))
1192 }
1193 })
1194 .await
1195 .flatten();
1196 if let Some(edit) = variable_edit {
1197 return Ok(Some(edit));
1198 }
1199 }
1200 Ok(self.indexed_rename(uri, position, ¶ms.new_name).await)
1204 })
1205 .await
1206 }
1207
1208 async fn signature_help(&self, params: SignatureHelpParams) -> Result<Option<SignatureHelp>> {
1209 guard_async_result("signature_help", async move {
1210 let uri = ¶ms.text_document_position_params.text_document.uri;
1211 let position = params.text_document_position_params.position;
1212 let source = self.get_open_text(uri).unwrap_or_default();
1213 let doc = match self.get_doc(uri) {
1214 Some(d) => d,
1215 None => return Ok(None),
1216 };
1217 let analysis = self.cached_analysis_async(uri).await;
1218 let docs = Arc::clone(&self.docs);
1219 let doc_clone = Arc::clone(&doc);
1220 let result = self
1221 .blocking("signature_help", move || {
1222 docs.read_snapshot(|snap| {
1223 signature_help(
1224 &source,
1225 &doc_clone,
1226 position,
1227 analysis.as_deref(),
1228 Some(snap.db()),
1229 )
1230 })
1231 .ok()
1232 })
1233 .await
1234 .flatten()
1235 .flatten();
1236 Ok(result)
1237 })
1238 .await
1239 }
1240
1241 #[tracing::instrument(skip_all)]
1242 async fn hover(&self, params: HoverParams) -> Result<Option<Hover>> {
1243 guard_async_result("hover", async move {
1244 let _interactive = self.docs.interactive_read_guard();
1248 let uri = ¶ms.text_document_position_params.text_document.uri;
1249 let position = params.text_document_position_params.position;
1250 let mut source = self.get_open_text(uri).unwrap_or_default();
1251 let doc = match self.get_doc(uri) {
1252 Some(d) => d,
1253 None => {
1254 tokio::task::yield_now().await;
1262 source = self.get_open_text(uri).unwrap_or_default();
1263 match self.get_doc(uri) {
1264 Some(d) => d,
1265 None => return Ok(None),
1266 }
1267 }
1268 };
1269 let laravel = self.laravel.load();
1274 let laravel_root = self.root_paths.load().first().cloned();
1275 let laravel_hover = crate::laravel::hover_for_string_key(
1276 &doc,
1277 position,
1278 &laravel,
1279 laravel_root.as_deref(),
1280 )
1281 .or_else(|| {
1282 crate::laravel::blade::hover(
1283 uri,
1284 &source,
1285 position,
1286 &laravel,
1287 laravel_root.as_deref(),
1288 )
1289 });
1290 drop(laravel);
1291 if let Some(hover) = laravel_hover {
1292 return Ok(Some(hover));
1293 }
1294 let open = self.open_urls();
1295 let other_docs = self.docs.other_docs(uri, &open);
1296 let other_maps = self.docs.other_symbol_maps(uri, &open);
1297 let analysis = self.cached_analysis_async(uri).await;
1298 let docs_for_hover = Arc::clone(&self.docs);
1299 let source_clone = source.clone();
1300 let doc_clone = Arc::clone(&doc);
1301 let mut wi_cache: Option<Arc<crate::db::workspace_index::WorkspaceIndexData>> = None;
1308 let wi = self.workspace_index_cached(&mut wi_cache).await;
1309 let docs_for_lookup = Arc::clone(&self.docs);
1310 let imports_for_lookup = doc.file_imports();
1311 let doc_for_lookup = Arc::clone(&doc);
1312 let find_class_doc_fn = move |name: &str| -> Option<Arc<ParsedDoc>> {
1313 let fqn = class_lookup_fqn(&doc_for_lookup, name, &imports_for_lookup);
1314 let cr = docs_for_lookup
1315 .resolve_class_ref_by_fqn(&wi, &fqn)
1316 .or_else(|| {
1317 if !is_bare_php_name(name) {
1318 return None;
1319 }
1320 let name_lc = name.to_lowercase();
1321 let table = &wi.classes_by_lowercase_name;
1322 let pos =
1323 table.partition_point(|(short, _)| short.as_ref() < name_lc.as_str());
1324 table.get(pos).and_then(|(short, cr)| {
1325 (short.as_ref() == name_lc.as_str()).then_some(*cr)
1326 })
1327 })?;
1328 let (uri, _) = wi.at(cr)?;
1329 docs_for_lookup.get_doc_salsa(uri)
1330 };
1331 let result = self
1332 .blocking("hover", move || {
1333 hover_info_with_maps(
1334 &source_clone,
1335 &doc_clone,
1336 analysis.as_deref(),
1337 position,
1338 &other_docs,
1339 &other_maps,
1340 Some(&docs_for_hover),
1341 Some(&find_class_doc_fn),
1342 )
1343 })
1344 .await
1345 .flatten();
1346 if result.is_some() {
1347 return Ok(result);
1348 }
1349 if let Some(word) = crate::text::word_at_position(&source, position) {
1354 let wi = self.workspace_index_cached(&mut wi_cache).await;
1355 let docs = Arc::clone(&self.docs);
1356 let doc = Arc::clone(&doc);
1357 let source = source.clone();
1358 let uri = uri.clone();
1359 let fallback = self
1361 .blocking("hover_fallback", move || {
1362 let fallback_imports = doc.file_imports();
1363 let fallback_class_fqcn = |name: &str| {
1364 let fqn = class_lookup_fqn(&doc, name, &fallback_imports);
1365 docs.resolve_class_ref_by_fqn(&wi, &fqn).and_then(|cr| {
1366 wi.at(cr)
1367 .map(|(_, cls)| normalized_php_fqn(&cls.fqn).to_string())
1368 })
1369 };
1370 if let Some(offset) = crate::text::word_range_at(&source, position)
1373 .map(|r| doc.view().byte_of_position(r.start))
1374 && let Ok(Some(mir_analyzer::Name::Class(fqcn))) =
1375 docs.mir_name_at(&uri, offset)
1376 && let Some(h) = docs
1377 .read_snapshot(|snap| class_hover_for_fqcn(snap, &fqcn))
1378 .ok()
1379 .flatten()
1380 {
1381 return Some(h);
1382 }
1383 if let Some(line_text) = source.lines().nth(position.line as usize)
1385 && let Some(class_token) = extract_static_class_before_cursor(
1386 line_text,
1387 position.character as usize,
1388 )
1389 {
1390 if let Some(fqcn) = fallback_class_fqcn(&class_token)
1391 && let Some(h) = docs
1392 .read_snapshot(|snap| method_hover_for_fqcn(snap, &fqcn, &word))
1393 .ok()
1394 .flatten()
1395 {
1396 return Some(h);
1397 }
1398 if let Some(resolved_class) =
1399 crate::hover::resolve_use_alias(&doc.program().stmts, &class_token)
1400 && let Some(fqcn) = fallback_class_fqcn(&resolved_class)
1401 && let Some(h) = docs
1402 .read_snapshot(|snap| method_hover_for_fqcn(snap, &fqcn, &word))
1403 .ok()
1404 .flatten()
1405 {
1406 return Some(h);
1407 }
1408 }
1409 None
1410 })
1411 .await
1412 .flatten();
1413 return Ok(fallback);
1414 }
1415 Ok(None)
1416 })
1417 .await
1418 }
1419
1420 async fn document_symbol(
1421 &self,
1422 params: DocumentSymbolParams,
1423 ) -> Result<Option<DocumentSymbolResponse>> {
1424 guard_async_result("document_symbol", async move {
1425 let uri = ¶ms.text_document.uri;
1426 let doc = match self.get_doc(uri) {
1427 Some(d) => d,
1428 None => return Ok(None),
1429 };
1430 let symbols = self
1432 .blocking_gated(super::debug_gate::GATE_DOCUMENT_SYMBOL, move || {
1433 document_symbols(doc.source(), &doc)
1434 })
1435 .await
1436 .unwrap_or_default();
1437 Ok(Some(DocumentSymbolResponse::Nested(symbols)))
1438 })
1439 .await
1440 }
1441
1442 async fn folding_range(&self, params: FoldingRangeParams) -> Result<Option<Vec<FoldingRange>>> {
1443 guard_async_result("folding_range", async move {
1444 let uri = ¶ms.text_document.uri;
1445 let doc = match self.get_doc(uri) {
1446 Some(d) => d,
1447 None => return Ok(None),
1448 };
1449 let ranges = self
1452 .blocking_gated(super::debug_gate::GATE_FOLDING_RANGE, move || {
1453 folding_ranges(doc.source(), &doc)
1454 })
1455 .await
1456 .unwrap_or_default();
1457 Ok(if ranges.is_empty() {
1458 None
1459 } else {
1460 Some(ranges)
1461 })
1462 })
1463 .await
1464 }
1465
1466 async fn inlay_hint(&self, params: InlayHintParams) -> Result<Option<Vec<InlayHint>>> {
1467 guard_async_result("inlay_hint", async move {
1468 let uri = ¶ms.text_document.uri;
1469 let doc = match self.get_doc(uri) {
1470 Some(d) => d,
1471 None => return Ok(None),
1472 };
1473 let analysis = self.cached_analysis_async(uri).await;
1474 let docs = Arc::clone(&self.docs);
1475 let hints = self
1476 .blocking("inlay_hint", move || {
1477 docs.read_snapshot(|snap| {
1478 inlay_hints(
1479 doc.source(),
1480 &doc,
1481 analysis.as_deref(),
1482 Some(snap.db()),
1483 params.range,
1484 )
1485 })
1486 .unwrap_or_default()
1487 })
1488 .await
1489 .unwrap_or_default();
1490 Ok(Some(hints))
1491 })
1492 .await
1493 }
1494
1495 async fn inlay_hint_resolve(&self, mut item: InlayHint) -> Result<InlayHint> {
1496 let fallback = item.clone();
1497 let resolved = guard_async("inlay_hint_resolve", async move {
1498 if item.tooltip.is_some() {
1499 return Some(item);
1500 }
1501 let symbol = item.data.as_ref().and_then(inlay_hint_symbol_from_data);
1502 if let Some(symbol) = symbol {
1503 let docs = Arc::clone(&self.docs);
1504 let tooltip = self
1505 .blocking_gated(super::debug_gate::GATE_INLAY_HINT_RESOLVE, move || {
1506 docs.read_snapshot(|snap| inlay_hint_tooltip_for_symbol(snap.db(), &symbol))
1507 .ok()
1508 })
1509 .await
1510 .flatten()
1511 .flatten();
1512 if let Some(md) = tooltip {
1513 item.tooltip = Some(InlayHintTooltip::MarkupContent(MarkupContent {
1514 kind: MarkupKind::Markdown,
1515 value: md,
1516 }));
1517 }
1518 }
1519 Some(item)
1520 })
1521 .await;
1522 Ok(resolved.unwrap_or(fallback))
1523 }
1524
1525 async fn symbol(
1526 &self,
1527 params: WorkspaceSymbolParams,
1528 ) -> Result<Option<WorkspaceSymbolResponse>> {
1529 guard_async_result("symbol", async move {
1530 let wi = self.workspace_index_async().await;
1534 let docs = Arc::clone(&self.docs);
1535 let query = params.query;
1536 let results = self
1537 .blocking("workspace/symbol", move || {
1538 let get_doc = |uri: &Uri| docs.get_doc_salsa(uri);
1539 workspace_symbols_from_workspace(&query, &wi, &get_doc)
1540 })
1541 .await
1542 .unwrap_or_default();
1543 Ok(Some(WorkspaceSymbolResponse::Flat(results)))
1544 })
1545 .await
1546 }
1547
1548 async fn symbol_resolve(&self, params: WorkspaceSymbol) -> Result<WorkspaceSymbol> {
1549 let docs = self.docs.docs_for(&self.open_urls());
1551 let fallback = params.clone();
1552 let resolved = guard_async("symbol_resolve", async move {
1553 Some(resolve_workspace_symbol(params, &docs))
1554 })
1555 .await;
1556 Ok(resolved.unwrap_or(fallback))
1557 }
1558
1559 #[tracing::instrument(skip_all)]
1560 async fn semantic_tokens_full(
1561 &self,
1562 params: SemanticTokensParams,
1563 ) -> Result<Option<SemanticTokensResult>> {
1564 guard_async_result("semantic_tokens_full", async move {
1565 let uri = params.text_document.uri;
1566 let doc = match self.get_doc(&uri) {
1567 Some(d) => d,
1568 None => {
1569 return Ok(Some(SemanticTokensResult::Tokens(SemanticTokens {
1570 result_id: None,
1571 data: vec![],
1572 })));
1573 }
1574 };
1575 let docs = Arc::clone(&self.docs);
1576 let uri_for_task = uri.clone();
1577 let (result_id, data) = self
1580 .blocking_gated(super::debug_gate::GATE_SEMANTIC_TOKENS_FULL, move || {
1581 let tokens = semantic_tokens(doc.source(), &doc);
1582 let result_id = token_hash(&tokens);
1583 let tokens_arc = Arc::new(tokens);
1584 docs.store_token_cache(
1585 &uri_for_task,
1586 result_id.clone(),
1587 Arc::clone(&tokens_arc),
1588 );
1589 let data = Arc::try_unwrap(tokens_arc).unwrap_or_else(|arc| (*arc).clone());
1590 (result_id, data)
1591 })
1592 .await
1593 .unwrap_or_default();
1594 Ok(Some(SemanticTokensResult::Tokens(SemanticTokens {
1595 result_id: Some(result_id),
1596 data,
1597 })))
1598 })
1599 .await
1600 }
1601
1602 async fn semantic_tokens_range(
1603 &self,
1604 params: SemanticTokensRangeParams,
1605 ) -> Result<Option<SemanticTokensRangeResult>> {
1606 guard_async_result("semantic_tokens_range", async move {
1607 let uri = params.text_document.uri;
1608 let doc = match self.get_doc(&uri) {
1609 Some(d) => d,
1610 None => {
1611 return Ok(Some(SemanticTokensRangeResult::Tokens(SemanticTokens {
1612 result_id: None,
1613 data: vec![],
1614 })));
1615 }
1616 };
1617 let range = params.range;
1618 let tokens = self
1619 .blocking_gated(super::debug_gate::GATE_SEMANTIC_TOKENS_RANGE, move || {
1620 semantic_tokens_range(doc.source(), &doc, range)
1621 })
1622 .await
1623 .unwrap_or_default();
1624 Ok(Some(SemanticTokensRangeResult::Tokens(SemanticTokens {
1625 result_id: None,
1626 data: tokens,
1627 })))
1628 })
1629 .await
1630 }
1631
1632 async fn semantic_tokens_full_delta(
1633 &self,
1634 params: SemanticTokensDeltaParams,
1635 ) -> Result<Option<SemanticTokensFullDeltaResult>> {
1636 guard_async_result("semantic_tokens_full_delta", async move {
1637 let uri = params.text_document.uri;
1638 let doc = match self.get_doc(&uri) {
1639 Some(d) => d,
1640 None => return Ok(None),
1641 };
1642
1643 let docs = Arc::clone(&self.docs);
1644 let uri_for_task = uri.clone();
1645 let prev_id = params.previous_result_id;
1646 let result = self
1647 .blocking_gated(
1648 super::debug_gate::GATE_SEMANTIC_TOKENS_FULL_DELTA,
1649 move || {
1650 let new_tokens = Arc::new(semantic_tokens(doc.source(), &doc));
1651 let new_result_id = token_hash(&new_tokens);
1652
1653 let result = match docs.get_token_cache(&uri_for_task, &prev_id) {
1654 Some(old_tokens) => {
1655 let edits = compute_token_delta(&old_tokens, &new_tokens);
1656 SemanticTokensFullDeltaResult::TokensDelta(SemanticTokensDelta {
1657 result_id: Some(new_result_id.clone()),
1658 edits,
1659 })
1660 }
1661 None => SemanticTokensFullDeltaResult::Tokens(SemanticTokens {
1663 result_id: Some(new_result_id.clone()),
1664 data: (*new_tokens).clone(),
1665 }),
1666 };
1667
1668 docs.store_token_cache(&uri_for_task, new_result_id, new_tokens);
1669 result
1670 },
1671 )
1672 .await;
1673 let Some(result) = result else {
1674 return Ok(None);
1675 };
1676 Ok(Some(result))
1677 })
1678 .await
1679 }
1680
1681 async fn selection_range(
1682 &self,
1683 params: SelectionRangeParams,
1684 ) -> Result<Option<Vec<SelectionRange>>> {
1685 guard_async_result("selection_range", async move {
1686 let uri = ¶ms.text_document.uri;
1687 let doc = match self.get_doc(uri) {
1688 Some(d) => d,
1689 None => return Ok(None),
1690 };
1691 let ranges = self
1695 .blocking_gated(super::debug_gate::GATE_SELECTION_RANGE, move || {
1696 selection_ranges(&doc, ¶ms.positions)
1697 })
1698 .await
1699 .unwrap_or_default();
1700 Ok(if ranges.is_empty() {
1701 None
1702 } else {
1703 Some(ranges)
1704 })
1705 })
1706 .await
1707 }
1708
1709 async fn prepare_call_hierarchy(
1710 &self,
1711 params: CallHierarchyPrepareParams,
1712 ) -> Result<Option<Vec<CallHierarchyItem>>> {
1713 guard_async_result("prepare_call_hierarchy", async move {
1714 let uri = ¶ms.text_document_position_params.text_document.uri;
1715 let position = params.text_document_position_params.position;
1716 let source = self.get_open_text(uri).unwrap_or_default();
1717 let word = match word_at_position(&source, position) {
1718 Some(w) => w,
1719 None => return Ok(None),
1720 };
1721 if is_unresolvable_bareword_at(&source, position, &word) {
1725 return Ok(None);
1726 }
1727 let wi = self.workspace_index_async().await;
1732 let docs = Arc::clone(&self.docs);
1733 let cursor_uri = uri.clone();
1734 let item = self
1735 .blocking_gated(super::debug_gate::GATE_PREPARE_CALL_HIERARCHY, move || {
1736 let get_doc = |u: &Uri| docs.get_doc_salsa(u);
1737 let mention_candidates =
1738 |name: &str| docs.declaration_candidate_files(&wi, name);
1739 prepare_call_hierarchy_indexed(
1740 &word,
1741 (&cursor_uri, position),
1742 &wi,
1743 &get_doc,
1744 &mention_candidates,
1745 )
1746 })
1747 .await
1748 .flatten();
1749 Ok(item.map(|item| vec![item]))
1750 })
1751 .await
1752 }
1753
1754 async fn incoming_calls(
1755 &self,
1756 params: CallHierarchyIncomingCallsParams,
1757 ) -> Result<Option<Vec<CallHierarchyIncomingCall>>> {
1758 guard_async_result("incoming_calls", async move {
1759 let docs = Arc::clone(&self.docs);
1762 let item = params.item;
1763 let calls = self
1764 .blocking("incoming_calls", move || {
1765 let (_interactive, cancel_rev) = docs.settled_write_rev_guard();
1768 incoming_calls_indexed(&item, &docs, Some(cancel_rev))
1769 })
1770 .await
1771 .unwrap_or_default();
1772 Ok(if calls.is_empty() { None } else { Some(calls) })
1773 })
1774 .await
1775 }
1776
1777 async fn outgoing_calls(
1778 &self,
1779 params: CallHierarchyOutgoingCallsParams,
1780 ) -> Result<Option<Vec<CallHierarchyOutgoingCall>>> {
1781 guard_async_result("outgoing_calls", async move {
1782 let docs = Arc::clone(&self.docs);
1783 let item = params.item;
1784 let calls = self
1785 .blocking("outgoing_calls", move || {
1786 outgoing_calls_via_mir(&docs, &item).unwrap_or_default()
1787 })
1788 .await
1789 .unwrap_or_default();
1790 Ok(if calls.is_empty() { None } else { Some(calls) })
1791 })
1792 .await
1793 }
1794
1795 async fn document_highlight(
1796 &self,
1797 params: DocumentHighlightParams,
1798 ) -> Result<Option<Vec<DocumentHighlight>>> {
1799 guard_async_result("document_highlight", async move {
1800 let uri = ¶ms.text_document_position_params.text_document.uri;
1801 let position = params.text_document_position_params.position;
1802 let source = self.get_open_text(uri).unwrap_or_default();
1803 let doc = match self.get_doc_stale(uri) {
1806 Some(d) => d,
1807 None => return Ok(None),
1808 };
1809 let highlights = self
1811 .blocking("document_highlight", move || {
1812 document_highlights(&source, &doc, position)
1813 })
1814 .await
1815 .unwrap_or_default();
1816 Ok(if highlights.is_empty() {
1817 None
1818 } else {
1819 Some(highlights)
1820 })
1821 })
1822 .await
1823 }
1824
1825 async fn linked_editing_range(
1826 &self,
1827 params: LinkedEditingRangeParams,
1828 ) -> Result<Option<LinkedEditingRanges>> {
1829 guard_async_result("linked_editing_range", async move {
1830 self.handle_linked_editing_range(params).await
1831 })
1832 .await
1833 }
1834
1835 async fn goto_implementation(
1836 &self,
1837 params: tower_lsp_server::ls_types::request::GotoImplementationParams,
1838 ) -> Result<Option<tower_lsp_server::ls_types::request::GotoImplementationResponse>> {
1839 guard_async_result("goto_implementation", async move {
1840 let uri = ¶ms.text_document_position_params.text_document.uri;
1841 let position = params.text_document_position_params.position;
1842 let source = self.get_open_text(uri).unwrap_or_default();
1843 let imports = self.file_imports(uri);
1844 let raw_word = crate::text::word_at_position(&source, position).unwrap_or_default();
1845 if raw_word.is_empty() || is_unresolvable_bareword_at(&source, position, &raw_word) {
1846 return Ok(None);
1847 }
1848 let (word, fqn): (String, String) = if is_bare_php_name(&raw_word) {
1851 let resolved = match self.get_doc(uri) {
1855 Some(doc) => crate::navigation::moniker::resolve_fqn(&doc, &raw_word, &imports),
1856 None => raw_word.clone(),
1857 };
1858 (raw_word, normalized_php_fqn(&resolved).to_string())
1859 } else {
1860 let short = crate::text::fqn_short_name(&raw_word).to_string();
1861 (short, normalized_php_fqn(&raw_word).to_string())
1862 };
1863
1864 let method_offset = self.get_doc(uri).and_then(|doc| {
1868 crate::text::word_range_at(&source, position)
1869 .map(|r| doc.view().byte_of_position(r.start))
1870 });
1871 let docs_for_method_check = Arc::clone(&self.docs);
1872 let uri_for_method_check = uri.clone();
1873 let on_method_decl = self
1874 .blocking_gated(super::debug_gate::GATE_GOTO_IMPLEMENTATION, move || {
1875 method_offset.is_some_and(|offset| {
1876 matches!(
1877 docs_for_method_check.mir_name_at(&uri_for_method_check, offset),
1878 Ok(Some(mir_analyzer::Name::Method { .. }))
1879 )
1880 })
1881 })
1882 .await
1883 .unwrap_or(false);
1884
1885 let mut locs: Vec<Location> = if on_method_decl {
1888 Vec::new()
1889 } else {
1890 let docs = Arc::clone(&self.docs);
1891 let fqn_task = fqn.clone();
1892 self.blocking("goto_implementation.subtypes", move || {
1893 docs.indexed_subtype_classes(&fqn_task, false)
1894 })
1895 .await
1896 .unwrap_or_else(|| Ok(Vec::new()))?
1897 .into_iter()
1898 .filter_map(|site| subtype_site_to_location(&site.file, &site.range))
1899 .collect()
1900 };
1901 if locs.is_empty() && !on_method_decl {
1902 let wi = self.workspace_index_async().await;
1903 let docs = Arc::clone(&self.docs);
1904 let target_fqn = fqn.clone();
1905 let target_short = word.clone();
1906 locs = self
1907 .blocking("goto_implementation.index_fallback", move || {
1908 let mention_candidates =
1909 |name: &str| docs.declaration_candidate_files(&wi, name);
1910 let get_doc = |uri: &Uri| docs.get_doc_salsa(uri);
1911 implementation_locations_from_index(
1912 &wi,
1913 &target_fqn,
1914 &target_short,
1915 &mention_candidates,
1916 &get_doc,
1917 )
1918 })
1919 .await
1920 .unwrap_or_default();
1921 }
1922
1923 if locs.is_empty()
1926 && let Some(doc) = self.get_doc(uri)
1927 && let Some(enclosing) =
1928 crate::types::type_map::enclosing_class_at(&source, &doc, position)
1929 {
1930 let resolved_enclosing =
1931 crate::navigation::moniker::resolve_fqn(&doc, &enclosing, &imports);
1932 let enclosing_fqn = normalized_php_fqn(&resolved_enclosing).to_string();
1933 let docs = Arc::clone(&self.docs);
1934 let method = word.clone();
1935 let impls = self
1936 .blocking("goto_implementation.method_impls", move || {
1937 docs.indexed_method_implementations(&enclosing_fqn, &method)
1938 })
1939 .await
1940 .unwrap_or_else(|| Ok(Vec::new()))?;
1941 locs = impls
1942 .into_iter()
1943 .filter_map(|(_, file, range)| subtype_site_to_location(&file, &range))
1944 .collect();
1945 }
1946 if locs.is_empty() {
1947 Ok(None)
1948 } else {
1949 Ok(Some(GotoDefinitionResponse::Array(locs)))
1950 }
1951 })
1952 .await
1953 }
1954
1955 async fn goto_declaration(
1956 &self,
1957 params: tower_lsp_server::ls_types::request::GotoDeclarationParams,
1958 ) -> Result<Option<tower_lsp_server::ls_types::request::GotoDeclarationResponse>> {
1959 guard_async_result("goto_declaration", async move {
1960 let uri = ¶ms.text_document_position_params.text_document.uri;
1961 let position = params.text_document_position_params.position;
1962 let source = self.get_open_text(uri).unwrap_or_default();
1963 if let Some(doc) = self.get_doc(uri)
1964 && let Some(offset) = crate::text::word_range_at(&source, position)
1965 .map(|range| doc.view().byte_of_position(range.start))
1966 {
1967 let docs = Arc::clone(&self.docs);
1968 let uri_task = uri.clone();
1969 let implemented = self
1970 .blocking("goto_declaration.implemented", move || {
1971 crate::navigation::mir_definition::mir_implemented_declaration(
1972 &docs, &uri_task, offset,
1973 )
1974 })
1975 .await;
1976 if let Some(Ok(Some(loc))) = implemented {
1977 return Ok(Some(GotoDefinitionResponse::Scalar(loc)));
1978 }
1979 }
1980 let open_docs = self.docs.docs_for(&self.open_urls());
1986 let source_for_pass1 = source.clone();
1987 let first_pass = self
1988 .blocking("goto_declaration.open_docs", move || {
1989 goto_declaration(&source_for_pass1, &open_docs, position)
1990 })
1991 .await
1992 .unwrap_or_default();
1993 if let Some(loc) = first_pass {
1994 return Ok(Some(GotoDefinitionResponse::Scalar(loc)));
1995 }
1996 let wi = self.workspace_index_async().await;
1999 let docs = Arc::clone(&self.docs);
2000 let second_pass = self
2001 .blocking("goto_declaration.index", move || {
2002 let mention_candidates =
2003 |name: &str| docs.declaration_candidate_files(&wi, name);
2004 let get_doc = |uri: &Uri| docs.get_doc_salsa(uri);
2005 goto_declaration_from_index(
2006 &source,
2007 &wi,
2008 position,
2009 &get_doc,
2010 &mention_candidates,
2011 )
2012 })
2013 .await
2014 .unwrap_or_default();
2015 Ok(second_pass.map(GotoDefinitionResponse::Scalar))
2016 })
2017 .await
2018 }
2019
2020 async fn goto_type_definition(
2021 &self,
2022 params: tower_lsp_server::ls_types::request::GotoTypeDefinitionParams,
2023 ) -> Result<Option<tower_lsp_server::ls_types::request::GotoTypeDefinitionResponse>> {
2024 guard_async_result("goto_type_definition", async move {
2025 let uri = ¶ms.text_document_position_params.text_document.uri;
2026 let position = params.text_document_position_params.position;
2027 let source = self.get_open_text(uri).unwrap_or_default();
2028 let doc = match self.get_doc(uri) {
2029 Some(d) => d,
2030 None => return Ok(None),
2031 };
2032 let analysis = self.cached_analysis_async(uri).await;
2033 let docs = Arc::clone(&self.docs);
2034
2035 let results = self
2036 .blocking_gated(super::debug_gate::GATE_GOTO_TYPE_DEFINITION, move || {
2037 let fqns = type_class_fqns(&source, &doc, analysis.as_deref(), position);
2038 crate::navigation::mir_definition::mir_class_locations(&docs, &fqns)
2039 })
2040 .await
2041 .unwrap_or_else(|| Ok(Vec::new()))?;
2042 Ok(match results.len() {
2043 0 => None,
2044 1 => Some(GotoDefinitionResponse::Scalar(
2045 results.into_iter().next().unwrap(),
2046 )),
2047 _ => Some(GotoDefinitionResponse::Array(results)),
2048 })
2049 })
2050 .await
2051 }
2052
2053 async fn prepare_type_hierarchy(
2054 &self,
2055 params: TypeHierarchyPrepareParams,
2056 ) -> Result<Option<Vec<TypeHierarchyItem>>> {
2057 guard_async_result("prepare_type_hierarchy", async move {
2058 let uri = ¶ms.text_document_position_params.text_document.uri;
2059 let position = params.text_document_position_params.position;
2060 let wi = self.workspace_index_async().await;
2061 let class_ref = self
2062 .get_doc(uri)
2063 .and_then(|doc| {
2064 word_at_position(doc.source(), position).map(|word| {
2065 let imports = doc.file_imports();
2066 crate::navigation::moniker::resolve_fqn(&doc, &word, &imports)
2067 })
2068 })
2069 .and_then(|fqn| self.docs.class_ref_by_fqn(&wi, &fqn));
2070 let item = self
2071 .blocking_gated(super::debug_gate::GATE_TYPE_HIERARCHY, move || {
2072 prepare_type_hierarchy_from_fqn(class_ref?, &wi)
2073 })
2074 .await
2075 .flatten();
2076 Ok(item.map(|item| vec![item]))
2077 })
2078 .await
2079 }
2080
2081 async fn supertypes(
2082 &self,
2083 params: TypeHierarchySupertypesParams,
2084 ) -> Result<Option<Vec<TypeHierarchyItem>>> {
2085 guard_async_result("supertypes", async move {
2086 let wi = self.workspace_index_async().await;
2090 let Some(item_fqn) = crate::navigation::type_hierarchy::item_fqn(¶ms.item) else {
2091 return Ok(None);
2092 };
2093 let loaded_new = self.ensure_direct_supertypes_loaded(item_fqn, &wi).await;
2094 let wi = if loaded_new {
2095 self.workspace_index_async().await
2096 } else {
2097 wi
2098 };
2099 let docs = Arc::clone(&self.docs);
2100 let item_fqn = item_fqn.to_owned();
2101 let result = self
2102 .blocking_gated(super::debug_gate::GATE_TYPE_HIERARCHY, move || {
2103 let supertype_fqns = docs.class_direct_supertypes(&item_fqn)?;
2104 let resolve_class_ref = |fqn: &str| docs.resolve_class_ref_by_fqn(&wi, fqn);
2105 Ok::<_, crate::document::document_store::ContentModified>(
2106 supertypes_of_from_workspace(&supertype_fqns, &wi, &resolve_class_ref),
2107 )
2108 })
2109 .await
2110 .unwrap_or_else(|| Ok(Vec::new()))?;
2111 Ok(if result.is_empty() {
2112 None
2113 } else {
2114 Some(result)
2115 })
2116 })
2117 .await
2118 }
2119
2120 async fn subtypes(
2121 &self,
2122 params: TypeHierarchySubtypesParams,
2123 ) -> Result<Option<Vec<TypeHierarchyItem>>> {
2124 guard_async_result("subtypes", async move {
2125 let docs = Arc::clone(&self.docs);
2126 let item = params.item;
2127 let Some(item_fqn) = crate::navigation::type_hierarchy::item_fqn(&item) else {
2128 return Ok(None);
2129 };
2130 let item_fqn = item_fqn.to_owned();
2131 let result = self
2132 .blocking_gated(super::debug_gate::GATE_TYPE_HIERARCHY, move || {
2133 let sites = docs.indexed_direct_subtype_classes(&item_fqn, true)?;
2134 Ok::<_, crate::document::document_store::ContentModified>(subtypes_from_sites(
2135 &sites,
2136 ))
2137 })
2138 .await
2139 .unwrap_or_else(|| Ok(Vec::new()))?;
2140 Ok(if result.is_empty() {
2141 None
2142 } else {
2143 Some(result)
2144 })
2145 })
2146 .await
2147 }
2148
2149 async fn code_lens(&self, params: CodeLensParams) -> Result<Option<Vec<CodeLens>>> {
2150 guard_async_result("code_lens", async move {
2151 let uri = ¶ms.text_document.uri;
2152 let doc = match self.get_doc(uri) {
2153 Some(d) => d,
2154 None => return Ok(None),
2155 };
2156 let docs = Arc::clone(&self.docs);
2162 let docs_cancel = Arc::clone(&self.docs);
2163 let uri_owned = uri.clone();
2164 let imports = self.file_imports(uri);
2165 let lenses = self
2166 .blocking("code_lens", move || {
2167 let (_interactive, cancel_rev) = docs.settled_write_rev_guard();
2170 code_lenses(
2171 &uri_owned,
2172 &doc,
2173 &docs,
2174 &imports,
2175 Some(cancel_rev),
2176 move || docs_cancel.write_rev() != cancel_rev,
2177 )
2178 })
2179 .await
2180 .unwrap_or_default();
2181 Ok(if lenses.is_empty() {
2182 None
2183 } else {
2184 Some(lenses)
2185 })
2186 })
2187 .await
2188 }
2189
2190 async fn code_lens_resolve(&self, params: CodeLens) -> Result<CodeLens> {
2191 let fallback = params.clone();
2192 let resolved = guard_async("code_lens_resolve", async move {
2193 Some(params)
2195 })
2196 .await;
2197 Ok(resolved.unwrap_or(fallback))
2198 }
2199
2200 async fn document_link(&self, params: DocumentLinkParams) -> Result<Option<Vec<DocumentLink>>> {
2201 guard_async_result("document_link", async move {
2202 let uri = params.text_document.uri;
2203 let doc = match self.get_doc(&uri) {
2204 Some(d) => d,
2205 None => return Ok(None),
2206 };
2207 let laravel = self.laravel.load_full();
2208 let links = self
2211 .blocking_gated(super::debug_gate::GATE_DOCUMENT_LINK, move || {
2212 let mut links = document_links(&uri, &doc, doc.source());
2213 links.extend(crate::laravel::document_links(&doc, &laravel));
2214 links.extend(crate::laravel::blade::document_links(
2215 &uri,
2216 doc.source(),
2217 &laravel,
2218 ));
2219 links
2220 })
2221 .await
2222 .unwrap_or_default();
2223 Ok(if links.is_empty() { None } else { Some(links) })
2224 })
2225 .await
2226 }
2227
2228 async fn document_link_resolve(&self, params: DocumentLink) -> Result<DocumentLink> {
2229 let fallback = params.clone();
2230 let resolved = guard_async("document_link_resolve", async move {
2231 Some(params)
2233 })
2234 .await;
2235 Ok(resolved.unwrap_or(fallback))
2236 }
2237
2238 async fn formatting(&self, params: DocumentFormattingParams) -> Result<Option<Vec<TextEdit>>> {
2239 guard_async_result("formatting", async move {
2240 let uri = ¶ms.text_document.uri;
2241 let source = self.get_open_text(uri).unwrap_or_default();
2242 Ok(self
2245 .blocking("formatting", move || format_document(&source))
2246 .await
2247 .unwrap_or_default())
2248 })
2249 .await
2250 }
2251
2252 async fn range_formatting(
2253 &self,
2254 params: DocumentRangeFormattingParams,
2255 ) -> Result<Option<Vec<TextEdit>>> {
2256 guard_async_result("range_formatting", async move {
2257 let uri = ¶ms.text_document.uri;
2258 let source = self.get_open_text(uri).unwrap_or_default();
2259 let range = params.range;
2260 Ok(self
2261 .blocking("range_formatting", move || format_range(&source, range))
2262 .await
2263 .unwrap_or_default())
2264 })
2265 .await
2266 }
2267
2268 async fn on_type_formatting(
2269 &self,
2270 params: DocumentOnTypeFormattingParams,
2271 ) -> Result<Option<Vec<TextEdit>>> {
2272 guard_async_result("on_type_formatting", async move {
2273 let uri = ¶ms.text_document_position.text_document.uri;
2274 let source = self.get_open_text(uri).unwrap_or_default();
2275 let position = params.text_document_position.position;
2276 let edits = self
2281 .blocking_gated(super::debug_gate::GATE_ON_TYPE_FORMATTING, move || {
2282 on_type_format(&source, position, ¶ms.ch, ¶ms.options)
2283 })
2284 .await
2285 .unwrap_or_default();
2286 Ok(if edits.is_empty() { None } else { Some(edits) })
2287 })
2288 .await
2289 }
2290
2291 async fn execute_command(
2292 &self,
2293 params: ExecuteCommandParams,
2294 ) -> Result<Option<serde_json::Value>> {
2295 guard_async_result("execute_command", async move {
2296 match params.command.as_str() {
2297 "php-lsp.runTest" => {
2298 let file_uri = params
2300 .arguments
2301 .first()
2302 .and_then(|v| v.as_str())
2303 .and_then(|s| (s).parse::<Uri>().ok());
2304 let filter = params
2305 .arguments
2306 .get(1)
2307 .and_then(|v| v.as_str())
2308 .unwrap_or("")
2309 .to_string();
2310
2311 let root = self.root_paths.load().first().cloned();
2312 let client = self.client.clone();
2313
2314 tokio::spawn(async move {
2315 run_phpunit(&client, &filter, root.as_deref(), file_uri.as_ref()).await;
2316 });
2317
2318 Ok(None)
2319 }
2320 _ => Ok(None),
2321 }
2322 })
2323 .await
2324 }
2325
2326 async fn will_rename_files(&self, params: RenameFilesParams) -> Result<Option<WorkspaceEdit>> {
2327 guard_async_result("will_rename_files", async move {
2328 self.handle_will_rename_files(params).await
2329 })
2330 .await
2331 }
2332
2333 async fn did_rename_files(&self, params: RenameFilesParams) {
2334 guard_async("did_rename_files", async move {
2335 self.handle_did_rename_files(params).await
2336 })
2337 .await
2338 }
2339
2340 async fn will_create_files(&self, params: CreateFilesParams) -> Result<Option<WorkspaceEdit>> {
2341 guard_async_result("will_create_files", async move {
2342 self.handle_will_create_files(params).await
2343 })
2344 .await
2345 }
2346
2347 async fn did_create_files(&self, params: CreateFilesParams) {
2348 guard_async("did_create_files", async move {
2349 self.handle_did_create_files(params).await
2350 })
2351 .await
2352 }
2353
2354 async fn will_delete_files(&self, params: DeleteFilesParams) -> Result<Option<WorkspaceEdit>> {
2357 guard_async_result("will_delete_files", async move {
2358 self.handle_will_delete_files(params).await
2359 })
2360 .await
2361 }
2362
2363 async fn did_delete_files(&self, params: DeleteFilesParams) {
2364 guard_async("did_delete_files", async move {
2365 self.handle_did_delete_files(params).await
2366 })
2367 .await
2368 }
2369
2370 async fn moniker(&self, params: MonikerParams) -> Result<Option<Vec<Moniker>>> {
2371 guard_async_result("moniker", async move {
2372 let uri = ¶ms.text_document_position_params.text_document.uri;
2373 let position = params.text_document_position_params.position;
2374 let source = self.get_open_text(uri).unwrap_or_default();
2375 let doc = match self.get_doc(uri) {
2376 Some(d) => d,
2377 None => return Ok(None),
2378 };
2379 let imports = self.file_imports(uri);
2380 let moniker = self
2381 .blocking_gated(super::debug_gate::GATE_MONIKER, move || {
2382 moniker_at(&source, &doc, position, &imports)
2383 })
2384 .await
2385 .flatten();
2386 Ok(moniker.map(|m| vec![m]))
2387 })
2388 .await
2389 }
2390
2391 async fn inline_value(&self, params: InlineValueParams) -> Result<Option<Vec<InlineValue>>> {
2392 guard_async_result("inline_value", async move {
2393 let uri = ¶ms.text_document.uri;
2394 let source = self.get_open_text(uri).unwrap_or_default();
2395 let range = params.range;
2396 let values = self
2397 .blocking_gated(super::debug_gate::GATE_INLINE_VALUE, move || {
2398 inline_values_in_range(&source, range)
2399 })
2400 .await
2401 .unwrap_or_default();
2402 Ok(if values.is_empty() {
2403 None
2404 } else {
2405 Some(values)
2406 })
2407 })
2408 .await
2409 }
2410
2411 async fn diagnostic(
2412 &self,
2413 params: DocumentDiagnosticParams,
2414 ) -> Result<DocumentDiagnosticReportResult> {
2415 let report = guard_async("diagnostic", async move {
2416 Some(self.handle_diagnostic(params).await)
2417 })
2418 .await;
2419 match report {
2420 Some(r) => r,
2421 None => Ok(DocumentDiagnosticReportResult::Report(
2422 DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport::default()),
2423 )),
2424 }
2425 }
2426
2427 async fn workspace_diagnostic(
2428 &self,
2429 params: WorkspaceDiagnosticParams,
2430 ) -> Result<WorkspaceDiagnosticReportResult> {
2431 let report = guard_async("workspace_diagnostic", async move {
2432 Some(self.handle_workspace_diagnostic(params).await)
2433 })
2434 .await;
2435 match report {
2436 Some(r) => r,
2437 None => Ok(WorkspaceDiagnosticReportResult::Report(
2438 WorkspaceDiagnosticReport::default(),
2439 )),
2440 }
2441 }
2442
2443 async fn code_action(&self, params: CodeActionParams) -> Result<Option<CodeActionResponse>> {
2444 guard_async_result("code_action", async move {
2445 self.handle_code_action(params).await
2446 })
2447 .await
2448 }
2449
2450 async fn code_action_resolve(&self, item: CodeAction) -> Result<CodeAction> {
2451 guard_async_result("code_action_resolve", async move {
2452 self.handle_code_action_resolve(item).await
2453 })
2454 .await
2455 }
2456}
2457
2458fn implementation_locations_from_index(
2469 wi: &crate::db::workspace_index::WorkspaceIndexData,
2470 target_fqn: &str,
2471 target_short: &str,
2472 mention_candidates: &dyn Fn(&str) -> Vec<Uri>,
2473 get_doc: &dyn Fn(&Uri) -> Option<Arc<ParsedDoc>>,
2474) -> Vec<Location> {
2475 let mut exact = Vec::new();
2476 let mut short_name = Vec::new();
2477 let candidate_uris = mention_candidates(target_short);
2478 wi.for_each_class_in_uris(&candidate_uris, |uri, cls| {
2479 let Some(doc) = get_doc(uri) else {
2480 return;
2481 };
2482 let imports = doc.file_imports();
2483 let mut saw_short_name_match = false;
2484 let mut relation_matches = |name: &str| {
2485 let resolved = crate::navigation::moniker::resolve_fqn(&doc, name, &imports);
2486 if normalized_php_fqn(&resolved).eq_ignore_ascii_case(target_fqn) {
2487 return true;
2488 }
2489 if crate::text::fqn_short_name(name).eq_ignore_ascii_case(target_short) {
2490 saw_short_name_match = true;
2491 }
2492 false
2493 };
2494 let exact_match = cls.parent.as_deref().is_some_and(&mut relation_matches)
2495 || cls
2496 .implements
2497 .iter()
2498 .any(|iface| relation_matches(iface.as_ref()));
2499
2500 let loc = class_index_location(uri, cls);
2501 if exact_match {
2502 exact.push(loc);
2503 } else if saw_short_name_match {
2504 short_name.push(loc);
2505 }
2506 });
2507
2508 let mut result = if exact.is_empty() { short_name } else { exact };
2509 result.sort_by(|a, b| {
2510 a.uri
2511 .as_str()
2512 .cmp(b.uri.as_str())
2513 .then(a.range.start.line.cmp(&b.range.start.line))
2514 .then(a.range.start.character.cmp(&b.range.start.character))
2515 });
2516 result.dedup_by(|a, b| a.uri == b.uri && a.range == b.range);
2517 result
2518}
2519
2520fn class_index_location(uri: &Uri, cls: &crate::index::file_index::ClassDef) -> Location {
2521 let start = Position {
2522 line: cls.start_line,
2523 character: cls.name_char,
2524 };
2525 Location {
2526 uri: uri.clone(),
2527 range: Range {
2528 start,
2529 end: Position {
2530 line: cls.start_line,
2531 character: cls.name_char + cls.name.len() as u32,
2532 },
2533 },
2534 }
2535}
2536
2537#[cfg(test)]
2538mod tests {
2539 use super::*;
2540
2541 #[test]
2545 fn class_hover_loads_builtin_interface_stub_without_deadlock() {
2546 let (tx, rx) = std::sync::mpsc::channel();
2547 std::thread::spawn(move || {
2548 let mut session = mir_analyzer::AnalysisSession::new(mir_analyzer::PhpVersion::LATEST);
2549 session.upsert_source_file(
2550 Arc::from("file:///foo.php"),
2551 Arc::from("<?php\nclass Foo implements Countable {\n public function count(): int { return 0; }\n}\n"),
2552 salsa::Durability::LOW,
2553 );
2554 let hover = class_hover_for_fqcn(&session.snapshot(), "Foo");
2555 let _ = tx.send(hover.map(|h| match h.contents {
2556 HoverContents::Markup(m) => m.value,
2557 _ => String::new(),
2558 }));
2559 });
2560 let value = rx
2561 .recv_timeout(std::time::Duration::from_secs(30))
2562 .expect("class hover deadlocked")
2563 .expect("class hover for Foo");
2564 assert!(value.contains("implements \\Countable"), "{value}");
2565 }
2566}