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