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