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