1use std::sync::Arc;
2
3use tower_lsp::jsonrpc::Result;
4use tower_lsp::lsp_types::*;
5use tower_lsp::{LanguageServer, async_trait};
6
7use super::panic_guard::{guard_async, guard_async_result};
8use crate::completion::{CompletionCtx, filtered_completions_at};
9use crate::document::ast::ParsedDoc;
10use crate::document::open_files::compute_open_file_diagnostics;
11use crate::hover::{
12 class_hover_from_index, docs_for_symbol_from_index, extract_static_class_before_cursor,
13 hover_info_with_maps, method_hover_from_index, signature_for_symbol_from_index,
14};
15use crate::index::workspace_scan::{scan_workspace, send_refresh_requests};
16use crate::lang::config::LspConfig;
17use crate::navigation::symbols::{
18 document_symbols, resolve_workspace_symbol, workspace_symbols_from_workspace,
19};
20use crate::text::word_at_position;
21
22use crate::navigation::call_hierarchy::{
23 incoming_calls, outgoing_calls_indexed, prepare_call_hierarchy_indexed,
24};
25use crate::navigation::declaration::{goto_declaration, goto_declaration_from_index};
26use crate::navigation::implementation::{
27 find_implementations, find_implementations_mir_backed, find_method_implementations_mir_backed,
28};
29use crate::navigation::moniker::moniker_at;
30use crate::navigation::type_definition::{goto_type_definition, goto_type_definition_from_index};
31use crate::navigation::type_hierarchy::{
32 prepare_type_hierarchy_from_workspace, subtypes_of_mir_backed, supertypes_of_from_workspace,
33};
34
35use crate::analysis::code_lens::code_lenses;
36use crate::analysis::diagnostics::{diagnostics_from_doc, parse_document, 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, rename_property, rename_variable};
49use crate::editing::selection_range::selection_ranges;
50use crate::editing::signature_help::signature_help;
51
52use super::helpers::{
53 cursor_is_on_property_decl, is_after_arrow, promoted_property_at_cursor, run_phpunit,
54};
55use super::{Backend, publish_with_dependents};
56
57#[async_trait]
58impl LanguageServer for Backend {
59 async fn initialize(&self, params: InitializeParams) -> Result<InitializeResult> {
60 self.handle_initialize(params).await
61 }
62
63 async fn initialized(&self, _params: InitializedParams) {
64 guard_async("initialized", async move {
65 self.handle_initialized(_params).await
66 })
67 .await
68 }
69
70 async fn did_change_configuration(&self, _params: DidChangeConfigurationParams) {
71 guard_async("did_change_configuration", async move {
72 let items = vec![ConfigurationItem {
75 scope_uri: None,
76 section: Some("php-lsp".to_string()),
77 }];
78 if let Ok(values) = self.client.configuration(items).await
79 && let Some(value) = values.into_iter().next()
80 {
81 let roots = self.root_paths.load_full();
82
83 let file_cfg = crate::lang::autoload::load_project_config_json(&roots);
86
87 if let Some(ver) = value.get("phpVersion").and_then(|v| v.as_str())
88 && !crate::lang::autoload::is_valid_php_version(ver)
89 {
90 self.client
91 .log_message(
92 tower_lsp::lsp_types::MessageType::WARNING,
93 format!(
94 "php-lsp: unsupported phpVersion {ver:?} — valid values: {}",
95 crate::lang::autoload::SUPPORTED_PHP_VERSIONS.join(", ")
96 ),
97 )
98 .await;
99 }
100
101 let file_obj = file_cfg.as_ref().filter(|v| v.is_object());
102 let merged = LspConfig::merge_project_configs(file_obj, Some(&value));
103 let mut cfg = LspConfig::from_value(&merged);
104
105 let (ver, source) = self.resolve_php_version(cfg.php_version.as_deref());
107 self.client
108 .log_message(
109 tower_lsp::lsp_types::MessageType::INFO,
110 format!("php-lsp: using PHP {ver} ({source})"),
111 )
112 .await;
113 let ver = if source != "set by editor"
115 && !crate::lang::autoload::is_valid_php_version(&ver)
116 {
117 let clamped = crate::lang::autoload::clamp_php_version(&ver);
118 self.client
119 .show_message(
120 tower_lsp::lsp_types::MessageType::WARNING,
121 format!(
122 "php-lsp: detected PHP {ver} is outside the supported range ({}); \
123 using PHP {clamped} for analysis",
124 crate::lang::autoload::SUPPORTED_PHP_VERSIONS.join(", ")
125 ),
126 )
127 .await;
128 clamped.to_string()
129 } else {
130 ver
131 };
132 cfg.php_version = Some(ver.clone());
133 if let Ok(pv) = ver.parse::<mir_analyzer::PhpVersion>() {
134 self.docs.set_php_version(pv);
135 }
136 self.config.store(Arc::new(cfg));
137 send_refresh_requests(&self.client).await;
138 }
139 })
140 .await
141 }
142
143 async fn did_change_workspace_folders(&self, params: DidChangeWorkspaceFoldersParams) {
144 guard_async("did_change_workspace_folders", async move {
145 {
147 let mut roots = (**self.root_paths.load()).clone();
148 for removed in ¶ms.event.removed {
149 if let Ok(path) = removed.uri.to_file_path() {
150 roots.retain(|r| r != &path);
151 }
152 }
153 self.root_paths.store(Arc::new(roots));
154 }
155
156 let (exclude_paths, include_paths, max_indexed_files) = {
158 let cfg = self.config.load();
159 (
160 cfg.exclude_paths.clone(),
161 cfg.include_paths.clone(),
162 cfg.max_indexed_files,
163 )
164 };
165 for added in ¶ms.event.added {
166 if let Ok(path) = added.uri.to_file_path() {
167 let is_new = {
168 let mut roots = (**self.root_paths.load()).clone();
169 if !roots.contains(&path) {
170 roots.push(path.clone());
171 self.root_paths.store(Arc::new(roots));
172 true
173 } else {
174 false
175 }
176 };
177 if is_new {
178 let docs = Arc::clone(&self.docs);
179 let open_files = self.open_files.clone();
180 let ex = exclude_paths.clone();
181 let ip = include_paths.clone();
182 let path_clone = path.clone();
183 let client = self.client.clone();
184 tokio::spawn(async move {
185 let cache = crate::index::cache::WorkspaceCache::new(&path_clone);
186 scan_workspace(
187 path_clone,
188 docs,
189 open_files,
190 cache,
191 &ex,
192 &ip,
193 max_indexed_files,
194 )
195 .await;
196 send_refresh_requests(&client).await;
197 });
198 }
199 }
200 }
201 })
202 .await
203 }
204
205 async fn shutdown(&self) -> Result<()> {
206 Ok(())
207 }
208
209 #[tracing::instrument(skip_all)]
210 async fn did_open(&self, params: DidOpenTextDocumentParams) {
211 guard_async("did_open", async move {
212 let uri = params.text_document.uri;
213 let text = params.text_document.text;
214
215 self.set_open_text(uri.clone(), text);
219
220 let parse_diags = self
223 .docs
224 .get_doc_salsa(&uri)
225 .map(|doc| diagnostics_from_doc(&doc))
226 .unwrap_or_default();
227 self.set_parse_diagnostics(&uri, parse_diags);
228
229 publish_with_dependents(
230 self.client.clone(),
231 Arc::clone(&self.docs),
232 self.open_files.clone(),
233 uri,
234 self.config.load().diagnostics.clone(),
235 )
236 .await;
237 })
238 .await
239 }
240
241 #[tracing::instrument(skip_all)]
242 async fn did_change(&self, params: DidChangeTextDocumentParams) {
243 guard_async("did_change", async move {
244 let uri = params.text_document.uri;
245 let mut updated: Option<String> = None;
250 for change in params.content_changes {
251 match change.range {
252 None => updated = Some(change.text),
253 Some(range) => {
254 let mut cur = match updated.take() {
255 Some(t) => t,
256 None => self.get_open_text(&uri).unwrap_or_default(),
257 };
258 crate::text::apply_content_change(&mut cur, range, &change.text);
259 updated = Some(cur);
260 }
261 }
262 }
263 let Some(text) = updated else { return };
264
265 let version = self.set_open_text(uri.clone(), text.clone());
269
270 let docs = Arc::clone(&self.docs);
271 let open_files = self.open_files.clone();
272 let client = self.client.clone();
273 let cfg = self.config.load();
274 let diag_cfg = cfg.diagnostics.clone();
275 let debounce_ms = cfg.debounce_ms;
276 tokio::spawn(async move {
277 tokio::time::sleep(std::time::Duration::from_millis(debounce_ms)).await;
280
281 if open_files.current_version(&uri) != Some(version) {
285 return;
286 }
287
288 let (_doc, parse_diags) =
289 tokio::task::spawn_blocking(move || parse_document(&text))
290 .await
291 .unwrap_or_else(|_| (ParsedDoc::default(), vec![]));
292
293 if open_files.current_version(&uri) == Some(version) {
295 open_files.set_parse_diagnostics(&uri, parse_diags);
296 publish_with_dependents(client, docs, open_files, uri, diag_cfg).await;
297 }
298 });
299 })
300 .await
301 }
302
303 async fn did_close(&self, params: DidCloseTextDocumentParams) {
304 guard_async("did_close", async move {
305 let uri = params.text_document.uri;
306 self.close_open_file(&uri);
307 let disk_text = match uri.to_file_path() {
312 Ok(path) => tokio::fs::read_to_string(&path).await.ok(),
313 Err(_) => None,
314 };
315 if !self.open_files.contains(&uri) {
319 match disk_text {
320 Some(text) => self.docs.ingest(uri.clone(), &text),
321 None => self.docs.remove(&uri),
322 }
323 }
324 self.client.publish_diagnostics(uri, vec![], None).await;
326 })
327 .await
328 }
329
330 async fn will_save(&self, _params: WillSaveTextDocumentParams) {}
331
332 async fn will_save_wait_until(
333 &self,
334 params: WillSaveTextDocumentParams,
335 ) -> Result<Option<Vec<TextEdit>>> {
336 guard_async_result("will_save_wait_until", async move {
337 let source = self
338 .get_open_text(¶ms.text_document.uri)
339 .unwrap_or_default();
340 Ok(format_document(&source))
341 })
342 .await
343 }
344
345 async fn did_save(&self, params: DidSaveTextDocumentParams) {
346 guard_async("did_save", async move {
347 let uri = params.text_document.uri;
348 let diag_cfg = self.config.load().diagnostics.clone();
354 let all = compute_open_file_diagnostics(&self.docs, &self.open_files, &uri, &diag_cfg);
355 self.client
356 .publish_diagnostics(uri.clone(), all, None)
357 .await;
358
359 if let (Some(root), Ok(path)) =
371 (self.root_paths.load().first().cloned(), uri.to_file_path())
372 {
373 tokio::task::spawn_blocking(move || {
374 let Some(cache) = crate::index::cache::WorkspaceCache::new(&root) else {
375 return;
376 };
377 let Ok(meta) = std::fs::metadata(&path) else {
380 return;
381 };
382 let Ok(text) = std::fs::read_to_string(&path) else {
383 return;
384 };
385 let mtime_secs = meta
386 .modified()
387 .ok()
388 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
389 .map(|d| d.as_secs())
390 .unwrap_or(0);
391 let key = crate::index::cache::WorkspaceCache::key_for_stat(
392 uri.as_str(),
393 mtime_secs,
394 meta.len(),
395 );
396 let doc = parse_document_no_diags(&text);
397 let index = crate::index::file_index::FileIndex::extract(&doc);
398 let _ = cache.write(&key, &index);
399 });
400 }
401 })
402 .await
403 }
404
405 async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) {
406 guard_async("did_change_watched_files", async move {
407 for change in params.changes {
408 match change.typ {
409 FileChangeType::CREATED | FileChangeType::CHANGED => {
410 if let Ok(path) = change.uri.to_file_path()
411 && let Ok(text) = tokio::fs::read_to_string(&path).await
412 {
413 let doc = parse_document_no_diags(&text);
418 self.ingest_from_doc_if_not_open(change.uri.clone(), &doc);
419 }
420 }
421 FileChangeType::DELETED => {
422 self.docs.remove(&change.uri);
423 }
424 _ => {}
425 }
426 }
427 send_refresh_requests(&self.client).await;
429 })
430 .await
431 }
432
433 #[tracing::instrument(skip_all)]
434 async fn completion(&self, params: CompletionParams) -> Result<Option<CompletionResponse>> {
435 guard_async_result("completion", async move {
436 let uri = ¶ms.text_document_position.text_document.uri;
437 let position = params.text_document_position.position;
438 let source = self.get_open_text(uri).unwrap_or_default();
439 let doc = match self.get_doc(uri) {
440 Some(d) => d,
441 None => return Ok(Some(CompletionResponse::Array(vec![]))),
442 };
443 let other_docs: Vec<Arc<ParsedDoc>> = self
444 .docs
445 .other_docs(uri, &self.open_urls())
446 .into_iter()
447 .map(|(_, d)| d)
448 .collect();
449 let trigger = params
450 .context
451 .as_ref()
452 .and_then(|c| c.trigger_character.as_deref());
453 let meta_loaded = self.meta.load();
454 let meta_opt = if meta_loaded.is_empty() {
455 None
456 } else {
457 Some(&**meta_loaded)
458 };
459 let imports = self.file_imports(uri);
460 let wi = self.workspace_index_async().await;
461 let docs_for_lookup = Arc::clone(&self.docs);
462 let find_class_doc_fn = move |name: &str| -> Option<Arc<ParsedDoc>> {
463 let cr = *wi.classes_by_name.get(name)?.first()?;
464 let (uri, _) = wi.at(cr)?;
465 docs_for_lookup.get_doc_salsa(uri)
466 };
467 let analysis = self.cached_analysis_async(uri).await;
468 let docs_for_tm = Arc::clone(&self.docs);
472 let doc_for_tm = Arc::clone(&doc);
473 let uri_for_tm = uri.clone();
474 let get_type_map =
475 move || docs_for_tm.cached_type_map(&uri_for_tm, &doc_for_tm, meta_opt);
476 let session = self
477 .docs
478 .analysis_session(self.docs.workspace_php_version());
479 let ctx = CompletionCtx {
480 source: Some(&source),
481 position: Some(position),
482 meta: meta_opt,
483 doc_uri: Some(uri),
484 file_imports: Some(&imports),
485 find_class_doc: Some(&find_class_doc_fn),
486 analysis: analysis.as_deref(),
487 type_map: Some(&get_type_map),
488 session: Some(session),
489 };
490 Ok(Some(CompletionResponse::Array(filtered_completions_at(
491 &doc,
492 &other_docs,
493 trigger,
494 &ctx,
495 ))))
496 })
497 .await
498 }
499
500 async fn completion_resolve(&self, mut item: CompletionItem) -> Result<CompletionItem> {
501 guard_async_result("completion_resolve", async move {
502 if item.documentation.is_some() && item.detail.is_some() {
503 return Ok(item);
504 }
505 let name = item.label.trim_end_matches(':');
507 let all_indexes = self.docs.all_indexes();
508 if item.detail.is_none()
509 && let Some(sig) = signature_for_symbol_from_index(name, &all_indexes)
510 {
511 item.detail = Some(sig);
512 }
513 if item.documentation.is_none()
514 && let Some(md) = docs_for_symbol_from_index(name, &all_indexes)
515 {
516 item.documentation = Some(Documentation::MarkupContent(MarkupContent {
517 kind: MarkupKind::Markdown,
518 value: md,
519 }));
520 }
521 Ok(item)
522 })
523 .await
524 }
525
526 async fn goto_definition(
527 &self,
528 params: GotoDefinitionParams,
529 ) -> Result<Option<GotoDefinitionResponse>> {
530 guard_async_result("goto_definition", async move {
531 self.handle_goto_definition(params).await
532 })
533 .await
534 }
535
536 async fn references(&self, params: ReferenceParams) -> Result<Option<Vec<Location>>> {
537 guard_async_result(
538 "references",
539 async move { self.handle_references(params).await },
540 )
541 .await
542 }
543
544 async fn prepare_rename(
545 &self,
546 params: TextDocumentPositionParams,
547 ) -> Result<Option<PrepareRenameResponse>> {
548 guard_async_result("prepare_rename", async move {
549 let uri = ¶ms.text_document.uri;
550 let source = self.get_open_text(uri).unwrap_or_default();
551 Ok(prepare_rename(&source, params.position).map(PrepareRenameResponse::Range))
552 })
553 .await
554 }
555
556 async fn rename(&self, params: RenameParams) -> Result<Option<WorkspaceEdit>> {
557 guard_async_result("rename", async move {
558 let uri = ¶ms.text_document_position.text_document.uri;
559 let position = params.text_document_position.position;
560 let source = self.get_open_text(uri).unwrap_or_default();
561 let word = match word_at_position(&source, position) {
562 Some(w) => w,
563 None => return Ok(None),
564 };
565 if word.starts_with('$') {
566 let doc = match self.get_doc(uri) {
567 Some(d) => d,
568 None => return Ok(None),
569 };
570 let prop_name = cursor_is_on_property_decl(&source, &doc.program().stmts, position)
574 .or_else(|| {
575 promoted_property_at_cursor(&source, &doc.program().stmts, position)
576 });
577 if let Some(prop_name) = prop_name {
578 let all_docs = self.docs.all_docs_for_scan();
579 return Ok(Some(rename_property(
580 &prop_name,
581 ¶ms.new_name,
582 &all_docs,
583 )));
584 }
585 Ok(Some(rename_variable(
586 &word,
587 ¶ms.new_name,
588 uri,
589 &doc,
590 position,
591 )))
592 } else if is_after_arrow(&source, position) {
593 let all_docs = self.docs.all_docs_for_scan();
594 Ok(Some(rename_property(&word, ¶ms.new_name, &all_docs)))
595 } else {
596 let all_docs = self.docs.all_docs_for_scan();
597 let doc_opt = self.get_doc(uri);
598 let target_fqn: Option<String> = doc_opt.as_ref().map(|doc| {
599 let imports = self.file_imports(uri);
600 crate::navigation::moniker::resolve_fqn(doc, &word, &imports)
601 });
602 Ok(Some(rename(
603 &word,
604 ¶ms.new_name,
605 &all_docs,
606 target_fqn.as_deref(),
607 )))
608 }
609 })
610 .await
611 }
612
613 async fn signature_help(&self, params: SignatureHelpParams) -> Result<Option<SignatureHelp>> {
614 guard_async_result("signature_help", async move {
615 let uri = ¶ms.text_document_position_params.text_document.uri;
616 let position = params.text_document_position_params.position;
617 let source = self.get_open_text(uri).unwrap_or_default();
618 let doc = match self.get_doc(uri) {
619 Some(d) => d,
620 None => return Ok(None),
621 };
622 let all_indexes = self.docs.all_indexes();
623 Ok(signature_help(&source, &doc, position, &all_indexes))
624 })
625 .await
626 }
627
628 #[tracing::instrument(skip_all)]
629 async fn hover(&self, params: HoverParams) -> Result<Option<Hover>> {
630 guard_async_result("hover", async move {
631 let uri = ¶ms.text_document_position_params.text_document.uri;
632 let position = params.text_document_position_params.position;
633 let source = self.get_open_text(uri).unwrap_or_default();
634 let doc = match self.get_doc(uri) {
635 Some(d) => d,
636 None => return Ok(None),
637 };
638 let other_docs = self.docs.other_docs(uri, &self.open_urls());
639 let other_maps = self.docs.other_symbol_maps(uri, &self.open_urls());
640 let analysis = self.cached_analysis_async(uri).await;
641 let hover_session = self
642 .docs
643 .analysis_session(self.docs.workspace_php_version());
644 let result = hover_info_with_maps(
645 &source,
646 &doc,
647 analysis.as_deref(),
648 position,
649 &other_docs,
650 &other_maps,
651 Some(&hover_session),
652 );
653 if result.is_some() {
654 return Ok(result);
655 }
656 if let Some(word) = crate::text::word_at_position(&source, position) {
661 let wi = self.workspace_index_async().await;
662 if let Some(h) = class_hover_from_index(&word, &wi.files) {
664 return Ok(Some(h));
665 }
666 if let Some(resolved) = crate::hover::resolve_use_alias(&doc.program().stmts, &word)
668 && let Some(h) = class_hover_from_index(&resolved, &wi.files)
669 {
670 return Ok(Some(h));
671 }
672 if let Some(line_text) = source.lines().nth(position.line as usize)
674 && let Some(class_token) =
675 extract_static_class_before_cursor(line_text, position.character as usize)
676 {
677 if let Some(h) = method_hover_from_index(&class_token, &word, &wi.files) {
678 return Ok(Some(h));
679 }
680 if let Some(resolved_class) =
681 crate::hover::resolve_use_alias(&doc.program().stmts, &class_token)
682 && let Some(h) = method_hover_from_index(&resolved_class, &word, &wi.files)
683 {
684 return Ok(Some(h));
685 }
686 }
687 }
688 Ok(None)
689 })
690 .await
691 }
692
693 async fn document_symbol(
694 &self,
695 params: DocumentSymbolParams,
696 ) -> Result<Option<DocumentSymbolResponse>> {
697 guard_async_result("document_symbol", async move {
698 let uri = ¶ms.text_document.uri;
699 let doc = match self.get_doc(uri) {
700 Some(d) => d,
701 None => return Ok(None),
702 };
703 Ok(Some(DocumentSymbolResponse::Nested(document_symbols(
704 doc.source(),
705 &doc,
706 ))))
707 })
708 .await
709 }
710
711 async fn folding_range(&self, params: FoldingRangeParams) -> Result<Option<Vec<FoldingRange>>> {
712 guard_async_result("folding_range", async move {
713 let uri = ¶ms.text_document.uri;
714 let doc = match self.get_doc(uri) {
715 Some(d) => d,
716 None => return Ok(None),
717 };
718 let ranges = folding_ranges(doc.source(), &doc);
719 Ok(if ranges.is_empty() {
720 None
721 } else {
722 Some(ranges)
723 })
724 })
725 .await
726 }
727
728 async fn inlay_hint(&self, params: InlayHintParams) -> Result<Option<Vec<InlayHint>>> {
729 guard_async_result("inlay_hint", async move {
730 let uri = ¶ms.text_document.uri;
731 let doc = match self.get_doc(uri) {
732 Some(d) => d,
733 None => return Ok(None),
734 };
735 let analysis = self.cached_analysis_async(uri).await;
736 let wi = self.workspace_index_async().await;
737 Ok(Some(inlay_hints(
738 doc.source(),
739 &doc,
740 analysis.as_deref(),
741 params.range,
742 &wi.files,
743 )))
744 })
745 .await
746 }
747
748 async fn inlay_hint_resolve(&self, mut item: InlayHint) -> Result<InlayHint> {
749 let fallback = item.clone();
750 let resolved = guard_async("inlay_hint_resolve", async move {
751 if item.tooltip.is_some() {
752 return Some(item);
753 }
754 let func_name = item
755 .data
756 .as_ref()
757 .and_then(|d| d.get("php_lsp_fn"))
758 .and_then(|v| v.as_str())
759 .map(str::to_string);
760 if let Some(name) = func_name {
761 let all_indexes = self.docs.all_indexes();
762 if let Some(md) = docs_for_symbol_from_index(&name, &all_indexes) {
763 item.tooltip = Some(InlayHintTooltip::MarkupContent(MarkupContent {
764 kind: MarkupKind::Markdown,
765 value: md,
766 }));
767 }
768 }
769 Some(item)
770 })
771 .await;
772 Ok(resolved.unwrap_or(fallback))
773 }
774
775 async fn symbol(
776 &self,
777 params: WorkspaceSymbolParams,
778 ) -> Result<Option<Vec<SymbolInformation>>> {
779 guard_async_result("symbol", async move {
780 let wi = self.workspace_index_async().await;
784 let results = workspace_symbols_from_workspace(¶ms.query, &wi);
785 Ok(Some(results))
786 })
787 .await
788 }
789
790 async fn symbol_resolve(&self, params: WorkspaceSymbol) -> Result<WorkspaceSymbol> {
791 let docs = self.docs.docs_for(&self.open_urls());
793 let fallback = params.clone();
794 let resolved = guard_async("symbol_resolve", async move {
795 Some(resolve_workspace_symbol(params, &docs))
796 })
797 .await;
798 Ok(resolved.unwrap_or(fallback))
799 }
800
801 #[tracing::instrument(skip_all)]
802 async fn semantic_tokens_full(
803 &self,
804 params: SemanticTokensParams,
805 ) -> Result<Option<SemanticTokensResult>> {
806 guard_async_result("semantic_tokens_full", async move {
807 let uri = ¶ms.text_document.uri;
808 let doc = match self.get_doc(uri) {
809 Some(d) => d,
810 None => {
811 return Ok(Some(SemanticTokensResult::Tokens(SemanticTokens {
812 result_id: None,
813 data: vec![],
814 })));
815 }
816 };
817 let tokens = semantic_tokens(doc.source(), &doc);
818 let result_id = token_hash(&tokens);
819 let tokens_arc = Arc::new(tokens);
820 self.docs
821 .store_token_cache(uri, result_id.clone(), Arc::clone(&tokens_arc));
822 let data = Arc::try_unwrap(tokens_arc).unwrap_or_else(|arc| (*arc).clone());
823 Ok(Some(SemanticTokensResult::Tokens(SemanticTokens {
824 result_id: Some(result_id),
825 data,
826 })))
827 })
828 .await
829 }
830
831 async fn semantic_tokens_range(
832 &self,
833 params: SemanticTokensRangeParams,
834 ) -> Result<Option<SemanticTokensRangeResult>> {
835 guard_async_result("semantic_tokens_range", async move {
836 let uri = ¶ms.text_document.uri;
837 let doc = match self.get_doc(uri) {
838 Some(d) => d,
839 None => {
840 return Ok(Some(SemanticTokensRangeResult::Tokens(SemanticTokens {
841 result_id: None,
842 data: vec![],
843 })));
844 }
845 };
846 let tokens = semantic_tokens_range(doc.source(), &doc, params.range);
847 Ok(Some(SemanticTokensRangeResult::Tokens(SemanticTokens {
848 result_id: None,
849 data: tokens,
850 })))
851 })
852 .await
853 }
854
855 async fn semantic_tokens_full_delta(
856 &self,
857 params: SemanticTokensDeltaParams,
858 ) -> Result<Option<SemanticTokensFullDeltaResult>> {
859 guard_async_result("semantic_tokens_full_delta", async move {
860 let uri = ¶ms.text_document.uri;
861 let doc = match self.get_doc(uri) {
862 Some(d) => d,
863 None => return Ok(None),
864 };
865
866 let new_tokens = Arc::new(semantic_tokens(doc.source(), &doc));
867 let new_result_id = token_hash(&new_tokens);
868 let prev_id = ¶ms.previous_result_id;
869
870 let result = match self.docs.get_token_cache(uri, prev_id) {
871 Some(old_tokens) => {
872 let edits = compute_token_delta(&old_tokens, &new_tokens);
873 SemanticTokensFullDeltaResult::TokensDelta(SemanticTokensDelta {
874 result_id: Some(new_result_id.clone()),
875 edits,
876 })
877 }
878 None => SemanticTokensFullDeltaResult::Tokens(SemanticTokens {
880 result_id: Some(new_result_id.clone()),
881 data: (*new_tokens).clone(),
882 }),
883 };
884
885 self.docs.store_token_cache(uri, new_result_id, new_tokens);
886 Ok(Some(result))
887 })
888 .await
889 }
890
891 async fn selection_range(
892 &self,
893 params: SelectionRangeParams,
894 ) -> Result<Option<Vec<SelectionRange>>> {
895 guard_async_result("selection_range", async move {
896 let uri = ¶ms.text_document.uri;
897 let doc = match self.get_doc(uri) {
898 Some(d) => d,
899 None => return Ok(None),
900 };
901 let ranges = selection_ranges(&doc, ¶ms.positions);
902 Ok(if ranges.is_empty() {
903 None
904 } else {
905 Some(ranges)
906 })
907 })
908 .await
909 }
910
911 async fn prepare_call_hierarchy(
912 &self,
913 params: CallHierarchyPrepareParams,
914 ) -> Result<Option<Vec<CallHierarchyItem>>> {
915 guard_async_result("prepare_call_hierarchy", async move {
916 let uri = ¶ms.text_document_position_params.text_document.uri;
917 let position = params.text_document_position_params.position;
918 let source = self.get_open_text(uri).unwrap_or_default();
919 let word = match word_at_position(&source, position) {
920 Some(w) => w,
921 None => return Ok(None),
922 };
923 let wi = self.workspace_index_async().await;
926 let docs = Arc::clone(&self.docs);
927 let get_doc = move |u: &Url| docs.get_doc_salsa(u);
928 Ok(prepare_call_hierarchy_indexed(&word, &wi, &get_doc).map(|item| vec![item]))
929 })
930 .await
931 }
932
933 async fn incoming_calls(
934 &self,
935 params: CallHierarchyIncomingCallsParams,
936 ) -> Result<Option<Vec<CallHierarchyIncomingCall>>> {
937 guard_async_result("incoming_calls", async move {
938 let docs = Arc::clone(&self.docs);
941 let item = params.item;
942 let calls = tokio::task::spawn_blocking(move || {
943 let all_docs = docs.all_docs_for_scan();
944 incoming_calls(&item, &all_docs)
945 })
946 .await
947 .unwrap_or_default();
948 Ok(if calls.is_empty() { None } else { Some(calls) })
949 })
950 .await
951 }
952
953 async fn outgoing_calls(
954 &self,
955 params: CallHierarchyOutgoingCallsParams,
956 ) -> Result<Option<Vec<CallHierarchyOutgoingCall>>> {
957 guard_async_result("outgoing_calls", async move {
958 let wi = self.workspace_index_async().await;
961 let docs = Arc::clone(&self.docs);
962 let item = params.item;
963 let calls = tokio::task::spawn_blocking(move || {
964 let get_doc = |u: &Url| docs.get_doc_salsa(u);
965 outgoing_calls_indexed(&item, &wi, &get_doc)
966 })
967 .await
968 .unwrap_or_default();
969 Ok(if calls.is_empty() { None } else { Some(calls) })
970 })
971 .await
972 }
973
974 async fn document_highlight(
975 &self,
976 params: DocumentHighlightParams,
977 ) -> Result<Option<Vec<DocumentHighlight>>> {
978 guard_async_result("document_highlight", async move {
979 let uri = ¶ms.text_document_position_params.text_document.uri;
980 let position = params.text_document_position_params.position;
981 let source = self.get_open_text(uri).unwrap_or_default();
982 let doc = match self.get_doc(uri) {
983 Some(d) => d,
984 None => return Ok(None),
985 };
986 let highlights = document_highlights(&source, &doc, position);
987 Ok(if highlights.is_empty() {
988 None
989 } else {
990 Some(highlights)
991 })
992 })
993 .await
994 }
995
996 async fn linked_editing_range(
997 &self,
998 params: LinkedEditingRangeParams,
999 ) -> Result<Option<LinkedEditingRanges>> {
1000 guard_async_result("linked_editing_range", async move {
1001 self.handle_linked_editing_range(params).await
1002 })
1003 .await
1004 }
1005
1006 async fn goto_implementation(
1007 &self,
1008 params: tower_lsp::lsp_types::request::GotoImplementationParams,
1009 ) -> Result<Option<tower_lsp::lsp_types::request::GotoImplementationResponse>> {
1010 guard_async_result("goto_implementation", async move {
1011 let uri = ¶ms.text_document_position_params.text_document.uri;
1012 let position = params.text_document_position_params.position;
1013 let source = self.get_open_text(uri).unwrap_or_default();
1014 let imports = self.file_imports(uri);
1015 let raw_word = crate::text::word_at_position(&source, position).unwrap_or_default();
1016 let (word, fqn_owned): (String, Option<String>) = if raw_word.contains('\\') {
1021 let short = raw_word
1022 .rsplit('\\')
1023 .next()
1024 .unwrap_or(&raw_word)
1025 .to_string();
1026 let full = raw_word.trim_start_matches('\\').to_string();
1027 (short, Some(full))
1028 } else {
1029 let fqn = imports.get(&raw_word).cloned();
1030 (raw_word, fqn)
1031 };
1032 let fqn = fqn_owned.as_deref();
1033 let open_docs = self.docs.docs_for(&self.open_urls());
1035 let mut locs = find_implementations(&word, fqn, &open_docs);
1036 let wi = self.workspace_index_async().await;
1039 if locs.is_empty() {
1040 let fqn_for_mir = fqn.unwrap_or(&word).to_string();
1041 let subtype_urls = self.docs.class_subtype_urls(&fqn_for_mir);
1042 locs = find_implementations_mir_backed(&word, fqn, &wi, &subtype_urls);
1043 }
1044 if locs.is_empty()
1048 && let Some(doc) = self.get_doc(uri)
1049 && let Some(enclosing) =
1050 crate::types::type_map::enclosing_class_at(&source, &doc, position)
1051 {
1052 let enclosing_fqn = wi
1054 .classes_by_name
1055 .get(&enclosing)
1056 .and_then(|refs| refs.first())
1057 .and_then(|r| wi.at(*r))
1058 .map(|(_, cls)| cls.fqn.as_ref().to_string());
1059 let method_subtype_urls = enclosing_fqn
1060 .as_deref()
1061 .map(|f| self.docs.class_subtype_urls(f))
1062 .unwrap_or_default();
1063 locs = find_method_implementations_mir_backed(
1064 &word,
1065 &enclosing,
1066 &wi,
1067 &method_subtype_urls,
1068 );
1069 }
1070 if locs.is_empty() {
1071 Ok(None)
1072 } else {
1073 Ok(Some(GotoDefinitionResponse::Array(locs)))
1074 }
1075 })
1076 .await
1077 }
1078
1079 async fn goto_declaration(
1080 &self,
1081 params: tower_lsp::lsp_types::request::GotoDeclarationParams,
1082 ) -> Result<Option<tower_lsp::lsp_types::request::GotoDeclarationResponse>> {
1083 guard_async_result("goto_declaration", async move {
1084 let uri = ¶ms.text_document_position_params.text_document.uri;
1085 let position = params.text_document_position_params.position;
1086 let source = self.get_open_text(uri).unwrap_or_default();
1087 let open_docs = self.docs.docs_for(&self.open_urls());
1089 if let Some(loc) = goto_declaration(&source, &open_docs, position) {
1090 return Ok(Some(GotoDefinitionResponse::Scalar(loc)));
1091 }
1092 let all_indexes = self.docs.all_indexes();
1094 Ok(goto_declaration_from_index(&source, &all_indexes, position)
1095 .map(GotoDefinitionResponse::Scalar))
1096 })
1097 .await
1098 }
1099
1100 async fn goto_type_definition(
1101 &self,
1102 params: tower_lsp::lsp_types::request::GotoTypeDefinitionParams,
1103 ) -> Result<Option<tower_lsp::lsp_types::request::GotoTypeDefinitionResponse>> {
1104 guard_async_result("goto_type_definition", async move {
1105 let uri = ¶ms.text_document_position_params.text_document.uri;
1106 let position = params.text_document_position_params.position;
1107 let source = self.get_open_text(uri).unwrap_or_default();
1108 let doc = match self.get_doc(uri) {
1109 Some(d) => d,
1110 None => return Ok(None),
1111 };
1112 let analysis = self.cached_analysis_async(uri).await;
1113 let open_docs = self.docs.docs_for(&self.open_urls());
1115 let mut results =
1116 goto_type_definition(&source, &doc, analysis.as_deref(), &open_docs, position);
1117
1118 if results.is_empty() {
1120 let all_indexes = self.docs.all_indexes();
1121 results = goto_type_definition_from_index(
1122 &source,
1123 &doc,
1124 analysis.as_deref(),
1125 &all_indexes,
1126 position,
1127 );
1128 }
1129
1130 let response = match results.len() {
1132 0 => None,
1133 1 => Some(GotoDefinitionResponse::Scalar(
1134 results.into_iter().next().unwrap(),
1135 )),
1136 _ => Some(GotoDefinitionResponse::Array(results)),
1137 };
1138 Ok(response)
1139 })
1140 .await
1141 }
1142
1143 async fn prepare_type_hierarchy(
1144 &self,
1145 params: TypeHierarchyPrepareParams,
1146 ) -> Result<Option<Vec<TypeHierarchyItem>>> {
1147 guard_async_result("prepare_type_hierarchy", async move {
1148 let uri = ¶ms.text_document_position_params.text_document.uri;
1149 let position = params.text_document_position_params.position;
1150 let source = self.get_open_text(uri).unwrap_or_default();
1151 let wi = self.workspace_index_async().await;
1153 Ok(
1154 prepare_type_hierarchy_from_workspace(&source, &wi, position)
1155 .map(|item| vec![item]),
1156 )
1157 })
1158 .await
1159 }
1160
1161 async fn supertypes(
1162 &self,
1163 params: TypeHierarchySupertypesParams,
1164 ) -> Result<Option<Vec<TypeHierarchyItem>>> {
1165 guard_async_result("supertypes", async move {
1166 let wi = self.workspace_index_async().await;
1170 let loaded_new = self
1171 .ensure_direct_supertypes_loaded(¶ms.item.name, &wi)
1172 .await;
1173 let wi = if loaded_new {
1174 self.workspace_index_async().await
1175 } else {
1176 wi
1177 };
1178 let result = supertypes_of_from_workspace(¶ms.item, &wi);
1179 Ok(if result.is_empty() {
1180 None
1181 } else {
1182 Some(result)
1183 })
1184 })
1185 .await
1186 }
1187
1188 async fn subtypes(
1189 &self,
1190 params: TypeHierarchySubtypesParams,
1191 ) -> Result<Option<Vec<TypeHierarchyItem>>> {
1192 guard_async_result("subtypes", async move {
1193 let wi = self.workspace_index_async().await;
1194 let item_fqn = wi
1196 .classes_by_name
1197 .get(¶ms.item.name)
1198 .and_then(|refs| refs.first())
1199 .and_then(|r| wi.at(*r))
1200 .map(|(_, cls)| cls.fqn.as_ref().to_string());
1201 let subtype_urls = item_fqn
1202 .as_deref()
1203 .map(|f| self.docs.class_subtype_urls(f))
1204 .unwrap_or_default();
1205 let result =
1206 subtypes_of_mir_backed(¶ms.item, item_fqn.as_deref(), &wi, &subtype_urls);
1207 Ok(if result.is_empty() {
1208 None
1209 } else {
1210 Some(result)
1211 })
1212 })
1213 .await
1214 }
1215
1216 async fn code_lens(&self, params: CodeLensParams) -> Result<Option<Vec<CodeLens>>> {
1217 guard_async_result("code_lens", async move {
1218 let uri = ¶ms.text_document.uri;
1219 let doc = match self.get_doc(uri) {
1220 Some(d) => d,
1221 None => return Ok(None),
1222 };
1223 let docs = Arc::clone(&self.docs);
1226 let uri_owned = uri.clone();
1227 let lenses = tokio::task::spawn_blocking(move || {
1228 let all_docs = docs.all_docs_for_scan();
1229 code_lenses(&uri_owned, &doc, &all_docs)
1230 })
1231 .await
1232 .unwrap_or_default();
1233 Ok(if lenses.is_empty() {
1234 None
1235 } else {
1236 Some(lenses)
1237 })
1238 })
1239 .await
1240 }
1241
1242 async fn code_lens_resolve(&self, params: CodeLens) -> Result<CodeLens> {
1243 let fallback = params.clone();
1244 let resolved = guard_async("code_lens_resolve", async move {
1245 Some(params)
1247 })
1248 .await;
1249 Ok(resolved.unwrap_or(fallback))
1250 }
1251
1252 async fn document_link(&self, params: DocumentLinkParams) -> Result<Option<Vec<DocumentLink>>> {
1253 guard_async_result("document_link", async move {
1254 let uri = ¶ms.text_document.uri;
1255 let doc = match self.get_doc(uri) {
1256 Some(d) => d,
1257 None => return Ok(None),
1258 };
1259 let links = document_links(uri, &doc, doc.source());
1260 Ok(if links.is_empty() { None } else { Some(links) })
1261 })
1262 .await
1263 }
1264
1265 async fn document_link_resolve(&self, params: DocumentLink) -> Result<DocumentLink> {
1266 let fallback = params.clone();
1267 let resolved = guard_async("document_link_resolve", async move {
1268 Some(params)
1270 })
1271 .await;
1272 Ok(resolved.unwrap_or(fallback))
1273 }
1274
1275 async fn formatting(&self, params: DocumentFormattingParams) -> Result<Option<Vec<TextEdit>>> {
1276 guard_async_result("formatting", async move {
1277 let uri = ¶ms.text_document.uri;
1278 let source = self.get_open_text(uri).unwrap_or_default();
1279 Ok(format_document(&source))
1280 })
1281 .await
1282 }
1283
1284 async fn range_formatting(
1285 &self,
1286 params: DocumentRangeFormattingParams,
1287 ) -> Result<Option<Vec<TextEdit>>> {
1288 guard_async_result("range_formatting", async move {
1289 let uri = ¶ms.text_document.uri;
1290 let source = self.get_open_text(uri).unwrap_or_default();
1291 Ok(format_range(&source, params.range))
1292 })
1293 .await
1294 }
1295
1296 async fn on_type_formatting(
1297 &self,
1298 params: DocumentOnTypeFormattingParams,
1299 ) -> Result<Option<Vec<TextEdit>>> {
1300 guard_async_result("on_type_formatting", async move {
1301 let uri = ¶ms.text_document_position.text_document.uri;
1302 let source = self.get_open_text(uri).unwrap_or_default();
1303 let edits = on_type_format(
1304 &source,
1305 params.text_document_position.position,
1306 ¶ms.ch,
1307 ¶ms.options,
1308 );
1309 Ok(if edits.is_empty() { None } else { Some(edits) })
1310 })
1311 .await
1312 }
1313
1314 async fn execute_command(
1315 &self,
1316 params: ExecuteCommandParams,
1317 ) -> Result<Option<serde_json::Value>> {
1318 guard_async_result("execute_command", async move {
1319 match params.command.as_str() {
1320 "php-lsp.runTest" => {
1321 let file_uri = params
1323 .arguments
1324 .first()
1325 .and_then(|v| v.as_str())
1326 .and_then(|s| Url::parse(s).ok());
1327 let filter = params
1328 .arguments
1329 .get(1)
1330 .and_then(|v| v.as_str())
1331 .unwrap_or("")
1332 .to_string();
1333
1334 let root = self.root_paths.load().first().cloned();
1335 let client = self.client.clone();
1336
1337 tokio::spawn(async move {
1338 run_phpunit(&client, &filter, root.as_deref(), file_uri.as_ref()).await;
1339 });
1340
1341 Ok(None)
1342 }
1343 _ => Ok(None),
1344 }
1345 })
1346 .await
1347 }
1348
1349 async fn will_rename_files(&self, params: RenameFilesParams) -> Result<Option<WorkspaceEdit>> {
1350 guard_async_result("will_rename_files", async move {
1351 self.handle_will_rename_files(params).await
1352 })
1353 .await
1354 }
1355
1356 async fn did_rename_files(&self, params: RenameFilesParams) {
1357 guard_async("did_rename_files", async move {
1358 self.handle_did_rename_files(params).await
1359 })
1360 .await
1361 }
1362
1363 async fn will_create_files(&self, params: CreateFilesParams) -> Result<Option<WorkspaceEdit>> {
1364 guard_async_result("will_create_files", async move {
1365 self.handle_will_create_files(params).await
1366 })
1367 .await
1368 }
1369
1370 async fn did_create_files(&self, params: CreateFilesParams) {
1371 guard_async("did_create_files", async move {
1372 self.handle_did_create_files(params).await
1373 })
1374 .await
1375 }
1376
1377 async fn will_delete_files(&self, params: DeleteFilesParams) -> Result<Option<WorkspaceEdit>> {
1380 guard_async_result("will_delete_files", async move {
1381 self.handle_will_delete_files(params).await
1382 })
1383 .await
1384 }
1385
1386 async fn did_delete_files(&self, params: DeleteFilesParams) {
1387 guard_async("did_delete_files", async move {
1388 self.handle_did_delete_files(params).await
1389 })
1390 .await
1391 }
1392
1393 async fn moniker(&self, params: MonikerParams) -> Result<Option<Vec<Moniker>>> {
1394 guard_async_result("moniker", async move {
1395 let uri = ¶ms.text_document_position_params.text_document.uri;
1396 let position = params.text_document_position_params.position;
1397 let source = self.get_open_text(uri).unwrap_or_default();
1398 let doc = match self.get_doc(uri) {
1399 Some(d) => d,
1400 None => return Ok(None),
1401 };
1402 let imports = self.file_imports(uri);
1403 Ok(moniker_at(&source, &doc, position, &imports).map(|m| vec![m]))
1404 })
1405 .await
1406 }
1407
1408 async fn inline_value(&self, params: InlineValueParams) -> Result<Option<Vec<InlineValue>>> {
1409 guard_async_result("inline_value", async move {
1410 let uri = ¶ms.text_document.uri;
1411 let source = self.get_open_text(uri).unwrap_or_default();
1412 let values = inline_values_in_range(&source, params.range);
1413 Ok(if values.is_empty() {
1414 None
1415 } else {
1416 Some(values)
1417 })
1418 })
1419 .await
1420 }
1421
1422 async fn diagnostic(
1423 &self,
1424 params: DocumentDiagnosticParams,
1425 ) -> Result<DocumentDiagnosticReportResult> {
1426 let report = guard_async("diagnostic", async move {
1427 Some(self.handle_diagnostic(params).await)
1428 })
1429 .await;
1430 match report {
1431 Some(r) => r,
1432 None => Ok(DocumentDiagnosticReportResult::Report(
1433 DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport::default()),
1434 )),
1435 }
1436 }
1437
1438 async fn workspace_diagnostic(
1439 &self,
1440 params: WorkspaceDiagnosticParams,
1441 ) -> Result<WorkspaceDiagnosticReportResult> {
1442 let report = guard_async("workspace_diagnostic", async move {
1443 Some(self.handle_workspace_diagnostic(params).await)
1444 })
1445 .await;
1446 match report {
1447 Some(r) => r,
1448 None => Ok(WorkspaceDiagnosticReportResult::Report(
1449 WorkspaceDiagnosticReport::default(),
1450 )),
1451 }
1452 }
1453
1454 async fn code_action(&self, params: CodeActionParams) -> Result<Option<CodeActionResponse>> {
1455 guard_async_result("code_action", async move {
1456 self.handle_code_action(params).await
1457 })
1458 .await
1459 }
1460
1461 async fn code_action_resolve(&self, item: CodeAction) -> Result<CodeAction> {
1462 guard_async_result("code_action_resolve", async move {
1463 self.handle_code_action_resolve(item).await
1464 })
1465 .await
1466 }
1467}